346 Commits
Author SHA1 Message Date
Valiantsin DzerakhandClaude Opus 5.5 7a37fc3c34 Up version 0.4.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 19:25:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 bb8b4b1797 Turn the recorder thread on by default on AMD GPUs too; vk_recorder = 0 turns it off where the device is lost
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 19:25:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 396c4ae621 Describe the profile-guided build in docs/pgo.md: where and how to train, free play, merge, measure
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 19:07:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 27a63dfb0f Merge pull request #1065: Xbox prompts at every game resolution the setup lays the UI out for (cdonatom, #378)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:59:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6f6704640a Merge pull request #1066: describe deswizzle, kspin_us, pcore, rec_async and rec_spin_us in docs/settings.md (itaskaev2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:55:58 +03:00
Valiantsin DzerakhandClaude Opus 5.5 574c1ecc49 Merge task/climb: Link climbs walls and ladders at full speed above 60 fps (#35, #276, #1001)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:46:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 06811330e7 Extend the climb controller's frame-rate factor above 60 fps, so Link climbs walls and ladders at full speed (#35, #276, #1001)
The climb velocity step (1.4.3 main+0x1f49754, 1.2.1 main+0x1918220) multiplies the requested velocity by Nintendo's
factor for a movement loss that grows with the frame rate. 1.4.3 fits it at 30 and 60 fps and caps it at 2 from about
77 fps (63% climb speed at 120, 34% at 240); 1.2.1 has no 60 fps branch (78% at 60). Below a 1/60 s step the set climb
continues the factor with a curve fitted to a replayed ladder climb, taking the patch page's frame ratio without the
time scale, so 30 and 60 fps keep the shipping factor bit for bit, slow motion included. Only the player's climb
modules reach this code. Its own setting, climb_fix under [frame rate], with a menu toggle; off without the real-time
step. Ladder replay, 1.4.3: 1.665 / 1.663 / 1.669 / 1.681 m/s at 30 / 60 / 120 / 240 (shipping 120 1.046, 240 0.563).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:44:18 +03:00
itaskaev2andClaude Opus 5.5 e8fd6fd7a5 Describe deswizzle, kspin_us, pcore, rec_async and rec_spin_us in docs/settings.md
tests/settings_doc_check.py failed on dev with these five names in known[] and described nowhere. The rows follow the
code: vk_deswizzle.c (TOTK_DESWIZZLE), kernel.c kspin_ns, main.c (rt_knob PCORE), vk_rec.c rec_submit and rec_main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 16:28:21 +01:00
Valiantsin DzerakhandClaude Opus 5.5 be7c27047a Keep the 4096-a-side scene of an output near 8K under the new heap bound
The heap bound in scene_for counts the output's own growth against the 2 GiB limit, so a 7680x4320 output at
render_scale game went from the 4096x2304 scene dev renders to 2712x1526, with no setting changed. The bound now
shrinks no scene below 4096 a side, the size dev gave; scenes the 4096 cap kept smaller still grow up to the bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:20:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1c1ff975f3 Default dynamic_res to 1 so the game keeps its own dynamic resolution
With dynamic_res at 0 by default the fixres set held the scene at 100 % for every player, the reason #573 was turned
down. The default is now 1, so the set stays off unless a player turns dynamic resolution off in the menu or
totk.ini, and the Steam Deck profile no longer needs its own entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:20:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b6738cf41b Merge pull request #916: dynamic resolution as a setting, on by default, and a scene past 4096 wide (#573 by ProSep, carried by itaskaev2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:20:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dfc0b57457 Keep the per-frame pass table's comment to what the code does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:59:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 173462f33d Merge pull request #1040: keep the targets a frame renders in the per-frame pass table, so draws with a pipeline still building skip a frame instead of waiting 50 to 400 ms (ipavlin98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:59:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3e037f0044 Keep the shadow log of players without sync_ahead as it was, sort the new setting names, and fix the sync_ahead frame-time sentence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:55:57 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b2db7809b4 Merge pull request #917: let the game run one frame further ahead of the GPU, sync_ahead, off by default (#676 by ProSep, carried by itaskaev2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:55:23 +03:00
Carlos DonatoandClaude Opus 5.5 fb34177cfd Make the Xbox prompts for every game resolution the setup lays the UI out for (#378)
The setup wrote romfs-patches-xbox and romfs-patches-xbox-label for 16:9 only, so prompts = xbox on a 21:9, 32:9,
16:10 or 3:2 screen (the Steam Deck's 1280x800 among them) kept the game resolution at 1920x1080 (cfg.c) or, with
output_res set to the screen's shape, fell back to the Switch prompts. #979 fixed this for prompts = pc; this extends
the same mechanism to both Xbox folders, the shared-files option of #378 by way of #979's --reuse.

- The setup builds romfs-patches-xbox-<w>x<h> and romfs-patches-xbox-label-<w>x<h> for each of ASPECT_SIZES, from
  their 16:9 folder with --reuse, as it does for romfs-patches-pc-<w>x<h>.
- romfs.c overlay_init: at a game resolution of another shape any prompts' folder loads from <folder>-<w>x<h> in place
  of romfs-patches-<w>x<h>, not only the PC one; without it the Switch prompts stay, and the log names the missing
  folder and the command that makes it. romfs_shaped_pc_ready becomes romfs_shaped_prompts_ready(folder, w, h).
- cfg.c: the display-shaped output_res default applies with prompts = xbox when the folder of the pad layout
  (romfs-patches-xbox with the Nintendo layout, romfs-patches-xbox-label with any other) is there for that size.
- The menu's note for a missing shaped folder reads "off at this game resolution (no layout made for it)" in every
  menu language, since 16:9 is no longer the only shape with prompts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:53:49 +01:00
Valiantsin DzerakhandClaude Opus 5.5 8da13aa9b4 Merge pull request #1006: stop the installer space check at an unavailable drive root (Luxury213)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:43:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 719196f183 Merge pull request #1061: let a validation run with DLSS finish, a valid OpSelect in the motion fragment shader and set 1 bound first for the motion draws (itaskaev2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:43:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 fce6364236 Merge pull request #1060: take the feedback-layout images back to GENERAL before the contact shadows' march (itaskaev2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 17:43:40 +03:00
itaskaev2andClaude Opus 5.5 0adcc96bae Bind the motion draws' set 1 before pushing set 0 under the validation layer, as the replay's draws do
vk_mvobj_execute's draws (mo_emit) push set 0 and then bind set 1. Under the validation layer (SDK 1.4.363) a push for
a new INDEPENDENT_SETS layout keeps the push set the layer made for an earlier layout (the replay's last draw) and drops
the writes to bindings that layout lacks: every run with DLSS and validation reported the last-frame banks as missing
(VUID-vkCmdDrawIndexed-None-08114, variable vp_c20) and then faulted inside VkLayer_khronos_validation.dll. The
replay's own draws avoid this by binding set 1 first (set1_first in bind_descriptors); the motion draws now do the
same, only while the layer is active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:11:08 +01:00
itaskaev2andClaude Opus 5.5 f804facd8b Take the feedback-layout images back to GENERAL before the contact shadows' march
vk_contact_exec ended the pass and recorded a memory barrier only. Once a G-buffer draw samples the linear depth or the
scene colour in its own pass, those images stay in ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT across passes
(vk_exec_draw.c, feedback loops), and the march then bound them in GENERAL as a sampled and a storage image. The Vulkan
validation layer reported it on every frame with contact shadows on (vkCmdDispatch imageLayout-00344, followed by
barrier, BeginRendering and QueueSubmit layout errors: 40 per run at Lookout Landing, 0 with contact_shadows = 0).
vk_exec_barrier ends the pass and moves those images back to GENERAL in the same full barrier, as every other
barrier does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:59:34 +01:00
itaskaev2andClaude Opus 5.5 708e0cfe8d Give the per-object motion fragment shader's OpSelect a condition vector, as SPIR-V 1.0 needs
tools/make_mvobj_fs.py assembled OpSelect of a vec4 with a scalar bool condition, which only SPIR-V 1.4 and later
allow. The module is 1.0, so spirv-val in the Vulkan validation layer rejected it (Expected vector sizes of Result Type
and the condition to be equal: Select). The condition is now splatted into a bvec4 first; runtime/vk_mvobj_fs.h is
regenerated and passes spirv-val.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:59:31 +01:00
Valiantsin DzerakhandClaude Opus 5.5 7f82a52da8 Merge task/gears: gear doors open above 30 fps on 1.2.1 (#330, #984)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:36:35 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6433550e3d Scale the 1.2.1 gear rotation threshold by the AI update's frame delta, as 1.4.3 does (#330, #984)
ActorCheckRotateModule's update compared the angle a gear turned since the last update with one 30 Hz frame's worth,
so above 30 fps gears and water wheels read as stopped and the Riogok, Turakamik and Wind Temple doors stayed shut.
The new set gears keeps the update context's +8 (frames elapsed in 30 Hz units, the value 1.4.3 divides by) and
multiplies the threshold by it. Measured: the context is 0.25 at 120 fps with the real-time step and 0.5 at 60 fps
without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:35:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1503b17f7d Merge task/crash705: list the compound physics shape's children in the crash report (#705)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:20:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4c4bd8c67a List the compound physics shape's children in the crash report for the 1.2.1 fault after the material lookup (#705)
On 1.2.1 the game asks a compound physics shape for its first child's material at main+0xe056d0 and reads the result
without a check. At the reported crash the shape had none of its 125 children left, although every child is filled
when the shape is built. The crash handler now prints that shape's live bits and each record's shape pointer and name,
which tells removed children from switched-off ones and names the object. It runs only after the crash.
--probe-kernel builds a fake shape in a crashing child and checks the listing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:19:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3d386fe37e Merge task/inteluhd: dynamic alpha to coverage and vertex input off by default on Intel's Windows driver for integrated GPUs (#970, #992)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 09:13:52 +03:00
Valiantsin DzerakhandClaude Opus 5.5 905e2e720d Turn dynamic alpha to coverage and dynamic vertex input off by default on Intel's Windows driver for integrated GPUs (#970, #992)
UHD Graphics (i3-1215U, i3-1305U) and Iris Xe drew coloured noise on foliage with dynamic alpha to coverage and black
patches over the scenery with dynamic vertex input; players confirmed that vk_dyn_a2c = 0 and vk_dyn_vertex = 0 clear
both. vk_caps.intel_igpu_win marks Intel's proprietary Windows driver on an integrated GPU, and both defaults follow it.
An explicit ini value still wins. Arc discrete GPUs and Mesa's ANV keep both on: no report there, and the ANV report
(#989) had both off with its glitch still present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 09:01:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 729b00a1c0 Merge task/sdl3420: ship SDL 3.4.20 in the release for pads without motion calibration (#912)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 08:58:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2fe1ea1716 Bump the bundled Windows SDL to 3.4.20 (#912)
SDL 3.4.20 uses default Switch controller stick and IMU calibration when the
pad has none, which fixes the infinite gyro readings on the Beitong Asura 2 Pro+.
The pins in tools/make_release.py (version, zip and devel sha256, SDL3.dll sha256)
and the Windows rows of THIRD_PARTY_NOTICES.md follow. The sniper sysroot row stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 08:53:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 32f86a3296 Merge task/cheatc0: run cheat opcodes C0-C3 in the data cheat VM (#973)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 08:07:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 14b1b1aece Run cheat opcodes C0-C3 in the data cheat VM (#973)
The VM refused C0 (register conditional), so cheats like C046B400 FFFFFFFF were greyed out as unsupported. C0 now
runs with dmnt's operand types 0-5, widths and conditions; C1 and C2 save, restore and clear registers, C3 moves
registers to and from the static registers. tools/cheats.py gives the same opcode lengths and accepts the same opcodes.

skip() took the end of a nested block at depth 1 for the else of the failed outer block; it now counts depth the way
dmnt does, and 2x000000 is an else only for x = 1.

tests/cheats_vm_check.py runs tools/cheats.py and runtime/cheats.c on cheats of its own, the two from the issue
included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 08:05:01 +03:00
ipavlin98 a4b46fa7a9 Keep the targets a frame renders in the table that tells per-frame passes from one-shot ones
The table of the frames each target was last rendered into was direct-mapped with 1024 entries. In a 1.4.3 field run about 16 entries rendered the frame before were taken by another subresource each frame, the scene's colour target among them, so every draw there whose pipeline was still building waited for it as one-shot, 50 to 400 ms, instead of skipping a frame. Sets of 8 ways that evict the way rendered longest ago lost none in the same run.
2026-11-09 21:22:38 +03:00
Valiantsin Dzerakh 8aba3647e0 Up version 0.3.14 2026-11-09 19:57:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0238f3069a Merge task/pgostale: warn when a PGO profile was trained with other runtime headers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 18:50:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2d2c6782af Record the runtime headers a PGO profile was trained with and warn when they change
The floating-point helpers in simd.h and simd_fp.h inline into every lifted function before llvm-profdata matches
the profile, so the fpcheap change left pgo-v143 with a fifth of its weight while recomp/ stayed the same and
make_release saw nothing. pgo.py merge now reads the instrumented build's Ninja deps log and stores the digests of
the runtime/ headers the generated objects include (cpu.h, simd.h, simd_fp.h); make_release warns when one differs
or a record has none. docs/building.md says to retrain after a change to those headers, and build_game.py names the
libiconv-2.dll that MSYS2's SDL3.dll needs in deps-mingw/bin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 18:50:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 bdcfb42c07 Add contributor and agent guides for pull requests, running and debugging
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:44:59 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f9ed079614 Merge task/handheldscene: render the handheld scene at the asked size on 1.4.3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:38:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 bed6962e09 Render the handheld scene at the asked size on 1.4.3
1.4.3 scales the scene size it reads from the patch page by canvas width over the console mode's base width, which is
1280 in handheld, not 1920. A handheld 1280x720 output at render_scale 100 put 1920x1080 on the page, so the game
rendered a 1920x1080 scene while the DynamicTextureAllocator heap was sized for 1280x720, and heavy scenes ran it out
(crash at main+0x1c2590). scene_scale now uses the mode's base for res_init and res_dlss_mode. In handheld the game
composes a 1280x720 canvas unless the output patch is on, so the output patch is switched on with the scene patch there
and the canvas is the output the factor and the heap assume. Docked is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:38:03 +03:00
itaskaev2 59eb026bd7 Merge branch 'dev' into carry/dynamic-res-100 2026-11-09 09:55:24 +01:00
itaskaev2 2e5d59df83 Merge remote-tracking branch 'origin/dev' into carry/sync-ahead
# Conflicts:
#	docs/architecture.md
#	runtime/cfg.c
#	runtime/vk_sync.c
2026-11-09 09:54:45 +01:00
Valiantsin DzerakhandClaude Opus 5.5 34b39ceca0 Merge task/memcommit: take 64 MiB blocks from the driver for buffers and other images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 11:08:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 29cb79086f Take 64 MiB blocks from the driver for buffers and other images
Since the textures moved to a pool of their own, the default device-local
blocks hold mirrors and other buffers. With VMA's 256 MiB blocks one block
held 53 MiB of them after 10 minutes in the field, and the headroom thread
kept half a block of each device-local type ahead. TOTK_VMA_BLOCK_MIB
(default 64, 0 = VMA's 256) sets the size; the headroom spare follows it.

On the RX 9070, 10 minutes in the field: VMA blocks 3582 -> 3252 MiB,
process commit 8614 -> 8327 MB, slow frames 13 (2.8 s) -> 9 (1.1 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:51:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a78268935a Merge task/memcommit: wait out a short out-of-memory spell instead of ending the game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:15:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 cf44e9013c Merge task/memoom: wait out a short out-of-memory spell instead of ending the game
Conflicts: the image retry goes through vk_texture_image, sweep_pressure keeps the released-texture cap on the new
drop helpers, and vk_headroom_release also frees the texture pool's anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:12:55 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4ea472e864 Merge task/memcommit: textures in a pool of their own with 64 MiB blocks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:11:51 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8e05d2b409 Give textures a pool of their own with 64 MiB blocks
Textures shared the default 256 MiB blocks with the mirrors and other buffers, and a block went back to the driver
only once everything in it was gone. On the RX 9070 after a few minutes of roaming the field, 3 of 9 device-local
blocks were 13-27% full, held by a hundred small textures and buffers each, and blocks held 0.8-1.4 GB more than their
allocations; where device memory takes process commit, that commit backs nothing.

Own-memory textures now go to a VMA pool with 64 MiB blocks (TOTK_TEX_BLOCK_MIB, 0 = the default pools). Images of a
quarter block or more, images the driver wants a dedicated allocation for, and images the pool's memory type does not
take get memory of their own; the requirements are read before the image is made, since VMA checks neither for a
custom pool. The headroom thread keeps the pool's next block ahead like the default pools'.

Same 10-minute route: 807 MiB of blocks beyond their allocations instead of 1037, most pool blocks 75-100% full, slow
frames not up. Probes pass on v143.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:11:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a79ff17314 Merge task/memcommit: count device memory against system memory where it takes process commit, BC3 for ASTC on integrated GPUs, cap the released-texture cache by bytes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:58:10 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0005c01cf5 Remove the stray brace in vk_mem_census that stopped vk_memory.c from compiling
79c6951 left the closing brace of a debug block in the per-heap loop, so the function ended early and the build failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:57:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 34b340a208 Cap the released-texture cache by bytes
Released textures stayed until 2048 of them were kept or VRAM passed 90% of the budget. On the RX 9070 a 10-minute
field run kept 818 MiB of them, while almost every take-back came within a second. TOTK_TEX_RELEASED_MIB (default
256) runs the same drops as under VRAM pressure while the released entries hold more own memory than that. On the
same route the run ended with 1.1 GB less process commit and 1 GB less in VMA blocks; 15 dropped textures were
created again and the slow frames did not change.

Also documents TOTK_MEM_RESERVE_MIB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:57:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 213bc6af6a Wait out a short out-of-memory spell instead of ending the game
Crash logs ended in fatal errors when the system's commit ran out for a moment: a staging buffer (-2), a guest commit
with the system at 24442 of 24447 MB, image creation (-2). Windows grows a system-managed page file and some drivers
charge device memory to the process commit, so the same request often succeeds a moment later.

oom.c holds the wait nvn_api_cmd.c realloc_wait already used: it requests a release, retries every 10 ms for up to
30 s, logs the first 8 with the commit numbers. rt_oom_request sets a flag that vk_texture_new_submit takes (one atomic
load per submit): it drops up to 256 released textures of the two kinds sweep_pressure drops (overwritten, clean
mirrored), never GPU-written content, and frees the headroom blocks. vk_oom_release does at once what needs no submit:
headroom blocks, finished upload batches and deferred objects.

- memory.c commit: waits and retries without frz_lock between tries; every caller waits, boot ones too.
- vk_staging and the draw-data chunks (vk_exec_draw.c scratch): release, then wait; the open upload batch is not
  submitted, its caller still records into it.
- create_tex: release and one retry; no wait under the texture lock, the texture is asked for again at its next use.
- the texture write-back buffer: release, then wait.
- the headroom thread keeps no spare block for 30 s after a failure.

TOTK_TEST_OOM=<site>:<n> (commit, staging, image, chunk) fails the first n attempts; probe-kernel forces 3 refused
commits, probe-vkexec 2 staging buffers, 1 image and 1 chunk, and both pass through them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:53:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5e479f593b Remove the stray brace in vk_mem_census that stopped vk_memory.c from compiling
79c6951 left the closing brace of a debug block in the per-heap loop, so the function ended early and the build failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:51:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 79c6951688 Measure whether device-local memory takes process commit instead of assuming it per vendor
c838d96 counted system memory for every discrete GPU but NVIDIA's, an assumption. An RTX 5080 log (#398) showed 10.4 GB
of process commit at frame 1200, about what the RX 9070 holds with its VRAM counted, and the RTX 3060 Ti in #670 ran
out of system commit (17.8 GB) at 4K with its VRAM full. vk_mem_host_probe now allocates 128 MiB of device-local memory
once after device creation and compares the process commit before and after; half the size or more turns the system
memory cap on. The log line carries the measured amount, so user logs show what each driver does.

RX 9070: "a 128 MiB device-local allocation took 128 MiB of process commit".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:39:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a689922180 Decode ASTC to BC3 by default on integrated GPUs
The size rule for TOTK_ASTC_BC looked at the largest device-local heap only. On an integrated GPU that heap is system
RAM reported as VRAM (10.6 GiB on the Ally X, 14.9 GiB on the 880M), so RGBA8 stayed on exactly where memory is
shortest. vk_mem_uma() now turns BC3 on there as well.

7900X iGPU, field save, same exe, frame 1800: RGBA8 commit 7047 MB, GPU 49-53 ms, 16.8-18.4 fps; BC3 commit 6155 MB,
GPU 42.6 ms, 21.6-21.8 fps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:20:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d8090a85fe Keep headroom blocks for device-local memory types only
Headroom woke for every memory type that got new device memory, the staging pool's own 64 MiB blocks included, and
then kept a 256 MiB spare block in that type's default pool. On the RX 9070 the default pools of host memory types 1
and 3 held 4 MiB, beside 512 MiB of spare blocks. Textures and mirrors, whose block allocations headroom moves off the
render thread, live in device-local types.

RX 9070, field save, 60 s: heap 0 VMA blocks 192 MiB instead of 704, driver usage 1384 MiB instead of 1896; 60 fps,
1% low 51.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:05:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c838d9692f Count system memory in the device-local budget where device memory takes host memory
The paths that give memory back under pressure (headroom's spare block, released textures, pool mirrors) watched the
Vulkan heap budget only. On a UMA GPU that budget is gigabytes of the same RAM the game runs in, and on Windows the
AMD driver charges device-local allocations to the process commit one to one (RX 9070: VRAM use and private bytes
grew by the same 1164, 256 and 256 MiB). Crash logs show the result: the process at 4.4-10.5 GB of commit, 16 GB
machines at 80-94% RAM, std::bad_alloc inside amdvlk64.dll (#285), allocation failures on Intel UHD.

vk_mem_host_room (vk_memory.c) is what the system can still give beyond a reserve (TOTK_MEM_RESERVE_MIB, 1536):
commit, and on a UMA GPU physical memory too; Linux overcommits, so there only a UMA GPU's available memory counts.
NVIDIA is left out, not measured. vk_mem_vram caps the budget at use plus that room, and headroom keeps its spare
block only while four blocks fit in it.

RX 9070, field save, 60 s with the room near zero from the start (TOTK_MEM_RESERVE_MIB=21000): commit 7124 MB instead
of 8965, VRAM 2631 MiB instead of 3781, released textures 153 instead of 1866; mirrors refused for 423 MiB of pools,
GPU 4.45 ms a frame instead of 3.34 ms, 60 fps either way. Without pressure nothing changes. The census logs the
room, the device class line the mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:02:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6e9ca1ebfd Log the process commit in crash reports and in the memory census
A driver that runs out of memory faults or throws std::bad_alloc instead of returning an error: #285's log shows
bad_alloc inside amdvlk64.dll under gpl_link, and nothing in the report told that apart from a driver bug. The crash
report now prints the process and system commit, as rt_fatal does for our own failed allocations; --probe-kernel
checks the line.

The census at frames 600 and 1800 logs the commit as a baseline and, per heap, VMA's blocks and allocations beside
the driver's usage and budget, so a report shows whose memory grew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:53:57 +03:00
Valiantsin DzerakhandClaude Opus 5.5 717e68f601 Merge task/crashinfo: crash reports carry host registers, code bytes and memory at the register values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:20:01 +03:00
Valiantsin DzerakhandClaude Opus 5.5 06d70d0dfa Report host registers, code bytes and memory at a crash
The crash report named the lifted function and printed the guest registers, but the lifted code keeps guest
registers in host locals and writes them back only at calls, so the dump showed stale values and the faulting
instruction stayed unknown. Logs from #1005, #967 and #998 could not be traced further than the function.

The report now adds the x86 registers of the faulting thread (Windows and Linux), 64 bytes of machine code around
rip with the offset into the lifted function, so the instruction can be found in a local build of the same version,
64 bytes of memory around every register value that points at readable memory, and the first 128 bytes of the
guest stack. --probe-kernel checks the new lines with a child that faults with known values in rbx and x19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:19:31 +03:00
Luxury 2907f4efc3 Stop the installer space check at an unavailable drive root 2026-11-08 23:09:58 -05:00
Valiantsin DzerakhandClaude Opus 5.5 7e440be7ca Merge pull request #978: draw amiibo file names in any script in the F1 menu (KyokoMiki)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:13:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e17a27acba Merge pull request #623: report heap corruption from the vectored handler (ipavlin98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:13:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 35968c1efb Merge pull request #990: Linux AppImage: ignore the user site-packages so installer.gui imports (jay23606)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:13:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 169d057add Merge pull request #660: keep DLSS off the inventory screen (mictony6)
Ported onto vk_dlaa_plan (#794): the scene end in the inventory also records its submission and op (end_fence,
end_pos) so chunked replays see it at the same point. DLSS-only: everything sits behind dl_active().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:50:18 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0704543249 Merge pull request #947: sync shadow pools from the per-submission write-watch snapshots (corne-ac)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:41:23 +03:00
Valiantsin DzerakhandClaude Opus 5.5 bdb41dff7e Merge pull request #979: PC prompts for every game resolution the setup lays the UI out for (Wintego)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:36:02 +03:00
Wintego 62b8e9dee5 Draw the PC prompts' Shift as an arrow over its name
The arrow alone read as a direction (review of #585). The key keeps the width of the game's own glyph, so the name goes
under a solid arrow rather than on a wider key.
2026-11-08 20:35:55 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5e56bfc7f3 Merge task/chunked: record large submissions in chunks on several threads (#628, #794), off by default
replay_workers stays 0. On the 7900X 4 threads cut the replay by a quarter (town 6.7 -> 5.0 ms, fight2 5.3 -> 3.9 ms)
with no fps change: the game's own threads bound the frame there. Weak CPUs may gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:24:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 35b3d37ea3 Make a chunk's new texture entries follow replay order, and add a knob that records chunks out of order
entry_of decides at record time whether a new texture takes the content of a live GPU-written texture at its storage
(alias_copy) or the storage bytes. In a chunked replay the source's GPU ownership is set when a chunk records its
render, so the choice followed the order the threads got to the chunks: a later chunk could take the old bytes before
an earlier chunk recorded the render, and a mirrored texture keeps them. Now a chunk that makes an entry while a live
colour texture of its size sits at the storage waits until the chunks before it are recorded (vk_exec_chunk_settle,
the texture lock released for the wait), skips sources the scan revives at a later op, and ignores GPU ownership a
later chunk set in this submission. slot_resolve and vk_texture_by_id_locked no longer read tslots through a pointer
taken before the wait.

TOTK_REPLAY_SKEW_US (debug) makes the even chunks wait before they record, so the chunk after each one records
first in every submission. With it at 3000 us and 4 threads the field save runs clean (grass drawn, 0 validation errors); the alias path
did not fire there (0 copied, 0 filled from storage), so this does not reproduce the missing grass reported on
RADV, and the fix there stays unconfirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:53:12 +03:00
jay23606 9d07157b3e Linux AppImage fails with "No module named installer.gui" when the PyPI 2026-11-08 11:45:21 -05:00
ipavlin98 fd4427cb17 Run DLSS on the chunked replay
DLSS kept the replay on one thread: its decisions follow the passes in replay order, its jitter and camera are noted
draw by draw, and its evaluation reads the per-object motion list the scene's draws build. Now the scan takes DLSS's
decisions on its own state (vk_dlaa_plan, split out of vk_dlaa_pass) and notes the scene camera in replay order
(vk_dlaa_scan_draw), and the copy of the lit scene and the evaluation are records of the chunk that holds the op
(vk_dlaa_exec). A chunk's draws are jittered while they come before the op the scan ended the scene at. The
evaluation waits until the chunks before it are recorded, and the chunks after it wait for it (a hold), since they
sample its output. Chunks note their draws for the per-object motion one at a time (mo_lock in vk_exec_draw.c); the
uniform overrides of a motion draw and shader_for's last-frame variant flag are per thread, as the other chunks' draws
go on meanwhile; the FXAA shader table takes concurrent lookups. fxaa = 0 runs in chunks too.

With dlaa = 1 at 4K (DLSS Quality) on 1.2.1 and an RTX 5070, replay_workers = 4 takes the frame rate from 70 to 78
fps where the CPU is the limit, with no one-frame glitch in 241 consecutive frames.

(cherry picked from commit 04f8bb1b57)
2026-11-08 19:39:18 +03:00
ipavlin98 6b4d84ec50 Record large submissions in chunks on several threads
replay_workers = N (0, the default, keeps the sequential replay) records a submission of enough draws in chunks that start between passes, on N threads and in twice as many chunks, and submits them in order. In the town at 4K on an RTX 5070, 4 threads take the replay from 8.2 to 4.5 ms a frame and the frame rate from 71 to 79.

A sequential scan on the submitting thread applies only what is queue state (fixed-function and bind ops, render targets, render enable and conditions, samples-passed counting) and keeps a copy of it where each chunk starts, with how far its lists got: inline uniform updates, RENDER_COND results, contact shadow marches and the ops that drop uniform copies. A chunk takes those from the lists instead of from the chunks before it, starts with every image in GENERAL and ends with a full barrier, so its recording depends only on the copy and the lists.

Each thread records with a context of its own: the replay's memos, descriptor state, occlusion query segments and per-command-buffer state moved from file statics into VkExec and VkExecDraw, and the tables the contexts share (stand-ins, depth twins, views, mirrors, timestamps, completion callbacks, crumbs) take locks or atomics. The scan makes render targets live in replay order and notes the op (vk_texture_revive), since a chunk can sample a storage before the chunk that renders it got there or after the scan went past a later target there: a chunk's texture slots resolve as of its own op, and the chunks after a texture with no entry yet wait for the chunk that makes it. Uniform copies are dropped at the op that writes them (vk_usnap_drop_at).

Contact shadows plan their march in replay order (vk_contact_plan) and the chunk that holds its op records it; the HDR source keeps the copy that comes last in replay order. DLSS and fxaa = 0 act on the frame draw by draw and keep the replay sequential. TOTK_REPLAY_STATE_CHECK=1 compares the state after the last chunk with the scan's.

A draw or dispatch whose shaders write storage buffers drops the uniform copies they cover from its own op on, as the other GPU writes do (scan_writes). The scan still translates no shader: it reads the shaders the recording contexts translated from a table shared with them (vk_exec_draw_writes), and an op whose shader no context translated yet holds the chunks after it until it is recorded. Before, the chunk that recorded such a write dropped the copies for every position at that moment, so a later chunk recorded first bound the copy taken at the submit call: on an RX 7800 XT on RADV the grass near the camera came out dark or unshaded in some runs with 4 threads.

TOTK_TRACE=perf counts the chunks held per submission by reason and logs when each chunk of the last submission began. The settings menu has the thread count under Advanced, Scheduling, with the count this run uses under it.

The recorder thread begins, ends and submits the submission's open part (vk_rec.c rec_end_cb), so a chunk's mirror prologue stays off it: vk_sync_part waits until the recorder is idle, as the part pool allows one thread at a time, then records, ends and submits the prologue itself. Through the recorder the prologue could reach the queue before it was ended, and the GPU lost the device at the title screen.

(cherry picked from commit b94e2e9642)
2026-11-08 19:39:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4fcedda5e6 Merge task/fpcheap: cheaper FPCR checks in the FP helpers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:03:20 +03:00
Valiantsin DzerakhandClaude Opus 5.5 922edb177d Cheaper FPCR checks in the FP helpers
FCMP and the vector compares no longer flush denormal operands under FPCR.FZ: rt_fpcr_sync keeps MXCSR.DAZ equal to
FPCR.FZ, and x86 compares under DAZ read a denormal as a zero of its sign, as ARM does under FZ. The directed RMode
check of FN(op) and FN(vop) moved into the result check's branch, and min/max skip it, since they do not round.

Hot lifted functions shrink by about 10% of their x86 instructions; the town camera-pan bench (1080p, two runs each)
goes from 10.25 to 9.78 ms per frame. simd.py (60000 encodings), lift.py and func.py report 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:03:14 +03:00
Valiantsin DzerakhandClaude Opus 5.5 31630dee65 Merge task/vbbind: rebind only the vertex streams a draw reads that changed (#974)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:01:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7c5f88d33c Rebind only the vertex streams a draw reads that changed, and leave the streams it does not read bound
With vb_used the streams a draw does not read got the zero buffer, so draws in a row that read different streams
bound all 17 slots again almost every draw. On an RTX 5060 Laptop that cost 16 % of the frame rate (issue #974). Now
only the read streams are compared, one bind covers the changed ones, and the others keep the buffer bound earlier
in the command buffer, which the deferred frees keep alive until the submission is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:50:32 +03:00
美樹杏子 9382686c12 Fix Amiibo file names with non-ASCII characters displaying as garbled text in F1 menu 2026-11-08 18:39:21 +08:00
Wintego ceece5683a Make the PC prompts for every game resolution the setup lays the UI out for
The setup wrote romfs-patches-pc for 16:9 only, so prompts = pc on a 21:9, 32:9, 16:10 or 3:2 screen kept the game
resolution at 1920x1080 (cfg.c) or, with output_res set to the screen's shape, fell back to the Switch prompts.

- make_romfs_patches.py --reuse <folder>: a file of an earlier overlay whose contents match is hard-linked (copied
  where links fail) instead of compressed again; the output is byte for byte the same.
- The setup builds romfs-patches-pc-<w>x<h> for each of ASPECT_SIZES from romfs-patches-pc with --reuse: only the
  Common archive is compressed, so the fifteen folders take about two minutes and 6 MiB each instead of 22 minutes
  and 73 MiB each.
- cfg.c: the display-shaped output_res default applies with prompts = pc when romfs-patches-pc-<w>x<h> is there
  (romfs_shaped_pc_ready); Xbox prompts keep 1920x1080 as before.
2026-11-08 13:20:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6cf4aab5f0 Merge task/pcore4: no P-core preference on hybrid CPUs with fewer than 4 P-cores
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:11:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0d9525ba4a Leave the P-core preference off on hybrid CPUs with fewer than 4 P-cores
An i5-1235U or Core 3 100U has 2 P-cores. Binding the five frame-critical threads there, with a spinning recorder
among them, crowds two cores and their hyperthreads while the E-cores stay idle. plat_perf_core_setup now counts the
allowed physical P-cores and selects nothing below PC_MIN_CORES (4), so the scheduler places those threads as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:11:06 +03:00
Valiantsin Dzerakh b16c4c8938 Merge task/pcore: frame-critical threads prefer the P-cores of a hybrid CPU
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	runtime/vk_rec.c
2026-11-08 12:03:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 87fefa8ade Merge task/recasync: the recorder submits without the submitting thread waiting, larger publish batches without the poll
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:03:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 90fa6e1788 Let the recorder submit parts without the submitting thread waiting for it
rec_submit queued each part's submission and then drained the ring until the recorder had submitted it: 0.29 ms a
frame in the town on a Ryzen 7900X, 0.34 ms on 4 cores. Now it publishes the submission and returns
(TOTK_REC_ASYNC=1, default). The queue order the present, uploads, readbacks and device waits rely on is kept by
vk_queue_lock: a thread's first lock waits until the recorder submitted what was queued before (rec_queue_wait).
The recorder itself and a thread that already holds the lock skip the wait. rec_submit reports the first failed
submission the recorder made. Town, in-run A/B: replay -4.1% on all cores, -2.9% on 4 cores / 8 threads; queue
users waited about 0.02 ms a frame. fight2 with TOTK_REC_CHECK and the town under sync validation are clean.

Where the recorder does not poll (fewer than 6 usable logical processors) the producer now publishes the ring head
every 8192 words instead of 64, as Savage Wind Recomp does (runtime/nvn/vulkan/record.c rec_init): each publish
there costs an event set and a wake-up. Town on 4 logical processors: frame interval -1.3%. TOTK_REC_PUB=ab
compares both; the perf log counts the recorder's wake-ups and the queue waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:02:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3a26046cde Let the threads every frame goes through prefer the P-cores of a hybrid CPU
On an Intel CPU with P-cores and E-cores a thread on an E-core runs at about 60% of a P-core's speed, and the frame
waits for its slowest critical thread. The game's main thread, the thread that presents, and the vk replay, recorder
and present threads now select the CPU sets of the P-core logical processors the process affinity allows. Nothing is
selected on a CPU with one efficiency class or when the affinity leaves only one kind of core. TOTK_PCORE=0 turns it
off; "power:" log lines say what was selected and which threads took it. LAS Recomp does the same for its game,
render and vsync threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:46:10 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1446f3b06b Merge task/hybrid: hybrid laptops ask for the dedicated GPU, Windows power throttling off
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:13:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 320a41e0dc Ask hybrid laptops for the dedicated GPU and opt out of Windows power throttling
The executable exports NvOptimusEnablement and AmdPowerXpressRequestHighPerformance, which NVIDIA Optimus and AMD
switchable graphics read at start to run it on the dedicated GPU; Optimus can otherwise hide its GPU from a program
it put on the integrated one. At start the process opts out of EcoQoS (E-cores and low clocks on hybrid Intel CPUs
when Windows deems it in the background) and of Windows 11 ignoring its timer resolution while the window is hidden.
One "power:" log line says whether Windows accepted it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:13:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8b81beb98b Merge task/kspin: kernel waits and the recorder poll briefly before they sleep
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:05:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2d92565ada Poll briefly before kernel waits and the recorder sleep
Guest waits and waits for a virtual core usually end within microseconds. A thread now polls for 20 us with the
global lock released before it sleeps on its condition variable, so a quick wake costs no OS wake-up
(TOTK_KSPIN_US, 0 turns it off). The recorder polls by the clock for 60 us instead of 4096 pauses, which last a few
us on Zen 4 and about 140 us on Coffee Lake (TOTK_REC_SPIN_US). Both stay off with fewer than 6 usable logical
processors. Savage Wind Recomp polls the same way before it parks.

Machine A, town save, in-run A/B: frame interval -1.8% on average for the kernel poll (5 of 7 block pairs), -0.7%
with the process limited to 4 cores / 8 threads; the recorder poll is neutral there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:05:33 +03:00
itaskaev2 a48416dcf4 Merge branch 'dev' into carry/dynamic-res-100 2026-11-08 08:36:47 +01:00
itaskaev2 9b5ca3670f Merge branch 'dev' into carry/sync-ahead 2026-11-08 08:36:44 +01:00
corne-acandClaude Opus 5.5 a733b94645 Keep shadow_snap off by default under Wine until it has been measured there
Shadow storage is on by default under Wine as well, and the region snapshots were only measured on a native Linux
host. TOTK_SHADOW_SNAP=1 still turns them on under Wine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:20:03 +02:00
corne-acandClaude Opus 5.5 02752376f7 Sync shadow pools from the per-submission write-watch snapshots
Each 2 MiB region is now queried at most once per guest submission, shared with the texture checks, instead of once
per pool at every part of a submission. On a Steam Deck at Lookout Landing the replay thread's write tracking fell
from about 4.4 to 1.3 ms a frame and the frame rate rose about 7.5%. TOTK_SHADOW_SNAP=0 restores the old queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:01:05 +02:00
Valiantsin Dzerakh ae30a61b7b Some PR review ruleset 2026-11-07 20:22:50 +03:00
itaskaev2andClaude Opus 5.5 ce9ed75cee Merge dev into carry/sync-ahead
vk_memory.c: dev now keeps every finalized pool until retire_value (at
least the submission being recorded), which also covers a shadow with a
deferred copy, so shadow_had_deferred goes. The deferred shadow prologue
and dev's upload scratch pool both stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:22:50 +01:00
Valiantsin Dzerakh 214ba53a2a Up version 0.3.13 2026-11-07 20:22:35 +03:00
itaskaev2andClaude Opus 5.5 91f6fc9838 Merge dev into carry/dynamic-res-100
cfg.c: the key list takes dev's DESWIZZLE and keeps this branch's
DYNAMIC_RES and FIXED_RES_PATCH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:21:41 +01:00
Valiantsin DzerakhandClaude Opus 5.5 8bbcf52709 Merge task/poolfree: pool memory and texture records kept until the submission being recorded is done (device faults under the synchronous replay)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:05:55 +03:00
Valiantsin DzerakhandClaude Opus 5.5 36a83bd78f Keep pool memory and texture records until the submission being recorded is done
vk_pool_fini freed import, physical and virtual pools right after vkQueueWaitIdle, which covers submitted work only;
with the synchronous replay another game thread may be recording a submission that names the pool. Every pool kind
now retires through the completion queue at the later of the recording's value and the next one.

destroy_tex freed the texture record while other threads still held &e->pub (render targets for a pass, storage
bindings writing wr_fence through it): they read a stale VkImage and cached views by that handle. The record is now
freed with its image's stamp (vk_defer_free), and views of a dropped entry are deferred at once instead of cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:05:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9492315bac Merge task/deferstamp: deferred frees stamped with the submission being recorded (NVIDIA write-invalid faults, #788, #802) and the GPU address map for device faults
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:16:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dc45de0e9a Stamp deferred frees with the submission being recorded, whichever thread destroys the object
With the synchronous replay (the default since 7be68ec) a game thread finalizing a texture destroys released texture
cache entries while another game thread records a submission that slot_resolve may already have handed one of them.
defer() saw the recording's value only on the recording thread and stamped the others with the last submitted value,
so the completion thread freed the image before the submission naming it ran: "write invalid" device faults on NVIDIA
(#788, #802). vk_sync_submit now notes the value at the start of the recording, and every deferral made from then on
waits for it. A fight run counted 10 such deferrals during the world load.

Device faults now log the deferred-free state, and with VK_EXT_device_address_binding_report the object that held the
faulting address and whether it was unbound before the fault (TOTK_ADDR_REPORT=0 turns the map off). The census line
counts deferrals made off the recording thread during a recording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:16:50 +03:00
Valiantsin Dzerakh 6e5074e203 Merge task/pcsave: #911 (pipeline builds kept apart from the cache copy, the Intel crash at start, #681) and the cache save time in the log 2026-11-07 16:53:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5b5273b541 Log how long each pipeline cache save copies and waits for builds
The copy time bounds how long a compute miss on the drawing thread can wait for the save now that builds and the copy exclude each other; a put-off autosave is logged too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:53:43 +03:00
Valiantsin Dzerakh d02a917af7 Merge #911: keep pipeline builds apart from the cache copy, so Intel's driver does not crash at start (#681) 2026-11-07 16:52:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b7e72ae87f Merge task/gpuswz: block-linear textures deswizzle on the GPU (shorter stalls on entering new areas)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:38:33 +03:00
Valiantsin DzerakhandClaude Opus 5.5 57037ccc29 Deswizzle block-linear textures on the GPU when their storage is an imported pool
The replay thread deswizzled every block-linear texture on the CPU, and entering a new area stalled it for 50-236 ms. Now a compute pass reads the texture straight from the pool's imported buffer and writes a linear copy into a device-local scratch pool; the upload batch copies it into the image, or the ASTC decoder takes it. Large texture loads submit the batch early so the staging and scratch pools do not grow on the replay thread, and both pools are made at device creation.

On fight2 the render thread waits about 270 ms on textures per run instead of 900, and the load frame drops from 145 to about 80 ms. Mean fps is unchanged. TOTK_DESWIZZLE=cpu turns it off and check compares every upload with the CPU result (0 of 196k regions differ). Shadow mode (Linux/Wine) and textures outside the device's limits stay on the CPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:38:26 +03:00
itaskaev2andClaude Opus 5.5 b358b8f1f1 Keep pipeline builds apart from the cache copy, so Intel's driver does not fault in it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:24:04 +01:00
ProSepandClaude Opus 5.5 a3d876e2d2 Let sync_ahead lag the 1.4.3 frame wait too
On 1.4.3 the per-frame wait is the second of the two nvnSyncWait calls in f_70002db8ac (return address 0x70002db99c). Like on 1.2.1, its sync comes from a two-entry ring indexed per frame; the first call waits on one object's sync behind a mode check. wdiasjunior found the site and measured it on an RX 7800 XT (#676): about 67-70 to 81-83 fps on Great Sky Island.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:55:56 +01:00
ProSepandClaude Opus 5.5 4624bf8076 Let the game run one frame further ahead of the GPU (TOTK_SYNC_AHEAD, off by default)
Each frame the game waits in one nvnSyncWait (1.2.1 lr 0x7000b23dfc) for the frame it presented last. On the Switch
that work is long done by then; here it is still in the replay and on the GPU, and on Great Sky Island the wait is
about 4.6 ms of a 13 ms frame. With TOTK_SYNC_AHEAD=1 that one wait waits for the frame before it instead, once the
replay thread submitted the asked value, so the game builds the next frame while the GPU runs the last one.

The game then writes guest memory that GPU work still in flight reads. Shadow storage makes that safe: a shadow update
of a pool with a reader in flight is queued and copied by the GPU in the next part's prologue, in queue order, instead
of memcpy'd under that reader (TOTK_SHADOW_DEFER=1, the default with the switch; 0 makes the replay thread wait for
the reader instead). The guard is read under the shadow lock and the wait takes that lock once after raising it, so no
sync that read the old guard is still copying when the game writes again. The switch needs shadow storage (Linux and
Wine by default, TOTK_SHADOW=1 on native Windows) and the 1.2.1 call site; otherwise it stays off with a log line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:55:55 +01:00
ProSepandClaude Opus 5.5 a6c33b072c Hold the game's dynamic resolution at 100 % and let the scene grow past 4096 wide
The game picks a scene tier of 100, 90 or 80 % mostly from a fixed cost model made for the Switch that grows with the
scene width squared, so on a wide output it drops to 80 % on any GPU: at 5120x1440 every frame drew 80 % of the scene.
Patch set fixres replaces the one instruction that moves the tier's scale into the controller's result (1.2.1
main+0xcc2c00, 1.4.3 main+0x5848a8 as MacNativeGames patches it) with 1.0. The new setting dynamic_res (menu toggle,
live) brings the game's stepping back and is on by default in the Steam Deck profile.

The scene cap goes from 4096 to 8192 a side, so render_scale above 80 works on a 32:9 output, and the scene also stays
within a DynamicTextureAllocatorMem under 2 GiB; the log says when it was made smaller. The memory selftest now maps
past the real pool size instead of a fixed 4 GiB, which a larger scene grows past.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:55:54 +01:00
Valiantsin Dzerakh 71d8b0cc61 Merge #900: lay the action prompt's diamond out as an Xbox pad with the xbox-label prompts 2026-11-07 12:38:12 +03:00
Valiantsin DzerakhandClaude Opus 5.5 561fe54c06 Merge task/crashinfo: crash reports name the driver frames, the C++ exception type and the host function called (#285)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:14:55 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6e977280bb Name the driver code and the C++ exception type in crash reports
An exception no handler took (a fault in a GPU driver, a C++ exception a driver let escape) listed only the return addresses in totk.exe, and those match only the reporter's own build. #879 and #285 crash in the AMD driver with nothing in the log saying which driver code failed or what it threw.

The unhandled-exception filter now lists the frames of every module on the faulting thread with the module name and offset, up to 12 outside totk.exe so this executable's frames still fit. For a C++ exception thrown by MSVC-built code it reads the thrown type's decorated name from the ThrowInfo, and what() for a std::exception, with every read checked. The guest pc of a host function call is printed with that host function's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:14:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2f2bc6d9a3 Merge task/menumouse: #905 (T is Y's second default key, for the menus; a bind line takes its key from other buttons' defaults)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:08:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 469c528ffe Bind T to Y by default for the menus, and let a bind line take its key from the defaults of other buttons (#905)
While a game menu has the pointer the left mouse button is A, and it was Y's only default key, so keyboard and mouse players could not sort the inventory or press Y anywhere in the menus. T is now Y's second default key.

A bind line in totk.ini now also removes its keys from the defaults of buttons without a line, as the remapping rows already do: with bind_X = key:t, T presses X only, not X and Y.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:06:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d2bb096cd6 Merge task/menumouse: #908 (the middle and side mouse buttons keep their bindings while a game menu has the pointer)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 12:03:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 61e8fd6bb9 Keep the middle and side mouse buttons bound while a game menu has the pointer (#908)
The bindings dropped every mouse button while the cursor was not captured, and the menu pointer releases the capture,
so side buttons bound to L and R turned the menu's tabs only in the frames before the pointer appeared. While the menu
pointer is shown and over the window, the middle and side buttons now count; the left and right buttons stay the
menu's A and B.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:58:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 fdf990957c Merge task/rtstamp: #795 (rebuild install-side objects and relink when the runtime contents change, whatever the file dates)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:46:01 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1c59c98adf Merge #878: keep the native camera distance after aiming in direct mouse mode
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:21:19 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2eb5adebd6 Merge #898: record only the dynamic states that changed since the last pipeline state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:07:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6c6f48c40d Merge #897: overlay uniform updates between passes too, and clear the overlays once their copies are recorded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:07:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f81dc57e9e Rebuild install-side objects and relink when the runtime contents change, whatever the file dates
The release zip dates every file 1980-01-01, so a new release extracted over an old Setup folder has sources and a
libtotkrt.a older than the objects and the totk.exe the last install left in temp\<name>\build. Ninja compares mtimes
only: it kept the old mouse_direct.o and romfs.o and linked them with the new library (undefined symbol
input_direct_mouse_shake on 0.3.11, romfs_shaped_ready on 0.3.12, #795), or skipped the link and kept the old exe
when the version string did not change.

With --runtime-lib, build_game.py now writes obj/runtime/inputs.sha256, one hash over the library, the INSTALL_SIDE
sources and every runtime header, rewritten only when the hash changes. The install-side objects and the link depend
on it. Builds without --runtime-lib are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:47:29 +03:00
Valiantsin DzerakhandClaude Opus 5.5 204f2843de Merge #861: load the NGX core through the platform layer, so vk_dlaa.c has no Win32 calls
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:39:34 +03:00
Valiantsin DzerakhandClaude Opus 5.5 316ba63619 Merge task/uborder: #896 (order overlapping uniform overlay copies), #859, #860, #862, #863 (checks that failed on dev)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:26:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 45b34ff4dd Merge #863: expect the base language in the probe's automatic menu language check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:05:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d9a789fb5f Merge #862: translate the speaker setting into Brazilian Portuguese and Simplified Chinese
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:05:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 bc7f2e1aff Merge #860: ignore every linked file in the worktree test's scratch repository
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:05:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1b851e958f Merge #859: describe probe_movie_bench in docs/settings.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:05:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 70fa452002 Merge #896: order overlapping uniform overlay copies with a transfer barrier, so a partly overlapping newer update lands last
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:01:43 +03:00
Iker AcevedoandClaude Opus 5.5 90c14c9774 Lay the action prompt's diamond out as an Xbox pad with the xbox-label prompts
With pad_layout = xbox or auto, Switch A, B, X and Y are the buttons labelled so on an Xbox pad, but the diamond of
the action prompt (blyt/DoCommand_00.bflyt) kept the Switch positions: A on the right, where an Xbox pad has B.
make_romfs_patches.py --prompts xbox-label now moves each piece of the diamond to its partner's place (A with B, X
with Y): the button with its bar, the letters around it, the flashes of a repeated or held press, and the base image
that lights the button's side. X's bar reaches left, as Y's does on a Switch. The other prompt styles are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:11:33 -05:00
Claude 752e15bcc7 Record only the dynamic states that changed since the last pipeline state in the command buffer
Each change of pipeline state in prepare_draw re-recorded the whole dynamic state through vk_pipeline_set_dynamic:
about twenty commands, the vertex input packet rebuilt, and the blend enable, equation and write mask of every
attachment, however few fields differed. Every replay pipeline declares the same dynamic states, which a pipeline bind
leaves as they were, so vk_pipeline_set_dynamic_delta compares the values with what the command buffer already holds
and records only the commands that differ; the vertex input is rebuilt only when the vertex shader, attributes or
streams change. The record is per command buffer (B.dyn_valid, cleared with the rest of B on a new one or a split).
TOTK_DYN_DELTA=0 records the full set as before; ab alternates; TOTK_TRACE=perf counts calls, commands and calls that
changed nothing.

1.4.3, RTX 4070 Ti, 2560x1440 DLAA, standing still on Great Sky Island, TOTK_AB=10 (tools/ab_summary.py): 13.75 ms
a frame with the full sets, 12.59 ms with the deltas (-8.5%, 72.7 to 79.4 fps); replay 7.77 to 6.67 ms. An A/A run
with the same method moved the frame by 0.08 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-07 03:00:57 +00:00
Claude 324f85602f Clear the uniform overlays once their copies are recorded
After vk_exec_ub_flush the full barrier barrier_to records makes the copied bytes visible, so later bindings can read
the real buffer. The overlays stayed until the submission's end instead: every later binding inside their span
scanned them, and a block an overlay covered in part got a merged scratch copy that the descriptor build skip then
left alone. With the updates between passes overlaid too (Great Sky Island: 274 of 594 per submission) that was 290
merged copies and 1.1 ms more replay a frame, more than the barriers saved on the GPU (0.3 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-07 00:18:52 +00:00
null15564 2a937319b4 Restore native camera distance after aiming with direct mouse input 2026-11-07 05:55:00 +07:00
Claude 49b2314dd8 Overlay uniform updates between passes too, so a run of them before a pass costs one barrier
An update between passes was copied at once behind a full barrier of its own, and the copy it recorded made the next
update or pass begin need another: a run of k updates before a pass cost k + 1 full barriers, about 250 updates a
frame on Great Sky Island. They now become overlays like the updates inside a pass; every GPU reader of the real
buffer after them comes behind barrier_to, which records the copies first. TOTK_UB_OVERLAY_OUT: 1, 0, ab; the perf
line counts the overlays made between passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-06 22:35:01 +00:00
Claude b317f1c37e Order overlapping uniform overlay copies with a transfer barrier
vk_exec_ub_flush recorded the copies of a pass's overlaid uniform updates back to back behind one barrier. Copies
with no barrier between them may run in any order, and ov_add only drops an older pending copy of exactly the same
range, so two updates that overlap in part could leave the older bytes in the buffer where they overlap. A copy that
overlaps one recorded since the last barrier now waits for it behind a transfer-to-transfer barrier; the perf line
counts those barriers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-06 22:33:24 +00:00
itaskaev2andClaude Opus 5.5 1dfe05679e Expect the base language in the probe's automatic menu language check
resolve_menu_lang falls back from a region tag the catalog lacks to its base language (ru-RU: ru), but probe_menu_text expected English for such a tag, so --probe-services failed on any Windows whose UI language is ru-RU, de-DE and the like; en-US maps directly and passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:20:20 +01:00
itaskaev2andClaude Opus 5.5 1d89570ba8 Translate the speaker setting into Brazilian Portuguese and Simplified Chinese
963901c added the Speakers row (stereo or 5.1) in English and Russian only, so --probe-services failed: menu text ids 189 to 193 lacked pt-BR and zh-Hans, and the menu showed them in English in those languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:19:47 +01:00
itaskaev2andClaude Opus 5.5 cc4df7d140 Load the NGX core through the platform layer, so vk_dlaa.c has no Win32 calls
tools/check_platform.py failed on vk_dlaa.c: it read the NGX core's folder from the registry, loaded it with LoadLibraryA and GetProcAddress and named the executable's folder with GetModuleFileNameW. platform.h gets plat_ngx_core_path (the registry folder plus _nvngx.dll on Windows, libnvidia-ngx.so.1 on Linux), plat_exe_dir_wide (the folder as NGX's wchar_t string; the UTF-8 to UTF-32 conversion moves from vk_dlaa.c to linux.c) and plat_lib_error, and the core loads through plat_lib_open and plat_lib_sym on both hosts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:18:21 +01:00
itaskaev2andClaude Opus 5.5 9233322aa8 Ignore every linked file in the worktree test's scratch repository
tools/worktree.py links config.local.toml and CLAUDE.local.md since e3e72df, but the test's .gitignore listed only the first, so git status in the new worktree showed CLAUDE.local.md and the test failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:08:11 +01:00
itaskaev2andClaude Opus 5.5 5cd48ea696 Describe probe_movie_bench in docs/settings.md
tests/settings_doc_check.py failed on dev since 3cd2410 added TOTK_PROBE_MOVIE_BENCH to known[] without a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:07:38 +01:00
Valiantsin Dzerakh eda417fd1d Up version 0.3.12 2026-11-06 21:06:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 da86c39121 Merge #858: serve the HeapDef of the highest content mod below an HD texture pack, grown by the pack's texture heap, so the pack above The Second Quest no longer crashes at start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:45:25 +03:00
Valiantsin Dzerakh 4f113847f5 Merge task/perfdraw: bind only the vertex buffers a draw's vertex input reads 2026-11-06 20:43:39 +03:00
Valiantsin DzerakhandClaude Opus 5.5 da24dd4cba Bind only the vertex buffers a draw's vertex input reads
The game keeps about ten vertex streams bound while a draw reads a few of them, and bind_vertex_buffers resolved and
compared all 17 slots on every draw. vk_pipeline_set_dynamic now returns the bindings of the state's vertex input,
from the same vertex_input that builds the dynamic input or the pipeline's, and the other slots get the zero buffer.
A stream is taken from the previous bind only when it was resolved there. In the fort, bind_vertex_buffers drops from
9.0% to 7.2% of the replay thread's work (two profiles, recorder on). TOTK_VB_USED=0 binds every stream as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:43:33 +03:00
Valiantsin Dzerakh 5eaac2ee86 Merge branch 'pr858' into task/heapgrow 2026-11-06 20:39:15 +03:00
Valiantsin DzerakhandClaude Opus 5.5 03c703a712 Merge #809 and #814: draw a 16:9 picture with bars when the HUD layout for a wider or narrower game resolution is missing, and fix the installer DPI test at 125% scaling
#809: on 1.4.3 at 2560x1080 without romfs-patches-2560x1080 the game drew 1920x1080 with the 16:9 HUD, aspect patch
off, 0 validation errors; the four probes pass. #814 changes the test only; DpiTest passes at 96 DPI, the 125% case
was checked by hand against Tk's rounding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:27:18 +03:00
Valiantsin Dzerakh 545a97d7ff Merge branch 'pr814' into task/hudshape 2026-11-06 20:24:04 +03:00
Valiantsin Dzerakh 0d4c3555d5 Merge branch 'pr809' into task/hudshape 2026-11-06 20:24:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6cb41a5d17 Merge #835: store the data writes of mods' ExeFS patch files into main before the game runs
Checked on 1.4.3: tests/exefs_patches_test.py (14) and tests/installer_test.py (53) pass, --selftest, --probe-mods,
--probe-cfg and the four probes pass, the lifted main is byte-identical to generated/v143 apart from the new
folded.txt, and a field run without mods reaches gameplay with 0 validation errors. Not covered: folded.txt leaves out
the exception tables the lifter reads directly (recomp/jumptables.py landing pads), which no known mod patches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:12:53 +03:00
itaskaev2andClaude Opus 5.5 e69ee44a3a Grow a content mod's HeapDef when an HD texture pack above it grows the texture heap
df9ba65 keeps a content mod's heap sizes when its table is the one served, that
is when the mod is above the texture pack. With the pack on top (HDPacks.com's
4kTextures sorts first), the pack's table was still swapped for the game's own,
and The Second Quest's larger RSDB and UI heaps were lost: the game crashed at
start writing to address 0 in main+0x1d4200. heapdef_overlay now grows the
table of the highest switched-on mod that changes other heaps only, and falls
back to the game's own table when no mod has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:04:18 +01:00
Valiantsin Dzerakh 257f6201a3 Merge branch 'pr835' into task/exefsdata 2026-11-06 19:56:29 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a83b3b8b62 Merge #743: keep uniform updates inside a pass as scratch overlays and copy them at the pass end
Measured on machine A (RX 9070, 1.4.3, 3840x2160), in-run A/B of TOTK_UB_OVERLAY on standing scenes:
fight2 hills with Link standing after 55 s (about 104 updates inside a pass per submission): frame
11.99 -> 11.56 ms (-3.6%), GPU 7.41 -> 6.78 ms, replay unchanged. Field and sky island edge (6-19
updates inside a pass): -1.0% and -1.2%. No validation errors, no visible differences in fight2 shots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:34:52 +03:00
Valiantsin Dzerakh f594062a43 Merge branch 'dev' into task/uboverlay 2026-11-06 19:15:36 +03:00
fa94e36549 Store the data writes of mods' ExeFS patch files into main before the game runs
An IPS, IPS32 or pchtxt file for main that writes main's .rodata, .data or .bss was refused as a whole. Such writes
need no lifting: tools/exefs_patches.py now puts them in build-cache/exefs.data, and runtime/mods.c stores them into
the loaded main before any game code runs, as emulators and Atmosphere apply a patch file to the module before its
dynamic linker runs. A DT_RELR word gets main's base added; bytes over words an explicit-addend relocation fills change
nothing and are dropped; a write into the dynamic linker's tables, a lazily bound function pointer or outside main's
image keeps the file unsupported. A mod whose data lines do not all fit the build keeps its code patches off too.

The mods are applied from the bottom one up (mods_exefs_apply), so where two mods write the same bytes the higher one
wins, as it does for files. --probe-mods checks it with two mods writing one byte.

The lifted code reads some of main's bytes at lift time (the jump tables). recomp/gen.py lists those ranges in
<gen>/main/folded.txt, and the setup checks the data writes against them after the lift: a file that writes them
becomes unsupported, since the change would not reach the generated code. Generated C without mods is unchanged.

This is the data part of #790 (ericdallo's ExeFS work), split out on its own; the plugin part follows separately.

Co-Authored-By: itaskaev2 <165683571+itaskaev2@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:51:42 +01:00
Valiantsin DzerakhandClaude Opus 5.5 2327db91c3 Merge task/recline: keep the recorder queue's head, tail and producer state on separate cache lines and publish head in batches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:49:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0f4e0af554 Declare the recorder queue's put_h field by its own name, which the move into Q had rewritten as Q.put_h.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:24:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b578d26628 Keep the recorder queue's head, tail and producer state on separate cache lines and publish head in batches
The recorder spins on head while it waits, and tail, put_h, head and the producer's statistics shared one cache line,
so every packet moved that line between the two cores: put took 6% of the replay thread in the fort with the recorder
on. head, tail and sleeping now have a line each, the producer keeps its own state and the last tail it read, and pub
publishes head once 64 words wait (TOTK_REC_PUB; 0 publishes every packet). Every wait publishes everything first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:18:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 df9ba65a91 Swap a mod's HeapDef for the game's own only when that table grows TextureHeap
heapdef_overlay replaced the served mod table whenever any switched-on mod grew the texture heap. With an HD pack and
another mod that ships a HeapDef for other heaps on top of it, the other mod's sizes were lost. It now checks the
served table itself (resolution.c res_heapdef_tex_grown): only a table with TextureHeap over the shipped size is
swapped, any other stays the base and gets the growth on top.

A mod HeapDef the port cannot read no longer logs "TextureHeap 0 KiB": the failure is logged with the mod's name.
read_original sat between read_served and its comment; each function has its own comment again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:23:15 +03:00
Valiantsin DzerakhandClaude Opus 5.5 caf53eb764 Merge #713: grow the texture heap a mod's HeapDef asks for, so HD texture packs load (#673)
Brings in #576 (kurerb, tex_heap_extra_mb) and itaskaev2's mod HeapDef reading on top of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:08:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 76c55e3374 Merge task/copystat: keep NVN recording copies off the game threads' critical path
fight2 on the same dev base, A B B A whole runs, 10-75 s: 13.31 -> 12.82 ms per frame (-0.49 ms); a 4-core
affinity run showed the same -0.68 ms share on an older base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:17:45 +03:00
Valiantsin Dzerakh c0ec4e3a4f Merge branch 'dev' into task/copystat 2026-11-06 14:05:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3dd16691f1 Count host calls per thread instead of with a locked add on the slot's cache line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:05:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5c8f3ee5c6 Keep NVN recording copies off the game threads' critical path.
Three game threads flatten recordings, inline them and add control memory at once, and every large copy came
from a fresh VirtualAlloc, so each one paid zero-page faults, and release_copies freed them under the exclusive
copy lock. Copies are now freed after the lock through a per-thread dead list, blocks of 256 KiB to 16 MiB come
from a small best-fit cache, and each copy keeps its pin list so a free or clone no longer walks the records again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:05:44 +03:00
itaskaev2andClaude Opus 5.5 2ad82557fd Reserve the larger stack and heap block only for a texture heap extra
The block below the module window stays 6 GiB, as on dev, and grows by the texture heap extra (tex_heap_extra_mb or
a mod's HeapDef) rounded up to a GiB. A player without an HD pack reserves nothing more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 09:43:42 +01:00
itaskaev2andClaude Opus 5.5 8ac207f353 Grow the texture heap a mod's HeapDef asks for instead of serving that table
HD texture packs made for the emulators ship their own HeapDef with TextureHeap and its parent grown for the
emulators' 8 GB memory option (HDPacks.com's 4K pack: by 3,419,000 KiB). Served as it is, the game asked for room the
application pool does not have and crashed at start (#673); without the file it looped the panic Blood Moon.

Before mem_init sizes the pool, resolution.c reads the HeapDef of every switched-on mod (mods.c mods_peek_romfs, which
lists the mods' RomFS roots without the rest of the scan: mods_scan needs paths main sets later) and counts the largest
TextureHeap growth over the shipped 506,880 KiB like TOTK_TEX_HEAP_EXTRA_MB (the larger of the two, at most 4 GiB). The
pool and the heap cap grow by it, and romfs.c heapdef_overlay serves the game's own table grown by that amount in the
mod's place, so the other heaps keep the sizes of the game version that runs.

Tested with the HDPacks.com 4K Texture Pack v1.0 as players install it, on 1.4.3 and 1.2.1 (RTX 4080 SUPER): the game
loads a save and runs at 60 fps with 0 validation errors, and holds 4.5 GB of game memory pools against 1.2 GB without
the pack. docs/mods.md, README.md and totk.ini.example say so; other packs come with no guarantee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 09:43:41 +01:00
Herbert CostaandClaude Opus 5.5 2a70683007 Grow the HeapDef for tex_heap_extra_mb at the game's own resolution too
heapdef_overlay returned at once when res_heap_extra_kib() was 0, which it is at 1920x1080 with the game's scene size
and shadow map, so tex_heap_extra_mb did nothing there and a 1080p player with an HD pack still hit the Blood Moon
loop. It now also builds the table when res_tex_heap_extra_bytes() is set. Found by itaskaev2 in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 09:43:41 +01:00
Herbert CostaandClaude Opus 5.5 a57ea119fa Grow the world texture heap for HD texture packs with tex_heap_extra_mb
HD texture packs made for emulators with 8 GB of DRAM, such as the HDPacks 4K pack, grow "TextureHeap" in their own
HeapDef table. Served as a mod, that table replaces the one the port grows for the resolution and the shadow map, and
the game crashes at boot; without it the pack's textures do not fit, and the game loads and then starts its panic
Blood Moon over and over.

tex_heap_extra_mb ([mods], 0 to 4096 MiB, read at start) grows TextureHeap and its parent ゲームグラフィックス in the
port's HeapDef, and the application pool and the heap cap by the same amount. LOW_SIZE goes from 6 to 10 GiB so the
stack and heap block holds the largest heap. At 0 the pool, the heap cap and the HeapDef are what they were.

The selftest's "beyond the pool" MapPhysicalMemory asked for 4 GiB, which a grown pool holds; it now asks for just
past the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 09:43:41 +01:00
Valiantsin Dzerakh 959033e1fb Merge the latest head of pull request #743 2026-11-06 11:25:30 +03:00
Valiantsin Dzerakh 4402b030fe Merge branch 'dev' into task/uboverlay
# Conflicts:
#	runtime/cfg.c
2026-11-06 11:25:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e0f8121a6a Merge task/playtime: play time across runs on the Game tab (#729, #720)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:56:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4645b8a0d0 Leave scripted runs out of the play time, keep a damaged file whole, and test the file with --probe-playtime.
A --seconds run (benches, menu shots, input scripts) reads the total so the Game tab shows it, but starts no counting
thread. The file is now read once at start instead of on the thread. A playtime.txt with a UTF-8 byte order mark reads
as valid, and a damaged one is kept whole as playtime.txt.bad instead of its first 63 bytes. --probe-playtime checks the
parser, a missing, valid and damaged file, the 30 s save and the 2 s cap of one tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:55:42 +03:00
itaskaev2andClaude Opus 5.5 d850c2116d Count play time across runs and show it on the Game tab
A host thread adds each second in which the window has the focus and the game presents frames, keeps the total in
<profile>/playtime.txt (every 30 s and at exit) and the Game tab of the settings menu shows it with this run's share.
Texts in English, Russian, Brazilian Portuguese and Simplified Chinese. Closes #720.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:52:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a959eab275 Merge task/vrampress: drop released textures the game has overwritten under VRAM pressure (#754)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:08:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 30456bc0a2 Merge task/recown: the recorder thread owns the parts it records from begin to submit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:07:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 218d251950 Merge task/gpuwrite: regions the GPU writes are never frozen read-only (#788, #802)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:07:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 895d202cce Let the recorder thread begin, end and submit the parts it records.
The part command buffers and their pool were used by two threads. The submitting thread still waits for the
submission, so the queue order is unchanged. TOTK_REC_END=0 brings the old split back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:07:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9a81e3e2f1 Keep regions the GPU writes from being frozen read-only, and thaw a frozen one before a GPU write.
Storage-buffer stores, copies and fills land in imported guest memory without a fault or a write-watch bit, so a
region only the GPU wrote looked idle and was frozen while the GPU kept writing it. LAS Recomp never changes the
protection of imported memory because of NVIDIA. A possible cause of the NVIDIA "write invalid" device faults
(#788, #802), not proven.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:07:38 +03:00
Luxury 057a2d751c Account for Tk screen size rounding in the DPI test 2026-11-05 18:37:07 -05:00
Claude 0be3472fe0 Describe ub_overlay in docs/settings.md, which tests/settings_doc_check.py now asks of every name in known[]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-05 22:21:07 +00:00
Claude 53c916730a Merge remote-tracking branch 'origin/main' into pr/ub-overlay 2026-11-05 22:20:18 +00:00
ProSepandClaude Sonnet 5.5 0db730e800 Draw a 16:9 picture with bars when the HUD layout for the game resolution's shape is missing, and copy every shaped overlay into the build
A game resolution that is not 16:9 widened the camera while the HUD stayed at 16:9 stretched across the picture whenever romfs-patches-<w>x<h> was not next to romfs-index.bin, which is always so for a build from source. resolution.c now falls back to the largest 16:9 size inside the asked one and keeps the camera aspect patch off, logging the command that makes the layout. build_game.py copies every romfs-patches* folder with a manifest.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 22:44:39 +01:00
Valiantsin Dzerakh 0ae47a5782 Up version 0.3.11 2026-11-05 19:59:52 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4cd6b83716 Merge task/prwave10: PR #767 (game resolution follows the display's shape) and PR #625 (frame queue, off by default)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:56:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7f04a104a3 Merge PR #625: a frame queue that frees a window image at its display time, off by default (ipavlin98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:54:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 71831771cd Merge PR #767: default the game resolution to the display's shape (Wintego)
Keep 1920x1080 as the default with prompts = xbox or pc: the setup builds those prompts' folders for 16:9 only, so the
shaped default would have swapped the chosen prompts for the Switch ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:53:57 +03:00
Valiantsin Dzerakh 48c014843b Merge task/prwave9: PR #776 (size table entries of mods' pack members) and PR #650 (movie shape on outputs that are not 16:9) 2026-11-05 19:34:44 +03:00
Valiantsin Dzerakh 0d5719858e Merge PR #650: keep the shape of the pre-rendered movies on outputs that are not 16:9 (ProSep)
# Conflicts:
#	runtime/cfg.c
2026-11-05 19:32:18 +03:00
Valiantsin Dzerakh e4538aebc8 Merge PR #776: raise the size table entries of files inside mods' packs (ericdallo) 2026-11-05 19:32:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a27e85b673 Merge task/mouse (1.2.1): the direct mouse camera on game 1.2.1 (issue #751)
All 72 1.4.3 sites mapped to 1.2.1 main (82 patch lines), the values that differ in a per-version MouseLayout. The
player state (ability, bow in hand) is not known on 1.2.1 yet, so mouse_aim_scale has no effect there, and the menu
pointer and map cursor stay 1.4.3 only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:48:19 +03:00
Valiantsin DzerakhandClaude Opus 5.5 61d0075039 Give game 1.2.1 the direct mouse camera of 1.4.3 (issue #751)
The 72 mouse_direct sites of patches-v143.txt mapped to the 1.2.1 main by enclosing function, each original word
checked: the camera input update, free orbit, Ultrahand and the 16 player camera modes. 1.2.1 keeps some values in
other registers and stack slots (camera input x in s11, orbit angles at sp+0x3c/0x40, Ultrahand x in s9), so the
hooks read a per-version MouseLayout; the 1.4.3 values and behaviour are unchanged.

Where 1.2.1 adds a turn in a shared helper, the degrees go to the store of the helper's result (Aiming's pitch
target, new ROLE_BASE_STORE) or to the turn it is given (Horse yaw). Wakeboard, ShieldSurfing and Tail read the stick
twice (REPEAT on the first read), the dialogue cameras' angle loads are pre-indexed (mov -> add, as ClimbObj).
The player state (ability, bow in hand) is not known on 1.2.1 and reads as not found.

tests/mouse_direct_check.py runs the hooks with both layouts and checks each load-site table against its patch file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:47:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dc9a29f13c Merge task/mouse (1.4.3): PR #752 and #753 with the direct mouse camera follow-ups
The menu pointer stays through tab switches, the mouse moves the map cursor and the wheel zooms it (#769). Every player
camera turns at the 0.3.10 rate of 0.08 degrees a count, the pitch no longer springs back after an aim (#717) and a
mouse shake detaches a fused part from what Ultrahand holds (#734). The load hooks return the game's own value in a step
without mouse motion, so a gamepad player gets the game's values unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:02:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b77962bae5 Keep the 0.3.10 mouse rate in the direct camera: 0.08 degrees a count in every camera and Ultrahand at its old rate (PR #753 follow-up)
PR #753 unified the cameras at 70/1500 = 0.0467 degrees a count and doubled Ultrahand's rate, which made every
existing mouse_sens turn the free camera 42% slower. The free camera keeps 0.08 degrees a count at mouse_sens 1, every
camera #753 unifies takes the same 0.08, and the held object keeps 0.01 stick units a count over the frame ratio (with
#753's cap of three full tilts a step). A mouse shake now needs 125 counts a swing at sensitivity 1 (10 degrees).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:49:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d52463b2cc Keep the game's own value in the direct mouse camera load hooks when the mouse did not move (PR #753 follow-up)
The stick loads returned the sample's pad stick even in a step without mouse motion, which drops anything the game
writes to the camera input's stick after the input update (main has 426 stores to +0x420/+0x424). The base loads added
0 degrees, which turns a -0 into +0. Both now return the loaded value untouched when the step has no mouse motion and no
mouse shake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:48:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 184221f032 Merge PR #753 from Tempus3214: the direct mouse turns every player camera at one rate, the pitch no longer springs back after an aim, and a mouse shake detaches a fused part from what Ultrahand holds (fixes #734, #717)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:32:19 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c2c68aa2cc Prefilled base and update paths stay only while the chosen install recorded the same files
Another install, say 1.2.1 while the recorded update is 1.4.3, would extract again from them. Each dest change compares the prefilled file's name and size (one stat) with the install's totk-install.json inputs and clears the field on a mismatch. Fields the user picked or typed are never cleared, and a folder without state keeps the prefill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:27:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 23c278ef9f The setup window waits at most 1.5 seconds for the recorded paths
A recorded path on a disconnected network share can block its stat call. The paths load in a daemon thread and nothing is prefilled when it has not returned in time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:22:34 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a2843c3fbf The setup window remembers the base, update, keys and firmware paths along with the install folder
A reinstall or a switch to another game version starts with the form filled in. Only absolute paths are stored, never the keys file's contents, and an entry that no longer exists, has the wrong kind or cannot be statted is dropped on load. Follows #715 by DanielSvoboda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:21:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 de2f9f5cab Merge PR #752 from Tempus3214: the menu pointer stays through tab switches, the mouse moves the map's cursor and the wheel zooms it
Resolved the conflict with #760 in runtime/input_sdl.c by keeping both the window enter/leave cases and the new wheel case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:21:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 640990afcc The setup window starts with the last install folder, kept in a per-user file
An update needs only the install folder: the recorded game language and the other inputs come from totk-install.json. installer/recent.py stores the folder in %LOCALAPPDATA%\TotK Recomp\setup.json (XDG state dir elsewhere), outside the release folder, so a new release finds it.

Idea from #715 by DanielSvoboda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:14:23 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f9afb9b565 Merge PR #760: the mouse works in the game's menus on the SDL input backend, Linux only (Avraham)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:04:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9068d63847 Merge task/prqsmall: Recall reticle on non-16:9 screens, Linux fiber stack alignment, DPI-aware setup window (#685, #762, #602)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:57:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 49de824008 Merge PR #602: make the setup window DPI aware so Windows does not stretch it (ethanblazkowicz)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:56:52 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f2a59e92d6 Merge PR #762: align the stack of Linux fibers to the System V ABI (deadlynch)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:56:33 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e5f5d1ff97 Merge PR #685: keep the Recall reticle and gauge over their target on wide and tall resolutions (Takeya Saito)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:56:33 +03:00
Eric Dalloandeca-agent b672c6498f Raise the size table entries of the files inside the mods' packs, whose heaps ran out when the game loaded them
The game loads each member of a pack (Pack/**/*.pack.zs) as a resource of its own, in a heap sized by that member's
own entry in the resource size table. The port raised the entries of the mods' loose files only, so a mod that changed
a file inside a pack and shipped no table for it left the game's entry. A crash when heavy snow started in Hebra was
that: an .ainb member of about 13 KB loaded into a heap sized for the game's smaller copy, 0x48 bytes stayed free after
the file, the frame heap gave null for the 0x68-byte object the resource builds next, and main+0x21794c stored that
object's vtable at address 0.

restbl_overlay now unpacks each pack a mod serves and the game's copy of it with the dictionaries of
Pack/ZsDic.pack.zs, and raises each member's entry as it does a loose file's: by the growth for a member the game's
copy has, by the estimate of its type for one it lacks. It never lowers an entry, a mod's own table included, and the
log names the first members it raised. --probe-mods checks restbl_pack_raise on archives of its own, and docs/mods.md
says that the files inside packs count too.

🤖 Generated with [ECA](https://eca.dev) (nubank-anthropic/opus-5.5 - max)

Co-Authored-By: eca-agent <git@eca.dev>
2026-11-05 10:49:55 -03:00
Valiantsin DzerakhandClaude Opus 5.5 0544f225f0 Merge pull request #646: a finalized texture slot follows only a live entry of its own texel format (mrtink125792)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:37:18 +03:00
Valiantsin DzerakhandClaude Opus 5.5 beda188776 Drop released textures the game has overwritten when VRAM runs short.
Under pressure only clean mirrored entries could go, so once those ran out the render targets stayed: 1.7-2.0 GiB of released images with the use still over the line. Most of them sit on storage the game has since given to another texture, and they are never taken back. The pressure path now drops those first, 30 s after their release and when no finalized slot has bound them for 600 submits. Clean entries wait 5 s, which ends the re-creation churn in fights, and both kinds stop once the use is back under the line.

TOTK_TRACE=perf gains mem: released lines that split released memory by kind, age, overlap and dedicated memory, and count take-backs and re-creations. TOTK_VRAM_LIMIT_MIB caps the budget the port sees, so the path can be driven on a large card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:53:17 +03:00
Tempus3214andClaude Opus 5.5 8235ea8d34 Fix the crash when the camera turns vertically while climbing a creature
The ClimbObj pitch hook replaced the pre-indexed load ldr s1, [x22, #0x88]! and wrote the write-back to x22 from the
host call. The lifter keeps X19-X29 in locals across a call and does not reload them, so the write-back was lost and
the following str s0, [x22] wrote the pitch over the camera object's vtable pointer; the next virtual call crashed
(seen climbing a Stone Talus). The mov x22, x19 before the load is now patched to add x22, x19, #0x88, the hook
performs a plain ldr s1, [x22], and load hooks no longer support write-back loads at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 05:43:25 -03:00
Valiantsin DzerakhandClaude Opus 5.5 627e560648 Merge pull request #743: keep uniform updates inside a pass as scratch overlays and copy them at the pass end (ApereLieZ)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:21:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c269d05bc9 Merge task/fsstart: the game goes fullscreen at start again and the swapchain follows (#738, #766, #622)
With fullscreen = 1 and a 1280x720 window on a 1920x1080 display, dev stays at 1280x720 and this branch reaches
1920x1080. A temporary log in build_swapchain showed the swapchain rebuilt at that size after the switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:20:27 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ea8fc2f6bc Merge pull request #622: keep a window resize that arrives while the swapchain is built (ipavlin98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:08:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 988d8c0630 Merge pull request #766: go fullscreen at start again when the window is raised (Wintego; deanzx06 found the same fix in #718)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:07:47 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9712658328 List the settings totk.ini.example leaves out in docs/settings.md
146 of the 254 names in known[] (runtime/cfg.c) were described nowhere for a reader: diagnostics, test hooks, paths and
experiments. docs/settings.md gives each one what it does, the effect of changing it and a safety level. The rows come
from reading the code, and most effects have not been checked in a run.

tests/settings_doc_check.py fails when a known name is in neither file, when the page lists an unknown name, or when a
default on the page differs from metas[] or an rt_knob call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:18:50 +03:00
Tempus3214andClaude Opus 5.5 21c65ec065 Zoom the map with the scroll wheel
On the map the mouse moves the cursor, so it had no way to zoom: the zoom is the right stick's y, which only the
keys and a pad reached. While the map is open, each wheel notch is now a full-tilt push of that stick for 100 ms with
50 ms of neutral after it (a turn away from the player zooms in). A fast scroll's notches wait their turn, at most four,
and a turn the other way drops them. The wheel stays unbound elsewhere.

The back ends count the notches while the cursor is captured (input_win.c from raw input's usButtonData, input_sdl.c
from SDL_EVENT_MOUSE_WHEEL), keeping a high-resolution wheel's part of a notch (ikb_wheel_take).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:48:13 -03:00
Tempus3214andClaude Opus 5.5 a1cd122625 Give the mouse the same degrees per count in every player camera, and fix the soothe button and the mouse shake
Only the free camera took the mouse as degrees. The other camera modes took it as stick input, which their own speeds,
frame time, slow motion and curves reshaped, with the on-foot camera's square-root pitch shaping on top: bow aiming was
far too sensitive vertically on slow movements, abilities and the Purah Pad camera eased their pitch, lock-on turned
27% slower, and climbing, shield surfing and the rest each turned at their own rate.

Each camera mode's update now loads the gamepad's part of the stick where it computes its turn and adds the mouse's
degrees where it adds that turn to its angle (runtime/mouse_direct.c camera_load, the load sites of
recomp/patches-v143.txt): aiming, scope, abilities (and Recall's cursor), horse, Purah Pad camera, lock-on, bow
lock-on, wall and object climbing, the waterfall's turn, shield surfing, wakeboard and the dialogue cameras. Recall's
cursor moves so that its aim turns as many degrees a count. The waterfall's height and the horse's empty lock-on are
not angles and stay stick input, now linear. A step whose mouse motion no hook took is logged with the camera's name.

Holding L to soothe the horse gave the mouse to the ability wheel's virtual cursor, a held stick for the camera: the
mouse goes to the wheel and the D-pad strips only while their screen has the focus (menu_cursor.c
menu_cursor_shortcut). A mouse shake no longer plays a 0.8 s swing after it: the right stick follows the mouse's own
swings and lets go when it rests, and the camera does not turn by that swing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:36:51 -03:00
Wintego 11d404930d Default the game resolution to the display's shape 2026-11-05 08:31:30 +03:00
Wintego d531e96d35 Go fullscreen at start again when the window is raised 2026-11-05 08:30:33 +03:00
AvrahamandGLM-5.2 2ed9f15b8d The mouse works in the game's menus on the SDL input backend too
menu_cursor.c reads the pointer's place through input_wsys.h, which the
Win32 back end fills (GetCursorPos, SetCursorPos, raw-input travel) and
the SDL one stubbed, so on Linux a right click still went back (B is
injected before the place is read) but no item took the focus under the
pointer and a left click confirmed nothing.

input_sdl.c now keeps the pointer's place from the window's own mouse
events, as fractions of its size packed in one atomic word the game
thread reads; entering the window seeds it, and leaving or losing the
focus drops it. ikb_pointer_set warps through the window thread, as SDL
asks for its video thread, and the warp target stands as the place where
the compositor does not echo the warp. Mouse motion also counts towards
the device used last (ikb_mouse_travel), so moving the mouse alone
brings the pointer into the menus after a gamepad.

Co-Authored-By: GLM-5.2 <noreply@z.ai>
2026-11-05 01:16:19 -03:00
deadlynchandClaude Opus 5.5 7544cfd528 Align the stack of Linux fibers to the System V ABI
plat_fiber_entry is reached by a ret with rsp at top - 8 and called the fiber function without realigning, so the
function started with rsp = 0 mod 16 and every 16-byte local was off by 8. The VEX instructions of a v3 build
tolerate that; the SSE ones of a v2 build fault on it, and probe-kernel's fiber test crashed in same_regs on pcmpeqb
with a memory operand. plat_fiber_entry now aligns rsp to 16 bytes before the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:07:45 -03:00
Tempus3214andClaude Opus 5.5 27f64d6a29 Check the camera's player-state pointers against the guest layout instead of asking the OS
player_state follows about fifteen pointers from the player root to its action verbs, and each was checked with
plat_mem_readable, a VirtualQuery. That query costs tens of microseconds in this process, so the read stalled the
game thread while the mouse moved. mem_guest_readable answers from the layout for the committed heap and the loaded
modules, where the player and its components live, and asks the OS only for anything else. Together with the same
change in the menu pointer, a Ryzen 5 4500 runs the scene that fell to 50-54 fps at 58-60 fps again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:22:10 -03:00
Tempus3214andClaude Opus 5.5 5b4b300a1e Check the menu pointer's game pointers against the guest layout instead of asking the OS
The map detection reads every open screen of eui::ScreenMgr every 100 ms, and each pointer it follows was checked
with plat_mem_readable, a VirtualQuery. That query costs tens of microseconds in this process, so each scan stalled
the game thread for milliseconds: on a Ryzen 5 4500 the mean frame rate fell from 59 to 50-54 fps and the 1% low from
44 to 23 fps. mem_guest_readable answers from the layout for the committed heap and the loaded modules, where the
game's objects and globals live, and asks the OS only for anything else. With it the same scene runs at 58-60 fps,
1% low 45-48 fps, as without the map detection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:21:45 -03:00
Tempus3214andClaude Opus 5.5 27f4d40e80 Detach a fused part by shaking the mouse while Ultrahand holds an object
The game detaches a fused part when the right stick swings left and right a few times. In direct mode the mouse never
reaches that stick, so shaking the mouse did nothing and only the shake key worked. A shake of the mouse while
Ultrahand holds an object (at least three changes of direction within 0.6 s, each swing worth 10 degrees of camera turn
or more) now plays the shake key's right-stick swing. Turning a held object back and forth at an ordinary pace takes
longer and does not count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:01:33 -03:00
Tempus3214andClaude Opus 5.5 55083b69b3 Give the direct mouse one turn rate in every camera and stop the pitch springing back after an aim
The free-orbit camera turned 0.08 degrees per count while the other cameras (the bow, the abilities) took the mouse as
0.01 stick units a count, about 0.023 degrees for yaw and through the camera's signed-square pitch for y, so the bow
turned at half the speed and small vertical moves barely moved it. Every camera now turns 70/1500 = 0.0467 degrees
per count: the stick path gets 0.02 a count for x and the signed square root for y, so pitch is linear and as fast as
yaw. While the player uses an ability (the action verb UseSpecialPower, read from the player actor with the 1.4.3
placement), the ability camera's own linear scales are undone instead. stick_sens is divided out, Ultrahand's held
object gets the camera's stick value a count (at most three full tilts a step), and mouse_aim_scale (0.1 to 3)
scales the mouse while the bow is drawn.

The game's lock-on, recenter and aiming override released the mouse's pitch hold and restored the target from before
it, so after a shot the camera sprang back to that pitch, or to the game's default target. The mouse now lets go
without restoring, and when the override ends (or the camera went 100 ms without a step) it holds the pitch the
camera has then. A gamepad's vertical input still hands the pitch back to the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:47:45 -03:00
Tempus3214andClaude Opus 5.5 366150cba4 Keep the menu pointer through a tab switch and move the map's cursor with the mouse on 1.4.3
A tab switch or a popup opening drops the game's focus box for a frame or two. The menu pointer read that as the menu
closing: the mouse camera captured the cursor again and, when the next tab took the focus, the pointer came back in the
middle of the window. A focus gap of up to 300 ms now keeps the pointer where it is.

The map has no focus box, so it stayed gameplay and the mouse drove the right stick, which zooms the map. The open
screens in eui::ScreenMgr's list now find the map (the Purah Pad's frame with AppMap_00 and the menu background; the
Scope opens AppMap_00 without the background), and there the mouse is the left stick, which moves the map's cursor.
The list is read every 100 ms, as each screen costs memory queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:43:41 -03:00
Claude 9069c9208f Keep uniform updates inside a pass as scratch overlays and copy them at the pass end
nvnCommandBufferUpdateUniformBuffer between draws was replayed as a buffer copy at that point, which needs a barrier
against the draws that read the old bytes, and a barrier ends the open pass. The terrain and the sky islands' meshes
update before most of their draws: about 200 updates inside passes in a field submission, each a pass end, a full
barrier and a pass begin.

An update inside a pass now goes to the scratch buffer only. Later bindings of a range it covers bind the scratch
bytes (ub_bind, from ubo_info and bind_range); a binding it covers only in part, or at an offset the alignment
refuses, gets a merged copy of the snapshot or guest memory with the submission's updates on top. The copies into the
real buffers are recorded behind one barrier of their own at the next full barrier (vk_exec_ub_flush from
barrier_to). A new update of a range replaces the older overlay and pending copy of it, so the lists stay as short as
the blocks. Copies, fills and storage writes of the same bytes retire the overlays they touch, and a storage binding
over a pending copy brings the copies forward with a pass restart. bind_sig hashes the overlay each of the draw's
uniform and storage bindings reads, so a draw of an updated block binds the newest bytes and the others still skip
their descriptor build. TOTK_UB_OVERLAY=0 copies at once as before; ab alternates; TOTK_TRACE=perf prints the
counts.

1.4.3, RTX 4070 Ti, 2560x1440 DLAA, standing still on Great Sky Island, TOTK_AB=10 (tools/ab_summary.py):
17.03 ms a frame with the copies at once, 14.39 ms with the overlays (-15.5%, 58.7 to 69.5 fps); replay time on the
CPU 8.81 to 8.30 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-04 23:54:02 +00:00
Valiantsin DzerakhandClaude Opus 5.5 101132a6c8 Build the Linux release again after build_sources gained its profile argument
282130a made make_release.build_sources take the release's profiles and left the call in make_release_linux.py with
one argument, so the Linux release stopped with a TypeError. The Linux setup compiles without a profile, so it passes
none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:16:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 da6f5b0151 Merge task/wbmerge: shader writes reach guest memory word by word, so falling rain and waterfall splashes draw in mirror mode (#91, #528)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:09:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8d349fa50d Merge branch 'dev' into task/wbmerge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:55:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 33d3645d90 Reuse the write-back merge's snapshot buffers instead of creating them per submission
A VMA allocation that needs a new memory block costs 8-11 ms on the replay thread, so finished submissions now return their chunks to a pool that chunk_new draws from. Chunk sizes are powers of two so that one fits the next request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:55:36 +03:00
Valiantsin Dzerakh 072cccdb41 Up version 0.3.10 2026-11-04 19:10:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a64904dfc8 Merge task/flushhull: flushes of a mirrored pool copy exact ranges, not one span over the pool (#698)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:42:34 +03:00
Valiantsin Dzerakh be94fe0aa9 Merge branch 'dev' into task/flushhull 2026-11-04 18:42:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 835310412c Copy a mirrored pool's flushes as exact ranges, so far-apart small flushes stop copying the pool between them
nvn_pool_flushed kept one span from the lowest to the highest flush of a pool and mirror_sync copied all of it. In
Kakariko one 252 MiB pool copied 22,121 MiB for 847 MiB flushed, in spans of up to the whole pool. Over PCIe 3.0 x8
(RX 6600, Ryzen 5 5500, issue #698) a typical span takes about 30 ms of GPU time, which fits the frame spikes there.

A pool now keeps up to 32 sorted ranges and merges the two nearest above that. Writes between ranges still reach the
mirror through the write watch. Over a walk in Kakariko the upload fell from 472.8 to 125.5 MiB and no flush range
reached 16 MiB. TOTK_FLUSH_SPAN=1 restores the span. The mirror summary gains a line with the upload by source, and
--probe-vkexec checks the range list and that two far-apart flushes copy only their pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:41:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 17eddf41cb Merge task/mesazero, function-local variables start at 0 on Mesa drivers only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:11:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9dd50131ca Start function-local variables at 0 on Mesa drivers only
SPIR-V leaves a register read before any write undefined and Mesa folds undefined values freely. The unconditional patch 0014 guarded against that and was taken out in #701 because the whole picture shook with DLSS on NVIDIA. Patch 0016 brings the zero start back behind a zero_locals job flag, which vk_caps_driver turns on for RADV, ANV and NVK. TOTK_ZERO_LOCALS overrides it, and gfx_shader_set_host mixes it into the translator identity so the variant has its own cache keys.

FXAA_FS gains the flag-on hash of the game's FXAA shader, so vk_dlaa_skip_draw knows it on Mesa too. A game run with TOTK_ZERO_LOCALS=1 on an RX 9070 produced that hash; a TOTK_FXAA=0 run logged no skip with either hash, so the switch itself is not confirmed.

All 29736 stored 1.4.3 inputs translate with the flag off and on, 0 spirv-val failures. FXAA hashes to 5c43ae18948d52cb with the flag off and aad42238801d31d8 with it on. Flag-on output matches the old translator byte for byte on a sample of 312 shaders. Not run on a Mesa driver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:05:21 +03:00
Valiantsin Dzerakh b8d5c08cf4 Merge task/hifps: 1.2.1 map cursor and vertical aim follow the frame time (#44) 2026-11-04 17:48:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0e1ddb41e0 Scale the 1.2.1 map cursor and vertical aim steps by the frame ratio
The map cursor, the vertical Ultrahand aim and the vertical gyro aim moved a fixed step per frame, so above 30 fps they ran faster with the frame rate (#44, #394). The new set cursor multiplies each step by the game's own 30 Hz ratio, ported from MacNativeGames 363a6eb and switched by TOTK_HIGHFPS_FIX.

Map pan with the same stick input on 1.2.1: 398 px/s at 30 fps, 1150 px/s at about 105 fps before, 402 px/s after. The Ultrahand and gyro sites were not exercised in game. 1.1.0 is not covered: its module is not available here to check the sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:48:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5e44a5fc5e Merge task/grassfire: write grass maps back at fence waits so fire spreads through grass, and batch the copies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:03:20 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3ae9e8360d Merge pull request #701, translator patch 0014 out so DLSS stops shaking the picture (itaskaev2)
Measured on an RTX 4080 SUPER by the author: shift 0.86 px a frame with the patch, 0.00 without it.

Review note: 0014 alone moves the SPIR-V hash of the game's FXAA shader from 5c43ae18948d52cb, which is in FXAA_FS
(runtime/vk_dlaa.c vk_dlaa_skip_draw), to aad42238801d31d8, which is not. With the initializers removed and the id gaps
closed, the 0014 build's shader disassembles identically to the pre-0014 one. So since c0e66fe the FXAA list matched
nothing, and this merge makes it match again, which also brings back fxaa = 0. The hash miss is the likely cause of the
shake rather than a driver reaction to OpConstantNull, but the path from the missed skip to the screen is not traced and
no RTX run checked it. Not run here: translator build, game run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:00:45 +03:00
Valiantsin Dzerakh 74058ac053 Merge branch 'dev' into task/grassfire 2026-11-04 16:49:47 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1a9d089a3d Write grass maps back at fence waits so fire spreads through grass, and batch the copies
TotK's fire spread reads its 260x260 RG8 grass maps after a fence wait with no invalidate. The fence write-back took targets up to 64 KiB only, so the CPU saw stale maps and fire died where it hit (#132).

A target over wb_fence_limit now goes back unless the GPU drew it in each of the last 16 frames. Raising the limit instead pulled in four 256x256 targets drawn every frame, about 2.7 ms a frame.

Every level and layer of every texture in a write-back now shares one upload batch and one wait. A burst of 9 grass maps took 7.8 ms with one round trip per level and takes 2 to 2.5 ms. The fence write-back on the field replay fell from about 45 to 22 ms per second of play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:49:46 +03:00
itaskaev2andClaude Opus 5.5 3fab752a21 Take out translator patch 0014 so DLSS stops shaking the picture on NVIDIA
Patch 0014 gave every function-local variable of a translated shader an OpConstantNull initializer. On an RTX 4080
SUPER with game 1.4.3 that made the whole picture move by about 0.9 output pixels a frame whenever DLSS or DLAA runs,
with a still camera (#612). The shake follows the DLAA jitter (TOTK_DLAA_JITTER=0 removes it) and goes away with the
v0.3.7 runtime and a translator built without this one patch. Patches 0012, 0013 and 0015 stay.

docs/maxwell.md 3.12 now says that registers read before any write have no fix for now; a zero start for Mesa only, or
for the shaders that need it, is still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:35:46 +01:00
Valiantsin DzerakhandClaude Opus 5.5 6c19856136 Merge task/fuseicon: wait for the pipelines of item icon captures instead of skipping their draws
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:33:19 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a248cc9f54 Wait for the pipelines of item icon captures instead of skipping their draws
The icon capture draws into a colour target rendered for the first time, with the per-frame depth buffer. Under
pipe_skip any and fullscreen the recent depth made the pass look per-frame, so draws with a pipeline still building
were dropped and the icon kept missing parts (a fused weapon without its blade) until the item was captured again.
vk_exec_targets_recent now ignores a recent depth target while a colour target is rendered for the first time.

On fight2 with a warm driver cache no draw waited. With a cold cache the first world frame after loading waits for
the scene's new HDR target (2.9 s without prepared shaders), and one 64x64 capture waited 29 ms in the fight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:33:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2201a4f1bc Merge task/pinleak: free texture pin copies once no recording names them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:13:23 +03:00
Michael Anthony Bitoon cd32de5ff9 Keep DLSS off the inventory screen and on at every camera angle, with the scope or the camera rune open
The inventory's preview light target is drawn without depth before the bloom chain; a pass like that with nothing armed ends the scene once the bloom follows, so DLSS no longer runs on the finished menu picture. Arming on that target stays allowed, because at some camera angles the field's effects composite comes before the first pass with the scene depth. The G-buffer width is the widest of the frame, a small pass ends the scene only into a scene colour target, and no pair narrower than the widest G-buffer arms: with the scope open the half-size effects pass armed and DLSS scaled the effects into the light target.
2026-11-04 20:01:27 +08:00
Valiantsin DzerakhandClaude Opus 5.5 a1964a0c31 Free texture pin copies once no recording names them
Every texture initialization got a 128-byte host copy for recordings to point at, kept for the whole run. The game
initializes its transient targets again every frame (about 29 new copies a frame on 1.4.3), so host memory grew by
about 44 MiB per minute of play, standing still too, until the commit limit ended long sessions (#670).

Each pin now counts the recording copies whose ops name it: counted in flatten_recording, uncounted when a copy or a
per-submission clone is freed. A pin with no copy left is freed NVN_PIN_AGE (1200) submits after its last use, so a
recording still in progress keeps its pins, and never while the replay holds it as a current render target, which
carries over to the next submission and is compared by pointer. TOTK_PIN_SWEEP=0 keeps every copy.

A 300 s standing-still run on the field save: host memory without VRAM grew 5 MiB, against about 220 MiB before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:00:36 +03:00
Valiantsin Dzerakh d5e5a454ce Merge branch 'dev' into task/wbmerge
# Conflicts:
#	runtime/cfg.c
2026-11-04 14:09:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 681ce0cb3b Merge task/relsrc: ship every install-time runtime source in the Windows release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 12:14:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 38e2ae1b15 Ship every install-time runtime source in the Windows release
make_release.py copied a hand-written list of the sources build_game.py compiles at install time and missed
mouse_direct.c, so the 06005d4 installer failed to link with undefined input_direct_mouse_* symbols. It now copies
build_game.INSTALL_SIDE, and build_game.py --runtime-lib stops with the missing file names before the link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 12:12:52 +03:00
Valiantsin DzerakhandClaude Opus 5.5 06005d40d8 Merge task/pgo: build the game code with an execution profile from the release
1.4.3 profile-guided build: fight2 -12.5% per frame, a fight outside the training -5.8%. The profile lives in
build/pgo (not in the repository) and ships in the Setup's data/; installs without one build at -O1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:08:02 +03:00
Valiantsin DzerakhandClaude Opus 5.5 282130a128 Build the game code with an execution profile when the release has one
build_game.py --pgo gen builds an instrumented totk.exe (llvm-mingw: MSYS2's clang has no profile runtime) that adds
its counts to <out>/pgo/*.profraw every minute and at exit; plat_exit ends the process with ExitProcess, which skips
compiler-rt's atexit write. tools/pgo.py merges them into build/pgo/pgo-<ver>.profdata with a record of the recomp/
tree it fits. build_game.py --pgo FILE compiles the generated code at -O2 and the runtime with that profile.

make_release.py ships build/pgo's profiles in data/ and builds the runtime libraries with them; the installer passes
the update's profile to build_game.py, and builds at -O1 without one. Profiles name lifted functions by address and
hold branch counts and control-flow hashes of the generated C, no bytes of the game.

1.4.3 on machine A, mean frame by world clock: the fight2 bench 11.49 -> 10.06 ms (-12.5%), a fight outside the
training 13.00 -> 12.24 ms (-5.8%); every window CPU-bound. The profile was trained on scripted runs and 24 minutes
of play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:04:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4b712a83e4 Log the port and game version at the top of totk.log
Two reports today attached a log from another build than the one they were about: #659 a 0.3.4 log for a 0.3.7
regression, #683 a pre-0.1.6 log from a folder named after the 0.3.7 setup. Telling that needed matching log
strings against release tags. The line follows mem_init, the first point where rt_log may run, so it is the second
line of the file after "cfg: read".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:58:35 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7be68ec5e1 Turn replay_async off by default again
83d2302 turned it on from one measurement where the game waits for the GPU (a GTX 1650 in the Depths, #88).
Nobody checked frame pacing at the 60 fps cap with GPU headroom or on a 6-thread CPU. #659 (GTX 1660 Ti,
i5-9400F, GPU at 26 %) reports uneven motion on 0.3.7 at a steady 60 fps that 0.3.4 did not have, and this
is the only change in the frame path between the two releases that reaches that machine. Players it helped
set replay_async = 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:50:38 +03:00
Takeya SaitoandClaude Opus 5.5 d3f841a737 Keep the Recall reticle and gauge over their target on wide and tall game resolutions
At a game resolution wider than 16:9 the ability reticle and the Recall progress ring sat between their target and the
middle of the screen (at half the distance on 32:9). Both are moved in code to a point in the 1920x1080 grid on a pane
under the root, so the root's scale k scaled their position too, like the markers in MARKER_ROOT.

Pointer_00 and ModorecoGauge_00 join MARKER_ROOT:
- Pointer_00: the code moves N_App_00. Its children's scales are animated, so k goes on N_App_00 itself (a pane's
  own scale doesn't scale its translate), and on the centred bow and throw pointers N_Arrow_00 / N_WeaponThrow_00.
- ModorecoGauge_00: the code moves N_Trans_00 and sets its scale by distance, so k goes on the ring's pictures. They
  ship mirrored at -1 and are turned only 0 or 180 degrees, so apply() now accepts a shipped scale of -1 and keeps
  its sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 07:43:34 +00:00
Valiantsin DzerakhandClaude Opus 5.5 06c5de23fc Merge task/kbm: keyboard and mouse like a PC game on 1.4.3
Direct mouse camera (#558, default on 1.4.3), mouse in the game's menus (#605), keyboard and mouse prompts
(#585, prompts = pc, in place of #483), a shake key (G) for detaching fused parts. The gamepad path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:26:51 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9e5a817916 Read the mouse mode with its default so direct is on without a totk.ini line
direct_mode read TOTK_MOUSE_MODE with cfg_get, which is NULL when the player never set it, so a fresh totk.ini kept
stick emulation (and its speed cap) while the menu showed Direct. cfg_value gives the settings table's default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:52:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5ae81ba8e6 Fix the left stick HUD images of the PC prompts and draw the shake key
In the 1.4.3 Common archive Nt_KeyTexLSt_00 is the left stick tilted up (U+E052) and Nt_KeyTexLSt_01 the left stick
(U+E05E): they showed WASD and Ctrl, now W and WASD. U+E068 (the right stick shaken) showed the mouse, which never
shakes the stick; it shows the SHAKE key (G by default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:44:55 +03:00
Wintego cb628b08b7 Add keyboard and mouse button prompts (prompts = pc)
The Button prompts row gets a third choice, PC: every button glyph of the game's button font and every image of the
button guide is drawn as the key or mouse button the keyboard defaults press it with (E, Shift, Space, left click, Q,
R, right click, F, Tab, M, 1 to 4, W A S D, Ctrl, C) and the right stick as the mouse, whatever the pad layout. The
setup writes romfs-patches-pc next to the Xbox folders (make_romfs_patches.py --prompts pc, button_icons.py pc_icons);
--binds <totk.ini> draws the keys of its bind_ lines instead. At a game resolution of another shape the game takes
romfs-patches-pc-<w>x<h> (--prompts pc --aspect) in place of romfs-patches-<w>x<h> when it is there.

(cherry picked from commit 3f81cf02be664100bd9c569c53588d620d90b527)
2026-11-04 08:36:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 56b3cd1246 Drop the keyboard prompts of #483 in favour of #585
#483 draws the stick glyphs on the wrong code points: U+E056 and U+E057 (the stick presses) get WASD and the mouse,
U+E05E and U+E05F (the sticks) get Ctrl and C, and both left-stick HUD images show Ctrl. #585 maps them by the game's
font and keeps each glyph's advance.

This reverts commit cd35ffa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:35:35 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3676e299c5 Add a shake key that detaches a fused part held with Ultrahand
G (bind_SHAKE, keys only) swings the right stick left and right for 0.8 s, longer while held, so a part comes off
without shaking the arrow keys by hand. The pad keeps shaking its own stick. Remappable in the Controls tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:35:12 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d0579731c5 Make the direct mouse camera the default and add a Controls row for the menu pointer
mouse_mode now defaults to direct: on 1.4.3 the camera turns by the raw mouse motion; 1.1.0 and 1.2.1 have no hooks
and keep stick emulation, and no setting changes the gamepad. The Controls tab gets a "Mouse in the game's menus" row
for menu_cursor, and the mouse rows are in all four catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:18:29 +03:00
0xEmz cd35ffaf36 Show keyboard and mouse icons in the prompts when prompts = keyboard
Add keyboard and mouse button icons to tools/button_icons.py and
tools/make_romfs_patches.py --prompts keyboard, with a third 'Keyboard
and mouse' choice under Controls > Button prompts (prompts = keyboard).

Keycaps cut their labels out of the game's own text fonts, mouse clicks
and right-stick camera look draw clean mouse silhouettes, and
make_romfs_patches.py --ini reads any bind_ overrides from totk.ini.
2026-11-04 08:11:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ff49f6b62f Keep the gamepad unchanged under the direct mouse and the menu pointer, and fix the mouse activity flags
The camera input's activity hook wrote the flags to +0x418, but the game's next instruction stores w8 over them
(1.4.3 main+0x874d80), so a mouse-only turn still read as idle. The hook now changes w8.

The free-orbit hooks rewrote the camera stick input from the last sample even in steps without mouse motion; they now
take a step only when the mouse moved in it or holds the pitch, so a gamepad-only step reaches the camera as the game
made it.

The mouse shake gesture for detaching parts is gone: three 24-count reversals fire while a held object is turned back
and forth. Arrow keys or the pad's right stick shake as before.

The menu pointer comes only while the keyboard and mouse are the device used last (input_kbm_last): a gamepad player
sees the menus as on the console. Raw mouse travel is counted while the cursor is released too, so moving the mouse
brings the pointer back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:10:59 +03:00
local 8ff29be86f Use the mouse in the game's menus on 1.4.3
While one of the game's menus has its focus box (the title, the save list, the inventory, Options, shops,
dialogue choices), the cursor is released from the mouse camera and shown; the item under the pointer takes
the focus as the d-pad would, a left click is A on it and a right click is B. runtime/menu_cursor.c reads
eui's screens at each pad read and moves the focus with the writes eui::BoxCursorMgr::moveBoxCursor makes,
adapted from Weeping of the Empire's totk_menu_cursor.cpp (1.0.0, MIT), with eui::ScreenMgr's instance placed
in 1.4.3 at main+0x3acf960. menu_cursor = 0 turns it off.
2026-11-04 07:56:03 +03:00
walterwhite69420 050563687b Add experimental direct mouse camera input for game 1.4.3 2026-11-04 07:55:49 +03:00
ProSepandClaude Opus 5.5 3cd241071a Check the movie picture fit against gradients and make it cheaper
The probe now feeds gradient planes and checks bar symmetry, the picture's shape on screen, black outside the picture
and the expected luma and separate U and V samples, with one cleanup path. The timing loop runs only with
TOTK_PROBE_MOVIE_BENCH. In the decoder, a plan made for another frame size is never used, an allocation failure is
logged, only the bar strips are filled with black, and the 3 and 4 tap filters are unrolled, which halves the cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:41:10 +01:00
ProSepandClaude Opus 5.5 8a8ef05241 Keep the shape of the pre-rendered movies on outputs that are not 16:9
The game stretches each decoded movie frame over the whole canvas, so the 16:9 movies were stretched on 32:9, 21:9 and
16:10 outputs. The host decoder now shrinks the picture inside the frame with a box filter and fills the rest with
black, so that the game's stretch gives the picture its own shape back. The new setting movie_aspect (TOTK_MOVIE_ASPECT,
keep or stretch, read when a movie starts) turns it off, and the menu has a row for it next to the picture shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:27:39 +01:00
mrtink125792andClaude Opus 5.5 1c5c6fdbe3 Let a finalized texture slot follow only a live entry of its own texel format
slot_resolve sent a slot whose entry was released to any live entry at the same storage, whatever its
format. The quick menu samples its 768x432 RGB10A2 scene capture through such a slot; after a fuse the
Autobuild thumbnail of the fused object, RGBA8 at the same storage, was the live entry there, and the menu
drew it behind the wheel for its opening frames: the fused object's silhouette, or garbled colours when the
thumbnail image held the capture's raw bytes. Maxwell reads the storage through the slot descriptor's
format, so a follower now needs the slot's texel format (sRGB aside); without one the released entry keeps
the slot, and its image is the capture the game last wrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:53:46 -05:00
ipavlin98 23d24bbc77 Report heap corruption from the vectored handler
The heap manager raises STATUS_HEAP_CORRUPTION and then ends the process itself, so the exception never reaches the unhandled-exception filter, and a run that corrupted its heap ended without a FATAL line. crash_veh hands it to crash_filter, which names the module and the host frames as for any other fault outside the executable.
2026-11-03 23:15:59 +03:00
ipavlin98 6ac2da7e8f Add a frame queue that frees a window image at its display time
nvnWindowAcquireTexture holds the game until the image is free, which was one period after its present's display time, when the next frame reaches the display. The present is a copy queued by then, so with frame_queue = 1 the image is free from that time: the game draws the next frame while the previous one waits for its turn, and a frame that takes up to one period more still makes its slot. In the town at 4K at 60 fps on an RTX 5070, frames over 17.5 ms fell from 25-33 to 1-8 per 30 s, for up to one frame more input delay. Off by default; the menu switches it at once.
2026-11-03 23:13:41 +03:00
ipavlin98 b3f162746d Clear the swapchain's stale mark before reading the surface so a resize during the build is not lost
The mark used to be cleared after the swapchain was built, which dropped a resize that arrived meanwhile. The switch to fullscreen at start lands there on the window thread, and the driver does not report the swapchain as out of date.
2026-11-03 21:38:15 +03:00
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
ethanblazkowiczandDeepSeek V4.1 Flash 1017 315064b3ec Make the setup window DPI aware so Windows does not stretch it
The window ran in a DPI-unaware process, so Windows drew it at 96 DPI and stretched the result on a display at 125% or
more, which is why it was blurry at 200%. The process is now system DPI aware before Tk starts (installer/dpi.py: Tk 9
has no per-monitor support, so system awareness is what it can honour); Tk then sizes the fonts with the display's DPI
and dpi.px sizes the paddings, the wrap widths and the progress bar with it. The launcher makes its own process aware
the same way for its error box. tests/installer_gui_test.py checks that a fresh process ends up aware and that Tk's
scaling follows the display.

Co-Authored-By: DeepSeek V4.1 Flash 1017 (DeepSeek Harness) <noreply@deepseek.com>
2026-11-03 22:25:35 +08: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 Dzerakh 1cd3e3a584 Merge branch 'pr/528' into task/wbmerge 2026-11-03 15:56:15 +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
Michael Anthony Bitoon e2fdb5b226 Merge a shader's storage-buffer writes back into guest memory word by word
In mirror mode the replay copied every range a shader wrote from the VRAM mirror back to guest memory at the end of the
submission. TotK's particle emitters write a new particle's whole record into a free slot of a buffer the simulation
shaders update, so the copy put the mirror's stale words over fresh spawns: the waterfall splashes and mist showed about
once a second and the falling rain drifted. Now each written range gets a snapshot in the prologue of the part that
first writes it, after the CPU-to-mirror copies, and at the end of the submission shaders/wb_merge.comp writes only the
32-bit words that differ from the snapshot. Words the GPU left alone keep what the CPU wrote. About 0.1 MiB and three or
four dispatches a frame in rain. TOTK_WB_MERGE=0 restores the copies. The merge's push descriptors go through rec_push
so they keep their place in the recorder's call order.
2026-11-03 01:19:10 +08: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
175 changed files with 22319 additions and 2278 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.
+36
View File
@@ -0,0 +1,36 @@
<!--
Target branch: dev. Rules: CONTRIBUTING.md, "Pull requests". Write each section in your own words about this diff;
delete the rows that do not apply. Never attach game files, keys, extracted data or logs that contain them.
-->
## Problem
<!-- What the player sees or what is wrong in the code. Issue link if there is one. -->
## Change
<!-- What the diff does and why this way. Name the switch (totk.ini setting or TOTK_* variable) that turns it off. -->
## Where it reproduces
<!-- OS, CPU, GPU and driver, game update (1.2.1 / 1.4.3), settings. Hardware it cannot appear on, if known. -->
## Proof
<!-- Before and after on the same repro: screenshots, log lines, a test that failed and now passes. -->
## Checks run
<!-- The rows of CONTRIBUTING.md "Checks by what you changed" that match the diff, each with its result. -->
- [ ] `python tools/check_content.py --staged`
- [ ] `totk.exe --selftest`
## Measurements
<!-- Frame path, timing or scheduling only. Per run, switch off and on, at least two each: average fps, 1% low or
worst frame, `perf: slow frame` count. Scene, save, seconds, CPU, GPU, driver, resolution. -->
## Not run
<!-- Paths you could not run (Linux, HDR, a vendor you do not have) and how they are gated. -->
+9
View File
@@ -29,6 +29,15 @@ This repository is public. Game data lives outside it and every derived file sta
- Graphics bugs: [docs/maxwell.md](docs/maxwell.md) lists the Maxwell behaviour the Vulkan path must match and the TotK bugs other ports fixed.
- Comments say what the code does and why, in words that stand on their own. Cite our code by file plus function (a line number may follow as a hint) and upstream code by project, file and function: `refs/` moves, so its line numbers rot. Commit hashes belong to the attribution of copied code.
## Running and debugging
- Running the game, screenshots, frame dumps, timings: [docs/agents/running.md](docs/agents/running.md).
- Investigating a bug, glitch or crash: search [docs/agents/symptoms.md](docs/agents/symptoms.md) by the player's words first, then follow [docs/agents/debugging.md](docs/agents/debugging.md). Read the code before running the game.
## Pull requests
Preparing a pull request: target `dev`; the checks by changed area, the proof and the performance runs it carries are in [CONTRIBUTING.md](CONTRIBUTING.md), "Pull requests". Reviewing one: [docs/pr-review-kit/](docs/pr-review-kit/).
## Writing
Code, comments, docs and commit messages are in English. Commit messages are plain sentences without type prefixes.
+86 -7
View File
@@ -29,14 +29,93 @@ Use the issue form. Include the port version and game update from the title line
## Pull requests
- One topic per pull request.
- Describe the problem, then what changed.
- Say how you tested it: which version, which scene, how long.
- A change to the lifter (`recomp/`) or the CPU headers passes the differential tests in [docs/building.md](docs/building.md), "Checks".
- Commit messages are plain sentences, without `feat:` or other prefixes.
- AI-assisted code is welcome. You are responsible for having read and run it.
### Branch
Open pull requests against `dev`. Branch from the current `dev` and rebase on it before you open the pull request. `main` receives `dev` at a release; a pull request against `main` is retargeted to `dev` before review.
### Before you start
- Search the open pull requests and issues on the topic. When another pull request already covers it, improve that one or say in yours how they differ.
- Fixes are reviewed before features, in order of how many players the bug hits. A feature with a design in [docs/milestones/](docs/milestones/) (mods, cheats, the menu) follows that design. For a feature without one, open an issue first.
- Find out where the bug reproduces: OS, GPU vendor and generation, driver, game update, settings. Put that in the description.
### What the change must keep
- **The default for everyone.** A fix for one setup (a GPU vendor, a memory size, a display) is gated on that setup. Players without the problem see no change.
- **A switch.** New behaviour on the frame path or in rendering sits behind a `totk.ini` setting or a `TOTK_*` variable, so a player can turn it off and a reviewer can compare. A new setting is described in [totk.ini.example](totk.ini.example) and [docs/settings.md](docs/settings.md).
- **The fix, not a disabled feature.** A crash that goes away with a feature off points at the bug in that feature. Turning the feature off for a vendor is not a fix.
- **The frame time on weak hardware.** Game threads and the render thread do no file I/O, no GPU allocations, no per-draw state copies and no global atomic counters. Memory kept per frame or per object is freed. The full list with measured costs: [docs/pr-review-kit/02-performance.md](docs/pr-review-kit/02-performance.md), "What the hot path must not do".
- **Other GPUs.** New Vulkan code queries each feature, extension, format and limit it uses and keeps a working path without it. Nothing is hardcoded from what one GPU reports.
- **Text in the catalogs.** Text a player sees gets an id in `runtime/ui_text.h` and an entry in `runtime/ui_text.c` for every menu language (English, Russian, Brazilian Portuguese, Simplified Chinese); installer text goes into `installer/text/*.json`.
- **The generator owns generated C.** Change `recomp/`, never `generated/`.
### Checks by what you changed
Run every row that matches your diff and list the results in the description. Commands and details: [docs/building.md](docs/building.md), "Checks".
| You changed | Run |
|---|---|
| anything | `python tools/check_content.py --staged`; `build\<ver>\totk.exe --selftest` |
| `recomp/`, `runtime/cpu.h`, `runtime/simd.h` | the differential tests against Unicorn: `tests\diff\lift.py`, `simd.py`, `atomic.py`, `func.py`, `tests\flags_check.py`, `tests\excl_mt.py`; the same with a `--cpu v2` build |
| `recomp/jumptables.py` | `tests\jumptables_check.py`: 0 disagreements on 1.2.1 and 1.4.3 |
| `runtime/` | `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`, plus the probe for the file you touched (list at the top of `runtime/main.c`) |
| settings | `totk.exe --probe-cfg`; `python tests\settings_doc_check.py` |
| button prompts | `.venv-py314\Scripts\python tests\romfs_prompts_test.py` |
| mods, ExeFS patches | `.venv\Scripts\python tests\exefs_patches_test.py`; `totk.exe --probe-mods` |
| `installer/` | `python tests\installer_test.py`; `python tests\installer_gui_test.py` |
| shader translator | `tools\xlate_smoke.py`; `tools\shader_census.py --version <ver> --translate` with no new failure groups |
| rendering | a run in a scene where the change acts, with `TOTK_VK_VALIDATION=1`: no new validation messages; screenshots before and after |
| frame path, timing, scheduling | the measurements below |
Checks that need game files run on your own copy of the game. Results go into the pull request as text: pass or fail, counts, log lines. Never attach logs or dumps that contain game data.
### Proof
- **A bug fix** shows the bug before the change and gone after it, on the same repro: a screenshot pair, a log line, a probe or test that fails before and passes after. A scripted repro (`TOTK_INPUT_RECORD`, [docs/controls.md](docs/controls.md), "Scripted input") is the strongest. If the bug cannot be reproduced on your hardware, say so and point at the code that causes it.
- **A crash fix** names the crash: the log's crash report, `vk: pass marks` lines for a lost device.
- **Parts you could not run** (Linux-only code, HDR, vendor-only paths) are listed as unrun and gated away from everyone else.
### Performance measurements
A change on the frame path, in timing or in scheduling carries these numbers, or it waits until someone measures it.
1. **Ceiling first.** Before the work, estimate the share of the frame the changed code holds on the critical path (`TOTK_CRIT` + `tools/crit_path.py`). A ceiling of a few percent is not worth a hot-path change.
2. **Build 1.4.3** with prepared shaders in that build folder (`tools/prepare_shaders.py`, [docs/building.md](docs/building.md) step 6). Caches copied from another build folder do not count.
3. **A heavy scene** where the change acts: a fight with a large enemy and a crowd, or a town, with `TOTK_FPS_LIMIT=360`.
4. **Whole runs, switch off and on, at least two each**, the same save and the same input script, timed by game seconds:
```powershell
.venv\Scripts\python tools\run_game.py build\v143 --script <input.txt> --seconds 60 --quiet --env TOTK_TRACE=perf,sched --env TOTK_FPS_LIMIT=360 --env <SWITCH>=0
```
`--quiet` waits until no other game or build runs. Close everything heavy on the machine. Before reading the numbers, check that each run reached the world and showed the same scene.
5. **Report per run**: average fps, 1% low or worst frame, the number of `perf: slow frame` lines, VRAM; `tools/perf_summary.py` condenses the log. Add CPU, GPU, driver, RAM, resolution and the settings you changed.
A switch toggled inside one run on a moving scene gives false results: compare whole runs. Zero new slow frames is a hard bar; an fps gain does not buy back new hitches. Background and the reasons behind each rule: [docs/pr-review-kit/02-performance.md](docs/pr-review-kit/02-performance.md).
### Description and commits
Fill in the pull request template: problem, change, where it reproduces, checks, measurements, what stayed unrun. One topic per pull request.
Commit messages are plain English sentences, without `feat:`, `fix:` or other prefixes. A commit that works on an issue ends its subject with the issue numbers: `Render the handheld scene at the asked size on 1.4.3 (#998, #536)`.
### Review and merge
The maintainers review every pull request against [docs/pr-review-kit/](docs/pr-review-kit/): read it to see what gets a pull request rejected, which bug classes are already fixed and which invariants a change must keep. A good pull request that misses the project's conventions is fixed up while merging rather than sent back. Part of a pull request can be taken; the reason for what was left out or rejected is posted on the pull request.
Accepted work merges into `dev` and reaches players through test builds. `main` moves only at a release.
## Coding agents
AI-assisted code is welcome. You are responsible for having read and run it, and the pull request says which checks ran on your machine.
Point your agent at [AGENTS.md](AGENTS.md) (Claude Code reaches it through `CLAUDE.md`), this file and [docs/pr-review-kit/](docs/pr-review-kit/) before it writes code. [docs/agents/](docs/agents/) holds what it needs to run the game unattended, measure, and debug: the method, the bug classes of a recompiled Switch game, and a symptom catalog. The rules in them bind the agent as they bind you:
- Key values stay out of prompts, logs, commands and files. The agent passes the keys file path to the tools.
- Results in the pull request come from runs that happened. A check the agent did not run is listed as not run.
- The agent writes the description in its own words about this diff. A review kit checklist pasted in with every box ticked tells the reviewer nothing.
## Code and docs
- Code, comments, commit messages and documentation are in English.
- Generated code is never edited by hand: change the generator.
- Comments say what the code does and why. Cite our code by file and function, upstream code by project, file and function.
+12 -3
View File
@@ -17,19 +17,26 @@ 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)
- Play time across runs on the Game tab of the settings menu, kept in `playtime.txt` next to the saves
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
- Keyboard, mouse and gamepad with remapping, rumble and gyro
- Stereo or 5.1 surround sound output
- Xbox button prompts: LB, RB, LT, RT, Menu and View, and A, B, X and Y as labelled on the button to press, with either button layout
- Shaders prepared at install instead of during play
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
([docs/mods.md](docs/mods.md))
([docs/mods.md](docs/mods.md)). HD texture packs work: the port grows the texture heap a pack asks for by itself. Tested
with the HDPacks.com 4K Texture Pack v1.0 on 1.4.3 and 1.2.1; other packs come with no guarantee
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
## Planned
@@ -44,7 +51,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
+4 -2
View File
@@ -10,6 +10,7 @@ Code copied or adapted from another project is listed here with its source, the
| MacNativeGames TotK host runtime: dynamic linking, identity check, module permissions, guest memory and SVCs, threads, IPC, audio renderer, system services, RomFS and save storage, boot sequence, the NVN proc name list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/runtime/`, `games/totk/scripts/prepare_runtime.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/loader.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/svc.c`, `runtime/ipc.c`, `runtime/ipc.h`, `runtime/audren.c`, `runtime/audren.h`, `runtime/audio_mixer.c`, `runtime/audio_effects.c`, `runtime/audio_effects.h`, `runtime/audio_reverb.c`, `runtime/audio_reverb.h` (SDL3 output replaces CoreAudio), `runtime/ov_system.c` (also `shared_font.c`: the BFTTF layout behind nn::pl), `runtime/ov_input.c` (also `hid_feedback.c`: vibration and six-axis handles and calls), `runtime/motion_fusion.c` (six-axis fusion of `motion_fusion.c`), `runtime/ov_media.c` (hardware Opus decoder over libopus), `runtime/swkbd_win.c` (`swkbd_host.c`: ShowKeyboardArg offsets, UTF-8/UTF-16 conversion, disabled-key filter, result codes; Win32 window replaces the macOS text bar), `runtime/ov_gfx.c`, `runtime/romfs.c`, `runtime/romfs_path.h`, `runtime/fs_host.c`, `runtime/fs.h`, `runtime/boot.c`; `runtime/gfx_window.c`; `tools/gen_nvn_procs.py`, `runtime/nvn_procs.inc` (`NVN/names.txt` and the `NVN_EXTRA` names of `totk_nvn_ext.c`) |
| MacNativeGames TotK frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/totk/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/totk/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
| MacNativeGames TotK 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/totk/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
| MacNativeGames TotK 1.2.1 cursor and aim steps: the map cursor, vertical Ultrahand aim and vertical gyro aim sites, their instruction words and the step scaled by the frame ratio | https://github.com/ly05010419/MacNativeGames @ `363a6eb50434387f7d623349934bcf1e910be48f` (`games/totk/runtime/game_patches.c` patch_cursor_speed), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v121.txt` (set cursor) |
| MacNativeGames TotK aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/totk/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
| MacNativeGames TotK button icons: the Xbox controller's shoulder buttons, triggers, Menu, View and d-pad drawn with letters from the game's own text fonts, the .bfotf key, the button guide's images and their block-linear layout, the resource size table raised for the archives that grew | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/scripts/make_button_icons.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/button_icons.py`, `tools/make_romfs_patches.py` (--prompts) |
| RenoDX TotK mod (game mod by souperman9, tonemapper by Musa, RenoDX framework by ShortFuse, RenoVK fork by Ritsu): the approach of the HDR tonemap (the game's 1 - e^-x curve kept below a pivot and continued along its tangent above it, the saturation grade applied to the colour scaled into 0..1) and the observation that the passes after the tonemap keep values above 1. No code copied; the shader is our own | https://github.com/clshortfuse/renodx @ `b6f87272ad1bfdc0e6444910a93606815944a085` (`src/games/totk/tonemap.slang`, `addon.cpp`), Copyright (c) 2025 Carlos Lopez Jr. | MIT | `runtime/shaders/hdr_tonemap.frag`, `runtime/vk_hdr.c` |
@@ -26,7 +27,7 @@ Code copied or adapted from another project is listed here with its source, the
| deko3d block-linear image layout, as ported in MacNativeGames' `nvn_tex.c` (Apple parts dropped, layout math unchanged; licence text in `games/totk/third_party/d2r_gfx/LICENSE.deko3d`, reproduced in the file header) | https://github.com/devkitPro/deko3d (`source/dk_image.cpp`), via MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/nvn/nvn_tex.c`, `nvn_tex.h`), Copyright (C) 2018-2020 fincs | zlib | `runtime/nvn_tex.c`, `runtime/nvn_tex.h` |
| MacNativeGames texture auto-layout goldens (40 original-SDK size and block-height cases) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/tests/texture_auto_layout/probe.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/nvn_tex_probe.c`; `runtime/nvn_probe.c` (four of the cases, through the procs) |
| MacNativeGames block-linear deswizzler (address function; copy loop split out as `tex_bl_copy`) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/tex/nvn_deswizzle.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/tex_deswizzle.c`, `runtime/tex.h` |
| SDL 3.4.18, dynamic link, `SDL3.dll` copied next to `totk.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.18-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
| SDL 3.4.20, dynamic link, `SDL3.dll` copied next to `totk.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.20-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
| libopus (MSYS2 `mingw-w64-ucrt-x86_64-opus`), dynamic link, `libopus-0.dll` copied next to `totk.exe` | https://opus-codec.org, Copyright (c) 2001-2023 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon | BSD-3-Clause | `runtime/ov_media.c` (nn::codec hardware Opus decoder) |
| Dear ImGui 1.91.9 (core and Vulkan back end, unmodified), as vendored by LAS Recomp | https://github.com/ocornut/imgui, via https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f` (`third_party/imgui/`), Copyright (c) 2014-2025 Omar Cornut | MIT | `runtime/third_party/imgui/` (compiled into `totk.exe` for the in-game menu, `runtime/ui/`) |
| SavageWindRecomp settings menu: two-pane option rows (row look, left / right stepping, hold speed-up, description pane), the menu style and tab bar | https://github.com/Revivedx/SavageWindRecomp @ `575ad88e02f9a89a9a885d90039d48af33c982f4` (`runtime/ui/option_rows.mm`, `runtime/ui/menu.mm`), Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai | MIT | `runtime/ui/option_rows.cpp`, `runtime/ui/option_rows.h`, `runtime/ui/menu.cpp` |
@@ -72,7 +73,7 @@ These tools run on the developer's or player's machine during the build. They ar
| CPython 3.14.9 embeddable package, with `tkinter` and Tcl/Tk 9.0.4 taken from the python.org 3.14.9 installer's component MSI (unpacked, never run); includes `vcruntime140*.dll` (Microsoft, redistributable with Python), OpenSSL, libffi, SQLite, zlib, libtommath | https://www.python.org/downloads/release/python-3149/ | PSF-2.0 (Tcl/Tk: Tcl/Tk licence, BSD-style) | `python/` |
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), fontTools 4.68.0, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; fontTools MIT; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
| llvm-mingw 20261020 (LLVM/Clang/LLD 23, mingw-w64, libc++, libunwind, compiler-rt), x86_64 subset | https://github.com/mstorsjo/llvm-mingw/releases/tag/20261020 | Apache-2.0 with LLVM exception; mingw-w64 ZPL-2.1/BSD/public domain; libc++ and libunwind Apache-2.0 with LLVM exception and MIT/UIUC; winpthreads MIT | `llvm/` |
| SDL 3.4.18 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.18 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
| SDL 3.4.20 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.20 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
| libopus 1.6.1 (MSYS2 `mingw-w64-ucrt-x86_64-opus` 1.6.1-2; `libopus-0.dll` imports only KERNEL32 and the UCRT api-ms-win-crt DLLs), headers and import library | https://repo.msys2.org/mingw/ucrt64/ | BSD-3-Clause | `bin/libopus-0.dll`, `runtime/deps/` |
| libvpx 1.17.0 (MSYS2 `mingw-w64-ucrt-x86_64-libvpx` 1.17.0-1), headers and static library compiled into `totk.exe`; `libvpx-LICENSE.txt` and `libvpx-PATENTS.txt` come from the v1.17.0 source tag | https://github.com/webmproject/libvpx | BSD-3-Clause with the WebM patent grant | `runtime/deps/`, `licenses/` |
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd` 1.5.7-2), headers and static library compiled into `totk.exe`; `zstd-LICENSE.txt` from the package | https://github.com/facebook/zstd | BSD-3-Clause | `runtime/deps/`, `licenses/` |
@@ -103,3 +104,4 @@ folder's `licenses/` and in each component's own folder.
| `totk_xlate` (NativeAOT linux-x64 build of `tools/xlate/`, linked with the folder's clang, lld and sysroot) | built by `tools/build_xlate.py --llvm --sysroot` | MIT | `bin/totk_xlate` |
| AppImage type-2 runtime (static; includes libfuse and squashfuse), the AppImage's first part; appimagetool packs the folder and is not distributed | https://github.com/AppImage/type2-runtime | MIT; libfuse LGPL-2.1; squashfuse BSD-2-Clause | the AppImage header |
| MacNativeGames shader variant list | as in the installer release row above | MIT | `data/shader_list.tsv` |
| Weeping of the Empire menu cursor: eui's focus box read through ScreenMgr, BoxCursorMgr and the screens' node lists, AnimButton's hit test, the stale-hit-pane fallback to the parts panes, the three writes of moveBoxCursor, the structure offsets (1.0.0; the ScreenMgr instance placed in 1.4.3 by us) | https://github.com/daniel-madrid-07/weeping-of-the-empire @ `df0586cdc5702ca19bbebfa59697e0b6980f6237` (`engine/runtime/totk_menu_cursor.cpp`), Copyright (c) 2026 daniel-madrid-07 | MIT | `runtime/menu_cursor.c` |
+1 -1
View File
@@ -1 +1 @@
0.3.4
0.4.0
+117
View File
@@ -0,0 +1,117 @@
# Debugging the port: method for agents
A game run costs minutes and a full rebuild costs several more, while reading code costs seconds. Order the work so the
cheap steps decide as much as they can before the game starts. Each step ends on its stated condition.
## Steps
1. **Evidence.** Hold one artifact that shows the symptom: the player's screenshot, log or input recording, a
flight-recorder dump, a frame dump ([running.md](running.md), "Observing"). Done when you can point at the wrong
pixel, line or frame.
2. **Where it reproduces.** OS, GPU vendor and generation, driver, game update, settings, mods, the save. Done when you
know whether your hardware can show the bug at all. NVIDIA bugs do not reproduce on AMD; RDNA1/2 bugs do not
reproduce on RDNA3/4 (another driver branch); a Ryzen 7000 iGPU is RDNA2 and runs that branch. When your hardware
cannot show it, say so and work from the code and the reporter's logs; runs that cannot decide anything are skipped.
3. **Known already?** Search [symptoms.md](symptoms.md), [pr-review-kit/04-knowledge.md](../pr-review-kit/04-knowledge.md)
and [maxwell.md](../maxwell.md) sections 6-7 with the words the player used (black, magenta, flicker, slow motion,
missing, stale, streaks, stuck) and the object (sky, grass, shadow, fog, HUD, weapon). Then the upstream notes:
`refs/nxnative/docs`, `refs/MacNativeGames/docs` and the SavageWindRecomp issue tracker (`tools/fetch_refs.sh`
fetches `refs/`). Done when every hit is either ruled out or is your first hypothesis. A hit on a class we already
fixed: confirm that fix still holds before deriving anything new.
4. **Read the code.** The game's function in Ghidra ([building.md](../building.md), "Reverse engineering") or in
`generated/<ver>/`, and our runtime path in `runtime/`. Done when the code either rules the hypothesis out or names
the exact condition a run must show. A run confirms what the code could not rule out.
5. **The cheapest falsifying check.** An existing switch (a `totk.ini` setting or a `TOTK_*` variable), a dump, a trace
line, a probe. When it refutes the hypothesis, stop: write down what was checked and why it fell, and start again
at step 3 with the next hypothesis. No build, no benchmark for a refuted idea.
6. **Repro red, then green.** The same repro shows the bug before the fix and its absence after. A fix without a
reproduced failure is a guess.
7. **Fix the cause.** A crash that goes away with a feature off points at the bug in that feature; fix the feature.
A fix that one setup needs is gated on that setup, and everyone else keeps today's behaviour.
## Traps
- **One-off failures.** A failure that happens once and not again: rerun before you debug it as a code bug. A crash or
a forced shutdown leaves zero-filled cache files, and memory errors on the host flip bits in files and objects.
- **Every build fails the same way at once.** Suspect the inputs and caches (game files, the OS file cache, shader and
pipeline caches), not the code. Bisecting code is pointless when the old builds fail too.
- **Compile errors in generated C** on a line the generator could not have written are a flipped bit. Regenerate and
compare hashes; `recomp/` is fine.
- **A crash that looks like a race.** Try the save first: some saves crash the same way on a real Switch.
- **Theories about locks and threads.** Read the game's own protocol in Ghidra before running anything. Ten runs on
lock theories once went where one read of the game's double-buffer code would have narrowed it.
- **Numbers from a fast machine.** A change neutral on a fast desktop can matter on a weak laptop or the Steam Deck,
in either direction. Say which hardware your numbers come from.
## Bug classes of a recompiled Switch game
The port runs the game's own code on another OS and another GPU. Most bugs come from places where the host differs
from the console.
### Variable frame step and epsilon-gated timers
The console steps the game by whole 30 Hz units, so a countdown started at a whole value reaches exactly 0. With the
real-time step (`realtime = 1`, the default) the step is a measured ratio, and the last step can leave a remainder of
about 1e-8. Many of the game's updaters step a timer only while it is above `FLT_EPSILON`; a remainder in
(0, `FLT_EPSILON`] is never stepped again. A consumer that tests the same field with `> 0` then sees it active
forever, until a load reinitialises the object. Issue #18 (weapons locked after a Fuse) was this.
Symptom words: stuck until a reload, a teleport or a shrine; locked after an action; works sometimes; only above
30 fps. To check a report: find the field the stuck behaviour reads, then compare the updater's threshold with the
consumer's test. A change to the frame step, the pacer or the fps limit is checked for this class.
### Thread scheduling: Horizon is not a desktop OS
- Horizon runs a woken better-priority thread at once on its core, before the thread that woke it goes on. A host
runs both in parallel, so state can be read one step early or freed while another thread still uses it.
- Threads created with ideal core -2 start pinned to one core on Horizon. The game keys per-core data by
`GetCurrentProcessorNumber` without locks and expects that pinning.
- The game has hundreds of spin loops without a system call. On the console a spinning thread holds one core of three;
on the host it burns a host core and can keep an equal-priority waiter out.
- ARMv8 `stlr` followed by `ldar` is ordered (RCsc). C11 release/acquire lets x86 reorder that store and load. A
two-thread flag handshake that loses a wake points here; lifter changes to barriers and atomics pass
`tests\diff\atomic.py` and `tests\excl_mt.py`.
- A scheduling change is judged by how long a ready, better thread waits for a core it may use, plus the slow-frame
count. A check that made one bug rarer once added 50-75 ms hitches.
### Maxwell behaviour versus Vulkan defaults
The port must do what the Switch GPU does: last-vertex provoking, (0,0,0,1) for unwritten vertex outputs, colour
targets outside the shader's output map keep their texels, the G-buffer pass samples its own targets. The full list
with sources: [maxwell.md](../maxwell.md), section 7.
### Memory shared by the CPU and the GPU
- The game fills textures and buffers with the CPU and samples them on the GPU without the flush a desktop API needs.
Missing uploads show as magenta, brown or stale textures.
- The game reads GPU-rendered textures with the CPU (grass maps, cut and burn masks). Missing write-backs show as
effects that never happen: cut grass stays up, fire does not spread.
- Memory only the GPU writes looks idle to write-watch tracking: a store from a shader leaves no fault and no dirty bit.
- Anything kept per texture, per frame or per object without a free grows host memory until a commit failure after
hours. Watch private memory over a few idle minutes.
### Device lost and GPU hangs
- The `vk: pass marks` lines name the pass in flight. Validation names the misuse when there is one.
- Submits and presents share one queue under one lock. A present from its own thread on a second queue gave device
lost on NVIDIA before Turing in another port.
- Caches from another driver or GPU (pipeline caches, translated shaders) gave black windows after driver updates;
every cache is validated on load and dropped when vendor, device or `pipelineCacheUUID` change.
- A first-time shader stall over 2 s on a slow GPU trips the Windows GPU timeout (display event 4101). Large one-shot
submissions (a boot-time decode of every texture) do the same.
- Point lists from vertex shaders that write no point size are undefined without `VK_KHR_maintenance5`.
### Windows and Wine host costs
What the hot path must not do, with measured costs: [pr-review-kit/02-performance.md](../pr-review-kit/02-performance.md),
"What the hot path must not do". For debugging, also:
- `SuspendThread` returns at once, but `GetThreadContext` waits until the thread leaves the kernel. A sampler or a
debugger stalls for as long as the stopped thread's file open or driver call runs.
- A thread stopped between its `sub rsp` and its stack probe has its stack pointer inside the guard page. Check
`PAGE_GUARD` with `VirtualQuery` before reading another thread's stack.
- Paths go through the W APIs with UTF-8 to UTF-16 conversion. An A API call breaks on a path with non-Latin letters,
such as a Windows user name in Cyrillic or Chinese.
- Wine emulates `GetWriteWatch` with `mprotect` and one fault per first write. Anything relying on cheap OS memory
tricks (write-watch, protection changes, fault handlers) costs more under Proton.
- `win32u.dll` frames in a host stack are kernel graphics calls: memory allocation or submission.
+90
View File
@@ -0,0 +1,90 @@
# Running and observing the game as an agent
How an agent runs a build into gameplay without a person at the controls, captures what the game shows, and reads the
results without flooding its context. Setting names and file formats live in [settings.md](../settings.md) and
[controls.md](../controls.md); this page says which to use when. How to judge a performance change:
[pr-review-kit/02-performance.md](../pr-review-kit/02-performance.md).
## Before the first run
- **A build dir per change.** Build into its own folder (`tools/build_game.py --out build/<ver>-<name>`) so a
comparison has two exes side by side. After every build check the exe's modification time: a failed link leaves the
old `totk.exe` in place and you test old code.
- **Prepared shaders in that folder.** `tools/prepare_shaders.py build/<dir> --version <ver>` ([building.md](../building.md)
step 6). Without it the game compiles shaders during play, and every timing and every "a draw is missing" shot of the
first minutes says nothing about your change. Shader and pipeline caches are not copied between build folders.
- **The same `totk.ini` on both sides** of any comparison. A folder without one runs at the defaults; check the log's
`resolution:` line of each run before comparing them.
- **Saves.** `tools/run_game.py` copies the profile `build/profile`, swaps in a save from `build/save-backup/<name>/`
and deletes the copy afterwards, so the real profile is never written. To make a backup, copy the user folder from
`build/profile/save/` into `build/save-backup/<name>/`. Pick a save that starts where the change acts.
## Running
```powershell
.venv\Scripts\python tools\run_game.py build\<dir> --save <name> --seconds 30 [--script <input.txt>] [--shot out.png] [--env KEY=VALUE] [--quiet] [--tag <name>]
```
- `--seconds` is game time since the world loaded (the log line `frame step: world clock starts` marks the start), at
any frame rate. Measure and script by game time; a frame count at another fps is another scene.
- `--script` replays input ([controls.md](../controls.md), "Scripted input"). A new script starts with the presses of
`tools/input/load_save.txt`, which load the first save from the title. `TOTK_INPUT_RECORD` turns a person's play into
a script; ask the user to record the repro rather than guessing the button sequence.
- `--shot` saves the last frame and builds pipelines synchronously: use it for pictures, never for timing.
- `--quiet` waits until no other `totk.exe` or `ninja.exe` runs. Every timing you report comes from a `--quiet` run.
- The log goes to `<dir>\logs\<tag>.log`; the tool prints `<log>: rc=<code>`.
Run one game at a time: two instances share the GPU's memory and the CPU, and both runs' numbers are void.
Running `totk.exe` yourself: set `TOTK_ASSUME_FOCUS=1` (an unfocused window otherwise pauses the game),
`TOTK_INPUT=none` (the user's keyboard and pad stay out) and `TOTK_BACKGROUND_WINDOW=1`. Stop only the process you
started, by PID: `taskkill /IM totk.exe` also closes the user's game.
**Check that the run reached the world** before trusting it: the world clock line is in the log and the shot shows the
scene you expected. Title menus on a slow boot eat presses, and runs of one script drift apart after 10-15 game seconds
(enemy AI, physics, weather). Two runs are comparable only when shots at the same game second show the same scene.
## Observing
| You need | Use |
|---|---|
| pictures over a time span | `TOTK_SHOTS=w<lo>-<hi>:<step>` (world seconds) and `TOTK_SHOTS_DIR`; 480x270 by default |
| the frames around a hitch or a one-frame glitch | `TOTK_FLIGHT=1`: keeps the last 600 frames with timings and thumbnails, dumps on F8, on a frame over `TOTK_FLIGHT_SPIKE_MS` or at `TOTK_FLIGHT_AT=<frame>`; `python tools/flight_view.py <dump dir>` lists the worst frames and writes a contact sheet |
| every render pass of one frame | `TOTK_DUMP=<frame>` and `TOTK_DUMP_DIR`: a PNG per pass target plus `passes.txt` (draws, bound textures with swizzles, storage buffers, copies, clears) and `images.txt`; `TOTK_DUMP_BUF` adds a guest buffer |
| Vulkan misuse | `TOTK_VK_VALIDATION=1` (needs the Vulkan SDK), run into the world; compare the message kinds with a run without your change. The layer caps repeats, so counts are not totals |
| which pass the GPU was in at a device lost | the `vk: pass marks` lines; on by default on AMD, `TOTK_PASS_CRUMBS=1` elsewhere |
| a crash | the crash report at the end of the log: exception, guest and host addresses, registers |
| a run on a large card that acts like a small one | `TOTK_VRAM_LIMIT_MIB=<MiB>` |
Frame dumps: a draw's argument lines come before its pass header. Pass numbers shift between runs, so find a pass by
its draw count or its targets, not by its number. RenderDoc cannot capture this renderer ([controls.md](../controls.md),
"RenderDoc capture").
## Measuring
| What | How |
|---|---|
| fps, worst frame, render-thread waits, GPU busy time, VRAM, `bound: gpu\|cpu\|pace` | `--env TOTK_TRACE=perf,sched`, then `python tools\perf_summary.py <log> <lo> <hi>` |
| slow frames | count the `perf: slow frame` lines; each names what the render thread was blocked on (pipeline, translation, texture, fence, pacing) |
| the frame's critical path, by thread and state | `TOTK_CRIT=w<lo>-<hi>` and `TOTK_CRIT_OUT=<absolute path>` with `sched` tracing, then `python tools\crit_path.py <file>` |
| where CPU time goes | `TOTK_PROFILE=<path>` and `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `python tools\prof_report.py <profile> <totk.map>`; `TOTK_PROFILE_RENDER=1` samples the render thread with call chains |
| a switch on a static scene | a setting set to `ab` with `TOTK_AB=<seconds>` alternates the two sides in blocks of game time; `python tools\ab_summary.py <log> --skip <seconds>`. On a moving scene compare whole runs instead |
A single run proves little: runs of one script on one build spread by a third in their 1% lows. Compare medians of at
least two whole runs per side, on a quiet machine.
## Probes
Each probe is a mode of `totk.exe`, run from the build folder; the list with what each covers is at the top of
`runtime/main.c`. `--selftest` needs no game files. `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex` and
`--probe-shader` cover most runtime changes and run in seconds: run them before any game run.
## Reading output without filling the context
- Game logs run to megabytes. `grep` the lines you need, `tail` the end, pipe `perf_summary.py` through `grep SUMMARY`.
- Generated C runs to hundreds of thousands of lines per file. `grep -n` the function, then print its line range.
- Look at screenshots downscaled: paste several into one 1280x720 grid and read the grid once. Write down what it shows
instead of opening it again.
- Send long builds and runs to the background with their output in a file; read the last lines when they end.
- Keep a notes file in the build folder from the first finding: what is ruled out, the leading hypothesis, the next
step, temporary debug edits with file and line. A fresh session continues from it.
+71
View File
@@ -0,0 +1,71 @@
# Symptoms and their usual causes
What a player reports, the causes this port and other Switch ports (nxnative, MacNativeGames, SavageWindRecomp,
LAS Recomp) found behind it, and the first check. Search by the player's words and by the object. Which of these are
already fixed here, and where: [pr-review-kit/04-knowledge.md](../pr-review-kit/04-knowledge.md), "Fixed bug classes".
GPU semantics behind the graphics rows: [maxwell.md](../maxwell.md). The method around this table:
[debugging.md](debugging.md).
## Graphics
| Symptom | Usual causes | First check |
|---|---|---|
| magenta, brown or stale textures; caves or sky wrong | the CPU filled the texture and the GPU never got the upload (no flush on the console) | frame dump: the texture's upload count and mirror state in `passes.txt` |
| effects that never happen: cut grass stays up, fire does not spread | the CPU reads a GPU-rendered texture that was never written back | which texture the game reads after its fence; write-back of that target |
| black scene, black dialog background or loading map | deferred binds read at record time; a 3D target rejected; a texture compare ignoring the channel mapping | frame dump: the first pass whose target goes black |
| lime silhouettes, rectangles in the sky | a target outside the shader's output map overwritten instead of kept | dump: the target's previous contents versus after the pass |
| dark or magenta 16x16 blocks, missing characters | a pass samples its own render targets (G-buffer feedback) | feedback-loop layout and barriers on that pass |
| tile rectangles, wrong data in tiles | storage buffers bound in the wrong slot order | `passes.txt`: storage buffers with their c0 slots |
| streaks on cliffs, characters stretched into streaks | primitive restart ignored; integer vertex inputs against a float format | validation run; the draw's topology and vertex formats in the dump |
| garbage geometry on title or load screens | 8-bit indices widened on the CPU before a copy in the same submission wrote them | whether a copy to the index buffer precedes the draw in one submission |
| black tiles, masks not cleared | clears with a partial channel or stencil mask skipped or done in full | `passes.txt` clears and their masks |
| garbage in feedback effects at first use | new GPU-only images start undefined; the console starts them at zero | clear on creation and compare |
| empty clouds or fog for the whole session | a one-shot draw skipped while its pipeline built, never drawn again | `perf: slow frame` lines naming pipeline waits; the draw's first frame |
| fog that never parts, flicker in tile rows | driver constants (c0: dispatch size, point size, base instance) not filled | the c0 contents the translator expects for that stage |
| no sun shadows, wrong lighting | unwritten vertex outputs read as zero instead of (0,0,0,1) | the shader's output map |
| one flat ambient colour | a 1-layer 2D array sampled as 2D | the view type the shader expects |
| upside-down text | lower-left window origin | origin mode in the log |
| flicker or a texture swapped with a similar one | the newest texture picked at shared storage instead of the one with the matching view | texture lookup by id in the dump |
## Timing and frame pacing
| Symptom | Usual causes | First check |
|---|---|---|
| stuck until a reload or teleport, locked after an action | a timer remainder below `FLT_EPSILON` with the real-time step ([debugging.md](debugging.md)) | `realtime = 0` in `totk.ini`; if the bug is gone, find the timer field |
| slow motion in GPU-heavy scenes, dynamic resolution never drops | the engine's GPU time comes from report timestamps; wrong timestamps make the frame look free | what the engine reads as GPU time and what the report writes |
| frame counter wanders 50-60 | two 60 Hz clocks (vsync event and present acquire) drifting apart | whether both follow one display tick |
| a 30 Hz thread runs at 27 Hz, slow cutscenes | host sleep overshoot: Windows waitable timers wake 0.3-0.5 ms late | `TOTK_TRACE=sched` sleep asked versus took |
| 20 fps after a scheduler change | a yield back-off that starts too early | yield handling against the console's |
| climbing or movement wrong only above some fps | game code that assumes a fixed step count per frame | compare at 30, 60 and the failing rate |
## Threads and crashes
| Symptom | Usual causes | First check |
|---|---|---|
| state read one step early, one frame of inconsistent data | a woken better-priority thread does not run before the waker goes on | sched trace around the handoff |
| per-core data corrupted, rare crash in a container walk | threads the console pins to one core run on any host core | the thread's core mask from its creator; a run on one core |
| rare hang or lost wake | address-arbiter signal values or wake order differ from Horizon | the arbiter path against mesosphere |
| a flag handshake loses a wake | `stlr`/`ldar` ordering lost in x86 release/acquire | the guest code's barrier pattern |
| a host core at 100% while the game waits | game spin loops without system calls | profile: which guest function spins |
| crash on world load | sizes the game reads from the driver (command memory used) differ from the console's | the sizes the game asks for against the console's |
| crash with one save only | the save itself; some crash on a real Switch | the same save on another build and, if possible, on hardware |
## Device lost, hangs, black window
| Symptom | Usual causes | First check |
|---|---|---|
| device lost a few frames in, GPU writes to address 0 (older NVIDIA) | present from another thread or queue | every submit and present on one queue under one lock |
| device lost or garbage on one AMD generation | views the driver branch mishandles (BC block views of 3D textures) | `vk: pass marks`, validation; the formats in the pass |
| device lost on one scene, every run, one vendor | a shader or pass that driver cannot run | pass marks name the pass; skip shaders by hash to bisect |
| GPU timeout after 1-2 min, single frames over 2 s | first-time shader stalls or a huge one-shot submission on a slow GPU | prepared shaders; submission sizes at boot |
| GPU hang at boot with pipeline libraries | point lists without a point size | the shader writes point size or the device clamps it |
| black window after a driver update | caches from another driver | the log line `vk_pipeline: ... written by another device or driver`; delete the caches next to `totk.exe` |
## Memory
| Symptom | Usual causes | First check |
|---|---|---|
| host memory grows in play until a commit failure after hours | state kept per texture init or per frame and never freed | private memory over a few idle minutes |
| multi-GB peaks while travelling | upload staging grows inside one submission | staging bytes per submission |
| out of memory after minutes | dead textures re-entering tables | live images and allocator bytes over time |
| stutter as VRAM fills | new memory blocks allocated on the render thread (8-23 ms each) | profile the render thread over the hitch: `vkAllocateMemory` under the allocator's lock |
+17 -5
View File
@@ -84,12 +84,18 @@ 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 off 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), a gain measured only there; while it was the default, a GTX 1660 Ti at the 60 fps cap with the GPU at 26 % reported uneven motion (#659). It adds up to a frame of latency; `replay_async = 1` turns it on.
- Chunked replay. A large submission is recorded in chunks by several threads (`TOTK_REPLAY_WORKERS`, 0 by default: the sequential replay; 4 is a good value from 8 logical processors) and the chunks go to the queue in order (`vk_exec.c`). A sequential scan keeps a copy of the queue state where each chunk starts and lists what a chunk would otherwise learn from the chunks before it: inline uniform updates, conditions, contact shadow marches, uniform drops. It also makes render targets live in replay order and notes the op, since a chunk can sample a storage before the chunk that renders it got there or after the scan went past a later target there: a chunk's texture slots resolve as of its own op, and the chunks after a texture with no entry yet wait for the chunk that makes it. DLSS's decisions (the G-buffer, the light target, the end of the scene, the scaling pass) are the scan's too, and its copy of the lit scene and its evaluation are records of the chunk that holds the op; a chunk's draws are jittered while they come before the scan's end of the scene. The evaluation waits until the chunks before it are recorded, as it reads what their draws noted (the camera block, the jitter, the per-object motion list, which chunks add to under one lock), and the chunks after it wait for it, as they sample its output. `TOTK_REPLAY_STATE_CHECK=1` compares the state after the last chunk with the scan's.
- Frame ahead. `TOTK_SYNC_AHEAD=1` (`sync_ahead`, off by default) makes the one `nvnSyncWait` per frame that waits for the frame presented last wait for the frame before it, so the game builds the next frame while the GPU still runs the previous one (`nvn_api_core.c`, `nvnSyncWait`). The game then writes memory that in-flight work still reads. This is safe only with shadow storage (`vk_memory.c`, the CPU-visible pools are host-visible copies of guest memory, on by default on Linux and Wine, `TOTK_SHADOW=1` on native Windows; without it the switch is ignored): a CPU write reaches the GPU only through the shadow copy made before each submit (`vk_mem_shadow_sync`), and a copy that would overwrite bytes an unfinished submission reads is queued as a GPU copy in the prologue command buffer of the part submitted next (`vk_mem_shadow_prologue`, `TOTK_SHADOW_DEFER=1`, the default with the feature) or, with `=0`, waited for on the replay thread (`shadow_reader_wait`). Only that one wait lags (identified by its call site, known for 1.2.1 and 1.4.3); results the game reads back from the GPU after it can be a frame old.
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. It is off by default on AMD GPUs, where the driver loses the device or hangs the GPU at startup with it (`TOTK_VK_RECORDER=1` forces it on, `=0` makes every call on the submitting thread).
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
`TOTK_UBO_SNAPSHOT`); without it, late draws read another object's matrix.
- Uniform updates. `nvnCommandBufferUpdateUniformBuffer` between the draws of a pass is replayed as an overlay: the
bytes go to scratch memory, later bindings of the range bind them, and the copies into the game's buffers are
recorded behind one barrier when the pass ends (`vk_exec_draw.c` `op_update_ub`, `TOTK_UB_OVERLAY`). A copy at the
update itself would end the pass with a full barrier, which the terrain paid before most of its draws.
- Driver data. The NVN driver keeps a per-stage constant bank (c0) that shaders read for things like point size, dispatch size and base instance. The back end fills it the way the driver does.
- Shaders: the Ryujinx translator 1.3.2 with MacNativeGames' patches and ours (`tools/xlate/patches/`), built with .NET 10 NativeAOT as `totk_xlate.exe` and run as a separate process. Its SPIR-V goes to Vulkan directly. `tools/prepare_shaders.py` and `totk.exe --prepare-shaders` translate and compile the variants of MacNativeGames' shared list ahead of play (29,587 for 1.4.3); the rest are translated on first use and cached on disk.
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it. Shader stages are compiled ahead into libraries; a draw that meets a new state links them at once (fast link), and an optimised pipeline replaces the linked one when a background thread finishes it. Pipeline keys seen in play are saved and built at boot. A draw whose pipeline is not ready is skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise, so one-shot passes are never lost; a full-screen draw is skipped as well rather than waiting (`TOTK_PIPE_SKIP`); the stage libraries a waiting draw needs are built on idle workers in parallel (`TOTK_PIPE_WAIT`). Without the extension, a missing pipeline is built on a worker and the draw waits or is skipped by the same rule.
@@ -99,8 +105,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 +124,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`).
+22 -2
View File
@@ -115,7 +115,15 @@ of them.
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
For Xbox button prompts, run `make_romfs_patches.py` twice more, with `--prompts xbox` and `--prompts xbox-label`:
they write `build\<ver>\romfs-patches-xbox` and `romfs-patches-xbox-label` ([controls.md](controls.md), "Xbox
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`.
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`. `--prompts pc` writes
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
A build from source has no layouts for other shapes than 16:9. At an `output_res` such as 5120x1440 the game then
draws the largest 16:9 size inside it (2560x1440) and the present adds bars, instead of a wide camera over a
stretched HUD (`runtime/resolution.c`, `shape_fallback`). For the full shape make `romfs-patches-<w>x<h>` once per
size: `make_romfs_patches.py --version <ver> --aspect <w>x<h> --out build\<ver>\romfs-patches-<w>x<h>` (the sizes
the installer makes are `ASPECT_SIZES` in `installer/steps.py`); `build_game.py` copies every `romfs-patches*`
folder next to `totk.exe`.
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
for the shared shader list:
@@ -150,6 +158,12 @@ your machines without MSYS2, Python or .NET. `romfs.bin` stays out unless you pa
`romfs\` next to the exe or point `TOTK_ROMFS` at its folder. Run `totk.exe --prepare-shaders` once in the copied
folder. The folder holds code and data derived from your game: for your own machines only.
### Profile-guided build
`build_game.py --pgo <file>.profdata` compiles the generated code and the runtime at -O2 with an execution
profile, 6-12% shorter frames. Which build uses which profile, when to retrain, where and how to train and how to
measure a new profile: [pgo.md](pgo.md).
## Linux (in progress)
The runtime builds and runs on x86-64 Linux with a window, input and menu on SDL3 (`*_sdl.c`) in place of the Win32
@@ -202,7 +216,13 @@ Before a commit:
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
files); a change to the settings passes `totk.exe --probe-cfg`.
files); a change to the settings passes `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`, which fails
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md). A change to the mods' ExeFS patches
(`tools/exefs_patches.py`, `runtime/mods.c`, the `folded.txt` of `recomp/gen.py`) passes
`.venv\Scripts\python tests\exefs_patches_test.py`, which makes its own modules and runs `gen.py` on one with a jump
table (no game files), and `totk.exe --probe-mods`. A change to the cheat VM or the cheat opcode tables
(`runtime/cheats.c`, `tools/cheats.py`) passes `.venv\Scripts\python tests\cheats_vm_check.py`, which runs both on
cheats of its own with clang from PATH (`--cc` names another).
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
change passes these, all run with `.venv`:
+2 -2
View File
@@ -58,7 +58,7 @@ The game's code is compiled ahead of time, so a cheat cannot write new instructi
|---|---|---|
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A, every presented frame while on. |
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A and C0 to C3, every presented frame while on. |
| master | `{name}` with code writes | A patch set that is always on. |
| restore | Writes back only the game's original instructions | Not needed: switching a cheat off restores them. Greyed out. |
@@ -66,7 +66,7 @@ A cheat whose code writes call or jump into a code cave the cheat writes itself
Not supported, and shown greyed out with the reason:
- the 0xC and 0xF opcode extensions;
- opcode C4 (extended button condition) and the 0xF extensions (FF0 pause, FF1 resume, FFF debug log);
- opcode 5 with a register offset;
- code writes mixed with other opcodes, except the button pattern above;
- code writes that change the CPU flags differently from the game's instruction, or code caves with branches or pc-relative instructions.
+120 -9
View File
@@ -14,7 +14,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|---|---|---|---|
| Left stick | left stick | W A S D | move |
| Right stick | right stick | mouse; arrow keys as a second source | camera |
| Y | west | left mouse button | attack |
| Y | west | left mouse button, T | attack |
| ZL | left trigger (over 50%) | right mouse button, Z | shield, lock on |
| ZR | right trigger (over 50%) | F, X | bow |
| X | north | Space | jump |
@@ -27,12 +27,13 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
| D-pad left, up, right, down | D-pad | 1 2 3 4 | quick-select |
| Plus | Start | Tab | menu, inventory |
| Minus | Back | M | map |
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. T duplicates Y for the game's menus, where the left mouse button is A while the pointer is shown (sort the inventory with T). The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
## Xbox button prompts
`TOTK_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab, read at start) picks the controller whose buttons the game's prompts show: `nintendo` (default) keeps the game's own, `xbox` shows an Xbox controller's.
`TOTK_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab, read at start) picks the controller whose buttons the game's prompts show: `nintendo` (default) keeps the game's own, `xbox` shows an Xbox controller's, `pc` the keyboard and mouse (PC button prompts, below).
- L, R, ZL, ZR, Plus and Minus become LB, RB, LT, RT, Menu and View, and the d-pad prompts a cross with an arrow for each direction meant. The letters on the keys are cut out in the game's own text font.
- A, B, X and Y name the label of the button the pad layout presses, so each prompt names the button to press. `pad_layout = nintendo` plays an Xbox pad by position: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel), so the prompts swap A with B and X with Y. `xbox` and `auto` put Switch A on the button labelled A, and the letters stay.
@@ -45,7 +46,7 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
| Plus, Minus | Menu, View | Menu, View |
| D-pad | cross | cross |
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters stay and the diamond is laid out as an Xbox pad: A at the bottom, B on the right, X on the left and Y at the top, the button the prompt names lit in its place, its bar reaching left from X and right from the others (`tools/make_romfs_patches.py`, `bflyt_xbox_diamond`: the panes of `blyt/DoCommand_00.bflyt` take their partner's place, A's of B and X's of Y). The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
@@ -54,7 +55,27 @@ The game reads its UI archive and fonts once at boot, so the prompts change at t
- every prompt but the button guide in play is text in the button font, `scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`: glyphs U+E040 to U+E043 (A B X Y), U+E044 to U+E047 (L R ZL ZR), U+E04B and U+E04C (Plus, Minus), U+E049 and U+E060 to U+E065 (the d-pad). fontTools writes the font again, so the archives grow and the folder also carries the resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`) with their entries raised by as much; without that the game stops at start.
- the button guide in play draws L and ZL from the images `Nt_KeyTexL_00` and `Nt_KeyTexZL_00` of the Common archive's `timg/__Combined.bntx`, painted with the same icons. The action prompt draws its button from `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped for `nintendo`, and the diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it), swapped the same way.
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 (its own UI layout wins; 16:10 and 21:9), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 that has no folder of its own (below), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
At a game resolution of another shape than 16:9 the game loads `romfs-patches-xbox-<w>x<h>` (or `romfs-patches-xbox-label-<w>x<h>`) in place of `romfs-patches-<w>x<h>`: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts xbox --aspect <w>x<h> --reuse <install>\romfs-patches-xbox --out <install>\romfs-patches-xbox-<w>x<h>`. The setup writes both for each of the fifteen sizes it lays the UI out for, as for the PC prompts below, so on a display of one of those shapes the game resolution follows the display with these prompts too. At another size, without that folder, the Switch prompts stay (*off at this game resolution*).
## PC button prompts
`prompts = pc` (*PC* on the *Button prompts* row) shows the key or mouse button the default keyboard bindings press each Switch button with, whatever the pad layout. The game loads `romfs-patches-pc` in place of `romfs-patches`, written by `tools/make_romfs_patches.py --prompts pc` (`button_icons.py`, `pc_icons`), which the setup runs next to the Xbox ones:
| Switch | Prompt |
|---|---|
| A, B, X, Y | E, Shift, Space, left click |
| L, R, ZL, ZR | Q, R, right click, F |
| Plus, Minus | Tab, M |
| D-pad | 1 (left), 2 (up), 3 (right), 4 (down), as a cross or the keys of the directions meant |
| Left stick, its press | W A S D (or the keys of the directions meant), Ctrl |
| Right stick, its press | the mouse, with an arrow for each direction meant, C |
- The glyphs keep the width of the game's own, so the right-aligned guides do not run into their text. A key is a rounded square with its label cut out in the game's own text fonts (Shift: an arrow over its name); a mouse button is a mouse with that button filled and the others hollow.
- The button guide in play draws every button from images of the Common archive (`Nt_KeyTexA/B/X/Y/L/ZL_00`, `Nt_KeyTexLSt_00` and `_01`, the stick tilted and pressed): all of them are painted with the same icons. The letters of the action prompt's diamond are one character of the Normal font each: E, an arrow for Shift, a bar for Space, L and R for the mouse buttons.
- Like the Xbox prompts they follow the defaults and not the bindings. `--binds <totk.ini>` draws the first key of each `bind_` line of that file instead, for a folder made by hand.
- At a game resolution of another shape than 16:9 the game loads `romfs-patches-pc-<w>x<h>` in place of `romfs-patches-<w>x<h>`: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts pc --aspect <w>x<h> --reuse <install>\romfs-patches-pc --out <install>\romfs-patches-pc-<w>x<h>`. The setup writes one for each of the fifteen sizes it lays the UI out for, so on a display of one of those shapes the game resolution follows the display with these prompts too. At another size, without that folder, the Switch prompts stay (*off at this game resolution*).
## Window keys
@@ -68,16 +89,16 @@ The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-p
## Remapping
The *Controls* tab of the settings menu lists, under the button layout, every Switch button and the eight stick directions the keys drive (movement and camera), each with two key cells and a controller cell. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
The *Controls* tab of the settings menu lists, under the button layout, every Switch button, the eight stick directions the keys drive (movement and camera) and the shake key, each with two key cells and a controller cell. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind_<button> = <entry>, <entry>, ...`. The button is `A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK`, or a stick direction `LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN` (movement, W A S D by default) and `RSTICK_...` (camera, the arrow keys). An entry is `pad:<name>` or `key:<name>`:
The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind_<button> = <entry>, <entry>, ...`. The button is `A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK`, or a stick direction `LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN` (movement, W A S D by default) and `RSTICK_...` (camera, the arrow keys), or `SHAKE` (keys only, G by default). An entry is `pad:<name>` or `key:<name>`:
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down lpaddle1 rpaddle1 lpaddle2 rpaddle2` (`zl`, `zr` are the triggers; the paddles are the back buttons of an Xbox Elite, DualSense Edge or Steam Controller, upper pair 1, lower pair 2, and no Switch button has them by default)
- key names: `a` to `z`, `0` to `9`, `f2` to `f7`, `f9`, `f12`, `num0` to `num9`, `space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert delete home end pageup pagedown up down left right`, `num_add num_sub num_mul num_div num_decimal`, `minus equals lbracket rbracket semicolon apostrophe comma period slash backslash grave`, `mouse_l mouse_r mouse_m mouse_x1 mouse_x2`
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
- `none` leaves the button unbound.
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above, less any key a line binds: with `bind_X = key:t`, T presses X only, not also Y. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
A pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
@@ -90,7 +111,85 @@ bind_Y = pad:north, key:mouse_l
## Mouse camera
Mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * TOTK_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on games 1.4.3 and 1.2.1, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt` and `recomp/patches-v121.txt`). None of them changes a step of the gamepad alone. The free-orbit camera receives raw-count
angular increments at its final spherical-angle output: 0.08 degrees per count on both axes at sensitivity 1 (about
14.3 cm a turn at 800 DPI), with no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with
the output. Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and
velocity filters cannot process it a second time.
The aiming camera (bow, throwing), the Purah Pad camera, the scope, the ability camera (Ultrahand, Fuse, Ascend,
Recall, Autobuild), the horse camera, lock-on, the bow drawn while locked on, climbing (walls and objects), the
waterfall's turn, shield surfing, the wakeboard and the dialogue cameras take the mouse the same way, at the same
degrees per count: each turns by
the stick times its own speeds and the frame time and adds that to a base angle, so the load of the stick it turns by
gets the gamepad's part only and the load of that base angle gets the mouse's degrees (`camera_load`, two sites per
axis per camera). Their speeds, time step, slow motion (bullet time) and, for the Purah Pad camera and the abilities,
the eased curve their pitch moves along do not touch the mouse; their own limits, collision and blends apply to the
result. Past the pitch limit of the scope or the ability camera the stick moves the camera's distance; the mouse's
motion past the limit keeps doing that through the stick. Recall's cursor is the ability camera's offset mode: the
camera keeps a direction, the stick moves the cursor across the screen (ReverseRecorderCursorSpeed, within
ReverseRecorderCursorRange) and the camera follows it by up to ReverseRecorderCameraRange (10 degrees across, 7.8 up).
The mouse moves the cursor so that its aim, the camera's follow plus the cursor's angle from the screen's centre (the
offset taken as the screen's half width or height at the camera's 50 degree view and aspect ratio), turns as many
degrees a count as the camera does elsewhere. The follow range is read from the game each step (until it has been read
once, the mouse is stick input there). The stick loads that only test for input keep the mouse in the stick. A step with mouse motion that no
direct hook took is logged (`mouse: step N with mouse motion took the stick path`, the first 30, then one in 600, with
the player's state), so the cameras still on stick input show in `logs/totk.log`.
Lock-on keeps pointing the camera at the target; the stick, and so the mouse, adds an offset around that direction,
which the game limits and lets return: the mouse's degrees are the step's turn the game adds to the offset and judges
the return by.
Shield surfing and the wakeboard turn their pitch by the square of the stick's y and move a second camera parameter
with it: their stick y keeps the mouse (that parameter and the camera's state follow it as before), the pitch gets
the degrees, and the pitch turn the game made from y is cut to the gamepad's share (p |p| / t |t|). Their stick x, and
the dialogue camera's (which is also its test for input), give a tiny value in the mouse's direction when the
gamepad's axis is at rest, so the camera still sees input without a stick turn.
Two inputs stay stick input: the waterfall's vertical axis moves the camera's height, not an angle, and the horse's
lock-on without a target (HorseLockOnEmpty) turns the stick's position into an offset that returns when the stick
does, which a mouse has no position for.
Those receive input after
the radial dead zone, unit-circle clamp and stick response exponent: x is 0.0343 stick units a count over the frame
ratio (the stick turns yaw 70 x / 30 degrees a step) and y 70 / 51 times that, linear like their pitch (51 y / 30
degrees a step), so both axes turn by the same amount for the same motion. `stick_sens` (which scales the game's
stick turn rates) is divided out. While the player uses an ability and the ability camera's hooks do not take the
mouse, its part is scaled by 1 / 0.8 and 70 / (1.5 x 51), the ability camera's own stick scales.
The player's verbs are read every 50 ms from the player actor (the player root's GOT slot main+0x3969918, the
component array +0x228, its AS component +0x158 and the verbs of its five slots). Ultrahand has a separate hook after
its manipulation input's power curves; it gets 0.01 stick units a count over the frame ratio, at most three full tilts
a step.
`mouse_aim_scale` (0.1 to 3, default 1) multiplies the mouse's motion while the bow is drawn (the verb UseBow or
DrawnBow with ZR held). The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw
counts, with the same meaning as in 0.3.10 (the free camera turns 0.08 degrees a count at 1, and now every other camera
does too); its scale differs from the stick-emulation mode.
The Controls tab switches between *Direct* and *Stick emulation* (`mouse_mode = stick`).
The orbit camera keeps its desired pitch at the final angular output, including stationary frames, so a later game
filter cannot recenter it. The persistent filter state accounts for the camera's current vertical offset instead of
accumulating corrections. A gamepad vertical input releases that mouse target to the game's own. The game's
lock-on, recenter and override (aiming) drive the camera themselves: the mouse stops holding without restoring its
earlier target, and when they end (or after the camera went 100 ms without a step) it holds the pitch the camera has
then, so the camera does not spring back to a pitch from before the aim or to the game's default after a shot.
While the ability wheel is open (L held and its screen, ShortCutMainItem_00 or ShortCut_00, has the focus), mouse
counts move its virtual cursor, which stays where it was when the mouse stops. L or the D-pad held with no wheel or
strip on screen (L soothes the horse, turns menu tabs) leaves the mouse on the camera; with the menu cursor off
(`menu_cursor = 0`) the screens are not read and the buttons alone decide.
The horizontal quick lists (D-pad up/left/right: materials, shields and weapons) use separate 32 ms right-stick steps,
with at least 32 ms of neutral between them. At sensitivity 1, 32 horizontal counts request a step; a large flick
cannot queue continued scrolling after the mouse stops. Vertical movement is ignored in these lists. Switching or
releasing a selector resets its cursor/step state without sending the last movement to the camera. Selector input is
not consumed by the direct camera or Ultrahand hooks.
To detach a fused part from what Ultrahand holds, shake the mouse left and right, or press G (`bind_SHAKE`). A press
swings the right stick left and right for 0.8 s, holding the key keeps it swinging. With `mouse_mode = direct` a shake
of the mouse while Ultrahand holds an object swings the right stick with it: once the swings (10 degrees of camera turn
or more each, 125 counts at sensitivity 1) change direction twice within 0.35 s, the stick goes full tilt the
way the mouse moves, each side held at least two frames so the game reads it, and goes neutral as soon as the mouse
rests. Only your own swings reach the stick, so nothing keeps swinging after you stop; shake until the part comes off.
The camera hooks ignore that swing (the mouse turns the camera in degrees already). Turning a held object back and
forth at an ordinary pace is slower and does not count. The arrow keys or the gamepad's right stick shaken by hand
work too.
A step of the gamepad alone, with no mouse motion and no pitch the mouse holds, reaches the camera exactly as without
the hooks.
The mapping below is still used when the camera-input hook is unavailable or the game disables camera input.
Game 1.1.0 has no hooks and keeps stick emulation whatever `mouse_mode` says. On 1.2.1 `mouse_aim_scale` has no effect (the bow-in-hand state is not read there).
In stick-emulation mode, mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * TOTK_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
@@ -98,6 +197,12 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
## Mouse in the game's menus
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The middle and side mouse buttons keep their bindings (bound to L and R they turn the tabs), and T presses Y (sort). The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
## Nintendo Switch 2 controllers
Windows does not present the Switch 2 Pro Controller as a gamepad SDL can open (the log says `input: no gamepad`), so it needs a bridge program that wakes it and creates a virtual gamepad. The DS4Windows fork by hbashton ([github.com/hbashton/DS4Windows](https://github.com/hbashton/DS4Windows), tested with release VIIPERRC4.5.1, a pre-release) with the output set to an emulated DualSense gives buttons, rumble, gyro and accelerometer, and reads the sticks through the controller's calibration (they rest at 0 to 2%). The game reads its virtual DualSense directly; the log then shows `input: gamepad "DualSense Wireless Controller" (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Its release notes also list Joy-Con 2 support, which is untested here.
@@ -111,14 +216,20 @@ Bridges that output a ViGEm DualShock 4 (ProCon2Windows, connectyourpro as teste
```
<frame> <button> [hold_frames] # A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK; hold defaults to 4
<frame> LSTICK|RSTICK <x> <y> [hold_frames] # -32767..32767, up is +y
<frame> MOUSE <dx> <dy> # direct-mode raw counts, once; down is +dy
@<ms> <button> [hold_ms] # timed on the world clock
@<ms> LSTICK|RSTICK <x> <y> [hold_ms]
@<ms> MOUSE <dx> <dy>
```
`<frame>` counts 60 Hz ticks of game time since the first frame (presented frames at 60 fps), so the title menus get the presses at the same game time at any frame limit. `@<ms>` counts milliseconds of the world clock: game time stepped since the end of the first save load (the player leaves the title screen's origin), so a timed event lands at the same game time at any frame rate and load length. A later load, such as fast travel, still shifts what follows. In a script with timed lines the untimed ones stop when the world clock starts, as the load ends at another tick in each run. Each event logs once (`input: script frame N tick T: A`). `tools/input/load_save.txt` loads the first save from the title screen.
`TOTK_INPUT_RECORD=<file>` (`input_record` in `totk.ini`, or Input recording in the menu) writes the merged input in the same format, one line per button press and per stick segment: events before the world clock starts by tick, the rest timed; a button or stick held when the world clock starts is cut there and goes on as a timed event. The file replays as a script, which makes it the repro to attach to a bug report.
In direct mouse mode the recording also writes one `MOUSE` event per consumed raw displacement. These are one-shot
displacements rather than held stick segments. They replay without cursor capture, including with `input = none`;
the game must be accepting camera input when the event reaches its camera update.
`TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry>` attaches an SDL virtual gamepad at start whose sticks rest at these raw SDL axis values (-32768 to 32767, down is +y), so a drifting or held stick can be tested without a controller: the values go through the gamepad mapping and `TOTK_STICK_DEADZONE` like a real pad's, and `TOTK_INPUT_RECORD` shows what reached the game.
## RenderDoc capture (blocked)
+48 -16
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
@@ -36,7 +38,10 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
`shared-fonts/`, `profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the
shaders. A newer release that changes nothing game-derived skips the rebuild.
shaders. A newer release that changes nothing game-derived skips the rebuild. The window starts with the last install folder
and the base, update, keys and firmware paths it ran with (paths only, never the keys file's contents), kept in
`%LOCALAPPDATA%\TotK Recomp\setup.json` (`$XDG_STATE_HOME/totk-recomp/setup.json` elsewhere). Entries that no longer exist
stay empty. A reinstall or a switch to another game version starts filled in.
Given an update file other than the recorded one (1.1.0, then 1.2.1, then 1.4.3), the installer extracts and builds again from it
and keeps `profile/`. Saves carry over both ways between 1.2.1 and 1.4.3: 1.4.3 converts a 1.2.1 save on load, and 1.2.1
@@ -44,7 +49,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.
@@ -55,7 +60,9 @@ loads the saves 1.4.3 wrote.
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
tools refuse costs those cheats or patches, not the install.
tools refuse costs those cheats or patches, not the install. The patch files' writes into main's data go to
`build-cache/exefs.data`, which the game stores into main at start; after the lift they are checked against the
bytes the lifted code read as constants (`<gen>/main/folded.txt`, its jump tables).
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
hashes and the game version's patch page) and link
them with the prebuilt runtime library for the host's level (`libtotkrt.a` or `libtotkrt-v2.a`) into `totk.exe`
@@ -65,13 +72,23 @@ loads the saves 1.4.3 wrote.
`totk.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). totk.exe reads the pack first and
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
The patches are written three times: `romfs-patches`, and `romfs-patches-xbox` and `romfs-patches-xbox-label`
The patches are written four times: `romfs-patches`, `romfs-patches-xbox` and `romfs-patches-xbox-label`
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
and `auto`). The two Xbox folders are optional: when one cannot be made, the setup warns and finishes.
and `auto`), and `romfs-patches-pc` (keyboard and mouse prompts for `prompts = pc`). Each of the three prompt
folders is then laid out once more for each game resolution of another shape, `romfs-patches-<style>-<w>x<h>`
(`--prompts <style> --aspect`). Those differ from `romfs-patches-<style>` in the UI archive only, and `--reuse`
hard-links the other files from it, so the forty-five folders take seconds and a few MiB each. The prompt folders are optional: when one cannot be made, the
setup warns and finishes.
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
many were left out.
7. **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`).
@@ -120,9 +137,12 @@ python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR
English as the game language, because Chinese game text needs the Switch shared fonts, which the setup extracts only
from a firmware folder. A folder with
`totk-install.json` needs only the folder. `--selftest [--shots DIR]` builds every screen in every language and
closes itself.
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the self-closing window and one end-to-end run
of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its docstring).
closes itself. The window is not stretched on a display whose scale is the system DPI: `installer/dpi.py` makes the
process system DPI aware before Tk starts, Tk then draws the fonts at the display's scale and `dpi.px` sizes the
paddings and wrap widths with it; the launcher does the same for its error box.
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the display scale, the self-closing window and
one end-to-end run of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its
docstring).
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
`config.local.toml`.
@@ -139,15 +159,17 @@ llvm/ llvm-mingw, x86_64-only subset (Apache-2.0 with LLVM excep
runtime/ libtotkrt.a, libtotkrt-v2.a and the headers the generated code and modules.c/romfs.c/patches.c
include
bin/ totk_xlate.exe, hactool.exe, SDL3.dll, libopus-0.dll, ninja.exe
data/ shader_list.tsv (MacNativeGames, MIT)
data/ shader_list.tsv (MacNativeGames, MIT); pgo-<ver>.profdata + .json, the execution profile an
update's code and the runtime libraries compile with (pgo.md)
THIRD_PARTY_NOTICES.md
totk.ini.example, VERSION the default ini; the release string (the repository's VERSION plus the commit)
```
- SDL3 comes from the upstream release, not MSYS2: MSYS2's DLL imports the LGPL libiconv.
- Every bundled third-party piece gets a row in `THIRD_PARTY_NOTICES.md`.
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
finishes.
- The release contains no code, data, text or assets of the game; `make_release.py` runs `tools/check_content.py` over
the folder before it finishes. The execution profiles name lifted functions by address (`f_<address>`) and hold
branch counts and control-flow hashes of the generated C.
## Switch system fonts
@@ -201,7 +223,8 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
## Constraints
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
- The installer steps contain no Win32 calls; only the window's DPI awareness (`installer/dpi.py`) is Windows-only, and
it does nothing elsewhere. Linux runs the same steps with its own compiler ("Linux release").
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
@@ -213,8 +236,17 @@ 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.
- Per-monitor DPI in the setup window: Tk 9 has none, so a display whose scale differs from the primary one still gets
the window stretched.
+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: on Mesa drivers (RADV, ANV, NVK; `vk_caps_driver`) every function-local variable starts at 0 (`OpConstantNull`, job flag `zero_locals`, translator patch 0016; `TOTK_ZERO_LOCALS` overrides). Elsewhere the load stays undefined: a zero start on every driver (the old patch 0014) made the whole picture shake by about 0.9 output pixels a frame with DLSS or DLAA on an RTX 4080 SUPER (#612). It also moved the SPIR-V hash of the game's FXAA shader out of `FXAA_FS` (`vk_dlaa.c` `vk_dlaa_skip_draw`), the likely cause of the shake; that path is not traced. | — |
## 4. Memory model
+23 -1
View File
@@ -48,7 +48,10 @@ Two mods that edit the same game file do not combine: the higher one wins the wh
(`.tkcl` files) go the same way.
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
and raises the entries for files a mod made larger or added, the files inside a mod's packs (`Pack\**\*.pack.zs`)
included. A separate RESTBL mod is not needed. The table tells the game how much memory each file needs: a file that
outgrew its entry crashes the game when it loads, which can be long after the start, when the game first needs it. The
log line `RomFS: resource size table: ...` says how many entries were raised and names the first files inside packs.
## Code patches (ExeFS)
@@ -68,6 +71,10 @@ The rebuild needs the setup folder the game was installed from. When that folder
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
A patch file may also change the game's data (constants and tables in its `.rodata` and `.data`). Those bytes are
stored into the loaded game at start, before it runs. When two mods write the same bytes, the higher one wins,
as with files.
## Cheats
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
@@ -84,6 +91,21 @@ The port keys a mod by its folder name. A renamed folder counts as a new mod:
Rename mod folders before you set them up in the menu, not after.
## HD texture packs
Texture packs made for the emulators replace the game's textures with larger ones, so the heap the world's textures
stream into must grow. Such a pack grows it in its own `System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs`, sized for
the emulators' 8 GB memory option. The port reads that file at start: the texture heap and the game's memory pool grow
by the pack's amount, and the game gets the port's own heap table with that growth (the log says
`resolution: mod <name> ships a HeapDef with TextureHeap ...`). Nothing needs to be set. For a pack without such a file
that still loops the panic Blood Moon, set `tex_heap_extra_mb` under `[mods]` in `totk.ini` (0 to 4096 MiB).
Tested: **HDPacks.com Tears of the Kingdom 4K Texture Pack v1.0** (`4kTextures`, 10,407 textures, 6.5 GB) on game 1.4.3
and 1.2.1, RTX 4080 SUPER: it loads into the game from a save and runs at the same frame rate, with about 3.3 GB more of
textures in memory (4.5 GB of game memory pools against 1.2 GB without it). Other packs are not tested and come with no
guarantee. Such a pack needs a lot of video memory; on a GPU with less than 8 GB, `astc_bc = 1` under `[performance]`
keeps its textures at a quarter of the size.
## What does not work
| On the Mods tab | Why | What to do |
+120
View File
@@ -0,0 +1,120 @@
# Profile-guided build
A profile-guided build compiles the generated code and the runtime at -O2 with an execution profile: a record of
which functions and branches ran while someone played. Hot paths are laid out straight, hot calls are inlined, and code
that never ran is compiled for size. On 1.4.3 a trained fight ran 12% faster per frame and an untrained fight about
6%; on 1.2.1 the trained fight ran 11% faster.
Each game version has its own profile, `pgo-v121.profdata` and `pgo-v143.profdata`, trained on that version's code.
## How a build uses a profile
- `build_game.py --pgo <file>.profdata` compiles with that profile. Every compile depends on the file, so a new
profile rebuilds everything.
- `tools/pgo.py merge` writes the profile to `build\pgo\pgo-<ver>.profdata`, next to its record `pgo-<ver>.json`. The
record holds the git tree of `recomp/` it was trained on and the digests of the runtime headers the generated C
includes (`cpu.h`, `simd.h`, `simd_fp.h`).
- `make_release.py` ships every profile in `build\pgo` in the release's `data\` folder. It warns when a record names
another `recomp/` tree than the one released, or a header digest differs. The runtime libraries of the release
compile with the newest version's profile.
- The installer builds a player's game with `data\pgo-<ver>.profdata` when the release has one for that version, and
at -O1 without it.
A profile only fits the code it was trained on. clang matches each function by name and control-flow hash, and a
function whose code changed loses its counts without a warning (`build_game.py` passes `-Wno-backend-plugin`, which
hides the "hash mismatch" messages). The inline helpers of `cpu.h`, `simd.h` and `simd_fp.h` become part of every
lifted function, so a change to them invalidates the profile as surely as a generator change: one change to the
floating-point helpers left a profile with a fifth of its weight and 2% of its gain.
## When to retrain
- After a change to the lifter or the generator (`recomp/`) that changes the generated C. Patch table changes
(`recomp/patches-<ver>.txt`) touch only the patched functions and can wait for the next retrain.
- After a change to `runtime/cpu.h`, `runtime/simd.h` or `runtime/simd_fp.h`.
- Before a release whose `make_release.py` run warns about a profile.
Train both versions from the same commit, from a clean `recomp/`.
## Train
### 1. Instrumented build
MSYS2's clang has no profile runtime, so the instrumented build uses llvm-mingw (unpack it under `toolchain\`):
```powershell
$llvm = "toolchain\llvm-mingw-<date>\llvm-mingw-<date>-ucrt-x86_64"
.venv\Scripts\python tools\build_game.py --version v143 --out build\v143-pgogen --toolchain $llvm --pgo gen
```
Regenerate `generated\<ver>` first if it is older than the commit you train on. If `totk.exe` stops at start with
`0xc000007b`, copy `libiconv-2.dll` from `C:\msys64\ucrt64\bin` next to it.
`totk.exe` writes `build\v143-pgogen\pgo\default_<id>.profraw` every minute and at exit. Each run of the same exe adds
to that file.
### 2. Shaders
Copy your `totk.ini` into the build folder and prepare the shaders (`tools\prepare_shaders.py build\v143-pgogen
--version v143`). Shader preparation runs `totk.exe` too, so move `pgo\*.profraw` out of the build folder afterwards:
its counts describe shader building, not play.
Keep the default settings, with the GPU command recorder on (`vk_recorder = 1`, the default).
### 3. Training scenes
The instrumented build runs slower than a normal one. Count each scene's time by the game clock, not by the wall
clock. Spend 90 to 120 seconds on each of the first four scenes, where most frames are spent, and about 60 seconds on
each of the others, which cover code the first four do not reach.
| Place | What to do | Time |
| --- | --- | --- |
| Lookout Landing | Stand in the middle of the town and turn the camera slowly all the way round, then walk through the crowd. | 120 s |
| Kakariko Village | The same: a slow camera turn, then walk between the houses. | 100 s |
| A fight with a Battle Talus | Fight it with weapons and the bow. | 110 s |
| The Depths, near a Lightroot | Turn the camera over the lit area, throw a Brightbloom Seed. | 90 s |
| The Depths, away from any light | Walk and turn the camera in the dark. | 60 s |
| The edge of a sky island | Run off the edge, skydive, open the paraglider. | 60 s |
| Outdoors in the rain | Walk and look around while it rains. | 60 s |
| A cave with Brightbloom plants | Walk in and fight the Moblins and Bokoblins inside. | 60 s |
| Inside a shrine | Walk through the first rooms. | 60 s |
| A Moblin camp | Fight several Moblins at once. | 60 s |
| Next to Yunobo | Use his ability: roll him into something. | 60 s |
| Hyrule Field, by the standing stone pillars | Run across the field, also backwards past the pillars. | 60 s |
Keep two scenes out of the training, so that you can measure on scenes the profile has not seen: Hyrule Field in tall
grass, and a fight with Moblins and Zonai constructs at once.
With a save at a scene, `tools\run_game.py` plays it without you: `--save <backup> --script <input file> --seconds
<game time>` ([agents/running.md](agents/running.md)). Without a save, play the scenes by hand.
### 4. Free play
Then play for 15 to 25 minutes and touch every kind of thing the game does, so that no common path is left without
counts:
- Run, sprint, climb, swim and glide; ride a horse; jump from a tower and skydive.
- Fight with one-handed and two-handed weapons, a spear and the bow; shield parry, flurry rush, fused arrows.
- Use every ability: Ultrahand (build a vehicle with a fan and wheels and drive it), Fuse, Ascend, Recall, Autobuild.
- Call the sages and use their abilities: Tulin, Riju, Sidon, Yunobo, Mineru.
- Open the inventory, the map and the camera; cook at a pot; talk to people in a town.
- Travel quickly to a few places, so that areas load; enter and leave a shrine and the Depths.
- Play in daytime and at night, in clear weather and in rain or snow.
### 5. Merge
```powershell
.venv\Scripts\python tools\pgo.py merge build\v143-pgogen --version v143
```
writes `build\pgo\pgo-v143.profdata` and its record. Merge right after the training, from the checkout and commit the
instrumented build was made in: the record reads the `recomp/` tree from git and the header digests from the build's
Ninja deps log.
Repeat steps 1 to 5 with `v121` for 1.2.1.
## Measure
Build the same code three ways in three folders with llvm-mingw, as the installer does: with the new profile, with
the old one and with none. Prepare the shaders in each folder. Then run the held-out scenes and one trained scene
in the order new, old, none, none, old, new, and compare the mean frame time over the same stretch of game time. One
run of a fight can vary by up to 15% from the next, so a profile counts as better only when it wins both pairs.
+91
View File
@@ -0,0 +1,91 @@
# Rules for taking a PR
## 1. What to review first
1. Fixes before features. A PR that fixes a bug, a glitch or a crash goes first; feature PRs wait.
2. Order fixes by reach: how many players the bug hits and how badly. Windows and default settings first, rare
hardware and unusual setups later.
3. Scale the depth of the checks to the reach. A crash many players hit and anything on the frame path get the full
checks. A rare case (a CPU without AVX, an unusual setup) gets the minimal fix, one targeted check and a short
note. A crash fix on rare hardware is still worth taking: cut the checks around it, not the fix.
4. Before testing, find out where the bug reproduces: OS, GPU vendor and generation, game version, settings. If the
symptom cannot appear on the hardware at hand (an NVIDIA paging bug on an AMD machine), say so and do not run
tests that cannot decide it.
5. Check a feature PR against the designs in `docs/milestones/`. A feature with a design (mods, cheats) goes in as
part of that design.
6. Several PRs on one topic: compare them before taking either. Another PR may already do it right.
## 2. How to read the diff
- The author's claim is a hypothesis. Read the whole diff: races, wrong semantics, edge cases, leaks, lock order.
- Check the behaviour against evidence: how yuzu, Ryujinx, LAS Recomp, SavageWindRecomp, nxnative and MacNativeGames
do the same thing in `refs/` (`tools/fetch_refs.sh` recreates it); the Vulkan spec; hardware docs
([docs/maxwell.md](../../docs/maxwell.md)); upstream issues.
- Weigh two things for every change: its cost on the hot path, and how it fails (device lost, hang, stale data, weak
or non-AVX CPUs, other GPU vendors, Wine/Proton).
- Read the code a hypothesis depends on before running the game: the game's functions in Ghidra or
`generated/<ver>`, our path in `runtime/`. A run confirms what the code could not rule out. It is one run, not a
series of variants.
- Order the work so the cheapest check that could refute the PR's idea runs first. Refuted: stop, write down why,
no build, no benches.
## 3. What a PR needs
- **Proof.** The bug reproduced before the fix, the same repro after, both results side by side: a screenshot, a log
line, a probe, a differential test. A fix without a reproduced failure is not proof. Where a repro is impossible
on the hardware at hand, say so; the fix is then taken only on strong static evidence in the code. Where a repro is
risky (device lost or a TDR can hang the machine), write the recipe and let the owner decide.
- **Frame-path changes** get whole runs of the heavy scene with the switch off and on, at least two each
([02-performance.md](02-performance.md)). Zero new slow frames is a hard bar.
- **A switch.** A change on the hot path or in rendering sits behind an env or `totk.ini` switch, so a player can turn
it off and we can compare.
- **Nothing changes for players outside the feature.** The DLSS PRs touched shared code; they went in once every
path that runs without DLSS was gated on DLSS being on.
- **A fix for one setup keeps the default for everyone.** The PRs that raised the low memory block from 6 to 10 GiB for
all players were rejected; the right shape is 6 GiB plus the extra where it is needed.
- **Parts we cannot run** (Linux-only code, an HDR display, RTX-only paths) are gated so they cannot touch anyone
else, or wait for a report from someone with that hardware. Note them as unrun.
- **Project tests** from [03-correctness.md](03-correctness.md): lifter differential tests, probes, `--selftest`,
`tools/check_content.py --staged`.
- **Text the player sees** goes into the catalog: `runtime/ui_text.h` ids and `runtime/ui_text.c` entries in every
language the menu has (English, Russian, Brazilian Portuguese, Simplified Chinese), and `installer/text/*.json` for
the installer. No hardcoded sentences, no second string table.
- **A new setting** is described in `totk.ini.example` and [docs/settings.md](../../docs/settings.md).
- **Licences.** MIT, BSD, ISC, zlib and Apache-2.0 code may be copied, with its header and a row in
`THIRD_PARTY_NOTICES.md`. yuzu, suyu and other GPL code is read for behaviour only; we write our own.
## 4. Reasons to reject or rework
- No test or repro for a change that is not trivial.
- Slow: a state copy per draw, a global atomic counter on the hot path, synchronous file I/O or a GPU allocation on a
game or render thread, new slow frames.
- Changes behaviour for everyone to serve one setup, or is on by default without a run in a scene where it acts.
- "Fixes" a crash by turning a feature off for a vendor. A crash that goes away with a feature off points at the bug;
the feature gets fixed.
- Breaks another feature (a preview that loses anti-aliasing).
- Fake features: labelled FSR4 or frame generation, and not that.
- Wrong measurements: percentiles over window means, counters over the wrong set.
- Generated C edited by hand instead of the generator (`recomp/`).
- A disallowed licence or pasted GPL code.
## 5. Finishing a good PR
A good PR that misses the project's scheme (hardcoded text, conflicts, layout, comment style) is brought to it while
merging, not sent back to the author for another round. When only part of a PR is good, take that part and note what
was left out and why.
A bug found outside the PR under review is not fixed on the spot: search the open PRs on that topic, report the bug
with them, and let the owner decide.
## 6. Merging, branches, commits
- Each PR or wave of PRs gets its own branch `task/<tag>` from a fresh `dev` and its own build dir. It merges into
`dev`.
- Once the review is done, the symptom is gone on one paired shot and the probes pass, merge into `dev` and hand the
build to players: they test in game faster and wider than our routes. The wider checks (other routes, validation,
fps cost) follow as a separate diff. Speed never skips the review of the diff itself.
- `main` only with the owner's explicit yes. Push only when told to.
- A commit message is a plain English sentence, no `feat:`/`fix:` prefixes.
- The version lives in the repo-root `VERSION` file; the owner decides when it moves.
- After a merge, record which PRs went in, what was reworked, what was rejected and why. The reason for a rejection
goes into the PR as a plain English comment.
+97
View File
@@ -0,0 +1,97 @@
# Performance: no lost frames on weak hardware
## Who we tune for
- The goal is many more fps on modest hardware, and Steam Deck later (Zen 2 4c/8t APU, shared LPDDR5, the exe runs
through Proton/Wine).
- The developer's machine (Ryzen 9 7900X + RX 9070) is fast and its numbers do not carry over. A laptop with an
i7-9750H and a GTX 1050 Ti is a sample of a weak machine, not the target to fit constants to.
- What weak hardware measured:
- GTX 1050 Ti (4 GiB, 214 MiB BAR, no ReBAR): the GPU read imported host pools over PCIe, 110-120 ms a frame
against 4.3 ms on the RX 9070. The VRAM mirror brought it to 10-11 ms; that machine is now CPU-bound at 25-45 fps,
replay 25-30 ms a frame.
- The same write-watch scan costs about 3x the CPU time there.
- NVIDIA and desktop AMD GPUs have no ASTC: textures decode in a compute shader. CPU decodes before that made
0.4-0.7 s load frames on the 1050 Ti.
- Wine emulates `GetWriteWatch` with `mprotect` and a fault per first write. Anything relying on cheap OS memory
tricks (write-watch, `VirtualProtect` freezing, fault handlers) is checked against Wine's implementation.
- The port uses about half of even a weak CPU, so the frame's critical path is the limit, not throughput. TotK keys
per-core data by `GetCurrentProcessorNumber` without locks, so the game's threads share three virtual cores on any
host. In a fight the main thread and five job workers keep 2.6 of the 3 busy: guest code speed sets the fps.
- Perf work builds and measures 1.4.3. A 1.2.1 build is for a change about to merge with a large gain.
## Gain ceiling first
Before writing or taking a perf change, compute its ceiling: the share of the code it removes times the share of the
frame that code holds on the critical path, on weak hardware. A ceiling of a few percent or less is not worth it.
- Use the critical path (`TOTK_CRIT` + `tools/crit_path.py`, heavy frames only), not one thread's profile. On the weak
laptop the submit thread is 55% replay, yet the critical path of heavy frames is 67% guest code, 15%
`nvnSyncWait`, 13% core waits and wake latency.
- Changes that can move the frame by 10% or more are usually design changes. Polishing a hot function from a flat
profile almost always gives zero.
## A neutral safe change stays
An optimization that measures neutral on the fast machine stays on by default when:
- upstream (`refs/`, other ports) reports a gain for it, especially on weak CPUs or GPUs;
- the static read leaves no hazard, and the probe or differential test passes;
- whole heavy-scene runs off/on x2 show no fps loss and no new slow frames;
- a switch turns it off.
"No gain here" is not a reason to drop a safe change with upstream evidence. A regression or a hazard nobody ruled
out is.
## How to measure
- **By game time, never by frame counts.** Run length, analysis windows, A/B blocks and input scripts go by the world
clock (`--seconds`, `TOTK_CRIT=w<lo>-<hi>`, `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, `TOTK_SHOTS=w<lo>-<hi>:<step>`). The
same frame count at another fps is another scene.
- **Check a scripted run reached the world.** Title menus on a slow boot eat button presses; confirm the run is in the
world before trusting its numbers.
- **Whole runs off and on, two each at least.** An in-run A/B on a moving scene lies: the A and B blocks land on the
same game seconds every run, so a content difference repeats. One in-run A/B showed "-11% replay"; whole runs gave
5.84/5.73 against 5.76/5.73 ms, nothing. In-run A/B only on a static scene (standing still).
- **Spread.** One script on near-identical builds gave 1% lows from 31.5 to 50 fps. Compare medians of three runs,
never one run against one run.
- **A quiet machine.** `tools/run_game.py --quiet` takes the machine-wide lock; no other `totk.exe`, build or test runs
meanwhile. One game at a time.
- **A warm shader cache, prepared properly.** Shader and pipeline caches are not copied between build dirs:
`totk.exe --prepare-shaders` or `--warm-shaders` in that dir. The AMD driver cache is keyed to the exe path and
limited in size: preparing another build dir can evict this one's libraries (40-220 ms per new effect in play).
- **A heavy scene.** A fight with a large enemy and a crowd, fps limit at the maximum (`TOTK_FPS_LIMIT=360`). Compare
one shot with the scene's reference before trusting numbers.
- **No base runs by habit.** Run the candidate alone and judge it against the targets: max fps, zero slow frames
(`perf: slow frame`), no black or flat frames, no new validation errors. Glitches are found by looking at shots and
frame dumps, not by diffing metrics. Run a base build only when a specific question needs a side-by-side.
| What | How |
|---|---|
| fps, worst frame, render-thread buckets, GPU busy, VRAM | `--env TOTK_TRACE=perf,sched`, then `python tools/perf_summary.py <log> <lo> <hi>` |
| the frame's critical path | `TOTK_CRIT=w<lo>-<hi>` + `TOTK_CRIT_OUT=<path>`, then `python tools/crit_path.py <file>` |
| sampling profiler | `TOTK_PROFILE=<path>` + `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `tools/prof_report.py <profile> <totk.map>` |
| A/B on a static scene | `TOTK_AB=<seconds>`, then `tools/ab_summary.py --skip <seconds>` |
| VRAM pressure on a large card | `TOTK_VRAM_LIMIT_MIB=<MiB>` caps the budget the port sees |
## What the hot path must not do (Windows host)
- **Files.** The game threads (main, job workers) and the render thread never open, read, write or flush a file, and
never wait for a thread that does. Opening a 20-byte file in a 139k-file folder: 0.1 ms idle, 11-40 ms in play while
the translator, the pipeline cache and the save wrote to the same volume. A save's `FlushFileBuffers` and
`MOVEFILE_WRITE_THROUGH`: 93-135 ms. Saving the pipeline cache under its lock on the present thread froze the replay
every 20 s. File I/O goes to a background thread; results come back in memory.
- **Waits.** Short waits spin on the counter; waits over 1 ms use the high-resolution timer. A waitable timer wakes
0.3-0.5 ms late; the SDK's 20 us sleeps took 460-515 us. A 1 ms timer thread does not help: it gave more slow frames.
- **GPU allocations.** VMA's `vkAllocateMemory` of a 256 MiB block under its lock: 8-23 ms. A new block on the render
thread is a hitch.
- **128-bit division.** `(unsigned __int128)t * num / den` compiles to the library call `__udivti3`; counter
conversions that way took a tenth of the game threads' time in a fight. Use the precomputed scale (`platform.h`
`PlatScale`).
- **Per-draw copies and counters.** A pipeline-state copy per draw, a global atomic counter, a scan of every page each
frame: on a weak CPU each is frames.
- **Memory kept per frame.** Anything allocated per texture initialization or per frame must be freed. One copy kept
per texture init grew host memory by ~44 MiB a game minute until a commit failure after hours (#670). Check private
memory over a few idle minutes for a PR that stores per-object state.
- **Scheduling.** Any timing or scheduling change is checked for slow frames. A preempt check at every host call made
one bug rarer but added 50-75 ms CPU hitches and was reverted.
+92
View File
@@ -0,0 +1,92 @@
# Correctness: checks that keep behaviour intact
## Lifter and generated C
- Generated C is never edited by hand: change the generator in `recomp/`.
- `recomp/layout.py` alone decides module bases.
- Every change to the lifter or the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) passes the differential tests
against Unicorn, all run with `.venv`:
| Test | Checks |
|---|---|
| `tests\diff\lift.py` | instructions sampled from the modules; `--coverage` lists the words the lifter rejects |
| `tests\diff\simd.py` | the whole FP/SIMD encoding space |
| `tests\diff\atomic.py` | exclusive load/store and ARMv8.1 atomics |
| `tests\diff\func.py` | whole functions as the generator emits them: loops, jump tables, calls |
| `tests\flags_check.py` | the function-local flag helpers against the packed ones |
| `tests\excl_mt.py` | the exclusive monitor under contention and ABA, on several threads |
| `tests\jumptables_check.py` | `recomp/jumptables.py` against Unicorn: 0 disagreements on 1.2.1 and 1.4.3 |
- A `--cpu v2` build (no AVX2) passes the same tests with `--cflags=-march=x86-64-v2`. Players without AVX exist.
- A compile error in generated C on a line the generator could not have written is a bit flip on disk or in memory.
Regenerate and compare hashes; leave `recomp/` alone.
## Runtime
- From the build dir: `totk.exe --selftest`, `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`
pass.
- Settings: `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`.
- Button prompts: `.venv-py314\Scripts\python tests\romfs_prompts_test.py`. Build dirs carry only `romfs-patches`; to
test PC or Xbox prompts in game, generate the overlay first:
`.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts pc --out build\<dir>\romfs-patches-pc`,
then restart the game.
- Installer: `tests\installer_gui_test.py` (event model, form, catalogs, cancel, display scale).
- Shaders: `tools\shader_census.py --version <ver> --translate`, `tools\xlate_smoke.py`. A translator patch
(`tools/xlate/patches/`) needs `build/xlate` rebuilt and changes shader hashes: anything keyed to a shader hash
breaks with it.
## GPU portability
- New Vulkan code queries the feature, extension, format or limit it uses and keeps a working path or a logged
fallback without it (provoking vertex and subgroup size pinning in `vk_pipeline.c` are examples).
- Never hardcode what one GPU reports: alignments, subgroup size (RDNA4 may run wave64, old AMD Vega is wave64 only),
depth formats, `maxPushDescriptors`.
- One queue: every submit and present takes `vk_queue_lock`. Presenting from its own thread on a second queue gave
device lost on GTX 10-series in another port.
- RDNA1/2 bugs do not reproduce on RDNA4 (another driver branch); NVIDIA bugs do not reproduce on AMD. A Ryzen 7000
iGPU is RDNA2 and runs the RDNA1/2 driver branch: a cheap testbed for those reports.
- Validation: a `--validation` run in the world shows no new messages.
## Maxwell / NVN behaviour
The port must do what the Switch GPU does, not what Vulkan does by default. The full list with sources:
[docs/maxwell.md](../../docs/maxwell.md), section 7. The traps a PR breaks most easily:
- Provoking vertex: Vulkan takes the first vertex, NVN the last.
- Unwritten vertex outputs read (0, 0, 0, 1).
- A colour target outside the fragment shader's output map keeps its texel (write mask 0). AMD writes garbage there.
- Feedback loops: the G-buffer pass reads its own targets. It needs the feedback layout and a by-region barrier.
- Depth bias argument order, depth mode, depth clamp: checked against deko3d.
- Report timestamps: the engine's GPU time comes from them, and dynamic resolution and game speed follow it.
## Timers and the real-time step
- With `realtime` set, the game steps by a measured ratio, not whole 30 Hz units. A countdown remainder in
(0, FLT_EPSILON] is never stepped again, and a consumer testing `> 0` sees it active forever. That was "weapons
cannot be changed after a Fuse until a reload" (#18).
- Symptoms of this class: stuck until a reload or teleport, locked after an action, works sometimes.
- A PR touching the frame step, the pacer or the fps limit is checked for this class.
## Threads and scheduling
- ARMv8 `stlr` then `ldar` is ordered (RCsc); C11 release/acquire lets x86 reorder that store and load. Changes to
the lifter's barriers or atomics pass `tests\diff\atomic.py` and `tests\excl_mt.py`.
- A scheduler change is judged from one `TOTK_TRACE=sched` run by how long a ready, better thread waits for a core it
may use, plus the slow-frame count. Not by statistics over many runs.
- A crash that looks like a race: rule out the save first. One reported crash was the save itself; it crashed the
same way on a real Switch.
## Files and caches
- A crash leaves zero-filled files. Caches are validated on load, not trusted because they exist.
- The pipeline cache is dropped when vendor, device or `pipelineCacheUUID` change.
- Unicode paths: W APIs with UTF-8 to UTF-16 conversion.
- A failure that happens once and does not come back: rerun before debugging it as a code bug. Broken caches and bit
flips happen.
## Game content boundary
- The repository is public. No game files, and nothing extracted or generated outside `build/` and `generated/`.
- Keys never appear in logs, commands, files, issues or comments.
- Every file the generator emits carries the `GENERATED_MARKER` defined in `tools/check_content.py`.
- `python tools/check_content.py --staged` before every commit. CI runs the same check.
+97
View File
@@ -0,0 +1,97 @@
# Knowledge base
Before investigating a symptom, or trusting a PR that "fixes" it, look for it here. Search by what the player sees
(black, magenta, flicker, slow motion, missing, stale, streaks) and by the object (sky, grass, shadow, fog, HUD).
## Invariants of recent changes
A PR that touches these areas must keep them.
- **Queue order.** When anything is submitted, every earlier part is already on the queue. Present, uploads, readbacks
from guest threads and the `vkDeviceWaitIdle` sites rely on it. The recorder thread now begins, ends and submits
the parts it records; the submitting thread still waits for that submission, so the order holds.
`TOTK_REC_END=0` restores the old split.
- **GPU-written memory is never frozen.** Storage-buffer stores, copies and fills land in imported guest memory
without a fault or a write-watch bit, so a region only the GPU writes looks idle. Such regions are never frozen
read-only, and a frozen region is thawed before a GPU write. LAS Recomp never changes the protection of imported
memory because of NVIDIA. A suspected cause of NVIDIA "write invalid" device faults (#788, #802), not proven.
- **VRAM pressure.** Under pressure, released textures whose storage the game has given to another texture go first:
30 s after release and once no finalized slot has bound them for 600 submits. Clean mirrored entries wait 5 s.
Both stop once use is back under the line. A PR touching texture lifetime is tested with `TOTK_VRAM_LIMIT_MIB`.
- **Texture pins are freed.** Each pin counts the recording copies naming it and is freed 1200 submits after its last
use unless the replay holds it as a render target. Before that, pins leaked ~44 MiB a game minute (#670).
## Fixed bug classes
If a PR touches this code, confirm the fix still holds.
| Symptom | Cause | Where the fix is |
|---|---|---|
| lime silhouettes in grass; rectangles in the sky | a target outside the output map must keep its texel; zero-filling it wiped the half-res depth | `vk_pipeline.c` `target_wmask` |
| stale grass maps in the far terrain for a second after a hit | write-back of a texture the CPU had rewritten | `vk_texture.c` `write_back` |
| cut grass stays standing; fire does not spread (#132) | CPU reads of GPU-rendered textures without an invalidate | `vk_texture.c` `vk_texture_write_back_small`, `nvn_ext.c` `nvn_pool_invalidate` |
| cloud and fog volumes empty for the session | a one-shot draw skipped instead of waiting for its pipeline | `vk_exec_draw.c` `prepare_draw` |
| tile rectangles | storage buffers bound by Ryujinx first-use order, not the c0 slot | `vk_exec_draw.c` `ssbo_slot` |
| black dialog background and loading map | 3D targets rejected, the grading LUT never written | `vk_exec.c` `resolve_target` |
| streaks on cliffs | primitive restart ignored | `vk_exec_draw.c` `restart_for` |
| streaky map screen | the newest texture picked at shared storage | `vk_texture.c` `vk_texture_by_id` |
| dark 16x16 blocks on the ground, sage spirit not drawn | G-buffer feedback loop | `vk_exec_draw.c`, `TOTK_FEEDBACK` |
| magenta cave walls, brown sky | CPU-filled textures without a flush | write-watched mirrors, `vk_memory.c`, `vk_texture.c` |
| black 3D scene | deferred texture binds | `nvn_api_cmd.c` `Bind*Deferred` |
| HUD prompt boxes, black stamina wheel | `nvnTextureCompare` ignored the channel mapping | `nvn_api_obj.c` |
| crash on world load | `GetCommandMemoryUsed` did not follow the driver's sizes; the game sizes model heaps from it | `nvn_api_cmd.c` |
| 20 fps | a yield back-off starting before 2 ms | `kernel.c` SVC 0x0B: no back-off |
| weapons locked after a Fuse (#18) | timer remainder in (0, FLT_EPSILON] with the real-time step | patch under the `realtime` setting |
| magenta/blue garbage or device lost on RDNA1/2 (#5) | writes into 3D BC block views | in `dev` |
| black window after a driver update | a pipeline cache from another driver | `vk_pipeline.c` drops the cache by UUID |
## Known open risks
A PR touching these needs stronger proof.
- Buffer textures are not replayed and bind a stand-in: light and decal lists may read 0.
- New GPU-only colour images start undefined; feedback effects may show garbage.
- Clears with a partial channel or stencil mask are skipped (`W_CLEAR_MASK`, `W_STENCIL_MASK`).
- 8-bit indices are widened on the CPU at record time; a copy in the same submission gives stale indices.
- `UpdateUniformBuffer` is a copy between full barriers and ends the render pass (fps outdoors).
- Report timestamps carry host time at fence completion: the engine's GPU time is wrong.
- The game's SVC-free spin loops burn a host core (843 in `main`).
- Images aliased onto one GPU-only pool allocation do not see each other's data on AMD.
## Past PR decisions
| PR | Decision | Why |
|---|---|---|
| #690 shrine snapshots | rejected | a pass restart per copy, a `VkPipeState` copy per draw, broken `bind_sig` skip; fps loss |
| #674 | rejected | a global STXR counter on the hot path |
| #660 | rejected | the preview loses anti-aliasing |
| #713, #576 | rejected | low memory block 10 GiB for everyone; should be 6 GiB plus the extra where needed |
| #573 | rejected | dynamic resolution off by default for everyone, the 1.4.3 site never run |
| #568, #569 | rejected | fake FSR4 and frame generation |
| #487 | rejected | wrong metrics: percentiles over window means, counters over the wrong set |
| #451, #380 | rejected | not painless to take |
| #566, #594, #534 DLSS | taken with changes | #594's shared paths gated on DLSS being on; nothing changes without DLSS |
| #489 | partly taken | the runtime part checked by a frame dump; the installer's DLL download was the owner's call, its pinned hash re-checked |
| #519, #582 amiibo | rewritten by us | took the data-format fixes; left out the features and the Linux event rework |
| #483 vs #585 | #585's mapping kept | #483's stick glyph mapping was rewritten unasked while #585 already had it right |
| #528 rain | taken after a check | code review fine, merged only after a run on a rain save |
| #767 | taken with our fix | the default was skipped with `prompts = xbox|pc` when those folders did not exist |
| #625 frame queue | taken, off by default | a new frame queue, one field run with it on |
| #743 UB overlay | waiting | +25% in a Depths fight on an RTX 4080; needs review, fight runs x2 and shots on AMD |
## Our mistakes, not to repeat
- **"-11%" was an A/B artifact** of a moving scene. Whole runs off/on showed zero.
- **Micro-optimizations from a flat profile** (1-3% of samples each) gave 26.2 against 26.4 ms on the weak laptop:
nothing. Nobody had computed a ceiling.
- **A scheduler patch merged on run statistics** added 50-75 ms hitches. Slow frames are a hard bar.
- **Ten game runs on lock theories** where one Ghidra read of the game's code narrowed it at once.
- **45 minutes of builds and benches** on an idea its own first thread census had refuted: the game pins 53 of 56
threads itself. The cheapest refuting check goes first; when it refutes, stop and write the idea down.
- **Tests of a bug that could not appear on the hardware at hand** (NVIDIA paging on an AMD machine): 245k tokens and
37 minutes for nothing. Ask first where the bug reproduces.
- **Turning the recorder off for a vendor** instead of finding its bug. Stopped: the feature gets fixed, not silenced.
- **Bisecting code when every build crashed the same way at once.** The cause was a bad page of a game file in the OS
file cache. When all builds fail identically, suspect inputs and caches first.
- **A duplicate issue closed with a bare "Same as #N"** lost the logs and links. Move everything useful to the issue
that stays open first: hardware, driver, version, when it happens, workarounds, links from comments.
+49
View File
@@ -0,0 +1,49 @@
# Per-PR checklist
## Before reading
- [ ] Fix or feature? Fixes first, ordered by how many players they reach.
- [ ] Where does the bug reproduce: OS, GPU vendor and generation, game version, settings? Can the hardware at hand
show it?
- [ ] Is the symptom in [04-knowledge.md](04-knowledge.md) or `docs/maxwell.md`?
- [ ] Another open PR on the same topic? Compare them.
- [ ] Feature: is there a design in `docs/milestones/`?
## Reading the diff
- [ ] The whole diff read, not the description: races, edge cases, lock order, leaks.
- [ ] Behaviour checked against `refs/` (Ryujinx, yuzu, LAS, SavageWind, nxnative, MacNativeGames), the Vulkan spec,
`docs/maxwell.md`.
- [ ] Hot-path cost: no file I/O, GPU allocations, waits, per-draw copies or global atomics on game or render threads.
- [ ] No memory kept per frame or per object without a free.
- [ ] Failure modes: device lost, hang, stale data, CPUs without AVX, NVIDIA/Intel/old AMD, Wine/Proton.
- [ ] New Vulkan code queries the feature and has a fallback; nothing hardcoded for one GPU.
- [ ] Invariants kept: queue order, GPU-written memory never frozen, VRAM pressure path, pins freed.
- [ ] Players without the feature see no change. The default for everyone stays for one setup's sake.
- [ ] New behaviour behind a switch. A new setting described in `totk.ini.example` and `docs/settings.md`.
- [ ] Player-visible text in the catalogs, in every menu language (`runtime/ui_text.c`, `installer/text/*.json`).
- [ ] Copied code: allowed licence, header kept, row in `THIRD_PARTY_NOTICES.md`. No GPL code.
- [ ] Generated C not edited by hand.
- [ ] Parts we cannot run (Linux, HDR, RTX-only) gated away from everyone else and noted as unrun.
## Proof
- [ ] Repro before the fix and the same repro after, side by side.
- [ ] Lifter or CPU headers: differential tests against Unicorn; `jumptables_check.py` 0 disagreements.
- [ ] Runtime: `--selftest` and the four probes pass.
- [ ] Settings: `--probe-cfg` and `tests\settings_doc_check.py`. Prompts: `romfs_prompts_test.py`, overlay generated
for a game check.
- [ ] Frame path: whole heavy-scene runs off/on, two each at least, by game time, quiet machine, warm cache prepared
in that build dir. No fps loss, zero new slow frames.
- [ ] Rendering: shots or a frame dump in a scene where the change acts; no black or flat frames; no new validation
errors.
- [ ] Perf PR: gain ceiling computed for weak hardware.
## Decision
- [ ] Taken, partly taken or rejected, with the reason written down and posted to the PR.
- [ ] What missed the project's scheme brought to it while merging.
- [ ] Bugs found outside the PR reported, not fixed on the spot.
- [ ] Merged into `dev` from `task/<tag>`. `main` only with the owner's yes.
- [ ] `python tools/check_content.py --staged` before the commit.
- [ ] Commit message: a plain English sentence, no `feat:`/`fix:`.
+32
View File
@@ -0,0 +1,32 @@
# PR review kit
Rules and background for a tester or reviewer who takes community PRs into `dev`. One goal: no PR breaks the
game's behaviour or costs frames on weak hardware.
## The short version
1. A PR description and "works for me" prove nothing. Read the diff yourself and check it against `refs/`, the
Vulkan spec and upstream issues.
2. Almost every change needs a test: a repro before and after, a probe, a differential test, or whole runs with the
switch off and on.
3. The developer's machine is fast; players run weak machines. "Neutral here" says nothing about them, in either
direction.
4. A player without the problem sees no change: the new path sits behind a switch and the old path stays as it was.
5. Nothing goes into `main` without the project owner's explicit yes. Accepted work lives in `dev` first.
## Files
| File | Covers |
|---|---|
| [01-rules.md](01-rules.md) | triage order, what a PR needs, reasons to reject, merging and commits |
| [02-performance.md](02-performance.md) | target hardware, gain ceiling, how to measure, what the hot path must not do |
| [03-correctness.md](03-correctness.md) | lifter, tests and probes, GPU portability, Maxwell behaviour, timers, threads, game content |
| [04-knowledge.md](04-knowledge.md) | invariants of recent changes, fixed bug classes, open risks, past PR decisions, our mistakes |
| [05-checklist.md](05-checklist.md) | the per-PR checklist |
The public docs this kit builds on: [docs/architecture.md](../../docs/architecture.md),
[docs/building.md](../../docs/building.md) ("Checks"), [docs/maxwell.md](../../docs/maxwell.md),
[docs/settings.md](../../docs/settings.md), [docs/tester-guide.md](../../docs/tester-guide.md), `AGENTS.md`.
Working with a coding agent: give it this folder and `AGENTS.md` before the first review.
State as of 2026-11-06. The code and `docs/` win over this text where they differ.
+215
View File
@@ -0,0 +1,215 @@
# Settings reference
`totk.ini.example` describes the settings for players. This page lists the rest: diagnostics, test hooks, paths and experiments. Set one as `name = value` in `totk.ini`, or as the environment variable `TOTK_NAME`, which wins over the file. Remove the line to go back to the default.
## Safety
- **Safe**: changes only what the row says, with no effect on stability or saves.
- **Caution**: can slow the game, grow files, or change the picture, and undoes cleanly when removed.
- **Risky**: can crash or hang the game, or break the picture or game logic, while it is set.
- **Save risk**: can damage or lose saves, or point the game at other game or save files.
- **Unknown**: the code did not settle it.
Many rows below are developer tools that only act together with another setting or a command-line mode (for example a trace mode or the DLSS path). The row says so where the code does. The rows come from reading the code, and most effects have not been checked in a run.
`tests/settings_doc_check.py` fails when a name in `known[]` (`runtime/cfg.c`) is described neither here nor in `totk.ini.example`, or when a default here differs from the code.
## Diagnostics and logging
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `astc_time` | `1` times the GPU dispatches of each ASTC texture decode and logs the time per block footprint and the largest call. | Adds GPU timestamp queries around every decode; if the queue has no timestamps or no `hostQueryReset`, the log says the decodes are not timed and nothing is measured. | Caution |
| `bind_stats` | `1` counts bindings and descriptors per draw of each stage and how much of the bound state changes between draws. | Adds per-draw bookkeeping to the render thread and extra log output; frames get slower. | Caution |
| `contact_debug` | `1` draws the contact-shadow factor in grey in place of the picture. | The frame shows a grey debug image while it is set; has no effect when contact shadows are off. | Caution |
| `core_diag` | `1` records why ready guest threads wait for a virtual core and for how long, with a histogram every 60 presents. | Turns on host-call and SVC depth tracking and writes many log lines (one per present over 25 ms); frames get slower. | Caution |
| `menu_cursor_log` | `1` logs what the menu cursor code reads at every screen change. | Only adds log lines. | Safe |
| `perf_slow_ms` (default `50`) | Frames at least this many milliseconds long get a `perf: slow frame` log line with the time blocked per cause. | Changes how many frames are logged; a small value fills the log. | Safe |
| `replay_check` | `1` reads every texture read-back a second time after a full drain of the replay queue and compares the two. | Stalls the replay queue at every read-back, so frames get much slower; mismatches are only counted and logged. | Caution |
| `resolve_check` | `1` compares every cached pool lookup with the locked lookup and stops on a difference. | Slows every pool lookup; the game stops if the two ever differ. | Caution |
| `speed_probe` | `1` adds the player's position and ground speed to the frame-rate line that `TOTK_TRACE=perf` prints every 120 presents. | Only adds log text; has no effect without `TOTK_TRACE=perf`. | Safe |
## Profiling and measurement
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `ab` | Splits game time into blocks of this many seconds in the order A B B A and lets settings written as `ab` take effect only in the B blocks. | Settings that accept `ab` flip on and off every few seconds, so frame times and the picture change with the block; without such a setting it does nothing. | Caution |
| `crit` | `<first>-<last>` frame range (with `TOTK_TRACE=sched`) that records every state change and wake of the guest threads, then writes a file at the last frame. | Recording adds overhead to every thread switch in the range and writes one file; `tools/crit_path.py` reads it. | Caution |
| `crit_out` (default `crit.bin`) | Path of the file that `crit` writes. | Only moves the file; nothing is written without `crit`. | Safe |
| `profile` | Path of a sampling profile file; the profiler suspends threads to unwind their stacks. | Frames get slower while it samples and the profile file grows; `tools/prof_report.py` reads it. | Caution |
| `profile_chains` | `1` keeps call chains of every thread in the profile. | Larger profile file and more work per sample; has no effect without `profile`. | Caution |
| `profile_frames` | `<lo>-<hi>` samples only while presents lo to hi-1 are pending; `w<lo>-<hi>` uses world-clock seconds. | Limits the profile to a window; has no effect without `profile`. | Safe |
| `profile_main` | `1` samples only the guest main thread. | Narrows the profile; has no effect without `profile`. | Safe |
| `profile_render` | `1` samples only the render thread and keeps its call chains. | Narrows the profile and makes it larger; has no effect without `profile`. | Caution |
## Capture and frame dumps
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `dump` | Frame number to dump: every render pass target and storage image of that frame is copied out as PNG, with `passes.txt` and `images.txt`. | Writes many PNG files and shader hashes to disk and slows the dumped frame; size is capped by the dump code. | Caution |
| `dump_buf` | `<addr>:<size>[:<off>:<stride>:<w>:<h>]` snapshots one GPU buffer range after every logged pass of a dump. | Writes `buf_<pass>.bin` files and `bufs.txt`; has no effect without `dump`. | Caution |
| `dump_dir` (default `dump`) | Folder that receives the frame dump files. | Only moves the output. | Safe |
| `dump_log` | Frame number; logs the frames from there up to the dumped frame into `passes.txt`, with PNGs only of targets up to 64K texels. | Writes a longer `passes.txt` and more PNGs; has no effect without `dump`. | Caution |
| `flight_at` | Comma-separated frame numbers that trigger a flight-recorder dump. | Writes a dump folder when each frame is reached; has no effect unless the flight recorder (`TOTK_FLIGHT=1`) is on. | Caution |
| `flight_dir` | Folder for flight-recorder dumps; the default is `flight` beside the folder of `totk.exe`. | Only moves the output. | Safe |
| `flight_frames` (default `600`) | Number of presents the flight recorder keeps. | A larger ring keeps more history and more thumbnails in memory; has no effect unless `TOTK_FLIGHT=1`. | Caution |
| `flight_thumbs` (default `1`) | `0` turns off the 320x180 thumbnail the flight recorder saves for each present. | Dumps hold only `frames.tsv`; thumbnails cost a GPU blit per present when on. | Safe |
| `rdoc` | Comma-separated frame numbers to capture with RenderDoc, which is loaded before Vulkan starts. | The game runs under RenderDoc, with its capture options set to keep `VK_EXT_external_memory_host` available and to reference all resources, and captures are written to disk. | Caution |
| `rdoc_dir` | Output folder for RenderDoc captures (the default is `build/captures`). | Only moves the output. | Safe |
| `rdoc_dll` | Path of the RenderDoc library, for when it is not found by default. | A wrong path logs that the library cannot be loaded and no capture is taken. | Safe |
| `shots` | `<lo>-<hi>:<step>` saves every step-th present in the range as `f<n>.png`, scaled to 480x270; `w<lo>-<hi>:<step>` takes them by world-clock seconds. | Writes one PNG per step and adds a readback of the window texture; the pictures clip highlights in HDR. | Caution |
| `shots_dir` (default `.`) | Folder for the `shots` pictures; it is created when needed. | Only moves the output. | Safe |
| `shots_full` | Set to any value to save `shots` at full size instead of 480x270. | Files get far larger and writing them takes longer; has no effect without `shots`. | Caution |
## Automated tests and test hooks
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `vram_limit_mib` | Pretends the device-local memory budget is at most this many MiB, for the paths that react to the budget (`tex_vram_pressure`, mirror refusal); the driver knows nothing of it. | Lets a large card drive those paths; a low value drops released textures and refuses mirrors that would have fit. | Caution |
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
| `input_script` | Path of a file with lines `<frame> <button> [hold]` or `<frame> LSTICK or RSTICK <x> <y> [hold]` that are ORed into the real input. | The game presses those buttons by itself; a file that cannot be opened is logged and ignored. | Caution |
| `menu_test` | `<frame>[:<frames per page>]` opens the menu at that frame, shows each tab for 30 frames (or the given count), saves one screenshot per tab and closes the menu. | The menu opens by itself and screenshots are written. | Caution |
| `menu_test_dir` | Folder for the `menu_test` screenshots; the default is the working directory. | Only moves the output. | Safe |
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
| `probe_movie_bench` | `1` makes the movie probe also time the resampler that fits a movie to a game resolution of another shape, and print the time per frame. | Only used by the movie decoder probe. | Safe |
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
| `test_oom` | `<site>:<n>[,...]` fails the first n allocations at a site: `commit` (guest memory), `staging` (upload buffers), `image` (texture images), `chunk` (draw data buffers). | Each forced failure takes the out-of-memory path: a memory release, a wait of 10 ms per retry and log lines; a large n stalls up to 30 s and then ends the game. | Caution |
| `test_pad` | `<lx>,<ly>,<rx>,<ry>` attaches a virtual gamepad whose sticks rest at these raw SDL axis values (-32768 to 32767). | The game sees a controller with that drift or deflection; a malformed value is logged and ignored. | Caution |
| `test_prepare_shaders` | `1` translates every recorded shader on all usable CPUs during start. | Start takes longer and fills the shader cache folder; the CPU is fully used while it runs. | Caution |
## Input
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `bind_<button>` | Remaps one Switch button or stick direction to `pad:<name>` or `key:<name>` entries; covers `bind_a`, `bind_b`, `bind_x`, `bind_y`, `bind_l`, `bind_r`, `bind_zl`, `bind_zr`, `bind_plus`, `bind_minus`, `bind_left`, `bind_up`, `bind_right`, `bind_down`, `bind_lclick`, `bind_rclick`, `bind_lstick_left`, `bind_lstick_up`, `bind_lstick_right`, `bind_lstick_down`, `bind_rstick_left`, `bind_rstick_up`, `bind_rstick_right`, `bind_rstick_down` and `bind_shake`. | A line replaces every default of that button; button prompts in the game still show the default layout. The Controls tab of the menu writes these lines; `docs/controls.md` lists the entry names. | Safe |
## Paths and files
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `amiibo_dir` | Folder the Amiibo tab starts in; the tab saves the folder you last browsed here. | Only changes which folder the tab lists first. | Safe |
| `exefs` | Folder of the game's ExeFS (with `main.npdm`); a relative path is relative to `totk.exe`'s folder. | Without it, an `exefs` folder next to `totk.exe` is used, else the path set at build time. The game code is compiled into `totk.exe`, and the loader checks the files here by SHA-256, so files from another build or game version stop the start with a message. | Caution |
| `mods_dir` (default `mods`) | Folder that holds one subfolder per player mod, beside `totk.exe`. | Mods found there replace game files; see `docs/mods.md`. | Caution |
| `mods_disabled` | Names of mods that are switched off, separated by `\|`. | Listed mods are not applied; the Mods tab writes this line. | Safe |
| `mods_order` | Mod names, top first, separated by `\|`; the top mod wins a file that two mods replace. | Changes which mod's file the game gets; unlisted mods go below the listed ones, by name. | Safe |
| `pipeline_cache` | Path of the Vulkan pipeline cache file (default `pipeline-cache.bin` beside `totk.exe`). | Moves the cache; a new path starts empty and pipelines are built again. | Caution |
| `profile_dir` | Folder of the game profile, which holds the user account id that save data is named after; the default is a `profile` folder beside `totk.exe`, else the build-time path. | Another folder means a different set of saves. | Save risk |
| `romfs` | Folder of the game's RomFS (with `romfs.bin`); a relative path is relative to `totk.exe`'s folder. | Without it, a `romfs` folder next to `totk.exe` is used, else the build-time path; a missing image stops the start with a message. | Save risk |
| `romfs_index` | Path of `romfs-index.bin`, which `tools/romfs_index.py` wrote; the default is beside `totk.exe`. | The index is checked against the image by SHA-256 and size; a mismatch stops the start. | Caution |
| `romfs_mmap` | `1` maps the RomFS image into memory instead of reading it with positional reads. | Changes how the image is read; the log reports when the mapped path is on. | Caution |
| `romfs_mods` | Folder of an overlay with a `manifest.tsv` of sizes and SHA-256 hashes; its files are served in place of the originals. | A missing file, size or hash mismatch, or a changed original stops the start; player mods win over this overlay. | Save risk |
| `romfs_patches` | Folder of an overlay with a `manifest.tsv`, served in place of the original files; the default is `romfs-patches` beside the index. | Same checks as `romfs_mods`; while it is set the Xbox and PC button prompts are off, and the menu says so. | Save risk |
| `romfs_proof_dir` | Folder for the `.romfs-verified-v1` proof file of a full verification; the default is beside the index. | Only moves one small file. | Safe |
| `romfs_verify` | `full` hashes the whole RomFS image at start; `force` hashes it again even when a proof file exists. | Start takes many minutes for a 17 GB image; a proof file is written so later starts skip the hash. | Caution |
| `shader_cache` | Folder of the translated shader cache (default `shader-cache` beside `totk.exe`). | A new folder starts empty, so shaders are translated again during play. | Caution |
| `shared_fonts` | Folder of the extracted system fonts; the default is `shared-fonts` beside `totk.exe`, else beside the RomFS. | Simplified Chinese, Traditional Chinese and Korean need these fonts: without them the game exits before its first frame in those languages. Other languages do not use them. | Caution |
## Shader translation
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `xlate` | Path of the shader translator program; the default is `totk_xlate` beside `totk.exe`, else under `build/xlate`. | Shaders are translated by that program; if it is missing the log says so and graphics shaders cannot be translated. | Risky |
| `xlate_id` | Text or 64 hex digits that identify the translator build in shader cache keys; the default comes from an `.id` file beside the translator, else a hash of the program. | A value that two different builds share lets the cache serve shaders translated by the other build. | Caution |
| `xlate_keep` (default `0`) | `1` keeps the translation job of a last-frame variant as `<key>.job` with its code. | Writes extra files to the shader cache folder; meant for running the translator by hand. | Caution |
| `xlate_procs` (default `2`) | Number of translator processes allowed during play. | More processes translate new shaders sooner and use more CPU and memory; values below 1 count as 1. | Caution |
## Graphics experiments
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `block_views_3d` (default `0`) | `1` gives 3D block-compressed images block views too. | On some AMD cards a GPU write past the end of the image's memory loses the Vulkan device, and the game stops. | Risky |
| `deswizzle` (default `auto`) | Where block-linear textures are deswizzled: `auto` or `gpu` uses a compute pass where the texture's storage is an imported pool and the CPU elsewhere, `cpu` always uses the CPU, `check` uses the GPU and also deswizzles every upload on the CPU and compares the two. | `cpu` brings back the long frames on entering new areas (the replay thread deswizzles every texture); `check` waits for the GPU at every upload and is for testing only. The log names the mode in use. | Caution |
| `dyn_delta` (default `1`) | Records only the pipeline dynamic states (cull mode, depth and stencil tests, vertex input, blend enables, equations and write masks, ...) whose values changed since the last draw in the command buffer, also across pipeline changes; `0` records the full set at every change of pipeline state, as before; `ab` alternates with the `ab` blocks. | Fewer commands per pipeline change on the render and recorder threads; a driver that loses dynamic state across pipeline binds would draw with stale state. `TOTK_TRACE=perf` prints the calls and commands per submission. | Caution |
| `feedback` | `0` samples a target that an earlier draw of the same pass wrote with no barrier; `restart` ends and restarts the pass behind a full barrier; otherwise the feedback-loop layout is used when the GPU supports it. | `0` brings back the old behaviour that the barrier fixed, so the picture can break in passes that read a target they draw into; `restart` costs extra passes. | Risky |
| `flush_span` (default `0`) | `1` copies one span from the lowest to the highest flushed address of a pool, as an older version did. | Copies far more data to the GPU in crowded towns: an estimated 30-50 ms of GPU time per large copy over PCIe 3.0 x8. | Caution |
| `shadow_snap` (default `1`) | Shadow storage (Linux, Wine): `1` syncs the pools from per-submission write-watch snapshots, each 2 MiB region queried at most once per submission and shared with the texture checks; `0` queries each pool at every part of a submission, as before. | `1` cut the replay thread's write tracking from about 4.4 to 1.3 ms a frame on a Steam Deck; `0` is for comparison runs. Under Wine/Proton the default is `0` until it has been measured there; `1` turns it on. No effect where shadow storage is off (Windows by default). | Caution |
| `freeze_idle` (default `60`) | Number of submissions a 2 MiB region stays unwritten before it is made read-only; `0` turns the freeze off. | A larger value keeps more regions writable and costs more write tracking; a write to a frozen region costs about 60 microseconds. | Caution |
| `gpl_draw_build` (default `1`) | `0` leaves the missing shader libraries of a waiting draw to its background job instead of building them in parallel at once. | Draws whose pipeline is not ready are skipped meanwhile, until the job finishes. | Caution |
| `lib_prebuild_keep` (default `0`) | Number of recorded graphics shader library variants that stay in memory after the boot prebuild. | Each kept library costs 140-460 KB of memory; the driver cache already makes a rebuild cheap. | Caution |
| `minimal_layout` (default `1`) | `0` skips the compare against one-block tiles when a texture's memory layout is chosen. | The code calls it a negative control for fresh sessions; the comment does not say what the picture does. | Unknown |
| `mirror_gap` (default `2`) | Hot pools at most this many 2 MiB regions apart share one write-watch query. | Changes the number of queries per submission; negative values count as 0. | Caution |
| `movie_host` (default `1`) | `0` leaves every movie decoder to the game's own library. | The code comment says that library aborts at the first movie, so the game stops at the first cutscene. | Risky |
| `movie_pace` (default `1`) | `0` hands every decoded movie frame over at the next present instead of when it is due. | Movies play at the game's frame rate rather than their own, so their speed no longer matches the sound. | Caution |
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
| `rec_async` (default `1`) | `0` makes the submitting thread wait until the recorder thread has recorded and submitted each part; `ab` alternates with the `ab` blocks. | With `0` the submitting thread waits for every part (about 0.3 ms a frame in a town on a Ryzen 7900X). | Caution |
| `rec_end` (default `1`) | `0` lets the submitting thread begin, end and submit a recorded part again, as older versions did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
| `rec_spin_us` (default `60`) | Microseconds the recorder thread polls for the next packets before it sleeps; `0` polls 4096 pauses instead; `ab` polls only in the B blocks of `ab`. With fewer than 6 logical processors it always polls 4096 pauses. | A longer poll saves an OS wake-up per packet and takes more CPU time; a shorter one sleeps sooner. | Caution |
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which released textures are dropped, 0 to 100; `0` is off. First those the game has overwritten with a later texture and left alone for 30 s, then those that can be uploaded again. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets the card spill into system memory. Also a slider in the menu. | Caution |
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
| `ub_overlay` (default `1`) | `0` copies each uniform update inside a pass into its buffer at once, ending the pass with a full barrier; `ab` alternates with the `ab` blocks. | With `0` scenes with many updated blocks (the terrain, the sky islands) take longer on the GPU; `TOTK_TRACE=perf` prints how many updates stayed in their pass. | Caution |
| `ub_overlay_out` (default `1`) | `0` copies each uniform update between passes into its buffer at once, behind a full barrier of its own; `ab` alternates with the `ab` blocks. Needs `ub_overlay`. | With `0` a run of updates before a pass costs a full GPU barrier each; `TOTK_TRACE=perf` prints how many updates between passes were overlaid. | Caution |
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
| `vk_subgroup` (default `32`) | Subgroup size to pin where the driver allows it. | A value other than 32 changes what the translator is told about the GPU, and shaders are translated again. | Caution |
| `vram_headroom` (default `1`) | `0` turns off the spare half-block of video memory that a background thread keeps per memory type. | Texture allocations on the render thread can wait for a new block again. | Caution |
| `vs_outs` | `all` declares all 32 vertex shader output locations; otherwise only the union of the paired fragment inputs. | Larger vertex shaders and new pipelines; unwritten outputs read the same as before. | Caution |
| `wb_exact` (default `1`) | `0` decides write-back of GPU-written textures by page only. | A target that shares a page with data the CPU writes can go unwritten, so a name label next to the reticle can blink. | Caution |
| `wb_merge` (default `1`) | `0` copies GPU-written ranges back as plain copies instead of writing word by word the words that changed. | In mirror memory mode the GPU's copy can overwrite words the CPU wrote, which breaks effects such as falling rain and waterfall splashes. | Caution |
## Motion vectors and DLSS
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `dlaa_jitter` (default `1`) | `0` turns off the camera jitter fed to DLSS. | DLSS gets no jitter between frames; a test setting. | Caution |
| `dlaa_maskdebug` (default `0`) | `1` shows the reactive mask in place of the picture. | The frame shows a debug image while it is set. | Caution |
| `dlaa_mv` (default `1`) | `0` sends zero motion vectors to DLSS. | DLSS sees no motion, so moving content can smear; a test setting. | Caution |
| `dlaa_mvdebug` (default `0`) | `1` shows the motion vectors in place of the picture, x in red and y in green. | The frame shows a debug image while it is set. | Caution |
| `dlaa_survey` (default `0`) | `1` logs what is drawn into the light target that DLSS reads. | Adds log output and per-draw work. | Caution |
| `dlaa_times` (default `0`) | `1` times the DLSS passes on the GPU. | Adds GPU timestamps and log lines. | Caution |
| `dlss_reactive` (default `0`) | `1` builds a reactive mask for DLSS where it ran. | Changes how DLSS blends moving areas; a test setting. | Caution |
| `mv_check` | Frame interval; every that many DLSS frames copies the light target and motion images to host memory and logs how well the vectors explain the picture. | Costs a GPU-to-host copy and a comparison on the completion thread at each interval. | Caution |
| `mv_survey` (default `0`) | `1` notes the uniform blocks of each scene draw and compares them with those of earlier frames. | Adds per-draw work and log output. | Caution |
| `mvobj_all` (default `0`) | `1` also lists draws for object motion that read no object blocks (terrain and grass). | More draws are checked and listed, which costs time. | Caution |
| `mvobj_banks` (default `0`) | `1` logs which words of the other uniform banks changed for each scene draw. | Adds log output and per-draw work. | Caution |
| `mvobj_equal` (default `0`) | `1` uses an EQUAL depth compare for the object motion pass, for comparison. | Changes which pixels get object vectors; the code comment names the default as the one that hides objects behind nearer geometry. | Caution |
| `mvobj_exact` | Hex bank mask; only draws whose position uses exactly these banks get object motion. | Fewer draws get object vectors, so their motion falls back to the camera's. | Caution |
| `mvobj_exec` (default `1`) | `0` lists object motion draws but never records them. | Objects get camera motion only. | Caution |
| `mvobj_noc4` (default `0`) | `1` draws again positions that have no scene camera bank. | Those draws get this frame's view, so they can show wrong motion. | Caution |
| `mvobj_only` | Hex bank mask; only draws whose position reads no bank outside the mask get object motion. | Fewer draws get object vectors. | Caution |
| `mvobj_times` (default `0`) | `1` adds timings to the object-motion summary. | The clock reads themselves cost frame time. | Caution |
| `mvprev_check` (default `0`) | `1` verifies the stored first bytes of each previous-frame table entry. | The code comment measures 3.6 ms per frame. | Caution |
| `mvprev_grow` (default `1`) | `0` keeps only three banks in the previous-frame tables. | Object motion that depends on the other banks is no longer tracked. | Caution |
| `mvprev_skip` | Hex bank mask of banks left out of the previous-frame copy, replacing the default. | Different banks are tracked, which changes object motion. | Caution |
| `mvprev_trust` | Hex bank mask of banks whose found previous-frame copy is taken without the check. | A copy that does not match is used anyway, which can give wrong motion. | Caution |
## Scheduling and host
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `app_pool` | Size in bytes of the guest application memory pool; accepts decimal or `0x` hex. | The default depends on the resolution settings; `0` stops the start with a message, and too small a pool leaves the game without memory. | Risky |
| `audio_off` | Any value mixes audio without opening an output device. | The game runs in silence and reports two channels; same as `audio = none`. | Caution |
| `kspin_us` (default `20`) | Microseconds a guest wait or a wait for a virtual core polls before it sleeps; `0` turns the poll off; `ab` polls only in the B blocks of `ab`. Off with fewer than 6 logical processors. | A wake within the poll costs no OS wake-up; a longer poll takes more CPU time from other threads. | Caution |
| `mem_reserve_mib` (default `1536`) | MiB of system memory kept free for Windows, other programs and the game's own heap when the renderer sizes what it keeps in device memory, where device memory takes system memory (integrated GPUs, and discrete GPUs whose driver charges it to the process). | A larger value keeps fewer released textures and spare blocks when the system runs short; a smaller one lets the game use more before it holds back. | Caution |
| `one_per_core` (default `0`) | `1` keeps the process on the first logical processor of each physical core; `ab` switches it with the `ab` blocks. | Busy threads stop sharing a core with a sibling thread, which can help or hurt on the CPU; it only applies when the CPU has hyperthreads. | Caution |
| `pcore` (default `1`) | On a hybrid CPU (performance and efficiency cores), the game's main and render threads and the replay, recorder and present threads prefer the performance cores; `0` turns that off. | With `0` Windows may run those threads on efficiency cores; no effect on a CPU with one kind of core. The log says which cores were found. | Caution |
| `reexec` | Counter of address-space retries that the runtime sets itself when it starts the process again. | The counter is cleared when the window is recreated; a value of 20 or more stops the start with a fatal error. | Risky |
| `replay_chunks` (default `0`) | Most chunks a submission is cut into when `replay_workers` is set; `0` uses twice the number of recording threads. | Fewer chunks leave threads idle at the end of a submission, more add scan and submit work per chunk; no effect while `replay_workers` is `0`. | Caution |
| `replay_chunk_draws` (default `128`) | Least draws a chunk of the chunked replay holds before it may end at a pass. | Small values make many short chunks, large ones leave big submissions on few threads; no effect while `replay_workers` is `0`. | Caution |
| `replay_state_check` (default `0`) | `1` compares the replay state after the last chunk of a chunked submission with the state of the scan and logs differences. | Adds a comparison per submission and log lines on a mismatch; no effect while `replay_workers` is `0`. | Caution |
| `replay_skew_us` (default `0`) | Debug: the even chunks of a chunked submission wait this many microseconds before they record, so the chunk after each one records first. | Shows a recording that depends on which thread gets to a chunk first in every submission; slows the replay; no effect with fewer than 2 `replay_workers`. | Caution |
| `restart_wait` | Set by the runtime when it restarts itself; names the old process by id and creation time so the new process waits for it to exit. | Meant for the runtime's own use; the code does not describe a manual value. | Unknown |
| `spin` (default `1`) | `0` makes a guest thread that polls memory keep its virtual core. | Polling threads can starve the thread whose item they wait for, so frames can stall. | Caution |
| `spin_handoff_us` (default `50`) | Microseconds a polling wait lasts before it gives its virtual core to a waiting thread. | A shorter value hands cores over sooner; a longer one keeps them longer. | Caution |
| `tex_block_mib` (default `64`) | Block size in MiB of the pool that holds the game's textures; images of a quarter block or more get memory of their own. `0` puts textures in the default pools with the buffers. | Larger blocks hold more memory that no texture uses; smaller ones mean more allocations from the driver. | Caution |
| `tex_released_mib` (default `256`) | MiB of textures the game released that are kept for reuse; older ones beyond it are dropped. `0` keeps up to 2048 of them, whatever their size. | A larger value keeps more device memory and process commit; a smaller one makes the game create more textures again. | Caution |
| `vma_block_mib` (default `64`) | Block size in MiB that the Vulkan memory allocator takes from the driver for buffers and other images on heaps over 1 GiB; larger allocations than half a block get memory of their own. `0` uses the allocator's 256. | Larger blocks hold more memory that nothing uses; smaller ones mean more allocations from the driver. | Caution |
## Generated-code patches
These names are code patch sets built into the generated game code (`recomp/patches-*.txt`). `rt_patches_init` reads the environment name at start, then the resolution and mouse code switches some sets itself.
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `aspect_patch` (default `0`) | Patch set that feeds the camera the output's aspect ratio when it is not 16:9. | The resolution code switches this set on or off itself at start, so a value here is replaced; the code does not settle what a manual value does. | Unknown |
| `fixed_res_patch` (default `0`) | Patch set that holds the game's dynamic resolution at 100 percent (the scene tier). | The resolution code switches this set from `dynamic_res` (on at `0`); a manual value is replaced. | Unknown |
| `fov_patch` (default `0`) | Patch set that scales the camera's vertical view angle by the field-of-view setting. | The resolution code switches this set itself whenever the field of view is not 100 percent; a manual value is replaced. | Unknown |
| `mouse_direct_patch` (default `1`) | Patch set of the hooks in the game's camera code behind `mouse_mode = direct` on game 1.4.3. | `0` stops `mouse_mode = direct` from taking effect; other mouse modes are unaffected. | Safe |
| `patch_output` (default `0`) | Patch set that makes the game use the configured output size for its swapchain and display configuration. | The resolution code switches this set whenever the output differs from the base size; a manual value is replaced. | Unknown |
| `patch_scene` (default `0`) | Patch set that makes the game use the configured scene render size. | The resolution code switches this set from the render size setting; a manual value is replaced. | Unknown |
+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])
+109
View File
@@ -0,0 +1,109 @@
"""The display scale of the installer window: process DPI awareness and the pixel sizes that follow it.
Windows draws a DPI-unaware process at 96 DPI and stretches its windows to the display's scale, which is what made the
setup window blurry at 125% and above. gui.py calls enable() before it imports tkinter, so the process is system DPI
aware before its first window exists; Tk then reads the display's DPI when it starts (`tk scaling` is 2.667 at 200%)
and every font size in points comes out at the right size. The pixel sizes of the layout have no such scaling of their
own, so gui.py passes them through px().
System DPI awareness, not per-monitor: Tk 9.0.4, the one the release bundles, has no per-monitor support (its DLL
imports no DPI-awareness function), so it draws everything at the system DPI. Per-monitor awareness would leave the
window crisp on a display at another scale but with the primary display's font sizes; system awareness draws the
primary display exactly and lets Windows stretch the window only on a display whose scale differs from it, as before.
On Linux the window stays 1:1 (px returns n as it is): Tk and the desktop scale what they draw.
"""
import ctypes
import os
DPI_96 = 96 # the scale Windows calls 100%
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
PROCESS_SYSTEM_DPI_AWARE = 1 # SetProcessDpiAwareness' value for the same thing (Windows 8.1)
LOGPIXELSX = 88 # GetDeviceCaps' horizontal DPI, the only source before Windows 10 1607
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 # ctypes.WinDLL's winmode: System32 only, never the installer's folder
_scale = None
def enable():
"""Make the process system DPI aware, then return the display scale (1.0 at 96 DPI). Must run before the first
window; a later call only returns the scale."""
global _scale
if _scale is None:
dpi = _system_dpi() if os.name == 'nt' else DPI_96
_scale = dpi / DPI_96
return _scale
def scale():
"""The display scale: 1.0 at 100%, 2.0 at 200%."""
return enable()
def px(n):
"""n pixels at 100% as this display's pixels: 14 stays 14 at 100% and becomes 28 at 200%."""
return int(round(n * enable()))
def awareness():
"""What Windows says the process is: 0 unaware, 1 system aware, 2 per-monitor aware."""
if os.name != 'nt':
return 0
try: # Windows 10 1607
user32 = ctypes.WinDLL('user32')
user32.GetThreadDpiAwarenessContext.restype = ctypes.c_void_p
user32.GetAwarenessFromDpiAwarenessContext.argtypes = [ctypes.c_void_p]
return int(user32.GetAwarenessFromDpiAwarenessContext(user32.GetThreadDpiAwarenessContext()))
except (AttributeError, OSError):
pass
try: # Windows 8.1
value = ctypes.c_int()
shcore = ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
shcore.GetProcessDpiAwareness.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
if shcore.GetProcessDpiAwareness(None, ctypes.byref(value)) == 0:
return value.value
except (AttributeError, OSError):
pass
try: # Windows Vista
return 1 if ctypes.WinDLL('user32').IsProcessDPIAware() else 0
except (AttributeError, OSError):
return 0
def _system_dpi():
"""Make the process system DPI aware and return the display DPI. Each call that this Windows version has is tried;
one failing means the process already is aware (a manifest, or the call before it) and changes nothing."""
try:
user32 = ctypes.WinDLL('user32', use_last_error=True)
except OSError:
return DPI_96
try: # Windows 10 1607, and the only way to ask for the system context explicitly
user32.SetProcessDpiAwarenessContext.argtypes = [ctypes.c_void_p]
user32.SetProcessDpiAwarenessContext.restype = ctypes.c_bool
user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE))
except (AttributeError, OSError):
pass
try: # Windows 8.1
ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32).SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
except (AttributeError, OSError):
pass
try: # Windows Vista
user32.SetProcessDPIAware()
except (AttributeError, OSError):
pass
try: # Windows 10 1607
return int(user32.GetDpiForSystem()) or DPI_96
except (AttributeError, OSError):
pass
try: # before that
gdi32 = ctypes.WinDLL('gdi32')
user32.GetDC.restype = ctypes.c_void_p
user32.GetDC.argtypes = [ctypes.c_void_p]
user32.ReleaseDC.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
gdi32.GetDeviceCaps.argtypes = [ctypes.c_void_p, ctypes.c_int]
dc = user32.GetDC(None)
dpi = gdi32.GetDeviceCaps(ctypes.c_void_p(dc), LOGPIXELSX)
user32.ReleaseDC(None, ctypes.c_void_p(dc))
return dpi or DPI_96
except (AttributeError, OSError):
return DPI_96
+133 -50
View File
@@ -3,7 +3,8 @@
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
events. Decisions live in guilogic.py; this file only draws. The window text is in
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids.
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids. On Windows the window follows the
display's DPI (installer/dpi.py): fonts and paddings grow with the scale, so Windows does not stretch it.
--selftest [--shots DIR] build every screen in every language and close by itself (PNG per screen with --shots)
--lang en|ru|pt-BR|zh-Hans window language (default: the Windows display language)
@@ -15,9 +16,18 @@ import json
import os
import subprocess
import sys
import tkinter as tk
import tempfile
import threading
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from . import dpi
# Windows fixes the DPI awareness of a process when its first window is created, and Tk reads the display's DPI when it
# starts: both happen inside the first tkinter.Tk(), so this call comes before tkinter is imported at all.
dpi.enable()
import tkinter as tk # noqa: E402 (after dpi.enable, see above)
from tkinter import filedialog, messagebox, ttk # noqa: E402
try:
from PIL import ImageGrab # only the screenshots of --selftest --shots need it; the release has no PIL
@@ -26,13 +36,28 @@ except ImportError:
from . import ini
from . import layout as layouts
from . import recent
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGE_LABELS, UI_LANGUAGES, Catalog, Form, Model,
Process, amount, default_game_language, describe_event, parse_line, windows_ui_language)
POLL_MS = 100
GLYPH = {PENDING: '\u25cb', RUNNING: '\u25b6', DONE: '\u2714', SKIPPED: '\u2013', FAILED: '\u2716'}
COLOR = {PENDING: '#808080', RUNNING: '#1a5fb4', DONE: '#26a269', SKIPPED: '#808080', FAILED: '#c01c28'}
PAD = 14
# The window's pixels at 100%: Tk scales the fonts by itself (tk scaling), these through installer/dpi.py.
PAD = dpi.px(14) # padding of the window and its frames
WRAP = dpi.px(620) # wrap width of the long labels
RECENT_WAIT = 1.5 # seconds the window waits for the recorded paths
def load_recent(wait=RECENT_WAIT):
"""recent.load() in a daemon thread, waited for at most `wait` seconds: a recorded path on a disconnected network
share can block its stat call for a long time. Nothing is prefilled when it has not returned by then."""
out = {}
thread = threading.Thread(target=lambda: out.update(recent.load()), daemon=True)
thread.start()
thread.join(wait)
return {} if thread.is_alive() else out
def fix_tcl_paths():
@@ -57,6 +82,11 @@ class App:
self.bound = [] # (widget, option, id, params) retranslated on a language switch
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
self.vars['lang'] = tk.StringVar()
self.recorded = {} # base/update paths prefilled from the recorded ones, by field
self.touched = set() # fields the user picked or typed
self.programmatic = False
for name in ('base', 'update'):
self.vars[name].trace_add('write', lambda *_, n=name: self.touched.add(n) if not self.programmatic else None)
self.lang_code = default_game_language(ui_lang)
self.screen = None
self.fitted = None # the (width, height) fit last gave the window
@@ -74,7 +104,7 @@ class App:
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
style = ttk.Style(root)
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=dpi.px(8))
style.configure('Err.TLabel', foreground='#c01c28')
style.configure('Version.TLabel', foreground='#808080')
@@ -84,7 +114,7 @@ class App:
self.title.pack(side='left')
version = ttk.Label(top, style='Version.TLabel')
self.bind_text(version, 'text', 'ui.version', version=layout.release)
version.pack(side='left', padx=(10, 0), pady=(6, 0))
version.pack(side='left', padx=(dpi.px(10), 0), pady=(dpi.px(6), 0))
sw = ttk.Frame(top)
sw.pack(side='right')
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
@@ -103,6 +133,15 @@ class App:
self.bind_text(self.title, 'text', 'ui.rebuild_title')
self.vars['dest'].set(str(rebuild))
root.after(POLL_MS, self.start)
else:
# recorded sources fill only empty fields; dest goes last, its trace runs refresh_form (update hint and
# recorded language). In update mode filled files are harmless: Extract.current hashes one only when its
# size or mtime differ from the recorded ones.
last = load_recent()
self.recorded = {n: last[n] for n in ('base', 'update') if n in last and not self.vars[n].get().strip()}
for name in ('base', 'update', 'keys', 'firmware', 'dest'):
if name in last and not self.vars[name].get().strip():
self.set_prefill(name, last[name])
# ---- text -----------------------------------------------------------------------------------------------------
@@ -185,19 +224,20 @@ class App:
self.root.geometry(f'{max(w, self.root.winfo_width())}x{max(h, self.root.winfo_height())}')
def notice(self, parent):
w = ttk.Label(parent, style='Notice.TLabel', wraplength=620, justify='left')
w = ttk.Label(parent, style='Notice.TLabel', wraplength=WRAP, justify='left')
self.bind_text(w, 'text', 'ui.notice')
return w
def build_form(self):
f = ttk.Frame(self.body)
gap = dpi.px(4) # between the form's rows
bar = ttk.Frame(f) # packed first so pack shrinks the text above it, never the buttons
bar.pack(side='bottom', fill='x', pady=(12, 0))
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
self.start_button = self.button(bar, 'ui.start', self.start)
self.start_button.pack(side='right')
self.form_intro = self.label(f, 'ui.intro', wraplength=620, justify='left')
self.form_intro.pack(anchor='w', pady=(0, 8))
self.notice(f).pack(fill='x', pady=(0, 10))
self.form_intro = self.label(f, 'ui.intro', wraplength=WRAP, justify='left')
self.form_intro.pack(anchor='w', pady=(0, dpi.px(8)))
self.notice(f).pack(fill='x', pady=(0, dpi.px(10)))
grid = ttk.Frame(f)
grid.pack(fill='x')
grid.columnconfigure(1, weight=1)
@@ -206,37 +246,44 @@ class App:
self.form_rows = {}
for i, (name, label_id, pick_id) in enumerate(rows):
lab = self.label(grid, label_id)
lab.grid(row=i, column=0, sticky='w', pady=4, padx=(0, 10))
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=4)
lab.grid(row=i, column=0, sticky='w', pady=gap, padx=(0, dpi.px(10)))
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=gap)
b = self.button(grid, 'ui.browse', lambda n=name, p=pick_id: self.pick_file(n, p))
b.grid(row=i, column=2, padx=(8, 0))
b.grid(row=i, column=2, padx=(dpi.px(8), 0))
self.form_rows[name] = lab
i = len(rows)
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=gap)
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=gap)
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(dpi.px(8), 0))
i += 1
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=gap)
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
self.lang_box.grid(row=i, column=1, sticky='w', pady=gap)
self.lang_touched = False
self.lang_box.bind('<<ComboboxSelected>>', lambda _e: setattr(self, 'lang_touched', True))
self.fill_languages()
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.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 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')
self.form_error.pack(anchor='w', pady=(8, 0))
self.label(f, 'ui.keys_private', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=gap)
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=gap)
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(dpi.px(8), 0))
self.label(f, 'ui.compressed_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
self.label(f, 'ui.firmware_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(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=(dpi.px(8), 0))
self.label(f, 'ui.dlss_help', wraplength=WRAP, justify='left').pack(anchor='w', padx=(dpi.px(22), 0))
self.update_hint = ttk.Label(f, wraplength=WRAP, justify='left')
self.update_hint.pack(anchor='w', pady=(dpi.px(8), 0))
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=WRAP, justify='left')
self.form_error.pack(anchor='w', pady=(dpi.px(8), 0))
self.label(f, 'ui.keys_private', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
return f
def build_run(self):
f = ttk.Frame(self.body)
bar = ttk.Frame(f)
bar.pack(side='bottom', fill='x', pady=(8, 0))
bar.pack(side='bottom', fill='x', pady=(dpi.px(8), 0))
self.cancel_button = self.button(bar, 'ui.cancel', self.cancel)
self.cancel_button.pack(side='right')
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
@@ -245,14 +292,22 @@ class App:
self.run_header.pack(anchor='w')
self.step_rows = {}
self.steps_box = ttk.Frame(f)
self.steps_box.pack(fill='x', pady=(8, 8))
self.phase_label = ttk.Label(f, wraplength=620, justify='left')
self.steps_box.pack(fill='x', pady=dpi.px(8))
self.phase_label = ttk.Label(f, wraplength=WRAP, justify='left')
self.phase_label.pack(anchor='w')
self.bar = ttk.Progressbar(f, mode='determinate', maximum=1000)
self.bar.pack(fill='x', pady=(4, 6))
self.space_label = ttk.Label(f, wraplength=620, justify='left')
bar_holder = ttk.Frame(f)
bar_holder.pack(fill='x', pady=(dpi.px(4), dpi.px(6)))
self.bar = ttk.Progressbar(bar_holder, mode='determinate', maximum=1000)
self.bar.pack(fill='both', expand=True)
if os.name == 'nt' and ttk.Style(f).theme_use() == 'vista':
# The vista theme draws the bar 22 px high at every DPI and has no thickness option: the holder gives it the
# height it would have at 100%, scaled with the display. Every other theme draws its own height, and the
# holder follows it.
bar_holder.pack_propagate(False)
bar_holder.configure(height=dpi.px(22))
self.space_label = ttk.Label(f, wraplength=WRAP, justify='left')
self.space_label.pack(anchor='w')
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
self.warn_label = ttk.Label(f, wraplength=WRAP, justify='left', foreground='#9a6700')
self.warn_label.pack(anchor='w')
self.log_frame = ttk.Frame(f)
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
@@ -266,33 +321,60 @@ class App:
def build_finish(self):
f = ttk.Frame(self.body)
bar = ttk.Frame(f)
bar.pack(side='bottom', fill='x', pady=(12, 0))
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
self.button(bar, 'ui.close', self.on_close).pack(side='right')
self.open_button = self.button(bar, 'ui.open_folder', self.open_folder)
self.open_button.pack(side='right', padx=(0, 8))
self.open_button.pack(side='right', padx=(0, dpi.px(8)))
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
self.back_button.pack(side='left')
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, dpi.px(8)))
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=WRAP, justify='left')
self.finish_title.pack(anchor='w')
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
self.finish_text.pack(anchor='w', pady=(8, 8))
self.finish_text = ttk.Label(f, wraplength=WRAP, justify='left')
self.finish_text.pack(anchor='w', pady=dpi.px(8))
self.finish_steps = ttk.Frame(f)
self.finish_steps.pack(fill='x', pady=(0, 8))
self.finish_steps.pack(fill='x', pady=(0, dpi.px(8)))
self.finish_notice = self.notice(f)
self.finish_notice.pack(fill='x', pady=(8, 0))
self.finish_notice.pack(fill='x', pady=(dpi.px(8), 0))
return f
# ---- form -----------------------------------------------------------------------------------------------------
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 set_prefill(self, name, value):
self.programmatic = True
try:
self.vars[name].set(value)
finally:
self.programmatic = False
def match_prefill(self, state):
"""Prefilled base/update stay only while the chosen install recorded the same file (name and size, one stat);
another install would otherwise extract again from them. Fields the user touched are left alone."""
for name, value in self.recorded.items():
if name in self.touched:
continue
keep = True
if state is not None:
known = (state.get('inputs') or {}).get(name)
try:
keep = (isinstance(known, dict) and Path(value).name == known.get('name')
and Path(value).stat().st_size == known.get('size'))
except (OSError, ValueError):
keep = False
want = value if keep else ''
if self.vars[name].get() != want:
self.set_prefill(name, want)
def refresh_form(self):
form = self.form()
state = form.installed_state()
self.match_prefill(state)
if state is not None:
self.update_hint.configure(text=self.cat('ui.update_mode'))
recorded = ini.canonical_language(state.get('lang'))
@@ -303,9 +385,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)
@@ -332,6 +413,7 @@ class App:
self.fit()
return
self.form_error.configure(text='')
recent.save(form.dest, {name: getattr(form, name) for name in recent.SOURCES})
# in a repository the Python running this window: the tools' embeddable Python there has no ._pth entry for
# the repository, so `-m installer` would not find the package
python = Path(sys.executable) if self.layout.dev else self.layout.python
@@ -358,7 +440,7 @@ class App:
self.step_rows = {}
for s in steps:
row = ttk.Frame(self.steps_box)
row.pack(fill='x', pady=1)
row.pack(fill='x', pady=dpi.px(1))
glyph = ttk.Label(row, width=3)
glyph.pack(side='left')
name = ttk.Label(row)
@@ -443,7 +525,7 @@ class App:
def toggle_log(self):
self.log_open = not self.log_open
if self.log_open:
self.log_frame.pack(fill='both', expand=True, pady=(8, 0))
self.log_frame.pack(fill='both', expand=True, pady=(dpi.px(8), 0))
else:
self.log_frame.pack_forget()
self.bound = [b for b in self.bound if b[0] is not self.log_toggle]
@@ -537,7 +619,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': {}},
@@ -580,6 +662,7 @@ def grab(root, path):
def selftest(layout, shots=None):
"""Every screen in every language; the window closes itself. Returns 0 when nothing raised."""
fix_tcl_paths()
os.environ[recent.ENV] = os.path.join(tempfile.mkdtemp(prefix='totk-selftest-'), 'setup.json')
root = tk.Tk()
root.geometry('+60+40')
app = App(root, 'en', layout)
+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
+5 -1
View File
@@ -3,7 +3,10 @@
No console window. The two message strings come from the catalog (installer/text/<lang>.json, ids launcher.title and
launcher.python_missing); tools/make_release.py writes them into launcher_text.h before it compiles this file.
Build: llvm/bin/llvm-windres -I runtime -O coff launcher.rc -o launcher_rc.o, then
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe */
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe
System DPI aware, like the window's Python process (installer/dpi.py): Windows draws the error box of fail() at the
display's scale instead of stretching it, which it would at 125% and above. */
#include <windows.h>
#include <wchar.h>
@@ -38,6 +41,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, i
STARTUPINFOW si = { sizeof si };
PROCESS_INFORMATION pi;
(void)instance; (void)previous; (void)command_line; (void)show;
SetProcessDPIAware(); /* before the message box exists; the call fails harmlessly when a manifest set it */
if (n == 0 || n >= PATH_CHARS) { fail(); return 1; }
while (n > 0 && folder[n - 1] != L'\\') n--;
folder[n > 0 ? n - 1 : 0] = 0;
+15 -3
View File
@@ -12,6 +12,8 @@ Release (docs/installer.md "Release layout"), root = the unpacked folder:
include/ + lib/ of Vulkan, SDL3, Opus, libvpx and VMA (build_game.py --deps)
bin/ totk_xlate.exe (+ totk_xlate.id), hactool.exe, ninja.exe, SDL3.dll, libopus-0.dll
data/shader_list.tsv the MacNativeGames shader list
data/pgo-<v>.profdata the execution profile the update's code is compiled with (tools/pgo.py; Windows only);
without one the game is built at -O1
totk.ini.example
VERSION the release version, one line
temp/ the default temp folder (created)
@@ -24,7 +26,7 @@ Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
runtime/ as the sources with build/install-rtlib/libtotkrt.a and libtotkrt-v2.a (tools/build_game.py --make-runtime-lib
--out build/install-rtlib, once per --cpu); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
refs/MacNativeGames; build/pgo/pgo-<v>.profdata when there is one; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
"""
import os
@@ -66,6 +68,7 @@ class Layout:
temp: Path | None = None
sysroot: Path | None = None # Linux release: what the game links against, instead of the host's
runtime_lib_v2: Path | None = None # libtotkrt-v2.a: the runtime for CPUs without AVX2 (build_game.py --cpu v2)
pgo_dir: Path | None = None # pgo-<v>.profdata (tools/pgo.py); None: no profiles (Linux)
@property
def clang(self):
@@ -79,6 +82,12 @@ class Layout:
return lib
return None if self.dev else lib
def pgo(self, version):
"""The execution profile build_game.py --pgo compiles the update's code with, None without one (the game is
then built at -O1)."""
p = self.pgo_dir / f'pgo-{version_tag(version)}.profdata' if self.pgo_dir else None
return p if p and p.is_file() else None
def patches(self, version):
"""The code patch table for recomp/gen.py."""
return self.recomp / f'patches-{version_tag(version)}.txt'
@@ -90,7 +99,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',
@@ -128,6 +138,7 @@ def release_layout(root):
xlate=root / 'bin' / ('totk_xlate' + EXE),
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example',
pgo_dir=root / 'data' if WIN else None,
# Linux: None, the install folder's .setup-temp (installer/cli.py): the AppImage is mounted read-only, and on
# a Steam Deck /home is often too full for the generated C while the install folder is on the SD card
temp=root / 'temp' if WIN else None,
@@ -153,7 +164,8 @@ def dev_layout(root):
ninja=UCRT / ('ninja' + EXE), hactool=root / 'toolchain' / 'hactool' / ('hactool' + EXE),
xlate=root / 'build' / 'xlate' / ('totk_xlate' + EXE),
dlls=tuple(sorted((deps / 'bin').glob('*.dll'))),
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp',
pgo_dir=root / 'build' / 'pgo')
def dev_commit(root):
+86
View File
@@ -0,0 +1,86 @@
"""The last install folder and source paths, in a per-user file, so an update, a reinstall or a switch to another game
version starts with the form filled in.
{"format": 1, "dest": ..., "base": ..., "update": ..., "keys": ..., "firmware": ...}
Absolute paths only. The keys entry is the path of the keys file, never its contents.
Windows: %LOCALAPPDATA%\TotK Recomp\setup.json; elsewhere $XDG_STATE_HOME/totk-recomp/setup.json (default
~/.local/state/totk-recomp/setup.json). TOTK_SETUP_RECENT names another file (tests, --selftest). Nothing else is
stored: the language comes from the install's own totk-install.json. A file of format 0 (only "dest") still loads.
"""
import json
import os
import sys
from pathlib import Path
from . import state
FORMAT = 1
ENV = 'TOTK_SETUP_RECENT'
FILES = ('base', 'update', 'keys') # recorded when they are files
SOURCES = FILES + ('firmware',)
FIELDS = ('dest',) + SOURCES
def path():
override = os.environ.get(ENV)
if override:
return Path(override)
if sys.platform == 'win32':
base = os.environ.get('LOCALAPPDATA') or str(Path.home() / 'AppData' / 'Local')
return Path(base) / 'TotK Recomp' / 'setup.json'
base = os.environ.get('XDG_STATE_HOME') or str(Path.home() / '.local' / 'state')
return Path(base) / 'totk-recomp' / 'setup.json'
def usable(name, value):
"""value when it is a non-empty absolute path of the right kind for the field, else None. One stat call per
path, no file is opened or hashed; a path the OS rejects (embedded NUL, unreachable share) counts as missing."""
if not isinstance(value, str) or not value or not os.path.isabs(value):
return None
try:
p = Path(value)
if name == 'dest':
ok = (p / state.NAME).is_file()
elif name == 'firmware':
ok = p.is_dir()
else:
ok = p.is_file()
except (OSError, ValueError):
return None
return value if ok else None
def read():
try:
data = json.loads(path().read_text(encoding='utf-8'))
except (OSError, ValueError):
return {}
return data if isinstance(data, dict) else {}
def load():
"""The recorded entries that still exist, by field name. Never raises."""
data = read()
return {name: data[name] for name in FIELDS if usable(name, data.get(name))}
def save(dest, sources=None):
"""Records dest and the non-empty, existing entries of sources (base, update, keys, firmware); an entry left out
or empty keeps its saved value. Atomic; a failure to write is ignored, the window works without it."""
target = path()
tmp = target.with_name(target.name + '.tmp')
data = {name: v for name, v in read().items() if name in FIELDS and isinstance(v, str)}
data['dest'] = os.path.abspath(dest)
for name, value in (sources or {}).items():
if name in SOURCES and isinstance(value, str) and value.strip():
value = os.path.abspath(value.strip())
if usable(name, value):
data[name] = value
try:
target.parent.mkdir(parents=True, exist_ok=True)
tmp.write_text(json.dumps({'format': FORMAT, **data}), encoding='utf-8')
os.replace(tmp, target)
except OSError:
pass
+259 -39
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,23 @@ 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/, romfs-patches-pc/, romfs-patches-<style>-<w>x<h>/ (each of the three for each shaped size),
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).
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status and exefs.data (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 +33,7 @@ import subprocess
import sys
import threading
import time
import urllib.request
from pathlib import Path
from . import ini
@@ -36,6 +42,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)
@@ -162,7 +169,8 @@ def run_tool(ctx, step, script, args, parse=None, cwd=None):
def free_bytes(path):
"""Free bytes on the disk of path (its nearest existing parent) and that parent's device."""
p = Path(path)
while not p.exists():
# An unavailable drive or share has no existing ancestor: let disk_usage report its OS error at the root.
while not p.exists() and p.parent != p:
p = p.parent
return shutil.disk_usage(p).free, os.stat(p).st_dev
@@ -174,6 +182,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 +198,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 +465,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 +612,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 +734,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 +746,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 +758,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 +780,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 +802,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-'
@@ -762,6 +858,12 @@ def exefs_status(ctx):
return ctx.dest / 'build-cache' / 'exefs.status'
def exefs_data(ctx):
"""The data writes of the mods' ExeFS patch files (tools/exefs_patches.py --data), which runtime/mods.c stores into
main before the game runs."""
return ctx.dest / 'build-cache' / 'exefs.data'
def cheats_status(ctx):
"""The build's cheat status file (tools/cheats.py --status), which runtime/cheats.c reads: in build-cache, not in a
mod's own cheats folder, which the player (or TKMM's merge) may empty."""
@@ -776,7 +878,7 @@ class Lift(Step):
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
files = cheat_files(ctx) + exefs_files(ctx)
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
return fingerprint('lift-5', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
@@ -797,36 +899,54 @@ class Lift(Step):
if m:
rep.progress('lift', 'emit', int(m.group(1)), int(m.group(2)), 'chunks')
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets (tools/cheats.py,
# tools/exefs_patches.py); their status files tell the runtime what the build has. Both are the player's
# downloads, so files the tools refuse cost those cheats or patches, not the install: the game is built
# without them and the setup warns.
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets and data writes
# (tools/cheats.py, tools/exefs_patches.py); their status files tell the runtime what the build has. Both are
# the player's downloads, so files the tools refuse cost those cheats or patches, not the install: the game is
# built without them and the setup warns.
plain = layout.patches(game_version(ctx))
stages = [('cheats.py', 'patches-cheats.txt', cheats_status(ctx), cheat_files(ctx), 'warn.cheats_skipped'),
('exefs_patches.py', 'patches-exefs.txt', exefs_status(ctx), exefs_files(ctx), 'warn.exefs_skipped')]
# (tool, patch file it writes, its outputs for the runtime: the status first, then the data file, the files it
# reads, the warning when it fails)
stages = [('cheats.py', 'patches-cheats.txt', [cheats_status(ctx)], cheat_files(ctx), 'warn.cheats_skipped'),
('exefs_patches.py', 'patches-exefs.txt', [exefs_status(ctx), exefs_data(ctx)], exefs_files(ctx),
'warn.exefs_skipped')]
patches, skipped = plain, {}
for tool, out, status, inputs, warn in stages:
status.unlink(missing_ok=True) # no status: the runtime treats every such file as not built
def drop(outputs): # no status: the runtime treats every such file as not built
for f in outputs:
f.unlink(missing_ok=True)
for tool, out, outputs, inputs, warn in stages:
drop(outputs)
if not inputs:
continue
code, log = run_tool(ctx, 'lift', layout.tools / tool,
['--exefs', ctx.dest / 'exefs', '--mods', ctx.dest / 'mods', '--patches', patches,
'--out', ctx.temp / out, '--status', status])
'--out', ctx.temp / out, '--status', outputs[0],
*(['--data', outputs[1]] if len(outputs) > 1 else [])])
if code:
status.unlink(missing_ok=True)
drop(outputs)
skipped[warn] = log
else:
patches = ctx.temp / out
args = ['--exefs', ctx.dest / 'exefs', '--out', gen]
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', patches], parse)
if code and patches != plain:
for tool, out, status, inputs, warn in stages:
for tool, out, outputs, inputs, warn in stages:
if inputs and warn not in skipped:
status.unlink(missing_ok=True)
drop(outputs)
skipped[warn] = log
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', plain], parse)
if code:
raise InstallError('lift.failed', code=code, log=str(log))
# The mods' data writes must stay off the bytes the lifted code reads as constants (its jump tables), which only
# the lift knows: gen.py lists them in folded.txt, and a file that writes them becomes unsupported
if exefs_data(ctx).is_file():
code, log = run_tool(ctx, 'lift', layout.tools / 'exefs_patches.py',
['--exefs', ctx.dest / 'exefs', '--status', exefs_status(ctx), '--data',
exefs_data(ctx), '--folded', gen / 'main' / 'folded.txt'])
if code:
drop([exefs_status(ctx), exefs_data(ctx)])
skipped.setdefault('warn.exefs_skipped', log)
for warn, log in skipped.items():
rep.warning(warn, log=str(log))
(gen / STAMP).write_text(ctx.keys['lift'] + '\n')
@@ -852,7 +972,8 @@ class Compile(Step):
id = 'compile'
def key(self, ctx):
return fingerprint('compile-3', ctx.keys['lift'], runtime_key(ctx))
return fingerprint('compile-3', ctx.keys['lift'], runtime_key(ctx),
ctx.layout.pgo(game_version(ctx)) or 'no profile')
def current(self, ctx, record):
exe = ctx.dest / ('totk' + EXE)
@@ -878,10 +999,12 @@ class Compile(Step):
rep.progress('compile', 'compile', force=True)
cpu = getattr(ctx, 'cpu', 'v3')
lib = layout.runtime_lib_for(cpu)
pgo = layout.pgo(game_version(ctx))
code, log = run_tool(ctx, 'compile', layout.tools / 'build_game.py', [
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm, '--cpu', cpu,
*(['--deps', layout.deps] if layout.deps else []), *(['--sysroot', layout.sysroot] if layout.sysroot else []),
'--runtime', layout.runtime, *(['--runtime-lib', lib] if lib else ()), '--ninja', layout.ninja,
'--runtime', layout.runtime, *(['--runtime-lib', lib] if lib else ()), *(['--pgo', pgo] if pgo else ()),
'--ninja', layout.ninja,
'--version', version_tag(game_version(ctx)), '--expected', layout.expected(game_version(ctx)),
'--exefs', dest / 'exefs', '--romfs', dest / 'romfs',
'--profile', dest / 'profile'], parse)
@@ -907,9 +1030,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):
@@ -973,9 +1098,10 @@ class Prepare(Step):
# romfs-patches-xbox for the Nintendo pad layout, romfs-patches-xbox-label for xbox and auto (runtime/input_map.c
# input_prompts_folder). Both are written on every install, so the menu choices work without another run of
# the setup. Optional: when one fails, the game keeps the Switch prompts with that layout and the menu says the
# files are missing, so the setup warns once and goes on.
# files are missing, so the setup warns once and goes on. romfs-patches-pc likewise, with keyboard and mouse
# prompts for prompts = pc.
failed = None
for style in ('xbox', 'xbox-label'):
for style in ('xbox', 'xbox-label', 'pc'):
out = dest / f'romfs-patches-{style}'
remove(out)
code, log = run_tool(ctx, f'prepare-{style}', layout.tools / 'make_romfs_patches.py',
@@ -984,6 +1110,25 @@ class Prepare(Step):
if code:
remove(out)
failed = failed or (code, log)
# Each prompts' folder once more for each shape of ASPECT_SIZES, romfs-patches-<style>-<w>x<h> with that
# shape's UI layout (--prompts <style> --aspect), which runtime/romfs.c overlay_init loads in place of
# romfs-patches-<w>x<h>: without it a player with Xbox or PC prompts on a screen that is not 16:9 chose between
# the prompts and the screen's shape. Only the Common archive differs from romfs-patches-<style>; --reuse links
# the fonts, messages and size table from there instead of compressing them again, so each folder takes
# seconds and a few MiB. Optional like romfs-patches-<style>, and skipped without it.
for style in ('xbox', 'xbox-label', 'pc'):
plain = dest / f'romfs-patches-{style}'
for size in ASPECT_SIZES:
out = dest / f'romfs-patches-{style}-{size}'
remove(out)
if not (plain / 'manifest.tsv').is_file():
continue
code, log = run_tool(ctx, f'prepare-{style}-{size}', layout.tools / 'make_romfs_patches.py',
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
out, '--prompts', style, '--aspect', size, '--reuse', plain])
if code:
remove(out)
failed = failed or (code, log)
if failed:
rep.warning('warn.xbox_prompts_failed', code=failed[0], log=str(failed[1]))
@@ -1022,7 +1167,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 +1260,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 +1310,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()]
+16 -7
View File
@@ -5,12 +5,15 @@
"step.lift": "Translating the game code to C",
"step.compile": "Compiling {game_exe}",
"step.prepare": "Preparing the shaders",
"step.dlss": "Downloading NVIDIA DLSS",
"step.cleanup": "Cleaning up",
"phase.check.host": "Checking the processor and the graphics card",
"phase.check.verify": "Checking the files already installed",
"phase.check.verify_romfs": "Checking the installed game data",
"phase.check.inputs": "Opening the game files with the keys",
"phase.check.unpack_base": "Unpacking the compressed base game",
"phase.check.unpack_update": "Unpacking the compressed update",
"phase.check.versions": "Checking the game versions",
"phase.fonts.extract": "Extracting the shared fonts from the firmware",
"phase.fonts.copy": "Copying the shared fonts into the install folder",
@@ -30,6 +33,7 @@
"phase.prepare.translate": "Translating the shaders",
"phase.prepare.libraries": "Building the shaders for the graphics driver",
"phase.prepare.pack": "Packing the shader cache into one file",
"phase.dlss.download": "Downloading nvngx_dlss.dll from NVIDIA's DLSS SDK",
"phase.cleanup.ini": "Writing totk.ini",
"phase.cleanup.temp": "Removing temporary files",
@@ -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}.",
@@ -98,9 +103,11 @@
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
"warn.xbox_prompts_failed": "The Xbox or PC 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",
+16 -7
View File
@@ -5,12 +5,15 @@
"step.lift": "Traduzindo o código do jogo para C",
"step.compile": "Compilando o {game_exe}",
"step.prepare": "Preparando os shaders",
"step.dlss": "Baixando o NVIDIA DLSS",
"step.cleanup": "Finalizando",
"phase.check.host": "Verificando o processador e a placa de vídeo",
"phase.check.verify": "Verificando os arquivos já instalados",
"phase.check.verify_romfs": "Verificando os dados do jogo instalados",
"phase.check.inputs": "Abrindo os arquivos do jogo com as chaves",
"phase.check.unpack_base": "Descompactando o jogo base compactado",
"phase.check.unpack_update": "Descompactando a atualização compactada",
"phase.check.versions": "Verificando as versões do jogo",
"phase.fonts.extract": "Extraindo as fontes compartilhadas do firmware",
"phase.fonts.copy": "Copiando as fontes compartilhadas para a pasta de instalação",
@@ -30,6 +33,7 @@
"phase.prepare.translate": "Traduzindo os shaders",
"phase.prepare.libraries": "Montando os shaders para o driver de vídeo",
"phase.prepare.pack": "Juntando o cache de shaders num único arquivo",
"phase.dlss.download": "Baixando o nvngx_dlss.dll do DLSS SDK da NVIDIA",
"phase.cleanup.ini": "Gravando o totk.ini",
"phase.cleanup.temp": "Removendo os arquivos temporários",
@@ -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}.",
@@ -98,9 +103,11 @@
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox (código de saída {code}); o jogo mantém os ícones do Switch. Detalhes: {log}",
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox ou do PC (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",
+16 -7
View File
@@ -5,12 +5,15 @@
"step.lift": "Перевод кода игры в C",
"step.compile": "Сборка {game_exe}",
"step.prepare": "Подготовка шейдеров",
"step.dlss": "Загрузка NVIDIA DLSS",
"step.cleanup": "Уборка",
"phase.check.host": "Проверка процессора и видеокарты",
"phase.check.verify": "Проверка уже установленных файлов",
"phase.check.verify_romfs": "Проверка установленных данных игры",
"phase.check.inputs": "Открытие игровых файлов с помощью ключей",
"phase.check.unpack_base": "Распаковка сжатой базовой игры",
"phase.check.unpack_update": "Распаковка сжатого обновления",
"phase.check.versions": "Проверка версий игры",
"phase.fonts.extract": "Извлечение общих шрифтов из прошивки",
"phase.fonts.copy": "Копирование общих шрифтов в папку установки",
@@ -30,6 +33,7 @@
"phase.prepare.translate": "Трансляция шейдеров",
"phase.prepare.libraries": "Сборка шейдеров для драйвера видеокарты",
"phase.prepare.pack": "Упаковка кэша шейдеров в один файл",
"phase.dlss.download": "Загрузка nvngx_dlss.dll из DLSS SDK от NVIDIA",
"phase.cleanup.ini": "Запись totk.ini",
"phase.cleanup.temp": "Удаление временных файлов",
@@ -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}.",
@@ -98,9 +103,11 @@
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox или PC (код выхода {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": "Начать",
+16 -7
View File
@@ -5,12 +5,15 @@
"step.lift": "将游戏代码转换为 C 代码",
"step.compile": "编译 {game_exe}",
"step.prepare": "准备着色器",
"step.dlss": "下载 NVIDIA DLSS",
"step.cleanup": "清理",
"phase.check.host": "检查处理器和显卡",
"phase.check.verify": "检查已安装的文件",
"phase.check.verify_romfs": "检查已安装的游戏数据",
"phase.check.inputs": "用密钥打开游戏文件",
"phase.check.unpack_base": "正在解压压缩的游戏本体",
"phase.check.unpack_update": "正在解压压缩的更新",
"phase.check.versions": "检查游戏版本",
"phase.fonts.extract": "正在从固件中提取共享字体",
"phase.fonts.copy": "正在将共享字体复制到安装文件夹",
@@ -30,6 +33,7 @@
"phase.prepare.translate": "转译着色器",
"phase.prepare.libraries": "为显卡驱动构建着色器",
"phase.prepare.pack": "将着色器缓存打包成一个文件",
"phase.dlss.download": "从 NVIDIA 的 DLSS SDK 下载 nvngx_dlss.dll",
"phase.cleanup.ini": "写入 totk.ini",
"phase.cleanup.temp": "删除临时文件",
@@ -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}。",
@@ -98,9 +103,11 @@
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
"warn.xbox_prompts_failed": "无法准备 Xbox 按键提示(退出代码 {code});游戏将继续使用 Switch 按键提示。详情:{log}",
"warn.xbox_prompts_failed": "无法准备 Xbox 或 PC 按键提示(退出代码 {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": "开始",
+41 -2
View File
@@ -9,6 +9,9 @@ Output, under --out:
<mod>/<mod>_NNNN.c one `void f_<addr>(Cpu *c)` per guest function, about --chunk-bytes of C per file
<mod>/<mod>_table.c sorted <mod>_fn_addrs[] / <mod>_fn_ptrs[] / <mod>_fn_count for rt_dispatch
<mod>/manifest.json chunk list, function count, statistics and timings
<mod>/folded.txt the module's bytes the generated code took as constants: what the jump tables read at
lift time, as module offsets [start, end); tools/exefs_patches.py keeps a mod's data
writes off them
patches.h code patch sets for the runtime (recomp/patches.py; no sets without --patches)
Function discovery runs in this process, vectorised over the module's .text words. Function starts are the module
@@ -555,7 +558,8 @@ def _retry(fn, *args):
# ------------------------------------------------------------------------------------------------ workers
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter},
# 'reads' {name: [(start, end)]} module bytes read since the last chunk
def _worker_init(mods, starts, table, opts, jobs):
@@ -565,6 +569,7 @@ def _worker_init(mods, starts, table, opts, jobs):
_W['patches'] = table
_W['opts'] = opts # Emitter keyword arguments
_W['emitters'] = {}
_W['reads'] = {}
def _emitter(name):
@@ -572,12 +577,31 @@ def _emitter(name):
if e is None:
path, base = _W['mods'][name]
mod = Module(name, path, base, 'c')
# Module.read is how the jump tables (recomp/jumptables.py _eval) read .text and .rodata at lift time: the
# bytes it returns become part of the generated code, so <mod>/folded.txt lists them
read, reads = mod.read, _W['reads'].setdefault(name, [])
def recorded(a, n, signed=False):
reads.append((a - base, a - base + n))
return read(a, n, signed=signed)
mod.read = recorded
e = _W['emitters'][name] = Emitter(mod, set(_W['starts'][name].tolist()),
_W['patches'].for_module(name, base),
spins=_W['patches'].spins_for(name, base), **_W['opts'])
return e
def merge_ranges(ranges):
"""Sorted [start, end) ranges with overlapping and touching ones joined."""
out = []
for s, e in sorted(ranges):
if out and s <= out[-1][1]:
out[-1][1] = max(out[-1][1], e)
else:
out.append([s, e])
return out
def _emit_chunk(task):
"""Worker: write one chunk. Returns its statistics."""
name, number, path, fns = task
@@ -595,9 +619,12 @@ def _emit_chunk(task):
written = write_if_changed(Path(path), text)
except OSError as ex:
written, error = False, f'{path}: {ex}'
reads = _W['reads'].get(name, [])
folded = merge_ranges(reads)
reads.clear()
return {'module': name, 'number': number, 'file': Path(path).name, 'functions': len(fns), 'bytes': len(text),
'written': written, 'error': error, 'seconds': time.perf_counter() - t0, 'stats': dict(e.stats),
'unsupported': dict(e.unsupported)}
'unsupported': dict(e.unsupported), 'folded': folded}
# ------------------------------------------------------------------------------------------------ driver
@@ -613,6 +640,14 @@ def table_text(name, addrs):
return ''.join(lines)
def folded_text(name, ranges):
"""<mod>/folded.txt: the module offsets [start, end) of the bytes the jump tables read at lift time, in hex."""
lines = [f'# {GENERATED_MARKER}: {name} bytes the generated code took as constants (jump tables), module offsets '
f'[start, end), generated by recomp/gen.py from game code. Do not edit or commit.']
lines += [f'{s:x} {e:x}' for s, e in ranges]
return '\n'.join(lines) + '\n'
def remove_stale(moddir, name, nchunks, errors):
"""Delete chunks an earlier run wrote beyond this run's count, and temporary files an interrupted run left."""
pat = re.compile(re.escape(name) + r'_(\d{4})\.c')
@@ -714,6 +749,7 @@ def main():
# Largest chunks first, so the pool does not end on a long tail.
tasks.sort(key=lambda t: -t[0])
results = {name: [] for name in info}
folded = {name: [] for name in info}
done_at, errors = {}, []
t_emit = time.perf_counter()
ctx = mp.get_context('spawn')
@@ -724,9 +760,11 @@ def main():
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
try:
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
# Its folded bytes come with its result: a chunk emitted again reads them again in its new worker.
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
results[r['module']].append(r)
folded[r['module']] += r['folded']
if r['error']:
errors.append(r['error'])
done_at[r['module']] = time.perf_counter() - t_emit
@@ -753,6 +791,7 @@ def main():
unsupported.update(r['unsupported'])
t = time.perf_counter()
table_written = write_if_changed(moddir / f'{name}_table.c', table_text(name, d['emitted']))
write_if_changed(moddir / 'folded.txt', folded_text(name, merge_ranges(folded[name])))
removed = remove_stale(moddir, name, d['chunks'], errors)
manifest = {
'module': name,
+147
View File
@@ -55,6 +55,47 @@ site cloth main 8222f0 9401d7cf @page:x8 bd400104 1e240842 1e240863 9401d7cf #
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
site bounce main bda798 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
# Climbing (issues #35, #276, #1001): the climb controller's velocity step (main+0x1918220, the same code as 1.4.3
# main+0x1f49754) multiplies the requested velocity by a factor that makes up for a movement loss growing with the
# frame rate: 1.272 - 0.362 (r - 1) clamped to [0.5, 1.5] (r = VFR +0, the 30 Hz ratio times the time scale). Right at
# 30 fps, 78% of the speed at 60 and 30% at 240. Below r = 1 use 1.4.3's factor, 1.272 + 1.189 (1 - r) at most 2
# (Nintendo's 60 fps fit), and below r = 0.5 the 1.4.3 set climb's g(r) = 0.66265 / r + 0.4982 + 0.0848 r, at most 8.
# Without flags: h = max(min(1.4.3 factor, 2), g - 16 max(r - 0.5, 0)), s0 = max(s0, h), s1 = max(1.5, h), and the
# following fmin s1, s0, s1 gives max(min(s0, 1.5), h). For r >= 1 h is at most the shipping value, so 30 fps and
# slower keep the factor bit for bit. x8 is dead after the load at 0x19182c0; s5-s7 and s16 are written before any
# later read in this function.
# g takes u = max(page +0, r): the patch page's 30 Hz frame ratio without the time scale (runtime/frame_rate.c), so
# slow motion at 30 or 60 fps keeps the shipping factor and only a real frame rate above 60 raises it; the runtime
# switches the set off without the real-time step, where the page is not written.
set climb TOTK_CLIMB_FIX 1
site climb main 19182cc 1e2f1001 @page:x8 bd400111 1e296a31 1e2e1005 1e3138a5 52862508 72a7e308 1e270106 1e2608a5 1e2528a5 529a1ca8 72a7f448 1e270106 1e2628a5 1e201006 1e2678a5 52946dc8 72a7e528 1e270106 1e3118c6 52828248 72a7dfe8 1e270107 1e2728c6 529573e8 72a7b5a8 1e270107 1f1118e6 1e283a27 2f00e410 1e3068e7 1e261010 1f1098e6 1e241010 1e3078c6 1e2668a5 1e256800 1e2f1001 1e256821 # fmov s1, #1.5 -> s5 = h(r); s0 = max(s0, s5); s1 = max(1.5, s5)
# Cursor and aim steps (upstream game_patches.c patch_cursor_speed, 363a6eb). These updates read the game's 30 Hz frame
# ratio for their smoothing but leave it out of the step itself, so the map cursor and the vertical Ultrahand aim moved
# 2x as fast at 60 fps and 4x at 120 as at the shipping 30 fps (issue #44). Each site multiplies the step by that ratio,
# so the speed is the shipping one at any frame rate. x9 and s16 are free: the next read of x9 follows a write, the
# function does not touch s16, and x20 keeps the update's frame object for the whole function.
set cursor TOTK_HIGHFPS_FIX 1
site cursor main ec3d9c bd400361 f94002e9 bd400930 1e300800 bd400361 # map pan speed s0 *= [[x23] + 8], then ldr s1, [x27]
site cursor main 1d48d64 1e2a198c 1e2a198c bd400a90 1e30098c # fdiv s12, s12, s10, then s12 *= [x20 + 8]: vertical Ultrahand aim
site cursor main 1d484e4 1e200923 1e200923 bd400a90 1e300863 # fmul s3, s9, s0, then s3 *= [x20 + 8]: vertical gyro aim (Recall)
# Gear rotation check (issues #330, #984). The update of ActorCheckRotateModule (main+0x21350c0; vtable +0x110, called
# by the AI node dispatchers main+0x11f5a40 / 0x2adbcf8 with x1 = the update context) reports a gear or water wheel as
# turning when the angle it turned since the previous update is above RotateStartDeg / 30, the angle of one 30 Hz
# frame. Above 30 fps a gear turns less per update than that, reads as stopped, and the shrine doors that wait on it
# (Riogok, Turakamik, the Wind Temple) never open. 1.4.3 (main+0x273d080) divides the angle by the context's +8, the
# frame delta in 30 Hz frames (the AI root sets it from its owner's virtual +0x30, main+0x106acc4, and the frame
# counter node SelectorGenericFrameCounter, main+0x10b66f8, adds it up per update). 1.2.1 gets the same context and
# ignores it: the update overwrites x1 with its first instruction. The first site keeps ctx+8 in s12 (the prologue has
# saved d12, the function first writes it at the second site, and callees preserve it); the second multiplies the
# threshold by it, which is 1.4.3's test with both sides multiplied by the delta. s4 is free after the fdiv: the next
# read follows a write in the callee main+0x11c2014. The context follows the measured step with the set realtime and is
# the whole-vsync ratio without it (0.25 at 120 fps, 0.5 at 60 fps measured), so the set works either way.
set gears TOTK_HIGHFPS_FIX 1
site gears main 21350e0 f9402001 bd40082c f9402001 # ldr x1, [x0, #0x40], after ldr s12, [x1, #8] (frame delta)
site gears main 21352d0 1e24180c 1e241804 1e2c088c # fdiv s12, s0, s4 -> fdiv s4, s0, s4; fmul s12, s4, s12
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
@@ -71,6 +112,14 @@ site scene main cc3f68 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
site scene main cc3f70 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
site scene main cc3f74 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
# Fixed scene size (MacNativeGames game_patches_v143.c patch_fixed_res, 1.4.3 main+0x5848a8; runtime/resolution.c
# res_dynres_live switches the set on unless TOTK_DYNAMIC_RES=1). The game's dynamic resolution controller
# (main+0xcc26f4, called once per frame from the renderer at main+0xcc0fa0) steps the scene through tiers of 100, 90 and
# 80 % by a cost model that grows with the scene width squared (runtime/resolution.c res_dynres_live), and the renderer stores scene width * tier / output width at +0xccc (main+0xcc1150).
# Every path leaves through one epilogue, where the tier's scale moves into the result: 1.0 there makes every tier 100 %.
set fixres TOTK_FIXED_RES_PATCH 0
site fixres main cc2c00 2a1703e9 52a7f009 # mov w9, w23 -> mov w9, #0x3f800000 (1.0f)
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches.c, the 1.2.1 sites of aspect_install;
# runtime/resolution.c res_init writes the output's width / height to page +56 and switches the set on). The same four
# sites as patches-v143.txt, with the same original words: two camera sites (update and setup) load the camera's copy of
@@ -93,6 +142,104 @@ set pooloff TOTK_POOL_OFFSET_PATCH 1
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
# Direct mouse camera (runtime/mouse_direct.c; the same hooks as patches-v143.txt, mapped by enclosing function to the
# 1.2.1 main, each original word checked). The camera input update (main+0xd18088, 1.4.3 0x874c1c) after its dead zone,
# clamp and StickExponent keeps x in s11 (1.4.3 s9); the host callback performs the displaced store and adds raw mouse
# counts. The free orbit (main+0xae28b8) keeps its final angles at [sp+0x3c] / [sp+0x40], Ultrahand's curve results
# (main+0x1768ebc) are s9 and s15. The host functions are at page +0xfd0 .. +0xf80; the page is main+0x472b000.
set mouse_direct TOTK_MOUSE_DIRECT_PATCH 1
site mouse_direct main d18140 bd04228b 94e84fa4 # camera input: str s11, [x20, #0x420]
site mouse_direct main d181fc 6d4123e9 94e84f71 # camera input activity: ldp d9, d8, [sp, #0x10]
site mouse_direct main ae28e8 f9400408 94f125b2 # free orbit begin: ldr x8, [x0, #8]
site mouse_direct main ae355c bd403fed 94f12291 # free orbit angles: ldr s13, [sp, #0x3c]
site mouse_direct main 176949c b9001be9 94bf0abd # Ultrahand input: str w9, [sp, #0x18]
# The aiming, scope, ability, horse and Purah Pad cameras: per axis, the stick load and the angle the turn is added to,
# each a bl to page +0xf80 told apart by the return address (runtime/mouse_direct.c load_sites_121). Where 1.2.1 adds
# the turn in a shared helper (main+0x1bb70e8, 0x1bd0d00: *p = wrap(*p + s0)), the hook is on the turn the helper takes
# (Horse) or on the store of the helper's result (Aiming's pitch target). Every enclosing function saves x30 in its
# prologue and reads it only to return.
site mouse_direct main 1d34338 bd4426c1 94a7df12 # Aiming: ldr s1, [x22, #0x424] (stick y)
site mouse_direct main 1d34374 bd015660 94a7df03 # Aiming: str s0, [x19, #0x154] (pitch target: +0x15c + turn from a helper)
site mouse_direct main 1d34584 bd4422c1 94a7de7f # Aiming: ldr s1, [x22, #0x420] (stick x)
site mouse_direct main 1d345d0 bc5b83a1 94a7de6c # Aiming: ldur s1, [x29, #-0x48] (yaw)
site mouse_direct main 1d35fd4 bd4422e0 94a7d7eb # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
site mouse_direct main 1d35fe0 bd4426e9 94a7d7e8 # AimingTelescope: ldr s9, [x23, #0x424] (stick y)
site mouse_direct main 1d36280 bd416260 94a7d740 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
site mouse_direct main 1d36224 bd405be1 94a7d757 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
site mouse_direct main 1d48478 bd4422cf 94a78ec2 # SpecialPower: ldr s15, [x22, #0x420] (stick x)
site mouse_direct main 1d48480 bd4426cc 94a78ec0 # SpecialPower: ldr s12, [x22, #0x424] (stick y)
site mouse_direct main 1d48bb4 bd416262 94a78cf3 # SpecialPower: ldr s2, [x19, #0x160] (pitch)
site mouse_direct main 1d48b38 bc5a83a1 94a78d12 # SpecialPower: ldur s1, [x29, #-0x58] (yaw)
site mouse_direct main 1d4070c bd442288 94a7ae1d # Horse: ldr s8, [x20, #0x420] (stick x)
site mouse_direct main 1d40740 bd401be0 94a7ae10 # Horse: ldr s0, [sp, #0x18] (yaw turn, which a helper adds to +0x68)
site mouse_direct main 1d408f8 bd4426a8 94a7ada2 # Horse: ldr s8, [x21, #0x424] (stick y)
site mouse_direct main 1d409ac bd406661 94a7ad75 # Horse: ldr s1, [x19, #0x64] (pitch)
site mouse_direct main 1d48ccc bd4422c9 94a78cad # SpecialPower offset mode: ldr s9, [x22, #0x420] (stick x)
site mouse_direct main 1d48cf0 bd41da61 94a78ca4 # SpecialPower offset mode: ldr s1, [x19, #0x1d8] (offset x)
site mouse_direct main 1d48cfc bd4426c9 94a78ca1 # SpecialPower offset mode: ldr s9, [x22, #0x424] (stick y)
site mouse_direct main 1d48d18 bd41de61 94a78c9a # SpecialPower offset mode: ldr s1, [x19, #0x1dc] (offset y)
site mouse_direct main 1d485f8 bd400400 94a78e62 # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
site mouse_direct main 1d37b44 bd4426ad 94a7d10f # CameraFinder: ldr s13, [x21, #0x424] (stick y)
site mouse_direct main 1d37b68 bd405261 94a7d106 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
site mouse_direct main 1d37bcc bd4422ad 94a7d0ed # CameraFinder: ldr s13, [x21, #0x420] (stick x)
site mouse_direct main 1d37bf4 bd405be1 94a7d0e3 # CameraFinder: ldr s1, [sp, #0x58] (yaw)
# The other player cameras. ClimbObj, EventMultiTalk and EventTalkManual load an angle with a pre-indexed load whose
# write-back the code after it needs; the mov before it adds the offset instead and the hook loads [base] (no register
# between the two words, no branch into them). Wakeboard, ShieldSurfing and Tail read the stick twice, once for its
# length and once for its angle: the first pair gives the same values without degrees of its own (REPEAT).
site mouse_direct main 1d38e08 bd442301 94a7cc5e # ClimbObj: ldr s1, [x24, #0x420]
site mouse_direct main 1d38e10 bd409261 94a7cc5c # ClimbObj: ldr s1, [x19, #0x90] (yaw)
site mouse_direct main 1d38dac bd442701 94a7cc75 # ClimbObj: ldr s1, [x24, #0x424]
site mouse_direct main 1d38da4 aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
site mouse_direct main 1d38db8 bc488ec1 94a7cc72 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
site mouse_direct main 1d50738 bd4422c0 94a76e12 # Wakeboard: ldr s0, [x22, #0x420] (x for the length, REPEAT)
site mouse_direct main 1d50740 bd4426c1 94a76e10 # Wakeboard: ldr s1, [x22, #0x424] (y for the length, REPEAT)
site mouse_direct main 1d50768 bd4426c0 94a76e06 # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
site mouse_direct main 1d5076c bd4422c1 94a76e05 # Wakeboard: ldr s1, [x22, #0x420]
site mouse_direct main 1d50e5c bd40da60 94a76c49 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
site mouse_direct main 1d508bc bd401fa0 94a76db1 # Wakeboard: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
site mouse_direct main 1d50f20 bd403bec 94a76c18 # Wakeboard: ldr s12, [sp, #0x38] (yaw)
site mouse_direct main 1d51e10 bd4422cd 94a7685c # WaterfallClimb: ldr s13, [x22, #0x420]
site mouse_direct main 1d51fb8 b9403be8 94a767f2 # WaterfallClimb: ldr w8, [sp, #0x38] (yaw)
site mouse_direct main 1d51f48 bd4426c9 94a7680e # WaterfallClimb: ldr s9, [x22, #0x424]
site mouse_direct main 1d4a11c bd442280 94a78799 # Tail: ldr s0, [x20, #0x420] (x for the length, REPEAT)
site mouse_direct main 1d4a124 bd442681 94a78797 # Tail: ldr s1, [x20, #0x424] (y for the length, REPEAT)
site mouse_direct main 1d4a14c bd442680 94a7878d # Tail (climbing): ldr s0, [x20, #0x424] (stick y)
site mouse_direct main 1d4a150 bd442281 94a7878c # Tail: ldr s1, [x20, #0x420]
site mouse_direct main 1d4a4a0 bd40c260 94a786b8 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
site mouse_direct main 1d4a514 bd40b661 94a7869b # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
site mouse_direct main 1d4c930 bd442320 94a77d94 # LockOn / Target: ldr s0, [x25, #0x420]
site mouse_direct main 1d4c958 bd442321 94a77d8a # LockOn / Target: ldr s1, [x25, #0x420] (stick x)
site mouse_direct main 1d4c97c bd402be0 94a77d81 # LockOn / Target: ldr s0, [sp, #0x28] (yaw turn)
site mouse_direct main 1d4c98c bd402be9 94a77d7d # LockOn / Target: ldr s9, [sp, #0x28] (yaw turn)
site mouse_direct main 1d4cd5c bd442721 94a77c89 # LockOn / Target: ldr s1, [x25, #0x424] (stick y)
site mouse_direct main 1d4cd80 bd402be0 94a77c80 # LockOn / Target: ldr s0, [sp, #0x28] (pitch turn)
site mouse_direct main 1d4cd90 bd402beb 94a77c7c # LockOn / Target: ldr s11, [sp, #0x28] (pitch turn)
site mouse_direct main 1d443e8 bd4426a9 94a79ee6 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
site mouse_direct main 1d44430 bd419a61 94a79ed4 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
site mouse_direct main 1d44444 bd4422a9 94a79ecf # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
site mouse_direct main 1d44490 bd419e61 94a79ebc # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
site mouse_direct main 1d418dc bd44234b 94a7a9a9 # HorseLockOnEmpty: ldr s11, [x26, #0x420]
site mouse_direct main 1d3a484 bd44230b 94a7c6bf # EventMultiTalk: ldr s11, [x24, #0x420]
site mouse_direct main 1d3a3f8 bd44270b 94a7c6e2 # EventMultiTalk: ldr s11, [x24, #0x424] (stick y)
site mouse_direct main 1d3a418 aa1303f7 91017277 # EventMultiTalk: mov x23, x19 -> add x23, x19, #0x5c
site mouse_direct main 1d3a420 bc45cee1 94a7c6d8 # EventMultiTalk: ldr s1, [x23, #0x5c]! (pitch), as ldr s1, [x23]
site mouse_direct main 1d3a4dc aa1303f6 91019276 # EventMultiTalk: mov x22, x19 -> add x22, x19, #0x64
site mouse_direct main 1d3a4e4 bc464ec1 94a7c6a7 # EventMultiTalk: ldr s1, [x22, #0x64]! (yaw), as ldr s1, [x22]
site mouse_direct main 1d3e2b0 bd442729 94a7b734 # EventTalkManual: ldr s9, [x25, #0x424] (stick y)
site mouse_direct main 1d3e2d4 aa1303f8 9101d278 # EventTalkManual: mov x24, x19 -> add x24, x19, #0x74
site mouse_direct main 1d3e2d8 bc474f01 94a7b72a # EventTalkManual: ldr s1, [x24, #0x74]! (pitch), as ldr s1, [x24]
site mouse_direct main 1d3e33c bd442329 94a7b711 # EventTalkManual: ldr s9, [x25, #0x420] (stick x)
site mouse_direct main 1d3e360 aa1303f6 9101f276 # EventTalkManual: mov x22, x19 -> add x22, x19, #0x7c
site mouse_direct main 1d3e364 bc47cec1 94a7b707 # EventTalkManual: ldr s1, [x22, #0x7c]! (yaw), as ldr s1, [x22]
site mouse_direct main 1d46108 bd4422e0 94a7979e # ShieldSurfing: ldr s0, [x23, #0x420] (x for the length, REPEAT)
site mouse_direct main 1d46110 bd4426e1 94a7979c # ShieldSurfing: ldr s1, [x23, #0x424] (y for the length, REPEAT)
site mouse_direct main 1d46138 bd4426e0 94a79792 # ShieldSurfing: ldr s0, [x23, #0x424] (stick y, kept)
site mouse_direct main 1d4613c bd4422e1 94a79791 # ShieldSurfing: ldr s1, [x23, #0x420] (stick x)
site mouse_direct main 1d46788 bd40de60 94a795fe # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
site mouse_direct main 1d46910 bd401fa0 94a7959c # ShieldSurfing: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
site mouse_direct main 1d467b4 bd403bec 94a795f3 # ShieldSurfing: ldr s12, [sp, #0x38] (yaw)
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
spin main ac600 ac620 ac630 # 13 instructions; loads
+111
View File
@@ -82,6 +82,13 @@ site scene main 585e50 bd0cc660 2d420460 # str s0, [x19, #0xcc4] -> ldp s0, s
site scene main 585e54 1e220140 bd0cc660 # scvtf s0, w10 -> str s0, [x19, #0xcc4]
site scene main 585e60 bd0cca60 bd0cca61 # str s0, [x19, #0xcc8] -> str s1, [x19, #0xcc8]
# Fixed scene size (MacNativeGames game_patches_v143.c patch_fixed_res; runtime/resolution.c res_dynres_live switches
# the set on unless TOTK_DYNAMIC_RES=1). The dynamic resolution controller (main+0x584300, 1.2.1 0xcc26f4) steps the
# scene through tiers of 100, 90 and 80 % by a cost model that grows with the scene width squared; at its one epilogue the tier's scale moves into
# the result, and 1.0 there makes every tier 100 %. The original word is MacNativeGames' check against 1.4.3.
set fixres TOTK_FIXED_RES_PATCH 0
site fixres main 5848a8 2a1703e9 52a7f009 # mov w9, w23 -> mov w9, #0x3f800000 (1.0f)
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches_v143.c and aspect_patch.h
# aspect_install / aspect_stub; runtime/resolution.c res_init writes the output's width / height to page +56 and
# switches the set on). Each site leaves the ratio sead's perspective projection (+0xb0) takes in s0: two camera sites
@@ -103,6 +110,94 @@ set fov TOTK_FOV_PATCH 0
site fov main 628c3c bd413e60 f81f0fea @page:x10 bd413e60 bd403d48 1e280800 f84107ea # ldr s0, [x19, #0x13c] (camera update)
site fov main b4342c bd4066c0 f81f0fea @page:x10 bd4066c0 bd403d48 1e280800 f84107ea # ldr s0, [x22, #0x64] (camera setup)
# The camera input update, after its stick dead zone, unit-radius clamp and StickExponent. The host callback
# performs the displaced store and adds raw mouse counts to s9/s10 (runtime/mouse_direct.c camera_input).
# Its address is page +0xfd0; the page is main+0x3adf000 on this verified build.
set mouse_direct TOTK_MOUSE_DIRECT_PATCH 1
site mouse_direct main 874cb4 bd042289 94c9acc7
site mouse_direct main 874d7c 6d40a3e9 94c9ac91
site mouse_direct main 18e444 f9400408 94e546db
site mouse_direct main 18f108 bd4047ec 94e543a6
# Keep native camera-distance easing when direct mouse orbit bypasses the stick integrators.
site mouse_direct main 18ea34 b9418a88 94e5454f # orbit state: ldr w8, [x20, #0x188]
site mouse_direct main 1d59460 b9004be8 94761acc
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
# +0xf80, told apart by the return address (runtime/mouse_direct.c camera_load, load_sites). Every enclosing function
# saves x30 in its prologue, so the bl's link register write is harmless.
site mouse_direct main 23beb94 bd4426c1 945c84fb # Aiming: ldr s1, [x22, #0x424] (stick y)
site mouse_direct main 23bebc8 bd415e61 945c84ee # Aiming: ldr s1, [x19, #0x15c] (pitch)
site mouse_direct main 23bee2c bd4422c1 945c8455 # Aiming: ldr s1, [x22, #0x420] (stick x)
site mouse_direct main 23beeec bc5b83a1 945c8425 # Aiming: ldur s1, [x29, #-0x48] (yaw)
site mouse_direct main 23c0a10 bd4422e0 945c7d5c # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
site mouse_direct main 23c0a1c bd4426eb 945c7d59 # AimingTelescope: ldr s11, [x23, #0x424] (stick y)
site mouse_direct main 23c0b00 bd416260 945c7d20 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
site mouse_direct main 23c0e80 bd405be1 945c7c40 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
site mouse_direct main 23d2eec bd4422e8 945c3425 # SpecialPower: ldr s8, [x23, #0x420] (stick x)
site mouse_direct main 23d2ef4 bd4426ee 945c3423 # SpecialPower: ldr s14, [x23, #0x424] (stick y)
site mouse_direct main 23d3158 bd416260 945c338a # SpecialPower: ldr s0, [x19, #0x160] (pitch)
site mouse_direct main 23d34cc bc5b03a1 945c32ad # SpecialPower: ldur s1, [x29, #-0x50] (yaw)
site mouse_direct main 23cb274 bd442288 945c5343 # Horse: ldr s8, [x20, #0x420] (stick x)
site mouse_direct main 23cb2a0 bd406a61 945c5338 # Horse: ldr s1, [x19, #0x68] (yaw)
site mouse_direct main 23cb3f8 bd4426a8 945c52e2 # Horse: ldr s8, [x21, #0x424] (stick y)
site mouse_direct main 23cb424 bd406661 945c52d7 # Horse: ldr s1, [x19, #0x64] (pitch)
site mouse_direct main 23d2f30 bd4422e8 945c3414 # SpecialPower offset mode: ldr s8, [x23, #0x420] (stick x)
site mouse_direct main 23d2f5c bd41da62 945c3409 # SpecialPower offset mode: ldr s2, [x19, #0x1d8] (offset x)
site mouse_direct main 23d2f6c bd4426e8 945c3405 # SpecialPower offset mode: ldr s8, [x23, #0x424] (stick y)
site mouse_direct main 23d2f98 bd41de62 945c33fa # SpecialPower offset mode: ldr s2, [x19, #0x1dc] (offset y)
site mouse_direct main 23d3210 bd400400 945c335c # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
site mouse_direct main 23c2890 bd4426ad 945c75bc # CameraFinder: ldr s13, [x21, #0x424] (stick y)
site mouse_direct main 23c28b4 bd405261 945c75b3 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
site mouse_direct main 23c2934 bd4422ad 945c7593 # CameraFinder: ldr s13, [x21, #0x420] (stick x)
site mouse_direct main 23c2964 bd404be1 945c7587 # CameraFinder: ldr s1, [sp, #0x48] (yaw)
# The other player cameras (runtime/mouse_direct.c load_sites; a ROLE_WATCH load stays the game's and only names its
# mode in the log when the mouse was in it):
site mouse_direct main 23c3dd0 bd442301 945c706c # ClimbObj: ldr s1, [x24, #0x420]
site mouse_direct main 23c3dd8 bd409261 945c706a # ClimbObj: ldr s1, [x19, #0x90] (yaw)
site mouse_direct main 23c3d64 bd442701 945c7087 # ClimbObj: ldr s1, [x24, #0x424]
# ClimbObj's pitch is a pre-indexed load, ldr s1, [x22, #0x88]!, whose write-back the code after it needs (ldp and
# str through x22). A host call cannot write x22: the lifter keeps X19-X29 in locals across a call. So the mov before
# it adds the 0x88 instead (x22 = x19 + 0x88) and the hook loads [x22] with no write-back.
site mouse_direct main 23c3d5c aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
site mouse_direct main 23c3d70 bc488ec1 945c7084 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
site mouse_direct main 23da694 bd4422c1 945c163b # Wakeboard: ldr s1, [x22, #0x420]
site mouse_direct main 23da698 bd4426c0 945c163a # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
site mouse_direct main 23da840 bd40da60 945c15d0 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
site mouse_direct main 23da894 bd4017e0 945c15bb # Wakeboard: ldr s0, [sp, #0x14] (pitch turn, cut to the pad)
site mouse_direct main 23daed8 bd403fe9 945c142a # Wakeboard: ldr s9, [sp, #0x3c] (yaw)
site mouse_direct main 23dced8 bd4422cd 945c0c2a # WaterfallClimb: ldr s13, [x22, #0x420]
site mouse_direct main 23dd070 b85cc3a8 945c0bc4 # WaterfallClimb: ldur w8, [x29, #-0x34] (yaw)
site mouse_direct main 23dd008 bd4426c9 945c0bde # WaterfallClimb: ldr s9, [x22, #0x424]
site mouse_direct main 23d4b14 bd44228c 945c2d1b # Tail: ldr s12, [x20, #0x420]
site mouse_direct main 23d4b1c bd44268d 945c2d19 # Tail (climbing): ldr s13, [x20, #0x424] (stick y)
site mouse_direct main 23d4f34 bd40c260 945c2c13 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
site mouse_direct main 23d5028 bd40b661 945c2bd6 # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
site mouse_direct main 23d756c bd442300 945c2285 # LockOn / Target: ldr s0, [x24, #0x420]
site mouse_direct main 23d7594 bd442301 945c227b # LockOn / Target: ldr s1, [x24, #0x420] (stick x)
site mouse_direct main 23d75b4 bd4023e0 945c2273 # LockOn / Target: ldr s0, [sp, #0x20] (yaw turn)
site mouse_direct main 23d75c4 bd4023e9 945c226f # LockOn / Target: ldr s9, [sp, #0x20] (yaw turn)
site mouse_direct main 23d7998 bd442701 945c217a # LockOn / Target: ldr s1, [x24, #0x424] (stick y)
site mouse_direct main 23d79b8 bd4023e0 945c2172 # LockOn / Target: ldr s0, [sp, #0x20] (pitch turn)
site mouse_direct main 23d79c8 bd4023ea 945c216e # LockOn / Target: ldr s10, [sp, #0x20] (pitch turn)
site mouse_direct main 23cecb4 bd4426a9 945c44b3 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
site mouse_direct main 23cecf8 bd419a61 945c44a2 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
site mouse_direct main 23ced0c bd4422a9 945c449d # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
site mouse_direct main 23ced4c bd419e61 945c448d # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
site mouse_direct main 23cc4b4 bd44232b 945c4eb3 # HorseLockOnEmpty: ldr s11, [x25, #0x420]
site mouse_direct main 23c54d4 bd4422eb 945c6aab # EventMultiTalk: ldr s11, [x23, #0x420]
site mouse_direct main 23c542c bd4426eb 945c6ad5 # EventMultiTalk: ldr s11, [x23, #0x424] (stick y)
site mouse_direct main 23c546c bd405e61 945c6ac5 # EventMultiTalk: ldr s1, [x19, #0x5c] (pitch)
site mouse_direct main 23c5554 bd406661 945c6a8b # EventMultiTalk: ldr s1, [x19, #0x64] (yaw)
site mouse_direct main 23c90ac bd442709 945c5bb5 # EventTalkManual: ldr s9, [x24, #0x424] (stick y)
site mouse_direct main 23c90d8 bd407661 945c5baa # EventTalkManual: ldr s1, [x19, #0x74] (pitch)
site mouse_direct main 23c9140 bd442309 945c5b90 # EventTalkManual: ldr s9, [x24, #0x420] (stick x)
site mouse_direct main 23c9170 bd407e61 945c5b84 # EventTalkManual: ldr s1, [x19, #0x7c] (yaw)
site mouse_direct main 23d0100 bd4422c1 945c3fa0 # ShieldSurfing: ldr s1, [x22, #0x420] (stick x)
site mouse_direct main 23d0104 bd4426c0 945c3f9f # ShieldSurfing: ldr s0, [x22, #0x424] (stick y, kept)
site mouse_direct main 23d02ac bd40de60 945c3f35 # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
site mouse_direct main 23d0a34 bd4013e0 945c3d53 # ShieldSurfing: ldr s0, [sp, #0x10] (pitch turn, cut to the pad)
site mouse_direct main 23d087c bd403fe9 945c3dc1 # ShieldSurfing: ldr s9, [sp, #0x3c] (yaw)
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
@@ -991,3 +1086,19 @@ site cascade main 6fabb8 52a83408 @page:x8 b9404108 # mov w8, #20.0 -> ldr w8
site cascade main 6fac40 52a84408 @page:x8 b9404508 # mov w8, #40.0 -> ldr w8, [page+68]
site cascade main 6facc8 52a85408 @page:x8 b9404908 # mov w8, #80.0 -> ldr w8, [page+72]
site cascade main 6fad4c 52a86408 @page:x8 b9404d08 # mov w8, #160.0 -> ldr w8, [page+76]
# Climbing (issues #35, #276, #1001): the climb controller's velocity step (main+0x1f49754, shared by wall and ladder
# climbing, climb jumps and climb-overs) multiplies the requested velocity by a factor that makes up for a movement loss
# growing with the frame rate. Below ratio 1 the factor is 1.272 + 1.189 (1 - r) clamped to [1, 2] (r = VFR +0, the
# 30 Hz ratio times the time scale): Nintendo's fit for 30 and 60 fps, flat at 2 from about 77 fps, so Link climbs at
# 63% of the speed at 120 fps and 34% at 240. Below r = 0.5 take g(r) = 0.66265 / r + 0.4982 + 0.0848 r instead,
# fitted to the replayed ladder climb at 120 and 240 fps and equal to the shipping 1.8665 at r = 0.5; at most 8. The
# site computes g with 16 * max(r - 0.5, 0) subtracted, so g stays under the shipping factor for r >= 0.5, and folds it
# into the existing clamp without flags: s0 = max(s0, g) (the value clamped), s1 = max(2, g) (the bound). r >= 0.5
# keeps the shipping factor bit for bit. x8 is dead after the constant load at 0x1f497e8, s2-s4 are loaded again at
# 0x1f49844 before any read.
# g takes u = max(page +0, r): the patch page's 30 Hz frame ratio without the time scale (runtime/frame_rate.c), so
# slow motion at 30 or 60 fps keeps the shipping factor and only a real frame rate above 60 raises it; the runtime
# switches the set off without the real-time step, where the page is not written.
set climb TOTK_CLIMB_FIX 1
site climb main 1f497f4 1e201001 @page:x8 bd400104 1e286884 52946dc8 72a7e528 1e270102 1e241842 52828248 72a7dfe8 1e270103 1e232842 529573e8 72a7b5a8 1e270103 1f040862 1e2a3883 2f00e404 1e246863 1e261004 1f048862 1e241004 1e247842 1e226800 1e201001 1e226821 # fmov s1, #2.0 -> s2 = g(u); s0 = max(s0, s2); s1 = max(2.0, s2)
+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) {
+111
View File
@@ -0,0 +1,111 @@
// Generated by tools/build_shaders.py from runtime/shaders/bl_deswizzle.comp; do not edit.
// source sha256 bd2065290e315e85d2fcdb609d812c15ba9a109d7506b3105850a5860677b0ef
#pragma once
#include <stdint.h>
static const uint32_t bl_deswizzle_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x000000d0, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001f, 0x00060010,
0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x0000000b, 0x0000000b,
0x0000001a, 0x00030047, 0x00000010, 0x00000002, 0x00050048, 0x00000010, 0x00000000, 0x00000023,
0x00000000, 0x00050048, 0x00000010, 0x00000001, 0x00000023, 0x00000004, 0x00050048, 0x00000010,
0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x00000010, 0x00000003, 0x00000023, 0x0000000c,
0x00050048, 0x00000010, 0x00000004, 0x00000023, 0x00000010, 0x00050048, 0x00000010, 0x00000005,
0x00000023, 0x00000014, 0x00050048, 0x00000010, 0x00000006, 0x00000023, 0x00000018, 0x00050048,
0x00000010, 0x00000007, 0x00000023, 0x0000001c, 0x00050048, 0x00000010, 0x00000008, 0x00000023,
0x00000020, 0x00050048, 0x00000010, 0x00000009, 0x00000023, 0x00000024, 0x00050048, 0x00000010,
0x0000000a, 0x00000023, 0x00000028, 0x00040047, 0x0000001f, 0x0000000b, 0x0000001b, 0x00040047,
0x000000ad, 0x00000006, 0x00000010, 0x00030047, 0x000000ae, 0x00000002, 0x00040048, 0x000000ae,
0x00000000, 0x00000019, 0x00050048, 0x000000ae, 0x00000000, 0x00000023, 0x00000000, 0x00030047,
0x000000b0, 0x00000019, 0x00040047, 0x000000b0, 0x00000021, 0x00000001, 0x00040047, 0x000000b0,
0x00000022, 0x00000000, 0x00040047, 0x000000c1, 0x00000006, 0x00000010, 0x00030047, 0x000000c2,
0x00000002, 0x00040048, 0x000000c2, 0x00000000, 0x00000018, 0x00050048, 0x000000c2, 0x00000000,
0x00000023, 0x00000000, 0x00030047, 0x000000c4, 0x00000018, 0x00040047, 0x000000c4, 0x00000021,
0x00000000, 0x00040047, 0x000000c4, 0x00000022, 0x00000000, 0x00040047, 0x000000cd, 0x0000000b,
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
0x00000020, 0x00000000, 0x00040017, 0x00000009, 0x00000006, 0x00000003, 0x00040020, 0x0000000a,
0x00000001, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000001, 0x0004002b, 0x00000006,
0x0000000c, 0x00000001, 0x00040020, 0x0000000d, 0x00000001, 0x00000006, 0x000d001e, 0x00000010,
0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006,
0x00000006, 0x00000006, 0x00000006, 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b,
0x00000011, 0x00000012, 0x00000009, 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b,
0x00000013, 0x00000014, 0x0000000a, 0x00040020, 0x00000015, 0x00000009, 0x00000006, 0x0004002b,
0x00000006, 0x00000019, 0x00000000, 0x0004002b, 0x00000006, 0x0000001d, 0x00000040, 0x0004003b,
0x0000000a, 0x0000001f, 0x00000001, 0x0004002b, 0x00000013, 0x00000024, 0x00000002, 0x00020014,
0x00000027, 0x0004002b, 0x00000013, 0x0000002d, 0x00000006, 0x0004002b, 0x00000013, 0x00000032,
0x00000007, 0x0004002b, 0x00000006, 0x00000037, 0x00000020, 0x0004002b, 0x00000006, 0x00000048,
0x00000005, 0x0004002b, 0x00000006, 0x0000004c, 0x0000001f, 0x0004002b, 0x00000013, 0x0000004f,
0x00000008, 0x0004002b, 0x00000013, 0x00000052, 0x00000009, 0x0004002b, 0x00000006, 0x0000006a,
0x00000004, 0x0004002b, 0x00000006, 0x0000006f, 0x00000002, 0x0004002b, 0x00000006, 0x00000078,
0x00000008, 0x0004002b, 0x00000006, 0x00000083, 0x00000003, 0x0004002b, 0x00000013, 0x00000093,
0x00000003, 0x0004002b, 0x00000013, 0x0000009b, 0x00000004, 0x0004002b, 0x00000013, 0x000000a4,
0x00000005, 0x00040017, 0x000000ac, 0x00000006, 0x00000004, 0x0003001d, 0x000000ad, 0x000000ac,
0x0003001e, 0x000000ae, 0x000000ad, 0x00040020, 0x000000af, 0x0000000c, 0x000000ae, 0x0004003b,
0x000000af, 0x000000b0, 0x0000000c, 0x0004002b, 0x00000013, 0x000000b1, 0x00000000, 0x0004002b,
0x00000013, 0x000000b2, 0x00000001, 0x0003001d, 0x000000c1, 0x000000ac, 0x0003001e, 0x000000c2,
0x000000c1, 0x00040020, 0x000000c3, 0x0000000c, 0x000000c2, 0x0004003b, 0x000000c3, 0x000000c4,
0x0000000c, 0x00040020, 0x000000c9, 0x0000000c, 0x000000ac, 0x0006002c, 0x00000009, 0x000000cd,
0x0000001d, 0x0000000c, 0x0000000c, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
0x000200f8, 0x00000005, 0x000300f7, 0x000000ce, 0x00000000, 0x000300fb, 0x00000019, 0x000000cf,
0x000200f8, 0x000000cf, 0x00050041, 0x0000000d, 0x0000000e, 0x0000000b, 0x0000000c, 0x0004003d,
0x00000006, 0x0000000f, 0x0000000e, 0x00050041, 0x00000015, 0x00000016, 0x00000012, 0x00000014,
0x0004003d, 0x00000006, 0x00000017, 0x00000016, 0x00050084, 0x00000006, 0x00000018, 0x0000000f,
0x00000017, 0x00050041, 0x0000000d, 0x0000001a, 0x0000000b, 0x00000019, 0x0004003d, 0x00000006,
0x0000001b, 0x0000001a, 0x00050080, 0x00000006, 0x0000001c, 0x00000018, 0x0000001b, 0x00050084,
0x00000006, 0x0000001e, 0x0000001c, 0x0000001d, 0x00050041, 0x0000000d, 0x00000020, 0x0000001f,
0x00000019, 0x0004003d, 0x00000006, 0x00000021, 0x00000020, 0x00050080, 0x00000006, 0x00000022,
0x0000001e, 0x00000021, 0x00050041, 0x00000015, 0x00000025, 0x00000012, 0x00000024, 0x0004003d,
0x00000006, 0x00000026, 0x00000025, 0x000500ae, 0x00000027, 0x00000028, 0x00000022, 0x00000026,
0x000300f7, 0x0000002a, 0x00000000, 0x000400fa, 0x00000028, 0x00000029, 0x0000002a, 0x000200f8,
0x00000029, 0x000200f9, 0x000000ce, 0x000200f8, 0x0000002a, 0x00050041, 0x00000015, 0x0000002e,
0x00000012, 0x0000002d, 0x0004003d, 0x00000006, 0x0000002f, 0x0000002e, 0x000500c4, 0x00000006,
0x00000030, 0x0000000c, 0x0000002f, 0x00050041, 0x00000015, 0x00000033, 0x00000012, 0x00000032,
0x0004003d, 0x00000006, 0x00000034, 0x00000033, 0x000500c4, 0x00000006, 0x00000035, 0x0000000c,
0x00000034, 0x00050080, 0x00000006, 0x0000003c, 0x0000002f, 0x00000034, 0x000500c4, 0x00000006,
0x0000003d, 0x00000037, 0x0000003c, 0x00050086, 0x00000006, 0x00000041, 0x00000022, 0x0000003d,
0x00050089, 0x00000006, 0x00000045, 0x00000022, 0x0000003d, 0x000500c2, 0x00000006, 0x00000049,
0x00000045, 0x00000048, 0x000500c7, 0x00000006, 0x0000004d, 0x00000045, 0x0000004c, 0x00050041,
0x00000015, 0x00000050, 0x00000012, 0x0000004f, 0x0004003d, 0x00000006, 0x00000051, 0x00000050,
0x00050041, 0x00000015, 0x00000053, 0x00000012, 0x00000052, 0x0004003d, 0x00000006, 0x00000054,
0x00000053, 0x00050084, 0x00000006, 0x00000055, 0x00000051, 0x00000054, 0x00050086, 0x00000006,
0x00000059, 0x00000041, 0x00000055, 0x00050089, 0x00000006, 0x0000005d, 0x00000041, 0x00000055,
0x00050086, 0x00000006, 0x00000062, 0x0000005d, 0x00000051, 0x00050089, 0x00000006, 0x00000067,
0x0000005d, 0x00000051, 0x00050084, 0x00000006, 0x0000006b, 0x00000067, 0x0000006a, 0x000500c2,
0x00000006, 0x0000006d, 0x0000004d, 0x0000006a, 0x000500c7, 0x00000006, 0x0000006e, 0x0000006d,
0x0000000c, 0x00050084, 0x00000006, 0x00000070, 0x0000006e, 0x0000006f, 0x00050080, 0x00000006,
0x00000071, 0x0000006b, 0x00000070, 0x000500c2, 0x00000006, 0x00000073, 0x0000004d, 0x0000000c,
0x000500c7, 0x00000006, 0x00000074, 0x00000073, 0x0000000c, 0x00050080, 0x00000006, 0x00000075,
0x00000071, 0x00000074, 0x00050084, 0x00000006, 0x00000079, 0x00000062, 0x00000078, 0x00050084,
0x00000006, 0x0000007b, 0x00000079, 0x00000030, 0x00050089, 0x00000006, 0x0000007e, 0x00000049,
0x00000030, 0x00050084, 0x00000006, 0x0000007f, 0x0000007e, 0x00000078, 0x00050080, 0x00000006,
0x00000080, 0x0000007b, 0x0000007f, 0x000500c2, 0x00000006, 0x00000082, 0x0000004d, 0x0000006f,
0x000500c7, 0x00000006, 0x00000084, 0x00000082, 0x00000083, 0x00050084, 0x00000006, 0x00000085,
0x00000084, 0x0000006f, 0x00050080, 0x00000006, 0x00000086, 0x00000080, 0x00000085, 0x000500c7,
0x00000006, 0x00000088, 0x00000045, 0x0000000c, 0x00050080, 0x00000006, 0x00000089, 0x00000086,
0x00000088, 0x00050084, 0x00000006, 0x0000008d, 0x00000059, 0x00000035, 0x00050086, 0x00000006,
0x00000090, 0x00000049, 0x00000030, 0x00050080, 0x00000006, 0x00000091, 0x0000008d, 0x00000090,
0x00050041, 0x00000015, 0x00000094, 0x00000012, 0x00000093, 0x0004003d, 0x00000006, 0x00000095,
0x00000094, 0x000500ae, 0x00000027, 0x00000096, 0x00000075, 0x00000095, 0x000400a8, 0x00000027,
0x00000097, 0x00000096, 0x000300f7, 0x00000099, 0x00000000, 0x000400fa, 0x00000097, 0x00000098,
0x00000099, 0x000200f8, 0x00000098, 0x00050041, 0x00000015, 0x0000009c, 0x00000012, 0x0000009b,
0x0004003d, 0x00000006, 0x0000009d, 0x0000009c, 0x000500ae, 0x00000027, 0x0000009e, 0x00000089,
0x0000009d, 0x000200f9, 0x00000099, 0x000200f8, 0x00000099, 0x000700f5, 0x00000027, 0x0000009f,
0x00000096, 0x0000002a, 0x0000009e, 0x00000098, 0x000400a8, 0x00000027, 0x000000a0, 0x0000009f,
0x000300f7, 0x000000a2, 0x00000000, 0x000400fa, 0x000000a0, 0x000000a1, 0x000000a2, 0x000200f8,
0x000000a1, 0x00050041, 0x00000015, 0x000000a5, 0x00000012, 0x000000a4, 0x0004003d, 0x00000006,
0x000000a6, 0x000000a5, 0x000500ae, 0x00000027, 0x000000a7, 0x00000091, 0x000000a6, 0x000200f9,
0x000000a2, 0x000200f8, 0x000000a2, 0x000700f5, 0x00000027, 0x000000a8, 0x0000009f, 0x00000099,
0x000000a7, 0x000000a1, 0x000300f7, 0x000000aa, 0x00000000, 0x000400fa, 0x000000a8, 0x000000a9,
0x000000aa, 0x000200f8, 0x000000a9, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000aa, 0x00050041,
0x00000015, 0x000000b3, 0x00000012, 0x000000b2, 0x0004003d, 0x00000006, 0x000000b4, 0x000000b3,
0x00050041, 0x00000015, 0x000000b6, 0x00000012, 0x0000009b, 0x0004003d, 0x00000006, 0x000000b7,
0x000000b6, 0x00050084, 0x00000006, 0x000000b8, 0x00000091, 0x000000b7, 0x00050080, 0x00000006,
0x000000ba, 0x000000b8, 0x00000089, 0x00050084, 0x00000006, 0x000000bd, 0x000000ba, 0x00000095,
0x00050080, 0x00000006, 0x000000be, 0x000000b4, 0x000000bd, 0x00050080, 0x00000006, 0x000000c0,
0x000000be, 0x00000075, 0x00050041, 0x00000015, 0x000000c5, 0x00000012, 0x000000b1, 0x0004003d,
0x00000006, 0x000000c6, 0x000000c5, 0x00050080, 0x00000006, 0x000000c8, 0x000000c6, 0x00000022,
0x00060041, 0x000000c9, 0x000000ca, 0x000000c4, 0x000000b1, 0x000000c8, 0x0004003d, 0x000000ac,
0x000000cb, 0x000000ca, 0x00060041, 0x000000c9, 0x000000cc, 0x000000b0, 0x000000b1, 0x000000c0,
0x0003003e, 0x000000cc, 0x000000cb, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000ce, 0x000100fd,
0x00010038,
};
+1
View File
@@ -307,6 +307,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
fflush(stderr);
fs_save_flush();
vk_pipeline_save(); // no-op without the Vulkan back end
playtime_save();
prof_write();
fflush(stderr);
plat_exit(0);
+126 -42
View File
@@ -7,6 +7,7 @@
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -28,75 +29,84 @@ static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
// 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.
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line, and gets a
// description in totk.ini.example or docs/settings.md (tests/settings_doc_check.py checks both).
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_SHAKE",
"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",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLIMB_FIX", "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",
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS",
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"DESC_TEMPLATE", "DESWIZZLE", "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", "DYNAMIC_RES", "DYN_DELTA", "EXEFS", "FEEDBACK", "FIXED_RES_PATCH", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAME_QUEUE",
"FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "KSPIN_US", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC",
"MEM_RESERVE_MIB", "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "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", "PCORE", "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_BENCH", "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_ASYNC", "REC_END", "REC_SPIN_US", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_CHUNKS", "REPLAY_CHUNK_DRAWS", "REPLAY_SKEW_US", "REPLAY_STATE_CHECK", "REPLAY_WORKERS",
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
"SHADER_MEMO", "SHADOW_DEFER", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHADOW_SNAP", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
"SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS",
"SWKBD_ANSWER", "SYNC_AHEAD",
"SWKBD_TEST", "TEST_OOM", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_BLOCK_MIB", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_RELEASED_MIB", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UB_OVERLAY_OUT", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VMA_BLOCK_MIB", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "VS_OUTS",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
"ZSTD_CHECK", "ZSTD_NATIVE",
};
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
static const struct { const char *key, *section; } sections[] = {
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"},
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"FRAME_QUEUE", "frame rate"},
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
{"CLIMB_FIX", "frame rate"},
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
{"SHARPNESS", "display"}, {"DYNAMIC_RES", "display"}, {"ASPECT", "display"}, {"MOVIE_ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
{"COLOR_PRESET", "display"},
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
{"HDR_UI_NITS", "display"}, {"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"},
{"TEX_HEAP_EXTRA_MB", "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"},
{"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"MOUSE_AIM_SCALE", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"},
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"FREE_CORES", "performance"},
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"REPLAY_WORKERS", "performance"}, {"FREE_CORES", "performance"},
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_RELEASED_MIB", "performance"}, {"TEX_BLOCK_MIB", "performance"}, {"VMA_BLOCK_MIB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"VB_USED", "performance"}, {"DESC_TEMPLATE", "performance"},
{"SYNC_AHEAD", "performance"}, {"SHADOW_DEFER", "performance"}, {"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "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
@@ -110,9 +120,12 @@ static const CfgMeta metas[] = {
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
{"DYNAMIC_RES", "1", "0|1", LIVE}, // resolution.c res_dynres_live
{"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},
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", 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}, {"MOVIE_ASPECT", "keep", "keep|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
{"ANISO", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
@@ -120,44 +133,55 @@ 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},
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
{"PCSS", "0", "0..200", RESTART},
// Game
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE}, {"FRAME_QUEUE", "0", "0|1", LIVE},
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
{"MODS_REBUILD", "auto", "auto|off", RESTART},
{"HIGHFPS_FIX", "1", "0|1", RESTART},
{"HIGHFPS_FIX", "1", "0|1", RESTART}, {"CLIMB_FIX", "1", "0|1", RESTART},
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
// Sound, Controls, Menu
{"VOLUME", "100", "0..100", LIVE},
{"VOLUME", "100", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", RESTART},
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
{"PROMPTS", "nintendo", "nintendo|xbox", RESTART},
{"PROMPTS", "nintendo", "nintendo|xbox|pc", RESTART},
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
{"MOUSE_SENS", "1", "0..10", LIVE},
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MOUSE_AIM_SCALE", "1", "0.1..3", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
{"AMIIBO_DIR", "", NULL, LIVE},
// Discord Rich Presence (discord_rpc.c): off by default, the Menu tab of the settings menu switches it.
{"DISCORD_RPC", "0", "0|1", LIVE},
{"DISCORD_APP_ID", "1567232599846293504", NULL, LIVE},
{"DISCORD_IMAGE", "totk_512x512", NULL, LIVE},
// Advanced: scheduling
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
{"REPLAY_WORKERS", "0", "0..16", RESTART}, {"REPLAY_CHUNKS", "0", "0..64", RESTART},
{"REPLAY_CHUNK_DRAWS", "128", "1..100000", RESTART}, {"REPLAY_STATE_CHECK", "0", "0|1", RESTART},
{"REPLAY_SKEW_US", "0", "0..100000", RESTART},
{"SYNC_AHEAD", "0", "0|1", RESTART}, {"SHADOW_DEFER", "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},
{"TEX_RELEASED_MIB", "256", "0..65536", RESTART}, {"TEX_BLOCK_MIB", "64", "0..1024", RESTART}, {"VMA_BLOCK_MIB", "64", "0..1024", 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},
{"VB_USED", "1", "0|1", RESTART},
{"DESC_TEMPLATE", "1", "0|1", RESTART},
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
{"SPLIT_US", "1000", "0..20000", RESTART},
@@ -166,7 +190,7 @@ static const CfgMeta metas[] = {
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_MERGE", "1", "0|1", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
{"INPUT_RECORD", "", NULL, RESTART},
};
@@ -315,9 +339,68 @@ static const char *raw_get(const char *name) {
return NULL;
}
// ---- the screen's shape (desktop): a primary display of another shape than 16:9 gives output_res a default of that
// shape while neither the environment nor totk.ini sets it, so the first start fills the screen instead of a 16:9
// picture between black bars. The sizes are the ones the installer lays the HUD out for (installer/steps.py
// ASPECT_SIZES, ui/menu.cpp row_output_res): of the display's shape within 1%, the smallest at least 1080 high that fits
// the display, else the largest that fits. A 16:9 display, an unknown one or one of another shape keeps 1920x1080.
// Xbox and keyboard prompts have a folder for each of these sizes (romfs-patches-xbox-<w>x<h>, -xbox-label-<w>x<h>,
// -pc-<w>x<h>, installer/steps.py Prepare): they keep 1920x1080 only when the one for the display's size is missing,
// as in a source build without make_romfs_patches.py --prompts <style> --aspect or an install by an older setup,
// where a default of another shape would trade the prompts the player chose for the Switch ones.
static const unsigned SHAPED[][2] = {
{2560, 1080}, {3440, 1440}, {3840, 1600}, {5120, 2160}, // 21:9
{3840, 1080}, {5120, 1440}, // 32:9
{1280, 800}, {1680, 1050}, {1920, 1200}, {2560, 1600}, // 16:10
{1920, 1280}, {2160, 1440}, {2256, 1504}, {2880, 1920}, {3000, 2000}, // 3:2
};
static char screen_res[16]; // set once by cfg_init, "" for none
// romfs-patches-xbox-, romfs-patches-xbox-label- and romfs-patches-pc-<screen_res> have a manifest (set once by
// cfg_init)
static const char *const screen_folders[3] = {"romfs-patches-xbox", "romfs-patches-xbox-label", "romfs-patches-pc"};
static int screen_prompts[3];
// Whether the prompts in effect have their folder at screen_res. The same picks as input_map.c input_prompts_pick and
// input_prompts_folder: an unknown or empty value is the Nintendo prompts, an unset one Xbox while the pad layout is
// "xbox_prompts"; the Xbox prompts take romfs-patches-xbox with the Nintendo pad layout (or "xbox_prompts") and
// romfs-patches-xbox-label with any other.
static int shape_keeps_prompts(void) {
const char *p = raw_get(PREFIX "PROMPTS"), *l = raw_get(PREFIX "PAD_LAYOUT");
if (p && *p && !strcasecmp(p, "pc")) return screen_prompts[2];
int xbox = p && *p ? !strcasecmp(p, "xbox") : l && !strcasecmp(l, "xbox_prompts");
if (!xbox) return 1;
int by_position = l && (!strcasecmp(l, "nintendo") || !strcasecmp(l, "xbox_prompts"));
return screen_prompts[by_position ? 0 : 1];
}
static void screen_default(void) {
int sw, sh;
if (!plat_screen_size(&sw, &sh)) return;
double r = (double)sw / sh;
if (fabs(r / (16.0 / 9.0) - 1) <= 0.01) return;
int best = -1, fit = -1;
for (int i = 0; i < (int)(sizeof SHAPED / sizeof *SHAPED); i++) {
unsigned w = SHAPED[i][0], h = SHAPED[i][1];
if (fabs((double)w / h / r - 1) > 0.01 || w > (unsigned)sw || h > (unsigned)sh) continue;
if (fit < 0 || w > SHAPED[fit][0]) fit = i; // the largest that fits
if (h >= 1080 && (best < 0 || w < SHAPED[best][0])) best = i;
}
if (best < 0) best = fit;
if (best < 0) return;
snprintf(screen_res, sizeof screen_res, "%ux%u", SHAPED[best][0], SHAPED[best][1]);
for (int i = 0; i < 3; i++)
screen_prompts[i] = romfs_shaped_prompts_ready(screen_folders[i], SHAPED[best][0], SHAPED[best][1]);
if (!raw_get(PREFIX "OUTPUT_RES") && shape_keeps_prompts())
rt_log("cfg: the display is %dx%d: output_res defaults to %s, its shape (totk.ini output_res sets another)", sw,
sh, screen_res);
}
const char *cfg_get(const char *name) {
const char *v = raw_get(name);
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
if (v || strncmp(name, PREFIX, PREFIX_LEN)) return v;
if (!profile_of())
return screen_res[0] && !strcasecmp(name + PREFIX_LEN, "OUTPUT_RES") && shape_keeps_prompts() ? screen_res
: NULL;
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
return NULL;
@@ -692,6 +775,7 @@ static void load_ini(void) {
void cfg_init(void) {
load_ini();
if (!profile_of()) screen_default();
if (profile_of())
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
"turns it off)");
+19
View File
@@ -230,10 +230,24 @@ int probe_cfg(void) {
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
memset(dn, 0, sizeof dn);
dn[KEY_T] = 1;
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: T is Y too (the left button is A in the menus)");
plat_setenv("TOTK_BIND_X", "key:t");
input_map_configure();
CHECK(input_map_buttons(dn) == 1ull << B_X, "bind: bind_X = key:t takes T from Y's defaults");
dn[KEY_T] = 0, dn[KEY_MOUSE_L] = 1;
CHECK(input_map_buttons(dn) == 1ull << B_Y, "bind: Y keeps the left mouse button");
plat_setenv("TOTK_BIND_X", NULL);
input_map_reset();
memset(dn, 0, sizeof dn);
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
memset(dn, 0, sizeof dn);
dn[KEY_G] = 1;
CHECK(input_map_buttons(dn) == 0 && input_map_held(dn, B_SHAKE) && !input_map_held(dn, B_A),
"keys: G shakes and presses no button");
memset(dn, 0, sizeof dn);
dn[KEY_W] = 1;
input_map_stick(dn, 0, st);
CHECK(st[0] == 0 && st[1] == 32767, "keys: W is left stick up");
@@ -301,6 +315,11 @@ int probe_cfg(void) {
CHECK(input_prompts_pick(NULL, "auto") == PROMPTS_NINTENDO && input_prompts_pick("", "xbox") == PROMPTS_NINTENDO &&
input_prompts_pick("Xbox", "nintendo") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox") == PROMPTS_NINTENDO,
"prompts: the game's own unless set to Xbox");
CHECK(input_prompts_pick("PC", "nintendo") == PROMPTS_PC && input_prompts_pick("pc", "xbox_prompts") == PROMPTS_PC &&
EQ(input_prompts_folder(PROMPTS_PC, "nintendo"), "romfs-patches-pc") &&
EQ(input_prompts_folder(PROMPTS_PC, "xbox"), "romfs-patches-pc") &&
EQ(input_prompts_folder(PROMPTS_PC, NULL), "romfs-patches-pc"),
"prompts: pc shows the keyboard and mouse whatever the pad layout");
CHECK(input_prompts_pick(NULL, "xbox_prompts") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox_prompts") == PROMPTS_NINTENDO,
"prompts: 0.1.9's pad_layout = xbox_prompts means Xbox while prompts is unset");
CHECK(!input_prompts_folder(PROMPTS_NINTENDO, "nintendo") && !input_prompts_folder(PROMPTS_NINTENDO, "xbox"),
+83 -9
View File
@@ -10,8 +10,9 @@
// (tools/cheats.py has the format). A cheat whose status line is missing or has another content hash was added or
// edited after the build: a data cheat runs anyway, a code cheat waits for the setup to run again.
//
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A; the 0xC.. and 0xF.. extensions are not
// supported), every presented frame while their switch is on, with registers cleared before each cheat. Memory is the
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A and C0-C3; C4 and the 0xF.. extensions are not
// supported), every presented frame while their switch is on, with registers and saved registers cleared before each
// cheat; the static registers (C3) stay, shared by all cheats, as dmnt keeps them. Memory is the
// guest's, mapped at its own addresses: main's image is written with its pages made writable for the write (read-only
// data included, as dmnt writes through the debugger), everything else after mem_query says the guest may read or write
// it. Buttons are input_poll's HidNpadButton bits, the same as the cheat format's key masks; they read 0 while the menu
@@ -53,6 +54,7 @@ static int g_n, g_cap, g_vm; // g_vm: some cheat needs cheats_frame
static char g_file[64]; // "<build id>.txt"
static char g_dir[PLAT_PATH_MAX]; // the folder of totk.exe, with a trailing separator
static int g_files; // mods whose file was read
static uint64_t g_static[256]; // C3's static registers: 0x00-0x7F loaded by cheats, 0x80-0xFF stored by them
static uint64_t g_main, g_main_size, g_code_end; // main's image; its code with the padding up to .rodata
static uint32_t fnv(const char *s, size_t n) {
@@ -96,6 +98,11 @@ static int op_len(const uint32_t *w, int left) {
case 5: case 7: n = 2; break;
case 9: n = 1 + (((f >> 8) & 0xF) ? (width == 8 ? 2 : 1) : 0); break;
case 0xA: n = ((f >> 8) & 0xF) == 2 || ((f >> 8) & 0xF) == 4 || ((f >> 8) & 0xF) == 5 ? 2 : 1; break;
case 0xC: // C0TcSX..: an address word for operand types 0 and 2, the value (two words at width 8) for 4
if (((f >> 24) & 0xF) != 0) n = 1;
else if (((f >> 8) & 0xF) == 0 || ((f >> 8) & 0xF) == 2) n = 2;
else n = ((f >> 8) & 0xF) == 4 ? (((f >> 20) & 0xF) == 8 ? 3 : 2) : 1;
break;
default: n = 1; break;
}
return n <= left ? n : 0;
@@ -105,12 +112,15 @@ static int op_len(const uint32_t *w, int left) {
static const char *vm_unsupported(const uint32_t *w, char *buf, int n) {
uint32_t f = w[0];
int t = f >> 28, width = (f >> 24) & 0xF;
if (t > 0xA || (t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
int ext = f >> 24; // 0xC0-0xC3 run; C0's width is at bits 20-23 and its operand type (at most 5) at bits 8-11
if (ext == 0xC0) t = -1, width = (f >> 20) & 0xF;
if ((t > 0xA && (ext < 0xC1 || ext > 0xC3)) || (t == -1 && ((f >> 8) & 0xF) > 5) ||
(t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
snprintf(buf, n, "opcode %08X is not supported", f);
return buf;
}
if ((t <= 1 || t == 5 || t == 6 || t == 7 || t == 9 || t == 0xA) && width != 1 && width != 2 && width != 4 &&
width != 8) {
width != 8) { // C0 too (t -1)
snprintf(buf, n, "opcode %08X has a bad width", f);
return buf;
}
@@ -227,7 +237,9 @@ static int compare(int cond, uint64_t a, uint64_t b) {
}
// From i (after a failed condition, or an else reached while running), the opcode to continue at: after the
// matching else (else_ok) or end.
// matching else (else_ok) or end. Conditions 1, 8 and C0 open a block, as in Atmosphere's dmnt
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::SkipConditionalBlock), which steps
// over whole opcodes so that an operand word reading 20000000 ends nothing.
static int skip(const uint32_t *w, int n, int i, int else_ok) {
int depth = 0;
while (i < n) {
@@ -236,17 +248,41 @@ static int skip(const uint32_t *w, int n, int i, int else_ok) {
uint32_t f = w[i];
int t = f >> 28;
i += len;
if (t == 1 || t == 8) depth++;
else if (t == 2 && ((f >> 24) & 0xF) == 0 && depth-- == 0) return i;
else if (t == 2 && depth == 0 && else_ok) return i;
if (t == 1 || t == 8 || f >> 24 == 0xC0) depth++;
else if (t == 2 && ((f >> 24) & 0xF) == 1) { // else; any other 2x000000 ends, as dmnt decodes is_else
if (depth == 0 && else_ok) return i;
} else if (t == 2 && depth-- == 0) {
return i;
}
}
return n;
}
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
// C0TcSX..: if register S <cond c> operand X, all cut to width T; the operand as Atmosphere's dmnt reads it
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::DecodeNextOpcode and Execute,
// BeginRegisterConditionalBlock). X: 0 region M + a, 1 region M + register r, 2 register R + a, 3 register R +
// register r, 4 the value V, 5 register R. M, R at bits 4-7, r and the top bits of a at bits 0-3. A read the guest
// cannot make compares as 0: dmnt ignores the read's result and compares the zero it started with.
static int reg_cond(const uint32_t *w, const uint64_t *r) {
uint32_t f = w[0];
int width = (f >> 20) & 0xF, cond = (f >> 16) & 0xF, a4 = (f >> 4) & 0xF, a0 = f & 0xF;
uint64_t src = masked(width, r[(f >> 12) & 0xF]), b = 0, addr;
switch ((f >> 8) & 0xF) { // w[1] exists for types 0, 2 and 4 only (op_len)
case 0: addr = region(a4) + (((uint64_t)a0 << 32) | w[1]); break;
case 1: addr = region(a4) + r[a0]; break;
case 2: addr = r[a4] + (((uint64_t)a0 << 32) | w[1]); break;
case 3: addr = r[a4] + r[a0]; break;
case 4: return compare(cond, src, width == 8 ? imm64(w + 1) : masked(width, w[1]));
default: return compare(cond, src, masked(width, r[a4]));
}
if (!rd(addr, width, &b)) b = 0;
return compare(cond, src, b);
}
static void run(const Cheat *c, uint64_t keys) {
uint64_t r[16] = {0};
uint64_t r[16] = {0}, saved[16] = {0};
int loop[16] = {0};
const uint32_t *w = c->words;
int n = c->n, budget = OPS_PER_RUN;
@@ -323,6 +359,44 @@ static void run(const Cheat *c, uint64_t keys) {
if (z) r[y] += (uint64_t)width;
break;
}
case 0xC: // C1-C3 as dmnt's Execute runs SaveRestoreRegister(Mask) and ReadWriteStaticRegister
switch (f >> 24) {
case 0xC0:
if (!reg_cond(w + i, r)) {
i = skip(w, n, i + len, 1);
continue;
}
break;
case 0xC1: { // C10D0Sx0: x 0 register D = saved S, 1 saved D = register S, 2 saved D = 0,
// 3 register D = 0
int d = (f >> 16) & 0xF, s = (f >> 8) & 0xF, op = (f >> 4) & 0xF;
if (op == 1) saved[d] = r[s];
else if (op == 2) saved[d] = 0;
else if (op == 3) r[d] = 0;
else r[d] = saved[s]; // dmnt restores for any other x too
break;
}
case 0xC2: { // C2x0XXXX: for each register in the mask X, x as C1 with D = S = that register
int op = (f >> 20) & 0xF;
for (int k = 0; k < 16; k++) {
if (!(f >> k & 1)) continue;
if (op == 1) saved[k] = r[k];
else if (op == 2) saved[k] = 0;
else if (op == 3) r[k] = 0;
else r[k] = saved[k];
}
break;
}
case 0xC3: { // C3000XXx: static register XX < 0x80 into register x, else register x into it
int s = (f >> 4) & 0xFF;
if (s < 0x80) r[f & 0xF] = g_static[s];
else g_static[s] = r[f & 0xF];
break;
}
default:
return;
}
break;
default:
return;
}
+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,
};
+5
View File
@@ -91,6 +91,11 @@ void frame_step_init(void) {
rt_patch_disable(cloth);
rt_log("frame rate: cloth clock off, it needs the real-time step (TOTK_REALTIME=0)");
}
int climb = rt_patch_set("climb"); // reads the page's frame ratio, written only with the real-time step
if (climb >= 0 && !rt_patch_enabled(realtime) && rt_patch_enabled(climb)) {
rt_patch_disable(climb);
rt_log("frame rate: climb speed fix off, it needs the real-time step (TOTK_REALTIME=0)");
}
if (rt_patch_enabled(realtime)) {
step_word = (_Atomic uint64_t *)rt_patch_page();
step_interval = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 8);
+2 -1
View File
@@ -41,7 +41,8 @@ size_t romfs_overlay_count(void);
int romfs_patches_loaded(void); // a romfs-patches manifest gave at least one file
// The button prompts (input_prompts_folder): the folder loaded at start, NULL for the game's own prompts; whether a
// folder had a manifest at start; and why the folder the settings asked for at start was not loaded (ROMFS_PROMPTS_OK
// for any other folder: it was not asked for).
// for any other folder: it was not asked for). romfs-patches-pc goes by that name also where romfs-patches-pc-<w>x<h>
// stands in for it (romfs.c overlay_init).
enum { ROMFS_PROMPTS_OK, ROMFS_PROMPTS_MISSING, ROMFS_PROMPTS_FAILED, ROMFS_PROMPTS_SHAPE, ROMFS_PROMPTS_MODS,
ROMFS_PROMPTS_PATCHES };
const char *romfs_prompts(void);
+9 -2
View File
@@ -23,6 +23,7 @@ uint64_t gfx_vsync_ticks(void); // ticks since start
uint64_t gfx_vsync_time_ns(uint64_t tick); // kern_now_ns() time of a tick (pacing)
double gfx_window_refresh_hz(void); // refresh rate of the window's monitor; 0 unknown
int gfx_window_focused(void);
int gfx_picture_rect(float out[4]); // where the last present put the picture, fractions of the swapchain (x, y, w, h); 0 before one
// ---- shader service client (gfx_shader.c)
enum { GFX_STAGE_VS, GFX_STAGE_TCS, GFX_STAGE_TES, GFX_STAGE_GS, GFX_STAGE_FS, GFX_STAGE_CS };
@@ -35,6 +36,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,
@@ -50,7 +53,7 @@ const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8
uint64_t gfx_shader_async_done(void); // translations finished by the background worker
void gfx_shader_counts(uint64_t *keys, uint64_t *translated); // distinct keys in memory, translations done (TOTK_TRACE=perf)
// Translator answers for this device (vk_caps.c): shaderFloat64, storage offset rounding, subgroup size per vs/fs/cs.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits);
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits, int zero_locals);
const uint8_t *gfx_shader_key(const GfxShader *s); // the 32-byte cache key (stable while the translator is)
const uint8_t *gfx_shader_input_id(const GfxShader *s); // the 32-byte input id (stable across translators)
// By input id from memory or disk; xlate 1 also translates it from its stored inputs. NULL if absent.
@@ -73,6 +76,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 +103,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.
+1
View File
@@ -100,6 +100,7 @@ static void null_replay(uint64_t handle) {
}
static uint64_t null_submit(const uint64_t *handles, int n) {
nvn_pins_sweep(nvn_submit_count(), NULL, 0); // no replay state outlives a submission here
for (int i = 0; i < n; i++) null_replay(handles[i]);
return __atomic_add_fetch(&fence, 1, __ATOMIC_ACQ_REL);
}
+39 -5
View File
@@ -33,9 +33,20 @@
#define VSYNC_HZ 60
// ---- queue lock
// The recorder queues the parts it records and submits them later (vk_rec.c rec_submit): the first lock of a thread
// waits until the parts queued before it are submitted, so whatever it puts on the queue comes after them, as when
// the submitting thread submitted them itself. Not inside a held lock: the recorder takes this lock to submit.
static PlatRMutex g_ql;
void vk_queue_lock(void) { plat_rmutex_lock(&g_ql); }
void vk_queue_unlock(void) { plat_rmutex_unlock(&g_ql); }
static _Thread_local int ql_depth;
void vk_queue_lock(void) {
if (!ql_depth) rec_queue_wait();
plat_rmutex_lock(&g_ql);
ql_depth++;
}
void vk_queue_unlock(void) {
ql_depth--;
plat_rmutex_unlock(&g_ql);
}
// ---- window state shared with the back end
static VkSurfaceKHR g_surface;
@@ -72,6 +83,7 @@ void gfx_window_closed(void) {
rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting");
fs_save_flush();
vk_pipeline_save();
playtime_save();
prof_write(); // plat_exit runs no atexit handlers
if (restart) {
plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c)
@@ -235,6 +247,7 @@ static void window_settings_live(void) {
if (started && v == seen) return;
seen = v;
res_fov_live();
res_dynres_live();
const char *e = cfg_value("TOTK_VK_PRESENT");
if (!started || strncmp(e, present, sizeof present - 1)) {
snprintf(present, sizeof present, "%s", e);
@@ -295,8 +308,7 @@ static void free_capture(void) {
g_cap_valid = 0;
}
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held.
static int create_swapchain(void) {
static int build_swapchain(void) {
flip_locked(); // the acquired image goes out before its swapchain is replaced
VkCtx *c = vk_ctx();
VkSurfaceCapabilitiesKHR caps;
@@ -364,10 +376,19 @@ static int create_swapchain(void) {
}
free_capture();
vk_queue_unlock();
plat_xchg32(&g_dirty, 0);
return 1;
}
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held. The stale mark
// goes before the surface is read: a resize during the build keeps it. The switch to fullscreen at start lands there
// on the window thread, and the driver does not report the swapchain as out of date.
static int create_swapchain(void) {
plat_xchg32(&g_dirty, 0);
if (build_swapchain()) return 1;
plat_xchg32(&g_dirty, 1);
return 0;
}
static void sync_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0,
VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) {
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0,
@@ -543,6 +564,15 @@ static void save_capture(const char *path) {
// TOTK_ASPECT=stretch, else (fit, the default) the largest one of the picture's own aspect ratio, centred (a 16:9 picture
// on the Steam Deck's 1280x800 screen gets bars of 40 rows above and below; a 16:9 picture in a 21:9 window, bars at the
// sides).
// Where the last present put the picture, as fractions of the swapchain (x, y, w, h): menu_cursor.c's pointer
static float g_pic[4];
static volatile int32_t g_pic_ok;
int gfx_picture_rect(float out[4]) {
if (!__atomic_load_n(&g_pic_ok, __ATOMIC_ACQUIRE)) return 0;
memcpy(out, g_pic, sizeof g_pic);
return 1;
}
void gfx_fit_rect(int cw, int ch, int dw, int dh, int out[4]) {
out[0] = out[1] = 0, out[2] = dw, out[3] = dh;
if (cw <= 0 || ch <= 0 || !strcmp(cfg_value("TOTK_ASPECT"), "stretch")) return;
@@ -616,6 +646,10 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
const int rect[4] = {cx, cy, cw, ch};
int to[4]; // where the picture goes in the swapchain image: {x, y, w, h}
gfx_fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to);
if (g_sc_ext.width && g_sc_ext.height) {
for (int i = 0; i < 4; i++) g_pic[i] = (float)to[i] / (float)(i & 1 ? g_sc_ext.height : g_sc_ext.width);
__atomic_store_n(&g_pic_ok, 1, __ATOMIC_RELEASE);
}
if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) { // black bars beside or above and below it
VkClearColorValue black = {.float32 = {0, 0, 0, 1}};
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
+46 -9
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
@@ -118,7 +119,7 @@ static struct { // translations in fl
static char cache_dir[PATH_CAP];
static char xlate_exe[PATH_CAP];
static uint8_t xlate_id[32]; // translator identity (see the keys at the top)
static int host_f64 = 1, host_set; // gfx_shader_set_host
static int host_f64 = 1, host_zero_locals, host_set; // gfx_shader_set_host
static uint32_t host_sbuf, host_sg[3] = {32, 32, 32}, host_gather;
static PlatOnce paths_once = PLAT_ONCE_INIT;
@@ -312,18 +313,19 @@ static void init_paths(void *param) {
static void paths(void) { plat_once(&paths_once, init_paths, NULL); }
// Host answers (vk_caps.c, at device creation, before any translation). Answers other than the defaults (Float64,
// exact storage offsets, 32 lanes, no gather bias) are mixed into the translator identity, so they get their own cache
// keys.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits) {
// exact storage offsets, 32 lanes, no gather bias, undefined locals) are mixed into the translator identity, so they
// get their own cache keys.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits, int zero_locals) {
paths();
if (host_set++) return;
host_f64 = float64, host_sbuf = sbuf_align, host_gather = gather_bits;
host_f64 = float64, host_sbuf = sbuf_align, host_gather = gather_bits, host_zero_locals = zero_locals;
memcpy(host_sg, subgroup, sizeof host_sg);
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32 && !gather_bits) return;
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32 && !gather_bits && !zero_locals) return;
uint8_t buf[32 + 96];
int n = snprintf((char *)buf + 32, 96, "TKHO f64=%d sbuf=%u sg=%u,%u,%u", float64, sbuf_align, subgroup[0], subgroup[1],
subgroup[2]);
if (gather_bits) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " gather=%u", gather_bits);
if (zero_locals) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " zero_locals");
memcpy(buf, xlate_id, 32);
rt_sha256(buf, 32 + (size_t)n, xlate_id);
rt_log("gfx_shader: host options %s: own shader cache keys", (char *)buf + 37);
@@ -593,6 +595,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 +847,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 +1059,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 +1091,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,9 +1171,11 @@ 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);
if (host_zero_locals) o += sprintf(o, ",\"zero_locals\":true");
o += sprintf(o, "},\"out\":\"");
json_esc(&o, cache_dir);
o += sprintf(o, "\"}");
@@ -1350,8 +1375,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 +1741,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 +1832,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.
+26 -5
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,9 +333,12 @@ 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) {
// Pending before the raise: the activation it gets is the first WM_ACTIVATEAPP, which takes the fullscreen
// setting; set after it, fullscreen waited for the next activation and the game started in a window
g_fs_pending = cfg_fullscreen();
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();
}
note_applied();
refresh_rate_update(g_hwnd, 1);
+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,
};
+44
View File
@@ -36,6 +36,10 @@ void perf_end(int kind, uint64_t t0); // counts render-thread time onl
void perf_replay(uint64_t t0); // replay (NVN -> Vulkan recording) time on any thread, waits inside it included
void perf_replay_thread(void); // the calling thread replays for the render thread: perf_end counts its waits
void perf_main_thread(void); // the calling thread is the guest main thread (TOTK_PROFILE_MAIN)
// The calling thread is one every frame goes through: on a hybrid CPU it prefers the P-cores (main.c, TOTK_PCORE);
// the first thread of each role logs it.
enum { PCORE_MAIN, PCORE_RENDER, PCORE_REPLAY, PCORE_RECORDER, PCORE_PRESENT, PCORE_N };
void perf_core_thread(int role);
// Events per 60-frame window: graphics pipelines finished by the background workers, draws skipped while pending,
// first texture uploads and re-uploads after CPU writes, draws skipped while a shader translates in the background,
// draws that waited for their pending pipeline and got it, draws into a target the previous frame did not render that
@@ -167,6 +171,26 @@ void mem_thaw(uint64_t a, uint64_t n); // makes the frozen r
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws);
// regions frozen now; freezes and thaws since the last call
void mem_kernel_free(void *p, uint64_t size); // TLR/TLS reuse after thread exit
int mem_guest_readable(uint64_t a, uint64_t n); // [a, a+n) can be read without a fault: inside a loaded
// module or the committed heap answered from the layout,
// anything else by plat_mem_readable (an OS query)
// ---- short out-of-memory spells (oom.c)
enum { RT_OOM_COMMIT, RT_OOM_STAGING, RT_OOM_IMAGE, RT_OOM_CHUNK, RT_OOM_SITES };
extern volatile int32_t rt_oom_req; // set by rt_oom_request; vk_texture_new_submit takes it
void rt_oom_request(void); // asks the Vulkan back end to give memory back at its next
// submit (released textures, headroom blocks); any thread,
// any lock held
int rt_oom_wait(const char *what, uint64_t bytes, int (*retry)(void *arg), void *arg);
// after a failed allocation: requests a release, then calls
// retry every 10 ms for up to 30 s; 1 once retry returned 1,
// 0 after the wait (the caller ends the game). Logs the
// first 8 with the commit numbers
void *rt_realloc_wait(void *old, size_t n, const char *what); // realloc, NULL only after rt_oom_wait gave up
int rt_oom_forced(int site); // 1: fail this attempt at site (TOTK_TEST_OOM=<site>:<n>,
// sites commit, staging, image, chunk; or rt_oom_force)
void rt_oom_force(int site, int n); // probes: fail the next n attempts at site
int rt_oom_forced_left(int site); // forced failures still to come at site
// ---- modules (loader.c)
typedef struct {
@@ -257,17 +281,31 @@ void cam_pack_served(void); // romfs.c serves the
// ---- game resolution (resolution.c): TOTK_OUTPUT_RES, TOTK_RENDER_SCALE
uint64_t res_heap_extra_bytes(void); // added to the application pool (mem_init), 2 MiB steps
uint64_t res_heap_extra_kib(void); // added to the HeapDef texture heap and its parent; 0 = none
uint64_t res_tex_heap_extra_bytes(void); // TOTK_TEX_HEAP_EXTRA_MB: added to the application pool and
// the heap cap (mem_init), 2 MiB steps; 0 = none
int res_heapdef_tex_grown(const void *in, size_t n); // the HeapDef table (.byml.zs) grows TextureHeap over the
// shipped size (an HD texture pack's)
int res_heapdef_plain(const void *in, size_t n); // a readable HeapDef table whose TextureHeap is not
// grown (a content mod's)
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); // the HeapDef table (.byml.zs) with
// the grown heaps, malloc'd; 0 = cannot (logged)
void res_heapdef_served(void); // romfs.c serves the grown table
void res_init(void); // after rt_patches_init: the patch page and the sets
float res_dlss_lod_bias(void); // TOTK_DLSS upscaling: texture LOD bias (<= 0)
int res_dlss_mode(const char *mode); // TOTK_DLSS changed while running: new scene size
void res_fn_hit(uint64_t addr, uint64_t x0); // main.c fnprof_hit: catches the renderer at start
void res_present(Cpu *c); // nvnQueuePresentTexture: applies a pending scene size
void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene sizes in effect (the game's own
// until res_init switched the sets on)
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
int romfs_shaped_ready(unsigned w, unsigned h); // romfs-patches-<w>x<h> has a manifest (romfs.c; works
// before fs_init)
int romfs_shaped_prompts_ready(const char *folder, unsigned w, unsigned h); // the same for <folder>-<w>x<h>, a
// prompts' folder (romfs-patches-xbox, -xbox-label, -pc)
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
void res_dynres_live(void); // TOTK_DYNAMIC_RES to the game (set fixres); on a settings change
// ---- code patches (patches.c) and the frame step (frame_rate.c)
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
// page committed; logs the sets
@@ -309,6 +347,12 @@ 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
void playtime_init(void); // playtime.c: the play time thread, game runs only
void playtime_save(void); // playtime.c: writes <profile>/playtime.txt; at exit
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c: all runs and this one; 0 without a profile
int probe_playtime(void); // playtime.c: the file, the ticks and the save
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
+24 -5
View File
@@ -35,7 +35,7 @@ typedef struct {
const char *name;
GuestFn fn;
uint64_t original; // observers: the guest definition rt_original calls
uint64_t calls;
uint64_t calls; // with TOTK_TRACE=host only (hostfn_report)
int blocking; // runs with the virtual core given up (hostfn_blocking)
} Slot;
static Slot slots[MAX_HOSTFN];
@@ -44,6 +44,21 @@ static RtHostEntry added[MAX_ADDED];
static int nadded;
static int sealed;
// The call count (hostfn_calls): one counter per calling thread, each on its own cache line, written only by that
// thread. A locked add on a per-function counter beside the read-mostly slot fields made the game threads share and
// bounce those lines on every host call (1.4.3 town: about 1% of the game threads' time in hostfn_entry).
#define MAX_COUNTERS 4096
typedef struct { _Alignas(64) uint64_t n; } Counter;
static Counter counters[MAX_COUNTERS];
static uint32_t ncounters;
static uint64_t shared_calls; // the threads past MAX_COUNTERS, counted with an atomic add
static __thread uint64_t *thread_calls;
__attribute__((noinline)) static uint64_t *counter_new(void) {
uint32_t i = __atomic_fetch_add(&ncounters, 1, __ATOMIC_RELAXED);
return thread_calls = i < MAX_COUNTERS ? &counters[i].n : &shared_calls;
}
static GuestFn table_find(const RtHostEntry *start, const RtHostEntry *stop, const char *name) {
__asm__("" : "+r"(start), "+r"(stop)); // separate objects: keep the compiler from reasoning about them
for (const RtHostEntry *p = start; p < stop; p++)
@@ -146,8 +161,11 @@ static void hostfn_entry(Cpu *c) {
uint64_t i = (c->pc - RT_HOSTFN_BASE) >> 4;
if (__builtin_expect((c->pc & 15) || i >= (uint64_t)nslots, 0)) bad_entry(c);
Slot *s = &slots[i];
uint64_t n = __atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED);
if (__builtin_expect(!n && (rt_trace & RT_TRACE_HOSTCALL), 0)) {
uint64_t *k = thread_calls;
if (__builtin_expect(!k, 0)) k = counter_new();
if (__builtin_expect(k == &shared_calls, 0)) __atomic_fetch_add(k, 1, __ATOMIC_RELAXED);
else __atomic_store_n(k, *k + 1, __ATOMIC_RELAXED);
if (__builtin_expect(rt_trace & RT_TRACE_HOSTCALL, 0) && !__atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED)) {
char lr[96];
rt_log("host call %s from %s", s->name, ldr_describe(c->x[30] - 4, lr, sizeof lr));
}
@@ -178,8 +196,9 @@ void hostfn_seal(void) {
}
uint64_t hostfn_calls(void) {
uint64_t n = 0;
for (int i = 0; i < nslots; i++) n += __atomic_load_n(&slots[i].calls, __ATOMIC_RELAXED);
uint32_t m = __atomic_load_n(&ncounters, __ATOMIC_RELAXED);
uint64_t n = __atomic_load_n(&shared_calls, __ATOMIC_RELAXED);
for (uint32_t i = 0; i < m && i < MAX_COUNTERS; i++) n += __atomic_load_n(&counters[i].n, __ATOMIC_RELAXED);
return n;
}
+184 -21
View File
@@ -26,6 +26,13 @@ enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 100
static PlatMutex lock = PLAT_MUTEX_INIT;
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
static volatile int32_t g_block, g_menu; // text entry / host menu: 0 off, 1 open, 2 waiting for release
// The device the player used last (input_kbm_last): 1 keyboard and mouse, 0 the gamepad. A pad counts once a button is
// down or a stick is past half way (a drifting stick never switches), the mouse once it moved MOUSE_USED raw counts
// within one poll (a slow deliberate move does; a desk bump does not), the keyboard with any key or mouse button.
enum { PAD_USED = 16384, MOUSE_USED = 6 };
static volatile int32_t g_kbm_last = 1;
int input_kbm_last(void) { return __atomic_load_n(&g_kbm_last, __ATOMIC_RELAXED); }
static SDL_Gamepad *pad;
static uint64_t next_scan;
static int pad_rumble; // the open gamepad has rumble motors
@@ -66,6 +73,7 @@ static const struct { const char *name; int bit; } script_buttons[] = {
static ScriptEv *script_ev;
static int script_n, script_state; // 0 not loaded, 1 loaded
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
static int32_t script_mouse[2]; // one-shot raw counts, consumed by the camera hook; under lock
static void script_load(void) {
script_state = 1;
@@ -88,10 +96,11 @@ static void script_load(void) {
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
long a, b;
double c;
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
if (!strcmp(name, "MOUSE") || !strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
e.stick = name[0] == 'M' ? 3 : name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
if (e.stick == 3) e.hold = 1; // a displacement, never a held speed
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
} else {
@@ -123,12 +132,17 @@ static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
uint64_t now = e->timed ? (uint64_t)w : tick;
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
if (e->stick == 3 && e->logged) continue;
if (!e->logged) {
e->logged = 1;
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
}
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
if (e->stick == 3) {
int64_t x = (int64_t)script_mouse[0] + e->x, y = (int64_t)script_mouse[1] + e->y;
script_mouse[0] = (int32_t)(x < INT32_MIN ? INT32_MIN : x > INT32_MAX ? INT32_MAX : x);
script_mouse[1] = (int32_t)(y < INT32_MIN ? INT32_MIN : y > INT32_MAX ? INT32_MAX : y);
} else if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
else *b |= 1ull << e->bit;
}
}
@@ -223,6 +237,30 @@ static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
if (wrote) fflush(rec);
}
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy) {
plat_mutex_lock(&lock);
enabled = enabled && !g_block && !g_menu;
*dx = script_mouse[0]; *dy = script_mouse[1];
script_mouse[0] = script_mouse[1] = 0;
if (!enabled) { *dx = *dy = 0; }
else if (ikb_captured() && ikb_foreground() && input_cursor_allowed()) {
int32_t x, y;
ikb_mouse_take(&x, &y);
int64_t sx = (int64_t)*dx + x, sy = (int64_t)*dy + y;
*dx = (int32_t)(sx < INT32_MIN ? INT32_MIN : sx > INT32_MAX ? INT32_MAX : sx);
*dy = (int32_t)(sy < INT32_MIN ? INT32_MIN : sy > INT32_MAX ? INT32_MAX : sy);
}
if (enabled && (*dx || *dy) && rec_state == 1) {
char what[80];
snprintf(what, sizeof what, "MOUSE %d %d", *dx, *dy);
int64_t w = frame_step_world_us();
uint64_t tick = frame_step_ticks();
rec_line(tick, w, tick + 1, w, what);
fflush(rec);
}
plat_mutex_unlock(&lock);
}
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
static unsigned mouse_sens_seen;
@@ -282,15 +320,41 @@ static void walk_get(void) {
}
}
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`. The sample is divided by
// the camera speed factor the game runs with (camera_speed.c, cam_speed_applied), which multiplies every right-stick
// turn rate, so TOTK_MOUSE_SENS alone sets the mouse's speed; below 1 the sample stays within the stick's range, so the
// mouse's top speed drops with the factor.
static void mouse_camera(int32_t out[2]) {
static int mouse_selector;
int input_direct_mouse_selector_active(void) { return __atomic_load_n(&mouse_selector, __ATOMIC_RELAXED) != 0; }
// Selector 1 is a radial wheel, 2..4 are distinct horizontal quick lists; only a wheel retains a virtual cursor.
// Under `lock`.
static void mouse_camera(int32_t out[2], int selector) {
static uint64_t frame = ~0ull, t0;
static int32_t held[2];
static int was_selector;
static float cursor[2];
static InputMouseRow row;
uint64_t now = plat_now_ns();
int32_t dx, dy;
if (!input_direct_mouse_enabled() || !ikb_captured()) selector = 0;
__atomic_store_n(&mouse_selector, selector, __ATOMIC_RELAXED);
if (selector) {
if (was_selector != selector) cursor[0] = cursor[1] = 0, row = (InputMouseRow){0};
was_selector = selector;
ikb_mouse_take(&dx, &dy);
if (selector == 1) input_direct_mouse_radial(dx, dy, mouse_sens_get(), cursor, out);
else input_direct_mouse_row(dx, mouse_sens_get(), now, &row, out);
held[0] = held[1] = 0; frame = ~0ull;
return;
}
if (was_selector) {
was_selector = 0; cursor[0] = cursor[1] = 0; row = (InputMouseRow){0};
ikb_mouse_take(&dx, &dy); // release the wheel without sending its last movement to the camera
held[0] = held[1] = out[0] = out[1] = 0; frame = ~0ull;
return;
}
if (input_direct_mouse_active()) {
held[0] = held[1] = out[0] = out[1] = 0;
frame = ~0ull;
return; // the camera update takes the raw counts itself, once, outside the HID stick path
}
if (!ikb_captured()) {
ikb_mouse_take(&dx, &dy);
held[0] = held[1] = 0;
@@ -459,6 +523,53 @@ static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
if (b > a) { *x = kx; *y = ky; }
}
// The shake key (B_SHAKE): the game detaches a part fused to what Ultrahand holds when the right stick swings left and
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging; each side lasts SHAKE_SIDE_NS, two
// frames at the game's 30 fps, so every side is read even at 20 fps. Returns the stick x, 0 while not shaking.
// Under lock.
#define SHAKE_NS 800000000ull
#define SHAKE_SIDE_NS 66666667ull
static int32_t shake_stick(int held) {
static int was;
static uint64_t from, until;
uint64_t now = plat_now_ns();
if (held && !was && now >= until) from = now;
if (held && !was) until = now + SHAKE_NS;
if (held && until < now) until = now;
was = held;
if (now >= until && !held) return 0;
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
}
// The scroll wheel on the Purah Pad's map, which zooms with the right stick's y (up: in): each notch is a full-tilt
// pulse of MAP_ZOOM_PULSE_NS on that stick with MAP_ZOOM_GAP_NS of neutral after it, so a step of the zoom and a
// held zoom both see one notch as one push. A fast scroll's notches wait their turn (at most MAP_ZOOM_QUEUE), a turn the
// other way drops them. Returns the stick y for this poll, 0 between pulses and off the map. Under lock.
#define MAP_ZOOM_PULSE_NS 100000000ull
#define MAP_ZOOM_GAP_NS 50000000ull
#define MAP_ZOOM_QUEUE 4
static int32_t map_zoom_stick(int32_t notches, int on_map) {
static int32_t queued, dir;
static uint64_t until; // the end of the pulse (dir != 0) or of the neutral after it
uint64_t now = plat_now_ns();
if (!on_map) { queued = dir = 0, until = 0; return 0; }
if (notches) {
if (queued && (notches > 0) != (queued > 0)) queued = 0;
queued += notches;
queued = queued > MAP_ZOOM_QUEUE ? MAP_ZOOM_QUEUE : queued < -MAP_ZOOM_QUEUE ? -MAP_ZOOM_QUEUE : queued;
}
if (dir) {
if (now < until) return dir * STICK_MAX;
dir = 0, until = now + MAP_ZOOM_GAP_NS;
return 0;
}
if (now < until || !queued) return 0;
dir = queued > 0 ? 1 : -1;
queued -= dir;
until = now + MAP_ZOOM_PULSE_NS;
return dir * STICK_MAX;
}
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
static int pad_physical(uint8_t pd[PAD_COUNT]) {
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
@@ -497,6 +608,8 @@ int input_pad_stick_percent(double pct[2]) {
return ok;
}
static uint64_t g_buttons_last; // the merged buttons of the last poll (input_buttons_last)
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
@@ -505,19 +618,27 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
if (state > 0) {
binds_refresh();
if (state == 1) pad_read(&b, sl, sr);
int pad_used = b || abs(sl[0]) > PAD_USED || abs(sl[1]) > PAD_USED || abs(sr[0]) > PAD_USED ||
abs(sr[1]) > PAD_USED, kbm_used = 0;
if (ikb_foreground()) {
uint8_t down[KEY_COUNT] = {0};
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
// (the click that refocused the window) is ignored until it is released.
// (the click that refocused the window) is ignored until it is released. While a game menu has the pointer
// (menu_cursor.c) and it is over the window, the left and right buttons are that menu's A and B, and the
// middle and side buttons keep their bindings: bound to L and R they turn the menu's tabs.
static int was_captured;
static uint8_t mouse_ignore[KEY_COUNT];
int cap = ikb_captured();
float pointer_x, pointer_y;
int menu_pointer = !cap && menu_cursor_pointer() && ikb_pointer(&pointer_x, &pointer_y);
for (int k = 1; k < KEY_COUNT; k++) {
down[k] = ikb_down(k);
kbm_used |= down[k];
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
if (cap && !was_captured) mouse_ignore[k] = down[k];
if (!down[k]) mouse_ignore[k] = 0;
if (!cap || mouse_ignore[k]) down[k] = 0;
int counts = cap || (menu_pointer && k != KEY_MOUSE_L && k != KEY_MOUSE_R);
if (!counts || mouse_ignore[k]) down[k] = 0;
}
was_captured = cap;
b |= input_map_buttons(down);
@@ -530,9 +651,32 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
input_map_stick(down, 1, k);
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
pick(&sr[0], &sr[1], k[0], k[1]);
mouse_camera(k);
pick(&sr[0], &sr[1], k[0], k[1]);
int selector = b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
// Only while the wheel or a strip is on screen (menu_cursor.c reads the focused screen): L also soothes the
// horse and turns menu tabs, the D-pad does other things too, and then the mouse stays on the camera.
// Before the screens were read (the menu cursor off) the buttons alone decide, as before.
if (selector && menu_cursor_shortcut() == 0) selector = 0;
mouse_camera(k, selector);
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there,
// the scroll wheel zooms (map_zoom_stick). Elsewhere the wheel is unbound; its notches are dropped.
int map = menu_cursor_map();
if (map) pick(&sl[0], &sl[1], k[0], k[1]);
else pick(&sr[0], &sr[1], k[0], k[1]);
int32_t zoom = map_zoom_stick(ikb_wheel_take(), map);
if (zoom) pick(&sr[0], &sr[1], 0, zoom);
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
if (shake) sr[0] = shake, sr[1] = 0;
// A mouse shake (mouse_direct.c) moves the stick with the mouse, side by side, and lets go when it rests
else if (input_direct_mouse_shake()) sr[0] = input_direct_mouse_shake() * STICK_MAX, sr[1] = 0;
static uint32_t travel_seen;
uint32_t travel = ikb_mouse_travel();
if (travel - travel_seen >= MOUSE_USED) kbm_used = 1;
travel_seen = travel;
}
// A press or a full-way stick on the pad wins over keys held in the same poll: the player picked it up
if (pad_used) __atomic_store_n(&g_kbm_last, 0, __ATOMIC_RELAXED);
else if (kbm_used) __atomic_store_n(&g_kbm_last, 1, __ATOMIC_RELAXED);
}
script_apply(&b, sl, sr);
rec_apply(b, sl, sr);
@@ -548,21 +692,29 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
if (overlay) menu_perf_cycle();
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
__atomic_store_n(&g_buttons_last, b, __ATOMIC_RELAXED);
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
}
uint64_t input_buttons_last(void) { return __atomic_load_n(&g_buttons_last, __ATOMIC_RELAXED); }
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
// 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");
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none"));
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none")) && !menu_cursor_pointer();
}
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 +722,36 @@ 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);
if (!g_block && !g_menu) menu_cursor_poll(buttons);
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;
}
+29
View File
@@ -6,6 +6,35 @@
// Down=15) and the stick directions (StickLLeft..Down 16-19, StickRLeft..Down 20-23, derived from the sticks).
// Sticks are -32767..32767, up is +y. TOTK_INPUT=none reports no input. Callable from any thread.
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry);
// 1 when keyboard and mouse are the device the player used last, 0 when the gamepad is: what is shown for pointing
// (menu_cursor.c shows the pointer in the game's menus only for the keyboard and mouse)
int input_kbm_last(void);
// Direct mouse input is consumed by the game's camera-input update, after its stick response. While that update
// accepts camera input, the HID right stick contains only the controller and keyboard. Selectors route mouse input
// separately: a persistent radial cursor for the ability wheel, discrete steps for horizontal quick lists.
int input_direct_mouse_active(void);
int input_direct_mouse_enabled(void);
// A radial selector uses a persistent virtual cursor while its opening button is held.
void input_direct_mouse_radial(int32_t dx, int32_t dy, float sensitivity, float cursor[2], int32_t out[2]);
// Horizontal quick selectors receive short, separated steps instead of a latched radial-cursor direction.
typedef struct { float distance; uint64_t pulse_until, neutral_until; int direction; } InputMouseRow;
void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMouseRow *row, int32_t out[2]);
int input_direct_mouse_selector_active(void);
// Consume direct mouse counts, including one-shot MOUSE events from an input script. enabled=0 discards scripted
// counts while leaving the real mouse for menu input. Live counts require cursor capture and foreground focus.
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy);
// The buttons (B_* bits) of the last poll, keys and gamepad merged; any thread (mouse_direct.c: ZR for the bow's scale)
uint64_t input_buttons_last(void);
// mouse_direct.c: the right stick's side a mouse shake gives while Ultrahand holds an object (-1 / +1), 0 while the
// mouse does not shake; input.c puts it on the stick
int input_direct_mouse_shake(void);
// menu_cursor.c: the L ability wheel or a D-pad strip has the focus: 1 / 0, -1 with the menu cursor off (not read)
int menu_cursor_shortcut(void);
// menu_cursor.c: the pointer in the game's menus (1.4.3). poll: once per pad read, adds a click's A or B; pointer: a
// menu has the pointer, so the mouse camera's capture is released
void menu_cursor_poll(uint64_t *buttons);
int menu_cursor_pointer(void);
int menu_cursor_map(void); // the Purah Pad's map is open: the mouse's stick is the left one (its cursor)
// The same for host UI (the text entry): never blocked. input_block(1) while a text entry is open: input_poll then
// reports nothing; input_block(0) when it closes: nothing until all buttons and stick directions are released.
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry);
+31 -7
View File
@@ -16,11 +16,11 @@
#define DEFAULT_KEYS \
{KEY_W, B_SL_UP}, {KEY_A, B_SL_LEFT}, {KEY_S, B_SL_DOWN}, {KEY_D, B_SL_RIGHT}, /* move */ \
{KEY_UP, B_SR_UP}, {KEY_LEFT, B_SR_LEFT}, {KEY_DOWN, B_SR_DOWN}, {KEY_RIGHT, B_SR_RIGHT}, /* camera, second source */ \
{KEY_MOUSE_L, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
{KEY_MOUSE_L, B_Y}, {KEY_T, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
{KEY_SPACE, B_X}, {KEY_LSHIFT, B_B}, {KEY_E, B_A}, {KEY_Q, B_L}, {KEY_R, B_R}, \
{KEY_LCTRL, B_STICK_L}, {KEY_C, B_STICK_R}, \
{KEY_1, B_LEFT}, {KEY_2, B_UP}, {KEY_3, B_RIGHT}, {KEY_4, B_DOWN}, \
{KEY_TAB, B_PLUS}, {KEY_M, B_MINUS}
{KEY_TAB, B_PLUS}, {KEY_M, B_MINUS}, {KEY_G, B_SHAKE}
// By position: south is Switch B, east A, west Y, north X (Nintendo layout); the Xbox layout swaps A with B and X with Y
// (south A, as the Xbox labels; cross A on a PlayStation pad). The triggers are ZL and ZR.
@@ -31,7 +31,7 @@
{PAD_ZR, B_ZR}
#define NINTENDO_PADS DEFAULT_PADS(B_B, B_A, B_Y, B_X)
#define XBOX_PADS DEFAULT_PADS(B_A, B_B, B_X, B_Y)
enum { DEFAULT_KEY_COUNT = 26, DEFAULT_PAD_COUNT = 16 };
enum { DEFAULT_KEY_COUNT = 28, DEFAULT_PAD_COUNT = 16 };
_Static_assert(sizeof (Binding[]){DEFAULT_KEYS} / sizeof (Binding) == DEFAULT_KEY_COUNT, "DEFAULT_KEY_COUNT");
static const InputMap defaults = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX_PADS}},
@@ -41,7 +41,7 @@ InputMap input_map = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX
static const char *const action_names[ACTION_COUNT] = {
"A", "B", "X", "Y", "LCLICK", "RCLICK", "L", "R", "ZL", "ZR", "PLUS", "MINUS", "LEFT", "UP", "RIGHT", "DOWN",
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN"};
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN", "SHAKE"};
static const char *const pad_names[PAD_COUNT] = {
NULL, "south", "east", "west", "north", "lstick", "rstick", "l", "r", "start", "back",
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr", "lpaddle1", "rpaddle1", "lpaddle2", "rpaddle2"};
@@ -143,7 +143,7 @@ int input_map_bind(const char *button, const char *list) {
}
if (!ok) {
bad++;
rt_log("input: bind %s: ignoring '%s' (expected pad:<name> or key:<name>; stick directions take keys only)",
rt_log("input: bind %s: ignoring '%s' (expected pad:<name> or key:<name>; stick directions and SHAKE take keys only)",
button, tok);
}
}
@@ -166,11 +166,27 @@ int input_map_bind(const char *button, const char *list) {
void input_map_configure(void) {
input_map_reset();
uint8_t lined[ACTION_COUNT] = {0};
for (int a = 0; a < ACTION_COUNT; a++) {
char var[32];
snprintf(var, sizeof var, "TOTK_BIND_%s", action_names[a]);
const char *v = cfg_get(var);
if (v && *v && input_map_bind(action_names[a], v)) rt_log("input: %s = %s", var + 5, v);
if (v && *v && input_map_bind(action_names[a], v)) lined[a] = 1, rt_log("input: %s = %s", var + 5, v);
}
// A key a line binds leaves the defaults of the actions without a line, as the remapping rows move a key: with
// bind_X = key:t, T presses X only, not also Y, whose defaults hold it.
uint8_t bound[KEY_COUNT] = {0}, list[INPUT_BINDING_MAX];
for (int a = 0; a < ACTION_COUNT; a++) {
if (!lined[a]) continue;
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX);
for (int i = 0; i < n; i++) bound[list[i]] = 1;
}
for (int a = 0; a < ACTION_COUNT; a++) {
if (lined[a]) continue;
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX), w = 0;
for (int i = 0; i < n; i++)
if (!bound[list[i]]) list[w++] = list[i];
if (w < n) keys_put(&input_map, a, list, w);
}
}
@@ -289,11 +305,13 @@ int input_pad_layout_pick(const char *setting, int east_labelled_a) {
}
int input_prompts_pick(const char *prompts, const char *pad_layout) {
if (prompts && *prompts) return !strcasecmp(prompts, "xbox") ? PROMPTS_XBOX : PROMPTS_NINTENDO;
if (prompts && *prompts)
return !strcasecmp(prompts, "xbox") ? PROMPTS_XBOX : !strcasecmp(prompts, "pc") ? PROMPTS_PC : PROMPTS_NINTENDO;
return pad_layout && !strcasecmp(pad_layout, "xbox_prompts") ? PROMPTS_XBOX : PROMPTS_NINTENDO;
}
const char *input_prompts_folder(int prompts, const char *pad_layout) {
if (prompts == PROMPTS_PC) return "romfs-patches-pc";
if (prompts != PROMPTS_XBOX) return NULL;
// The folders are read at start, before a pad is open: auto counts as Xbox, as for a pad without Nintendo labels.
// A Nintendo-labelled pad under auto plays by label too (its A is on the button labelled A), so it matches as well.
@@ -315,6 +333,12 @@ uint64_t input_map_buttons(const uint8_t down[KEY_COUNT]) {
return b;
}
int input_map_held(const uint8_t down[KEY_COUNT], int action) {
for (int i = 0; i < input_map.nkeys; i++)
if (input_map.keys[i].action == action && down[input_map.keys[i].key]) return 1;
return 0;
}
void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]) {
int first = stick ? B_SR_LEFT : B_SL_LEFT, dir[4] = {0, 0, 0, 0}; // left, up, right, down
for (int i = 0; i < input_map.nkeys; i++) {
+17 -9
View File
@@ -7,11 +7,13 @@
#include <stdint.h>
// Actions are the bit numbers of libnx HidNpadButton (input.h); 16..23 are the stick directions, which keys drive as
// stick deflection.
// stick deflection. B_SHAKE is a key of its own, never a button bit: a press shakes the right stick left and right for
// the game (input.c shake_stick), which detaches a fused part held with Ultrahand. A pad shakes its own stick.
enum {
B_A = 0, B_B, B_X, B_Y, B_STICK_L, B_STICK_R, B_L, B_R, B_ZL, B_ZR, B_PLUS, B_MINUS, B_LEFT, B_UP, B_RIGHT, B_DOWN,
B_SL_LEFT = 16, B_SL_UP, B_SL_RIGHT, B_SL_DOWN, B_SR_LEFT, B_SR_UP, B_SR_RIGHT, B_SR_DOWN,
ACTION_COUNT = 24
B_SHAKE = 24,
ACTION_COUNT = 25
};
// Keys and mouse buttons. F1, F10, F11 and Esc belong to the menu and the window, F8 to the flight recorder: no ids.
@@ -67,17 +69,18 @@ extern InputMap input_map;
void input_map_reset(void);
// The script and totk.ini names, any case when parsed; NULL for an id without one. Actions: A B X Y LCLICK RCLICK L R ZL
// ZR PLUS MINUS LEFT UP RIGHT DOWN, then LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN and the same for RSTICK (the
// stick directions the keys drive). Keys: a-z 0-9 space tab lshift ... (key_names in input_map.c). Pads: south east ...
// stick directions the keys drive), then SHAKE. Keys: a-z 0-9 space tab lshift ... (key_names in input_map.c). Pads: south east ...
const char *input_action_name(int action);
const char *input_key_name(int key);
const char *input_pad_name(int pad);
// Replaces every binding of the action `button` (an action name) with `list`: comma separated "pad:<name>" and
// "key:<name>" entries, "key:default" / "pad:default" for that part's defaults, or "none" for no input. A part the list
// names neither way is left unbound. Pad names are physical buttons, so pad entries go into the gamepad table of every
// layout; pad:default keeps each layout's own button. Stick directions take keys only. Invalid entries are logged and
// layout; pad:default keeps each layout's own button. Stick directions and SHAKE take keys only. Invalid entries are logged and
// skipped; when none is valid the action keeps its bindings. 1 when applied.
int input_map_bind(const char *button, const char *list);
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
// A key a line binds leaves the defaults of the actions that have no line.
void input_map_configure(void);
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts", 0.1.9's name for the Nintendo
@@ -87,21 +90,26 @@ int input_pad_layout_pick(const char *setting, int east_labelled_a);
// Button prompts (TOTK_PROMPTS): the controller whose buttons the game's prompts show. Nintendo: the game's own. Xbox:
// LB RB LT RT, Menu, View and a d-pad cross for the other buttons, and the face buttons named by the label of the
// button the pad layout presses (runtime/romfs.c overlay_init, tools/make_romfs_patches.py --prompts).
enum { PROMPTS_NINTENDO, PROMPTS_XBOX };
// The prompts for a TOTK_PROMPTS value ("nintendo" or "xbox", any case). Unset or empty: Xbox while the pad layout
// setting is "xbox_prompts", else Nintendo.
// button the pad layout presses (runtime/romfs.c overlay_init, tools/make_romfs_patches.py --prompts). PC: the key or
// mouse button the keyboard defaults press each button with, the mouse for the right stick.
enum { PROMPTS_NINTENDO, PROMPTS_XBOX, PROMPTS_PC };
// The prompts for a TOTK_PROMPTS value ("nintendo", "xbox" or "pc", any case). Unset or empty: Xbox while the pad
// layout setting is "xbox_prompts", else Nintendo.
int input_prompts_pick(const char *prompts, const char *pad_layout);
// The folder next to romfs-index.bin that holds those prompts for a TOTK_PAD_LAYOUT value, NULL for the game's own. The
// Nintendo layout plays an Xbox pad by position, so its folder names the face buttons the other way round
// (romfs-patches-xbox: the right button does A and is labelled B); xbox and auto put every Switch button on the button
// labelled with its letter (auto picks the layout per pad by its labels), so they share romfs-patches-xbox-label.
// labelled with its letter (auto picks the layout per pad by its labels), so they share romfs-patches-xbox-label. The PC
// prompts follow no pad: romfs-patches-pc whatever the layout (romfs.c overlay_init serves romfs-patches-pc-<w>x<h>
// under that name at a game resolution of another shape).
const char *input_prompts_folder(int prompts, const char *pad_layout);
// down[pad] != 0 for the gamepad inputs held. Returns the Switch button bits (0..15) the layout's pad table presses.
uint64_t input_map_pad_buttons(const uint8_t down[PAD_COUNT], int layout);
// down[key] != 0 for the keys held. Returns the Switch button bits (0..15) the table presses.
uint64_t input_map_buttons(const uint8_t down[KEY_COUNT]);
// 1 when a key of action is held.
int input_map_held(const uint8_t down[KEY_COUNT], int action);
// out = x, y of the stick (0 left, 1 right) from its four direction actions; up is +y; a diagonal is scaled to the
// unit circle.
void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]);
+113 -4
View File
@@ -1,11 +1,13 @@
// The SDL3 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): SDL scancodes to
// the key ids of input_map.h, the mouse camera's relative motion and the cursor capture (SDL's relative mouse mode).
// Key and button state comes from the window's events (window thread) and is read by any thread. Keys and their map:
// docs/controls.md.
// the key ids of input_map.h, the mouse camera's relative motion, the cursor capture (SDL's relative mouse mode), the
// pointer's place in the window (the game's menus, menu_cursor.c) and the mouse travel (the last-device switch,
// input_kbm_last). Key and button state comes from the window's events (window thread) and is read by any thread. Keys
// and their map: docs/controls.md.
//
// The capture follows the Win32 back end's rules (input_win.c): captured while the window has the focus, the mouse
// camera is on and no text entry or menu is open; Esc releases it until a click in the window (and closing the menu
// opened by that Esc gives it back); losing the focus releases it until the window has the focus again.
#include <math.h>
#include <string.h>
#include <SDL3/SDL.h>
@@ -21,8 +23,18 @@ static SDL_Window *win;
static volatile uint8_t key_held[KEY_COUNT];
static volatile int32_t esc_held, f8_held, focused;
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
static volatile int32_t mouse_wheel; // wheel turned while captured, in 1/120 notches (ikb_wheel_take)
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
static volatile int32_t mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
static float travel_rem; // window thread: the fraction of a count beyond mouse_travel
static volatile int32_t captured; // relative mouse mode is on
// The pointer's place in the window, as fractions of its size packed 32.32 fixed point (x in the high word), written
// by the window thread from the mouse events and read by the game thread (menu_cursor.c through ikb_pointer).
// ptr_have is 0 whenever the pointer is not over the window, as the Win32 back end's client-rect test.
static volatile uint64_t ptr_pos;
static volatile int32_t ptr_have;
static volatile int32_t warp_pending; // ikb_pointer_set asked the window thread to move the pointer
static float warp_nx, warp_ny; // its target, read by the window thread once warp_pending is seen
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
static volatile int32_t cur_rel, cur_suspend, cur_pending;
static uint32_t wake_event; // SDL_RegisterEvents: wakes the window thread for a cursor change
@@ -133,6 +145,39 @@ static void cursor_apply(void) {
plat_xchg32(&captured, want);
}
// The pointer's place in the window (the window thread): x and y are SDL window units, the same space the mouse
// events report, so dividing by the window's size makes fractions of it whatever the pixel density is. At and past
// the edges the fraction is clamped to 0..1, where a letterboxed picture covers no item anyway.
static void pointer_store(float x, float y) {
if (!win) return;
int w = 0, h = 0;
SDL_GetWindowSize(win, &w, &h);
if (w < 1 || h < 1) return;
double fx = x / w, fy = y / h;
if (!(fx >= 0)) fx = 0; // also NaN
else if (fx > 1) fx = 1;
if (!(fy >= 0)) fy = 0;
else if (fy > 1) fy = 1;
__atomic_store_n(&ptr_pos, (uint64_t)(uint32_t)(fx * 4294967295.0) << 32 | (uint32_t)(fy * 4294967295.0),
__ATOMIC_RELEASE);
__atomic_store_n(&ptr_have, 1, __ATOMIC_RELEASE);
}
// A warp asked for by ikb_pointer_set, done here as every window call: on the window thread, and after the cursor
// change of the same menu opening has been applied. SDL's warp makes a motion event where it works (X11); where it
// does not (some Wayland compositors) the stored place stands, so the item under the pointer is the place asked for
// even if the visible cursor stayed behind.
static void pointer_apply(void) {
if (!plat_xchg32(&warp_pending, 0)) return;
if (!win || !focused) return;
int w = 0, h = 0;
SDL_GetWindowSize(win, &w, &h);
if (w < 1 || h < 1) return;
float x = warp_nx * w, y = warp_ny * h;
SDL_WarpMouseInWindow(win, x, y);
pointer_store(x, y);
}
// Any thread: the window thread applies the change at its next event.
void ikb_cursor_update(void) {
plat_xchg32(&cur_pending, 1);
@@ -158,11 +203,13 @@ void input_sdl_window(SDL_Window *w) {
win = w;
wake_event = SDL_RegisterEvents(1);
plat_xchg32(&focused, (SDL_GetWindowFlags(w) & SDL_WINDOW_INPUT_FOCUS) != 0);
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer's place is unknown until it enters or moves
cursor_apply();
}
void input_sdl_idle(void) {
if (cur_pending) cursor_apply();
pointer_apply();
}
void input_sdl_event(const SDL_Event *e) {
@@ -186,9 +233,19 @@ void input_sdl_event(const SDL_Event *e) {
int k = mouse_key(e->button.button);
if (k) key_held[k] = (uint8_t)down;
if (down && (cur_rel & CUR_REL)) plat_xchg32(&cur_rel, 0); // a click in the window captures again
if (!captured) pointer_store(e->button.x, e->button.y); // a click can come without a motion first
break;
}
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_MOTION: {
// The travel counts any motion, captured or not, as the Win32 back end's raw input does: it is what makes a
// moved mouse the device used last, so the menu pointer appears without a click or a key.
travel_rem += fabsf(e->motion.xrel) + fabsf(e->motion.yrel);
int32_t t = (int32_t)travel_rem;
if (t) {
travel_rem -= t;
plat_add32(&mouse_travel, t);
}
if (!captured) pointer_store(e->motion.x, e->motion.y); // in relative mode x and y are not the pointer's
if (captured) {
rem_x += e->motion.xrel;
rem_y += e->motion.yrel;
@@ -198,6 +255,22 @@ void input_sdl_event(const SDL_Event *e) {
if (dy) plat_add32(&mouse_dy, dy);
}
break;
}
case SDL_EVENT_WINDOW_MOUSE_ENTER: { // entering without moving yet: the place is where the enter found the pointer
float x, y;
SDL_GetMouseState(&x, &y); // the window with mouse focus is this one during its own enter event
pointer_store(x, y);
break;
}
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE);
break;
case SDL_EVENT_MOUSE_WHEEL: // notches, + away from the player (a flipped wheel reports the other sign)
if (captured) {
float y = e->wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -e->wheel.y : e->wheel.y;
plat_add32(&mouse_wheel, (int32_t)(y * 120));
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
plat_xchg32(&focused, 1);
break;
@@ -207,9 +280,11 @@ void input_sdl_event(const SDL_Event *e) {
memset((void *)key_held, 0, sizeof key_held); // the key-ups go elsewhere
plat_xchg32(&esc_held, 0);
plat_xchg32(&f8_held, 0);
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer left with the focus, leave or not
break;
}
cursor_apply();
pointer_apply(); // after cursor_apply: a menu opening uncaptures first, then the pointer moves to its middle
}
// ---- input_wsys.h
@@ -218,7 +293,41 @@ int ikb_down(int key) { return key > 0 && key < KEY_COUNT && key_held[key]; }
int ikb_escape(void) { return esc_held != 0; }
int ikb_captured(void) { return captured != 0; }
// The pointer's place in the window as fractions of its size (menu_cursor.c maps them onto the picture the present
// drew). 0 whenever the pointer is not over this window or the window has not the focus, as the Win32 back end's
// GetCursorPos test.
int ikb_pointer(float *nx, float *ny) {
if (!focused || !__atomic_load_n(&ptr_have, __ATOMIC_ACQUIRE)) return 0;
uint64_t p = __atomic_load_n(&ptr_pos, __ATOMIC_ACQUIRE);
*nx = (float)(uint32_t)(p >> 32) / 4294967295.0f;
*ny = (float)(uint32_t)p / 4294967295.0f;
return 1;
}
// Moves the pointer there (menu_cursor.c: to the middle when a menu opens). The window thread warps at its next
// event, as SDL asks for its video thread; until then the warp target stands as the pointer's place.
void ikb_pointer_set(float nx, float ny) {
if (!(nx >= 0 && nx <= 1) || !(ny >= 0 && ny <= 1)) return;
warp_nx = nx, warp_ny = ny;
__atomic_store_n(&warp_pending, 1, __ATOMIC_RELEASE);
if (wake_event) {
SDL_Event e = {.user = {.type = wake_event}};
SDL_PushEvent(&e);
}
}
uint32_t ikb_mouse_travel(void) { return (uint32_t)__atomic_load_n(&mouse_travel, __ATOMIC_RELAXED); }
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*dx = plat_xchg32(&mouse_dx, 0);
*dy = plat_xchg32(&mouse_dy, 0);
}
int32_t ikb_wheel_take(void) {
int32_t o, n;
do {
o = mouse_wheel;
n = o % 120; // the part of a notch stays for the next call
} while (!__atomic_compare_exchange_n(&mouse_wheel, &o, n, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST));
return o / 120;
}
+42 -3
View File
@@ -83,6 +83,8 @@ uint32_t input_key_char(int key) {
// ---- mouse camera: raw motion accumulated by the window thread, one stick sample per presented frame
static volatile LONG mouse_dx, mouse_dy; // raw counts since the last frame sample
static volatile LONG mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
static volatile LONG mouse_wheel; // wheel turned while captured, in WHEEL_DELTA units (ikb_wheel_take)
static volatile LONG cur_captured; // cursor hidden and confined to the client area
static volatile LONG cur_active; // WM_ACTIVATEAPP: the window's application is active
// Bits: CUR_REL, Esc or activation by a click on the title bar or border: until a client click; CUR_ESC, set with CUR_REL
@@ -137,11 +139,18 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
case WM_INPUT: {
RAWINPUT ri;
UINT sz = sizeof ri;
if (cur_captured && GetRawInputData((HRAWINPUT)l, RID_INPUT, &ri, &sz, sizeof(RAWINPUTHEADER)) != (UINT)-1 &&
if (GetRawInputData((HRAWINPUT)l, RID_INPUT, &ri, &sz, sizeof(RAWINPUTHEADER)) != (UINT)-1 &&
ri.header.dwType == RIM_TYPEMOUSE && !(ri.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
InterlockedAdd(&mouse_dx, ri.data.mouse.lLastX);
InterlockedAdd(&mouse_dy, ri.data.mouse.lLastY);
LONG x = ri.data.mouse.lLastX, y = ri.data.mouse.lLastY;
InterlockedAdd(&mouse_travel, (x < 0 ? -x : x) + (y < 0 ? -y : y));
if (cur_captured) {
InterlockedAdd(&mouse_dx, x);
InterlockedAdd(&mouse_dy, y);
}
}
// The wheel comes in the same message (usButtonData, signed, + away from the player)
if (cur_captured && ri.header.dwType == RIM_TYPEMOUSE && (ri.data.mouse.usButtonFlags & RI_MOUSE_WHEEL))
InterlockedAdd(&mouse_wheel, (SHORT)ri.data.mouse.usButtonData);
break; // DefWindowProc must see WM_INPUT for cleanup
}
case WM_SETCURSOR:
@@ -203,11 +212,41 @@ int ikb_escape(void) { return key_down(VK_ESCAPE); }
int ikb_captured(void) { return cur_captured != 0; }
int ikb_pointer(float *nx, float *ny) {
HWND h = gfx_window_handle();
RECT r;
POINT p;
if (!h || GetForegroundWindow() != h || !GetClientRect(h, &r) || r.right < 1 || r.bottom < 1 || !GetCursorPos(&p) ||
!ScreenToClient(h, &p))
return 0;
*nx = (float)p.x / (float)r.right, *ny = (float)p.y / (float)r.bottom;
return p.x >= 0 && p.y >= 0 && p.x < r.right && p.y < r.bottom;
}
void ikb_pointer_set(float nx, float ny) {
HWND h = gfx_window_handle();
RECT r;
if (!h || GetForegroundWindow() != h || !GetClientRect(h, &r)) return;
POINT p = {(LONG)(nx * r.right), (LONG)(ny * r.bottom)};
if (ClientToScreen(h, &p)) SetCursorPos(p.x, p.y);
}
uint32_t ikb_mouse_travel(void) { return (uint32_t)mouse_travel; }
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*dx = InterlockedExchange(&mouse_dx, 0);
*dy = InterlockedExchange(&mouse_dy, 0);
}
int32_t ikb_wheel_take(void) {
LONG o, n;
do {
o = mouse_wheel;
n = o % WHEEL_DELTA; // the part of a notch stays for the next call
} while (InterlockedCompareExchange(&mouse_wheel, n, o) != o);
return (int32_t)(o / WHEEL_DELTA);
}
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }
+6
View File
@@ -7,8 +7,14 @@
int ikb_foreground(void); // the game window is in front: keys count
int ikb_down(int key); // key id (input_map.h KEY_*) held, mouse buttons included
int ikb_escape(void); // Esc held (no key id: it belongs to the menu)
// The pointer in the client area, 0..1 from its top left; 0 when the window is not in front or the pointer is outside
int ikb_pointer(float *nx, float *ny);
void ikb_pointer_set(float nx, float ny); // moves the pointer there (the window in front only)
int ikb_captured(void); // the cursor is captured for the mouse camera
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
uint32_t ikb_mouse_travel(void); // raw counts the mouse moved in total (|dx| + |dy|), captured or not
int32_t ikb_wheel_take(void); // whole wheel notches turned while captured since the last call (+ away
// from the player); a high-resolution wheel's part of a notch is kept
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
+44
View File
@@ -246,6 +246,44 @@ static void wake_sync_waiters(void) {
}
}
// ---- a short poll before a sleep
// Most guest waits and waits for a virtual core end within microseconds. The thread polls for TOTK_KSPIN_US (default
// 20) with G released before it sleeps on its condition variable: a wake in that time costs no OS wake-up (4-12 us
// on a desktop Ryzen, more from a deep C-state) and the waker's signal finds nobody asleep. Off with fewer than 6 logical
// processors, where the poll would take a CPU that a running thread needs; TOTK_KSPIN_US=0 turns it off. Savage Wind
// Recomp polls the same way before it parks (runtime/ksync.c cond_wait). TOTK_KSPIN_US=ab polls 20 us in the
// B blocks of TOTK_AB only.
static int64_t g_kspin_ns = -1;
static int g_kspin_ab;
static uint64_t kspin_ns(void) {
int64_t v = __atomic_load_n(&g_kspin_ns, __ATOMIC_RELAXED);
if (v < 0) {
const char *e = cfg_get("TOTK_KSPIN_US");
g_kspin_ab = e && !strcmp(e, "ab");
long long us = e && *e && !g_kspin_ab ? atoll(e) : 20;
uint32_t cpus = plat_cpu_usable();
v = us <= 0 || cpus < 6 ? 0 : us * 1000;
__atomic_store_n(&g_kspin_ns, v, __ATOMIC_RELAXED);
rt_log("kernel: waits poll %lld us before they sleep%s (TOTK_KSPIN_US, %u logical processors)",
(long long)(v / 1000), g_kspin_ab ? " in the B blocks" : "", cpus);
}
return v && (!g_kspin_ab || rt_ab()) ? (uint64_t)v : 0;
}
// Holding G: releases it and polls *p until it reaches at_least or ns pass, then takes G again. 1 if it was reached.
static int kspin(const int *p, int at_least, uint64_t ns) {
uint64_t end = kern_now_ns() + ns;
int ok = 0;
unlock();
for (unsigned i = 1; !(ok = __atomic_load_n(p, __ATOMIC_ACQUIRE) >= at_least); i++) {
plat_pause();
if (!(i & 31) && kern_now_ns() >= end) break;
}
lock();
return ok;
}
// SleepConditionVariableSRW counts milliseconds: round up (late is legal, early is not) and re-check the deadline
// against the performance counter, because the wait may also end early.
static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
@@ -262,6 +300,8 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
uint64_t now = kern_now_ns();
int forever = ns < 0 || (uint64_t)ns > UINT64_MAX - now;
uint64_t deadline = forever ? 0 : now + (uint64_t)ns;
uint64_t poll = kspin_ns();
if (poll) kspin(&t->woke, 1, forever || poll < (uint64_t)ns ? poll : (uint64_t)ns);
while (!t->woke) {
if (forever) {
plat_cond_wait(&t->cv, &G);
@@ -991,6 +1031,9 @@ static void core_take(RtThread *t) {
while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) plat_pause();
lock();
if (t->cur_core >= 0) g_ap_spin_ok++;
} else if (t->cur_core < 0) { // a holder may block or yield any moment (kspin_ns)
uint64_t poll = kspin_ns();
if (poll) kspin(&t->cur_core, 0, poll);
}
int sampled = 0;
while (t->cur_core < 0) {
@@ -1518,6 +1561,7 @@ uint32_t kern_main_thread(Cpu **cpu) {
t->cpu->sp = t->stack_top;
t->tid = plat_thread_id();
perf_main_thread();
perf_core_thread(PCORE_MAIN);
PlatIntr *self = ap_open_self();
lock();
t->hthr = self;
+112
View File
@@ -10,6 +10,7 @@
#include "cfg.h"
#include "host.h"
#include "kernel.h"
#include "layout.h"
#include "platform/platform.h"
#define STACK 0x10000
@@ -682,6 +683,25 @@ static int cmp_u64(const void *a, const void *b) {
return x < y ? -1 : x > y;
}
// A refused commit waits and retries (memory.c commit, oom.c rt_oom_wait): three forced refusals, then the page maps.
static void test_oom_commit(void) {
const RtGuestLayout *g = mem_layout();
uint64_t a = g->alias.base + g->alias.size - 0x10000;
rt_oom_force(RT_OOM_COMMIT, 3);
uint64_t t0 = plat_now_ms();
uint32_t r = mem_map_physical(a, 0x1000);
uint64_t ms = plat_now_ms() - t0;
CHECK(r == 0 && rt_oom_forced_left(RT_OOM_COMMIT) == 0 && ms >= 20,
"MapPhysicalMemory after 3 refused commits: result %#x, %d refusals left, %llu ms", r,
rt_oom_forced_left(RT_OOM_COMMIT), (unsigned long long)ms);
rt_oom_force(RT_OOM_COMMIT, 0);
if (r == 0) {
*(volatile uint64_t *)(uintptr_t)a = 1;
CHECK(mem_unmap_physical(a, 0x1000) == 0, "UnmapPhysicalMemory after the refused commits");
}
printf("oom: commit waited %llu ms through 3 refusals\n", (unsigned long long)ms);
}
static void test_platform(void) {
PlatThread *t = plat_thread_new(plat_worker, (void *)(intptr_t)1, "probe-kernel plat");
CHECK(t, "plat_thread_new");
@@ -719,6 +739,90 @@ static void test_platform(void) {
CHECK(over[N / 2] < 500000, "plat_sleep_ns(1 ms) median overshoot %.3f ms (want < 0.5)", over[N / 2] / 1e6);
}
// ---- crash report (main.c crash_handler): a child faults on a guest thread with known values in rbx and x19; the
// report must hold the host registers, the faulting instruction's bytes and the memory both values point at.
#define CRASH_CHILD_ENV "TOTK_PROBE_KERNEL_CRASH_CHILD"
static uint64_t crash_host_words[6] = {0, 0, 0x5ca1ab1e0c0de001ull, 0, 0, 0};
static uint64_t crash_guest_words[6] = {0, 0, 0x5ca1ab1e0c0de002ull, 0, 0, 0};
// The compound shape note (main.c crash_compound_shape, 1.2.1): a shape of 3 records whose child 0 has no shape, child
// 1 has one but its live bit is clear, child 2 is live and filled.
static uint64_t crash_shape[0x1a0 / 8], crash_records[3 * 0x78 / 8], crash_child_shape;
static void crash_shape_fill(Cpu *c) {
uint8_t *s = (uint8_t *)crash_shape, *r = (uint8_t *)crash_records;
static const char *const names[3] = {"PartA", "PartB", "PartC"};
for (int i = 0; i < 3; i++) {
uint64_t name = (uint64_t)(uintptr_t)(r + i * 0x78 + 0x14);
memcpy(r + i * 0x78 + 8, &name, 8);
memcpy(r + i * 0x78 + 0x14, names[i], strlen(names[i]) + 1);
}
uint64_t child = (uint64_t)(uintptr_t)&crash_child_shape, vt = MOD_MAIN_BASE + 0x45e1218;
uint64_t rec = (uint64_t)(uintptr_t)crash_records;
uint32_t n = 3, bits = 0x5;
memcpy(r + 0x78, &child, 8);
memcpy(r + 2 * 0x78, &child, 8);
memcpy(s, &vt, 8);
memcpy(s + 0x78, &n, 4);
memcpy(s + 0x80, &rec, 8);
memcpy(s + 0x88, &bits, 4);
memcpy(s + 0x190, &n, 4);
c->x[20] = (uint64_t)(uintptr_t)crash_shape;
c->x[21] = 0x1000038;
c->x[30] = MOD_MAIN_BASE + 0xe056d4;
}
static void crash_child_main(Cpu *c) {
const char *mode = getenv(CRASH_CHILD_ENV);
if (mode && !strcmp(mode, "shape")) crash_shape_fill(c);
c->x[19] = (uint64_t)(uintptr_t)&crash_guest_words[2];
c->sp -= 0x100; // a frame on the guest stack, as a crashing function has
__asm__ volatile("movq %0, %%rbx\n\tmovq 0x10, %%rax" : : "r"(&crash_host_words[2]) : "rax", "rbx", "memory");
}
// The report line that holds needle, NULL when none does
static const char *report_line(const char *out, const char *needle, char *line, size_t cap) {
const char *p = strstr(out, needle);
if (!p) return NULL;
while (p > out && p[-1] != '\n') p--;
size_t n = strcspn(p, "\r\n");
snprintf(line, cap, "%.*s", (int)(n < cap ? n : cap - 1), p);
return line;
}
static void test_crash_report(void) {
static char out[65536];
char line[512], want[64];
int code = plat_run_self(CRASH_CHILD_ENV, "1", out, sizeof out, 30000);
CHECK(code == 71 && strstr(out, "FATAL: access violation") && strstr(out, "reading address 0x10"),
"crash child: exit code %d, no access violation at 0x10 reported", code);
snprintf(want, sizeof want, "rbx %016llx", (unsigned long long)(uintptr_t)&crash_host_words[2]);
CHECK(strstr(out, want), "crash report: no host register line with %s", want);
CHECK(report_line(out, "host code at rip-", line, sizeof line) && strstr(line, "| 48 8b 04 25 10 00 00 00"),
"crash report: the faulting instruction's bytes are missing");
const char *h = report_line(out, "*5ca1ab1e0c0de001", line, sizeof line);
CHECK(h && strstr(h, "rbx"), "crash report: no memory line for rbx (%s)", h ? h : "none");
const char *g = report_line(out, "*5ca1ab1e0c0de002", line, sizeof line);
CHECK(g && strstr(g, "x19"), "crash report: no memory line for x19 (%s)", g ? g : "none");
CHECK(strstr(out, "guest stack sp+0x00:"), "crash report: no guest stack words");
CHECK(report_line(out, " memory commit: this process ", line, sizeof line) && !strstr(line, "process -1 MB"),
"crash report: no memory commit line");
printf("crash report: exit code %d, host registers, code bytes, memory at rbx and x19, guest stack, commit %s\n", code,
failures ? "checked with failures" : "present");
if (rt_game_version != 121) return;
code = plat_run_self(CRASH_CHILD_ENV, "shape", out, sizeof out, 30000);
CHECK(report_line(out, " compound shape ", line, sizeof line) && strstr(line, "3 children, 3 records") &&
strstr(line, "live bits: 00000005"),
"crash report: no compound shape line (%s)", strstr(out, "compound shape") ? line : "none");
CHECK(report_line(out, "child 0:", line, sizeof line) && strstr(line, "shape 0 \"PartA\""),
"crash report: child 0 should have no shape (%s)", line);
CHECK(report_line(out, "child 1:", line, sizeof line) && strstr(line, "(off) \"PartB\""),
"crash report: child 1 should be off (%s)", line);
CHECK(report_line(out, "child 2:", line, sizeof line) && !strstr(line, "(off)") && strstr(line, "\"PartC\""),
"crash report: child 2 should be live (%s)", line);
printf("crash report, compound shape: exit code %d, %s\n", code, failures ? "checked with failures" : "children listed");
}
int probe_kernel(void) {
a_lock_waiter = hostfn_register("probe-kernel lock waiter", lock_waiter);
a_cv_waiter = hostfn_register("probe-kernel condvar waiter", cv_waiter);
@@ -742,7 +846,15 @@ int probe_kernel(void) {
printf("probe-kernel: FAILED (no PLT stub for %s in main)\n", TICK_SYMBOL);
return 1;
}
const char *child = getenv(CRASH_CHILD_ENV);
if (child && *child) {
kern_run_on_guest_main(crash_child_main);
printf("crash child: returned without a fault\n");
return 1;
}
test_platform();
test_oom_commit();
test_crash_report();
kern_run_on_guest_main(probe_main);
RtStats s = rt_stats();
printf("stats: %llu SVCs, %llu threads, %llu host calls\n", (unsigned long long)s.svcs,
+196 -16
View File
@@ -2,9 +2,12 @@
//
// totk.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
// --selftest layout, dispatch table and host-function registry; needs no game files
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
// --probe-mods the mods folder scan, order and switches (mods.c), the size table entries of the members of
// mods' packs (restbl_probe.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-playtime the play time file, its ticks and saves (playtime.c); needs 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
@@ -18,7 +21,8 @@
//
// Also here: the runtime services the generated code calls besides rt_dispatch and rt_svc (rt_brk, rt_udf,
// rt_ticks, the function profiler hook), logging, SHA-256, and the crash reporter (a vectored exception handler that
// names the lifted function, the guest registers and the guest frame-pointer chain).
// names the lifted function, the host and guest registers, the machine code around the fault, the guest frame-pointer
// chain and the memory the registers point at).
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
@@ -26,6 +30,7 @@
#include "cfg.h"
#include "host.h"
#include "layout.h"
#include "platform/platform.h"
#include "sha256.h"
@@ -153,8 +158,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];
@@ -185,8 +190,8 @@ static int by_host(const void *a, const void *b) {
}
// The lifted function that contains a host code address, by the nearest function start below it; 0 outside the lifted
// code.
static uint64_t lifted_function(uintptr_t rip) {
// code. *host, when given, gets the function's host start.
static uint64_t lifted_function_at(uintptr_t rip, uintptr_t *host) {
if (!fn_rev) {
static RtModule mods[MAX_MODULES];
int n = rt_linked_modules(mods, MAX_MODULES);
@@ -208,35 +213,167 @@ static uint64_t lifted_function(uintptr_t rip) {
if (fn_rev[mid].host <= rip) lo = mid + 1;
else hi = mid;
}
if (lo && host) *host = fn_rev[lo - 1].host;
return lo ? fn_rev[lo - 1].guest : 0;
}
static uint64_t lifted_function(uintptr_t rip) { return lifted_function_at(rip, NULL); }
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
// The machine code around the faulting instruction: 32 bytes before rip, then 32 from rip. The user's totk.exe is built
// on their machine, so its offsets need not match ours; these bytes and the offset into the lifted function find the
// same instruction in a local build of that version, and with it the guest instruction and the operands' registers.
static void crash_code_bytes(uintptr_t rip) {
uint8_t b[64];
int before = 32;
if (!plat_mem_read(rip - 32, b, sizeof b)) {
before = 0; // rip near the start of a mapping: the bytes from rip only
if (!plat_mem_read(rip, b + 32, 32)) return;
}
rt_logf(" host code at rip-%d:", before);
for (int i = 32 - before; i < 64; i++) rt_logf("%s%02x", i == 32 ? " | " : " ", b[i]);
rt_log_raw("\n", 1);
}
// 64 bytes around each register value that points at readable memory, as 8-byte words from 16 bytes below the value,
// the word holding the value marked with *. Host and guest registers that land on the same window share a line. A
// guest pointer is a host address here (the guest address space is mapped one to one), so both kinds are read alike.
// The guest registers come from the Cpu, which the lifted code writes back only at calls and returns, so the host
// registers carry the faulting function's live values.
typedef struct { uint64_t start; char names[56]; uint64_t w[8]; } CrashWin;
static void crash_win_add(CrashWin *wins, int *n, int cap, const char *name, uint64_t v) {
if (v < 0x10000 || v > 0x00007fffffffffffull) return; // small integers and non-canonical values
uint64_t start = (v & ~7ull) - 16;
for (int i = 0; i < *n; i++)
if (wins[i].start == start) {
size_t len = strlen(wins[i].names);
if (len + strlen(name) + 2 < sizeof wins[i].names)
snprintf(wins[i].names + len, sizeof wins[i].names - len, " %s", name);
return;
}
if (*n == cap || !plat_mem_read(start, wins[*n].w, sizeof wins[*n].w)) return;
wins[*n].start = start;
snprintf(wins[*n].names, sizeof wins[*n].names, "%s", name);
(*n)++;
}
static void crash_memory(const PlatCrash *x, const Cpu *c) {
static CrashWin wins[64]; // the crash handler runs once (busy flag), so static storage needs no allocation
static const char *const gpr_names[16] = PLAT_GPR_NAMES;
int n = 0;
char name[8];
if (x->has_regs)
for (int i = 0; i < 16; i++) crash_win_add(wins, &n, 64, gpr_names[i], x->gpr[i]);
if (c) {
for (int i = 0; i < 31; i++) snprintf(name, sizeof name, "x%d", i), crash_win_add(wins, &n, 64, name, c->x[i]);
crash_win_add(wins, &n, 64, "sp", c->sp);
}
if (!n) return;
rt_logf(" memory at the register values (8-byte words from value-16, * the value's word):\n");
for (int i = 0; i < n; i++) {
rt_logf(" %016llx %-12s", (unsigned long long)wins[i].start, wins[i].names);
for (int k = 0; k < 8; k++) rt_logf(" %s%016llx", k == 2 ? "*" : "", (unsigned long long)wins[i].w[k]);
rt_log_raw("\n", 1);
}
// The guest stack above sp: the crashing function's spills and locals, and the callers' saved registers
uint64_t s[16];
if (c && plat_mem_read(c->sp, s, sizeof s))
for (int i = 0; i < 16; i += 4)
rt_logf(" guest stack sp+0x%02x: %016llx %016llx %016llx %016llx\n", i * 8, (unsigned long long)s[i],
(unsigned long long)s[i + 1], (unsigned long long)s[i + 2], (unsigned long long)s[i + 3]);
}
// 1.2.1: a fault right after a compound physics shape was asked for its first child's material (the call returning to
// main+0xe056d4 reads the result without a check). The shape (vtable main+0x45e1218, built by main+0x7359fc) is in x20
// there. It returns null when no child slot is both live (bitmap at +0x88) and filled (shape pointer at +0 of the
// 0x78-byte records at +0x80). Every child is filled when the shape is built, so the list tells children removed later
// (null shape) from children switched off (clear bit), and the names name the object. Runs only in the crash handler.
static void crash_compound_shape(const Cpu *c) {
if (!c || rt_game_version != 121 || c->x[30] != MOD_MAIN_BASE + 0xe056d4) return;
uint64_t shape = c->x[20];
if (!readable(shape, 0x1a0) || LD64(shape) != MOD_MAIN_BASE + 0x45e1218) {
rt_logf(" compound shape: x20 %#llx is not one\n", (unsigned long long)shape);
return;
}
uint32_t children = LD32(shape + 0x190), records = LD32(shape + 0x78);
uint64_t rec = LD64(shape + 0x80);
rt_logf(" compound shape %#llx (holder %#llx): %u children, %u records at %#llx, live bits:",
(unsigned long long)shape, (unsigned long long)(c->x[21] - 0x38), children, records,
(unsigned long long)rec);
uint32_t n = children > 0x400 ? 0x400 : children; // the bitmap holds 0x400 bits
for (uint32_t w = 0; w < (n + 31) / 32; w++) rt_logf(" %08x", LD32(shape + 0x88 + 4 * w));
rt_log_raw("\n", 1);
if (records > 0x400 || !readable(rec, (uint64_t)records * 0x78)) return;
for (uint32_t i = 0; i < records; i++) {
uint64_t r = rec + (uint64_t)i * 0x78, child = LD64(r), s = LD64(r + 8);
char name[33];
size_t k = 0;
for (; k < 32 && readable(s + k, 1); k++) {
name[k] = *(const char *)(uintptr_t)(s + k);
if (!name[k]) break;
}
name[k] = 0;
int live = LD32(shape + 0x88 + 4 * (i >> 5)) >> (i & 31) & 1;
rt_logf(" child %3u: shape %#llx%s \"%s\"\n", i, (unsigned long long)child, live ? "" : " (off)", name);
}
}
static void crash_handler(const PlatCrash *x) {
static volatile int32_t busy;
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);
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
if (x->cxx_type)
rt_logf(" C++ type %s%s%s\n", x->cxx_type, x->cxx_what ? ", what(): " : "", x->cxx_what ? x->cxx_what : "");
// A driver that runs out of memory faults or throws (std::bad_alloc) instead of returning an error: the commit
// tells that apart from a driver bug, as rt_fatal's line does for our own failed allocations.
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
rt_logf(" memory commit: this process %.0f MB, the system %.0f of %.0f MB\n", proc, used, limit);
uintptr_t fstart = 0;
uint64_t f = x->module ? 0 : lifted_function_at(rip, &fstart);
if (f)
rt_logf(" in lifted function f_%llx = %s, host +%#llx into it\n", (unsigned long long)f,
ldr_describe(f, d, sizeof d), (unsigned long long)(rip - fstart));
if (x->has_regs) {
static const char *const gpr_names[16] = PLAT_GPR_NAMES;
for (int i = 0; i < 16; i++)
rt_logf(" %-3s %016llx%s", gpr_names[i], (unsigned long long)x->gpr[i], i % 4 == 3 ? "\n" : "");
crash_code_bytes(rip);
}
// abort and invalid parameter: where the host code called it; a fault in another module: the driver code and the
// host calls that led into it
uintptr_t exe = plat_exe_base();
for (int i = 0; i < x->nframes; i++) {
const char *m = x->frame_module[i];
if (m) {
rt_logf(" host frame %2d: %s+%#llx\n", i, *m ? m : "outside every module",
(unsigned long long)x->frame_offset[i]);
continue;
}
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);
}
if (x->frames_left_out) rt_logf(" (%d frames in other modules left out)\n", x->frames_left_out);
Cpu *c = kern_cpu();
if (c) {
for (int i = 0; i < 31; i++)
rt_logf(" x%-2d %016llx%s", i, (unsigned long long)c->x[i], i % 4 == 3 ? "\n" : "");
rt_logf(" sp %016llx\n pc %016llx lr %s\n", (unsigned long long)c->sp, (unsigned long long)c->pc,
ldr_describe(c->x[30], d, sizeof d));
// pc: the guest code running, or the host function a guest call entered (its name tells the host call apart)
rt_logf(" sp %016llx\n pc %016llx", (unsigned long long)c->sp, (unsigned long long)c->pc);
if (hostfn_name(c->pc)) rt_logf(" (%s)", ldr_describe(c->pc, d, sizeof d));
rt_logf(" lr %s\n", ldr_describe(c->x[30], d, sizeof d));
uint64_t fp = c->x[29];
for (int n = 0; n < 32 && fp && !(fp & 7) && readable(fp, 16); n++) {
uint64_t next = LD64(fp), lr = LD64(fp + 8);
@@ -247,6 +384,8 @@ static void crash_handler(const PlatCrash *x) {
} else {
rt_logf(" not on a guest thread\n");
}
crash_memory(x, c);
crash_compound_shape(c);
kern_free_cores_note();
fflush(stderr);
plat_exit(71);
@@ -389,7 +528,9 @@ static void check_memory(void) {
CHECK(q.state == 0 && q.base == g->alias.base && q.size == g->alias.size, "QueryMemory on the free alias region");
CHECK(mem_map_physical(a, 0x1000) == 0 && LD64(a + 0x10) == 0 && mem_unmap_physical(a, 0x1000) == 0,
"remapped physical memory is not zeroed");
CHECK(mem_map_physical(a, 0x100000000ull) == 0x10801, "MapPhysicalMemory beyond the pool is not LimitReached");
// past the pool whatever its size (TOTK_APP_POOL, TOTK_TEX_HEAP_EXTRA_MB): 4 GiB fits a grown one
CHECK(mem_map_physical(a, (g->pool + 0x100000) & ~0xfffffull) == 0x10801,
"MapPhysicalMemory beyond the pool is not LimitReached");
mem_query(RT_HOSTFN_BASE + 0x40, &q);
CHECK(q.state == 0x10 && q.base == RT_HOSTFN_BASE, "QueryMemory on the host-function window");
mem_query(g->alias.base + g->alias.size + 0x1000, &q);
@@ -439,11 +580,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-cfg | --probe-dpi | --probe-mods | --probe-playtime | --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;
}
@@ -481,6 +638,9 @@ static void portable_paths(RtConfig *c) {
int main(int argc, char **argv) {
mem_init(); // first: claim the guest address space before anything else can land there; reads totk.ini
// The build that wrote the log, at its top: an attached log from another install or version shows at a glance.
rt_log("version: TotK Recomp %s, game %d.%d.%d", rt_port_version, rt_game_version / 100, rt_game_version / 10 % 10,
rt_game_version % 10);
// A restart from the settings menu (gfx_present.c gfx_window_closed) starts this process while the one it replaces
// is still exiting: its log, RomFS and saves are free once it is gone.
if (plat_restart_wait(30000) < 0) rt_log("restart: the previous process still runs after 30 s; starting anyway");
@@ -492,6 +652,20 @@ int main(int argc, char **argv) {
// Timed guest waits (kernel.c block_inner) sleep in whole milliseconds; at the default 15.6 ms tick a 1 ms wait
// costs a full tick. TOTK_TIMER_1MS=0 keeps the default for comparison.
if (cfg_bool("TOTK_TIMER_1MS")) plat_timer_1ms();
int power = plat_power_full();
if (power >= 0)
rt_log("power: %s", power ? "OS power throttling off (EcoQoS, timer resolution in the background)"
: "OS power throttling: the opt-out was refused");
// Hybrid CPUs: the threads every frame goes through prefer the P-cores (perf_core_thread). TOTK_PCORE=0: off.
if (rt_knob("PCORE", "1")) {
char why[160];
int n = plat_perf_core_setup(why, sizeof why);
if (n) rt_log("power: %s; the game's main and render threads and the vk replay, recorder and present threads "
"prefer the %d P-core ones (TOTK_PCORE=0 turns it off)", why, n);
else rt_log("power: %s: no P-core preference", why);
} else {
rt_log("power: TOTK_PCORE=0: no P-core preference");
}
cfg = (RtConfig){.exefs = rt_default_exefs, .romfs = rt_default_romfs, .profile = rt_default_profile,
.gpu = cfg_get("TOTK_GPU")};
portable_paths(&cfg); // command-line paths below still win
@@ -526,16 +700,22 @@ 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
playtime_init(); // the play time thread (playtime.c): the Game tab's play time across runs
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-playtime")) return probe_playtime();
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
if (!strcmp(mode, "--probe-services")) return probe_services();
+71 -17
View File
@@ -20,11 +20,11 @@
#define PAGE 0x1000ull
#define GiB (1ull << 30)
#define LOW_SEARCH_FROM 0x100000000ull
#define LOW_SIZE (6 * GiB)
#define LOW_BASE (6 * GiB) // the stack and heap block without a texture heap extra
#define STACK_OFF 0x10000000ull
#define STACK_SIZE 0x10000000ull
#define HEAP_OFF (STACK_OFF + STACK_SIZE + 0x10000000ull)
#define HEAP_CAP (4 * GiB)
#define HEAP_CAP (4 * GiB) // without TOTK_TEX_HEAP_EXTRA_MB, which raises it by its own size (mem_init)
#define ALIAS_SIZE (64 * GiB)
#define ALIAS_ALIGN (4 * GiB)
#define ALIAS_PAGES (ALIAS_SIZE / PAGE)
@@ -48,6 +48,9 @@ enum { ATTR_PERMISSION_LOCKED = 16 };
static RtGuestLayout L;
static uint64_t mods_lo, mods_hi; // fixed module window
// The stack and heap block: LOW_BASE, plus the texture heap extra (tex_heap_extra_mb or a mod's HeapDef,
// res_tex_heap_extra_bytes) rounded up to a GiB, so only a player who grows the texture heap reserves more (mem_init).
static uint64_t low_size = LOW_BASE;
static PlatMutex lock = PLAT_MUTEX_INIT;
static uint64_t heap_size, alias_bytes;
static uint8_t *alias_pages; // 1 per mapped 4 KiB page of the alias region
@@ -78,7 +81,7 @@ static volatile int64_t frz_count[4]; // freezes; thaws by a write fault, by m
static int watched(uint64_t addr, uint64_t size) {
const RtGuestLayout *g = &L;
return (addr >= g->aslr.base && size <= LOW_SIZE && addr - g->aslr.base <= LOW_SIZE - size) ||
return (addr >= g->aslr.base && size <= low_size && addr - g->aslr.base <= low_size - size) ||
(addr >= g->alias.base && size <= ALIAS_SIZE && addr - g->alias.base <= ALIAS_SIZE - size);
}
@@ -172,12 +175,29 @@ static int thaw_fault(uint64_t a) {
}
// Mapping changes thaw the regions they touch and run under frz_lock, so no freeze sees them half done.
static void commit(uint64_t a, uint64_t n, const char *what) {
typedef struct { uint64_t a, n; } CommitTry;
static int commit_try(void *arg) {
const CommitTry *t = arg;
if (rt_oom_forced(RT_OOM_COMMIT)) return 0;
plat_mutex_lock(&frz_lock);
thaw_range_locked(a, n, 3);
int ok = !n || plat_mem_commit(a, n);
thaw_range_locked(t->a, t->n, 3);
int ok = !t->n || plat_mem_commit(t->a, t->n);
plat_mutex_unlock(&frz_lock);
if (!ok) rt_fatal("memory: commit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
return ok;
}
// A refused commit waits for memory (rt_oom_wait): the system's commit ran out for a moment (a page file growing, the
// driver's device memory charged to this process), and the Vulkan back end gives memory back at its next submit. The
// wait holds the caller's lock (lock or klock: only memory SVCs and kernel-region allocations wait behind it) but not
// frz_lock, which write faults on frozen regions need. Every caller waits, the boot's ones too: there a refusal has the
// same cause, and the wait only delays an end the game would reach anyway.
static void commit(uint64_t a, uint64_t n, const char *what) {
CommitTry t = {a, n};
if (commit_try(&t)) return;
char w[64];
snprintf(w, sizeof w, "memory: commit of %s at %#llx", what, (unsigned long long)a);
if (!rt_oom_wait(w, n, commit_try, &t))
rt_fatal("memory: commit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
}
static void decommit(uint64_t a, uint64_t n, const char *what) {
@@ -221,10 +241,15 @@ void mem_init(void) {
// Stack and heap block, then the alias region above it, both searched below the module window.
uint64_t low = 0, alias = 0;
for (uint64_t at = LOW_SEARCH_FROM; at + LOW_SIZE <= mods_lo && !low; at += GiB)
if (reserve_at(at, LOW_SIZE, 1)) low = at;
if (!low) reexec_or_fail("a 6 GiB stack and heap block below 2^39");
for (uint64_t at = align_up(low + LOW_SIZE, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
low_size = LOW_BASE + align_up(res_tex_heap_extra_bytes(), GiB);
for (uint64_t at = LOW_SEARCH_FROM; at + low_size <= mods_lo && !low; at += GiB)
if (reserve_at(at, low_size, 1)) low = at;
if (!low) {
char what[64];
snprintf(what, sizeof what, "a %llu GiB stack and heap block below 2^39", (unsigned long long)(low_size / GiB));
reexec_or_fail(what);
}
for (uint64_t at = align_up(low + low_size, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
if (reserve_at(at, ALIAS_SIZE, 1)) alias = at;
if (!alias) reexec_or_fail("a 64 GiB alias region below 2^39");
@@ -238,11 +263,11 @@ void mem_init(void) {
L = (RtGuestLayout){
.aslr = {low, RT_ADDRESS_LIMIT - low},
.stack = {low + STACK_OFF, STACK_SIZE},
.heap = {low + HEAP_OFF, HEAP_CAP},
.heap = {low + HEAP_OFF, HEAP_CAP + res_tex_heap_extra_bytes()},
.alias = {alias, ALIAS_SIZE},
.kernel = {RT_KERNEL_BASE, RT_KERNEL_SIZE},
.hostfn = {RT_HOSTFN_BASE, RT_HOSTFN_SIZE},
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes(), // resolution.c
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes() + res_tex_heap_extra_bytes(),
};
if (!L.pool) rt_fatal("memory: TOTK_APP_POOL is 0");
}
@@ -276,7 +301,7 @@ static int inside(uint64_t a, uint64_t n, RtRange r) { return a >= r.base && n <
// ---- 0x01 SetHeapSize
uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
plat_mutex_lock(&lock);
if (size > HEAP_CAP || size % 0x200000 || size + image_bytes() > L.pool) {
if (size > L.heap.size || size % 0x200000 || size + image_bytes() > L.pool) {
plat_mutex_unlock(&lock);
return R_INVALID_STATE;
}
@@ -288,6 +313,35 @@ uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
return R_OK;
}
// The runtime's readers of game objects (menu_cursor.c, mouse_direct.c) check every pointer before they follow it. An
// OS query (plat_mem_readable: VirtualQuery) costs tens of microseconds in this process, and a reader that checks a
// few dozen pointers at a time stalls the game thread for milliseconds. The game's objects live in the committed heap
// and its globals in the modules' images, which are readable for as long as they are mapped: those answer from the
// layout. Any other range asks the OS. The heap only shrinks when the game asks for it (mem_set_heap_size), not while
// it runs.
int mem_guest_readable(uint64_t a, uint64_t n) {
static uint64_t fast, slow;
if (a + n < a) return 0;
uint64_t hs = __atomic_load_n(&heap_size, __ATOMIC_ACQUIRE);
int ok = a >= L.heap.base && a + n <= L.heap.base + hs;
for (int i = 0; !ok && i < ldr_count(); i++) {
const RtLoaded *m = ldr_module(i);
ok = a >= m->base && a + n <= m->base + m->size;
}
if (ok) {
if (__atomic_add_fetch(&fast, 1, __ATOMIC_RELAXED) == 100000)
rt_log("memory: 100000 guest reads checked by the layout, %llu by the OS",
(unsigned long long)__atomic_load_n(&slow, __ATOMIC_RELAXED));
} else {
ok = plat_mem_readable(a, n);
uint64_t s = __atomic_add_fetch(&slow, 1, __ATOMIC_RELAXED);
if (!(s & (s - 1)) && s >= 1024) // 1024, 2048, 4096...: how often the layout could not answer
rt_log("memory: guest reads checked by the layout %llu, by the OS %llu",
(unsigned long long)__atomic_load_n(&fast, __ATOMIC_RELAXED), (unsigned long long)s);
}
return ok;
}
// ---- module data permissions (0x02, 0x03) and module pages for QueryMemory
static int module_index(uint64_t a) {
for (int i = 0; i < ldr_count(); i++) {
@@ -477,7 +531,7 @@ static void taken_gap(uint64_t a, uint64_t *base, uint64_t *size) {
uint64_t lo = 0, hi = RT_ADDRESS_LIMIT;
RtRange known[3 + MOD_COUNT + 2];
int n = 0;
known[n++] = (RtRange){L.aslr.base, LOW_SIZE};
known[n++] = (RtRange){L.aslr.base, low_size};
known[n++] = L.alias;
known[n++] = L.kernel;
known[n++] = L.hostfn;
@@ -520,14 +574,14 @@ uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
r.base = 0;
r.size = low;
r.state = ST_FREE;
} else if (a < low + LOW_SIZE) {
} else if (a < low + low_size) {
uint64_t heap = L.heap.base;
if (a >= heap && a < heap + heap_size) {
r = (RtMemoryInfo){.base = heap, .size = heap_size, .state = ST_NORMAL, .perm = PERM_RW};
} else if (a < heap) {
r = (RtMemoryInfo){.base = low, .size = heap - low, .state = ST_FREE};
} else {
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + LOW_SIZE - heap - heap_size, .state = ST_FREE};
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + low_size - heap - heap_size, .state = ST_FREE};
}
} else if (a - L.alias.base < L.alias.size) {
uint64_t p = (a - L.alias.base) / PAGE;
+398
View File
@@ -0,0 +1,398 @@
// The mouse pointer in TotK 1.4.3's own menus (the title, the inventory, the System tab, Options, shops, dialogue
// choices): while a menu screen has the game's focus box (eui's BoxCursor, the frame the d-pad moves), the Windows
// pointer is released from the mouse camera and shown, the item under it takes the focus as if the d-pad had moved
// there (highlight, sound and description included), a left click on an item is A on it and a right click is B.
// Nothing is drawn and no game code is called: at each pad read (ov_input.c GetNpadStates, the game thread) the open
// screens are read and the focus is moved with the three writes eui::BoxCursorMgr::moveBoxCursor makes, so the game's
// own BoxCursorControl moves the box at its next update.
//
// Adapted from Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp @ df0586cdc5702ca19bbebfa59697e0b6980f6237
// (https://github.com/daniel-madrid-07/weeping-of-the-empire), Copyright (c) 2026 daniel-madrid-07, MIT License: the
// focus read, the hit test, the stale-hit-pane fallback and the focus writes, which it does for 1.0.0.
// Placed in 1.4.3 by hand:
// - eui::ScreenMgr::sInstance: 1.0.0 main+0x4640010, 1.4.3 main+0x3acf960 (its createInstance main+0xc613d4 and
// constructor main+0xc61450 match 1.0.0's main+0x119c5cc / +0x119c648 instruction for instruction, the object
// 0x450 bytes with the same field stores, so its layout is 1.0.0's);
// - the screen's reserved node (+0x150) and request bits (+0x175, bit 2) and its draw target byte (+0x16c): the same
// stores at 1.4.3 main+0x87903c.
// The other fields are 1.0.0's; menu_cursor logs what it reads the first time a menu opens (TOTK_MENU_CURSOR_LOG=1:
// every screen change), so a field that moved shows as a menu with no nodes or boxes off the layout.
//
// Only while keyboard and mouse are the device used last (input_kbm_last): a gamepad player sees the menus unchanged.
// TOTK_MENU_CURSOR (live, default 1): 0 leaves the menus to the keys and the pad.
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "input.h"
#include "input_map.h"
#include "input_wsys.h"
#include "layout.h"
#include "platform/platform.h"
#define SCREEN_MGR_V143 0x3acf960ull // eui::ScreenMgr::sInstance
#define MAIN_SIZE 0x3adf000ull // main's image (code, data, bss) in 1.4.3
// eui::ScreenMgr, BoxCursorMgr, BoxCursorControl
enum { SM_BOX_MGR = 0x3f8, BM_CONTROLS = 0x28, CTL_ACTIVE = 0x58, CTL_RESERVED = 0x60 };
// eui::Screen
enum { SCR_LAYOUT = 0x28, SCR_DRAW_INFO = 0x60, SCR_NODES = 0x80, SCR_RESERVED = 0x150, SCR_STATE = 0x16e,
SCR_REQUEST = 0x175, SCR_OPENED = 2 };
// eui::BoxCursorNode, AnimButton
enum { NODE_BUTTON = 0x18, NODE_SCREEN = 0x20, BTN_LAYOUT = 0x20, BTN_FLAGS = 0x40, BTN_HIT_PANE = 0x58 };
#define BTN_USABLE 0x20010u // both set: active and enabled (ButtonGroup::Update's test)
// nn::ui2d::DrawInfo: projection (4 x 4) and view (3 x 4); nn::ui2d::Layout: root pane, size, name
enum { DI_PROJ = 0x10, DI_VIEW = 0x50, LYT_ROOT = 0x18, LYT_SIZE = 0x28, LYT_NAME = 0x30 };
// nn::ui2d::Pane
enum { PANE_PARENT = 0x18, PANE_SIZE = 0x50, PANE_FLAG = 0x58, PANE_ALPHA = 0x59, PANE_BASE = 0x5b, PANE_MTX = 0x70 };
static uint64_t main_lo, main_hi;
static int ok_at(uint64_t p, uint64_t n) {
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && mem_guest_readable(p, n);
}
static uint64_t rd64(uint64_t a) { uint64_t v; memcpy(&v, (const void *)(uintptr_t)a, 8); return v; }
static uint32_t rd32(uint64_t a) { uint32_t v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
static uint8_t rd8(uint64_t a) { return *(const volatile uint8_t *)(uintptr_t)a; }
static float rdf(uint64_t a) { float v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
// An object of the game: readable, its vtable in main's image
static int game_object(uint64_t p, uint64_t n) {
if (!ok_at(p, n)) return 0;
uint64_t vt = rd64(p);
return vt >= main_lo && vt < main_hi;
}
// ---- panes (AnimButton::HitTest's rules: 1.0.0 main+0x222ea40, +0x223b090)
static int pane_shown(uint64_t pane) {
if (!ok_at(pane, PANE_MTX + 0x20)) return 0;
if (!(rd8(pane + PANE_FLAG) & 1) || !rd8(pane + PANE_ALPHA)) return 0;
int depth = 0;
for (uint64_t p = rd64(pane + PANE_PARENT); p; p = rd64(p + PANE_PARENT)) {
if (!ok_at(p, PANE_MTX) || ++depth > 64) return 0;
uint8_t f = rd8(p + PANE_FLAG);
if (!(f & 1) || (!rd8(p + PANE_ALPHA) && (f & 2))) return 0;
}
return 1;
}
// The pane's box in layout coordinates (origin at the centre, y up): centre and half size
static void pane_box(uint64_t pane, float *cx, float *cy, float *hw, float *hh) {
float w = rdf(pane + PANE_SIZE), h = rdf(pane + PANE_SIZE + 4);
float m00 = rdf(pane + PANE_MTX), m11 = rdf(pane + PANE_MTX + 0x14);
uint8_t base = rd8(pane + PANE_BASE), bh = base & 3, bv = base >> 2 & 3;
*hw = fabsf(w * m00) * 0.5f, *hh = fabsf(h * m11) * 0.5f;
*cx = rdf(pane + PANE_MTX + 0xc), *cy = rdf(pane + PANE_MTX + 0x1c);
if (bh == 1) *cx += *hw; else if (bh == 2) *cx -= *hw; // the translation is the left / right edge
if (bv == 1) *cy -= *hh; else if (bv == 2) *cy += *hh; // ... the top / bottom edge
}
// The screen's draw transform, for a screen drawn away from its centred layout space (scale and translation only)
typedef struct { int ok; float v[12], p[16]; } Xf;
static Xf screen_xf(uint64_t scr) {
Xf x = {0};
uint64_t di = rd64(scr + SCR_DRAW_INFO);
if (!ok_at(di, DI_VIEW + sizeof x.v)) return x;
memcpy(x.p, (const void *)(uintptr_t)(di + DI_PROJ), sizeof x.p);
memcpy(x.v, (const void *)(uintptr_t)(di + DI_VIEW), sizeof x.v);
for (int i = 0; i < 16; i++) if (!isfinite(x.p[i])) return x;
for (int i = 0; i < 12; i++) if (!isfinite(x.v[i])) return x;
x.ok = x.p[0] != 0 && x.p[5] != 0 && fabsf(x.p[1]) < 1e-5f && fabsf(x.p[4]) < 1e-5f && fabsf(x.v[1]) < 1e-4f &&
fabsf(x.v[4]) < 1e-4f;
return x;
}
static int to_ndc(const Xf *t, float x, float y, float *nx, float *ny) {
float vx = t->v[0] * x + t->v[1] * y + t->v[3], vy = t->v[4] * x + t->v[5] * y + t->v[7],
vz = t->v[8] * x + t->v[9] * y + t->v[11];
float cx = t->p[0] * vx + t->p[1] * vy + t->p[2] * vz + t->p[3];
float cy = t->p[4] * vx + t->p[5] * vy + t->p[6] * vz + t->p[7];
float cw = t->p[12] * vx + t->p[13] * vy + t->p[14] * vz + t->p[15];
if (!(fabsf(cw) > 1e-6f)) return 0;
*nx = cx / cw, *ny = cy / cw;
return 1;
}
// The transform maps the layout as the plain layout test does (centre to 0,0, corner to 1,1): it adds nothing
static int xf_plain(const Xf *t, float lw, float lh) {
float ax, ay, bx, by;
if (!to_ndc(t, 0, 0, &ax, &ay) || !to_ndc(t, lw / 2, lh / 2, &bx, &by)) return 1;
return fabsf(ax) < 0.01f && fabsf(ay) < 0.01f && fabsf(bx - 1) < 0.01f && fabsf(by - 1) < 0.01f;
}
// The pointer: layout coordinates (x, y) and device coordinates (nx, ny), and the transform when it is not plain
typedef struct { float x, y, nx, ny; const Xf *t; } Pt;
static int pane_hit(uint64_t pane, const Pt *p) {
if (!pane_shown(pane)) return 0;
float cx, cy, hw, hh;
pane_box(pane, &cx, &cy, &hw, &hh);
if (!p->t) return p->x >= cx - hw && p->x <= cx + hw && p->y >= cy - hh && p->y <= cy + hh;
float ax, ay, bx, by;
if (!to_ndc(p->t, cx - hw, cy - hh, &ax, &ay) || !to_ndc(p->t, cx + hw, cy + hh, &bx, &by)) return 0;
return p->nx >= fminf(ax, bx) && p->nx <= fmaxf(ax, bx) && p->ny >= fminf(ay, by) && p->ny <= fmaxf(ay, by);
}
// ---- screens and nodes
typedef struct { uint64_t control, node, screen; } Focus;
static uint64_t screen_mgr(void) {
uint64_t slot = MOD_MAIN_BASE + SCREEN_MGR_V143, sm = rd64(slot);
return game_object(sm, SM_BOX_MGR + 8) ? sm : 0;
}
// The focused node of an open screen, from the draw targets' box cursors (all 0: none, as in gameplay)
static Focus focus_now(void) {
Focus none = {0};
uint64_t sm = screen_mgr(), bm = sm ? rd64(sm + SM_BOX_MGR) : 0;
if (!ok_at(bm, BM_CONTROLS + 16)) return none;
for (int i = 0; i < 2; i++) {
uint64_t ctl = rd64(bm + BM_CONTROLS + 8ull * i);
if (!ok_at(ctl, CTL_RESERVED + 8)) continue;
uint64_t node = rd64(ctl + CTL_ACTIVE);
if (!ok_at(node, NODE_SCREEN + 8)) continue;
uint64_t scr = rd64(node + NODE_SCREEN);
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
return (Focus){ctl, node, scr};
}
return none;
}
static void layout_name(uint64_t scr, char *out, size_t n) {
snprintf(out, n, "?");
uint64_t lyt = rd64(scr + SCR_LAYOUT), name = ok_at(lyt, LYT_NAME + 8) ? rd64(lyt + LYT_NAME) : 0;
if (name < 0x10000 || name >= (1ull << 47) || !mem_guest_readable(name, n)) return;
memcpy(out, (const void *)(uintptr_t)name, n - 1);
out[n - 1] = 0;
for (char *q = out; *q; q++) if (*q < 0x20 || *q > 0x7e) { *q = 0; break; }
}
// The open screens: eui::ScreenMgr's screen array, count at +0x8 and its pointers at +0x10 (1.0.0's layout, which the
// 1.4.3 constructor keeps). The map has no focus box, so it is found here: the Purah Pad's frame (AppMenuOverlay_00)
// with AppMap_00 and the menu's background AppMenuBG_00 open. The Scope opens the frame over AppMap_00 without that
// background (Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp census tells them apart the same way).
// TOTK_MENU_CURSOR_LOG=1 logs the open screens whenever the set changes.
enum { SM_COUNT = 0x8, SM_SCREENS = 0x10 };
static int map_open(void) {
uint64_t sm = screen_mgr();
if (!sm) return 0;
int32_t n = (int32_t)rd32(sm + SM_COUNT);
uint64_t arr = rd64(sm + SM_SCREENS);
if (n <= 0 || n > 1024 || !ok_at(arr, 8ull * (uint64_t)n)) return 0;
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
int verbose = e && !strcmp(e, "1"), pad = 0, map = 0, bg = 0;
char list[1024];
size_t len = 0;
uint64_t h = 1469598103934665603ull;
for (int32_t i = 0; i < n; i++) {
uint64_t scr = rd64(arr + 8ull * (uint64_t)i);
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
char nm[48];
layout_name(scr, nm, sizeof nm);
pad |= !strcmp(nm, "AppMenuOverlay_00");
map |= !strcmp(nm, "AppMap_00");
bg |= !strcmp(nm, "AppMenuBG_00");
if (!verbose) continue;
for (const char *c = nm; *c; c++) h = (h ^ (uint8_t)*c) * 1099511628211ull;
if (len < sizeof list - 64) len += (size_t)snprintf(list + len, sizeof list - len, "%s%s", len ? " " : "", nm);
}
static uint64_t last;
static int lines;
if (verbose && h != last && lines < 200) last = h, lines++, rt_log("menu_cursor: open screens: %s", list);
return pad && map && bg;
}
static void layout_size(uint64_t scr, float *lw, float *lh) {
*lw = 1920, *lh = 1080;
uint64_t lyt = rd64(scr + SCR_LAYOUT);
if (!ok_at(lyt, LYT_SIZE + 8)) return;
float w = rdf(lyt + LYT_SIZE), h = rdf(lyt + LYT_SIZE + 4);
if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) *lw = w, *lh = h;
}
// The screen's focusable nodes: an sead OffsetList, head {prev, next} at +0x80, node-to-link offset at +0x94
#define MAX_NODES 512
static int nodes_of(uint64_t scr, uint64_t *node, uint64_t *btn) {
uint64_t head = scr + SCR_NODES;
int32_t off = (int32_t)rd32(head + 0x14), n = 0;
if (off < 0 || off > 0x40) return 0;
for (uint64_t link = rd64(head + 8); link != head && n < MAX_NODES; link = rd64(link + 8)) {
if (!ok_at(link, 16)) break;
uint64_t nd = link - (uint64_t)off;
if (!ok_at(nd, NODE_SCREEN + 8)) continue;
uint64_t b = rd64(nd + NODE_BUTTON);
if (!ok_at(b, BTN_HIT_PANE + 8)) continue;
node[n] = nd, btn[n] = b, n++;
}
return n;
}
static int usable(uint64_t btn) { return !(~rd32(btn + BTN_FLAGS) & BTN_USABLE); }
static uint64_t root_pane(uint64_t btn) {
uint64_t blyt = rd64(btn + BTN_LAYOUT);
return ok_at(blyt, LYT_ROOT + 8) ? rd64(blyt + LYT_ROOT) : 0;
}
// Every usable node's shown hit pane lies off the visible layout: the hit panes are stale (TotK never uses touch;
// the dialogue choices' sit far from where the choices are drawn), so the buttons' own parts panes stand for them
static int stale_hit_panes(int n, const uint64_t *btn, float lw, float lh, const Xf *t) {
int seen = 0;
for (int i = 0; i < n; i++) {
if (!usable(btn[i])) continue;
uint64_t pane = rd64(btn[i] + BTN_HIT_PANE);
if (!ok_at(pane, PANE_MTX + 0x20) || !pane_shown(pane)) continue;
float cx, cy, hw, hh, vx, vy;
pane_box(pane, &cx, &cy, &hw, &hh);
int in = t ? to_ndc(t, cx, cy, &vx, &vy) && fabsf(vx) <= 1 && fabsf(vy) <= 1
: fabsf(cx) <= lw / 2 && fabsf(cy) <= lh / 2;
seen++;
if (in) return 0;
}
return seen > 0;
}
// The usable node under the pointer, 0: none
static uint64_t node_at(int n, const uint64_t *node, const uint64_t *btn, const Pt *p, float lw, float lh) {
if (!stale_hit_panes(n, btn, lw, lh, p->t)) {
for (int i = 0; i < n; i++)
if (usable(btn[i]) && pane_hit(rd64(btn[i] + BTN_HIT_PANE), p)) return node[i];
return 0;
}
uint64_t found = 0;
float best = 1e30f;
for (int i = 0; i < n; i++) { // parts panes can be several items wide: the nearest centre
uint64_t pane = root_pane(btn[i]);
if (!usable(btn[i]) || !pane_hit(pane, p)) continue;
float cx, cy, hw, hh, d;
pane_box(pane, &cx, &cy, &hw, &hh);
d = (cx - p->x) * (cx - p->x) + (cy - p->y) * (cy - p->y);
if (d < best) best = d, found = node[i];
}
return found;
}
// The focus to node: the writes moveBoxCursor makes. The request bit is a byte eui also writes, so it is raised
// again until the focus is there.
static void move_focus(const Focus *f, uint64_t node) {
memcpy((void *)(uintptr_t)(f->control + CTL_RESERVED), &node, 8);
memcpy((void *)(uintptr_t)(f->screen + SCR_RESERVED), &node, 8);
__atomic_fetch_or((uint8_t *)(uintptr_t)(f->screen + SCR_REQUEST), (uint8_t)4, __ATOMIC_SEQ_CST);
}
// Screens with a focus box that are gameplay, not menus: the L ability wheel, the D-pad strips, Autobuild's list, the
// Purah Pad's camera and the map (the stick moves its own cursor there)
static int gameplay_screen(const char *nm) {
static const char *const names[] = {"ShortCutMainItem_00", "ShortCut_00", "AutoBuilder_00", "CameraPointer_00",
"AppMap_00"};
for (size_t i = 0; i < sizeof names / sizeof *names; i++)
if (!strcmp(nm, names[i])) return 1;
return 0;
}
static volatile int32_t g_pointer; // a menu has the pointer: input_cursor_allowed releases the capture
static volatile int32_t g_map; // the Purah Pad's map is open (not the Scope): the mouse moves its cursor
int menu_cursor_pointer(void) { return __atomic_load_n(&g_pointer, __ATOMIC_RELAXED); }
int menu_cursor_map(void) { return __atomic_load_n(&g_map, __ATOMIC_RELAXED); }
static int enabled(void) {
if (!RT_V143) return 0;
const char *v = cfg_get("TOTK_MENU_CURSOR"), *in = cfg_get("TOTK_INPUT");
return !(v && !strcmp(v, "0")) && !(in && !strcmp(in, "none"));
}
static void set_pointer(int on) {
if (plat_xchg32(&g_pointer, on) == on) return;
ikb_cursor_update();
if (on) ikb_pointer_set(0.5f, 0.5f); // the pointer appears in the middle, not where the capture left it
}
// The pointer on the game's picture, 0..1 from its top left (the present's fit, letterbox or pillarbox)
static int picture_pos(float *px, float *py) {
float nx, ny, r[4];
if (!ikb_pointer(&nx, &ny) || !gfx_picture_rect(r) || r[2] <= 0 || r[3] <= 0) return 0;
*px = (nx - r[0]) / r[2], *py = (ny - r[1]) / r[3];
return *px >= 0 && *px <= 1 && *py >= 0 && *py <= 1;
}
static void log_screen(const Focus *f, const char *nm, int n, float lw, float lh, const Xf *t, int stale) {
static uint64_t last;
static int lines;
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
int verbose = e && !strcmp(e, "1");
if (f->screen == last || (!verbose && lines >= 6)) return;
last = f->screen, lines++;
rt_log("menu_cursor: screen %s (layout %gx%g, %d nodes, %s hit panes, draw transform %s)", nm, lw, lh, n,
stale ? "stale" : "own", t ? "scaled" : "plain");
}
// Once per pad read on the game thread: the pointer's hover and clicks, as pad buttons (A, B) for this read
void menu_cursor_poll(uint64_t *buttons) {
static int inited, pending_a, held_l, held_r;
static uint64_t pending_node, pending_until, hold_until, hold_bit, menu_seen;
static float last_px = -1, last_py = -1;
if (!inited) {
inited = 1;
main_lo = MOD_MAIN_BASE, main_hi = MOD_MAIN_BASE + MAIN_SIZE;
if (RT_V143) rt_log("menu_cursor: on for 1.4.3 (TOTK_MENU_CURSOR=0 turns it off)");
}
if (!enabled()) { set_pointer(0); __atomic_store_n(&g_map, 0, __ATOMIC_RELAXED); return; }
Focus f = focus_now();
char nm[48] = "";
if (f.screen) layout_name(f.screen, nm, sizeof nm);
// The open screens are scanned every 100 ms, not at every pad read: each screen costs memory queries
uint64_t now = plat_now_ms();
static uint64_t map_at;
static int map, map_logged;
if (!map_at || now - map_at >= 100) map_at = now, map = map_open();
if (map && !map_logged) map_logged = 1, rt_log("menu_cursor: the map is open: the mouse moves its cursor");
__atomic_store_n(&g_map, map, __ATOMIC_RELAXED);
// With the gamepad as the last device the menus stay as they are on the console: no pointer, the capture kept.
// Moving the mouse (or a key) makes keyboard and mouse the last device and the pointer comes.
int menu = f.screen && !gameplay_screen(nm) && ikb_foreground() && input_kbm_last();
if (menu) menu_seen = now;
// A menu's focus gap (a tab switch, a popup opening: no focus box for a frame or two) keeps the pointer where it
// is, for up to 300 ms: releasing it there captured it again and centred it when the next tab took the focus
int gap = !menu && !f.screen && menu_seen && now - menu_seen < 300 && ikb_foreground() && input_kbm_last();
set_pointer(menu || gap);
int l = ikb_down(KEY_MOUSE_L), r = ikb_down(KEY_MOUSE_R);
int click_l = l && !held_l, click_r = r && !held_r;
held_l = l, held_r = r;
if (now < hold_until) { // a click's button, held for 60 ms so the game sees one press whatever its read rate
*buttons |= hold_bit;
return;
}
if (gap) return; // a click's focus request waits for the next screen
if (!menu) { pending_a = 0; last_px = last_py = -1; return; }
if (pending_a) { // the focus asked for by a click: A once it is there
if (rd64(f.control + CTL_ACTIVE) == pending_node) {
pending_a = 0, hold_until = now + 60, hold_bit = 1ull << B_A;
*buttons |= hold_bit;
return;
}
if (now > pending_until) pending_a = 0;
else move_focus(&f, pending_node);
return;
}
if (click_r) { hold_until = now + 60, hold_bit = 1ull << B_B; *buttons |= hold_bit; return; }
float px, py;
if (!picture_pos(&px, &py)) return;
int moved = fabsf(px - last_px) > 0.001f || fabsf(py - last_py) > 0.001f;
if (!moved && !click_l) return;
last_px = px, last_py = py;
float lw, lh;
layout_size(f.screen, &lw, &lh);
Xf xf = screen_xf(f.screen);
Pt p = {(px - 0.5f) * lw, (0.5f - py) * lh, px * 2 - 1, 1 - py * 2, NULL};
if (xf.ok && !xf_plain(&xf, lw, lh)) p.t = &xf;
static uint64_t node[MAX_NODES], btn[MAX_NODES];
int n = nodes_of(f.screen, node, btn);
log_screen(&f, nm, n, lw, lh, p.t, n && stale_hit_panes(n, btn, lw, lh, p.t));
uint64_t at = n ? node_at(n, node, btn, &p, lw, lh) : 0;
if (!at) return;
if (at != f.node) move_focus(&f, at);
if (click_l) {
if (at == f.node) { hold_until = now + 60, hold_bit = 1ull << B_A; *buttons |= hold_bit; }
else pending_a = 1, pending_node = at, pending_until = now + 300;
}
}
// The L ability wheel or a D-pad strip (materials, weapons, shields, bows) has the focus: 1 / 0, -1 with the menu
// cursor off (the screens are not read). input.c asks while L or the D-pad is held, before it gives the mouse to their
// selectors: L also soothes the horse and turns menu tabs, the D-pad does other things too.
int menu_cursor_shortcut(void) {
if (!enabled()) return -1;
Focus f = focus_now();
char nm[48] = "";
if (f.screen) layout_name(f.screen, nm, sizeof nm);
return f.screen && (!strcmp(nm, "ShortCutMainItem_00") || !strcmp(nm, "ShortCut_00"));
}
+248 -45
View File
@@ -15,7 +15,9 @@
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
// rebuild at start (cheats.c), and its mod stays off until then.
// rebuild at start (cheats.c), and its mod stays off until then. A file's writes into main's data (.rodata, .data,
// .bss) are lines of build-cache/exefs.data, stored into the loaded main before the game runs (mods_exefs_data), as
// emulators apply a patch file to the module before its dynamic linker runs.
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
@@ -170,22 +172,26 @@ static int same_build(const char *hex, size_t n) {
return 1;
}
// build-cache/exefs.status next to totk.exe, read once; "" when there is none.
static const char *exefs_status(void) {
static char *text;
if (text) return text;
// build-cache/<name> next to totk.exe, read once into *text; "" when there is none (mods_probe_cache sets the text).
static char *status_text, *data_text;
static const char *build_cache(const char *name, char **text) {
if (*text) return *text;
char path[2100];
uint64_t n = 0;
if (plat_exe_path(path, sizeof path)) {
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
if (back > cut) cut = back;
if (cut) snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/exefs.status");
text = (char *)plat_file_read(path, &n);
if (cut && snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/%s", name) <
(int)(sizeof path - (size_t)(cut + 1 - path)))
*text = (char *)plat_file_read(path, &n);
}
if (!text) text = calloc(1, 1);
return text;
if (!*text) *text = calloc(1, 1);
return *text;
}
static const char *exefs_status(void) { return build_cache("exefs.status", &status_text); }
static const char *exefs_data(void) { return build_cache("exefs.data", &data_text); }
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
char want[20];
@@ -194,8 +200,9 @@ static int status_line(const char *key, const char *content, char *kind, char *o
const char *end = strchr(line, '\n');
size_t len = end ? (size_t)(end - line) : strlen(line);
if (len > 18 && !memcmp(line, want, 18)) {
char copy[600];
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : sizeof copy - 1), line + 18);
char copy[600]; // the line after the two hashes, without the next line
size_t rest = len - 18;
snprintf(copy, sizeof copy, "%.*s", (int)(rest < sizeof copy ? rest : sizeof copy - 1), line + 18);
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
if (!t1 || !t2) return 0;
*t1 = *t2 = 0;
@@ -269,9 +276,14 @@ static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
size_t k = strlen(m->overrides);
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
} else if (!strcmp(kind_s, "data") && m->ndata < (int)(sizeof m->data / sizeof *m->data)) {
m->data[m->ndata++] = (uint32_t)strtoul(key, NULL, 16);
} else if (!strcmp(kind_s, "other")) {
m->other_version++;
}
unsigned long bytes; // a code or data file's data writes: exefs.data must hold them all (mods_exefs_data)
if ((!strcmp(kind_s, "code") || !strcmp(kind_s, "data")) && sscanf(note, "data %lu", &bytes) == 1)
m->data_bytes += (uint32_t)bytes;
return 1;
}
@@ -326,34 +338,62 @@ static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
int unsupported = exefs_check(m, path);
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
: unsupported ? MOD_EXEFS_UNSUPPORTED
: m->pending ? MOD_EXEFS_LATER
: m->nsets || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
: MOD_EMPTY;
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
: unsupported ? MOD_EXEFS_UNSUPPORTED
: m->pending ? MOD_EXEFS_LATER
: m->nsets || m->ndata || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
: MOD_EMPTY;
m->enabled = !listed(disabled, m->name);
nmods++;
return 1;
}
void mods_scan(void) {
static int done;
if (done) return;
done = 1;
// The mods folder into out: TOTK_MODS_DIR, else <dir of totk.exe>/mods. 0 when the exe's path is unknown.
static int mods_folder(char *out, size_t cap) {
const char *d = cfg_get("TOTK_MODS_DIR");
if (d && *d) {
snprintf(dir, sizeof dir, "%s", d);
} else {
char exe[2048];
if (!plat_exe_path(exe, sizeof exe)) return;
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
if (back > cut) cut = back;
if (!cut) return;
*cut = 0;
snprintf(dir, sizeof dir, "%s/mods", exe);
}
if (d && *d) return snprintf(out, cap, "%s", d) < (int)cap;
char exe[2048];
if (!plat_exe_path(exe, sizeof exe)) return 0;
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
if (back > cut) cut = back;
if (!cut) return 0;
*cut = 0;
return snprintf(out, cap, "%s/mods", exe) < (int)cap;
}
typedef struct {
const char *folder, *disabled;
void (*cb)(const char *name, const char *romfs, void *ctx);
void *ctx;
} Peek;
static int peek_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
const Peek *pk = ctx;
if (kind != PLAT_FS_DIR || listed(pk->disabled, name)) return 1;
char path[2100], romfs[2048];
if (snprintf(path, sizeof path, "%s/%s", pk->folder, name) >= (int)sizeof path) return 1;
find_root(path, "romfs", romfs, sizeof romfs);
if (!romfs[0]) find_root(path, "romfslite", romfs, sizeof romfs);
if (romfs[0]) pk->cb(name, romfs, pk->ctx);
return 1;
}
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx) {
char folder[2048];
if (!mods_folder(folder, sizeof folder) || !plat_dir_exists(folder)) return;
Peek pk = {folder, cfg_get("TOTK_MODS_DISABLED"), cb, ctx};
plat_fs_list(folder, peek_cb, &pk);
}
static int scanned;
void mods_scan(void) {
if (scanned) return;
scanned = 1;
if (!mods_folder(dir, sizeof dir)) return;
if (!plat_dir_exists(dir)) {
rt_log("mods: no folder %s", dir);
return;
@@ -376,18 +416,171 @@ void mods_scan(void) {
rt_log("mods: %d folders in %s", nmods, dir);
}
void mods_exefs_switch(void) {
mods_scan();
for (int i = 0; i < nmods; i++) {
const ModInfo *m = &mods[i];
if (!start_enabled[i] || m->problem != MOD_OK || !m->nsets) continue;
for (int k = 0; k < m->nsets; k++) {
char name[32];
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
int set = rt_patch_set(name);
if (set >= 0) rt_patch_switch(set, 1);
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
// ---- ExeFS data writes (build-cache/exefs.data; tools/exefs_patches.py data_line has the format)
// 1 when key names one of m's usable ExeFS patch files: one with a patch set, or one with data writes only.
static int owns(const ModInfo *m, uint32_t key) {
for (int k = 0; k < m->nsets; k++)
if (m->sets[k] == key) return 1;
for (int k = 0; k < m->ndata; k++)
if (m->data[k] == key) return 1;
return 0;
}
static int hex_digit(char c) {
return c >= '0' && c <= '9' ? c - '0'
: c >= 'a' && c <= 'f' ? c - 'a' + 10
: c >= 'A' && c <= 'F' ? c - 'A' + 10
: -1;
}
// One line: "<file hash>\t<content hash>\t<offset in main>\t<bytes>[\tbase]", all hex. 0 when malformed.
typedef struct { char key[9], content[9]; uint64_t off; uint8_t bytes[256]; int n, base; } DataRun;
static int parse_run(const char *line, size_t len, DataRun *r) {
char copy[1100], *f[6];
if (len >= sizeof copy) return 0;
memcpy(copy, line, len);
copy[len] = 0;
int nf = 0;
for (char *p = copy; p && nf < 6; nf++) {
f[nf] = p;
p = strchr(p, '\t');
if (p) *p++ = 0;
}
if (nf < 4 || nf > 5 || strlen(f[0]) != 8 || strlen(f[1]) != 8) return 0;
memcpy(r->key, f[0], 9);
memcpy(r->content, f[1], 9);
char *end;
r->off = strtoull(f[2], &end, 16);
size_t hex = strlen(f[3]);
if (!*f[2] || *end || !hex || hex % 2 || hex / 2 > sizeof r->bytes) return 0;
r->n = (int)(hex / 2);
for (int i = 0; i < r->n; i++) {
int hi = hex_digit(f[3][2 * i]), lo = hex_digit(f[3][2 * i + 1]);
if (hi < 0 || lo < 0) return 0;
r->bytes[i] = (uint8_t)(hi << 4 | lo);
}
r->base = nf == 5;
return nf == 4 || (!strcmp(f[4], "base") && r->n == 8);
}
// Stores n bytes into the loaded main at a: a read-only page (.rodata, loader.c seal) is writable for its part of the
// copy only, as cheats.c wr_main does. 0 when a page is neither readable nor writable.
static int store_main(uint64_t a, const uint8_t *from, int n) {
while (n > 0) {
uint64_t page = a & ~0xFFFull, end;
int k = (int)(page + 0x1000 - a);
if (k > n) k = n;
int prot = plat_mem_state(page, page + 0x1000, &end);
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return 0;
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return 0;
memcpy((void *)(uintptr_t)a, from, (size_t)k);
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
a += (uint64_t)k;
from += k;
n -= k;
}
return 1;
}
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
if (!store) store = store_main;
long total = 0, found = 0;
for (int pass = 0; pass < 2; pass++) { // pass 0 checks every line, pass 1 stores
// exefs.data must hold every byte the status lines count: one missing or cut short would leave the code
// patches on without their data
if (pass && found != (long)m->data_bytes) {
rt_log("mods: %s: build-cache/exefs.data holds %ld of the %u bytes its ExeFS patch files write", m->name,
found, m->data_bytes);
return -1;
}
for (const char *line = exefs_data(), *next; *line; line = next) {
const char *nl = strchr(line, '\n');
size_t len = nl ? (size_t)(nl - line) : strlen(line);
next = nl ? nl + 1 : line + len;
if (len && line[len - 1] == '\r') len--;
char key[9];
snprintf(key, sizeof key, "%.*s", (int)(len < 8 ? len : 8), line);
if (!len || *line == '#' || !owns(m, (uint32_t)strtoul(key, NULL, 16))) continue;
DataRun r;
char kind[16], over[256], note[256];
// a line whose file the status does not list as built (another content) or that leaves main's data: the
// two build-cache files and this build disagree
if (!parse_run(line, len, &r) || !status_line(r.key, r.content, kind, over, note) ||
(strcmp(kind, "code") && strcmp(kind, "data")) || r.off < code_end || r.off > size ||
(uint64_t)r.n > size - r.off) {
rt_log("mods: %s: a line of build-cache/exefs.data does not fit this build: %.*s", m->name, (int)len,
line);
return -1;
}
if (!pass) {
found += r.n;
continue;
}
if (r.base) { // a DT_RELR word holds its addend; the dynamic linker adds the module's base
uint64_t w;
memcpy(&w, r.bytes, 8);
w += base;
memcpy(r.bytes, &w, 8);
}
if (!store(base + r.off, r.bytes, r.n)) {
rt_log("mods: %s: cannot write main+%#llx", m->name, (unsigned long long)r.off);
return -1;
}
total += r.n;
}
}
return total;
}
static char *dup_text(const char *s) {
if (!s) return NULL;
size_t n = strlen(s) + 1;
char *d = malloc(n);
if (d) memcpy(d, s, n);
return d;
}
void mods_probe_cache(const char *status, const char *data) {
free(status_text);
free(data_text);
status_text = dup_text(status);
data_text = dup_text(data);
}
void mods_rescan(void) {
free(mods);
mods = NULL;
nmods = 0;
scanned = 0;
}
// Switches the patch set mod_<key> on; 0 when this build lacks it (logged).
static int set_on(const ModInfo *m, uint32_t key) {
char name[32];
snprintf(name, sizeof name, "mod_%08x", key);
int set = rt_patch_set(name);
if (set >= 0) rt_patch_switch(set, 1);
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
return set >= 0;
}
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
// bottom first: the top mod's data writes land last and win, as its RomFS files do. Patch sets are only switches
// (rt_patch_switch), so their order does not matter.
for (int i = nmods - 1; i >= 0; i--) {
const ModInfo *m = &mods[i];
if (!start_enabled[i] || m->problem != MOD_OK || !(m->nsets || m->ndata)) continue;
// the data first: a mod whose data writes cannot all be stored keeps its code patches off too, as half of a
// patch file is worse than none
long stored = size ? mods_exefs_data(m, base, size, code_end, store) : 0;
if (stored < 0) {
rt_log("mods: %s: its ExeFS patch files stay off: their data writes could not all be stored", m->name);
continue;
}
for (int k = 0; k < m->nsets; k++) set_on(m, m->sets[k]);
char over[256];
snprintf(over, sizeof over, "%s", m->overrides);
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
@@ -395,10 +588,20 @@ void mods_exefs_switch(void) {
if (set >= 0) rt_patch_switch(set, 0);
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
}
rt_log("mods: %s: %d ExeFS patch files on", m->name, m->nsets);
rt_log("mods: %s: %d ExeFS patch files on, %ld bytes of main's data stored", m->name, m->nsets + m->ndata,
stored);
}
}
void mods_exefs_switch(void) {
mods_scan();
const RtLoaded *main_mod = NULL;
for (int i = 0; ldr_module(i); i++)
if (!strcmp(ldr_module(i)->name, "main")) main_mod = ldr_module(i);
if (main_mod) mods_exefs_apply(main_mod->base, main_mod->size, main_mod->text_end, NULL);
else mods_exefs_apply(0, 0, 0, NULL);
}
int mods_exefs_pending(uint32_t *key) {
int n = 0;
for (int i = 0; i < nmods; i++) {
+27 -2
View File
@@ -2,7 +2,8 @@
// them: <mod>/romfs, <mod>/exefs and <mod>/cheats, the Atmosphere layout (atmosphere/contents/<title id>/romfs,
// atmosphere/exefs_patches/<name>/*.ips) or TKMM's merged output (romfs or romfslite, exefs, cheats). ExeFS patch
// files (.ips, .pchtxt) for the game's main are patch sets the setup builds into the game (tools/exefs_patches.py),
// switched with their mod (mods_exefs_switch). The list is scanned once at start (romfs.c overlay_init);
// switched with their mod (mods_exefs_switch), and data writes stored into main before the game runs (mods_exefs_data).
// The list is scanned once at start (romfs.c overlay_init);
// its order and switches live in totk.ini [mods] (TOTK_MODS_ORDER: names top first, separated by '|';
// TOTK_MODS_DISABLED: names switched off). The top mod wins a file two mods replace.
#pragma once
@@ -36,10 +37,17 @@ typedef struct {
uint32_t pending, pending_key; // ExeFS patch files the build lacks; FNV-1a of their names and contents
int nsets;
uint32_t sets[16]; // the built patch sets mod_<hash> of its ExeFS patch files
int ndata;
uint32_t data[16]; // the hashes of its ExeFS patch files with data writes only (no set)
uint32_t data_bytes; // the bytes its usable ExeFS patch files write into main's data, as their
// status lines count them ("data <bytes>")
char overrides[256]; // the port's own patch sets its patches replace, comma separated
} ModInfo;
void mods_scan(void); // once; later calls return at once
// Every switched-on mod folder's RomFS root, without the rest of the scan (no ExeFS checks, no order): for mem_init,
// which runs before the paths mods_scan needs are set (resolution.c mod_tex_heap_kib). cb gets the name and root.
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx);
const char *mods_dir(void); // the folder scanned (default: <dir of totk.exe>/mods)
int mods_count(void);
ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
@@ -48,8 +56,25 @@ ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
void mods_set_enabled(int i, int on);
void mods_move(int i, int delta); // -1 up, +1 down; nothing at the ends
int mods_changed(void); // 1 when the order or a switch differs from the start
// After rt_patches_init: the patch sets of the enabled mods' ExeFS patches on, the port's own sets they replace off.
// After ldr_load and rt_patches_init, before any game code: the patch sets of the enabled mods' ExeFS patches on, the
// port's own sets they replace off, and their data writes stored into main (mods_exefs_data).
void mods_exefs_switch(void);
// What mods_exefs_switch does with main's image at base (size bytes, code below code_end; size 0: no data stored), for
// every enabled mod from the bottom one up, so that the top mod's data writes win where two mods write the same bytes.
// store as for mods_exefs_data.
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
// Stores the data writes of mod m's ExeFS patch files (build-cache/exefs.data) into main's image at base: size bytes,
// code below code_end. A DT_RELR word gets base added, as the dynamic linker adds it. Every line is checked before the
// first store: one that falls outside [code_end, size) or does not belong to a usable file of m keeps them all out.
// store writes n bytes at a guest address, 1 on success; NULL: into the loaded main, whose .rodata is read-only.
// Returns the bytes stored, -1 when a line kept them out or a store failed. --probe-mods passes a buffer and memcpy.
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
// --probe-mods: the texts of build-cache/exefs.status and exefs.data instead of the files (NULL: read the file), and a
// fresh scan at the next mods_scan.
void mods_probe_cache(const char *status, const char *data);
void mods_rescan(void);
// The ExeFS patch files of the enabled mods that the build lacks, their FNV-1a mixed into *key (cheats.c starts the
// rebuild for them).
int mods_exefs_pending(uint32_t *key);
+93 -3
View File
@@ -1,5 +1,6 @@
// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to totk.exe,
// scans it and removes it. Needs no game files.
// scans it and removes it. Also the resource size table's entries of the members of the mods' packs (restbl_probe.c).
// Needs no game files.
#include <stdio.h>
#include <string.h>
@@ -7,6 +8,7 @@
#include "host.h"
#include "mods.h"
#include "platform/platform.h"
#include "restbl.h"
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
@@ -37,6 +39,93 @@ static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
return 1;
}
static uint32_t fnv(const char *p, size_t n) {
uint32_t h = 0x811C9DC5u;
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)p[i]) * 0x01000193u;
return h;
}
static ModInfo *by_name(const char *name) {
for (int i = 0; mods_at(i); i++)
if (!strcmp(mods_at(i)->name, name)) return mods_at(i);
return NULL;
}
// mods_exefs_data's store into a buffer standing in for main
static int store_buffer(uint64_t a, const uint8_t *p, int n) {
memcpy((void *)(uintptr_t)a, p, (size_t)n);
return 1;
}
// A second scan of a folder of its own: data writes of ExeFS patch files (build-cache/exefs.data, given as text).
static void probe_data(const char *root) {
remove_tree(root);
plat_file_mkdir_p(root);
const char *bid = *mods_main_build_id() ? mods_main_build_id() : "0";
char n_ips[160], p_ips[160], q_ips[160], x_ips[160], y_ips[160];
snprintf(n_ips, sizeof n_ips, "N/exefs/%s.ips", bid);
snprintf(p_ips, sizeof p_ips, "P/exefs/%s.ips", bid);
snprintf(q_ips, sizeof q_ips, "Q/exefs/%s.ips", bid);
snprintf(x_ips, sizeof x_ips, "X/exefs/%s.ips", bid);
snprintf(y_ips, sizeof y_ips, "Y/exefs/%s.ips", bid);
const char *files[] = {n_ips, p_ips, q_ips, x_ips, y_ips};
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
uint32_t kn = fnv(n_ips, strlen(n_ips)), kp = fnv(p_ips, strlen(p_ips)), kq = fnv(q_ips, strlen(q_ips));
uint32_t kx = fnv(x_ips, strlen(x_ips)), ky = fnv(y_ips, strlen(y_ips));
uint32_t c = fnv("probe", 5);
char status[512], data[768];
snprintf(status, sizeof status,
"%08X\t%08X\tdata\t-\tdata 11\n%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 4\n"
"%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 1\n",
kn, c, kp, c, kq, c, kx, c, ky, c);
snprintf(data, sizeof data,
"# file hash, content hash, offset in main, bytes[, base]\n"
"%08X\t%08X\t10\t414243\n" // N: three bytes of data
"%08X\t%08X\t18\t0800000000000000\tbase\r\n" // N: a DT_RELR word, 8 plus main's base (a CRLF line)
"DEADBEEF\t%08X\t20\t99\n" // another file's: not N's
"%08X\t%08X\t4\t77\n" // P: below main's data
"%08X\t%08X\t30\t66\n" // Q: 1 of the 4 bytes its status counts
"%08X\t%08X\t28\tBB\n" // X: the byte Y writes too
"%08X\t%08X\t28\tAA\n", // Y, above X in the order: its byte stays
kn, c, kn, c, c, kp, c, kq, c, kx, c, ky, c);
mods_probe_cache(status, data);
cfg_set("TOTK_MODS_DIR", root);
cfg_set("TOTK_MODS_ORDER", "Y"); // Y on top, the rest by name: Y N P Q X
cfg_set("TOTK_MODS_DISABLED", "");
mods_rescan();
mods_scan();
ModInfo *n = by_name("N"), *p = by_name("P"), *q = by_name("Q");
CHECK(n && p && q && by_name("X") && by_name("Y"), "second scan: five mods");
if (n && p && q) {
CHECK(n->kind == MOD_EXEFS && n->problem == MOD_OK && n->ndata == 1 && !n->nsets,
"an ExeFS patch file with data writes only makes a usable mod");
uint8_t img[0x40];
memset(img, 0, sizeof img);
uint64_t base = (uint64_t)(uintptr_t)img, word;
long stored = mods_exefs_data(n, base, sizeof img, 8, store_buffer);
memcpy(&word, img + 0x18, 8);
CHECK(stored == 11 && !memcmp(img + 0x10, "ABC", 3) && word == base + 8 && !img[0x20],
"data writes: bytes as they are, a DT_RELR word rebased, other files' lines left");
memset(img, 0x5A, sizeof img);
int untouched = 1;
CHECK(mods_exefs_data(p, base, sizeof img, 8, store_buffer) == -1, "a line below main's data keeps all out");
CHECK(n->data_bytes == 11 && q->data_bytes == 4, "the status lines count the data bytes");
CHECK(mods_exefs_data(q, base, sizeof img, 8, store_buffer) == -1,
"a data file holding fewer bytes than the status counts keeps all out");
for (size_t i = 0; i < sizeof img; i++) untouched &= img[i] == 0x5A;
CHECK(untouched, "nothing stored when a line does not fit or bytes are missing");
// every enabled mod as at start: X and Y write the same byte, and Y, the higher one, wins as its RomFS files
// would; P and Q stay out as above
memset(img, 0, sizeof img);
CHECK(EQ(mods_at(0)->name, "Y") && EQ(mods_at(4)->name, "X"), "mods_order puts Y on top and X at the bottom");
mods_exefs_apply(base, sizeof img, 8, store_buffer);
CHECK(img[0x28] == 0xAA, "two mods write the same byte: the top mod's byte stays");
CHECK(!memcmp(img + 0x10, "ABC", 3) && !img[0x30], "the other mods' data as each one stores it alone");
}
mods_probe_cache(NULL, NULL);
remove_tree(root);
}
// s ends in tail, either path separator matching either.
static int ends_with(const char *s, const char *tail) {
size_t n = strlen(s), t = strlen(tail);
@@ -124,7 +213,8 @@ int probe_mods(void) {
CHECK(EQ(cfg_get("TOTK_MODS_DISABLED"), "A"), "switching A off writes it");
CHECK(mods_changed(), "changes seen");
}
remove_tree(root);
probe_data(root);
printf("mods: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
int table = restbl_probe();
return fails || table ? 1 : 0;
}
File diff suppressed because it is too large. Load diff
+9 -1
View File
@@ -29,10 +29,18 @@ typedef struct {
enum { MOVIE_EV_INPUT, MOVIE_EV_OUTPUT, MOVIE_EV_FORMAT, MOVIE_EV_ERROR }; // DecoderEvents: EventType * per event
enum { MOVIE_TYPE_VP9 = 3, MOVIE_MODE_BUFFER = 1, MOVIE_FORMAT_NV12 = 4, MOVIE_FLAG_EOS = 4 };
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed; } MovieStats;
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed, fit_ns; } MovieStats;
// fit_ns: the part of decode_ns spent writing the NV12 frame
void movie_set_guest(const MovieGuest *g); // probe: stand-ins instead of the modules
void movie_set_clock(uint64_t (*clock)(void)); // probe: a fake clock (ns) for the pacing; NULL = real
void movie_set_pace(int on); // probe: 0 hands every decoded frame over at the next tick
int movie_stats(uint64_t obj, MovieStats *out); // counters of the decoder at guest object obj; 0 if none
int movie_quiet(uint64_t obj); // 1 when its worker is idle and cannot take more input now
// Keeping a movie's shape on an output of another shape (ov_movie.c): the picture is shrunk inside the frame, with
// black (Y 16, U = V 128) around it. movie_fit_new returns NULL when the shapes agree within 1%: copy the frame.
typedef struct MovieFit MovieFit;
MovieFit *movie_fit_new(int w, int h, unsigned out_w, unsigned out_h);
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst);
void movie_fit_free(MovieFit *f);
+98
View File
@@ -9,6 +9,7 @@
// Without the RomFS (no game data) it prints why and exits 0. TOTK_PROBE_MOVIE_SHOT=<file.ppm> saves the middle frame.
#include <stdarg.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
@@ -267,7 +268,104 @@ static int paced(uint64_t obj, const uint8_t *data, const Track *t, int limit, u
return 0;
}
// The shape-keeping resampler (ov_movie.c movie_fit_new / movie_fit_apply) on a synthetic 1920x1080 I420 picture, which
// needs no game data. The planes are gradients, which a box filter keeps linear, so every output sample has an expected
// value: luma rises left to right and top to bottom, U rises left to right, V falls top to bottom. A 16:9 output must give
// no plan; the others must give bars (Y 16, U = V 128) that are symmetric, a picture of the movie's shape once the game
// stretches the frame, and samples of the picture where the gradients say. TOTK_PROBE_MOVIE_BENCH=1 also times the frame.
#define FIT_CHECK(cond, ...) \
do { \
if (!(cond)) { \
printf("probe-movie: FAILED: "); \
printf(__VA_ARGS__); \
printf("\n"); \
rc = 1; \
goto done; \
} \
} while (0)
static int check_fit(void) {
enum { W = 1920, H = 1080, CW = W / 2, CH = H / 2 };
int rc = 0;
MovieFit *f = NULL;
uint8_t *y = malloc((size_t)W * H), *u = malloc((size_t)CW * CH), *v = malloc((size_t)CW * CH);
uint8_t *out = malloc((size_t)W * H * 3 / 2);
FIT_CHECK(y && u && v && out, "fit test buffers");
for (int j = 0; j < H; j++)
for (int i = 0; i < W; i++) y[j * W + i] = (uint8_t)(32 + 150.0 * (i + 0.5) / W + 60.0 * (j + 0.5) / H + 0.5);
for (int j = 0; j < CH; j++)
for (int i = 0; i < CW; i++) {
u[j * CW + i] = (uint8_t)(40 + 150.0 * (i + 0.5) / CW + 0.5);
v[j * CW + i] = (uint8_t)(220 - 150.0 * (j + 0.5) / CH + 0.5);
}
const uint8_t *plane[3] = {y, u, v};
const int stride[3] = {W, CW, CW};
FIT_CHECK(!movie_fit_new(W, H, 1920, 1080) && !movie_fit_new(W, H, 2560, 1440), "16:9 outputs need no plan");
static const unsigned outs[][2] = {{5120, 1440}, {3440, 1440}, {1280, 800}, {1920, 1200}};
for (int k = 0; k < 4; k++) {
unsigned ow = outs[k][0], oh = outs[k][1];
f = movie_fit_new(W, H, ow, oh);
FIT_CHECK(f, "%ux%u: no plan", ow, oh);
memset(out, 0xAA, (size_t)W * H * 3 / 2); // a byte nothing else produces: every pixel must be written
movie_fit_apply(f, plane, stride, out);
const uint8_t *uv = out + (size_t)W * H;
int l = 0, r = W - 1, t = 0, b = H - 1; // the picture's extent: where luma leaves 16, on the middle row and column
while (l < W && out[(H / 2) * W + l] == 16) l++;
while (r >= 0 && out[(H / 2) * W + r] == 16) r--;
while (t < H && out[t * W + W / 2] == 16) t++;
while (b >= 0 && out[b * W + W / 2] == 16) b--;
int pw = r - l + 1, ph = b - t + 1;
FIT_CHECK(pw > 2 && ph > 2, "%ux%u: no picture found", ow, oh);
FIT_CHECK(abs(l - (W - 1 - r)) <= 2 && abs(t - (H - 1 - b)) <= 2,
"%ux%u: bars not symmetric: left %d right %d, top %d bottom %d", ow, oh, l, W - 1 - r, t, H - 1 - b);
// On screen the picture covers pw/W of the width and ph/H of the height of an ow x oh canvas; 1 = the movie's shape.
double shape = ((double)pw / W * ow) / ((double)ph / H * oh) / ((double)W / H);
FIT_CHECK(fabs(shape - 1) < 0.01, "%ux%u: picture %dx%d shows at %.3f of the movie's shape", ow, oh, pw, ph, shape);
FIT_CHECK(l == 0 || ph == H, "%ux%u: sideways bars with a squeezed height", ow, oh);
for (int j = 0; j < H; j++) // everything outside the picture is black in luma and chroma, left and right alike
for (int i = 0; i < W; i++)
if (j < t || j > b || i < l || i > r)
FIT_CHECK(out[j * W + i] == 16, "%ux%u: luma (%d,%d) is %d, not black", ow, oh, i, j, out[j * W + i]);
for (int j = 0; j < CH; j++)
for (int i = 0; i < CW; i++) {
int outside = 2 * j + 1 < t || 2 * j > b || 2 * i + 1 < l || 2 * i > r;
const uint8_t *c = uv + (size_t)j * W + 2 * i;
if (outside) FIT_CHECK(c[0] == 128 && c[1] == 128, "%ux%u: chroma (%d,%d) is %d/%d, not black", ow, oh, i, j,
c[0], c[1]);
}
static const double fr[] = {0.1, 0.35, 0.5, 0.8, 0.95}; // samples of the picture, as fractions of it
for (int a = 0; a < 5; a++)
for (int c = 0; c < 5; c++) {
int px = (int)(pw * fr[a]), py = (int)(ph * fr[c]); // picture pixel
double sx = (px + 0.5) * W / pw, sy = (py + 0.5) * H / ph; // where it comes from in the source
double ey = 32 + 150.0 * sx / W + 60.0 * sy / H;
int got = out[(t + py) * W + l + px];
FIT_CHECK(fabs(got - ey) <= 2, "%ux%u: luma at picture (%d,%d) is %d, want %.1f", ow, oh, px, py, got, ey);
int cx = px / 2, cy = py / 2, cpw = pw / 2, cph = ph / 2;
double sux = (cx + 0.5) * CW / cpw, svy = (cy + 0.5) * CH / cph;
const uint8_t *q = uv + (size_t)(t / 2 + cy) * W + 2 * (l / 2 + cx);
double eu = 40 + 150.0 * sux / CW, ev = 220 - 150.0 * svy / CH;
FIT_CHECK(fabs(q[0] - eu) <= 3 && fabs(q[1] - ev) <= 3,
"%ux%u: chroma at picture (%d,%d) is U %d V %d, want U %.1f V %.1f", ow, oh, cx, cy, q[0], q[1], eu,
ev);
}
if (cfg_get("TOTK_PROBE_MOVIE_BENCH")) {
uint64_t t0 = plat_now_ns();
for (int i = 0; i < 100; i++) movie_fit_apply(f, plane, stride, out);
printf("probe-movie: fit %ux%u: %.2f ms per frame\n", ow, oh, (plat_now_ns() - t0) / 1e8);
}
printf("probe-movie: fit %ux%u: picture %dx%d at (%d,%d) of the frame, bars Y 16 / UV 128, gradients as expected\n",
ow, oh, pw, ph, l, t);
movie_fit_free(f);
f = NULL;
}
done:
movie_fit_free(f);
free(y), free(u), free(v), free(out);
return rc;
}
int probe_movie(void) {
if (check_fit()) return 1;
const char *dir = rt_config()->romfs;
char image[PLAT_PATH_MAX];
snprintf(image, sizeof image, "%s/romfs.bin", dir);
+19 -1
View File
@@ -43,6 +43,15 @@ _Static_assert(sizeof(nvn_mapping_t) == 0x28, "NVNmappingRequest stride");
const pool_t *nvn_pool_find(uint64_t addr, uint64_t len); // GPU-visible pool holding [addr, addr+len], NULL if none
uint64_t nvn_new_serial(void); // next tex_t / prog_t serial, from 1
// TOTK_SYNC_AHEAD (nvn_api_core.c): nvnQueuePresentTexture notes the newest fence at each present; the newest
// previous-frame value the game was told is done although it may not be (0: none). The back end's shadow copies
// wait for it or are queued for the GPU (vk_sync.c prologue), so the GPU never reads CPU writes the game made under
// that assumption too early.
void nvn_sync_ahead_present(void);
uint64_t nvn_sync_ahead_guard(void);
int vk_mem_shadow_on(void); // vk_memory.c: 1 with shadow storage on (valid once the Vulkan back end is up)
void vk_mem_shadow_fence(void); // vk_memory.c: returns once every shadow sync that read the old guard is done
void gfx_replay_wait_value(uint64_t v); // vk_backend.c: the replay thread submitted value v
uint64_t nvn_fence_last(void); // fence value of the newest submit (0 before the first)
int nvn_depth_minus_w(void); // 1 after DeviceSetDepthMode(NEAR_IS_MINUS_W), else 0
void nvn_guest_written(uint64_t addr, uint64_t size); // the host or the game wrote pool storage: back end re-reads
@@ -75,7 +84,8 @@ uint64_t nvn_tex_view_offset(const tex_t *t, const view_t *v);
// A command handle (what QueueSubmitCommands receives) points at a host copy of one recording: a flat sequence of
// 8-byte-aligned records {op_hdr, payload}, OP_BEGIN first and OP_END last. Called recordings are inlined, so a copy
// holds no OP_CALL or OP_JUMP. Texture pointers in payloads (p_set_rt, p_copy_b2t, p_copy_t2t, p_clear_tex) are
// immutable host copies that live for the run, one per tex_t.serial. Deferred binds (p_bind_handles.src != 0) hold the
// immutable host copies ("pins"), one per tex_t.serial, that live while a recording copy names them and a while after
// (nvn_pins_sweep). Deferred binds (p_bind_handles.src != 0) hold the
// handles read at submit (nvn_submit_prepare). Payloads of the ops nvn_obj.h does not define:
typedef struct { volatile uint32_t *val; uint32_t mode, value; } p_signal_event; // OP_SIGNAL_EVENT; mode: NVNeventSignalMode
typedef struct { uint64_t type, addr; } p_report_counter; // OP_REPORT_COUNTER: NVNcounterType, NVNcounterData address
@@ -99,6 +109,14 @@ int nvn_handle_live(uint64_t handle); // 1 while the handle
// not added again)
uint64_t nvn_handle_bytes(uint64_t handle); // bytes of the copy, OP_BEGIN to OP_END; 0 if not live
uint64_t nvn_submit_count(void); // QueueSubmitCommands calls so far
// Submits a texture pin outlives its last tex_pin and the release of the last recording copy naming it: about 20 s at
// one submit a frame (MacNativeGames frees after two 600-submit windows).
#define NVN_PIN_AGE 1200
// Frees the texture pins no recording copy has named for NVN_PIN_AGE submits (nvn_api_cmd.c, "texture pins"), except keep[0..nkeep):
// the replay's current render targets, which carry over to the next submission. Called between submissions by the
// thread that replays them; now = nvn_submit_count().
void nvn_pins_sweep(uint64_t now, const tex_t *const *keep, int nkeep);
void nvn_pins_stats(uint64_t *live, uint64_t *freed); // pins that exist, pins freed so far
const char *nvn_op_name(uint32_t op); // "DRAW_IDX", ...; "?" past OP_COUNT_
// MemoryPoolInvalidateMappedRange: the CPU is about to read pool storage the GPU wrote. Weak no-op in nvn_ext.c; a back
// end that renders into host-side copies defines it and writes them back.
+306 -27
View File
@@ -4,7 +4,7 @@
// games/totk/third_party/d2r_gfx/gfx/nvn/hle_nvn.c @ b75be8cf (MIT, Copyright (c) 2026 jhaizhou; origin d2r-mobile),
// lines 647-1062, 1261-1286, 1486-1760 and 1975-1990, and the *Deferred and memory-callback entry points of
// totk_nvn_ext.c in the same directory. Metal, mach, the translation thread, Apple diagnostics, the per-frame
// statistics and the long-session sweeps (copy marks in control memory, texture pin sweep) dropped.
// statistics and the copy marks in control memory dropped; the texture pin sweep replaced by counting (texture pins).
//
// Records are written into a growable host buffer per command buffer, never into the game's command memory: games hand
// the same command memory to several command buffers recording at once (budgeted for real NVN encodings), and our
@@ -17,6 +17,7 @@
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
@@ -140,10 +141,14 @@ static void pool_drain(void) {
// system-managed page file, so the same request succeeds a moment later, and a stall of some frames is better than
// ending the game. Frees the pooled buffers first. NULL only after about 30 s without memory; the caller ends the game.
// Never called with a lock held.
static void big_drain(void);
static void *realloc_wait(void *old, size_t n, const char *what) {
void *p = realloc(old, n);
if (p) return p;
pool_drain();
big_drain();
rt_oom_request(); // the Vulkan back end gives memory back at its next submit (oom.c)
static uint32_t reports;
int report = __atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8;
if (report) {
@@ -235,30 +240,169 @@ static void copy_view(view_t *dst, uint8_t *has, const NVNtextureView *v) {
// ---------- texture pins
// Recordings refer to textures by pointer, but the game may finalize or reuse an NVNtexture after recording and before
// the submit (NVN itself captured only GPU state). Records therefore point at an immutable host copy per texture
// initialization (tex_t.serial). The copies are kept for the whole run (MacNativeGames sweeps them for long sessions).
static PlatMutex pin_lock = PLAT_MUTEX_INIT;
static nvn_hmap pins = {.klen = 8}; // serial -> tex_t copy
// initialization (tex_t.serial). The game initializes its transient targets again every frame, about 29 new serials a
// frame on 1.4.3, so copies kept for the run grew the heap by about 44 MiB per minute of play, standing still too, until
// the commit limit ended the game after a few hours. A copy is freed once nothing can read it any more:
// - refs counts the recording copies whose ops name it: live ones, released ones a queued submission still reads, and
// per-submission clones (pin_refs, at flatten_recording and wherever such a copy is freed or cloned);
// - a recording in progress holds pins no copy counts yet, through its own tex_pin calls and through ops inlined from a
// copy that was released since: a pin goes only NVN_PIN_AGE submits after its last tex_pin and after refs fell to 0;
// - the replay's current render targets carry over to later submissions and are compared by pointer (vk_exec.c set_rt),
// so a freed address reused by a new pin would read as the same target: nvn_pins_sweep runs between submissions on
// the thread that replays them and keeps the pins it is given.
// MacNativeGames hit the same growth (3.16 million copies, 405 MB after 27 minutes) and marks pins by walking every live
// recording each window (hle_nvn.c pin_scan); counting at copy and release walks each copy's ops once more, when it is
// freed. TOTK_PIN_SWEEP=0 keeps every copy.
typedef struct {
tex_t t; // first: ops hold pins as tex_t pointers
uint64_t last_use; // nsubmits at the last tex_pin or the last drop of refs
uint32_t refs;
uint8_t queued; // in pin_q
} pin_t;
_Static_assert(offsetof(pin_t, t) == 0, "ops hold pins as tex_t pointers");
typedef struct { pin_t *p; uint64_t at; } pin_due;
static PlatMutex pin_lock = PLAT_MUTEX_INIT; // lock order: copy_lock, then pin_lock
static nvn_hmap pins = {.klen = 8}; // serial -> pin_t
static pin_due *pin_q; // pins with no counted copy, oldest first: [pin_qh, pin_qn)
static size_t pin_qh, pin_qn, pin_qcap;
static uint64_t pins_live, pins_freed;
static uint64_t nsubmits; // QueueSubmitCommands calls so far
static void pin_queue(pin_t *p, uint64_t at) { // pin_lock held
if (p->queued) return;
if (pin_qh && pin_qh * 2 >= pin_qn) {
memmove(pin_q, pin_q + pin_qh, sizeof *pin_q * (pin_qn - pin_qh));
pin_qn -= pin_qh, pin_qh = 0;
}
if (pin_qn == pin_qcap) {
pin_qcap = pin_qcap ? pin_qcap * 2 : 4096;
if (!(pin_q = realloc(pin_q, sizeof *pin_q * pin_qcap))) rt_fatal("nvn: out of memory (texture pin queue)");
}
pin_q[pin_qn++] = (pin_due){p, at};
p->queued = 1;
}
static const tex_t *tex_pin(const NVNtexture *nt) {
const tex_t *t = (const tex_t *)nt;
if (!t) return NULL;
uint64_t serial = t->serial;
if (!serial) rt_fatal("nvn: a recording references the uninitialized texture object %p", (const void *)t);
plat_mutex_lock_shared(&pin_lock);
const tex_t *r = nvn_hm_get(&pins, &serial);
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
plat_mutex_lock_shared(&pin_lock); // nvn_pins_sweep frees under the exclusive lock
pin_t *r = nvn_hm_get(&pins, &serial);
if (r) __atomic_store_n(&r->last_use, now, __ATOMIC_RELAXED);
plat_mutex_unlock_shared(&pin_lock);
if (r) return r;
if (r) return &r->t;
plat_mutex_lock(&pin_lock);
r = nvn_hm_get(&pins, &serial);
if (!r) {
tex_t *copy = malloc(sizeof *copy);
if (!copy) rt_fatal("nvn: texture pin copy");
*copy = *t;
nvn_hm_put(&pins, &serial, copy);
r = copy;
if (!(r = malloc(sizeof *r))) rt_fatal("nvn: texture pin copy");
*r = (pin_t){.t = *t};
nvn_hm_put(&pins, &serial, r);
pins_live++;
pin_queue(r, now); // freed like any other if no copy ever names it
}
__atomic_store_n(&r->last_use, now, __ATOMIC_RELAXED);
plat_mutex_unlock(&pin_lock);
return &r->t;
}
// The pins an op's payload names, into out (at most 9); returns how many.
static int op_pins(uint32_t op, const void *a, const tex_t *out[9]) {
int k = 0;
switch (op) {
case OP_SET_RT: {
const p_set_rt *r = a;
int n = r->n < 0 ? 0 : r->n > 8 ? 8 : r->n;
for (int i = 0; i < n; i++)
if (r->color[i]) out[k++] = r->color[i];
if (r->depth) out[k++] = r->depth;
break;
}
case OP_COPY_B2T: case OP_COPY_T2B:
if (((const p_copy_b2t *)a)->tex) out[k++] = ((const p_copy_b2t *)a)->tex;
break;
case OP_CLEAR_TEX:
if (((const p_clear_tex *)a)->tex) out[k++] = ((const p_clear_tex *)a)->tex;
break;
case OP_COPY_T2T: {
const p_copy_t2t *c = a;
if (c->src) out[k++] = c->src;
if (c->dst) out[k++] = c->dst;
break;
}
default: break;
}
return k;
}
static void pin_ref(const tex_t *t, int delta, uint64_t now) {
pin_t *p = (pin_t *)(uintptr_t)t;
if (delta > 0) {
__atomic_add_fetch(&p->refs, 1, __ATOMIC_RELAXED);
return;
}
__atomic_store_n(&p->last_use, now, __ATOMIC_RELAXED);
if (__atomic_sub_fetch(&p->refs, 1, __ATOMIC_ACQ_REL)) return;
plat_mutex_lock(&pin_lock);
pin_queue(p, now);
plat_mutex_unlock(&pin_lock);
}
// Counts (delta 1) or uncounts (-1) the pins named by the copy at handle, OP_BEGIN to OP_END; returns how many.
static uint32_t pin_refs(const uint8_t *copy, int delta) {
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
uint32_t n = 0;
for (const op_hdr *h = nvn_op_first((uintptr_t)copy); h->op != OP_END; h = nvn_op_next(h)) {
const tex_t *p[9];
int k = op_pins(h->op, nvn_op_payload(h), p);
for (int i = 0; i < k; i++) pin_ref(p[i], delta, now);
n += (uint32_t)k;
}
return n;
}
void nvn_pins_sweep(uint64_t now, const tex_t *const *keep, int nkeep) {
if (now < NVN_PIN_AGE) return;
static int on = -1;
if (on < 0) {
const char *s = cfg_get("TOTK_PIN_SWEEP");
on = !(s && !strcmp(s, "0"));
if (!on) rt_log("nvn: TOTK_PIN_SWEEP=0: texture pin copies are kept for the run (the heap grows in long sessions)");
}
if (!on) return;
static uint64_t logged;
plat_mutex_lock(&pin_lock);
for (int budget = 1024; pin_qh < pin_qn && budget; budget--) {
pin_due d = pin_q[pin_qh];
if (d.at > now - NVN_PIN_AGE) break;
pin_qh++;
pin_t *p = d.p;
p->queued = 0;
if (__atomic_load_n(&p->refs, __ATOMIC_ACQUIRE)) continue; // queued again when the count falls to 0
uint64_t last = __atomic_load_n(&p->last_use, __ATOMIC_RELAXED);
int kept = 0;
for (int i = 0; i < nkeep; i++) kept |= keep[i] == &p->t;
if (kept || last > now - NVN_PIN_AGE) {
pin_queue(p, kept ? now : last);
continue;
}
nvn_hm_del(&pins, &p->t.serial);
free(p);
pins_live--, pins_freed++;
}
if (now / 7200 != logged) {
logged = now / 7200;
rt_log("nvn: texture pins at submit %llu: %llu live, %llu waiting, %llu freed so far", (unsigned long long)now,
(unsigned long long)pins_live, (unsigned long long)(pin_qn - pin_qh), (unsigned long long)pins_freed);
}
plat_mutex_unlock(&pin_lock);
return r;
}
void nvn_pins_stats(uint64_t *live, uint64_t *freed) {
plat_mutex_lock_shared(&pin_lock);
*live = pins_live, *freed = pins_freed;
plat_mutex_unlock_shared(&pin_lock);
}
// ---------- recording copies
@@ -266,14 +410,17 @@ static const tex_t *tex_pin(const NVNtexture *nt) {
// adding control memory again releases the copies recorded there without walking every live copy. 4 KiB pages cost
// about 500 lookups per AddControlMemory under the exclusive lock (1.4.3 field: 53 calls a frame, 2 MiB ranges, 3.7
// copies per page), and the recording threads waited on it.
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred; } copy_prefix; // deferred: 0 unknown, 1 none, 2 some
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs; pin_v: those pins, in
// record order, or NULL (then pin_refs walks the records); cap: bytes of the block from the large-block cache
// (copy_block), 0 for a plain heap block
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins, cap; const tex_t **pin_v; } copy_prefix;
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
static PlatMutex copy_lock = PLAT_MUTEX_INIT; // guards the two maps, the counters and every prefix
static nvn_hmap copy_pages = {.klen = 8}; // page -> copy_bucket
static nvn_hmap copy_live = {.klen = 8}; // handle -> copy_prefix of a live copy
static uint64_t ncopies, copy_bytes, copies_made, nsubmits;
static uint64_t ncopies, copy_bytes, copies_made;
// Asynchronous replay: copies released by the game, and per-submission clones, that a queued submission still reads.
// Freed once gfx_replayed_submits() reaches their used_at. Empty with synchronous replay.
static copy_prefix **zombies;
@@ -288,10 +435,117 @@ static void zombie_add(copy_prefix *pf) { // copy_lock held
zombies[nzombies++] = pf;
}
static void zombie_sweep(uint64_t replayed) { // copy_lock held
// Blocks of large copies are kept for the next ones. 1.4.3 makes two copies of 0.5-2 MiB a frame (the per-frame
// recordings that inline many others); above the heap's direct-allocation threshold (about 512 KiB) every malloc mapped
// fresh zero-filled pages and every free unmapped them, so the copy ran at half the speed of a small one and the free
// held copy_lock for tens of microseconds. A block is reused for a copy that fills at least half of it.
#define BIG_MIN ((size_t)256 << 10) // smaller blocks come from the heap and go back to it
#define BIG_MAX ((size_t)16 << 20) // a larger block is never kept
#define BIG_SLOTS 8
static PlatMutex big_lock = PLAT_MUTEX_INIT; // lock order: copy_lock, then big_lock
static struct { copy_prefix *p; size_t cap; } big[BIG_SLOTS];
static uint32_t nbig;
static void big_drain(void) {
plat_mutex_lock(&big_lock);
while (nbig) free(big[--nbig].p);
plat_mutex_unlock(&big_lock);
}
// A block for a copy of n bytes after its prefix; prefix.cap says where it goes back (copy_block_free). wait: a failed
// allocation waits for memory (realloc_wait), which is never done with a lock held; else it ends the game at once.
static copy_prefix *copy_block(size_t n, int wait, const char *what) {
size_t total = sizeof(copy_prefix) + n, cap = 0;
copy_prefix *pf = NULL;
if (total >= BIG_MIN && total <= BIG_MAX) {
plat_mutex_lock(&big_lock);
uint32_t best = BIG_SLOTS;
for (uint32_t i = 0; i < nbig; i++)
if (big[i].cap >= total && big[i].cap / 2 <= total && (best == BIG_SLOTS || big[i].cap < big[best].cap))
best = i;
if (best < BIG_SLOTS) {
pf = big[best].p;
cap = big[best].cap;
big[best] = big[--nbig];
}
plat_mutex_unlock(&big_lock);
if (!pf) cap = (total + total / 8 + 0xffff) & ~(size_t)0xffff; // room for the next frame's slightly larger one
}
if (!pf) pf = wait ? realloc_wait(NULL, cap ? cap : total, what) : malloc(cap ? cap : total);
if (!pf) rt_fatal("nvn: %s allocation (%zu bytes)", what, cap ? cap : total);
pf->cap = cap;
return pf;
}
static void copy_block_free(copy_prefix *pf) {
size_t cap = pf->cap;
if (cap) {
plat_mutex_lock(&big_lock);
if (nbig < BIG_SLOTS) {
big[nbig].p = pf;
big[nbig++].cap = cap;
pf = NULL;
} else {
uint32_t small = 0;
for (uint32_t i = 1; i < nbig; i++)
if (big[i].cap < big[small].cap) small = i;
if (big[small].cap < cap) {
copy_prefix *drop = big[small].p;
big[small].p = pf;
big[small].cap = cap;
pf = drop;
}
}
plat_mutex_unlock(&big_lock);
}
free(pf);
}
// Counts (delta 1) or uncounts (-1) the pins a copy names: from its list, without walking its records. A copy of a
// per-frame recording is 0.5-2 MiB of records naming a few pins; walking them again at every free cost about 40 us.
static void copy_pins_ref(const copy_prefix *pf, int delta) {
if (!pf->pins) return;
if (!pf->pin_v) {
pin_refs((const uint8_t *)(pf + 1), delta);
return;
}
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
for (uint64_t i = 0; i < pf->pins; i++) pin_ref(pf->pin_v[i], delta, now);
}
// A copy in no map and no zombie list: nothing else can reach it, so copy_lock need not be held.
static void copy_free(copy_prefix *pf) {
copy_pins_ref(pf, -1);
free((void *)pf->pin_v);
copy_block_free(pf);
}
// Copies retired under copy_lock and freed after it is released: the frees, the unpinning walk over their records and
// the large blocks kept the lock exclusive for most of release_copies, while the game's other recording threads waited
// for it in copy_touch (1.4.3 town: three threads, 0.6 ms a frame of waiting).
typedef struct { copy_prefix **v; uint32_t n, cap; } dead_list;
static __thread dead_list dead;
static void dead_add(copy_prefix *pf) {
if (dead.n == dead.cap) {
uint32_t cap = dead.cap ? dead.cap * 2 : 256;
copy_prefix **v = realloc(dead.v, sizeof *v * cap);
if (!v) { copy_free(pf); return; } // out of memory: free it here, under the lock, as before
dead.v = v;
dead.cap = cap;
}
dead.v[dead.n++] = pf;
}
static void dead_free(void) { // copy_lock not held
for (uint32_t i = 0; i < dead.n; i++) copy_free(dead.v[i]);
dead.n = 0;
}
static void zombie_sweep(uint64_t replayed) { // copy_lock held; the swept copies go to the dead list
for (uint32_t i = 0; i < nzombies;) {
if (zombies[i]->used_at <= replayed) {
free(zombies[i]);
dead_add(zombies[i]);
zombies[i] = zombies[--nzombies];
} else {
i++;
@@ -331,25 +585,44 @@ static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
}
// The recording [p, p + n), OP_BEGIN to OP_END (the recording buffer is one block: recording emits no OP_JUMP), copied
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
// into one host block (copy_block: its exact size, or a kept large block). One allocation and one copy: growing the
// copy record by record cost the game's main thread about 1 ms a frame in reallocations and small copies (1.4.3,
// field). The pins the records name are counted and listed (copy_prefix.pin_v) on the way.
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
if (!out) rt_fatal("nvn: command copy allocation");
copy_prefix *block = copy_block(n, 1, "command copy");
uint8_t *out = (uint8_t *)block;
memcpy(out + sizeof(copy_prefix), p, n);
static __thread struct { const tex_t **v; uint64_t cap; } named; // the pins of this recording, reused
uint64_t pins_named = 0;
int listed = 1; // 0 after a failed growth: the copy goes without a list
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
op_hdr h;
memcpy(&h, p + at, sizeof h);
if (h.size < 8 || (h.size & 7) || h.op >= OP_COUNT_ || h.op == OP_JUMP || h.op == OP_CALL || at + h.size > n ||
(!at && h.op != OP_BEGIN))
rt_fatal("nvn: corrupt recording at %p (op %u size %u)", (const void *)(p + at), h.op, h.size);
const tex_t *pinned[9];
int k = op_pins(h.op, out + sizeof(copy_prefix) + at + 8, pinned);
for (int i = 0; i < k; i++) pin_ref(pinned[i], 1, 0);
if (k && listed && pins_named + (uint64_t)k > named.cap) {
uint64_t cap = named.cap ? named.cap * 2 : 64;
while (cap < pins_named + (uint64_t)k) cap *= 2;
const tex_t **v = realloc((void *)named.v, sizeof *v * cap);
if (v) named.v = v, named.cap = cap;
else listed = 0;
}
if (k && listed) memcpy((void *)(named.v + pins_named), pinned, sizeof *pinned * (size_t)k);
pins_named += (uint64_t)k;
at += h.size;
if (h.op == OP_END) {
if (at != n) rt_fatal("nvn: recording at %p ends at %zu of %zu bytes", (const void *)p, at, n);
break;
}
}
*(copy_prefix *)out = (copy_prefix){COPY_MAGIC, n, 0, 0};
const tex_t **pin_v = NULL;
if (pins_named && listed && (pin_v = malloc(sizeof *pin_v * pins_named)))
memcpy((void *)pin_v, (const void *)named.v, sizeof *pin_v * pins_named);
*block = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named, .cap = block->cap, .pin_v = pin_v};
return out + sizeof(copy_prefix);
}
@@ -399,7 +672,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
copy_bytes -= pf->bytes;
pf->magic = 0;
if (pf->used_at > replayed) zombie_add(pf); // a queued submission still reads it
else free(pf);
else dead_add(pf);
b->v[i] = b->v[--b->n];
}
if (!b->n) {
@@ -409,6 +682,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
}
}
plat_mutex_unlock(&copy_lock);
dead_free();
}
// ---------- CommandBuffer: object and memory
@@ -1031,10 +1305,14 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
}
out[i] = handles[i];
if (pf->used_at > replayed && pf->used_at != now && pf->deferred != 1) {
size_t sz = sizeof *pf + pf->bytes;
copy_prefix *cl = malloc(sz);
if (!cl) rt_fatal("nvn: out of memory (recording clone, %zu bytes)", sz);
memcpy(cl, pf, sz);
copy_prefix *cl = copy_block(pf->bytes, 0, "recording clone"); // copy_lock held
size_t cap = cl->cap;
memcpy(cl, pf, sizeof *pf + pf->bytes);
cl->cap = cap;
cl->pin_v = NULL;
if (pf->pin_v && (cl->pin_v = malloc(sizeof *cl->pin_v * pf->pins)))
memcpy((void *)cl->pin_v, (const void *)pf->pin_v, sizeof *cl->pin_v * pf->pins);
copy_pins_ref(cl, 1);
cl->magic = 0; // not a live handle: the game never sees it
cl->used_at = now;
zombie_add(cl);
@@ -1045,6 +1323,7 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
}
}
plat_mutex_unlock(&copy_lock);
dead_free();
for (int i = 0; i < n; i++) {
int nd = resolve_deferred((const uint8_t *)(uintptr_t)out[i], 0);
if (out[i] == handles[i]) ((copy_prefix *)(uintptr_t)out[i] - 1)->deferred = nd ? 2 : 1;
+67 -3
View File
@@ -471,6 +471,49 @@ NVN_API(nvnQueueFenceSync) {
s->fenced = 1;
s->fence = nvn_fence_last();
}
// TOTK_SYNC_AHEAD=1: one more frame in flight. Each frame the game waits (one nvnSyncWait, frame_wait_lr) for
// the frame it presented last; on the Switch that work is long done by then, here it is still in the replay and on the
// GPU (about 11 ms from the submit), and the wait sets the frame time. With the switch on, that wait, and only that
// one, waits for the frame before it instead. What the game reads back of GPU results after the wait can then be a
// frame old. The game's CPU writes to memory the GPU still reads reach the GPU only through shadow copies, which are
// queued for the GPU or wait (nvn_sync_ahead_guard). The wait still lasts until the replay thread submitted the asked
// value, so no copy of that value itself picks up those writes. pa_fence: the newest fence at the latest present and
// the one before, present thread only for writes. pa_guard: the newest value the game was told is done early.
static uint64_t pa_fence[2], pa_guard;
// The return address of that wait. 1.2.1: 0x7000b23dfc. 1.4.3: 0x70002db99c, the second of the two nvnSyncWait calls
// in f_70002db8ac; like on 1.2.1 its sync comes from a two-entry ring indexed per frame, and the first call waits on
// one object's sync behind a mode check. 0: no site known, the switch stays off.
static uint64_t frame_wait_lr(void) {
return rt_game_version == 121 ? 0x7000b23dfcull : rt_game_version == 143 ? 0x70002db99cull : 0;
}
static int sync_ahead(void) {
static int on = -1;
if (on < 0) {
on = cfg_bool("TOTK_SYNC_AHEAD");
// Without shadow storage the GPU reads the game's own memory, which the game writes ahead: nothing guards
// that. The Vulkan back end sets the mode while it starts, before the game's first present or wait; the null
// back end never does.
if (on && !vk_mem_shadow_on()) {
rt_log("nvn: TOTK_SYNC_AHEAD needs shadow storage (TOTK_SHADOW=1 on native Windows); "
"the game waits for the GPU as before");
on = 0;
}
if (on && !frame_wait_lr()) {
rt_log("nvn: TOTK_SYNC_AHEAD: no frame wait site is known for this game version; the game waits for the "
"GPU as before");
on = 0;
}
if (on) rt_log("nvn: TOTK_SYNC_AHEAD: a wait for the frame presented last waits for the frame before it");
}
return on;
}
void nvn_sync_ahead_present(void) {
if (!sync_ahead()) return;
__atomic_store_n(&pa_fence[1], __atomic_load_n(&pa_fence[0], __ATOMIC_RELAXED), __ATOMIC_RELEASE);
__atomic_store_n(&pa_fence[0], nvn_fence_last(), __ATOMIC_RELEASE);
}
uint64_t nvn_sync_ahead_guard(void) { return __atomic_load_n(&pa_guard, __ATOMIC_ACQUIRE); }
// NVNsyncWaitResult nvnSyncWait(const NVNsync *s, uint64_t timeout_ns): a sync never fenced reports a timeout at once;
// a fenced one is released only after the back end completed that fence value
NVN_API(nvnSyncWait) {
@@ -478,13 +521,34 @@ NVN_API(nvnSyncWait) {
uint64_t timeout = arg_x(c, 1);
if (!s->fenced) { ret_x(c, NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED); return; }
const GfxBackend *b = gfx_backend();
if (b->fence_done() >= s->fence) { b->cpu_synced(s->fence); ret_x(c, NVN_SYNC_WAIT_RESULT_ALREADY_SIGNALED); return; }
uint64_t want = s->fence;
// Only the game's per-frame wait lags, and not a poll (timeout 0).
if (timeout && c->x[30] == frame_wait_lr() && sync_ahead() && b->fence_done() < want) {
uint64_t p0 = __atomic_load_n(&pa_fence[0], __ATOMIC_ACQUIRE);
uint64_t p1 = __atomic_load_n(&pa_fence[1], __ATOMIC_ACQUIRE);
if (want <= p0 && want > p1) want = p1;
}
if (want != s->fence) { // the replay of the asked value is done first: the guard then covers every copy of it
{
NVN_BLOCKING();
gfx_replay_wait_value(s->fence);
}
uint64_t g = __atomic_load_n(&pa_guard, __ATOMIC_RELAXED);
while (s->fence > g &&
!__atomic_compare_exchange_n(&pa_guard, &g, s->fence, 1, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {}
vk_mem_shadow_fence(); // a shadow sync that read the old guard finishes before the game writes again
}
if (b->fence_done() >= want) {
b->cpu_synced(want == s->fence ? s->fence : b->fence_done());
ret_x(c, NVN_SYNC_WAIT_RESULT_ALREADY_SIGNALED);
return;
}
int ok;
{
NVN_BLOCKING();
ok = b->fence_wait(s->fence, timeout);
ok = b->fence_wait(want, timeout);
}
if (ok) b->cpu_synced(s->fence);
if (ok) b->cpu_synced(want == s->fence ? s->fence : b->fence_done());
ret_x(c, ok ? NVN_SYNC_WAIT_RESULT_CONDITION_SATISFIED : NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED);
}
// void nvnEventBuilderSetDefaults(NVNeventBuilder *b)
+13 -5
View File
@@ -11,6 +11,7 @@
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
@@ -667,8 +668,12 @@ static void sleep_until(uint64_t deadline) {
}
// NVNwindowAcquireTextureResult nvnWindowAcquireTexture(NVNwindow *w, NVNsync *s, int *index): blocks here, not in the
// present, until the image's previous present plus one period has passed (the game often presents with a lock held).
// That is the time the next frame goes to the display, so the image is free as soon as that time is reached.
// present, until the image is free (the game often presents with a lock held). Without the frame queue that is the
// image's previous present plus one period, when the next frame goes to the display, so a frame gets exactly one period
// from acquire to present. Its present is a copy already on the queue by its display time, so with the frame queue
// (TOTK_FRAME_QUEUE=1) the image is free from that time on: the game draws the next frame while the previous one waits
// for its turn, and a frame that takes up to one more period still makes its slot, for up to one frame more latency.
// Town at 4K with DLAA at 60 on an RTX 5070: frames over 17.5 ms 1-8 per 30 s with the queue against 25-33 without.
NVN_API(nvnWindowAcquireTexture) {
win_t *o = arg_p(c, 0);
sync_t *s = arg_p(c, 1);
@@ -693,12 +698,14 @@ NVN_API(nvnWindowAcquireTexture) {
// void nvnQueuePresentTexture(NVNqueue *q, NVNwindow *w, int index): queues the copy and returns at once. The display
// time comes from the pacer: the back end's present thread holds the host present until then (back-edge pacing, as the
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later the
// image is free for the next acquire. Added latency: the slack of the frame (period minus its work), as on the Switch.
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later (at that
// time with the frame queue) the image is free for the next acquire. Added latency: the slack of the frame (period minus
// its work), as on the Switch, plus up to a period with the frame queue.
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);
@@ -706,9 +713,10 @@ NVN_API(nvnQueuePresentTexture) {
const tex_t *t = (const tex_t *)o->tex[idx];
uint64_t period = present_period(o), now = kern_now_ns();
uint64_t slot = now + pace(&o->pace, now, period);
nvn_sync_ahead_present();
gfx_backend()->present(t, o->crop, slot);
o->ticket[idx] = o->presents;
o->avail[idx] = slot + period;
o->avail[idx] = cfg_bool("TOTK_FRAME_QUEUE") ? slot : slot + period;
frame_step_present(slot, o->interval); // frame_rate.c: the real-time step follows the display times
rt_frame_presented(t, o->presents);
}
+50
View File
@@ -694,6 +694,55 @@ static void parse_all(uint64_t h) {
n_ops = OP_COUNT_ - OP_SIGNAL_EVENT;
}
// A recording naming tex3 as its render target; *pin: the texture pin in its SET_RT payload.
static uint64_t record_target(uint8_t *ctl, const tex_t **pin) {
const NVNtexture *colors[1] = {&tex3};
begin(&cmd2, ctl, 0x3a000);
CALL(nvnCommandBufferSetRenderTargets, U(&cmd2), 1, U(colors), 0, 0, 0);
uint64_t h = CALL(nvnCommandBufferEndRecording, U(&cmd2));
*pin = NULL;
for (const op_hdr *o = nvn_op_first(h); o->op != OP_END; o = nvn_op_next(o))
if (o->op == OP_SET_RT) *pin = ((const p_set_rt *)nvn_op_payload(o))->color[0];
return h;
}
// Texture pins are freed NVN_PIN_AGE submits after the last recording copy naming them is released, unless the replay
// keeps them as its current render targets; a texture named again gets a new pin.
static void probe_pins(void) {
uint8_t *ctl = mem_kernel_alloc(0x1000, 16);
set_tex(NVN_FORMAT_RGBA8, 16, 16, 1, 0x20000);
CHECK((uint8_t)CALL(nvnTextureInitialize, U(&tex3), U(&texb)) == 1, "pin probe texture");
uint64_t serial = ((const tex_t *)(const void *)&tex3)->serial;
uint64_t live0, freed0, live, freed;
nvn_pins_stats(&live0, &freed0);
const tex_t *pin;
uint64_t h = record_target(ctl, &pin);
nvn_pins_stats(&live, &freed);
CHECK(pin && pin->serial == serial && live == live0 + 1, "a recording naming a new texture makes one pin");
uint64_t s = nvn_submit_count();
nvn_pins_sweep(s + 10 * NVN_PIN_AGE, NULL, 0);
nvn_pins_stats(&live, &freed);
CHECK(live == live0 + 1 && freed == freed0 && nvn_handle_live(h), "a pin a live recording names stays");
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctl), 0x1000); // retires h: the pin has no copy left
CHECK(!nvn_handle_live(h), "the pin's recording is retired");
nvn_pins_sweep(s + NVN_PIN_AGE - 1, NULL, 0);
nvn_pins_stats(&live, &freed);
CHECK(live == live0 + 1 && freed == freed0, "a pin stays NVN_PIN_AGE submits after its last copy went");
nvn_pins_sweep(s + NVN_PIN_AGE, &pin, 1);
nvn_pins_stats(&live, &freed);
CHECK(live == live0 + 1 && freed == freed0, "the replay's current render target is kept");
nvn_pins_sweep(s + 2 * NVN_PIN_AGE, NULL, 0);
nvn_pins_stats(&live, &freed);
CHECK(live == live0 && freed == freed0 + 1, "an unnamed, unkept pin is freed (live %llu of %llu, freed %llu of %llu)",
(unsigned long long)live, (unsigned long long)live0, (unsigned long long)freed, (unsigned long long)freed0 + 1);
h = record_target(ctl, &pin);
nvn_pins_stats(&live, &freed);
CHECK(pin && pin->serial == serial && live == live0 + 1, "naming the texture again makes a new pin");
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctl), 0x1000);
CALL(nvnCommandBufferFinalize, U(&cmd2));
CALL(nvnTextureFinalize, U(&tex3));
}
static void probe_commands(void) {
ctlmem1 = mem_kernel_alloc(0x1000, 16);
ctlmem2 = mem_kernel_alloc(0x1000, 16);
@@ -722,6 +771,7 @@ static void probe_commands(void) {
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctlmem2), 0x1000);
CHECK(!nvn_handle_live(h2) && nvn_handle_live(h), "reusing control memory retires only the handles recorded in it");
CALL(nvnCommandBufferFinalize, U(&cmd2));
probe_pins();
n_objects += 5;
}
+106
View File
@@ -0,0 +1,106 @@
// Short out-of-memory spells: a failed allocation waits for memory instead of ending the game.
//
// Windows refuses commit beyond the current limit while it grows a system-managed page file, and on some drivers device
// memory counts as process commit (vk_memory.c vk_mem_host_probe), so for a moment every allocation of the process can
// fail and the same request succeeds a little later. The failure sites (memory.c commit, vk_memory.c vk_staging,
// vk_texture.c create_tex, vk_exec_draw.c transient chunks, nvn_api_cmd.c realloc_wait) ask the Vulkan back end to give
// memory back (rt_oom_request: released textures and headroom blocks, at its next submit, where its locks are free) and
// retry for up to OOM_WAIT_MS before they end the game.
//
// TOTK_TEST_OOM=<site>:<n>[,<site>:<n>...] (sites commit, staging, image, chunk) fails the first n attempts at a site, so
// the wait can be tested without running out of memory; the probes arm it with rt_oom_force.
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "platform/platform.h"
#define OOM_WAIT_MS 30000u
#define OOM_STEP_MS 10u
#define OOM_REPORTS 8
#define OOM_REQUEST_EVERY 100 // retries between two release requests: a second, since a release only frees what is old enough
volatile int32_t rt_oom_req;
static const char *const site_name[RT_OOM_SITES] = {"commit", "staging", "image", "chunk"};
static volatile int32_t force_left[RT_OOM_SITES];
static volatile int32_t forced_parsed;
static PlatOnce force_once = PLAT_ONCE_INIT;
static void force_parse(void *arg) {
(void)arg;
const char *s = cfg_get("TOTK_TEST_OOM");
while (s && *s) {
const char *colon = strchr(s, ':'), *end = strchr(s, ',');
if (!end) end = s + strlen(s);
if (colon && colon < end) {
for (int i = 0; i < RT_OOM_SITES; i++)
if ((size_t)(colon - s) == strlen(site_name[i]) && !strncmp(s, site_name[i], (size_t)(colon - s))) {
int n = atoi(colon + 1);
__atomic_store_n(&force_left[i], n > 0 ? n : 0, __ATOMIC_RELAXED);
rt_log("oom: TOTK_TEST_OOM fails the first %d %s attempts", n, site_name[i]);
}
}
s = *end ? end + 1 : end;
}
__atomic_store_n(&forced_parsed, 1, __ATOMIC_RELEASE);
}
int rt_oom_forced(int site) {
if (!__atomic_load_n(&forced_parsed, __ATOMIC_ACQUIRE)) plat_once(&force_once, force_parse, NULL);
if (__atomic_load_n(&force_left[site], __ATOMIC_RELAXED) <= 0) return 0;
return plat_dec32(&force_left[site]) >= 0;
}
void rt_oom_force(int site, int n) {
if (!__atomic_load_n(&forced_parsed, __ATOMIC_ACQUIRE)) plat_once(&force_once, force_parse, NULL);
__atomic_store_n(&force_left[site], n, __ATOMIC_RELAXED);
}
int rt_oom_forced_left(int site) {
int n = __atomic_load_n(&force_left[site], __ATOMIC_RELAXED);
return n > 0 ? n : 0;
}
void rt_oom_request(void) { __atomic_store_n(&rt_oom_req, 1, __ATOMIC_RELEASE); }
int rt_oom_wait(const char *what, uint64_t bytes, int (*retry)(void *arg), void *arg) {
rt_oom_request();
static volatile int32_t reports;
int report = plat_inc32(&reports) <= OOM_REPORTS;
double proc, used, limit;
if (report) {
plat_commit_info(&proc, &used, &limit);
rt_log("%s of %llu bytes failed (memory commit: this process %.0f MB, the system %.0f of %.0f MB); waiting up "
"to %u s for memory", what, (unsigned long long)bytes, proc, used, limit, OOM_WAIT_MS / 1000);
}
uint64_t t0 = plat_now_ms();
for (int n = 1; plat_now_ms() - t0 < OOM_WAIT_MS; n++) {
plat_sleep_ms(OOM_STEP_MS);
if (retry(arg)) {
if (report)
rt_log("%s of %llu bytes succeeded after %llu ms", what, (unsigned long long)bytes,
(unsigned long long)(plat_now_ms() - t0));
return 1;
}
if (n % OOM_REQUEST_EVERY == 0) rt_oom_request();
}
plat_commit_info(&proc, &used, &limit);
rt_log("%s of %llu bytes still failed after %u s (memory commit: this process %.0f MB, the system %.0f of %.0f MB)",
what, (unsigned long long)bytes, OOM_WAIT_MS / 1000, proc, used, limit);
return 0;
}
typedef struct { void *old, *p; size_t n; } Realloc;
static int realloc_try(void *arg) {
Realloc *r = arg;
return (r->p = realloc(r->old, r->n)) != NULL;
}
void *rt_realloc_wait(void *old, size_t n, const char *what) {
void *p = realloc(old, n);
if (p || !n) return p;
Realloc r = {old, NULL, n};
return rt_oom_wait(what, n, realloc_try, &r) ? r.p : NULL;
}
+196 -4
View File
@@ -73,6 +73,7 @@ typedef struct {
unsigned owed; // events to signal on the next guest thread (bit per MOVIE_EV_*)
uint64_t t0; // host time of the first frame handed over (pacing)
int64_t pts0; // its presentation time (us)
MovieFit *fit; // picture shrunk inside the frame; NULL = the shapes agree, copy
MovieStats st;
PlatMutex mu;
PlatCond work, idle;
@@ -207,6 +208,173 @@ static void to_nv12(const vpx_image_t *img, uint8_t *dst, int w, int h) {
}
}
// ---- Keeping the movie's shape. The game draws a decoded frame over the whole canvas, so on an output that is not 16:9
// (output_res in totk.ini) the 1920x1080 movies would be stretched. The frame keeps its size; the picture is shrunk
// inside it and the rest is black, so that after the game's stretch the picture has the movie's own shape again: a wider
// output gets a squeezed picture with black at the left and right, a taller one (16:10) black above and below.
// The shrink is a box filter (each output pixel averages the source pixels it covers, weights in 1/4096), separable, with
// the per-column and per-row source spans and weights built once per movie (movie_fit_new), so the frame loop only
// multiplies and adds.
enum { FIT_ONE = 4096, FIT_Y_BLACK = 16, FIT_UV_BLACK = 128 }; // black of limited-range video: Y 16, U = V 128
typedef struct { int n, taps; int *start; uint16_t *wt; } FitAxis; // output i: taps weights from source start[i]
struct MovieFit {
int w, h, x0, y0, pw, ph; // frame, and the picture's corner and size in it (all even)
FitAxis lx, ly, cx, cy; // luma, chroma; columns, rows; n = 0 where the axis is kept
uint8_t *tmp; // horizontally shrunk rows, before the vertical pass
uint32_t *acc; // one row of the vertical sum
};
// Box filter from src_n to dst_n (dst_n < src_n): output i covers [i, i + 1) * src_n / dst_n of the source.
static int axis_build(FitAxis *a, int src_n, int dst_n) {
double s = (double)src_n / dst_n;
a->n = dst_n;
a->taps = (int)s + 2 > src_n ? src_n : (int)s + 2;
a->start = malloc((size_t)dst_n * sizeof *a->start);
a->wt = malloc((size_t)dst_n * (size_t)a->taps * sizeof *a->wt);
if (!a->start || !a->wt) return 0;
for (int i = 0; i < dst_n; i++) {
double lo = i * s, hi = (i + 1) * s;
int st = (int)lo;
if (st > src_n - a->taps) st = src_n - a->taps;
a->start[i] = st;
int sum = 0, big = 0;
uint16_t *w = a->wt + (size_t)i * a->taps;
for (int t = 0; t < a->taps; t++) {
double ov = (st + t + 1 < hi ? st + t + 1 : hi) - (st + t > lo ? st + t : lo);
w[t] = ov > 0 ? (uint16_t)(ov / s * FIT_ONE + 0.5) : 0;
sum += w[t];
if (w[t] > w[big]) big = t;
}
w[big] = (uint16_t)(w[big] + FIT_ONE - sum); // the weights add up to exactly 1
}
return 1;
}
static void axis_free(FitAxis *a) {
free(a->start);
free(a->wt);
}
void movie_fit_free(MovieFit *f) {
if (!f) return;
axis_free(&f->lx), axis_free(&f->ly), axis_free(&f->cx), axis_free(&f->cy);
free(f->tmp);
free(f->acc);
free(f);
}
// The plan for a w x h movie shown on an ow x oh output; NULL when the shapes agree within 1% (the frame is copied).
MovieFit *movie_fit_new(int w, int h, unsigned ow, unsigned oh) {
if (w < 16 || h < 16 || (w | h) & 1 || !ow || !oh) return NULL;
double movie = (double)w / h, out = (double)ow / oh, r = out / movie;
if (r > 0.99 && r < 1.01) return NULL;
MovieFit *f = calloc(1, sizeof *f);
if (!f) {
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
return NULL;
}
f->w = w, f->h = h, f->pw = w, f->ph = h;
if (r > 1) f->pw = (int)(w / r / 2 + 0.5) * 2; // wider output: squeeze sideways
else f->ph = (int)(h * r / 2 + 0.5) * 2; // taller output: squeeze vertically
if (f->pw < 2) f->pw = 2;
if (f->ph < 2) f->ph = 2;
f->x0 = (w - f->pw) / 2 & ~1;
f->y0 = (h - f->ph) / 2 & ~1;
int ok = 1;
if (f->pw < w) ok = axis_build(&f->lx, w, f->pw) && axis_build(&f->cx, w / 2, f->pw / 2);
if (f->ph < h) ok = ok && axis_build(&f->ly, h, f->ph) && axis_build(&f->cy, h / 2, f->ph / 2);
f->tmp = malloc((size_t)f->pw * h);
f->acc = malloc((size_t)f->pw * sizeof *f->acc);
if (!ok || !f->tmp || !f->acc) {
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
movie_fit_free(f);
return NULL;
}
return f;
}
// One output row of the horizontal pass: dst gets the shrunk src row (dstep 2 writes every other byte, for U and V).
// The tap count is a constant in the 3 and 4 tap copies (what 2x and below squeezes need), so the compiler unrolls the sum.
static inline __attribute__((always_inline)) void fit_row_taps(const FitAxis *a, const uint8_t *src, uint8_t *dst,
int dstep, int taps) {
for (int i = 0; i < a->n; i++) {
const uint8_t *p = src + a->start[i];
const uint16_t *w = a->wt + (size_t)i * taps;
uint32_t v = FIT_ONE / 2;
for (int t = 0; t < taps; t++) v += w[t] * p[t];
dst[(size_t)i * dstep] = (uint8_t)(v / FIT_ONE);
}
}
static void fit_row_h(const FitAxis *a, const uint8_t *src, uint8_t *dst, int dstep) {
if (a->taps == 3) fit_row_taps(a, src, dst, dstep, 3);
else if (a->taps == 4) fit_row_taps(a, src, dst, dstep, 4);
else fit_row_taps(a, src, dst, dstep, a->taps);
}
// One plane (sw x sh) shrunk into the pw x ph picture whose corner (ox, oy) is in dst; ax/ay NULL = that axis is kept.
static void fit_plane(const MovieFit *f, const uint8_t *src, int ss, int sw, int sh, uint8_t *dst, int ds, int dstep,
int ox, int oy, const FitAxis *ax, const FitAxis *ay, int pw, int ph) {
uint8_t *base = dst + (size_t)oy * ds + (size_t)ox * dstep;
if (!ay) { // sideways only: straight into the frame
for (int y = 0; y < sh; y++) {
const uint8_t *row = src + (size_t)y * ss;
uint8_t *o = base + (size_t)y * ds;
if (ax) fit_row_h(ax, row, o, dstep);
else if (dstep == 1) memcpy(o, row, (size_t)sw);
else for (int x = 0; x < sw; x++) o[(size_t)x * dstep] = row[x];
}
return;
}
const uint8_t *in = src;
int is = ss;
if (ax) {
for (int y = 0; y < sh; y++) fit_row_h(ax, src + (size_t)y * ss, f->tmp + (size_t)y * pw, 1);
in = f->tmp, is = pw;
}
for (int y = 0; y < ph; y++) {
const uint16_t *w = ay->wt + (size_t)y * ay->taps;
const uint8_t *r0 = in + (size_t)ay->start[y] * is;
uint8_t *o = base + (size_t)y * ds;
if (ay->taps == 3) { // the common case, summed in registers
const uint8_t *r1 = r0 + is, *r2 = r1 + is;
for (int x = 0; x < pw; x++)
o[(size_t)x * dstep] = (uint8_t)((FIT_ONE / 2 + w[0] * r0[x] + w[1] * r1[x] + w[2] * r2[x]) / FIT_ONE);
continue;
}
memset(f->acc, 0, (size_t)pw * sizeof *f->acc);
for (int t = 0; t < ay->taps; t++) {
const uint8_t *row = r0 + (size_t)t * is;
uint32_t k = w[t];
for (int x = 0; x < pw; x++) f->acc[x] += k * row[x];
}
for (int x = 0; x < pw; x++) o[(size_t)x * dstep] = (uint8_t)((f->acc[x] + FIT_ONE / 2) / FIT_ONE);
}
}
// I420 planes -> packed NV12 with the picture shrunk inside black bars: w x h of Y, then w x h/2 of interleaved U, V.
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst) {
int w = f->w, h = f->h, cw = w / 2, ch = h / 2;
uint8_t *uv = dst + (size_t)w * h;
// Black only where the picture does not go: whole rows above and below it, the strips left and right of it.
int rx = f->x0 + f->pw, ry = f->y0 + f->ph;
memset(dst, FIT_Y_BLACK, (size_t)f->y0 * w);
memset(dst + (size_t)ry * w, FIT_Y_BLACK, (size_t)(h - ry) * w);
memset(uv, FIT_UV_BLACK, (size_t)(f->y0 / 2) * w);
memset(uv + (size_t)(ry / 2) * w, FIT_UV_BLACK, (size_t)(ch - ry / 2) * w);
for (int y = f->y0; y < ry; y++) {
memset(dst + (size_t)y * w, FIT_Y_BLACK, (size_t)f->x0);
memset(dst + (size_t)y * w + rx, FIT_Y_BLACK, (size_t)(w - rx));
}
for (int y = f->y0 / 2; y < ry / 2; y++) {
memset(uv + (size_t)y * w, FIT_UV_BLACK, (size_t)f->x0);
memset(uv + (size_t)y * w + rx, FIT_UV_BLACK, (size_t)(w - rx));
}
fit_plane(f, plane[0], stride[0], w, h, dst, w, 1, f->x0, f->y0, f->lx.n ? &f->lx : NULL, f->ly.n ? &f->ly : NULL,
f->pw, f->ph);
for (int c = 0; c < 2; c++)
fit_plane(f, plane[1 + c], stride[1 + c], cw, ch, uv + c, w, 2, f->x0 / 2, f->y0 / 2,
f->cx.n ? &f->cx : NULL, f->cy.n ? &f->cy : NULL, f->pw / 2, f->ph / 2);
}
static int free_out(const Movie *m) {
for (int i = 0; i < OUT_BUFS; i++)
if (!m->out_held[i]) return i;
@@ -250,7 +418,9 @@ static void worker(void *arg) {
slot = free_out(m);
m->out_held[slot] = 1; // reserved: in no queue, so the game cannot release it
}
uint64_t gen = m->gen, dt = 0;
uint64_t gen = m->gen, dt = 0, fit_ns = 0;
// A plan made for another frame size (a start that found the worker busy) is not used: it would overrun dst.
const MovieFit *fit = m->fit && m->fit->w == m->w && m->fit->h == m->h ? m->fit : NULL;
int got = 0, w = m->w, h = m->h;
uint8_t *dst = slot >= 0 ? m->out[slot] : NULL;
vpx_codec_err_t e = VPX_CODEC_OK;
@@ -265,7 +435,12 @@ static void worker(void *arg) {
const vpx_image_t *img;
while ((img = vpx_codec_get_frame(&m->vpx, &it)) != NULL) {
if (img->fmt != VPX_IMG_FMT_I420 || (int)img->d_w != w || (int)img->d_h != h) bad = "layout";
else if (!got) to_nv12(img, dst, w, h);
else if (!got) {
uint64_t t1 = kern_now_ns();
if (fit) movie_fit_apply(fit, (const uint8_t *const *)img->planes, img->stride, dst);
else to_nv12(img, dst, w, h);
fit_ns = kern_now_ns() - t1;
}
got++;
}
}
@@ -279,6 +454,7 @@ static void worker(void *arg) {
m->owed |= 1u << MOVIE_EV_INPUT;
if (p.size > 0) {
m->st.decode_ns += dt;
m->st.fit_ns += fit_ns;
if (dt > m->st.max_ns) m->st.max_ns = dt;
if (bad) rt_fatal("movie: VP9 frame layout differs from the configured %dx%d I420", w, h);
if (e != VPX_CODEC_OK || !got) {
@@ -302,8 +478,10 @@ static void worker(void *arg) {
push_decoded(m, slot, 0, p.time, frame_bytes(m));
m->st.frames++;
if (m->st.frames % 600 == 0)
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f)", (unsigned long long)m->st.frames,
m->st.decode_ns / 1e6 / (double)m->st.frames, m->st.max_ns / 1e6);
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f), of it %.2f ms %s",
(unsigned long long)m->st.frames, m->st.decode_ns / 1e6 / (double)m->st.frames,
m->st.max_ns / 1e6, m->st.fit_ns / 1e6 / (double)m->st.frames,
m->fit ? "fitting the picture" : "converting");
}
}
if (p.flags & MOVIE_FLAG_EOS && !m->eos_in) {
@@ -355,6 +533,8 @@ static void release_codec(Movie *m) { // m->mu held, worker quiesced
free(m->out[i]);
m->out[i] = NULL;
}
movie_fit_free(m->fit);
m->fit = NULL;
m->configured = m->started = 0;
reset_queues(m);
}
@@ -597,6 +777,18 @@ static void ov_start(Cpu *c) {
plat_mutex_lock(&m->mu);
int st = ST_NOT_CONFIGURED;
if (m->configured) {
if (!m->started && !m->busy) { // the worker is idle: it reads m->fit only when it takes an input
movie_fit_free(m->fit);
m->fit = NULL;
unsigned out[2];
res_wanted(out);
if (strcmp(cfg_value("TOTK_MOVIE_ASPECT"), "stretch")) {
m->fit = movie_fit_new(m->w, m->h, out[0], out[1]);
if (m->fit)
rt_log("movie: %dx%d picture kept in shape on the %ux%u output (%dx%d inside the frame)", m->w,
m->h, out[0], out[1], m->fit->pw, m->fit->ph);
}
}
m->started = 1;
m->owed |= 1u << MOVIE_EV_INPUT;
plat_cond_signal(&m->work);
+42 -4
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");
@@ -897,8 +929,14 @@ static void probe_menu_text(void) {
cfg_set("TOTK_MENU_LANG", "zh-Hans");
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
cfg_set("TOTK_MENU_LANG", "");
// the system language as ui_text.c resolve_menu_lang takes it: the tag, else its base language (ru-RU: ru), else en
const char *sys_lang = ui_text_lang(plat_ui_language());
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang : "en";
char sys_base[16] = "";
size_t base_n = strcspn(sys_lang, "-");
if (sys_lang[base_n] && base_n < sizeof sys_base) memcpy(sys_base, sys_lang, base_n);
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang
: *sys_base && ui_text_defined(sys_base, UI_MENU_TITLE) ? sys_base
: "en";
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
}
+9
View File
@@ -62,6 +62,15 @@ uint64_t perf_begin(void) { return kern_now_ns(); }
void perf_replay_thread(void) { prof_replay_tid = plat_thread_id(); }
void perf_main_thread(void) { prof_main_tid = plat_thread_id(); }
void perf_core_thread(int role) {
static const char *const names[] = {"game main", "game render", "vk replay", "vk recorder", "vk present"};
static int32_t said;
int r = plat_thread_perf_core();
if (r < 0 || role < 0 || role >= PCORE_N) return;
if (__atomic_fetch_or(&said, 1 << role, __ATOMIC_RELAXED) & (1 << role)) return;
rt_log("power: the %s thread %s the P-cores", names[role], r ? "prefers" : "could not select");
}
void perf_end(int kind, uint64_t t0) {
if (!t0) return;
uint32_t tid = plat_thread_id();
+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];
+150 -5
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>
@@ -268,7 +270,8 @@ int plat_intr_resume(PlatIntr *t, uint64_t to) {
// ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call
// on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the
// x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which calls fn(arg) (r12 and r13).
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which realigns the stack and calls fn(arg)
// (r12 and r13).
struct PlatFiber {
void *sp;
void *stack; // mapped stack (NULL for a thread's own context)
@@ -310,6 +313,11 @@ __asm__(".text\n"
".type plat_fiber_entry, @function\n"
".p2align 4\n"
"plat_fiber_entry:\n"
// fiber_swap's ret lands here with rsp = top - 8, as at the entry of a called function. The call below must
// be made with rsp 16-byte aligned so that fn starts with rsp = 8 mod 16, as the System V ABI demands; without
// the realignment every 16-byte local of fn is off by 8, which the VEX instructions of a v3 build tolerate
// but the SSE ones of a v2 or k10 build (movdqa, pcmpeqb with a memory operand) fault on.
" andq $-16, %rsp\n"
" movq %r13, %rdi\n"
" callq *%r12\n"
" ud2\n"
@@ -473,6 +481,14 @@ uint64_t plat_counter_hz(void) { return 1000000000u; }
// inherit it from the thread that creates them.
void plat_timer_1ms(void) { prctl(PR_SET_TIMERSLACK, 1000ul, 0, 0, 0); }
int plat_power_full(void) { return -1; }
int plat_perf_core_setup(char *why, size_t n) {
snprintf(why, n, "not done on Linux");
return 0;
}
int plat_thread_perf_core(void) { return -1; }
// clock_nanosleep wakes within the timer slack (50 us by default) of its deadline; the last PLAT_SPIN_NS are spent
// yielding so the deadline is met closely, as on Windows.
#define PLAT_SPIN_NS 100000u
@@ -916,8 +932,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,12 +944,23 @@ 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;
static const int gr[16] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP, REG_RSI, REG_RDI,
REG_R8, REG_R9, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15};
for (int i = 0; i < 16; i++) c.gpr[i] = (uint64_t)u->uc_mcontext.gregs[gr[i]];
c.has_regs = 1;
crash_fn(&c);
}
signal(sig, SIG_DFL); // returns into the faulting instruction, which faults again with the default action
@@ -983,6 +1011,12 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
return process_vm_readv(getpid(), &local, 1, &remote, 1, 0) == (ssize_t)n;
}
// Not known before the SDL window exists; the Steam Deck has its own profile (cfg.c)
int plat_screen_size(int *w, int *h) {
(void)w, (void)h;
return 0;
}
int plat_mem_readable(uint64_t addr, uint64_t n) {
uint8_t b;
uint64_t end = addr + (n ? n : 1);
@@ -1265,7 +1299,16 @@ int plat_stderr_path(char *out, size_t cap) {
// ---- process
#ifdef TOTK_PGO_GEN
// compiler-rt's profile runtime (an instrumented build, tools/build_game.py --pgo gen). It writes its counters from an
// atexit handler, which _exit skips; __llvm_profile_dump writes them once and makes later writes no-ops.
int __llvm_profile_dump(void);
#endif
void plat_exit(int code) {
#ifdef TOTK_PGO_GEN
__llvm_profile_dump();
#endif
fflush(stdout);
fflush(stderr);
_exit(code);
@@ -1425,6 +1468,41 @@ int plat_open_folder(const char *path) {
void *plat_lib_open(const char *name) { return dlopen(name, RTLD_NOW | RTLD_LOCAL); }
void *plat_lib_sym(void *lib, const char *name) { return lib ? dlsym(lib, name) : NULL; }
void plat_lib_error(char *out, size_t cap) {
const char *e = dlerror();
snprintf(out, cap, "%s", e ? e : "unknown error");
}
int plat_ngx_core_path(char *out, size_t cap) { return snprintf(out, cap, "libnvidia-ngx.so.1") < (int)cap; }
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
const unsigned char *u = (const unsigned char *)in;
size_t n = 0;
while (*u) {
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
if (more == 4) return 0;
if (more) c &= 0x3F >> more;
for (unsigned k = 0; k < more; k++, u++) {
if ((*u & 0xC0) != 0x80) return 0;
c = c << 6 | (*u & 0x3F);
}
if (n + 1 >= cap) return 0;
out[n++] = (wchar_t)c;
}
out[n] = 0;
return 1;
}
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
char dir[PLAT_PATH_MAX];
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
if (!cut) return 0;
cut[1] = 0;
return utf8_to_wide(dir, out, cap);
}
// ---- machine load: /proc/stat and /proc/self/stat for the CPU, hwmon for the temperature, the amdgpu sysfs files of
// the render device for the GPU.
@@ -1595,6 +1673,17 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
if (private_bytes) *proc_mb = (double)(private_bytes >> 20);
}
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) {
*commit_room = *phys_room = UINT64_MAX;
char line[128];
unsigned long long k;
FILE *f = fopen("/proc/meminfo", "r");
if (!f) return;
while (fgets(line, sizeof line, f))
if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) *phys_room = (uint64_t)k << 10;
fclose(f);
}
// ---- child processes
struct PlatChild {
@@ -1656,6 +1745,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
+55 -8
View File
@@ -99,6 +99,12 @@ uint64_t plat_now_ns(void); // monotonic, high resolution, arbitra
uint64_t plat_counter(void); // the OS's raw monotonic counter (QPC on Windows), plat_counter_hz per second
uint64_t plat_counter_hz(void);
void plat_timer_1ms(void); // asks the OS for timed waits accurate to 1 ms or better (call before threads)
int plat_power_full(void); // opts the process out of OS power throttling: 1 done, 0 refused, -1 no such thing
// Hybrid CPUs: finds the performance-core logical processors the process may use, once, before any thread asks; returns
// their count (0: none to prefer, why says which case). plat_thread_perf_core: the calling thread prefers them; 1 done,
// 0 refused, -1 nothing selected.
int plat_perf_core_setup(char *why, size_t n);
int plat_thread_perf_core(void);
// t * num / den for a fixed num / den without a 128-bit division per call (__udivti3 took 5% of the game's running
// samples through plat_now_ns and rt_ticks): t * q + (t * f >> 64) with q = num / den and f = (num % den) * 2^64 / den.
// Monotonic in t and at most one below the exact quotient while t * (num % den) / den fits 64 bits.
@@ -218,22 +224,39 @@ 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 on the faulting thread's stack in every module, so
// the report shows which driver code failed and which host call ran into it; on Linux from the signal handler,
// without frames. Drivers that raise and catch their own exceptions never reach fn. For a C++ exception thrown by
// MSVC-built code (a driver) the report names its type and, for a std::exception, its what() text.
// - 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, innermost first: for abort and invalid parameter (rip is frames[0])
uintptr_t frames[32]; // and for a fault in another module; 0 for a fault in this one
const char *frame_module[32]; // the module of each frame: NULL for this executable, "" for none
uintptr_t frame_offset[32]; // each frame's offset in its module
int frames_left_out; // frames in other modules past the limit the walk keeps, so this executable's still fit
const char *module; // the module holding rip: NULL for this executable, "" for none (a jump to address 0)
const char *cxx_type; // a C++ exception from MSVC-built code: the thrown type's decorated name (.?AV...@@)
const char *cxx_what; // and what() when the type derives from std::exception; NULL when unknown
int has_regs; // gpr holds the faulting thread's registers (a fault; not abort or invalid parameter)
uint64_t gpr[16]; // rax rcx rdx rbx rsp rbp rsi rdi r8..r15, in the x86 encoding order
} PlatCrash;
#define PLAT_GPR_NAMES {"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", \
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"}
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
int plat_screen_size(int *w, int *h); // the primary display's current mode in pixels; 0 unknown
int plat_mem_readable(uint64_t addr, uint64_t n); // every page of [addr, addr + n) can be read; no fault
void *plat_aligned_alloc(size_t size, size_t align); // free with plat_aligned_free; NULL on failure
void plat_aligned_free(void *p);
@@ -380,6 +403,16 @@ int plat_open_folder(const char *path); // shows the folder in
// in the system directories only. NULL when missing.
void *plat_lib_open(const char *name);
void *plat_lib_sym(void *lib, const char *name);
// Why the calling thread's last plat_lib_open failed, for a log line (the OS error code on Windows, dlerror on Linux).
void plat_lib_error(char *out, size_t cap);
// ---- NVIDIA NGX (vk_dlaa.c). The path of the driver's NGX core library into out: on Windows _nvngx.dll in the folder
// the driver records in the registry (NGXCore FullPath), on Linux libnvidia-ngx.so.1, which the dynamic loader finds
// where the driver put it. 0 when the driver records none.
int plat_ngx_core_path(char *out, size_t cap);
// The running executable's folder with a trailing separator, as the wchar_t string NGX takes for its application data
// path (UTF-16 on Windows, UTF-32 code points on Linux). 0 on failure or when out has no room.
int plat_exe_dir_wide(wchar_t *out, size_t cap);
// ---- machine load for the performance overlay (perfmon.c's sampling thread; each call can take milliseconds). A number
// the system does not give is -1.
@@ -400,6 +433,9 @@ void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb);
// Commit charge in MB: this process's private bytes, the system's commit in use and its limit (RAM plus page files).
// An allocation fails when the system's reaches the limit, however much physical memory is free. -1 when unknown.
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb);
// What the system can still give, in bytes: commit (Windows: limit minus charge; Linux, which overcommits: UINT64_MAX)
// and physical memory available without paging. UINT64_MAX when unknown.
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room);
// This process's memory in bytes: resident (the working set) and private (Windows: committed private bytes; Linux: the
// resident anonymous pages). 0 when unknown.
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes);
@@ -413,6 +449,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.
+364
View File
@@ -18,6 +18,13 @@
#include "platform.h"
// Hybrid laptops: the drivers read these exports of the executable when it starts and run it on the dedicated GPU.
// NVIDIA Optimus can otherwise hide its GPU from a program it put on the integrated one, whatever the Windows and NVIDIA
// settings say; AMD switchable graphics read the second one. TOTK_GPU_INDEX still picks another GPU. Savage Wind Recomp
// exports the same two (runtime/platform/windows.c).
__declspec(dllexport) DWORD NvOptimusEnablement = 1;
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
_Static_assert(sizeof(PlatMutex) == sizeof(SRWLOCK), "PlatMutex holds an SRWLOCK");
_Static_assert(sizeof(PlatCond) == sizeof(CONDITION_VARIABLE), "PlatCond holds a CONDITION_VARIABLE");
@@ -289,6 +296,98 @@ uint32_t plat_cpu_usable(void) {
return plat_cpu_count();
}
// ---- performance cores of a hybrid CPU
// An Intel CPU with P-cores and E-cores (a Core 5 210H: 4 P-cores with two threads each, 4 E-cores) runs a thread on an
// E-core at about 60% of a P-core's speed, and the frame waits for its slowest critical thread. The few threads every
// frame goes through ask for the P-cores (plat_thread_perf_core): their selected CPU sets are the logical processors
// above the lowest efficiency class that the process affinity allows. A thread's CPU sets bind it like an affinity
// mask, except that processors the system reserves for itself are left out instead of failing. Nothing is selected
// on a CPU with one efficiency class, or when the affinity already leaves only P-cores or only E-cores. LAS Recomp
// does the same for its game, render and vsync threads (runtime/platform/windows.c plat_thread_perf_core).
// Nothing is selected either below PC_MIN_CORES physical P-cores (an i5-1235U or Core 3 100U has 2): the five bound
// threads (perf_core_thread) and a spinning recorder would crowd two cores and their hyperthreads while the E-cores
// stay idle, which can be slower than letting the scheduler place them.
#define PC_MIN_CORES 4
static ULONG *pc_ids; // the CPU set ids of the allowed P-core logical processors; NULL: none selected
static ULONG pc_n;
int plat_perf_core_setup(char *why, size_t n) {
ULONG len = 0;
GetSystemCpuSetInformation(NULL, 0, &len, GetCurrentProcess(), 0);
uint8_t *buf = len ? malloc(len) : NULL;
if (!buf || !GetSystemCpuSetInformation((PSYSTEM_CPU_SET_INFORMATION)buf, len, &len, GetCurrentProcess(), 0)) {
free(buf);
snprintf(why, n, "no CPU set information (error %lu)", GetLastError());
return 0;
}
// The process affinity mask covers its first processor group only: it filters on a single-group system.
DWORD_PTR proc = 0, sys = 0;
int filter = GetActiveProcessorGroupCount() == 1 && GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys) && proc;
int lo = 255, hi = -1, all = 0;
for (ULONG off = 0; off < len;) {
SYSTEM_CPU_SET_INFORMATION *i = (SYSTEM_CPU_SET_INFORMATION *)(buf + off);
if (!i->Size) break;
if (i->Type == CpuSetInformation) {
int ec = i->CpuSet.EfficiencyClass;
lo = ec < lo ? ec : lo, hi = ec > hi ? ec : hi, all++;
}
off += i->Size;
}
if (!all || hi <= lo) {
free(buf);
snprintf(why, n, "not a hybrid CPU (%d logical processors, one efficiency class)", all);
return 0;
}
ULONG *ids = calloc((size_t)all, sizeof *ids);
uint16_t *cores = calloc((size_t)all, sizeof *cores); // group << 8 | core index of each allowed physical P-core
int np = 0, ne = 0, ok_p = 0, ok_e = 0, nc = 0;
for (ULONG off = 0; ids && cores && off < len;) {
SYSTEM_CPU_SET_INFORMATION *i = (SYSTEM_CPU_SET_INFORMATION *)(buf + off);
if (!i->Size) break;
if (i->Type == CpuSetInformation) {
int perf = i->CpuSet.EfficiencyClass > lo;
int allowed = !filter || (i->CpuSet.Group == 0 && i->CpuSet.LogicalProcessorIndex < 64 &&
(proc >> i->CpuSet.LogicalProcessorIndex) & 1);
perf ? np++ : ne++;
if (allowed) perf ? ok_p++ : ok_e++;
if (allowed && perf) {
ids[pc_n++] = i->CpuSet.Id;
uint16_t key = (uint16_t)(i->CpuSet.Group << 8 | i->CpuSet.CoreIndex);
int k = 0;
while (k < nc && cores[k] != key) k++;
if (k == nc) cores[nc++] = key;
}
}
off += i->Size;
}
int counted = ids && cores;
free(buf);
free(cores);
if (!counted || !ok_p || !ok_e) {
free(ids);
pc_n = 0;
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors; the affinity allows %d + %d, so it decides",
np, ne, ok_p, ok_e);
return 0;
}
if (nc < PC_MIN_CORES) {
free(ids);
pc_n = 0;
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors on %d P-cores, fewer than %d", np, ne, nc,
PC_MIN_CORES);
return 0;
}
pc_ids = ids;
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors%s", np, ne,
ok_p != np || ok_e != ne ? " (affinity-limited)" : "");
return (int)pc_n;
}
int plat_thread_perf_core(void) {
if (!pc_ids) return -1;
return SetThreadSelectedCpuSets(GetCurrentThread(), pc_ids, pc_n) ? 1 : 0;
}
// PLAT_PRIO_* are the THREAD_PRIORITY_* values.
void plat_thread_priority(int prio) { SetThreadPriority(GetCurrentThread(), prio); }
void plat_thread_set_priority(PlatThread *t, int prio) { SetThreadPriority(t->h, prio); }
@@ -319,6 +418,17 @@ uint64_t plat_counter_hz(void) { return qpc_freq(); }
// The default timer tick is 15.6 ms; a 1 ms wait would cost a full tick.
void plat_timer_1ms(void) { timeBeginPeriod(1); }
// Windows 11 runs the threads of a process it deems in the background at EcoQoS (on the E-cores of a hybrid Intel CPU,
// at a low clock) and ignores its timer resolution while its window is hidden. The game is always latency-bound, so it
// opts out of both. Windows before 10 1709 does not know the class: 0.
int plat_power_full(void) {
PROCESS_POWER_THROTTLING_STATE s = {
.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
.StateMask = 0};
return SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &s, sizeof s) ? 1 : 0;
}
static PlatScale qpc_ns; // set before main, so no thread reads it half written
__attribute__((constructor)) static void qpc_ns_init(void) { qpc_ns = plat_scale(1000000000u, qpc_freq()); }
@@ -626,8 +736,21 @@ void plat_mem_fault_hook(int (*fn)(uint64_t addr)) {
static void (*crash_fn)(const PlatCrash *c);
// The faulting thread's general registers, for the report to tie the faulting instruction to its operands.
static void regs_of(const CONTEXT *x, PlatCrash *c) {
const DWORD64 r[16] = {x->Rax, x->Rcx, x->Rdx, x->Rbx, x->Rsp, x->Rbp, x->Rsi, x->Rdi,
x->R8, x->R9, x->R10, x->R11, x->R12, x->R13, x->R14, x->R15};
for (int i = 0; i < 16; i++) c->gpr[i] = r[i];
c->has_regs = 1;
}
static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep);
static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
DWORD code = ep->ExceptionRecord->ExceptionCode;
// Heap corruption is raised by the heap manager, which then ends the process itself: it never reaches the
// unhandled-exception filter, so it is reported from here, wherever the heap was called from.
if (code == 0xc0000374u) return crash_filter(ep); // STATUS_HEAP_CORRUPTION
const char *what = code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
@@ -643,6 +766,7 @@ static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
PlatCrash c = {what, (uint32_t)code, rip, rip - (uintptr_t)mi.lpBaseOfDll, -1, 0};
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2)
c.access = (int)ep->ExceptionRecord->ExceptionInformation[0], c.addr = ep->ExceptionRecord->ExceptionInformation[1];
regs_of(ep->ContextRecord, &c);
crash_fn(&c);
return EXCEPTION_CONTINUE_SEARCH;
}
@@ -665,6 +789,134 @@ __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 on the crashed thread's stack, innermost first, with their modules: 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. A driver can
// nest deeper than the report holds, so frames in other modules stop being kept after the first 12 (counted in
// frames_left_out) and the frames in this executable, which name the host call that ran into the driver, still fit.
static void frames_of(const CONTEXT *at, PlatCrash *x) {
enum { MAX = sizeof x->frames / sizeof *x->frames, OTHER_MAX = 12 };
CONTEXT c = *at;
ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high);
void *exe = GetModuleHandleW(NULL);
int other = 0;
for (int i = 0; i < 256 && x->nframes < 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) break;
void *base = NULL;
RtlPcToFileHeader((void *)(uintptr_t)c.Rip, &base);
if (base != exe && other++ >= OTHER_MAX) {
x->frames_left_out++;
continue;
}
int n = x->nframes++;
x->frames[n] = (uintptr_t)c.Rip;
x->frame_module[n] = base == exe ? NULL : base ? image_name(base) : "";
x->frame_offset[n] = (uintptr_t)c.Rip - (uintptr_t)base;
}
}
// MSVC's C++ throw (0xe06d7363) passes the thrown object in ExceptionInformation[1], its ThrowInfo in [2] and the image
// base the ThrowInfo's 32-bit offsets are relative to in [3] (magic 0x19930520..0x19930522 in [0]). The ThrowInfo lists
// the types a catch may match, the thrown type first; each type descriptor holds a vtable pointer, a spare pointer and
// the decorated name. A std::exception keeps its what() text in the pointer right after its vtable pointer (MSVC's
// __std_exception_data). Every read is checked: the exception may come from a damaged heap.
typedef struct { uint32_t attributes; int32_t unwind, forward_compat, catchable_types; } CxxThrowInfo;
typedef struct { uint32_t properties; int32_t type, this_mdisp, this_pdisp, this_vdisp, size, copy; } CxxCatchableType;
// Copies the string at p into buf (cut to n - 1 characters), checking each page before reading it; NULL when the first
// byte cannot be read.
static const char *copy_cstr(uintptr_t p, char *buf, size_t n) {
size_t i = 0;
for (; p && i + 1 < n; i++) {
if ((!i || !((p + i) & 4095)) && !plat_mem_readable(p + i, 1)) break;
if (!(buf[i] = ((const char *)p)[i])) return buf;
}
buf[i] = 0;
return i ? buf : NULL;
}
static void cxx_info(const EXCEPTION_RECORD *er, PlatCrash *x) {
if (er->NumberParameters < 4 || er->ExceptionInformation[0] - 0x19930520u > 2) return;
uintptr_t obj = er->ExceptionInformation[1], info = er->ExceptionInformation[2],
base = er->ExceptionInformation[3];
if (!info || !plat_mem_readable(info, sizeof(CxxThrowInfo))) return;
const CxxThrowInfo *ti = (const void *)info;
uintptr_t arr = base + (uintptr_t)(intptr_t)ti->catchable_types;
if (!ti->catchable_types || !plat_mem_readable(arr, 4)) return;
int32_t n = *(const int32_t *)arr;
if (n <= 0 || n > 64 || !plat_mem_readable(arr + 4, 4u * (uint64_t)n)) return;
static char type[128], what[256]; // the crash report runs once: crash_fn ends the process
int exception = 0;
for (int32_t i = 0; i < n; i++) {
uintptr_t ct = base + (uintptr_t)(intptr_t)((const int32_t *)(arr + 4))[i];
if (!plat_mem_readable(ct, sizeof(CxxCatchableType))) continue;
uintptr_t td = base + (uintptr_t)(intptr_t)((const CxxCatchableType *)ct)->type;
char name[64];
if (!copy_cstr(td + 16, i ? name : type, i ? sizeof name : sizeof type)) // past vtable and spare pointers
continue;
if (!i) x->cxx_type = type;
if (!strcmp(i ? name : type, ".?AVexception@std@@")) exception = 1;
}
if (exception && obj && plat_mem_readable(obj + 8, 8))
x->cxx_what = copy_cstr(*(const uintptr_t *)(obj + 8), what, sizeof what);
}
// 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];
regs_of(ep->ContextRecord, &c);
void *base = NULL;
RtlPcToFileHeader((void *)rip, &base);
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) frames_of(ep->ContextRecord, &c);
if (code == 0xe06d7363u) cxx_info(er, &c);
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 +928,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);
}
@@ -704,6 +957,14 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
return ReadProcessMemory(GetCurrentProcess(), (const void *)(uintptr_t)addr, out, n, &got) && got == n;
}
// The display mode's own size: physical pixels whatever the process's DPI awareness and the display scale
int plat_screen_size(int *w, int *h) {
DEVMODEW dm = {.dmSize = sizeof dm};
if (!EnumDisplaySettingsW(NULL, ENUM_CURRENT_SETTINGS, &dm) || !dm.dmPelsWidth || !dm.dmPelsHeight) return 0;
*w = (int)dm.dmPelsWidth, *h = (int)dm.dmPelsHeight;
return 1;
}
int plat_mem_readable(uint64_t addr, uint64_t n) {
for (uint64_t a = addr, end = addr + n; a < end || a == addr;) {
MEMORY_BASIC_INFORMATION mbi;
@@ -720,7 +981,33 @@ void plat_aligned_free(void *p) { _aligned_free(p); }
// ---- process
#ifdef TOTK_PGO_GEN
// compiler-rt's profile runtime (an instrumented build, tools/build_game.py --pgo gen). It writes its counters from an
// atexit handler, which ExitProcess skips; __llvm_profile_dump writes them once and makes later writes no-ops.
int __llvm_profile_dump(void);
int __llvm_profile_write_file(void);
void __llvm_profile_reset_counters(void);
// A long played session must not lose its counts to a crash or a reboot: every minute the counters are added to the
// .profraw (the file merges, -fprofile-generate's %m) and zeroed. Counts taken between the write and the reset are lost.
static DWORD WINAPI pgo_flush_thread(void *arg) {
(void)arg;
for (;;) {
Sleep(60 * 1000);
if (__llvm_profile_write_file() == 0) __llvm_profile_reset_counters();
}
}
__attribute__((constructor)) static void pgo_flush_start(void) {
HANDLE h = CreateThread(NULL, 0, pgo_flush_thread, NULL, 0, NULL);
if (h) CloseHandle(h);
}
#endif
void plat_exit(int code) {
#ifdef TOTK_PGO_GEN
__llvm_profile_dump();
#endif
fflush(stdout);
fflush(stderr);
ExitProcess((UINT)code);
@@ -1284,6 +1571,27 @@ void *plat_lib_open(const char *name) {
void *plat_lib_sym(void *lib, const char *name) { return lib ? (void *)GetProcAddress((HMODULE)lib, name) : NULL; }
void plat_lib_error(char *out, size_t cap) { snprintf(out, cap, "error %lu", GetLastError()); }
int plat_ngx_core_path(char *out, size_t cap) {
wchar_t dir[WPATH];
DWORD n = sizeof dir;
char d[PLAT_PATH_MAX];
if (RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", L"FullPath", RRF_RT_REG_SZ,
NULL, dir, &n) != ERROR_SUCCESS ||
!WideCharToMultiByte(CP_UTF8, 0, dir, -1, d, (int)sizeof d, NULL, NULL))
return 0;
return snprintf(out, cap, "%s\\_nvngx.dll", d) < (int)cap;
}
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
DWORD len = GetModuleFileNameW(NULL, out, (DWORD)cap);
if (!len || len >= cap) return 0;
while (len && out[len - 1] != L'\\' && out[len - 1] != L'/') len--;
out[len] = 0;
return len != 0;
}
// ---- machine load: GetSystemTimes for the CPU, PDH (pdh.dll, loaded on first use) for the thermal zones and the GPU
// engine and adapter memory counters that Task Manager reads.
@@ -1476,6 +1784,12 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
*proc_mb = (double)(pmc.PrivateUsage >> 20);
}
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) {
*commit_room = *phys_room = UINT64_MAX;
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
if (GlobalMemoryStatusEx(&ms)) *commit_room = ms.ullAvailPageFile, *phys_room = ms.ullAvailPhys;
}
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
PROCESS_MEMORY_COUNTERS_EX pm = {.cb = sizeof pm};
*resident = *private_bytes = 0;
@@ -1553,6 +1867,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,
+214
View File
@@ -0,0 +1,214 @@
// Play time: how long the player has spent in the game, across runs, shown on the Game tab of the settings menu
// (ui/menu.cpp tab_game). The game itself keeps no play time a player can see, and the Switch's own play log is not
// there to read, so the port counts it.
//
// One host thread (it never holds a virtual core, as discord_rpc.c's) wakes every second and adds the time since its
// last wake when the window has the focus (gfx_window_focused) and the game presented at least one frame meanwhile
// (gfx_stats). So the shader prewarm before the first frame, a window left in the background and a hung game do not
// count; loading screens and the game's own menus do, as they present frames. One wake adds at most TICK_CAP_MS, so a
// machine that slept with the game open adds two seconds, not the night.
//
// The total lives in <profile>/playtime.txt, next to account.uid and the saves, so it stays with them when the
// install is updated or moved. It is plain text, "total_seconds <n>", written atomically (plat_file_write_atomic)
// every SAVE_EVERY_MS while it grows and once more when the game exits (playtime_save from boot.c, svc.c and
// gfx_present.c, next to prof_write); a crash loses at most SAVE_EVERY_MS. A file that does not read as that is kept
// as playtime.txt.bad and the count starts again from zero. A scripted run (--seconds) reads the total and adds nothing.
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gfx.h"
#include "host.h"
#include "platform/platform.h"
#define TICK_MS 1000
#define TICK_CAP_MS 2000
#define SAVE_EVERY_MS 30000
#define TOTAL_CAP_S (100ull * 365 * 24 * 3600) // a hundred years: anything larger is a damaged file
static PlatMutex lock = PLAT_MUTEX_INIT; // the totals and the file
static uint64_t total_ms, session_ms, saved_ms;
static int loaded; // 1 once the file was read (or found missing), -1 when there is no profile
static char path[1100];
// The seconds of a playtime.txt: "total_seconds <n>", a line end or nothing after it, a UTF-8 byte order mark before it
// (an editor may add one). 1 when the file reads so and n is at most TOTAL_CAP_S.
static int parse(const uint8_t *b, uint64_t n, uint64_t *seconds) {
if (n >= 3 && !memcmp(b, "\xEF\xBB\xBF", 3)) b += 3, n -= 3;
char text[64] = {0};
memcpy(text, b, n < sizeof text - 1 ? n : sizeof text - 1);
unsigned long long s = 0;
char tail = 0;
if (sscanf(text, "total_seconds %llu%c", &s, &tail) < 1 || (tail && tail != '\n' && tail != '\r') ||
s > TOTAL_CAP_S)
return 0;
*seconds = s;
return 1;
}
// Holding lock. dir: the profile directory.
static void load(const char *dir) {
if (loaded) return;
if (!dir || !*dir || snprintf(path, sizeof path, "%s/playtime.txt", dir) >= (int)sizeof path) {
loaded = -1;
rt_log("playtime: no profile directory; play time is not counted");
return;
}
loaded = 1;
uint64_t n = 0, s = 0;
uint8_t *b = plat_file_read(path, &n);
if (!b) {
rt_log("playtime: no %s yet; counting from zero", path);
return;
}
if (parse(b, n, &s)) {
free(b);
total_ms = saved_ms = s * 1000;
rt_log("playtime: %llu h %02llu min played before this run (%s)", (unsigned long long)(s / 3600),
(unsigned long long)(s / 60 % 60), path);
return;
}
char bad[1110];
snprintf(bad, sizeof bad, "%s.bad", path);
int kept = plat_file_write_atomic(bad, b, (size_t)n); // the whole file, for the player to recover the number
free(b);
rt_log("playtime: %s does not read as \"total_seconds <n>\"; %s, counting from zero", path,
kept ? "kept as playtime.txt.bad" : "could not keep a copy");
}
// Holding lock.
static void save_locked(void) {
if (loaded != 1 || total_ms / 1000 == saved_ms / 1000) return;
char text[48];
int n = snprintf(text, sizeof text, "total_seconds %" PRIu64 "\n", total_ms / 1000);
if (plat_file_write_atomic(path, text, (size_t)n)) saved_ms = total_ms;
else rt_log("playtime: cannot write %s", path);
}
void playtime_save(void) {
plat_mutex_lock(&lock);
save_locked();
plat_mutex_unlock(&lock);
}
int playtime_get(uint64_t *total_s, uint64_t *session_s) {
plat_mutex_lock(&lock);
int ok = loaded == 1;
*total_s = total_ms / 1000;
*session_s = session_ms / 1000;
plat_mutex_unlock(&lock);
return ok;
}
// Holding lock: the time since the last tick, d ms, when the game was played meanwhile; the file every SAVE_EVERY_MS.
static void tick_locked(uint64_t now, uint64_t d, int playing, uint64_t *last_save) {
if (d > TICK_CAP_MS) d = TICK_CAP_MS;
if (playing) total_ms += d, session_ms += d;
if (now - *last_save >= SAVE_EVERY_MS) save_locked(), *last_save = now;
}
static void playtime_main(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
uint64_t last = plat_now_ms(), last_save = last, frames = gfx_stats().frames;
for (;;) {
plat_sleep_ms(TICK_MS);
uint64_t now = plat_now_ms(), f = gfx_stats().frames;
int playing = f != frames && gfx_window_focused();
plat_mutex_lock(&lock);
tick_locked(now, now - last, playing, &last_save);
plat_mutex_unlock(&lock);
last = now, frames = f;
}
}
// The file is read at once, so the Game tab shows the total from the first frame. A scripted run (--seconds: benches,
// menu shots, input scripts) shows it but adds nothing: those runs are not play.
void playtime_init(void) {
static int32_t started;
if (plat_xchg32(&started, 1)) return;
plat_mutex_lock(&lock);
load(rt_config()->profile);
int on = loaded == 1;
plat_mutex_unlock(&lock);
if (!on) return;
if (rt_config()->seconds > 0) {
rt_log("playtime: a scripted run (--seconds); play time is shown, not counted");
return;
}
if (!plat_thread_spawn(playtime_main, NULL, "playtime", 0))
rt_log("playtime: cannot start its thread; play time is not counted");
}
// --probe-playtime: the file's parsing, a missing, a valid and a damaged file, the periodic save and the tick's cap, in
// a directory next to totk.exe. Needs no game files.
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL playtime: %s\n", what); } } while (0)
static void reset(void) { total_ms = session_ms = saved_ms = 0, loaded = 0, path[0] = 0; }
static int file_is(const char *p, const char *want) {
uint64_t n = 0;
uint8_t *b = plat_file_read(p, &n);
int ok = b && n == strlen(want) && !memcmp(b, want, n);
free(b);
return ok;
}
int probe_playtime(void) {
uint64_t s = 0;
CHECK(parse((const uint8_t *)"total_seconds 42\n", 17, &s) && s == 42, "a line with LF");
CHECK(parse((const uint8_t *)"total_seconds 42\r\n", 18, &s) && s == 42, "a line with CRLF");
CHECK(parse((const uint8_t *)"total_seconds 42", 16, &s) && s == 42, "no line end");
CHECK(parse((const uint8_t *)"\xEF\xBB\xBFtotal_seconds 7\n", 19, &s) && s == 7, "a byte order mark");
CHECK(!parse((const uint8_t *)"total_seconds 42x", 17, &s), "a character after the number");
CHECK(!parse((const uint8_t *)"total_seconds -1\n", 17, &s), "a negative number");
CHECK(!parse((const uint8_t *)"total_seconds 3153600001\n", 25, &s), "over a hundred years");
CHECK(!parse((const uint8_t *)"seconds 42\n", 11, &s), "another key");
CHECK(!parse((const uint8_t *)"", 0, &s), "an empty file");
char dir[1024];
if (!plat_exe_path(dir, sizeof dir)) return 1;
char *cut = plat_last_sep(dir);
if (!cut) return 1;
strcpy(cut + 1, "playtime-probe");
plat_file_mkdir_p(dir);
char file[1100], bad[1110];
snprintf(file, sizeof file, "%s/playtime.txt", dir);
snprintf(bad, sizeof bad, "%s.bad", file);
plat_file_delete(file);
plat_file_delete(bad);
uint64_t total, session, last_save = 0;
reset();
load(dir);
CHECK(playtime_get(&total, &session) && total == 0, "a missing file counts from zero");
tick_locked(1000, 1000, 1, &last_save);
tick_locked(2000, 1000, 0, &last_save);
CHECK(plat_file_size(file) < 0, "nothing is written before SAVE_EVERY_MS");
tick_locked(2000 + 3600000, 3600000, 1, &last_save); // a machine that slept: one tick adds TICK_CAP_MS, and saves
playtime_get(&total, &session);
CHECK(total == 3 && session == 3, "played ticks count, an idle one does not, a long one is capped");
CHECK(file_is(file, "total_seconds 3\n"), "the save after SAVE_EVERY_MS writes the total");
reset();
load(dir);
playtime_get(&total, &session);
CHECK(total == 3 && session == 0, "the next run reads the total back");
static const char damaged[] = "total_seconds twelve\n";
plat_file_write_atomic(file, damaged, sizeof damaged - 1);
reset();
load(dir);
playtime_get(&total, &session);
CHECK(total == 0, "a damaged file counts from zero");
CHECK(file_is(bad, damaged), "a damaged file is kept whole as playtime.txt.bad");
plat_file_delete(file);
plat_file_delete(bad);
reset();
fprintf(stderr, "playtime: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
return fails ? 1 : 0;
}
+356 -52
View File
@@ -14,10 +14,11 @@
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
// +56 the camera's aspect ratio as a float when the output is not 16:9 (set aspect), +60 the field-of-view factor
// (set fov, res_fov_live).
// 1.4.3 scales the scene it reads by output width / 1920 (a 2560x1440 output renders its 1600x900 scene at 2128x1200;
// MacNativeGames measured the same), so its page holds the wanted size divided by that factor and the game's 5/6 needs
// no scene patch. 1.2.1 renders the size it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and
// the scene set is on whenever the output differs from 1920x1080.
// 1.4.3 scales the scene it reads by canvas width / the console mode's base width, 1920 docked and 1280 handheld (a
// 2560x1440 output renders its 1600x900 scene at 2128x1200 docked; MacNativeGames measured the same), so its page holds
// the wanted size divided by that factor (scene_scale) and the game's 5/6 needs no scene patch. 1.2.1 renders the size
// it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and the scene set is on whenever the
// output differs from 1920x1080.
//
// Heap: agl's DynamicTextureAllocator is one fixed heap, "DynamicTextureAllocatorMem" in
// System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs (113664 KiB, carved from "Graphics", 201728 KiB). At 2560x1440
@@ -27,6 +28,9 @@
// rewritten table (res_heapdef_build) in place of the original; memory.c adds the amount to the application pool, so
// no other heap shrinks. Without the rewritten table (a RomFS that lacks it, a format this code does not know) the
// patch sets stay off and the game keeps its own sizes.
#ifndef _WIN32
#include <dirent.h>
#endif
#include <math.h>
#include <stdint.h>
#include <stdio.h>
@@ -36,6 +40,8 @@
#include "cfg.h"
#include "host.h"
#include "mods.h"
#include "ov.h"
#include "platform/platform.h"
#define BASE_W 1920
@@ -43,7 +49,10 @@
#define SCENE_W 1600
#define SCENE_H 900
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
#define SCENE_MAX 4096 // per side, as MacNativeGames' TOTK_RENDER_RES
#define TEX_HEAP_KIB 506880u // TextureHeap, shipped (1.2.1 and 1.4.3): what a mod's own HeapDef is measured against
#define SCENE_MAX 8192 // per side
#define SCENE_KEEP 4096 // per side: the heap bound shrinks no scene below this (scene_for)
#define DTA_MAX_KIB 2031616u // DynamicTextureAllocatorMem grown at most to 1984 MiB, under 2 GiB (decide)
#define SHADOW_BASE 960 // the sun shadow map's base size (its cascades: a D16 array of 4 layers)
static struct {
@@ -53,12 +62,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 +80,84 @@ 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
}
static uint64_t shadow_kib(void);
// The scene size for a share k of the output, within SCENE_MAX a side and within what a texture heap of DTA_MAX_KIB
// holds next to the output and the shadow map (res_heap_extra_kib's growth). 1.2.1 already misplaced textures in a
// pool past 2 GiB, where a signed 32-bit offset turns negative (recomp/patches-v121.txt, set pooloff); larger heaps
// have not been run. The heap bound never takes the scene below 4096 a side, the size earlier builds gave: an output
// near 8K grows the heap past 2 GiB for its own targets, and its scene stays what it was. Needs R.shadow. 1 when the
// scene was made smaller than k asks.
static int 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));
double keep = fmin(1.0, fmin(SCENE_KEEP / sw, SCENE_KEEP / sh));
double room = (double)(DTA_MAX_KIB - shadow_kib()) / DTA_KIB - 1.0 -
fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0);
double most = (1.0 + fmax(0.0, room)) * SCENE_W * SCENE_H; // scene pixels that fit
if (sw * sh * fit * fit > most) fit = fmax(keep, sqrt(most / (sw * sh)));
*w = (unsigned)(sw * fit + 0.5);
*h = (unsigned)(sh * fit + 0.5);
return fit < 1.0;
}
// 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;
}
// A game resolution that is not 16:9 needs the HUD's layout for its shape (romfs-patches-<w>x<h>, romfs.c
// overlay_init), which the installer makes but a developer build only has after tools/make_romfs_patches.py --aspect.
// Without it the camera would widen while the 16:9 HUD stretched across the picture, so the game draws the largest
// 16:9 size that fits inside the asked one and the camera aspect patch stays off (res_init); gfx_present.c shows the
// picture with bars (present setting aspect). Not when TOTK_ROMFS_PATCHES names the overlay itself or the game version
// has no camera aspect patch. TOTK_OUTPUT_RES is restart-only (cfg.c, RESTART), so this one decision holds all run.
static void shape_fallback(void) {
const char *own = cfg_get("TOTK_ROMFS_PATCHES");
unsigned out[2] = {R.ow, R.oh};
if (own || !res_wants_wide_overlay(out) || romfs_shaped_ready(R.ow, R.oh)) return;
unsigned h = R.oh, w = R.ow;
if ((uint64_t)w * 9 <= (uint64_t)h * 16) h = (unsigned)((uint64_t)w * 9 / 16) & ~1u;
w = (unsigned)((uint64_t)h * 16 / 9) & ~1u;
rt_log("resolution: no romfs-patches-%ux%u for the HUD at this shape; the game draws %ux%u (16:9) and the present "
"adds bars. Make the layout with: tools/make_romfs_patches.py --version <v121|v143> --aspect %ux%u --out "
"build/<version>/romfs-patches-%ux%u", R.ow, R.oh, w, h, R.ow, R.oh, R.ow, R.oh);
R.ow = w, R.oh = h;
}
static void decide(void) {
if (R.decided) return;
R.decided = 1;
@@ -83,31 +169,13 @@ static void decide(void) {
if (sscanf(o, "%ux%u%c", &w, &h, &tail) == 2 && w >= 1280 && h >= 720 && w <= 7680 && h <= 4320) {
R.ow = w & ~1u;
R.oh = h & ~1u;
shape_fallback();
} else {
rt_log("resolution: TOTK_OUTPUT_RES=%s is not <width>x<height> in 1280x720..7680x4320; 1920x1080", o);
}
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,17 +185,30 @@ 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);
const char *sr = cfg_value("TOTK_SHADOW_RES");
if (strcmp(sr, "game")) {
unsigned n = (unsigned)atoi(sr);
if (n >= 256 && n <= 4096 && !(n & 7)) R.shadow = n;
else rt_log("resolution: TOTK_SHADOW_RES=%s is not game or a size 256..4096 (a multiple of 8); game", sr);
}
// 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.scene_set = 1;
if (scene_for(sk, &R.sw, &R.sh)) {
R.scene_set = 1;
rt_log("resolution: a %.0fx%.0f scene is larger than the port allows (8192 a side, a texture heap under 2 GiB "
"past 4096 a side); %ux%u", R.ow * sk, R.oh * 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;
}
// Texture heap for a sun shadow map above the base size: the four layers of 2 bytes, three maps in flight (the engine
@@ -144,13 +225,51 @@ 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
}
// 1.4.3's factor between the scene size it reads from the page and the one it renders. The game's scene setter
// (main+0x585e48 and the code before it) multiplies the page's size by the canvas width (display configuration +0x10)
// over the current console mode's base width (+0xf4 of the mode's entry, mode 1 handheld: main+0x2dac1c stores
// GetOperationMode != 1 at +0x546). The base is 1280 handheld and 1920 docked. The canvas is the output whenever the
// scene patch is on: docked the game's own canvas is 1920, handheld res_init switches the output patch on (a 1280 base
// with the game's 1280x720 handheld canvas would render R.ow / 1280 times the scene). 1.2.1 renders the size it reads.
static double scene_scale(void) {
if (!RT_V143) return 1.0;
return (double)R.ow / (ov_handheld() ? 1280 : BASE_W);
}
uint64_t res_heap_extra_bytes(void) { return ((res_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
// TOTK_TEX_HEAP_EXTRA_MB (0 to 4096): more room in "TextureHeap" and its parent "ゲームグラフィックス", the heap the
// world's textures stream into. HD texture packs made for emulators with 8 GB of DRAM grow it by about 3.3 GiB in their
// own HeapDef; without the room the game runs out and starts its panic Blood Moon over and over. The pool and the heap
// cap grow by the same amount (memory.c mem_init).
static uint64_t mod_tex_heap_kib(void);
static uint64_t cfg_tex_heap_kib(void) {
const char *v = cfg_get("TOTK_TEX_HEAP_EXTRA_MB");
if (!v || !*v) return 0;
char *end;
long mb = strtol(v, &end, 10);
if (end == v || *end || mb < 0 || mb > 4096) {
static int logged;
if (!logged++) rt_log("resolution: TOTK_TEX_HEAP_EXTRA_MB=%s is not 0..4096; 0", v);
return 0;
}
return (uint64_t)mb << 10;
}
// The larger of TOTK_TEX_HEAP_EXTRA_MB and what an enabled mod's own HeapDef adds to the texture heap (mod_tex_heap_kib).
static uint64_t tex_heap_extra_kib(void) {
uint64_t c = cfg_tex_heap_kib(), m = mod_tex_heap_kib();
return c > m ? c : m;
}
uint64_t res_tex_heap_extra_bytes(void) { return ((tex_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
// ---- the HeapDef table. BYML version 2..7, little endian ("YB"): a hash node is {u8 0xC1, u24 count} and count
// entries {u24 key index, u8 type, u32 value}; an int (type 0xD1) is stored in the entry, so a size changes in place.
typedef struct { uint8_t *p; size_t n; uint32_t keys; } Byml;
@@ -188,28 +307,103 @@ static size_t heap_size_field(const Byml *b, uint32_t heaps, const char *heap) {
return at && u32_at(b, at, &node) ? field(b, node, "Size", 0xd1) : 0;
}
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
uint64_t extra = res_heap_extra_kib();
*out = NULL;
if (!extra) return 0;
// A HeapDef in b's place (zstd frame in[0..n) with its content size, no dictionary): b.p malloc'd and *heaps its
// "Heaps" node; 0 when it is not one (b.p freed). why: what the log line says the caller keeps then.
static int heapdef_open(const void *in, size_t n, Byml *b, uint32_t *heaps, const char *why) {
b->p = NULL;
if (ZSTD_getDictID_fromFrame(in, n)) {
rt_log("resolution: the HeapDef table needs a zstd dictionary; the game keeps its own resolution");
rt_log("resolution: the HeapDef table needs a zstd dictionary; %s", why);
return 0;
}
unsigned long long raw = ZSTD_getFrameContentSize(in, n);
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > (64u << 20)) {
rt_log("resolution: the HeapDef table is not a zstd frame with a size; the game keeps its own resolution");
rt_log("resolution: the HeapDef table is not a zstd frame with a size; %s", why);
return 0;
}
Byml b = {malloc((size_t)raw), (size_t)raw, 0};
if (!b.p) return 0;
size_t got = ZSTD_decompress(b.p, b.n, in, n);
uint32_t root, heaps = 0;
*b = (Byml){malloc((size_t)raw), (size_t)raw, 0};
if (!b->p) return 0;
size_t got = ZSTD_decompress(b->p, b->n, in, n);
uint32_t root;
uint16_t ver = 0;
if (!ZSTD_isError(got) && got == b.n && b.n >= 16) memcpy(&ver, b.p + 2, 2);
size_t heaps_at = 0, dta_at = 0, gfx_at = 0;
if (ver >= 2 && ver <= 7 && !memcmp(b.p, "YB", 2) && u32_at(&b, 4, &b.keys) && u32_at(&b, 12, &root) &&
(heaps_at = field(&b, root, "Heaps", 0xc1)) && u32_at(&b, heaps_at, &heaps)) {
if (!ZSTD_isError(got) && got == b->n && b->n >= 16) memcpy(&ver, b->p + 2, 2);
size_t heaps_at = 0;
if (ver >= 2 && ver <= 7 && !memcmp(b->p, "YB", 2) && u32_at(b, 4, &b->keys) && u32_at(b, 12, &root) &&
(heaps_at = field(b, root, "Heaps", 0xc1)) && u32_at(b, heaps_at, heaps))
return 1;
free(b->p);
b->p = NULL;
return 0;
}
// HD texture packs made for the emulators ship their own HeapDef with "TextureHeap" and its parent grown (HDPacks.com's
// 4K pack for TotK: by 3,419,000 KiB, 3.3 GiB, for the emulators' 8 GB memory option). Served as it is, the game asks
// for that room and the application pool has none: it crashed at start (issue #673). So before mem_init sizes the pool,
// every switched-on mod's HeapDef is read (mods.c mods_peek_romfs: mods_scan needs paths main sets later) and the
// largest TextureHeap growth over the shipped size counts as TOTK_TEX_HEAP_EXTRA_MB (the larger of the two, at most
// 4 GiB). romfs.c heapdef_overlay then serves another table grown by that amount in the pack's place: the table of the
// highest switched-on mod below it whose texture heap is not grown, else the game's own, so the other heaps keep the
// sizes the content that runs needs.
#define HEAPDEF_REL "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
static struct { int done; uint64_t kib; } MT;
// "TextureHeap" of the HeapDef table in[0..n) in KiB; 0 when the table cannot be read or has no such size (the failure
// is logged with why).
static int32_t heapdef_tex_kib(const void *in, size_t n, const char *why) {
Byml b;
uint32_t heaps;
size_t th_at = 0;
int32_t th = 0;
if (!heapdef_open(in, n, &b, &heaps, why)) return 0;
if ((th_at = heap_size_field(&b, heaps, "TextureHeap"))) memcpy(&th, b.p + th_at, 4);
else rt_log("resolution: the HeapDef table has no TextureHeap size; %s", why);
free(b.p);
return th;
}
static void mod_heapdef(const char *name, const char *romfs, void *ctx) {
(void)ctx;
char path[2300], why[400];
if (snprintf(path, sizeof path, "%s/%s", romfs, HEAPDEF_REL) >= (int)sizeof path) return;
uint64_t n = 0;
uint8_t *in = plat_file_read(path, &n);
if (!in) return;
snprintf(why, sizeof why, "mod %s: its texture heap growth is left out", name);
int32_t th = heapdef_tex_kib(in, (size_t)n, why);
free(in);
if (!th) return;
int64_t grow = (int64_t)th - (int64_t)TEX_HEAP_KIB;
if (grow > (int64_t)(4096u << 10)) grow = 4096u << 10;
if (grow > 0 && (uint64_t)grow > MT.kib) MT.kib = (uint64_t)grow;
rt_log("resolution: mod %s ships a HeapDef with TextureHeap %d KiB (the game's %u KiB)%s", name, th, TEX_HEAP_KIB,
grow > 0 ? ": the port grows the texture heap and the pool by the difference" : "");
}
static uint64_t mod_tex_heap_kib(void) {
if (!MT.done) MT.done = 1, mods_peek_romfs(mod_heapdef, NULL);
return MT.kib;
}
// Only such a table is swapped for the game's own (romfs.c heapdef_overlay): a mod table that changes other heaps
// keeps them. A table it cannot read stays the base (logged).
int res_heapdef_tex_grown(const void *in, size_t n) {
return heapdef_tex_kib(in, n, "the mod's table stays the base") > (int32_t)TEX_HEAP_KIB;
}
// A table that can stand in for an HD pack's (romfs.c heapdef_overlay): readable, with TextureHeap at most the shipped
// size. A content mod's table (The Second Quest grows RSDB and UI for its own files) is one.
int res_heapdef_plain(const void *in, size_t n) {
int32_t th = heapdef_tex_kib(in, n, "it does not stand in for the texture pack's table");
return th > 0 && th <= (int32_t)TEX_HEAP_KIB;
}
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
uint64_t extra = res_heap_extra_kib(), tex = tex_heap_extra_kib();
*out = NULL;
if (!extra && !tex) return 0;
Byml b;
uint32_t heaps = 0;
size_t dta_at = 0, gfx_at = 0;
if (heapdef_open(in, n, &b, &heaps, "the game keeps its own resolution")) {
dta_at = heap_size_field(&b, heaps, "DynamicTextureAllocatorMem");
gfx_at = heap_size_field(&b, heaps, "Graphics");
}
@@ -225,6 +419,26 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
int32_t dta2 = dta + (int32_t)extra, gfx2 = gfx + (int32_t)extra;
memcpy(b.p + dta_at, &dta2, 4);
memcpy(b.p + gfx_at, &gfx2, 4);
int32_t th = 0, th2 = 0, gg = 0, gg2 = 0;
if (tex) { // TOTK_TEX_HEAP_EXTRA_MB: the world texture heap and its parent grow by the same amount
static const char parent[] = "\xe3\x82\xb2\xe3\x83\xbc\xe3\x83\xa0\xe3\x82\xb0\xe3\x83\xa9\xe3\x83\x95"
"\xe3\x82\xa3\xe3\x83\x83\xe3\x82\xaf\xe3\x82\xb9"; // ゲームグラフィックス
size_t th_at = heap_size_field(&b, heaps, "TextureHeap"), gg_at = heap_size_field(&b, heaps, parent);
if (th_at && gg_at) {
memcpy(&th, b.p + th_at, 4);
memcpy(&gg, b.p + gg_at, 4);
th2 = th + (int32_t)tex, gg2 = gg + (int32_t)tex;
memcpy(b.p + th_at, &th2, 4);
memcpy(b.p + gg_at, &gg2, 4);
} else {
rt_log("resolution: the HeapDef table has no TextureHeap sizes; TOTK_TEX_HEAP_EXTRA_MB left out");
tex = 0;
if (!extra) {
free(b.p);
return 0;
}
}
}
size_t cap = ZSTD_compressBound(b.n);
void *z = malloc(cap);
size_t zn = z ? ZSTD_compress(z, cap, b.p, b.n, 9) : 0;
@@ -235,7 +449,12 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
}
*out = z;
*out_n = zn;
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx, gfx2);
if (extra)
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx,
gfx2);
if (tex)
rt_log("resolution: HeapDef TextureHeap %d -> %d KiB and its parent %d -> %d KiB (TOTK_TEX_HEAP_EXTRA_MB or a "
"mod's HeapDef)", th, th2, gg, gg2);
return 1;
}
@@ -298,17 +517,23 @@ void res_init(void) {
return;
}
uint8_t *page = rt_patch_page();
double k = RT_V143 ? (double)R.ow / BASE_W : 1.0; // 1.4.3's own scaling of the scene with the output width
double k = scene_scale();
int scene_on = R.scene_set || (!RT_V143 && (R.ow != BASE_W || R.oh != BASE_H));
// Handheld, 1.4.3 draws a 1280x720 canvas unless the output patch is on, whatever the output setting (measured: a
// 1920x1080 output with the patch off composed the frame at 1280x720 and only the swapchain was 1920x1080). With
// the scene patch on the output patch is switched on too, so the canvas is the output size that scene_scale and
// the heap size (res_heap_extra_kib) assume.
int output_on = R.ow != BASE_W || R.oh != BASE_H || (RT_V143 && scene_on && ov_handheld());
float sc[2] = {(float)(R.sw / k), (float)(R.sh / k)};
uint64_t out = pack(R.ow, R.oh), none = 0;
memcpy(page + 16, sc, sizeof sc);
memcpy(page + 32, &out, 8);
memcpy(page + 40, &none, 8);
memcpy(page + 48, &out, 8);
rt_patch_switch(output, R.ow != BASE_W || R.oh != BASE_H);
rt_patch_switch(output, output_on);
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).
@@ -374,11 +599,90 @@ void res_fov_live(void) {
rt_log("resolution: field of view %d%% of the game's", pct);
}
// TOTK_DYNAMIC_RES = 0 holds the game's dynamic resolution at 100 % (recomp/patches-v121.txt and patches-v143.txt,
// set fixres), so the scene renders at the size render_scale chose. The game picks its tier (100, 90 or 80 %) mostly
// from a fixed cost model made for the Switch: one term grows with the object counts, one with (scene width / 1600)
// squared. The GPU time it also measures (two NVN report timestamps, a frame apart) only lowers that prediction when the
// GPU is slower than the model says, which a PC GPU never is, so the model alone decides. Measured on 1.2.1: a
// 2867-wide scene stays at 100 %, one wider than about 2970 does not, and a 4267-wide one (5120x1440 at render_scale
// 83) fits no tier and falls back to 80 % every frame, whatever the GPU's load. 1 (the default) leaves the game's own
// stepping and the set off. Runs on a settings change (gfx_present.c window_settings_live), like res_fov_live.
void res_dynres_live(void) {
static int set = -2, shown = -1;
if (set == -2) set = rt_patch_set("fixres");
int dyn = cfg_bool("TOTK_DYNAMIC_RES");
if (dyn == shown) return;
shown = dyn;
if (set < 0) {
if (!dyn) rt_log("resolution: this game version has no fixed-resolution patch; the game keeps its dynamic resolution");
return;
}
rt_patch_switch(set, !dyn);
rt_log("resolution: dynamic resolution %s",
dyn ? "on: the game's own 100, 90 or 80 %" : "off: the scene stays at 100 %");
}
void res_wanted(unsigned out[2]) {
decide();
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 = scene_scale();
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;
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff. Show more