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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #131

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

After a shader loads, every two-dimensional gradient pair has its inner components swapped by two new composite constructs, so the shader cache is untouched. TOTK_TXD_TRANSPOSE=0 turns it off. The finding is row 2.13 of docs/maxwell.md.
2026-11-02 11:59:29 +08:00
Valiantsin DzerakhandClaude Opus 5.5 7d9a45855d Merge task/dlssoff: no DLSS scene size while DLSS/DLAA is off (#435)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:36:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3de08398bf Size the scene by the DLSS mode only while DLSS/DLAA is on
With dlaa = 0 and dlss = quality left from the menu, the scene still rendered at 67 % and the plain upscaler
stretched it, so the picture looked blurrier with DLSS switched off.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:28:32 +03:00
FaserDocs 86a9ea6d53 Read installer sources once as UTF-8 in the catalog test 2026-11-01 15:54:57 +01:00
Wellington Dias cd0917e18d Add a dead zone slider per analog stick to the Controls tab, carrying an older stick_deadzone line over to both. 2026-11-01 02:08:15 -03:00
69 changed files with 1811 additions and 256 deletions

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- Shaders prepared at install instead of during play
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
([docs/tester-guide.md](docs/tester-guide.md), "Mods")
([docs/mods.md](docs/mods.md))
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
## Planned
+1 -1
View File
@@ -1 +1 @@
0.3.1
0.3.4
+6 -1
View File
@@ -95,7 +95,12 @@ of them.
```
`--cpu v2` targets x86-64-v2 (SSE4.2, no AVX2/FMA) for older CPUs; the default `v3` needs AVX2. The v2 build links
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower.
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower. Inline assembly,
target attributes and intrinsics are not bound by `-march`, so a v2 build runs `tools/check_isa.py` over the runtime
objects before it links: any AVX, BMI or other instruction above x86-64-v2 outside a function listed there as
dispatched by a runtime CPU check stops the build.
`gen.py` sizes its emit pool from the CPU count and the free memory; `--jobs N` sets it, and so does the environment variable `TOTK_LIFT_JOBS=N`, which the installer's lift step honours too (it has no `--jobs`). When a worker runs out of memory, the pool restarts with half the workers (`emit: out of memory with N workers, retrying with M`), down to one; only an out-of-memory error with one worker fails the step.
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
+12 -3
View File
@@ -1,6 +1,6 @@
# Cheats
The port reads Atmosphère cheat files, the format Switch homebrew and the emulators use. It ships no cheat files: a cheat file goes into a mod in the `mods` folder next to `totk.exe`, like any other mod ([tester-guide.md](tester-guide.md), "Mods"). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `totk.ini`.
The port reads Atmosphère cheat files, the format Switch homebrew and the emulators use. It ships no cheat files: a cheat file goes into a mod in the `mods` folder next to `totk.exe`, like any other mod ([mods.md](mods.md)). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `totk.ini`.
## Adding a cheat file
@@ -12,7 +12,16 @@ The port reads Atmosphère cheat files, the format Switch homebrew and the emula
| 1.4.3 | `277178B7DBA1B6D4.txt` |
The Cheats tab names the file the installed game reads when no mod holds one, and `logs\totk.log` says `cheats: no enabled mod holds cheats/<id>.txt`.
2. Put it in the `cheats` folder of a mod: `mods\My cheats\cheats\277178B7DBA1B6D4.txt`. A download laid out for the Switch's SD card or an emulator works as it is: unpack it into a folder of its own under `mods`, and the port finds `atmosphere\contents\0100F2C0115B6000\cheats`, `contents\0100F2C0115B6000\cheats` or `0100F2C0115B6000\cheats` inside it. The file name's case does not matter.
2. Put it in the `cheats` folder of a mod, a folder of its own under `mods`:
```
mods\
My cheats\
cheats\
277178B7DBA1B6D4.txt
```
A download laid out for the Switch's SD card or an emulator works as it is: unpack it into a folder of its own under `mods`, and the port finds `atmosphere\contents\0100F2C0115B6000\cheats`, `contents\0100F2C0115B6000\cheats` or `0100F2C0115B6000\cheats` inside it. The file name's case does not matter.
3. Start the game. Cheats that write the game's memory work now.
4. Cheats that change the game's code need the game rebuilt with them. When the game starts and finds such cheats it did not build, it opens the setup's rebuild window, which builds them in (a few minutes; the shaders stay prepared) and starts the game again. Until then they are greyed out with "after the rebuild". When the rebuild fails or you cancel it, the game starts without them and does not try again for the same cheats; "Rebuild the game now" on the Cheats tab tries again. `mods_rebuild = off` under `[mods]` in `totk.ini` turns the automatic rebuild off; running `TotK-Setup.exe` on the same folder does the same job by hand. Saves and settings are kept.
5. Switch the cheats on the Cheats tab of the menu.
@@ -39,7 +48,7 @@ Standard Atmosphère: a `[name]` line starts a cheat, `{name}` a master cheat th
The words above are placeholders: `XXXXXXXX` an offset, `YYYYYYYY` a value.
Each cheat's switch is `cheat_<hash> = 1`, where the hash is FNV-1a 32 of `<mod folder name>/<cheat name>`. A name repeated in one file gets `#2`, `#3` and so on. Renaming a mod's folder resets its cheats' switches to off.
Each cheat's switch is `cheat_<hash> = 1`, where the hash is FNV-1a 32 of `<mod folder name>/<cheat name>`. A name repeated in one file gets `#2`, `#3` and so on. The build names each code cheat the same way, so renaming a mod's folder resets its cheats' switches to off and makes the next start rebuild its code cheats. Rename the folder before you switch its cheats on.
## How cheats run
+1 -1
View File
@@ -94,7 +94,7 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `TOTK_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `TOTK_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
+13 -1
View File
@@ -149,11 +149,23 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
finishes.
## Switch system fonts
Simplified Chinese, Traditional Chinese and Korean text needs the Switch system fonts (`0-standard.bfttf` and the other
nn::pl shared fonts). The game does not ship them. Without them it exits before its first frame in those languages
(`rt_fatal` in `runtime/ov_system.c`). Supply them from your own console, in either way:
- Give the setup the firmware folder (`--firmware DIR`, or the window's firmware field); the Fonts step extracts them to
`shared-fonts/` in the install folder.
- Run `python tools/extract_fonts.py --firmware <dir>` with `.venv-py314` and the keys from `config.local.toml`, or
point `TOTK_SHARED_FONTS` at a folder that holds the extracted fonts.
## Linux release
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `TotK-Setup-x86_64.AppImage` (the static
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
Desktop Mode. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
Desktop Mode. The command-line setup needs the desktop session's display (`DISPLAY` or `WAYLAND_DISPLAY`) because preparing the
shaders opens a window; the check step fails at once without it. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
`libSDL3.so.0` beside it. The folder inside the AppImage:
+1
View File
@@ -68,6 +68,7 @@ Source shorthands:
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | yuzu: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); TotK on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `TOTK_GATHER_BIAS=0` turns it off. | — |
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
| 2.13 | **TXD gradient register order**: TotK's terrain shaders fill the gradient registers of a `TXD` per coordinate (du/dx, du/dy, dv/dx, dv/dy). | Vulkan's `Grad` takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). | Ryujinx's `EmitTxd` takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (1.4.3, the Depths; found by the user from their F9 frame, a gradient sample's coordinate and gradients traced back to `DPdx` and `DPdy` of the interpolants). Ours: `gfx_shader.c` `patch_txd_transpose` swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). `TOTK_TXD_TRANSPOSE=0` turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. | — |
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. yuzu offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `TOTK_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
| 2.14 | **Linear CPU-written textures** (`FLAGS_LINEAR`, small ramps, glyph caches): CPU writes visible to the GPU directly. | Must re-upload or use linear images over host memory. | NXN Mario: sky black without its 64×1 RGBA16F ramps. | NXN |
+107
View File
@@ -0,0 +1,107 @@
# Mods
TotK Recomp loads the mods players make for the Switch emulators (Ryujinx, yuzu and its forks): texture and model
packs, gameplay and text changes from GameBanana and similar sites, TKMM's merged output, code patches (`.ips`,
`.pchtxt`) and Atmosphère cheat files. No mod or cheat file ships with the port.
## Install a mod
1. Open the settings menu (F1, Esc, or Minus and Plus together on a controller), go to the Mods tab and choose
**Open mods folder**. That is the `mods` folder next to `totk.exe`; the menu creates it when it is missing.
2. Make one folder per mod in it and unpack the mod's archive into that folder. The mod's own folders (`romfs`,
`exefs`, `cheats`) must sit directly inside the mod's folder:
```
mods\
Better Grass\
romfs\
...
Fast Travel Anywhere\
exefs\
patch.pchtxt
My cheats\
cheats\
277178B7DBA1B6D4.txt
```
An archive laid out for a Switch SD card or an emulator works as it is. Inside the mod's folder the port also finds
`atmosphere\contents\0100F2C0115B6000\romfs`, `contents\0100F2C0115B6000\romfs` and `0100F2C0115B6000\romfs`, the
same places for `exefs` and `cheats`, and `atmosphere\exefs_patches\<name>\` for code patches.
3. Start the game. A mod that patches the game's code makes the game rebuild itself first (see Code patches below).
4. Open the Mods tab to check the mod. Each row says how many game files the mod replaces and adds, how many cheats it
brings, and why a part of it is not used.
## Order and switches
The Mods tab lists every folder in `mods`. Select a mod and press A or Enter to switch it on or off. The top face button
or Ctrl+Up moves it up, the left face button or Ctrl+Down moves it down. When two mods change the same file, the
higher one wins; the lower mod's row says how many of its files the higher one covers. Changes apply after a restart.
The menu saves the order and the switches in `totk.ini`, under `[mods]`, by folder name: `mods_order` and
`mods_disabled`. A folder the list does not name yet, such as a new or renamed one, goes below the named ones.
### Mods that change the same files
Two mods that edit the same game file do not combine: the higher one wins the whole file. Merge such mods with
[TKMM](https://github.com/TKMM-Team/Tkmm) (TotK Mod Manager) and set its export location to an empty folder inside
`mods`, for example `mods\TKMM`. The exported folder then counts as one mod here. Mods packaged for TKMM only
(`.tkcl` files) go the same way.
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
## Code patches (ExeFS)
A mod with `.ips` or `.pchtxt` files patches the game's code. A patch is for one game update: an `.ips` file is named
after the build id of the update's code, a `.pchtxt` file names it on its `@nsobid` line. The game's code is compiled
ahead of time, so these patches are built into `totk.exe`:
1. The first start after such a mod is added closes the game and opens the setup's rebuild window. The rebuild takes
a few minutes (the shaders stay prepared), then the game starts again. Saves and settings are kept.
2. Until the rebuild succeeds, the whole mod stays off, its other files included.
3. After the rebuild the mod switches on and off from the Mods tab like any other, without another rebuild.
The rebuild needs the setup folder the game was installed from. When that folder was moved or deleted, run
`TotK-Setup.exe` again on the same install folder: it does the same job. `mods_rebuild = off` under `[mods]` in
`totk.ini` turns the automatic rebuild off.
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
## Cheats
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
that change the game's code need the same rebuild as code patches. [cheats.md](cheats.md) explains the file names for
each game update, the rebuild and which cheats cannot run.
## Renaming a mod folder
The port keys a mod by its folder name. A renamed folder counts as a new mod:
- it moves below the mods the list names, and comes back switched on;
- its cheats come back switched off;
- its code patches and code cheats are built again, so the next start runs the rebuild.
Rename mod folders before you set them up in the menu, not after.
## What does not work
| On the Mods tab | Why | What to do |
|---|---|---|
| This folder has no romfs, exefs or cheats files. | The archive was unpacked one level too deep (`mods\Mod\Mod\romfs`), or it holds no game files. | Move the folders up so `romfs` sits directly in the mod's folder. |
| ... files are made for another game version and are left out. | The mod's files carry another update's version in their names (`.Product.<number>.`). | Download the mod's release for your game update. |
| Its code patches (exefs) are made for another game version. | The patch files name another update's build id. | Same: get the release for your update. |
| This mod brings its own game code (a subsdk plugin or a replaced main). | Mods such as UltraCam ship compiled Switch code. The port cannot load it. | No workaround; the whole mod stays off. |
| This mod patches the game code (exefs) in a way the port cannot apply. | The patch writes the game's data, not its code, or changes code in a form the port cannot build. | No workaround; the reason follows on the row. |
| The folder name contains \|. | The mod list uses `\|` between names. | Rename the folder. |
A mod never stops the game from starting. Whatever the port cannot use is left out and shown on the Mods tab.
## When something looks wrong
1. Switch the newest mod off on the Mods tab and restart. If the problem goes away, check that the mod is made for your
game update and does not fight another mod over the same files.
2. `logs\totk.log` next to `totk.exe` names every mod at the start (lines starting with `mods:`), the cheats
(`cheats:`) and the resource size table (`RomFS: resource size table:`). Open log folder on the Advanced tab of
the menu shows the logs.
3. When reporting a problem, attach `totk.log` and list your mods with the links you downloaded them from.
+8 -28
View File
@@ -39,6 +39,10 @@ build\<ver>\totk.exe
`TOTK_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
do one run with it and one without (`set TOTK_VK_VALIDATION=`).
If the game stops with a lost device (the log says `device lost` or `VK_ERROR_DEVICE_LOST`), the lines starting with
`vk: pass marks` name the render pass or dispatch the GPU was running. AMD GPUs write them by default. On an NVIDIA GPU,
`set TOTK_PASS_CRUMBS=1` (or GPU pass marks: On in the Advanced tab) before the run that reproduces the crash.
The game keeps running behind other windows (`assume_focus = 0` in `totk.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
the first minutes of play can stutter.
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
@@ -54,34 +58,10 @@ The line `vk: using gpu N: <name>` says which one ran.
## Mods
Mods made for emulators go into `mods/` next to `totk.exe`, one folder per mod, unpacked as they come:
`mods/<name>/romfs/...` (the Atmosphere layout `mods/<name>/atmosphere/contents/0100F2C0115B6000/romfs` works
too). The game loads them at the next start. The Mods tab of the in-game menu (F1) lists them: switch a mod off, or move
it up or down. When two mods change the same file, the higher one wins.
- RomFS mods (textures, models, text, gameplay files) work.
- Mods packaged for TKMM (`.tkcl`, "TKMM only") and mods that change the same files: merge them in TKMM and set an
export location to an empty folder inside `mods/` (for example `mods/TKMM`). That folder is then one mod here.
- Files made for another game version (names with `.Product.<number>.` that this version does not have) are left out;
the Mods tab counts them. Look for the mod's release for your game version.
- The resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`), which tells the game how
much memory each file needs, is rebuilt at every start with mods enabled: the tables the mods ship are merged and
the entries of files a mod made larger or added are raised. The log line `RomFS: resource size table: ...` says
how many. A mod's table for another game version still counts for that mod's own files.
- Mods that patch the game code (`exefs/*.ips`, `exefs/*.pchtxt`, or `atmosphere/exefs_patches/<name>/*.ips`) work
when the patches are for this game version and change only code: the game rebuilds itself with them at the next
start (the setup's rebuild window, a few minutes) and switches them with the mod. Patches for another version are
counted and left out; a file that writes the game's data, or that the port cannot express, keeps the whole mod off
and the Mods tab says why. A patch over one of the port's own patches turns that built-in patch off while the mod
is on (the Mods tab names it).
- Mods with their own code (`exefs/subsdk*` or a replaced `exefs/main`, such as UltraCam) never load, their RomFS files
included.
- Cheats: an Atmosphère cheat file goes into a mod's `cheats` folder (`mods/<name>/cheats/<build id>.txt`, the
build id `277178B7DBA1B6D4` for 1.4.3, `9B4E43650501A4D4` for 1.2.1). The Cheats tab of the menu switches them.
Cheats that write memory work at the next start; for cheats that change the game's code the game opens the setup's
rebuild window at start and comes back after it (a few minutes). The port ships no cheat files. Details: [cheats.md](cheats.md).
The log names every mod at the start (`mods: <name>: ...`) and how many of its files it serves.
Mods and cheats made for the emulators go into `mods/` next to `totk.exe`, one folder per mod. [mods.md](mods.md) covers
installing them, their order, code patches, what does not work and what to check when something looks wrong;
[cheats.md](cheats.md) covers cheat files. The log names every mod at the start (`mods: <name>: ...`); when you
report a problem with mods, list them with their download links.
## What to send back
+11 -3
View File
@@ -349,6 +349,11 @@ class Check(Step):
raise InstallError('check.keys_unreadable') from None
if 'header_key' not in key_values:
raise InstallError('check.keys_no_header_key')
if (sys.platform == 'linux' and ctx.will_run('prepare')
and not (os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))):
# totk --prepare-shaders opens a window and needs a present-capable queue family; without a display it
# fails after lift and compile (~15 min), so stop here
raise InstallError('check.no_display')
ctx.lang = lang
ctx.temp.mkdir(parents=True, exist_ok=True)
@@ -768,9 +773,12 @@ class Lift(Step):
transient = True
def key(self, ctx):
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
files = cheat_files(ctx) + exefs_files(ctx)
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *cheat_files(ctx),
*exefs_files(ctx))
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
def current(self, ctx, record):
gen = ctx.temp / 'gen'
@@ -835,7 +843,7 @@ def runtime_key(ctx):
tools. Shader preparation depends on these, not on the code cheats a lift adds."""
layout, cpu = ctx.layout, getattr(ctx, 'cpu', 'v3')
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py')]
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py', 'check_isa.py')]
return fingerprint('runtime-1', cpu, layout.runtime_lib_for(cpu), layout.runtime, layout.deps or 'system',
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
+2 -1
View File
@@ -51,6 +51,7 @@
"check.vulkan_missing": "No Vulkan driver is installed. Install the latest driver for your graphics card.",
"check.vulkan_no_device": "Vulkan finds no graphics card. Install the latest driver for your graphics card.",
"check.vulkan_too_old": "The game needs a graphics card with Vulkan 1.3; found: {devices}. Update the graphics driver.",
"check.no_display": "Preparing the shaders needs a display. Run the setup from the desktop session, or export DISPLAY=:0 (on a Steam Deck in Game Mode also XDG_RUNTIME_DIR=/run/user/1000).",
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.0 or 1.2.1 or 1.4.3 update and prod.keys.",
"check.input_missing": "File not found ({input}): {path}",
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
@@ -93,7 +94,7 @@
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts. Choose the firmware folder (Contents/registered) to install them; without it some text may not show.",
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, and none were given. The game will not start in this language until they are added. Choose the firmware folder (Contents/registered), or see \"Switch system fonts\" in docs/installer.md.",
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
+2 -1
View File
@@ -51,6 +51,7 @@
"check.vulkan_missing": "Nenhum driver Vulkan está instalado. Instale o driver mais recente da sua placa de vídeo.",
"check.vulkan_no_device": "O Vulkan não encontra nenhuma placa de vídeo. Instale o driver mais recente da sua placa de vídeo.",
"check.vulkan_too_old": "O jogo precisa de uma placa de vídeo com Vulkan 1.3; encontrado: {devices}. Atualize o driver de vídeo.",
"check.no_display": "Preparar os shaders exige uma tela. Execute a instalação na sessão da área de trabalho ou use export DISPLAY=:0 (no Steam Deck em Game Mode, também XDG_RUNTIME_DIR=/run/user/1000).",
"check.inputs_needed": "Os arquivos do jogo são necessários: escolha o jogo base, a atualização 1.1.0, 1.2.1 ou 1.4.3 e o prod.keys.",
"check.input_missing": "Arquivo não encontrado ({input}): {path}",
"check.keys_unreadable": "Não foi possível ler o arquivo de chaves. Escolha um arquivo prod.keys.",
@@ -93,7 +94,7 @@
"prepare.pack_failed": "Falha ao juntar o cache de shaders (código de saída {code}). Detalhes: {log}",
"cleanup.ini_failed": "Não foi possível gravar o totk.ini: {detail}",
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, que o instalador não extrai; parte do texto pode não aparecer.",
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, e nenhuma foi informada. O jogo não inicia neste idioma até que sejam adicionadas. Escolha a pasta do firmware (Contents/registered) ou veja \"Switch system fonts\" em docs/installer.md.",
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
+2 -1
View File
@@ -51,6 +51,7 @@
"check.vulkan_missing": "Драйвер Vulkan не установлен. Установите свежий драйвер видеокарты.",
"check.vulkan_no_device": "Vulkan не находит видеокарту. Установите свежий драйвер видеокарты.",
"check.vulkan_too_old": "Игре нужна видеокарта с Vulkan 1.3; найдено: {devices}. Обновите драйвер видеокарты.",
"check.no_display": "Для подготовки шейдеров нужен дисплей. Запустите установку из сеанса рабочего стола или выполните export DISPLAY=:0 (на Steam Deck в игровом режиме ещё XDG_RUNTIME_DIR=/run/user/1000).",
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.0 или 1.2.1 или 1.4.3 и prod.keys.",
"check.input_missing": "Файл не найден ({input}): {path}",
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
@@ -93,7 +94,7 @@
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch. Выберите папку прошивки (Contents/registered), чтобы установить их; без неё часть текста может не отображаться.",
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, а они не указаны. Игра не запустится на этом языке, пока их не добавят. Выберите папку прошивки (Contents/registered) или см. раздел «Switch system fonts» в docs/installer.md.",
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
+2 -1
View File
@@ -51,6 +51,7 @@
"check.vulkan_missing": "未安装 Vulkan 驱动。请为你的显卡安装最新的驱动程序。",
"check.vulkan_no_device": "Vulkan 找不到显卡。请为你的显卡安装最新的驱动程序。",
"check.vulkan_too_old": "游戏需要支持 Vulkan 1.3 的显卡;检测到:{devices}。请更新显卡驱动程序。",
"check.no_display": "准备着色器需要显示器。请在桌面会话中运行安装程序,或执行 export DISPLAY=:0(Steam Deck 游戏模式下还需 XDG_RUNTIME_DIR=/run/user/1000)。",
"check.inputs_needed": "需要游戏文件:请选择游戏本体、1.1.0、1.2.1 或 1.4.3 更新以及 prod.keys。",
"check.input_missing": "找不到文件({input}):{path}",
"check.keys_unreadable": "无法读取密钥文件。请选择一个 prod.keys 文件。",
@@ -93,7 +94,7 @@
"prepare.pack_failed": "打包着色器缓存失败(退出代码 {code})。详情:{log}",
"cleanup.ini_failed": "无法写入 totk.ini:{detail}",
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,而安装程序不会提取这些字体;部分文字可能无法显示。",
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,但未提供。在添加这些字体之前,游戏无法以该语言启动。请选择固件文件夹(Contents/registered),或参阅 docs/installer.md 中的“Switch system fonts”一节。",
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
+39 -11
View File
@@ -558,7 +558,8 @@ def _retry(fn, *args):
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
def _worker_init(mods, starts, table, opts):
def _worker_init(mods, starts, table, opts, jobs):
_W['jobs'] = jobs
_W['mods'] = mods
_W['starts'] = starts
_W['patches'] = table
@@ -581,6 +582,10 @@ def _emit_chunk(task):
"""Worker: write one chunk. Returns its statistics."""
name, number, path, fns = task
t0 = time.perf_counter()
# Test hook: a pool wider than this raises the MemoryError a busy machine does, so the retry can be exercised.
limit = os.environ.get('TOTK_LIFT_TEST_OOM_ABOVE')
if limit and _W['jobs'] > int(limit) and number % 2 == 1:
raise MemoryError
e = _emitter(name)
e.stats.clear()
e.unsupported.clear()
@@ -632,6 +637,14 @@ def pick_jobs(requested):
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
if requested:
return requested, '--jobs'
env = os.environ.get('TOTK_LIFT_JOBS', '').strip()
if env:
try:
if int(env) >= 1:
return int(env), 'TOTK_LIFT_JOBS'
except ValueError:
pass
print(f'ignoring TOTK_LIFT_JOBS={env!r}: not a positive integer', file=sys.stderr, flush=True)
cpus = os.cpu_count() or 4
free = hostmem.available_ram()
if free is None:
@@ -705,16 +718,31 @@ def main():
t_emit = time.perf_counter()
ctx = mp.get_context('spawn')
jobs, why = pick_jobs(args.jobs)
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
for i, r in enumerate(pool.imap_unordered(_emit_chunk, [t for _, t in tasks], chunksize=1)):
results[r['module']].append(r)
if r['error']:
errors.append(r['error'])
done_at[r['module']] = time.perf_counter() - t_emit
if (i + 1) % 200 == 0 or i + 1 == len(tasks):
print(f'emitted {i + 1}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s', file=sys.stderr,
flush=True)
pending = [t for _, t in tasks]
emitted = 0
while True:
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
try:
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
results[r['module']].append(r)
if r['error']:
errors.append(r['error'])
done_at[r['module']] = time.perf_counter() - t_emit
emitted += 1
if emitted % 200 == 0 or emitted == len(tasks):
print(f'emitted {emitted}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s',
file=sys.stderr, flush=True)
break
except MemoryError:
if jobs == 1:
raise
smaller = jobs // 2
print(f'emit: out of memory with {jobs} workers, retrying with {smaller}', file=sys.stderr, flush=True)
jobs, why = smaller, 'after out of memory'
done = {(r['module'], r['number']) for rs in results.values() for r in rs}
pending = [t for t in pending if (t[0], t[1]) not in done]
for name, d in info.items():
rs = sorted(results[name], key=lambda r: r['number'])
+11
View File
@@ -82,6 +82,17 @@ site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
# Texture pool offsets past 2 GiB (MacNativeGames ca311e2d). The texture loader keeps a texture's place in its pool as a
# 32-bit offset (main+0xad4294: sub x25, x9, x10, stored as the word w25) and widens it as a signed number for
# nvnTextureBuilderSetStorage's offset and for SetPackagedTextureData's source (the pool's CPU address + offset). The
# shipped heap table keeps every pool far below 2 GiB; a texture pack's heap table with TOTK_APP_POOL raised can make
# one larger (MacNativeGames saw a 3.58 GiB pool), and every texture past 2 GiB then got storage 4 GiB below its pool.
# The offset is widened unsigned instead, the same value below 2 GiB, right up to 4 GiB. 1.4.3 needs no patch: its
# loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits.
set pooloff TOTK_POOL_OFFSET_PATCH 1
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
spin main ac600 ac620 ac630 # 13 instructions; loads
+23 -5
View File
@@ -29,9 +29,10 @@ static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
return false;
}
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C).
// Byte 0x0C must be 0xE1 (NDEF Magic / Type 2 Tag CC container byte).
if (data[0x0C] != 0xE1) {
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C). An amiibo's is F1 10 FF EE, not the E1 of a plain
// NDEF tag (3dbrew, "Amiibo"); the CC is not encrypted, so encrypted dumps carry it too.
static const uint8_t amiibo_cc[4] = {0xF1, 0x10, 0xFF, 0xEE};
if (memcmp(data + 0x0C, amiibo_cc, sizeof amiibo_cc)) {
return false;
}
@@ -106,17 +107,25 @@ bool amiibo_is_device_present(void) {
return res;
}
// While the game waits for a tag it stops and starts detection again several times a second (tens with the menu open),
// so the log names the first few and then a count every 200th start.
static uint64_t g_detect_starts;
static uint64_t g_detect_stop_ns; // the last stop: the menu holds "searching" across the game's restarts
void amiibo_start_detection(void) {
plat_mutex_lock(&g_amiibo_lock);
g_detecting = true;
rt_log("amiibo: start detection");
uint64_t n = ++g_detect_starts;
if (n <= 3) rt_log("amiibo: start detection");
else if (n % 200 == 0) rt_log("amiibo: detection started %llu times so far", (unsigned long long)n);
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_stop_detection(void) {
plat_mutex_lock(&g_amiibo_lock);
if (g_detecting) g_detect_stop_ns = plat_now_ns();
g_detecting = false;
rt_log("amiibo: stop detection");
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
plat_mutex_unlock(&g_amiibo_lock);
}
@@ -175,6 +184,15 @@ AmiiboDeviceState amiibo_get_device_state(void) {
return AMIIBO_DEV_STATE_INITIALIZED;
}
AmiiboDeviceState amiibo_get_reader_state(void) {
AmiiboDeviceState state = amiibo_get_device_state();
if (state != AMIIBO_DEV_STATE_INITIALIZED) return state;
plat_mutex_lock(&g_amiibo_lock);
bool recent = g_detect_stop_ns && plat_now_ns() - g_detect_stop_ns < 1000000000ull;
plat_mutex_unlock(&g_amiibo_lock);
return recent ? AMIIBO_DEV_STATE_SEARCHING : state;
}
bool amiibo_load_file(const char *path) {
if (!path || !*path) return false;
uint64_t size = 0;
+3
View File
@@ -51,6 +51,9 @@ bool amiibo_is_mounted(void);
// State query
AmiiboDeviceState amiibo_get_device_state(void);
// The state the menu shows: searching also within a second after a stop, since the game restarts detection many times
// a second while it waits for a tag. The game reads amiibo_get_device_state.
AmiiboDeviceState amiibo_get_reader_state(void);
// File loading & triggering actions
bool amiibo_load_file(const char *path);
+7 -4
View File
@@ -22,8 +22,11 @@ struct AudioEffectDsp {
};
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
/* The reference DelayLine wraps at CurrentSampleCount-1, including when its
backing allocation is larger. Preserve this timing for sample comparisons. */
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
per turn every tap reads a slot that is never written (a one-sample dropout
per tap, audible as grain). The cursor and the taps share one ring length here;
each delay is one sample longer than the reference. */
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
static void line_init(DelayLine *d, unsigned rate, float ms) {
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
@@ -32,12 +35,12 @@ static void line_init(DelayLine *d, unsigned rate, float ms) {
static float line_read(const DelayLine *d) { return d->data[d->at]; }
static float line_update(DelayLine *d,float v) {
float old=line_read(d);d->data[d->at++]=v;
if(d->at >= (d->count?d->count-1:0))d->at=0;
if(d->at >= d->count)d->at=0;
return old;
}
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
int at=(int)d->at-(int)sample-offset;
if(at<0)at+=(int)d->count;
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
return d->data[at];
}
+61 -5
View File
@@ -8,7 +8,7 @@
// 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
// plays silence and never blocks the render thread or the guest. TOTK_AUDIO=none (or TOTK_AUDIO_OFF) mixes without a
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20) is the queued audio the output waits for after start or underrun.
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
@@ -90,8 +90,15 @@ static void report_once(Mixer *m, unsigned bit, const char *msg, int value) {
// ---- output: SDL3 audio stream fed from the ring
// How much audio the output waits for before it starts, and again after an underrun: TOTK_AUDIO_PREFILL_MS rounded up
// to whole 5 ms render blocks. Returns -1 for a value outside 0..500 ms.
// The Linux default is higher: at 20 ms PipeWire's queue (max(prefill, two 1024-sample periods)) underran
// thousands of times within minutes in busy scenes, and 60 ms removed the underruns.
#ifdef __linux__
#define PREFILL_DEFAULT_MS 60
#else
#define PREFILL_DEFAULT_MS 20
#endif
static int prefill_blocks_for(const char *e) {
if (!e || !*e) return 4;
if (!e || !*e) return PREFILL_DEFAULT_MS / 5;
char *end;
double ms = strtod(e, &end);
if (end == e || *end || !(ms >= 0) || ms > 500) return -1;
@@ -232,6 +239,34 @@ static void output_push(Mixer *m, const float *lr, int frames) {
if (!m->device_ok) atomic_store(&m->rd, wr + (uint64_t)frames); // no device: the ring only keeps the latest frames
}
// TOTK_AUDIO_DUMP=<path>: the sink mix before the clamp as raw interleaved float32 stereo, and every 5 s one log line with
// how many frames were rendered less than 1 ms after the one before (a burst refill). File writes on the render thread:
// for diagnosis only.
static void sink_dump(const float *raw, int frames) {
static FILE *f;
static int opened;
static uint64_t last, window_start, total, bursts;
if (!opened) {
opened = 1;
const char *path = cfg_get("TOTK_AUDIO_DUMP");
if (path && *path) f = fopen(path, "wb");
}
if (!f) return;
fwrite(raw, sizeof(float), (size_t)frames * 2, f);
uint64_t now = kern_now_ns();
total++;
if (last && now - last < 1000000ull) bursts++;
last = now;
if (!window_start) window_start = now;
if (now - window_start >= 5000000000ull) {
rt_log("audio: dump: %llu frames in %.1f s, %llu rendered under 1 ms after the previous", (unsigned long long)total,
(now - window_start) / 1e9, (unsigned long long)bursts);
total = bursts = 0;
window_start = now;
fflush(f);
}
}
// Level meter (TOTK_TRACE=audio): one line per 5 s of output, and the first nonzero frame.
static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames) {
if (!(rt_trace & RT_TRACE_AUDIO)) return;
@@ -290,8 +325,8 @@ void audio_mixer_open(AudioRenderer *r) {
const char *pe = cfg_get("TOTK_AUDIO_PREFILL_MS");
m->prefill = prefill_blocks_for(pe);
if (m->prefill < 0) {
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using 20", pe);
m->prefill = 4;
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using %d", pe, PREFILL_DEFAULT_MS);
m->prefill = PREFILL_DEFAULT_MS / 5;
}
r->mixer = m;
atomic_store(&m->playing, r->state == 0);
@@ -505,8 +540,25 @@ static void walk_step(Walk *w) { /* the same transitions as advance_voice in a
walk_skip_empty(w);
}
/* Decodes `frames` output samples per channel into out[c][k]; the renderer advances the voice afterwards by the same amount. */
static double voice_step(const AudioRenderer *r, const ServerVoice *v) {
return v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
}
/* Source samples the voice consumes over `frames` output samples, counted with the exact accumulation voice_render steps by
(float rounding makes `fraction + seconds * rate * pitch` differ from it by one sample on some frames, a seam click);
leaves the fractional position in v->fraction. */
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames) {
const double step = voice_step(r, v);
double frac = v->fraction;
uint64_t consumed = 0;
for (int k = 0; k < frames; k++) {
frac += step;
while (frac >= 1.0) { frac -= 1.0; consumed++; }
}
v->fraction = frac;
return consumed;
}
static void voice_render(AudioRenderer *r, ServerVoice *v, float out[6][MAX_FRAME], int frames) {
const double step = v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
const double step = voice_step(r, v);
Walk w = {v, v->index, v->offset, v->loops_done, {0}, 0, v->adpcm.history[0], v->adpcm.history[1], v->adpcm.buffer, v->adpcm.at};
memcpy(w.valid, v->valid, sizeof w.valid);
walk_skip_empty(&w);
@@ -545,6 +597,7 @@ static void mix_aux(AudioRenderer *r, int mix, const AudrenEffectIn *e) {
uint8_t *send=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4), *ret=ipc_guest(p.return_info,128+(uint64_t)p.capacity*4);
uint32_t *sh=(uint32_t *)(send+64),*rh=(uint32_t *)(ret+64); int32_t *sd=(int32_t *)(send+128),*rd=(int32_t *)(ret+128);
uint32_t wr=sh[1],read=rh[0],total=p.channels*(uint32_t)m->frame;
if(e->enabled) report_once(m,6,"auxiliary effect in use, channels",(int)p.channels);
for(unsigned c=0;c<p.channels;c++) {
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("auxiliary effect mix buffer");
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
@@ -798,6 +851,7 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
if (sink && sink->input_count >= 1 && sink->input_count <= 6) {
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;
@@ -809,8 +863,10 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
} 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;
}
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);
+2 -2
View File
@@ -24,8 +24,8 @@ static void delay_set(Delay *d, unsigned n) { if (n > d->cap) rt_fatal("reverb d
static void delay_init(Delay *d, unsigned khz, float ms) { d->cap = samples(khz, ms); d->data = calloc(d->cap + 1, sizeof(float)); if (!d->data) rt_fatal("reverb delay allocation"); delay_set(d, d->cap); }
static float delay_update(Delay *d, float v) {
d->data[d->write++] = v; float out = d->data[d->read++];
if (d->read >= d->cap) d->read = 0;
if (d->write >= d->cap) d->write = 0;
if (d->read > d->cap) d->read = 0;
if (d->write > d->cap) d->write = 0;
return out;
}
static float delay_tap(const Delay *d, unsigned at) { int i = (int)d->write - (int)at - 1; if (i < 0) i += (int)d->cap + 1; return d->data[i]; }
+21 -6
View File
@@ -318,11 +318,9 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
static void advance_voice(AudioRenderer *r, ServerVoice *v) {
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
uint64_t samples = (uint64_t)want;
v->fraction = want - (double)samples;
uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count);
while (samples && v->valid[v->index]) {
AudrenWaveBuffer *b = &v->buffers[v->index];
uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset;
@@ -354,7 +352,19 @@ static void render_thread(void *p) {
plat_thread_priority(PLAT_PRIO_HIGHEST);
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
const double frame = (double)r->param.sample_count / r->param.sample_rate;
uint64_t next = kern_now_ns();
// When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target
// and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and
// the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux
// reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame
// apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls
// 480-sample periods, and a sink dump (TOTK_AUDIO_DUMP) of the bursts there showed no more seam spikes than
// chance, so a wait on this thread buys nothing.
#ifdef __linux__
const uint64_t gap_ns = frame_ns * 4 / 5;
#else
const uint64_t gap_ns = 0;
#endif
uint64_t next = kern_now_ns(), last = 0;
while (plat_cas32(&r->running, 0, 0)) {
uint64_t now = kern_now_ns();
int64_t pace = audio_mixer_pace_ns(r, now);
@@ -362,6 +372,10 @@ static void render_thread(void *p) {
plat_wait_ns((uint64_t)pace);
continue;
}
if (pace == 0 && last && now - last < gap_ns) {
plat_wait_ns(gap_ns - (now - last));
continue;
}
if (pace == 0) {
next = now + frame_ns; // where the timer goes on if the device stops pacing
} else {
@@ -369,11 +383,12 @@ static void render_thread(void *p) {
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
next += frame_ns;
}
last = kern_now_ns();
plat_mutex_lock(&r->lock);
if (r->state == 0) {
voices_prepare(r);
audio_mixer_render(r, frame);
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i], frame);
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]);
r->elapsed_frames++;
uint64_t given = r->stop_released + r->empty_played + r->flushed;
if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) {
+1
View File
@@ -184,5 +184,6 @@ void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voic
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
// device stalled).
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
+5 -4
View File
@@ -44,7 +44,7 @@ static const char *const known[] = {
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC",
@@ -53,7 +53,7 @@ static const char *const known[] = {
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
"SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS",
"ZSTD_CHECK", "ZSTD_NATIVE",
@@ -95,7 +95,7 @@ static const struct { const char *key, *section; } sections[] = {
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
};
@@ -122,7 +122,7 @@ static const CfgMeta metas[] = {
{"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},
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
{"SHADOW_RES", "game", "game|512|768|1024|1536|2048", RESTART},
{"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART},
{"SHADOW_SCALE", "1", "1..4", RESTART},
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
{"PCSS", "0", "0..200", RESTART},
@@ -167,6 +167,7 @@ static const CfgMeta metas[] = {
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
{"INPUT_RECORD", "", NULL, RESTART},
};
+18
View File
@@ -0,0 +1,18 @@
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
typedef void (*FolderPickDone)(const char *path, void *ctx);
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
// The caller keeps one dialog open at a time.
void folder_pick(const char *start, FolderPickDone done, void *ctx);
#ifdef __cplusplus
}
#endif
+42
View File
@@ -0,0 +1,42 @@
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
// thread, which runs SDL's event loop that delivers the portal's answer.
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "folder_pick.h"
#include "host.h"
#include "input_sdl.h"
typedef struct {
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
FolderPickDone done;
void *ctx;
} PickReq;
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
(void)filter;
PickReq *r = userdata;
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
r->done(list && list[0] ? list[0] : NULL, r->ctx);
free(r);
}
static void show(void *arg) {
PickReq *r = arg;
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
}
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
PickReq *r = calloc(1, sizeof *r);
if (!r) {
done(NULL, ctx);
return;
}
r->done = done;
r->ctx = ctx;
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
gfx_window_sdl_run(show, r);
}
+84
View File
@@ -0,0 +1,84 @@
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
// game window, on a thread of its own so the present thread that draws the menu keeps running.
#include <windows.h>
#include <shobjidl.h>
#include <stdlib.h>
#include <string>
extern "C" {
#include "folder_pick.h"
#include "host.h"
#include "input_win.h"
#include "platform/platform.h"
}
struct PickReq {
std::string start;
FolderPickDone done;
void *ctx;
};
static std::wstring wide(const std::string &utf8) {
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
std::wstring w(n > 0 ? n : 1, L'\0');
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
w.resize(wcslen(w.c_str()));
return w;
}
static std::string utf8(const wchar_t *w) {
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
std::string s(n > 0 ? n : 1, '\0');
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
s.resize(strlen(s.c_str()));
return s;
}
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
static void set_start(IFileOpenDialog *d, const std::string &start) {
wchar_t full[MAX_PATH * 2];
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
IShellItem *item = NULL;
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
d->SetFolder(item);
item->Release();
}
}
static void pick_main(void *arg) {
PickReq *r = (PickReq *)arg;
std::string chosen;
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
IFileOpenDialog *d = NULL;
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
if (SUCCEEDED(hr)) {
FILEOPENDIALOGOPTIONS opt = 0;
d->GetOptions(&opt);
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
set_start(d, r->start);
hr = d->Show(gfx_window_handle());
IShellItem *item = NULL;
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
PWSTR path = NULL;
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
chosen = utf8(path);
CoTaskMemFree(path);
}
item->Release();
}
d->Release();
}
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
if (SUCCEEDED(co)) CoUninitialize();
delete r;
}
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
PickReq *r = new PickReq{start ? start : "", done, ctx};
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
rt_log("folder_pick: cannot start the dialog thread");
done(NULL, ctx);
delete r;
}
}
+48 -5
View File
@@ -125,12 +125,30 @@ static void make_dirs(const char *p) {
if (!make_dir(buf)) rt_fatal("fs: cannot create directory %s", buf);
}
// Remove a directory tree; a link (reparse point, symbolic link) is removed as a link, never followed. 0 also when
// The first path remove_tree could not delete and the OS error code, for the message of a fatal.
static char remove_failed[1100];
static int remove_error;
static void remove_note(const char *path) {
if (*remove_failed) return;
snprintf(remove_failed, sizeof remove_failed, "%s", path);
remove_error = plat_fs_os_error();
}
// Remove a directory tree; a link (symbolic link, junction) is removed as a link, never followed. 0 also when
// nothing is there.
static int remove_tree(const char *path) {
int kind = plat_fs_kind(path);
if (kind == PLAT_FS_MISSING) return 0;
if (kind == PLAT_FS_FILE) return plat_fs_unlink(path) ? -1 : 0;
if (kind == PLAT_FS_FILE) {
if (!plat_fs_unlink(path)) return 0;
remove_note(path);
return -1;
}
if (kind == PLAT_FS_LINK) { // a link to a file goes by unlink, a link to a directory by rmdir
if (!plat_fs_unlink(path) || !plat_fs_rmdir(path)) return 0;
remove_note(path);
return -1;
}
int bad = 0;
if (kind == PLAT_FS_DIR) {
Listing l;
@@ -142,7 +160,23 @@ static int remove_tree(const char *path) {
}
listing_free(l.e, l.n);
}
return bad | (plat_fs_rmdir(path) ? -1 : 0);
if (plat_fs_rmdir(path)) {
remove_note(path);
bad |= -1;
}
return bad;
}
// remove_tree, tried again every 50 ms for up to 2 s while another process (the previous run still exiting, a virus
// scanner, a sync client) holds a file in the tree. The failing path and its OS error stay in remove_failed.
#define MOVE_TRIES 40 // 2 s, also move_dir's
static int remove_tree_retry(const char *path) {
for (int i = 0;; i++) {
*remove_failed = 0;
if (!remove_tree(path)) return 0;
if (i == MOVE_TRIES) return -1;
plat_sleep_ms(50);
}
}
// ---- save transactions
@@ -190,7 +224,6 @@ static int exists(const char *p) { return plat_fs_kind(p) != PLAT_FS_MISSING; }
// Rename a directory, durably; retried every 50 ms, tries times in all, while another process (a virus scanner, the
// indexer) holds a file in it. 0 on success.
#define MOVE_TRIES 40 // 2 s
static int move_dir(const char *from, const char *to, int tries) {
for (int i = 0; i < tries; i++) {
int e = plat_fs_rename(from, to, 1);
@@ -446,7 +479,8 @@ static void save_recover(void) {
did = swap_in() ? "a finished commit still waits in save-tx; the game loads it from there"
: "completed a finished commit";
if (exists(work_root)) {
if (remove_tree(work_root)) rt_fatal("fs: cannot delete the old save working copy");
if (remove_tree_retry(work_root))
rt_fatal("fs: cannot delete the old save working copy: %s (OS error %d)", remove_failed, remove_error);
if (!strcmp(did, "nothing to recover"))
did = "deleted the last run's working copy (changes after its last Commit)";
}
@@ -1575,6 +1609,15 @@ static void probe_save_tx(const uint64_t *uid) {
CALL(S_UNMOUNT, U("ro"));
CHECK(RES(S_DELETE_FILE, U(a)) == 0 && RES(S_DELETE_FILE, U(b)) == 0 && tx_commit() == 0 &&
tx_read_committed("tx_a.sav") == -1, "Commit of deletions");
// a run that left read-only folders and files in the working copy (attributes another emulator's save carried):
// the next start deletes them and names the failing path if it cannot
char *ro_dir = join(work_root, "ro_dir"), *ro_file = join(ro_dir, "ro.sav");
CHECK(!plat_fs_mkdir(ro_dir) && plat_file_write(ro_file, "x", 1) && plat_file_set_readonly(ro_file, 1) &&
plat_file_set_readonly(ro_dir, 1), "read-only leftovers in the working copy");
probe_restart(uid);
CHECK(!exists(ro_dir), "the next start deleted a working copy with read-only folders");
free(ro_dir);
free(ro_file);
printf("save tx: uncommitted writes visible to the game and dropped by a restart and by Unmount, commits kept; a "
"crash at each of %d commit steps recovers to the old save %d times, the new one %d times, mixed %d "
"times; %d backups\n", steps, olds, news, mixed, backups);
+62
View File
@@ -759,6 +759,67 @@ out:
free(loc), free(packed), free(ttype), free(tcomps), free(pointee);
}
// Every gradient sample (OpImageSampleExplicitLod with Grad) whose two gradients are each built from two components gets
// them transposed, dx = (a, c) and dy = (b, d) from dx = (a, b) and dy = (c, d), by two new OpCompositeConstruct before
// it. TotK's shaders fill a TXD's gradient registers per coordinate (du/dx, du/dy, dv/dx, dv/dy), and the translator
// (Ryujinx, InstEmitTexture EmitTxd) builds dx = (first two registers), dy = (last two) = (du/dx, du/dy) and
// (dv/dx, dv/dy): a mip level chosen from the wrong derivatives, too sharp on terrain faces turned across the screen,
// which crawled in patches that followed the faces (1.4.3, the Depths). TOTK_TXD_TRANSPOSE=0 turns it off.
// Applied to the SPIR-V after it is loaded, so the shader cache is untouched.
enum { OP_COMPOSITE_CONSTRUCT = 80, OP_SAMPLE_EXPLICIT = 88, IO_GRAD = 4 };
static void patch_txd_transpose(GfxShader *s) {
static int on = -1;
static volatile int32_t n_logged;
if (on < 0) {
const char *e = cfg_get("TOTK_TXD_TRANSPOSE");
on = !(e && *e == '0');
}
uint32_t *w = s->spirv;
uint64_t n = s->words;
if (!on || n < 5) return;
uint32_t bound = w[3] < (1u << 20) ? w[3] : (1u << 20);
uint64_t *cc = calloc(bound, sizeof *cc); // word index of a 2-constituent OpCompositeConstruct defining the id
int found = 0;
if (!cc) return;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) goto out;
if (op == OP_COMPOSITE_CONSTRUCT && len == 5 && w[i + 2] < bound) cc[w[i + 2]] = i;
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1])
found++;
i += len;
}
if (!found) goto out;
uint32_t *o = malloc((n + (uint64_t)found * 10) * sizeof *o);
if (!o) goto out;
uint64_t k = 0;
uint32_t next = w[3];
for (uint64_t i = 0; i < 5; i++) o[k++] = w[i];
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1]) {
const uint32_t *dx = w + cc[w[i + 6]], *dy = w + cc[w[i + 7]]; // [hdr, type, id, a, b], [hdr, type, id, c, d]
uint32_t ndx = next++, ndy = next++;
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndx, o[k++] = dx[3], o[k++] = dy[3];
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndy, o[k++] = dx[4], o[k++] = dy[4];
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
o[k - len + 6] = ndx, o[k - len + 7] = ndy;
} else {
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
}
i += len;
}
o[3] = next;
free(s->spirv);
s->spirv = o, s->words = k;
if (plat_inc32(&n_logged) <= 4) rt_log("gfx_shader: %d gradient samples now read their derivatives per coordinate", found);
out:
free(cc);
}
static void reflect(GfxShader *s) {
static const char *const kinds[GFX_RES_KINDS] = {"cbufs", "sbufs", "textures", "images"};
const char *js = s->reflection;
@@ -771,6 +832,7 @@ static void reflect(GfxShader *s) {
json_objects(js, kinds[k], add_res, s, k);
}
reflect_spirv(s);
patch_txd_transpose(s);
}
// ---- use masks (see the cache key at the top)
+2
View File
@@ -64,6 +64,8 @@ double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, _
SDL_Window *gfx_window_sdl(void) { return g_win; }
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
// ---- settings: size and fullscreen
static char g_applied_size[32];
static int g_applied_fs;
+1
View File
@@ -11,6 +11,7 @@ extern "C" {
// gfx_window_sdl.c
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg); // runs fn on the window thread and waits; any thread
// input_sdl.c
void input_sdl_window(SDL_Window *w); // the game window was created
+122 -77
View File
@@ -415,6 +415,18 @@ static int ap_disabled(const RtThread *t) { return g_ap > 0 && ap_tlr(t)[0]; }
// in kern_ap_yield and jumps back to kern_ap_rip(), whose value it first stores in the return slot it reserved at
// entry, so a stack walk sees the interrupted function as its caller. No ret: the jump keeps call/return pairs
// balanced. The waiter never writes the owner's stack.
// An x86-64-v2 build (no __AVX__) saves XMM0-15 with SSE moves into the same 32-byte slots instead: its game code
// keeps nothing in the upper YMM halves, and the CPUs it is for have no VEX encoding, so a vmovdqa there faults with
// an illegal instruction the first time a core is interrupted.
#ifdef __AVX__
#define AP_VMOV "vmovdqa"
#define AP_V "%ymm"
#define AP_VZEROUPPER " vzeroupper\n"
#else
#define AP_VMOV "movdqa"
#define AP_V "%xmm"
#define AP_VZEROUPPER ""
#endif
#ifdef _WIN32
__asm__(".text\n"
".globl kern_ap_stub\n"
@@ -446,48 +458,48 @@ __asm__(".text\n"
".seh_setframe %rbp, 0\n"
".seh_endprologue\n"
" andq $-32, %rsp\n"
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
" vmovdqa %ymm0, 64(%rsp)\n"
" vmovdqa %ymm1, 96(%rsp)\n"
" vmovdqa %ymm2, 128(%rsp)\n"
" vmovdqa %ymm3, 160(%rsp)\n"
" vmovdqa %ymm4, 192(%rsp)\n"
" vmovdqa %ymm5, 224(%rsp)\n"
" vmovdqa %ymm6, 256(%rsp)\n"
" vmovdqa %ymm7, 288(%rsp)\n"
" vmovdqa %ymm8, 320(%rsp)\n"
" vmovdqa %ymm9, 352(%rsp)\n"
" vmovdqa %ymm10, 384(%rsp)\n"
" vmovdqa %ymm11, 416(%rsp)\n"
" vmovdqa %ymm12, 448(%rsp)\n"
" vmovdqa %ymm13, 480(%rsp)\n"
" vmovdqa %ymm14, 512(%rsp)\n"
" vmovdqa %ymm15, 544(%rsp)\n"
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
" stmxcsr 32(%rsp)\n"
" movl $0x1f80, 36(%rsp)\n"
" ldmxcsr 36(%rsp)\n"
" cld\n"
" vzeroupper\n"
AP_VZEROUPPER
" call kern_ap_rip\n"
" movq %rax, 72(%rbp)\n" // the return slot: rbp + 8 pushes + pushfq
" call kern_ap_yield\n"
" ldmxcsr 32(%rsp)\n"
" vmovdqa 64(%rsp), %ymm0\n"
" vmovdqa 96(%rsp), %ymm1\n"
" vmovdqa 128(%rsp), %ymm2\n"
" vmovdqa 160(%rsp), %ymm3\n"
" vmovdqa 192(%rsp), %ymm4\n"
" vmovdqa 224(%rsp), %ymm5\n"
" vmovdqa 256(%rsp), %ymm6\n"
" vmovdqa 288(%rsp), %ymm7\n"
" vmovdqa 320(%rsp), %ymm8\n"
" vmovdqa 352(%rsp), %ymm9\n"
" vmovdqa 384(%rsp), %ymm10\n"
" vmovdqa 416(%rsp), %ymm11\n"
" vmovdqa 448(%rsp), %ymm12\n"
" vmovdqa 480(%rsp), %ymm13\n"
" vmovdqa 512(%rsp), %ymm14\n"
" vmovdqa 544(%rsp), %ymm15\n"
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
" movq %rbp, %rsp\n"
" popq %rbp\n"
" popq %r11\n"
@@ -525,48 +537,48 @@ __asm__(".text\n"
" pushq %rbp\n"
" movq %rsp, %rbp\n"
" andq $-32, %rsp\n"
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
" vmovdqa %ymm0, 64(%rsp)\n"
" vmovdqa %ymm1, 96(%rsp)\n"
" vmovdqa %ymm2, 128(%rsp)\n"
" vmovdqa %ymm3, 160(%rsp)\n"
" vmovdqa %ymm4, 192(%rsp)\n"
" vmovdqa %ymm5, 224(%rsp)\n"
" vmovdqa %ymm6, 256(%rsp)\n"
" vmovdqa %ymm7, 288(%rsp)\n"
" vmovdqa %ymm8, 320(%rsp)\n"
" vmovdqa %ymm9, 352(%rsp)\n"
" vmovdqa %ymm10, 384(%rsp)\n"
" vmovdqa %ymm11, 416(%rsp)\n"
" vmovdqa %ymm12, 448(%rsp)\n"
" vmovdqa %ymm13, 480(%rsp)\n"
" vmovdqa %ymm14, 512(%rsp)\n"
" vmovdqa %ymm15, 544(%rsp)\n"
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
" stmxcsr 32(%rsp)\n"
" movl $0x1f80, 36(%rsp)\n"
" ldmxcsr 36(%rsp)\n"
" cld\n"
" vzeroupper\n"
AP_VZEROUPPER
" call kern_ap_rip\n"
" movq %rax, 88(%rbp)\n" // the return slot: rbp + 10 pushes + pushfq
" call kern_ap_yield\n"
" ldmxcsr 32(%rsp)\n"
" vmovdqa 64(%rsp), %ymm0\n"
" vmovdqa 96(%rsp), %ymm1\n"
" vmovdqa 128(%rsp), %ymm2\n"
" vmovdqa 160(%rsp), %ymm3\n"
" vmovdqa 192(%rsp), %ymm4\n"
" vmovdqa 224(%rsp), %ymm5\n"
" vmovdqa 256(%rsp), %ymm6\n"
" vmovdqa 288(%rsp), %ymm7\n"
" vmovdqa 320(%rsp), %ymm8\n"
" vmovdqa 352(%rsp), %ymm9\n"
" vmovdqa 384(%rsp), %ymm10\n"
" vmovdqa 416(%rsp), %ymm11\n"
" vmovdqa 448(%rsp), %ymm12\n"
" vmovdqa 480(%rsp), %ymm13\n"
" vmovdqa 512(%rsp), %ymm14\n"
" vmovdqa 544(%rsp), %ymm15\n"
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
" movq %rbp, %rsp\n"
" popq %rbp\n"
" popq %r11\n"
@@ -1888,7 +1900,9 @@ int kern_svc(Cpu *c, uint32_t imm) {
if (type != 0) {
int32_t nv = val + 1;
if (type == 2) {
if (cnt <= 0) nv = nw > 0 ? val - 1 : val + 1;
// Waking all (count <= 0) with waiters subtracts 2, not 1 (mesosphere
// KAddressArbiter::SignalAndModifyByWaitingCountIfEqual; yuzu, LAS Recomp and nxnative agree).
if (cnt <= 0) nv = nw > 0 ? val - 2 : val + 1;
else nv = nw == 0 ? val + 1 : nw <= cnt ? val - 1 : val;
}
uint32_t e = (uint32_t)val;
@@ -1952,8 +1966,13 @@ int kern_svc(Cpu *c, uint32_t imm) {
int idx = -1;
lock();
g_sync_calls++;
// Horizon's order (mesosphere KSynchronizationObject::Wait): a signalled object, then timeout 0, then a pending
// CancelSynchronization. The cancel is for the next wait that would block: a poll or a wait on a signalled
// object leaves it pending. A cancel that wakes this wait (CancelSynchronization wakes it with R_CANCELLED; a
// signal wakes it with -1 to look again) ends it cancelled, signalled objects or not.
uint32_t woke = 0;
for (;;) {
if (t->cancel) {
if (woke == R_CANCELLED) {
t->cancel = 0;
r = R_CANCELLED;
break;
@@ -1969,6 +1988,15 @@ int kern_svc(Cpu *c, uint32_t imm) {
r = R_OK;
break;
}
if (ns == 0) {
r = R_TIMEOUT;
break;
}
if (t->cancel) {
t->cancel = 0;
r = R_CANCELLED;
break;
}
int64_t left = ns;
if (ns > 0) {
uint64_t spent = kern_now_ns() - start;
@@ -1980,7 +2008,8 @@ int kern_svc(Cpu *c, uint32_t imm) {
}
t->wobj = n ? hs[0] | (uint64_t)n << 32 : 0;
g_sync_blocks++;
if (block(t, W_SYNC, left) == R_TIMEOUT) {
woke = block(t, W_SYNC, left);
if (woke == R_TIMEOUT) {
r = R_TIMEOUT;
break;
}
@@ -1995,7 +2024,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
RtThread *t = h_get((uint32_t)x[0], O_THREAD);
if (t) {
t->cancel = 1;
if (t->wkind == W_SYNC) wake(t, (uint32_t)-1);
if (t->wkind == W_SYNC) wake(t, R_CANCELLED);
}
unlock();
x[0] = t ? R_OK : R_BAD_HANDLE;
@@ -2056,10 +2085,19 @@ void kern_event_signal(uint32_t hw) {
// System events of host services: the SDK's own nn::os::CreateSystemEvent creates the kernel event (SVC 0x45) on a
// guest thread; the writable handle is kept, so any thread (host audio tick included) can signal it later.
// The guest may destroy such an event and attach the same SystemEventType again: the game does that with the nfp
// activate and deactivate events before each detection. nn::os::DestroySystemEvent zeroes the state byte at +0x28
// (0 not initialised, 1 event, 2 inter-process event; nn::os::TryWaitSystemEvent aborts on 0) and closes both
// handles, so a kept handle counts only while that byte is nonzero.
#define SE_STATE 0x28
static struct { uint64_t ev; uint32_t wh; } g_se[512];
static int g_nse;
static PlatMutex g_se_lock = PLAT_MUTEX_INIT;
static int se_live(uint64_t ev) {
return __atomic_load_n((const uint8_t *)(uintptr_t)(ev + SE_STATE), __ATOMIC_RELAXED) != 0;
}
static uint32_t se_find(uint64_t ev) {
plat_mutex_lock_shared(&g_se_lock);
uint32_t wh = 0;
@@ -2069,8 +2107,9 @@ static uint32_t se_find(uint64_t ev) {
return wh;
}
// A bound event the guest destroyed since is created again and its table entry takes the new writable handle.
void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
if (se_find(ev)) return;
if (se_find(ev) && se_live(ev)) return;
if (!tcur || !c) rt_fatal("kern_bind_system_event(%#llx) on a host thread", (unsigned long long)ev);
static uint64_t create, getw;
if (!create) {
@@ -2082,15 +2121,21 @@ void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
if (r) rt_fatal("kern_bind_system_event: CreateSystemEvent(%#llx) failed %#x", (unsigned long long)ev, r);
uint32_t wh = (uint32_t)rt_call(c, getw, 1, ev);
plat_mutex_lock(&g_se_lock);
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
g_se[g_nse].ev = ev;
g_se[g_nse++].wh = wh;
int i = 0;
while (i < g_nse && g_se[i].ev != ev) i++;
if (i == g_nse) {
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
g_se[g_nse++].ev = ev;
}
g_se[i].wh = wh;
plat_mutex_unlock(&g_se_lock);
}
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread.
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread. A bound event the guest
// destroyed is not signalled: its handles are closed and the kernel hands their numbers to new objects.
void kern_signal_system_event(uint64_t ev) {
uint32_t wh = se_find(ev);
if (wh && !se_live(ev)) return;
if (!wh) {
kern_bind_system_event(kern_cpu(), ev, 0);
wh = se_find(ev);
+40 -4
View File
@@ -305,9 +305,18 @@ static void test_arbiter(Cpu *c) {
CHECK(sig(c, 2, 5, 0) == 0 && g_arb == 6, "signal type 2, no waiter, count 0: value %d (want 6)", (int32_t)g_arb);
ArbJob jc = {.type = 2, .value = 6, .timeout = -1};
h = arb_spawn(c, &jc);
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 5, "signal type 2, 1 waiter, count -1: value %d (want 5)", (int32_t)g_arb);
// Waking all (count <= 0) with waiters subtracts 2 (mesosphere KAddressArbiter::SignalAndModifyByWaitingCountIfEqual).
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count -1: value %d (want 4)", (int32_t)g_arb);
join(c, h);
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0, "wait type 2 results %#x %#x %#x", ja.r, jb.r, jc.r);
g_arb = 6;
ArbJob jd = {.type = 2, .value = 6, .timeout = -1}, je = jd;
uint32_t hd = arb_spawn(c, &jd), he = arb_spawn(c, &je);
CHECK(sig(c, 2, 6, 0) == 0 && g_arb == 4, "signal type 2, 2 waiters, count 0: value %d (want 4)", (int32_t)g_arb);
join(c, hd);
join(c, he);
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0 && jd.r == 0 && je.r == 0, "wait type 2 results %#x %#x %#x %#x %#x", ja.r,
jb.r, jc.r, jd.r, je.r);
g_arb = 5;
CHECK(sig(c, 2, 7, 1) == KERN_R_BAD_STATE, "signal type 2 with a different value");
// timeout, invalid type
uint64_t t0 = kern_now_ns();
@@ -320,11 +329,17 @@ static void test_arbiter(Cpu *c) {
}
// ---- events
typedef struct { uint32_t h, r, idx; int64_t timeout; volatile int delay, go; } EvJob;
// pre: a first wait on h0 with timeout t0 (result r0, index idx0) before the wait on h.
typedef struct { uint32_t h, r, idx, h0, r0, idx0; int64_t timeout, t0; volatile int delay, go; int pre; } EvJob;
static void event_waiter(Cpu *c) {
EvJob *j = arg_p(c, 0);
while (j->delay && !j->go) sleep_ms(c, 1);
if (j->pre) {
uint32_t h0[1] = {j->h0};
j->r0 = sv(c, 0x18, 0, ga(h0), 1, (uint64_t)j->t0);
j->idx0 = (uint32_t)c->x[1];
}
uint32_t hs[1] = {j->h};
j->r = sv(c, 0x18, 0, ga(hs), 1, (uint64_t)j->timeout);
j->idx = (uint32_t)c->x[1];
@@ -375,6 +390,26 @@ static void test_events(Cpu *c) {
join(c, h);
CHECK(g.r == KERN_R_CANCELLED, "wait after a cancel: %#x", g.r);
// A pending cancel is for the next WaitSynchronization that would block (mesosphere KSynchronizationObject::Wait:
// signalled objects first, then timeout 0, then the cancel): a poll times out and a wait on a signalled object
// succeeds, both leaving it pending; the next blocking wait ends cancelled. The blocking waits have a 500 ms
// timeout, so a cancel used up too early shows as a timeout instead of a hang.
CHECK(sv(c, 0x11, hw2, 0, 0, 0) == 0, "SignalEvent of the second event");
EvJob p = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr, .t0 = 0};
EvJob q = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr2, .t0 = -1};
EvJob *pq[2] = {&p, &q};
for (int i = 0; i < 2; i++) {
h = create(c, a_event_waiter, pq[i], 44, -2);
start(c, h);
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the %s", i ? "signalled wait" : "poll");
pq[i]->go = 1;
join(c, h);
}
CHECK(p.r0 == KERN_R_TIMEOUT, "poll (timeout 0) with a cancel pending: %#x (want timeout)", p.r0);
CHECK(p.r == KERN_R_CANCELLED, "blocking wait after the poll: %#x (want cancelled)", p.r);
CHECK(q.r0 == 0 && q.idx0 == 0, "wait on a signalled event with a cancel pending: %#x (want success)", q.r0);
CHECK(q.r == KERN_R_CANCELLED, "blocking wait after the signalled wait: %#x (want cancelled)", q.r);
uint32_t sw = kern_event_new(), sr = kern_event_readable(sw);
CHECK(wait1(c, sr, 0) == KERN_R_TIMEOUT, "service event before the signal");
kern_event_signal(sw);
@@ -382,7 +417,8 @@ static void test_events(Cpu *c) {
uint32_t handles[] = {sr, sw, hr2, hw2, hr, hw};
for (unsigned i = 0; i < 6; i++) CHECK(sv(c, 0x16, handles[i], 0, 0, 0) == 0, "CloseHandle %#x", handles[i]);
CHECK(sv(c, 0x16, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "CloseHandle twice");
printf("events: signal, clear, reset, two handles, cancel while waiting and before, service events, 10 ms "
printf("events: signal, clear, reset, two handles, cancel while waiting and before, a pending cancel kept by a poll "
"and a signalled wait, service events, 10 ms "
"timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
}
+1
View File
@@ -197,6 +197,7 @@ NVN_API(nvnTextureInitialize) {
o->magic = NVN_TEX_MAGIC;
o->d = s->d;
o->pool = s->pool;
o->pool_flags = s->pool ? (uint16_t)s->pool->flags : 0;
o->off = s->off;
o->has_layout = s->has_layout;
memcpy(o->layout, s->layout, 8);
+3
View File
@@ -48,6 +48,9 @@ typedef struct {
} tex_builder_t; FITS(tex_builder_t, NVNtextureBuilder);
typedef struct {
uint32_t magic; nvn_texdesc d; uint8_t has_layout, layout[8];
uint16_t pool_flags; // the pool's flags at Initialize (in the padding): a game may finalize and free
// the pool object while the back end still looks the texture up, and the
// driver keeps no pointer to it either
nvn_texlayout L; uint8_t *mem; pool_t *pool; int64_t off;
uint64_t serial; // Initialization serial: the back end uses it to find its texture in the host table (the game may copy the object by value, so object pointers cannot be stored)
} tex_t; FITS(tex_t, NVNtexture);
+27 -1
View File
@@ -12,6 +12,7 @@
#include <string.h>
#include <time.h>
#include "amiibo.h"
#include "cfg.h"
#include "fs.h"
#include "gfx.h"
@@ -25,6 +26,7 @@
#define CHILD_ENV "TOTK_PROBE_SERVICES_CHILD"
#define TRY_WAIT_SYSTEM_EVENT "_ZN2nn2os18TryWaitSystemEventEPNS0_15SystemEventTypeE"
#define DESTROY_SYSTEM_EVENT "_ZN2nn2os18DestroySystemEventEPNS0_15SystemEventTypeE"
typedef struct { const char *key, *name; int fatal; uint64_t addr; int bound, calls; } Ov;
static Ov ovs[] = {
@@ -386,6 +388,29 @@ static void probe_applet(void) {
CHECK(event_works(activate, 0), "the nfp activate event is set without an amiibo on the reader");
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection");
CHECK((uint32_t)CALL("nfp.StopDetection", U(handles)) == 0, "nfp StopDetection");
// The game attaches again before each detection: DestroySystemEvent on the attached event, then Attach* on the
// same SystemEventType. Both events must work again afterwards.
static uint64_t destroy;
if (!destroy && !(destroy = ldr_symbol(DESTROY_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", DESTROY_SYSTEM_EVENT);
rt_call(gc, destroy, 1, activate);
rt_call(gc, destroy, 1, deactivate);
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent again");
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent again");
int reattached = event_works(activate, 0) && event_works(deactivate, 0);
CHECK(reattached, "the nfp events do not work after DestroySystemEvent and a second Attach");
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection again");
// An amiibo's NTAG215 header: UID with its two check bytes, static lock 0F E0, capability container F1 10 FF EE.
// The loader takes it and refuses the same file with a plain NDEF container (E1).
static const uint8_t head[16] = {0x04, 0x11, 0x22, 0x88 ^ 0x04 ^ 0x11 ^ 0x22, 0x33, 0x44, 0x55, 0x66,
0x33 ^ 0x44 ^ 0x55 ^ 0x66, 0x48, 0x0F, 0xE0, 0xF1, 0x10, 0xFF, 0xEE};
uint8_t dump[AMIIBO_DUMP_SIZE] = {0};
memcpy(dump, head, sizeof head);
const char *dump_path = "probe-amiibo.bin";
int amiibo_ok = plat_file_write(dump_path, dump, sizeof dump) && amiibo_load_file(dump_path);
dump[0x0C] = 0xE1;
amiibo_ok = amiibo_ok && plat_file_write(dump_path, dump, sizeof dump) && !amiibo_load_file(dump_path);
plat_fs_unlink(dump_path);
CHECK(amiibo_ok, "the amiibo loader refuses a dump with the amiibo capability container, or takes a plain NDEF one");
CHECK(CALL("nfp.Finalize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 0, "nfp finalize");
CALL("err.ShowError", 0x2a8);
CALL("nndiagStartup", 0);
@@ -395,7 +420,8 @@ static void probe_applet(void) {
CALL("pctl.Initialize", 0);
CALL("diag.InitializeApplicationAbortObserver", 0);
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, an NFC reader without "
"an amiibo, availability event (bound through the SDK's CreateSystemEvent) %s\n", nfp_event ? "ok" : "FAILED");
"an amiibo, availability event (bound through the SDK's CreateSystemEvent) %s, activate and deactivate events "
"destroyed and attached again %s\n", nfp_event ? "ok" : "FAILED", reattached ? "ok" : "FAILED");
}
static void probe_account(void) {
+2
View File
@@ -1038,6 +1038,8 @@ const void *plat_file_map(PlatFile *f) {
return v == MAP_FAILED ? NULL : v;
}
int plat_fs_os_error(void) { return errno; }
static int fs_err(int e) {
switch (e) {
case ENOENT: case ENOTDIR: return PLAT_FS_NOT_FOUND;
+2 -1
View File
@@ -316,8 +316,9 @@ enum { PLAT_FS_OK, PLAT_FS_NOT_FOUND, PLAT_FS_EXISTS, PLAT_FS_NOT_EMPTY, PLAT_FS
enum { PLAT_FS_MISSING = -1, PLAT_FS_FILE, PLAT_FS_DIR, PLAT_FS_LINK }; // plat_fs_kind; LINK: a symbolic link or
// reparse point, never followed
int plat_fs_kind(const char *path);
int plat_fs_os_error(void); // the OS error code (GetLastError, errno) of this thread's last failed plat_fs_* call
int plat_fs_mkdir(const char *path); // PLAT_FS_EXISTS when something is there
int plat_fs_rmdir(const char *path); // an empty directory, or a link to one
int plat_fs_rmdir(const char *path); // an empty directory (a read-only one too), or a link to one
int plat_fs_unlink(const char *path); // a file or a link; a read-only one too
// Renames a file or directory; never replaces (PLAT_FS_EXISTS). durable: the change is on the disk at return.
int plat_fs_rename(const char *from, const char *to, int durable);
+31 -5
View File
@@ -866,7 +866,11 @@ const void *plat_file_map(PlatFile *f) {
return v;
}
static __thread DWORD fs_last_error;
int plat_fs_os_error(void) { return (int)fs_last_error; }
static int fs_err(DWORD e) {
fs_last_error = e;
switch (e) {
case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_DIRECTORY: return PLAT_FS_NOT_FOUND;
case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: return PLAT_FS_EXISTS;
@@ -876,12 +880,28 @@ static int fs_err(DWORD e) {
}
}
// A symbolic link or junction is a link: removed as a link, never followed. Any other reparse point (OneDrive's cloud
// placeholders, deduplicated files) is an ordinary file or directory whose content the file system fetches on access.
static int fs_kind_of(const WIN32_FIND_DATAW *fd) {
DWORD a = fd->dwFileAttributes;
if (a & FILE_ATTRIBUTE_REPARSE_POINT && (fd->dwReserved0 == IO_REPARSE_TAG_SYMLINK || fd->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT))
return PLAT_FS_LINK;
return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
}
int plat_fs_kind(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_MISSING;
DWORD a = GetFileAttributesW(w);
if (a == INVALID_FILE_ATTRIBUTES) return PLAT_FS_MISSING;
return a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
if (!(a & FILE_ATTRIBUTE_REPARSE_POINT)) return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
// the reparse tag comes with the find data only; FindFirstFileW is asked only here, since it fails on a path
// with a trailing separator and reads wildcards
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(w, &fd);
if (h == INVALID_HANDLE_VALUE) return PLAT_FS_LINK;
FindClose(h);
return fs_kind_of(&fd);
}
int plat_fs_mkdir(const char *path) {
@@ -893,7 +913,15 @@ int plat_fs_mkdir(const char *path) {
int plat_fs_rmdir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
return RemoveDirectoryW(w) ? PLAT_FS_OK : fs_err(GetLastError());
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
DWORD e = GetLastError(), a = GetFileAttributesW(w);
if (e == ERROR_ACCESS_DENIED && a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_READONLY)) {
// a read-only directory (copied over from another emulator's save folder) refuses RemoveDirectoryW
SetFileAttributesW(w, a & ~(DWORD)FILE_ATTRIBUTE_READONLY);
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
e = GetLastError();
}
return fs_err(e);
}
int plat_fs_unlink(const char *path) {
@@ -1038,9 +1066,7 @@ int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue;
char name[1024];
if (!WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, sizeof name, NULL, NULL)) continue;
DWORD a = fd.dwFileAttributes;
int kind = a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR
: PLAT_FS_FILE;
int kind = fs_kind_of(&fd);
if (!cb(name, kind, (uint64_t)fd.nFileSizeHigh << 32 | fd.nFileSizeLow, ctx)) break;
} while (FindNextFileW(h, &fd));
FindClose(h);
+29 -2
View File
@@ -36,6 +36,7 @@
#include "cfg.h"
#include "host.h"
#include "platform/platform.h"
#define BASE_W 1920
#define BASE_H 1080
@@ -55,6 +56,22 @@ static struct {
double dlss_k; // TOTK_DLSS quality/balanced/performance: the scene's share of the output, else 0
} 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.
static int dlss_dll(void) {
#ifdef _WIN32
char exe[1024], path[1024 + 32];
if (!plat_exe_path(exe, sizeof exe)) return 1;
char *last = plat_last_sep(exe);
if (!last) return 1;
snprintf(path, sizeof path, "%.*s/nvngx_dlss.dll", (int)(last - exe), exe);
return plat_file_exists(path);
#else
return 0;
#endif
}
static void decide(void) {
if (R.decided) return;
R.decided = 1;
@@ -71,11 +88,21 @@ static void decide(void) {
}
const char *s = cfg_value("TOTK_RENDER_SCALE");
double k = 5.0 / 6.0;
const char *m = cfg_value("TOTK_DLSS"); // a DLSS mode other than dlaa sets the scene size
// 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; i < 3; i++)
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;
+58 -11
View File
@@ -59,16 +59,60 @@ SN T FN(nan3)(Cpu *c, T a, T b, T d) {
return FN(fv)(DNAN);
}
// ---- FMA flavours. FMA_S / FMA_V: fast paths, host MXCSR is round-to-nearest there (simd_directed() is checked first).
// FMA_X: scalar FMA for the slow paths, any host rounding mode.
// Without hardware FMA (Ivy Bridge, no -mfma) clang turns __builtin_elementwise_fma into a call to the C runtime's
// fmaf/fma for every lane, which is very slow and keeps the thread outside generated code. For float the product of
// two floats is exact in a double (24 x 24 = 48 bits), so the fused op is done in double. A plain
// (float)(x * y + z) rounds twice and can differ by one ulp; rounding the double sum to odd first (Boldo-Melquiond)
// makes the final float rounding exact: TwoSum gives the error e of s = fl(p + z), and when e != 0 and s has an even
// last bit, s is moved one ulp toward the exact value (the odd neighbour). Inf/NaN lanes are left alone (e is NaN
// there, so the ordered compares below are false). Double has no wider type: it keeps the C runtime's fma.
#if defined(__FMA__) || defined(__aarch64__) || NB == 64
#define FMA_S(x, y, z) __builtin_elementwise_fma(x, y, z)
#define FMA_V(x, y, z) __builtin_elementwise_fma(x, y, z)
#define FMA_X(x, y, z) __builtin_elementwise_fma(x, y, z)
#else
typedef double simd_vf64x4 __attribute__((vector_size(32)));
typedef uint64_t simd_vu64x4 __attribute__((vector_size(32)));
SI float fma_rn_s(float x, float y, float z) {
double p = (double)x * (double)y, zd = (double)z, s = p + zd;
double bb = s - p, e = (p - (s - bb)) + (zd - bb); // TwoSum: s + e == p + zd exactly
uint64_t u;
memcpy(&u, &s, sizeof u);
if (((e < 0.0) | (e > 0.0)) && !(u & 1)) u += (e > 0.0) == (s > 0.0) ? 1 : (uint64_t)-1;
memcpy(&s, &u, sizeof s);
return (float)s;
}
SI vf32 fma_rn_v(vf32 x, vf32 y, vf32 z) {
simd_vf64x4 p = CVT(x, simd_vf64x4) * CVT(y, simd_vf64x4), zd = CVT(z, simd_vf64x4), s = p + zd;
simd_vf64x4 bb = s - p, e = (p - (s - bb)) + (zd - bb);
simd_vu64x4 u = (simd_vu64x4)s;
simd_vu64x4 fix = (simd_vu64x4)((e < 0.0) | (e > 0.0)) & (simd_vu64x4)((u & 1) == 0);
simd_vu64x4 up = (simd_vu64x4)((e > 0.0) == (s > 0.0));
u += fix & ((up & 1) | ~up); // +1 toward the exact value, i.e. +1 if e and s share a sign, else -1
return CVT((simd_vf64x4)u, vf32);
}
// Any rounding mode: directed modes are exact with a plain double (same-direction double rounding is harmless).
SI float fma_any_s(float x, float y, float z) {
if (__builtin_expect(_mm_getcsr() & 0x6000u, 0)) return (float)((double)x * (double)y + (double)z);
return fma_rn_s(x, y, z);
}
#define FMA_S(x, y, z) fma_rn_s(x, y, z)
#define FMA_V(x, y, z) fma_rn_v(x, y, z)
#define FMA_X(x, y, z) fma_any_s(x, y, z)
#endif
// The arithmetic itself, for scalars (KK = K) and vectors (KK = VK). MADD: a + b*d (a = addend).
// RECPS/RSQRTS get their first operand already negated (FPNeg happens before NaN processing).
#define FCALC(op, a, b, d, KK) \
#define FCALC(op, a, b, d, KK, FMA) \
((op) == FOP_ADD ? (a) + (b) : (op) == FOP_SUB ? (a) - (b) \
: (op) == FOP_MUL || (op) == FOP_MULX ? (a) * (b) : (op) == FOP_NMUL ? -((a) * (b)) \
: (op) == FOP_DIV ? (a) / (b) : (op) == FOP_ABD ? __builtin_elementwise_abs((a) - (b)) \
: (op) == FOP_SQRT ? __builtin_elementwise_sqrt(a) \
: (op) == FOP_MADD ? __builtin_elementwise_fma(b, d, a) \
: (op) == FOP_RECPS ? __builtin_elementwise_fma(a, b, KK(2)) \
: (op) == FOP_RSQRTS ? __builtin_elementwise_fma(a, b, KK(3)) * KK(0.5) : (a))
: (op) == FOP_MADD ? FMA(b, d, a) \
: (op) == FOP_RECPS ? FMA(a, b, KK(2)) \
: (op) == FOP_RSQRTS ? FMA(a, b, KK(3)) * KK(0.5) : (a))
// Lanes whose fast result needs fixing: NaN results (NaN operand or invalid op), RSQRTS overflow.
#define FBAD(op, r) ((op) == FOP_RSQRTS ? ((r) != (r)) | (__builtin_elementwise_abs(r) == K(INFINITY)) : (r) != (r))
@@ -76,9 +120,9 @@ SN T FN(nan3)(Cpu *c, T a, T b, T d) {
// (3 + a*b) / 2 with one rounding (a already negated): halve a normal operand first, so a product beyond
// the format's range (which the fast path rounds to inf, or to the largest normal when rounding down) is exact.
SN T FN(rsqrts)(T a, T b) {
if (__builtin_elementwise_abs(a) >= FN(fv)(2 * MINN)) return __builtin_elementwise_fma(a * K(0.5), b, K(1.5));
if (__builtin_elementwise_abs(b) >= FN(fv)(2 * MINN)) return __builtin_elementwise_fma(a, b * K(0.5), K(1.5));
return __builtin_elementwise_fma(a, b, K(3)) * K(0.5);
if (__builtin_elementwise_abs(a) >= FN(fv)(2 * MINN)) return FMA_X(a * K(0.5), b, K(1.5));
if (__builtin_elementwise_abs(b) >= FN(fv)(2 * MINN)) return FMA_X(a, b * K(0.5), K(1.5));
return FMA_X(a, b, K(3)) * K(0.5);
}
// ARM result for a lane flagged by FBAD (operands already flushed).
@@ -136,13 +180,13 @@ SN T FN(slow)(Cpu *c, int op, T a, T b, T d) {
if (FMINMAX(op)) return FN(minmax)(c, op, a, b);
unsigned old = simd_rm_enter(fpcr);
volatile T va = a, vb = b, vd = d;
volatile T vr = op == FOP_RSQRTS ? FN(rsqrts)(va, vb) : FCALC(op, va, vb, vd, K);
volatile T vr = op == FOP_RSQRTS ? FN(rsqrts)(va, vb) : FCALC(op, va, vb, vd, K, FMA_X);
T r = vr;
if (FBAD(op, r)) vr = FN(fix)(c, op, a, b, d), r = vr;
if ((fpcr & FPCR_FZ) && (FN(fb)(r) & ~SGN) <= MINN && !FN(iszero)(FN(fb)(r))) {
if ((FN(fb)(r) & ~SGN) == MINN) {
simd_rm_rz();
volatile T vz = FCALC(op, va, vb, vd, K);
volatile T vz = FCALC(op, va, vb, vd, K, FMA_X);
if ((FN(fb)(vz) & ~SGN) < MINN) r = FN(fv)(FN(fb)(r) & SGN);
} else {
r = FN(fv)(FN(fb)(r) & SGN);
@@ -172,7 +216,7 @@ SI T FN(op)(Cpu *c, int op, T a, T b, T d) {
if (c->fpcr & FPCR_FZ) a = FN(flush)(a), b = FN(flush)(b);
return FN(minmax)(c, op, a, b);
}
T r = FCALC(op, a, b, d, K);
T r = FCALC(op, a, b, d, K, FMA_S);
if (__builtin_expect(op == FOP_RSQRTS ? FREDO(op, r) : !FOK(r), 0)) r = FN(redo)(c, op, a, b, d, r);
return r;
}
@@ -207,7 +251,7 @@ SI VT FN(vop)(Cpu *c, int op, VT a, VT b, VT d) {
r = (VT)FN(vsel)(a < b, x, FN(vsel)(a == b, x | y, y));
return r;
}
r = FCALC(op, a, b, d, VK);
r = FCALC(op, a, b, d, VK, FMA_V);
if (__builtin_expect(op == FOP_RSQRTS ? __builtin_reduce_or(FREDO(op, r)) != 0 : __builtin_reduce_and(FOK(r)) == 0, 0))
return FN(vslow)(c, op, a, b, d);
return r;
@@ -363,3 +407,6 @@ SI T FN(recpx)(Cpu *c, T a) {
#undef MINN
#undef VK
#undef K
#undef FMA_S
#undef FMA_V
#undef FMA_X
+40 -11
View File
@@ -17,6 +17,7 @@
extern "C" {
#include "amiibo.h"
#include "cfg.h"
#include "folder_pick.h"
#include "platform/platform.h"
#include "ui_text.h"
}
@@ -30,7 +31,6 @@ struct FileEntry {
static const char kDefaultAmiiboDir[] = "./amiibo";
static std::string g_current_dir = kDefaultAmiiboDir;
static char g_dir_input[512] = "./amiibo";
static std::vector<FileEntry> g_entries;
static int g_selected_entry = -1;
// The last action: a catalog id and the file or folder it names, formatted when drawn (the menu language can change)
@@ -61,10 +61,24 @@ static bool iequals(std::string_view a, std::string_view b) {
static void set_current_dir(const std::string &path) {
g_current_dir = path;
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
cfg_set("TOTK_AMIIBO_DIR", g_current_dir.c_str());
}
// The host's folder dialog (folder_pick.h) answers on its own thread; the page takes the answer when it draws.
static PlatMutex g_pick_lock = PLAT_MUTEX_INIT;
static bool g_pick_open;
static bool g_pick_ready;
static std::string g_pick_path;
static void on_folder_picked(const char *path, void *ctx) {
(void)ctx;
plat_mutex_lock(&g_pick_lock);
g_pick_ready = path != NULL;
if (path) g_pick_path = path;
g_pick_open = false;
plat_mutex_unlock(&g_pick_lock);
}
static void refresh_dir(void) {
g_entries.clear();
g_selected_entry = -1;
@@ -122,11 +136,21 @@ void tab_amiibo(float scale) {
} else {
g_current_dir = kDefaultAmiiboDir;
}
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
refresh_dir();
s_inited = true;
}
plat_mutex_lock(&g_pick_lock);
bool picking = g_pick_open;
std::string picked;
if (g_pick_ready) picked.swap(g_pick_path);
g_pick_ready = false;
plat_mutex_unlock(&g_pick_lock);
if (!picked.empty()) {
set_current_dir(picked);
refresh_dir();
}
// Two panes: Left (Directory Browser & Tag List), Right (Amiibo Tag Details & Controls)
const float content_h = ImGui::GetContentRegionAvail().y - opt_bottom(); // Continue and Quit stay visible
const float left_w = ImGui::GetContentRegionAvail().x * 0.58f;
@@ -136,17 +160,17 @@ void tab_amiibo(float scale) {
ImGui::BeginChild("##amiibo_explorer", ImVec2(left_w, content_h), true);
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_DIR));
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::InputText("##amiibo_dir_input", g_dir_input, sizeof(g_dir_input), ImGuiInputTextFlags_EnterReturnsTrue)) {
set_current_dir(g_dir_input);
refresh_dir();
}
ImGui::TextWrapped("%s", g_current_dir.c_str());
ImGui::Spacing();
ImGui::BeginDisabled(picking);
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_OPEN))) {
set_current_dir(g_dir_input);
refresh_dir();
plat_mutex_lock(&g_pick_lock);
g_pick_open = true;
plat_mutex_unlock(&g_pick_lock);
folder_pick(g_current_dir.c_str(), on_folder_picked, NULL);
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_DEFAULT_DIR))) {
set_current_dir(kDefaultAmiiboDir);
@@ -161,6 +185,11 @@ void tab_amiibo(float scale) {
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_PICK));
ImGui::BeginChild("##file_list", ImVec2(0, 0), true);
if (g_status_id == UI_MENU_AMIIBO_NO_DIR) {
char missing[512];
snprintf(missing, sizeof missing, menu_text(UI_MENU_AMIIBO_NO_DIR), g_current_dir.c_str());
ImGui::TextWrapped("%s", missing);
}
for (size_t i = 0; i < g_entries.size(); i++) {
const auto &e = g_entries[i];
std::string label = e.is_dir ? e.name + "/" : e.name;
@@ -198,7 +227,7 @@ void tab_amiibo(float scale) {
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_READER));
ImGui::Separator();
AmiiboDeviceState state = amiibo_get_device_state();
AmiiboDeviceState state = amiibo_get_reader_state();
UiTextId state_id = UI_MENU_AMIIBO_STATE_IDLE;
ImVec4 state_col = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
switch (state) {
+9 -6
View File
@@ -518,7 +518,7 @@ static void row_render_scale(void) {
}
static void row_shadow_res(void) {
static const char *const vals[] = {"game", "512", "768", "1024", "1536", "2048"};
static const char *const vals[] = {"game", "256", "512", "768", "1024", "1536", "2048"};
enum { N = sizeof vals / sizeof *vals };
const char *names[N];
names[0] = menu_text(UI_MENU_SHADOW_RES_GAME);
@@ -822,8 +822,13 @@ static void tab_controls(float scale) {
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
// One row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With a gamepad connected each
// row shows its stick's deflection now, before the dead zone: a drifting stick's resting value, to set it above.
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
row_walk_key();
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
// Analog sticks: one dead zone row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With
// a gamepad connected each row shows its stick's deflection now, before the dead zone: a drifting stick's resting
// value, to set it above.
opt_group(UI_MENU_GROUP_STICKS);
double now[2];
bool pad = input_pad_stick_percent(now) != 0;
char stick_note[2][64];
@@ -834,9 +839,6 @@ static void tab_controls(float scale) {
opt_slider("TOTK_RSTICK_DEADZONE", UI_MENU_RSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
pad ? stick_note[1] : nullptr, "TOTK_STICK_DEADZONE");
opt_slider("TOTK_STICK_SENS", UI_MENU_STICK_SENS, 0.1f, 3, UI_MENU_STICK_SENS_DESC, 0.05f, "%.2f");
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
row_walk_key();
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
binds_rows(scale);
opt_end();
binds_after_page();
@@ -960,6 +962,7 @@ static void tab_advanced(float scale) {
row_set_or_not("TOTK_TRACE", UI_MENU_TRACE, UI_MENU_TRACE_DESC, "perf");
opt_toggle("TOTK_FLIGHT", UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC);
opt_slider("TOTK_FLIGHT_SPIKE_MS", UI_MENU_FLIGHT_SPIKE_MS, 20, 1000, UI_MENU_FLIGHT_SPIKE_MS_DESC, 10, "%.0f");
opt_choice("TOTK_PASS_CRUMBS", UI_MENU_PASS_CRUMBS, dyn, 3, UI_MENU_PASS_CRUMBS_DESC); // vk_crumbs.c pass marks
static char rec_path[600]; // the recording goes beside the log
if (!rec_path[0]) {
char dir[512];
+6 -2
View File
@@ -353,7 +353,10 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
const char *set = cfg_get(key);
const char *src = fallback && !(set && *set) ? fallback : key; // unset: show the value in effect, the fallback's
const CfgMeta *m = cfg_meta(src);
double v = cfg_double(src), def = m ? strtod(m->def, NULL) : lo;
// A key without a table default of its own (the per-stick dead zones) defaults to its fallback: the "default" mark
// follows the fallback's default, not 0, even once the key is set.
if (fallback && m && !m->def[0]) m = cfg_meta(fallback);
double v = cfg_double(src), def = m && m->def[0] ? strtod(m->def, NULL) : lo;
char shown[48];
snprintf(shown, sizeof shown, fmt, v);
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note};
@@ -367,7 +370,8 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
char out[32];
if (step >= 1) snprintf(out, sizeof out, "%d", (int)lround(n));
else snprintf(out, sizeof out, "%g", round(n / step) * step);
apply(key, out);
// Landing on the fallback's value unsets the key (its line leaves totk.ini), so the row follows the fallback again.
apply(key, fallback && fabs(n - cfg_double(fallback)) < step * 0.01 ? "" : out);
return true;
}
+2 -1
View File
@@ -61,7 +61,8 @@ void opt_group_text(const char *title);
void opt_text_str(const char *text, bool warn = false);
// lo..hi in steps of step (left and right held for a second go ten times as fast); fmt formats the value ("%.0f %%").
// note: a short line under the value in place of the "default" mark (NULL: none). fallback: the setting that applies
// while key is unset; the row shows its value then, and a change sets key.
// while key is unset; the row shows its value then, and a change sets key. A key with no table default of its own takes
// the "default" mark from the fallback's default, and a step onto the fallback's value unsets key again.
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step = 1,
const char *fmt = "%g", const char *note = nullptr, const char *fallback = nullptr);
+19 -4
View File
@@ -74,7 +74,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_SHADOW_RES] = "Shadow map size",
[UI_MENU_SHADOW_RES_DESC] = "Size of the sun's shadow map. The game's own size changes with the view, down to about 400 "
"near the ground, so leaf shadows look blocky and jump; a fixed size keeps them still and "
"sharper. Above 1024 the shadows cost more GPU time and video memory.",
"sharper. Above 1024 the shadows cost more GPU time and video memory; 256 is coarser than the "
"game's own and is for integrated GPUs short of memory bandwidth.",
[UI_MENU_SHADOW_RES_GAME] = "Game (changes)",
[UI_MENU_ANISO] = "Texture filtering",
[UI_MENU_ANISO_DESC] = "Anisotropic filtering of surface textures: ground, roads and walls seen at a shallow angle stay sharp into the distance instead of blurring. Game: the level the game asks for, as on the Switch.",
@@ -226,6 +227,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_RESTART_CONFIRM] = "Restart the game now? Progress since your last save is lost.",
[UI_MENU_RESTART_YES] = "Restart",
[UI_MENU_RESTART_SAVING] = "The game is saving: Restart is possible in a moment.",
[UI_MENU_GROUP_STICKS] = "Analog sticks",
[UI_MENU_LSTICK_DEADZONE] = "Left stick dead zone", [UI_MENU_RSTICK_DEADZONE] = "Right stick dead zone",
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own: let go of the stick, read \"stick now\" and set the dead zone a few percent above it.",
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
@@ -388,6 +390,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "Keeps a short history and saves it when a frame freezes.",
[UI_MENU_FLIGHT_SPIKE_MS] = "Freeze threshold (ms)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Frame time that counts as a freeze.",
[UI_MENU_PASS_CRUMBS] = "GPU pass marks",
[UI_MENU_PASS_CRUMBS_DESC] = "After a GPU crash, the log names the render pass that was running. Automatic: AMD only. On: also NVIDIA, at a small cost.",
[UI_MENU_INPUT_RECORD] = "Input recording",
[UI_MENU_INPUT_RECORD_DESC] = "Writes your inputs to a file for bug reports.",
[UI_MENU_OPEN_LOG] = "Open log folder", [UI_MENU_OPEN_LOG_DESC] = "Shows the folder with the log files.",
@@ -498,7 +502,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_SHADOW_RES] = "Размер карты теней",
[UI_MENU_SHADOW_RES_DESC] = "Размер карты теней от солнца. Собственный размер игры меняется вместе с видом, у земли до "
"примерно 400, поэтому тени листвы выглядят квадратными и дёргаются; постоянный размер делает "
"их ровными и чётче. Выше 1024 тени требуют больше времени GPU и видеопамяти.",
"их ровными и чётче. Выше 1024 тени требуют больше времени GPU и видеопамяти; 256 грубее "
"собственного размера игры и предназначен для встроенных GPU с нехваткой пропускной способности памяти.",
[UI_MENU_SHADOW_RES_GAME] = "Как в игре (меняется)",
[UI_MENU_ANISO] = "Фильтрация текстур",
[UI_MENU_ANISO_DESC] = "Анизотропная фильтрация текстур поверхностей: земля, дороги и стены под острым углом остаются чёткими вдали, а не размываются. Игра: уровень, который запрашивает игра, как на Switch.",
@@ -648,6 +653,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_RESTART_CONFIRM] = "Перезапустить игру сейчас? Прогресс после последнего сохранения будет потерян.",
[UI_MENU_RESTART_YES] = "Перезапустить",
[UI_MENU_RESTART_SAVING] = "Игра сохраняется: перезапуск будет доступен через мгновение.",
[UI_MENU_GROUP_STICKS] = "Стики",
[UI_MENU_LSTICK_DEADZONE] = "Мёртвая зона левого стика", [UI_MENU_RSTICK_DEADZONE] = "Мёртвая зона правого стика",
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами: отпустите стик, посмотрите «стик сейчас» и поставьте мёртвую зону на несколько процентов выше.",
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
@@ -816,6 +822,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "Хранит короткую историю и сохраняет её, когда кадр подвисает.",
[UI_MENU_FLIGHT_SPIKE_MS] = "Порог подвисания (мс)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Время кадра, которое считается подвисанием.",
[UI_MENU_PASS_CRUMBS] = "Метки проходов GPU",
[UI_MENU_PASS_CRUMBS_DESC] = "После сбоя GPU журнал называет проход отрисовки, который выполнялся. Автоматически: только AMD. Вкл.: также NVIDIA, с небольшой нагрузкой.",
[UI_MENU_INPUT_RECORD] = "Запись ввода",
[UI_MENU_INPUT_RECORD_DESC] = "Пишет ваш ввод в файл для отчёта об ошибке.",
[UI_MENU_OPEN_LOG] = "Открыть папку журналов", [UI_MENU_OPEN_LOG_DESC] = "Показывает папку с файлами журнала.",
@@ -998,6 +1006,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_PAD_LAYOUT_AUTO] = "Automático", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A à direita)",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A embaixo)",
[UI_MENU_PAD_LAYOUT_IN_USE] = "em uso: %s",
[UI_MENU_GROUP_STICKS] = "Analógicos",
[UI_MENU_LSTICK_DEADZONE] = "Zona morta do analógico esquerdo",
[UI_MENU_RSTICK_DEADZONE] = "Zona morta do analógico direito",
[UI_MENU_STICK_DEADZONE_DESC] = "Quanto um analógico do controle precisa se mover antes que o jogo perceba. Aumente "
@@ -1163,6 +1172,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "Guarda um histórico curto e o salva quando um quadro trava.",
[UI_MENU_FLIGHT_SPIKE_MS] = "Limite de travamento (ms)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Tempo de quadro que conta como travamento.",
[UI_MENU_PASS_CRUMBS] = "Marcas de passagem da GPU",
[UI_MENU_PASS_CRUMBS_DESC] = "Após uma falha da GPU, o log indica a passagem de renderização que estava em execução. Automático: só AMD. Ligado: também NVIDIA, com um pequeno custo.",
[UI_MENU_INPUT_RECORD] = "Gravação de comandos",
[UI_MENU_INPUT_RECORD_DESC] = "Grava os seus comandos num arquivo para relatórios de bug.",
[UI_MENU_OPEN_LOG] = "Abrir pasta de logs", [UI_MENU_OPEN_LOG_DESC] = "Mostra a pasta com os arquivos de log.",
@@ -1196,7 +1207,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_SHADOW_RES_DESC] = "Tamanho do mapa de sombras do sol. O tamanho do jogo muda com a vista, até cerca de "
"400 perto do chão, então as sombras das folhas ficam quadriculadas e pulam; um "
"tamanho fixo as deixa paradas e mais nítidas. Acima de 1024 as sombras custam mais "
"tempo de GPU e memória de vídeo.",
"tempo de GPU e memória de vídeo; 256 é mais grosseiro que o do jogo e serve para GPUs "
"integradas com pouca largura de banda de memória.",
[UI_MENU_SHADOW_RES_GAME] = "Do jogo (varia)",
[UI_MENU_ANISO] = "Filtragem de texturas",
[UI_MENU_ANISO_DESC] = "Filtragem anisotrópica das texturas de superfície: chão, estradas e paredes vistos em "
@@ -1497,6 +1509,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo(A 在右)",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox(A 在下)",
[UI_MENU_PAD_LAYOUT_IN_USE] = "使用中:%s",
[UI_MENU_GROUP_STICKS] = "摇杆",
[UI_MENU_LSTICK_DEADZONE] = "左摇杆死区", [UI_MENU_RSTICK_DEADZONE] = "右摇杆死区",
[UI_MENU_STICK_DEADZONE_DESC] = "手柄摇杆要推动多远游戏才会响应。如果角色或视角会自己移动,"
"请调高此值:松开摇杆,查看“当前摇杆”,把死区设得比它高几个百分点。",
@@ -1743,6 +1756,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "保留一小段历史记录,在画面卡住时保存。",
[UI_MENU_FLIGHT_SPIKE_MS] = "卡顿阈值(ms)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "算作卡顿的帧时间。",
[UI_MENU_PASS_CRUMBS] = "GPU 渲染通道标记",
[UI_MENU_PASS_CRUMBS_DESC] = "GPU 崩溃后,日志会写出当时正在运行的渲染通道。自动:仅 AMD。开:NVIDIA 也启用,有少量开销。",
[UI_MENU_INPUT_RECORD] = "输入录制",
[UI_MENU_INPUT_RECORD_DESC] = "把你的输入写入文件,用于反馈问题。",
[UI_MENU_OPEN_LOG] = "打开日志文件夹",
@@ -1795,7 +1810,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_OV_SCALE_BILINEAR] = "双线性",
[UI_MENU_SHADOW_RES] = "阴影贴图尺寸",
[UI_MENU_SHADOW_RES_DESC] = "太阳阴影贴图的尺寸。游戏自身的尺寸随视角变化,靠近地面时降到约 400,因此树叶阴影显得块状并且跳动;固定尺寸能让阴影稳定、更清晰。高于 1024 时阴影会占用更多 "
"GPU 时间和显存。",
"GPU 时间和显存;256 比游戏自身的尺寸更粗糙,适合显存带宽不足的集成显卡。",
[UI_MENU_SHADOW_RES_GAME] = "游戏默认(会变化)",
[UI_MENU_ANISO] = "纹理过滤",
[UI_MENU_ANISO_DESC] = "表面纹理的各向异性过滤:以小角度看到的地面、道路和墙壁在远处依然清晰,而不会模糊。游戏:使用游戏请求的级别,与 Switch 上相同。",
+2 -1
View File
@@ -102,7 +102,7 @@ typedef enum {
UI_MENU_PROMPTS_OFF_MODS, UI_MENU_PROMPTS_OFF_PATCHES,
// "Restart now" under the prompts while they change after restart, and its confirmation (Cancel: UI_MENU_RESET_NO)
UI_MENU_RESTART, UI_MENU_RESTART_DESC, UI_MENU_RESTART_CONFIRM, UI_MENU_RESTART_YES, UI_MENU_RESTART_SAVING,
UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
UI_MENU_GROUP_STICKS, UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
UI_MENU_STICK_DEADZONE_NOW, UI_MENU_STICK_SENS, UI_MENU_STICK_SENS_DESC,
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
UI_MENU_WALK_KEY, UI_MENU_WALK_KEY_DESC, UI_MENU_WALK_KEY_TAKEN, UI_MENU_WALK_SCALE, UI_MENU_WALK_SCALE_DESC,
@@ -194,6 +194,7 @@ typedef enum {
UI_MENU_TRACE, UI_MENU_TRACE_DESC,
UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC,
UI_MENU_FLIGHT_SPIKE_MS, UI_MENU_FLIGHT_SPIKE_MS_DESC,
UI_MENU_PASS_CRUMBS, UI_MENU_PASS_CRUMBS_DESC,
UI_MENU_INPUT_RECORD, UI_MENU_INPUT_RECORD_DESC,
UI_MENU_OPEN_LOG, UI_MENU_OPEN_LOG_DESC,
UI_MENU_COPY_INFO, UI_MENU_COPY_INFO_DESC, UI_MENU_COPY_INFO_DONE,
+40
View File
@@ -33,6 +33,7 @@ typedef struct VkCtx {
int has_custom_border_color, has_eds, has_eds2, has_eds3, has_vertex_input_dynamic_state,
has_conditional_rendering, has_transform_feedback, has_provoking_vertex, has_device_fault, has_memory_budget,
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock, has_buffer_marker,
has_nv_checkpoints, // VK_NV_device_diagnostic_checkpoints (TOTK_PASS_CRUMBS=1, vk_crumbs.c)
has_astc_ldr, // textureCompressionASTC_LDR (enabled when reported)
has_calibrated, // VK_KHR/EXT_calibrated_timestamps (GPU idle split in the perf line)
has_hdr_metadata; // VK_EXT_hdr_metadata (vk_hdr.c vk_hdr_metadata)
@@ -87,6 +88,45 @@ void vk_crumbs_release(void);
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n);
void vk_crumbs_report(void); // logs the two codes and what they name
// ---- pass marks (vk_crumbs.c): the replay (vk_exec.c vk_exec_pass_begin / vk_exec_pass_end, vk_exec_draw.c
// op_dispatch) marks where each render pass and dispatch starts and where it ends, so that a device loss names the
// pass the GPU was in. A mark is a number; a ring of VK_PASS_MARKS records keeps what it was. With
// VK_AMD_buffer_marker the GPU writes the number of the last mark it started (TOP_OF_PIPE, before vkCmdBeginRendering
// or the dispatch) and of the last it finished (BOTTOM_OF_PIPE, after vkCmdEndRendering or the dispatch) into the
// breadcrumb buffer; with VK_NV_device_diagnostic_checkpoints the same two points are checkpoints, read with
// vkGetQueueCheckpointData(2)NV after the loss. TOTK_PASS_CRUMBS: unset, AMD buffer marks only; 1, also NVIDIA
// checkpoints (the extension is enabled at device creation); 0, none.
#define VK_PASS_MARKS 4096 // power of two
enum { VK_MARK_PASS = 1, VK_MARK_DISPATCH };
typedef struct {
uint32_t id; // the mark this record belongs to (0: never written)
uint8_t kind; // VK_MARK_*
uint8_t ncolor; // colour attachments
uint32_t step; // index of the pass, or of the dispatch, within its submission, from 0
uint32_t w, h, d; // render area and layers; a dispatch: group counts (0 0 0: indirect)
uint32_t draws; // draws recorded in the pass; UINT32_MAX until its end is recorded
uint64_t fence; // the submission: the timeline value it signals
VkFormat color[8], depth; // attachment formats (VK_FORMAT_UNDEFINED: none)
const GfxShader *first[2], *last[2];// vertex and fragment shader of the first and last draw; a dispatch: first[0]
} VkPassMark;
// Pass marks wanted and possible (TOTK_PASS_CRUMBS and the extensions): set by vk_crumbs_init.
extern int vk_pass_marks_on;
// vk_device.c create_device, before vkCreateDevice: 1 when TOTK_PASS_CRUMBS asks for NVIDIA checkpoints.
int vk_pass_marks_want_nv(void);
// The next mark: records its start into cb (through the recorder thread when it owns cb) and returns its ring record,
// id, kind and fence set, for the caller to fill. NULL when pass marks are off. Replay thread only.
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence);
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m); // records the end of mark m into cb
// The recorder thread's side of both (vk_rec.c): the marker and checkpoint commands themselves.
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end);
// AMD only: the last mark the GPU started and the last it finished (0: none); 0 without buffer marks.
int vk_pass_marks_read(uint32_t *started, uint32_t *finished);
const VkPassMark *vk_pass_mark_record(uint32_t id); // the ring record of mark id; NULL once a later mark took its slot
// The report's text for a started and a finished mark, three lines without a trailing newline: which marks (source:
// the extension they came from), then each with its ring record. vk_pass_marks_report logs it; the probe checks it.
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size);
void vk_pass_marks_report(void); // vk_device_fault_report: the AMD words and the NVIDIA checkpoints, formatted
// ---- window and present (gfx_present.c). Host threads only.
// vk_present_image: blits the crop rectangle {x, y, w, h} of src (NULL, or w/h <= 0: the whole w x h image), scaled
// to the swapchain extent, into the acquired swapchain image and presents it. src is in layout, which is
+1
View File
@@ -315,6 +315,7 @@ static void replay_log(uint64_t frame, int total) {
// vk_backend_shutdown.
void gfx_exit_log(void) {
static int32_t done;
rec_check_log();
if (gfx_replay_async() && r_started && !plat_xchg32(&done, 1)) replay_log(game_frames, 1);
}
+202 -4
View File
@@ -4,19 +4,31 @@
// device loss word 0 names the newest point the GPU reached and word 1 the newest point it had finished: the hang lies
// between them. A code is the crumb kind in the top 4 bits and its number in the low 28. The markers cost two small
// writes per crumb, and crumbs mark submissions, upload batches, ASTC chunks and presents, not draws.
//
// Pass marks (vk.h vk_pass_mark_begin) use words 2 and 3 of the same buffer: the replay marks the start and the end of
// every render pass and dispatch, and a ring of records says what each mark was. The design follows Savage Wind
// Recomp's GPU checkpoints (refs/SavageWindRecomp runtime/nvn/vulkan/exec.c ck_mark and vk_checkpoints_report: a mark
// per pass and dispatch, numbers in a ring of 4096, text only at the device loss); the code is ours.
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "vk.h"
static PFN_vkCmdWriteBufferMarkerAMD write_marker;
static VkBuffer buf;
static VmaAllocation alloc;
static volatile uint32_t *words;
static volatile uint32_t *words; // 0, 1: crumbs; 2, 3: pass marks
void vk_crumbs_init(VkCtx *c) {
if (!c->has_buffer_marker) return;
// Pass marks (below)
int vk_pass_marks_on;
static int pm_amd; // marks into words 2 and 3
static PFN_vkCmdSetCheckpointNV set_checkpoint;
static void pass_marks_init(VkCtx *c);
static void buffer_init(VkCtx *c) {
write_marker = (PFN_vkCmdWriteBufferMarkerAMD)vkGetDeviceProcAddr(c->device, "vkCmdWriteBufferMarkerAMD");
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 16,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
@@ -30,14 +42,20 @@ void vk_crumbs_init(VkCtx *c) {
return;
}
words = info.pMappedData;
words[0] = words[1] = 0;
words[0] = words[1] = words[2] = words[3] = 0;
rt_log("vk: GPU breadcrumbs on (VK_AMD_buffer_marker)");
}
void vk_crumbs_init(VkCtx *c) {
if (c->has_buffer_marker) buffer_init(c);
pass_marks_init(c);
}
void vk_crumbs_release(void) {
VkCtx *c = vk_ctx();
if (buf && c) vmaDestroyBuffer(c->vma, buf, alloc);
buf = VK_NULL_HANDLE, alloc = NULL, words = NULL, write_marker = NULL;
vk_pass_marks_on = pm_amd = 0, set_checkpoint = NULL;
}
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n) {
@@ -65,3 +83,183 @@ void vk_crumbs_report(void) {
describe(words[1], finished, sizeof finished);
rt_log("vk: GPU breadcrumbs: the GPU last reached %s; everything before %s had finished", reached, finished);
}
// ---- pass marks
// Cost per pass or dispatch: about twenty stores into the ring and two commands, one at the start and one at the end,
// each a marker write or a checkpoint. Per draw nothing, except that the branch of vk_exec_draw.c prepare_draw that
// records changed pipeline state also notes the shaders of a pass's first draw. A full barrier already precedes every
// pass and dispatch (vk_exec.c vk_exec_pass_begin, vk_exec_draw.c op_dispatch), so the end-of-pipe write adds no wait.
static VkPassMark ring[VK_PASS_MARKS];
static uint32_t seq; // marks made; only the replay thread writes it
// TOTK_PASS_CRUMBS: unset, buffer marks where VK_AMD_buffer_marker exists; 1, also NVIDIA checkpoints; 0, neither.
int vk_pass_marks_want_nv(void) {
const char *e = cfg_get("TOTK_PASS_CRUMBS");
return e && *e && strcmp(e, "0") != 0;
}
static void pass_marks_init(VkCtx *c) {
const char *e = cfg_get("TOTK_PASS_CRUMBS");
int off = e && !strcmp(e, "0");
set_checkpoint = c->has_nv_checkpoints
? (PFN_vkCmdSetCheckpointNV)vkGetDeviceProcAddr(c->device, "vkCmdSetCheckpointNV") : NULL;
pm_amd = !off && write_marker && words;
vk_pass_marks_on = pm_amd || set_checkpoint;
if (vk_pass_marks_on)
rt_log("vk: pass marks on (%s%s%s; TOTK_PASS_CRUMBS=0 turns them off%s)", pm_amd ? "VK_AMD_buffer_marker" : "",
pm_amd && set_checkpoint ? " and " : "", set_checkpoint ? "VK_NV_device_diagnostic_checkpoints" : "",
set_checkpoint ? "" : ", =1 adds NVIDIA checkpoints where the driver has them");
else
rt_log("vk: pass marks off (%s)", off ? "TOTK_PASS_CRUMBS=0"
: "no VK_AMD_buffer_marker; TOTK_PASS_CRUMBS=1 asks for NVIDIA checkpoints");
}
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end) {
if (pm_amd)
write_marker(cb, end ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf,
end ? 12 : 8, id);
if (set_checkpoint) set_checkpoint(cb, (const void *)(uintptr_t)((uint64_t)id << 1 | (end != 0)));
}
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence) {
if (!vk_pass_marks_on || !cb) return NULL;
uint32_t id = ++seq;
if (!id) id = ++seq; // 0 means no mark
VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
m->id = id, m->kind = (uint8_t)kind, m->fence = fence, m->draws = UINT32_MAX;
rec_pass_mark(cb, id, 0);
return m;
}
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m) {
if (m && cb) rec_pass_mark(cb, m->id, 1);
}
int vk_pass_marks_read(uint32_t *started, uint32_t *finished) {
if (!pm_amd) return 0;
*started = words[2], *finished = words[3];
return 1;
}
const VkPassMark *vk_pass_mark_record(uint32_t id) {
const VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
return id && m->id == id ? m : NULL;
}
// The first 8 bytes of the shader's input id (gfx_shader.c), the start of its stored input's name <cache>/in/<id>.in.
static unsigned long long shader_id(const GfxShader *s) {
if (!s) return 0;
const uint8_t *iid = gfx_shader_input_id(s);
unsigned long long v = 0;
for (int i = 0; i < 8; i++) v = v << 8 | iid[i];
return v;
}
static void describe_mark(uint32_t id, char *o, size_t size) {
const VkPassMark *m = vk_pass_mark_record(id);
if (!id) {
snprintf(o, size, "nothing yet");
return;
}
if (!m) {
snprintf(o, size, "no record (a later mark took its slot)");
return;
}
if (m->kind == VK_MARK_DISPATCH) {
snprintf(o, size, "dispatch %u of submission %llu: %ux%ux%u groups%s, cs %016llx", m->step,
(unsigned long long)m->fence, m->w, m->h, m->d, m->w ? "" : " (indirect)", shader_id(m->first[0]));
return;
}
size_t n = (size_t)snprintf(o, size, "pass %u of submission %llu: %ux%u, %u layer%s, colour formats", m->step,
(unsigned long long)m->fence, m->w, m->h, m->d, m->d == 1 ? "" : "s");
for (int i = 0; i < m->ncolor && i < 8 && n < size; i++)
n += (size_t)snprintf(o + n, size - n, "%s%d", i ? "," : " ", (int)m->color[i]);
if (!m->ncolor && n < size) n += (size_t)snprintf(o + n, size - n, " none");
if (n < size) n += (size_t)snprintf(o + n, size - n, ", depth format %d", (int)m->depth);
if (n >= size) return;
if (m->draws == UINT32_MAX)
snprintf(o + n, size - n, ", still recording; first draw vs %016llx fs %016llx", shader_id(m->first[0]),
shader_id(m->first[1]));
else if (!m->draws)
snprintf(o + n, size - n, ", no draws");
else
snprintf(o + n, size - n, ", %u draws; first vs %016llx fs %016llx, last vs %016llx fs %016llx", m->draws,
shader_id(m->first[0]), shader_id(m->first[1]), shader_id(m->last[0]), shader_id(m->last[1]));
}
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size) {
char a[640], b[640];
describe_mark(started, a, sizeof a);
describe_mark(finished, b, sizeof b);
snprintf(o, size,
"vk: pass marks (%s, %u made): the GPU last started mark %u and last finished mark %u\n"
"vk: last started %u: %s\n"
"vk: last finished %u: %s",
source, (unsigned)seq, (unsigned)started, (unsigned)finished, (unsigned)started, a, (unsigned)finished, b);
}
static void log_lines(char *t) {
for (char *line = t, *nl; line && *line; line = nl ? nl + 1 : NULL) {
nl = strchr(line, '\n');
if (nl) *nl = 0;
rt_log("%s", line);
}
}
// vkGetQueueCheckpointData2NV, else vkGetQueueCheckpointDataNV, on the graphics queue (the replay's): one entry per
// stage with the last checkpoint that stage executed. A checkpoint is the mark's id times 2, plus 1 at its end, so the
// top-of-pipe entry names the last start or end the GPU reached, and the bottom-of-pipe entry the last one before which
// everything had finished: at a start, that is the end of the mark before it.
static void nv_report(void) {
VkCtx *c = vk_ctx();
if (!c || !set_checkpoint) return;
PFN_vkGetQueueCheckpointData2NV get2 =
(PFN_vkGetQueueCheckpointData2NV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointData2NV");
PFN_vkGetQueueCheckpointDataNV get1 =
(PFN_vkGetQueueCheckpointDataNV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointDataNV");
enum { MAXN = 32 };
uint64_t stage[MAXN], marker[MAXN];
uint32_t n = 0;
if (get2) {
VkCheckpointData2NV d[MAXN];
get2(c->gfx_queue, &n, NULL);
if (n > MAXN) n = MAXN;
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointData2NV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_2_NV};
get2(c->gfx_queue, &n, d);
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
} else if (get1) {
VkCheckpointDataNV d[MAXN];
get1(c->gfx_queue, &n, NULL);
if (n > MAXN) n = MAXN;
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointDataNV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV};
get1(c->gfx_queue, &n, d);
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
} else {
rt_log("vk: pass marks: no vkGetQueueCheckpointDataNV entry point");
return;
}
uint32_t started = 0, finished = 0;
for (uint32_t i = 0; i < n; i++) {
uint32_t id = (uint32_t)(marker[i] >> 1);
int end = (int)(marker[i] & 1);
rt_log("vk: checkpoint of stage %#llx: the %s of mark %u", (unsigned long long)stage[i], end ? "end" : "start", id);
if (stage[i] == VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT) started = id;
if (stage[i] == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT) finished = end ? id : id ? id - 1 : 0;
}
if (!n) rt_log("vk: pass marks: the driver reported no checkpoints");
char t[2048];
vk_pass_marks_format("VK_NV_device_diagnostic_checkpoints", started, finished, t, sizeof t);
log_lines(t);
}
void vk_pass_marks_report(void) {
if (!vk_pass_marks_on) return;
uint32_t started, finished;
char t[2048];
if (vk_pass_marks_read(&started, &finished)) {
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, t, sizeof t);
log_lines(t);
}
nv_report();
}
+10 -3
View File
@@ -261,7 +261,7 @@ static int create_device(void) {
CHAIN(fbl, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT);
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
}
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the ones below
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the seven below
if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }
// GPU timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not.
if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
@@ -272,6 +272,12 @@ static int create_device(void) {
}
// GPU breadcrumbs for a lost device (vk_crumbs.c): AMD's drivers and RADV report it.
if (has_ext(ep, ne, VK_AMD_BUFFER_MARKER_EXTENSION_NAME)) { g_vk.has_buffer_marker = 1; ext[n_ext++] = VK_AMD_BUFFER_MARKER_EXTENSION_NAME; }
// Pass marks on NVIDIA (vk_crumbs.c): checkpoints only with TOTK_PASS_CRUMBS=1, since NVIDIA documents a cost for
// the extension. NVIDIA's drivers report it, AMD's do not.
if (vk_pass_marks_want_nv() && has_ext(ep, ne, VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME)) {
g_vk.has_nv_checkpoints = 1;
ext[n_ext++] = VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME;
}
// Graphics pipeline library: chained last so it can be dropped again when vk_caps_gpl declines it.
void **gpl_link = NULL;
if (has_ext(ep, ne, VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && has_ext(ep, ne, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME)) {
@@ -430,6 +436,7 @@ void vk_device_fault_report(VkResult r) {
if (done) { plat_mutex_unlock(&lock); return; }
done = 1;
vk_crumbs_report();
vk_pass_marks_report();
VkCtx *c = vk_ctx();
PFN_vkGetDeviceFaultInfoEXT get =
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
@@ -500,11 +507,11 @@ int probe_vkdevice(void) {
c->device_index, c->props.deviceName, VK_API_VERSION_MAJOR(v), VK_API_VERSION_MINOR(v), VK_API_VERSION_PATCH(v),
c->gfx_family, c->xfer_family, c->xfer_shared ? " (shared queue)" : "", c->min_imported_host_alignment);
printf("probe-vkdevice: optional: custom_border=%d eds=%d eds2=%d eds3=%d vertex_input=%d cond_render=%d xfb=%d provoking=%d "
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d validation=%d\n",
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d buffer_marker=%d nv_checkpoints=%d validation=%d\n",
c->has_custom_border_color, c->has_eds, c->has_eds2, c->has_eds3, c->has_vertex_input_dynamic_state,
c->has_conditional_rendering, c->has_transform_feedback, c->has_provoking_vertex, c->has_robustness2,
c->has_device_fault, c->has_memory_budget, c->has_buffer_device_address, c->has_fragment_shader_interlock,
c->validation);
c->has_buffer_marker, c->has_nv_checkpoints, c->validation);
vk_shutdown();
printf("probe-vkdevice: %s (validation errors %ld)\n", g_errors_total ? "FAILED" : "passed", g_errors_total);
return g_errors_total ? 1 : 0;
+33 -6
View File
@@ -202,6 +202,8 @@ void vk_dlaa_init(void) {
driver ? "; the driver needs an update" : "");
}
int vk_dlaa_available(void) { return vk_dlaa_wanted() && N.available; }
// ---------------------------------------------------------------- where DLAA runs in TotK's frame
// Measured (TOTK_DUMP, frame 2100 at Lookout Landing, 2560x1440 scene): the G-buffer passes draw into five targets with
// the scene depth (d32f at the scene size), one of them the R11G11B10F light target; lighting and the transparents
@@ -254,6 +256,8 @@ static struct {
uint64_t mv_frames, mv_none;
uint64_t why[3]; // history resets: scene size changed, no camera, scene target changed
int on; // the dlaa setting, read at each present (the menu switches it live)
int scaler_skipped; // this frame: the game's scaling draw over DLSS's picture was skipped
uint64_t scaler_missed; // frames DLSS scaled whose scaling draw was not found (vk_dlaa_skip_draw)
} D = {.mv_ok = -1};
static int dl_active(void) { return N.available && !D.failed && D.on; }
@@ -624,7 +628,10 @@ void vk_dlaa_frame(void) {
if (D.jit_used && !D.done) D.hold = 60, D.holds++; // jittered but not resolved: that frame shook
else if (D.hold) D.hold--;
if (!D.done) D.jit_ok = 0;
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = 0;
if (D.up && !D.scaler_skipped && ++D.scaler_missed == 1)
rt_log("dlaa: the game's scaling draw over DLSS's picture was not found (its fragment shader is not in FXAA_FS): "
"the jittered scene reaches the screen and shakes");
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = D.scaler_skipped = 0;
D.cam_addr = 0;
}
@@ -638,8 +645,10 @@ void vk_dlaa_frame(void) {
// scaled picture into the output-sized target, in a pass without the depth attachment the in-place FXAA has: there it
// is always drawn, whatever fxaa says (skipping it would leave the 3D picture out), unless DLSS already wrote that
// target this frame.
// The stock translator's hash, and the one with tools/xlate patch 0010 (PCSS, PR #89) applied.
static const uint64_t FXAA_FS[] = {0xf450003ebeb3dc52ull, 0x0a12f4bc2a9a4fbeull};
// The stock translator's hash, the one with tools/xlate patch 0010 (PCSS, PR #89) applied, and the one with patches
// 0010 and 0011 (the shadow filter spread) applied. A translator change that gives the shader another hash is reported
// by vk_dlaa_frame: the game's scaling draw then covers DLSS's picture with the jittered scene, which shakes.
static const uint64_t FXAA_FS[] = {0xf450003ebeb3dc52ull, 0x0a12f4bc2a9a4fbeull, 0x5c43ae18948d52cbull};
static uint64_t spirv_hash(const GfxShader *s) {
static struct { const GfxShader *s; uint64_t h; } tab[8192];
@@ -660,13 +669,30 @@ static uint64_t spirv_hash(const GfxShader *s) {
int vk_dlaa_skip_draw(VkExec *ex, const GfxShader *fs) {
static uint64_t frame_read = ~0ull;
static int fxaa = 1;
// fxaa = 0 reports once whether it found the game's FXAA draw: the first skip, or a minute of frames (3600) with
// none, which is what a translator change that moves FXAA_FS looks like on every GPU, with or without DLSS
static uint64_t fx_off_frames;
static int fx_found, fx_reported;
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
if (f != frame_read) frame_read = f, fxaa = cfg_bool("TOTK_FXAA"); // once a frame: the menu switches it live
if (f != frame_read) { // once a frame: the menu switches it live
frame_read = f, fxaa = cfg_bool("TOTK_FXAA");
if (!fxaa && !fx_found && !fx_reported && ++fx_off_frames == 3600) {
fx_reported = 1;
rt_log("dlaa: fxaa = 0 found no draw of the game's FXAA in %llu frames (its fragment shader is not in "
"FXAA_FS): the FXAA stays on", (unsigned long long)fx_off_frames);
}
}
if (fxaa && !D.up) return 0; // nothing to skip: no shader lookup per draw at the defaults
if (!fs) return 0;
uint64_t h = spirv_hash(fs);
if (h != FXAA_FS[0] && h != FXAA_FS[1]) return 0;
if (!ex->depth.image) return D.up; // the scaling pass
int known = 0;
for (size_t i = 0; i < sizeof FXAA_FS / sizeof FXAA_FS[0]; i++) known |= h == FXAA_FS[i];
if (!known) return 0;
if (!ex->depth.image) return D.scaler_skipped = D.up; // the scaling pass
if (!fxaa && !fx_found) {
fx_found = 1;
rt_log("dlaa: fxaa = 0 skips the game's FXAA (fragment shader %016llx)", (unsigned long long)h);
}
return !fxaa;
}
@@ -680,6 +706,7 @@ void vk_dlaa_device_exts(const VkExtensionProperties *have, uint32_t nhave, cons
(void)have, (void)nhave, (void)ext, (void)n, (void)max;
}
void vk_dlaa_init(void) {}
int vk_dlaa_available(void) { return 0; }
void vk_dlaa_pass(VkExec *ex) { (void)ex; }
void vk_dlaa_frame(void) {}
void vk_dlaa_draw(VkExec *ex) { (void)ex; }
+2
View File
@@ -8,6 +8,8 @@ int vk_dlaa_wanted(void); // the setting is on (the device may still lack DLSS
void vk_dlaa_instance_exts(const char **ext, uint32_t *n, uint32_t max);
void vk_dlaa_device_exts(const VkExtensionProperties *have, uint32_t nhave, const char **ext, uint32_t *n, uint32_t max);
void vk_dlaa_init(void);
// 1 when dlaa is on and NGX came up with DLSS (after vk_dlaa_init): a DLSS mode's smaller scene is scaled up by DLSS.
int vk_dlaa_available(void);
// vk_exec.c vk_exec_pass_begin, once the pass's targets are resolved and before its barrier: arms the frame on the
// scene's light target or, at the first quarter-width pass after it, runs DLAA on that target. Each present ends the
// frame (vk_dlaa_frame).
+24
View File
@@ -612,6 +612,20 @@ void vk_exec_pass_begin(VkExec *ex) {
if (d->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) da = a;
if (d->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) sa = a;
}
// The start mark goes between the barrier and vkCmdBeginRendering, its end right after vkCmdEndRendering
// (vk_exec_pass_end): outside the pass, and without breaking it.
VkPassMark *m = ex->mark = vk_pass_mark_begin(ex->cb, VK_MARK_PASS, ex->fence);
if (m) {
m->step = ex->mark_passes++;
m->w = ex->pass_w, m->h = ex->pass_h, m->d = ex->pass_layers;
m->ncolor = (uint8_t)(ex->pass_ncolor < 8 ? ex->pass_ncolor : 8);
for (int i = 0; i < m->ncolor; i++) m->color[i] = ex->pass_color[i];
m->depth = ex->pass_depth ? ex->pass_depth : ex->pass_stencil;
// The shaders bound now are the first draw's when it repeats the pipeline state; when it does not,
// vk_exec_draw.c prepare_draw replaces them as it records the new state.
vk_exec_draw_shaders(&m->first[0], &m->first[1]);
ex->mark_draws0 = vk_exec_frame.draws;
}
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, {ex->pass_w, ex->pass_h}},
.layerCount = ex->pass_layers, .colorAttachmentCount = ex->pass_ncolor,
.pColorAttachments = ca,
@@ -629,6 +643,14 @@ void vk_exec_pass_end(VkExec *ex) {
vkCmdEndRendering(ex->cb);
ex->recorded++;
ex->in_pass = 0;
if (ex->mark) {
// vk_exec_frame_log resets the frame counters at a present (TOTK_TRACE=gfx): then only the draws since it count
uint64_t d = vk_exec_frame.draws, d0 = ex->mark_draws0;
ex->mark->draws = (uint32_t)(d >= d0 ? d - d0 : d);
if (ex->mark->draws) vk_exec_draw_shaders(&ex->mark->last[0], &ex->mark->last[1]);
vk_pass_mark_end(ex->cb, ex->mark);
ex->mark = NULL;
}
for (int i = 0; i < ex->pass_ncolor && i < 8; i++) // stand-ins of 3D BC targets go to their textures
if (ex->color[i].image && ex->color[i].img && ex->color[i].img->image != ex->color[i].image)
bc_copy_back(ex, &ex->color[i]);
@@ -901,6 +923,8 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
ex->fence = fence;
ex->recorded = 0;
ex->in_pass = 0;
ex->mark = NULL;
ex->mark_passes = ex->mark_dispatches = 0;
ex->dirty = VK_EXEC_DIRTY_ALL;
ex->rt_resolved = 0; // images may have changed since the last command buffer
ex->nviews = 0;
+7
View File
@@ -47,6 +47,11 @@ typedef struct VkExec {
int recorded; // Vulkan commands recorded so far; every handler adds the ones it records
int in_pass; // inside vkCmdBeginRendering
uint32_t dirty; // VK_EXEC_DIRTY_*
// Pass marks (vk.h vk_pass_mark_begin): the open pass's record (NULL: none or marks off), vk_exec_frame.draws when
// it began, and the passes and dispatches this submission marked so far.
VkPassMark *mark;
uint64_t mark_draws0;
uint32_t mark_passes, mark_dispatches;
// ---- render targets (OP_SET_RT) and the current pass
p_set_rt rt; // last OP_SET_RT payload (a change ends the open pass); rt.n 0, no depth: none
@@ -126,6 +131,8 @@ void vk_exec_inline_end(int reopen);
// vk_exec_draw.c: forget what the command buffer being recorded has bound (pipeline, sets, dynamic state, vertex
// buffers): the replay continues in a new one (vk_sync_split).
void vk_exec_draw_new_cb(void);
// vk_exec_draw.c: the vertex and fragment shader of the last draw recorded into this command buffer (NULL: none yet).
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs);
VkBuffer vk_exec_inline_buffer(VkDeviceSize size);
// vk_exec.c: the stand-in a 3D BC render target is drawn through goes with its image (vk_exec_image_gone).
void vk_exec_bc_gone(VkImage image);
+28 -2
View File
@@ -198,6 +198,21 @@ static void cb_done(void *arg) { // completion thread
void vk_exec_draw_new_cb(void) { memset(&B, 0, sizeof B); }
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs) {
*vs = B.st_valid ? B.st.vs : NULL;
*fs = B.st_valid ? B.st.fs : NULL;
}
// Marks a dispatch about to be recorded (vk.h vk_pass_mark_begin); the caller ends it after the dispatch.
static VkPassMark *dispatch_mark(VkExec *ex, const GfxShader *cs, uint32_t x, uint32_t y, uint32_t z) {
VkPassMark *m = vk_pass_mark_begin(ex->cb, VK_MARK_DISPATCH, ex->fence);
if (!m) return NULL;
m->step = ex->mark_dispatches++;
m->w = x, m->h = y, m->d = z, m->draws = 0, m->ncolor = 0;
m->first[0] = cs, m->first[1] = m->last[0] = m->last[1] = NULL;
return m;
}
static CbRes *cb_res(VkExec *ex) {
if (cur && cur_fence == ex->fence) return cur;
cur = calloc(1, sizeof *cur);
@@ -234,8 +249,13 @@ static uint8_t *scratch(VkExec *ex, uint64_t size, VkBuffer *buf, VkDeviceSize *
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
// HOST_COHERENT: the CPU fills these chunks and never flushes them. Intel's integrated GPUs (ANV, Meteor Lake)
// offer host-cached memory only without coherence, which VMA picks for random host access: the GPU then read
// what an earlier submission left in the chunk (an UpdateUniformBuffer payload of another draw: a lighting
// shader's step count of 0x3f800000 instead of 128, and a loop of a billion steps).
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
VmaAllocationInfo info;
VkResult res = vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &c->buf, &c->alloc, &info);
if (res != VK_SUCCESS) rt_fatal("vk: transient buffer of %llu bytes failed (%d)", (unsigned long long)n, (int)res);
@@ -351,7 +371,8 @@ static void dummies_init(void) {
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT}; // the memset below is not flushed
VmaAllocationInfo info;
if (vmaCreateBuffer(c->vma, &bi, &ai, &Z.zbuf, &Z.zalloc, &info) != VK_SUCCESS) rt_fatal("vk: zero buffer failed");
memset(info.pMappedData, 0, ZERO_BYTES);
@@ -2161,6 +2182,7 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
vk_pipeline_set_dynamic(ex->cb, &s);
ex->recorded++;
B.st = s, B.st_valid = 1;
if (ex->mark && vk_exec_frame.draws == ex->mark_draws0) ex->mark->first[0] = vs, ex->mark->first[1] = fs;
}
vk_dlaa_draw(ex);
record_fixed(ex);
@@ -2312,7 +2334,9 @@ static void op_dispatch(VkExec *ex, const p_dispatch *d) {
twin_dispatch = 1;
bind_resources(ex, info);
twin_dispatch = 0;
VkPassMark *m = dispatch_mark(ex, cs, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
vkCmdDispatch(ex->cb, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
vk_pass_mark_end(ex->cb, m);
ex->recorded++;
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, d);
}
@@ -2347,7 +2371,9 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
vk_exec_barrier(ex);
B.c0_valid[2] = 0; // the range no longer holds B.c0[2]
}
VkPassMark *m = dispatch_mark(ex, cs, 0, 0, 0);
vkCmdDispatchIndirect(ex->cb, ip.buffer, ip.offset);
vk_pass_mark_end(ex->cb, m);
ex->recorded++;
vk_mem_count(ip.cls, VK_MUSE_IND, sizeof(VkDispatchIndirectCommand));
VK_MEM_BOUND(ip);
+107 -2
View File
@@ -3,7 +3,8 @@
// the completion thread; a guest-memory word a callback writes is visible once fence_wait returns; finish drains every
// submission; a present of a cleared vk_texture.c image (layout GENERAL) reads back exactly through the swapchain
// capture. Clear part: a hand-built op stream (SET_RT to an RGBA8 and a D32 texture, whole-target clears,
// then scissored clears) through submit reads back exactly with read_texels.
// then scissored clears) through submit reads back exactly with read_texels. Pass marks part (test_marks): the GPU's
// buffer marks and the ring records name the last pass of a submission, and the device-lost report text says so.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -175,7 +176,7 @@ static void init_tex(tex_t *t, pool_t *pool, int64_t off, int format) {
t->d.target = NVN_TEXTURE_TARGET_2D, t->d.format = format, t->d.width = TEX_W, t->d.height = TEX_H;
t->d.depth = 1, t->d.levels = 1;
nvn_tex_layout(&t->d, &t->L);
t->pool = pool, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
t->pool = pool, t->pool_flags = (uint16_t)pool->flags, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
}
// Pixel of the RGBA8 target after the stream: the scissored second clear inside the rectangle, the first elsewhere.
@@ -386,6 +387,74 @@ static uint8_t *draw_prefix(uint8_t *q, prog_t *prog, const pool_t *pool, tex_t
return q;
}
// ---- pass marks part (vk_crumbs.c): one submission with a pass of 2 draws into the draw part's target, a dispatch and
// a pass of 3 draws into a second target. Once its fence is done, the buffer marks must name the last pass as both
// started and finished, the ring records must hold what the three were, and vk_pass_marks_format (the text
// vk_device_fault_report logs) must name it. Without VK_AMD_buffer_marker or with TOTK_PASS_CRUMBS=0 it is skipped:
// NVIDIA's checkpoints can only be read once the device is lost.
static char marks_text[2048];
static int marks_skipped;
static int test_marks(const GfxBackend *b, pool_t *pool, tex_t *color, prog_t *gfx, prog_t *cs) {
uint32_t s0, f0;
if (!vk_pass_marks_read(&s0, &f0)) {
marks_skipped = 1;
return 0;
}
static tex_t second;
init_tex(&second, pool, 0x40000, NVN_FORMAT_RGBA8);
CHECK(0x40000 + second.L.size <= 0x80000, "pass-marks target exceeds its pool slot");
static _Alignas(8) uint8_t stream[8192];
uint8_t *q = draw_prefix(stream, gfx, pool, color);
const p_draw d = {NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 3, 0, 0};
for (int i = 0; i < 2; i++) q = put(q, OP_DRAW, &d, sizeof d);
const p_bind_program bc = {cs, 1 << 5};
q = put(q, OP_BIND_PROGRAM, &bc, sizeof bc);
const p_dispatch dp = {2, 1, 1, 0};
q = put(q, OP_DISPATCH, &dp, sizeof dp);
const p_bind_program bg = {gfx, 3};
q = put(q, OP_BIND_PROGRAM, &bg, sizeof bg);
p_set_rt rt;
memset(&rt, 0, sizeof rt);
rt.n = 1, rt.color[0] = &second;
q = put(q, OP_SET_RT, &rt, sizeof rt);
for (int i = 0; i < 3; i++) q = put(q, OP_DRAW, &d, sizeof d);
q = put(q, OP_END, NULL, 0);
CHECK(q <= stream + sizeof stream, "pass-marks op stream overflow");
uint64_t handle = (uint64_t)(uintptr_t)stream;
uint64_t f = b->submit(&handle, 1);
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the pass-marks submission timed out");
uint32_t started = 0, finished = 0;
vk_pass_marks_read(&started, &finished);
CHECK(started && started == finished, "after the fence the GPU last started mark %u and finished mark %u (before "
"the submission: %u and %u)", started, finished, s0, f0);
const VkPassMark *p1 = vk_pass_mark_record(finished), *dm = vk_pass_mark_record(finished - 1),
*p0 = vk_pass_mark_record(finished - 2);
CHECK(p1 && p1->kind == VK_MARK_PASS && p1->fence == f && p1->step == 1,
"the last finished mark %u is not pass 1 of submission %llu", finished, (unsigned long long)f);
CHECK(p1->w == TEX_W && p1->h == TEX_H && p1->d == 1 && p1->ncolor == 1 &&
p1->color[0] == VK_FORMAT_R8G8B8A8_UNORM && p1->depth == VK_FORMAT_UNDEFINED,
"pass 1 has %ux%u, %u layers, %u colour targets, format %d, depth %d", p1->w, p1->h, p1->d, p1->ncolor,
(int)p1->color[0], (int)p1->depth);
CHECK(p1->draws == 3 && p1->first[0] && p1->first[1] && p1->first[0] == p1->last[0] && p1->first[1] == p1->last[1],
"pass 1 has %u draws (3 expected) and shaders %p %p ... %p %p", p1->draws, (const void *)p1->first[0],
(const void *)p1->first[1], (const void *)p1->last[0], (const void *)p1->last[1]);
CHECK(dm && dm->kind == VK_MARK_DISPATCH && dm->fence == f && dm->step == 0 && dm->w == 2 && dm->h == 1 &&
dm->d == 1 && dm->first[0],
"the mark before pass 1 is not dispatch 0 of 2x1x1 groups of submission %llu", (unsigned long long)f);
CHECK(p0 && p0->kind == VK_MARK_PASS && p0->fence == f && p0->step == 0 && p0->draws == 2 &&
p0->first[0] == p1->first[0],
"the mark before the dispatch is not pass 0 of submission %llu with 2 draws", (unsigned long long)f);
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, marks_text, sizeof marks_text);
char want[128];
snprintf(want, sizeof want, "last finished %u: pass 1 of submission %llu: %ux%u, 1 layer, colour formats %d, ",
finished, (unsigned long long)f, TEX_W, TEX_H, (int)VK_FORMAT_R8G8B8A8_UNORM);
CHECK(strstr(marks_text, want) && strstr(marks_text, ", 3 draws; first vs "),
"the report text lacks \"%s\" or the draw count:\n%s", want, marks_text);
b->texture_release(&second);
return 0;
}
static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_tries, int *n_cs) {
char root[PLAT_PATH_MAX * 2], path[PLAT_PATH_MAX * 2 + 64];
CHECK(probe_census_dir(root, sizeof root), "no build/v%d/shaders/jobs.json above the exe (run tools/shader_census.py)",
@@ -503,10 +572,12 @@ static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_t
// Compute: a few census compute shaders, one dispatch each, resources unbound (zero buffers and stand-in images).
int dispatched = 0;
prog_t *cs_prog = NULL;
for (int t = 0; t < 4 && t < nc; t++) {
const DrawEnt *one[1] = {&e[cs[(long long)t * nc / (nc < 4 ? nc : 4)]]};
prog_t *prog = dr_prog(root, one, 1);
CHECK(prog, "census code file of %.16s is missing", one[0]->key);
cs_prog = prog;
uint8_t *q = stream;
q = put(q, OP_BEGIN, NULL, 0);
const p_bind_program bp = {prog, 1 << 5};
@@ -530,6 +601,7 @@ static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_t
dispatched++;
ind_dispatches++;
}
if (test_marks(b, &pool, &color, hit, cs_prog)) return 1;
b->texture_release(&color);
b->pool_fini(&pool);
free(pairs), free(cs), free(e), free(jobs);
@@ -845,6 +917,31 @@ static int test_mirror(const GfxBackend *b, uint8_t *guest, int *bytes) {
return 0;
}
// A pool of zero bytes (TotK makes some now and then; MacNativeGames hit one on 1.2.1) initialises as an empty pool: no
// Vulkan buffer or memory (a buffer or import of 0 bytes is invalid usage) and no pool index entry, so a lookup at its
// address still finds the pool that starts there, before and after the empty pool's Finalize.
static int test_empty_pool(const GfxBackend *b, uint8_t *guest) {
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0x100000,
.game_mem = guest + 0xE00000};
pool_t empty = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0,
.game_mem = guest + 0xE00000};
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the empty-pool part");
long errors = vk_validation_errors_total();
CHECK(b->pool_init(&empty), "pool_init of a zero-byte pool");
uint64_t a = (uint64_t)(uintptr_t)pool.mem;
VkMemSpan whole, at, after;
CHECK(vk_mem_resolve(a + 16, 16, &whole) && whole.buffer, "the pool beside an empty pool is not resolved");
CHECK(vk_mem_resolve(a, 0, &at) && at.buffer == whole.buffer && at.offset == whole.offset - 16,
"a lookup at the start of a pool finds the empty pool there");
CHECK(vk_validation_errors_total() == errors, "the zero-byte pool raised %ld validation errors",
vk_validation_errors_total() - errors);
b->pool_fini(&empty);
CHECK(vk_mem_resolve(a + 16, 16, &after) && after.buffer == whole.buffer,
"the empty pool's Finalize took the pool beside it out of the index");
b->pool_fini(&pool);
return 0;
}
int probe_vkexec(void) {
vk_hdr_off(); // the present checks read 8-bit swapchain pixels
layouts_equal = 0;
@@ -866,6 +963,7 @@ int probe_vkexec(void) {
int nvnx_bytes = 0, mirror_bytes = 0;
if (!bad) bad = test_nvnx(b, guest, &nvnx_bytes);
if (!bad) bad = test_mirror(b, guest, &mirror_bytes);
if (!bad) bad = test_empty_pool(b, guest);
b->finish();
vk_pipeline_shutdown(); // the pipelines and the draw part's stand-ins go before the device
vk_exec_draw_shutdown();
@@ -910,5 +1008,12 @@ int probe_vkexec(void) {
"between submissions mirrored, CPU bytes beside a fill kept, %d bytes compared)\n", mirror_bytes);
else
printf("probe-vkexec: mirror part skipped (memory mode import)\n");
if (marks_skipped)
printf("probe-vkexec: pass marks part skipped (no VK_AMD_buffer_marker, or TOTK_PASS_CRUMBS=0)\n");
else
printf("probe-vkexec: pass marks part passed (pass, dispatch, pass in one submission: the GPU's last started and "
"finished mark is the last pass, ring records exact); the report reads:\n%s\n", marks_text);
printf("probe-vkexec: empty pool part passed (a zero-byte pool at the start of another: no Vulkan objects, lookups "
"there find the other pool before and after its Finalize)\n");
return 0;
}
+23 -2
View File
@@ -20,6 +20,7 @@
#include "nvn_api.h"
#include "nvn_pool_index.h"
#include "vk.h"
#include "vk_exec.h"
#include "platform/platform.h"
// Timestamps for the tracking cost, in ns; qpc_ms converts.
@@ -245,6 +246,11 @@ int vk_pool_init(pool_t *p) {
// virtual pools that map it. Either gets device-local memory: 1.4.3's CPU never writes its 0x18c pool's storage
// (unchanged over 3600 frames of the intro), so the guest storage and the GPU copy need no sync.
if ((p->flags & NVN_MEMORY_POOL_FLAGS_GPU_NO_ACCESS) && !(p->flags & NVN_MEMORY_POOL_FLAGS_PHYSICAL)) return 1;
// A pool of zero bytes (TotK makes some now and then) holds no address the GPU can use: it gets no record. A record
// would import 0 bytes (invalid usage; the driver refuses the memory) and put an empty range into the index at the
// pool's address, where a lookup of the pool that starts there found the empty one first. vk_pool_fini finds no
// record for it and does nothing.
if (!p->size) return 1;
PoolRec *r = calloc(1, sizeof *r);
if (!r) rt_fatal("vk: out of memory for a pool record");
r->key = p;
@@ -1801,6 +1807,8 @@ static void collect_locked(VkCtx *c) {
for (int i = 0; i < ndead; i++) {
Dead *d = &dead[i];
if (d->seq > done_seq || d->tl > tl) { dead[k++] = *d; continue; }
rec_check_handle(d->kind == DEAD_VIEW ? REC_VIEW : d->kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, d->handle, 1,
d->tl, tl);
switch (d->kind) {
case DEAD_IMAGE: vkDestroyImage(c->device, (VkImage)(uintptr_t)d->handle, NULL); break;
case DEAD_IMAGE_VMA: vmaDestroyImage(c->vma, (VkImage)(uintptr_t)d->handle, d->alloc); break;
@@ -1932,8 +1940,21 @@ static void defer(int kind, uint64_t handle, VmaAllocation alloc) {
plat_once(&cmd_once, cmd_init, NULL);
plat_rmutex_lock(&cmd_lock);
if (ndead == cap_dead) grow((void **)&dead, &cap_dead, sizeof *dead);
// The open batch may record into the object: it counts as the newest user.
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl_noted};
// The open batch may record into the object: it counts as the newest user. The GPU must also be past every
// submission that names it. tl_noted covers the submitted ones; an object the replay lets go of while it records
// a submission (vk_exec_recording_fence) may be named by that one too, not yet submitted: vk_texture.c
// slot_resolve hands a draw a released entry, and entry_of destroys released entries at that address later in
// the same submission. Stamped with the last submitted value, the object went as soon as the GPU reached it,
// before the submission that names it ran, and with the recorder thread (vk_rec.c) behind, before the recorder
// made the call that names it. LAS Recomp and Savage Wind Recomp stamp every deferral with the submission being
// recorded (runtime/nvn/vulkan/textures.c defer: vk_pending_seq). TOTK_DEFER_STAMP=0 brings back the old stamp.
static int stamp = -1;
if (stamp < 0) stamp = rt_knob("DEFER_STAMP", "1") != 0;
uint64_t tl = tl_noted, rf = stamp ? vk_exec_recording_fence() : 0;
if (rf > tl) tl = rf;
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl};
rec_check_handle(kind == DEAD_VIEW ? REC_VIEW : kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, handle, 0, tl,
vk_sync_fence_done());
plat_rmutex_unlock(&cmd_lock);
}
+87 -1
View File
@@ -26,6 +26,10 @@ static VkCommandBuffer rec_cb; // the part being recorded (set while the
static int rec_live; // producer: rec_begin started the recorder for rec_cb
static PlatThread *rec_thread;
static int rec_prio; // the submitting thread's priority, which the recorder takes
static int rec_chk; // TOTK_REC_CHECK (rec_check_handle)
static uint64_t rec_delay_ns; // TOTK_REC_DELAY_US (rec_main)
static volatile uint64_t part_h0; // put_h at rec_begin: where the packets of the part being recorded start
static volatile uint64_t part_fence; // the value of the part being recorded (rec_begin)
// Producer statistics, logged by rec_stats_log.
static struct { uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on; } RS_;
@@ -40,6 +44,7 @@ enum {
R_SetRasterizerDiscardEnable, R_SetScissorWithCount, R_SetStencilCompareMask, R_SetStencilOp, R_SetStencilReference,
R_SetStencilTestEnable, R_SetStencilWriteMask, R_SetViewportWithCount, R_WriteTimestamp, R_WriteTimestamp2,
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask, R_EndCb,
R_PassMark,
};
static uint64_t now_ns(void) { return plat_now_ns(); }
@@ -64,6 +69,9 @@ static void rec_main(void *arg) {
continue;
}
}
// TOTK_REC_DELAY_US (debug): the recorder falls this far behind each batch it finds, as on a slow machine,
// so objects the replay lets go of meet packets it has not made yet (TOTK_REC_CHECK).
if (rec_delay_ns) plat_sleep_ns(rec_delay_ns);
for (int k = 0; t != h; k++) {
uint64_t w = ring[t % RING_WORDS];
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
@@ -103,6 +111,9 @@ static void *put(uint32_t op, size_t bytes) {
}
wait_space(n);
ring[pos] = (uint64_t)op | n << 32;
// Packers reserve room for optional arrays they may leave unwritten (BeginRendering's depth and stencil); with
// TOTK_REC_CHECK those words would still hold handles from the ring's previous lap.
if (rec_chk) memset(&ring[pos + 1], 0, (n - 1) * 8);
put_h += n;
RS_.packets++, RS_.words += n;
return &ring[pos + 1];
@@ -124,7 +135,7 @@ void rec_drain(void) {
RS_.drains++, RS_.drain_ns += now_ns() - t0;
}
void rec_begin(VkCommandBuffer cb) {
void rec_begin(VkCommandBuffer cb, uint64_t fence) {
static int mode = -1;
if (mode < 0) {
// Default off on AMD GPUs. With the recorder those drivers lost the device or hung the GPU at startup on
@@ -140,6 +151,9 @@ void rec_begin(VkCommandBuffer cb) {
if (mode) {
plat_event_init(&wake_ev, 0, 0);
rec_prio = plat_thread_priority_get();
rec_chk = rt_knob("REC_CHECK", "0"); // debug switches, read before the recorder starts
const char *d = cfg_get("TOTK_REC_DELAY_US");
rec_delay_ns = d && *d && atoi(d) > 0 ? (uint64_t)atoi(d) * 1000 : 0;
rec_thread = plat_thread_new(rec_main, NULL, "vk recorder");
if (!rec_thread) {
rt_log("vk_rec: no recorder thread: the replay makes its Vulkan calls itself");
@@ -147,6 +161,9 @@ void rec_begin(VkCommandBuffer cb) {
} else {
rt_log("vk_rec: the replay's Vulkan commands are made by a recorder thread%s",
mode == 2 ? " in the B blocks (TOTK_AB)" : "");
if (rec_chk) rt_log("vk_rec: TOTK_REC_CHECK: deferred and destroyed objects are searched in the ring");
if (rec_delay_ns) rt_log("vk_rec: TOTK_REC_DELAY_US: the recorder waits %llu us before each batch",
(unsigned long long)(rec_delay_ns / 1000));
}
}
}
@@ -154,6 +171,8 @@ void rec_begin(VkCommandBuffer cb) {
if (!RT_KNOB_ON(mode)) return;
RS_.parts_on++;
rec_cb = cb; // the recorder is idle: rec_end drained it
__atomic_store_n(&part_h0, put_h, __ATOMIC_RELEASE);
__atomic_store_n(&part_fence, fence, __ATOMIC_RELEASE);
rec_live = 1;
}
@@ -202,6 +221,64 @@ void rec_stats_log(void) {
memset(&RS_, 0, sizeof RS_);
}
// TOTK_REC_CHECK=1 (debug, recorder on): the packets of the part being recorded are searched word by word for an object
// vk_memory.c lets go of. put() zeroes each packet's room, so only words a packer wrote can match. A hit is a hazard
// - at a deferral (destroy 0) when the stamp tl is below the part's value: vk_memory.c collect_locked may destroy the
// object once the GPU reaches tl, before the part ran and, with the recorder behind, before it made the call;
// - at a destruction (destroy 1) always: a packet not made yet hands the driver a dead handle, a made one leaves it
// in a command buffer not yet submitted.
// "Made" means before the tail the recorder published, which trails it by up to 64 packets. Hazards are logged (the
// first 64) and counted by kind, packet and case for rec_check_log; named deferrals that wait long enough are only
// counted. Reads race the producer only in words below head, which stay put until the ring
// wraps (16 MiB later).
static const char *const kind_name[REC_KINDS] = {"image", "view", "buffer"};
static volatile int32_t chk_seen[2], chk_named, chk_logged, chk_hz[2][2][REC_KINDS][64]; // [destroy][made][kind][op]
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu) {
if (!rec_chk || !handle || kind < 0 || kind >= REC_KINDS || !__atomic_load_n(&rec_live, __ATOMIC_RELAXED)) return;
uint64_t h = __atomic_load_n(&head, __ATOMIC_ACQUIRE), p = __atomic_load_n(&part_h0, __ATOMIC_ACQUIRE);
uint64_t fence = __atomic_load_n(&part_fence, __ATOMIC_ACQUIRE);
if (p > h || h - p > RING_WORDS / 2) return; // a part this large is not searched
plat_inc32(&chk_seen[destroy != 0]);
while (p < h) {
uint64_t w = ring[p % RING_WORDS];
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
if (!n) return; // overwritten meanwhile
if (op != R_PAD)
for (uint32_t i = 1; i < n; i++) {
if (ring[(p + i) % RING_WORDS] != handle) continue;
int made = p < __atomic_load_n(&tail, __ATOMIC_ACQUIRE);
if (!destroy && tl >= fence) { plat_inc32(&chk_named); return; }
plat_inc32(&chk_hz[destroy][made][kind][op & 63]);
if (plat_inc32(&chk_logged) <= 64)
rt_log("vk_rec: check: HAZARD: %s %#llx %s while packet op %u (byte %u of its arguments) of the "
"part being recorded names it, %s; deferral stamp %llu, part value %llu, GPU at %llu",
kind_name[kind], (unsigned long long)handle, destroy ? "destroyed" : "deferred", op,
8 * (i - 1), made ? "made" : "not known made", (unsigned long long)tl,
(unsigned long long)fence, (unsigned long long)gpu);
return;
}
p += n;
}
}
void rec_check_log(void) {
static volatile int32_t said;
if (!rec_chk || plat_xchg32(&said, 1)) return;
int any = 0;
for (int d = 0; d < 2; d++)
for (int m = 0; m < 2; m++)
for (int k = 0; k < REC_KINDS; k++)
for (int op = 0; op < 64; op++)
if (chk_hz[d][m][k][op]) {
any = 1;
rt_log("vk_rec: check summary: %d hazards: %s %s, packet op %d %s", (int)chk_hz[d][m][k][op],
kind_name[k], d ? "destroyed" : "deferred", op, m ? "made" : "not known made");
}
rt_log("vk_rec: check summary: %s; searched for %d objects deferred and %d destroyed while a part was recorded; %d "
"deferred ones the part named waited for its value", any ? "hazards above" : "no hazards", (int)chk_seen[0],
(int)chk_seen[1], (int)chk_named);
}
#define OWNED(cb) (rec_live && (cb) == rec_cb)
// A call the ring takes no packet for: everything queued first, then the call here.
#define FALLBACK() (rec_drain(), RS_.fallbacks++)
@@ -531,6 +608,14 @@ void rec_polygon_mode(PFN_vkCmdSetPolygonModeEXT fn, VkCommandBuffer cb, VkPolyg
pub();
}
typedef struct { uint32_t id; int end; } A_PassMark;
void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end) {
if (!OWNED(cb)) { vk_pass_mark_cmd(cb, id, end); return; }
A_PassMark *a = put(R_PassMark, sizeof *a);
*a = (A_PassMark){id, end};
pub();
}
typedef struct { void *fn; uint32_t first, n; const void *v; } A_Targets;
#define TARGET_CMD(fname, R, PFN, T) \
void fname(PFN fn, VkCommandBuffer cb, uint32_t first, uint32_t n, const T *v) { \
@@ -618,6 +703,7 @@ static void exec_packet(uint32_t op, void *p) {
case R_BlendEnable: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEnableEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_BlendEquation: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEquationEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_WriteMask: { A_Targets *a = p; ((PFN_vkCmdSetColorWriteMaskEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_PassMark: { A_PassMark *a = p; vk_pass_mark_cmd(cb, a->id, a->end); break; }
case R_EndCb: {
A_EndCb *a = p;
if (a->kind) vk_crumb(cb, a->kind, a->n);
+9 -1
View File
@@ -15,13 +15,19 @@
// MIT, Copyright (c) 2026 Peiikai): the driver's work behind the commands depends on nothing the replay decides next.
#pragma once
void rec_begin(VkCommandBuffer cb); // the submitting thread, sub_lock held
void rec_begin(VkCommandBuffer cb, uint64_t fence); // the submitting thread, sub_lock held; fence: the part's value
void rec_end(void); // waits for the recorder; no-op when rec_begin did not start it
// The end of part cb: the breadcrumb crumb (0: none, vk_crumbs.c), then vkEndCommandBuffer, both made by the recorder
// when it records cb, and the wait for it (1); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
void rec_drain(void); // waits until the recorder made every call queued so far
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
// TOTK_REC_CHECK=1 (debug, recorder on): vk_memory.c reports each object it defers (destroy 0, stamped tl) or destroys
// (destroy 1) with the value the GPU reached (gpu); a packet of the part being recorded that still names it is logged
// as a hazard when the object can go before the part ran. rec_check_log: the counts by kind and packet, at exit.
enum { REC_IMAGE, REC_VIEW, REC_BUFFER, REC_KINDS };
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu);
void rec_check_log(void);
// Extension commands called through device function pointers.
void rec_push_tpl(PFN_vkCmdPushDescriptorSetWithTemplateKHR fn, VkCommandBuffer cb, VkDescriptorUpdateTemplate tpl,
@@ -38,6 +44,8 @@ void rec_blend_equation(PFN_vkCmdSetColorBlendEquationEXT fn, VkCommandBuffer cb
const VkColorBlendEquationEXT *v);
void rec_write_mask(PFN_vkCmdSetColorWriteMaskEXT fn, VkCommandBuffer cb, uint32_t first, uint32_t n,
const VkColorComponentFlags *v);
// A pass mark (vk_crumbs.c vk_pass_mark_cmd: a buffer marker and a checkpoint), in order with the commands around it.
void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end);
#define REC_CMDS(X) \
X(BeginQuery, (VkCommandBuffer cb, VkQueryPool p, uint32_t q, VkQueryControlFlags f), (cb, p, q, f)) \
+2 -2
View File
@@ -389,7 +389,7 @@ VkCommandBuffer vk_sync_split(void) {
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
ts_begin(s->cb);
rec_begin(s->cb);
rec_begin(s->cb, cur_fence);
return s->cb;
}
@@ -418,7 +418,7 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
ts_begin(s->cb);
rec_begin(s->cb);
rec_begin(s->cb, fence);
int recorded = vk_exec_record(s->cb, handles, n, fence);
s = &slots[cur_slot]; // the last part (vk_sync_split)
if (recorded) ts_end(s->cb, fence);
+5 -4
View File
@@ -26,6 +26,7 @@
#include "nvn_tex.h"
#include "tex.h"
#include "vk.h"
#include "vk_dlaa.h"
#include "vk_exec.h"
#include "platform/platform.h"
@@ -166,8 +167,7 @@ static int same_but_swizzle(const TexKey *a, const TexKey *b) {
static int tex_mirrored(const tex_t *t) {
if (!t->mem || nvn_gpu_only((uintptr_t)t->mem)) return 0;
if (t->has_layout) return 1;
const pool_t *p = t->pool;
if (!p || !(p->flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
if (!(t->pool_flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
if (t->d.flags & (NVN_TEXTURE_FLAGS_DISPLAY | NVN_TEXTURE_FLAGS_COMPRESSIBLE | NVN_TEXTURE_FLAGS_ADAPTIVE_ZCULL |
NVN_TEXTURE_FLAGS_DEPTH16_PREFER_FAST_CLEAR | NVN_TEXTURE_FLAGS_ZCULL_SUPPORT_STENCIL))
return 0;
@@ -1681,8 +1681,9 @@ static VkSampler make_sampler(VkCtx *c, const samp_par *d) {
if (si.maxLod < si.minLod) si.maxLod = si.minLod;
float lim = c->props.limits.maxSamplerLodBias, bias = d->lod_bias;
// DLSS upscaling (resolution.c): the scene's material textures, the filtered mipmapped ones that repeat, are
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias.
if (!d->cmp_mode && mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR &&
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias. Without DLSS
// running (dlaa = 0, or NGX without DLSS) nothing accumulates the extra detail, and the sharper mips only shimmer.
if (vk_dlaa_available() && !d->cmp_mode && mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR &&
(si.addressModeU == VK_SAMPLER_ADDRESS_MODE_REPEAT || si.addressModeU == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT ||
si.addressModeV == VK_SAMPLER_ADDRESS_MODE_REPEAT || si.addressModeV == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT))
bias += res_dlss_lod_bias();
+17 -3
View File
@@ -254,6 +254,19 @@ class LiftKeyTest(unittest.TestCase):
self.add_cheats('main.npdm', 'not a patch', mod='Fps', where='exefs')
self.assertEqual(self.key(), both)
def test_renaming_a_mod_folder_changes_the_key(self):
# tools/cheats.py and tools/exefs_patches.py name a patch set after "<mod folder>/<cheat or file>": a renamed
# folder needs a lift even though its files are the same
self.add_cheats('277178B7DBA1B6D4.txt', '[a]', mod='MyNXCheats')
self.add_cheats('dynamic-fps.pchtxt', '@nsobid-277178B7DBA1B6D4', mod='Fps', where='exefs')
before = self.key()
mods = self.dest / 'mods'
(mods / 'MyNXCheats').rename(mods / 'MyNXCheats 1.4.3')
cheats_renamed = self.key()
self.assertNotEqual(cheats_renamed, before)
(mods / 'Fps').rename(mods / 'Fps 2')
self.assertNotEqual(self.key(), cheats_renamed)
def test_status_and_other_files_keep_the_key(self):
self.add_cheats('277178B7DBA1B6D4.txt', '[a]')
before = self.key()
@@ -520,9 +533,10 @@ class EventsTest(unittest.TestCase):
catalog = json.loads((ROOT / 'installer' / 'text' / 'en.json').read_text(encoding='utf-8'))
used = set()
for path in (ROOT / 'installer').glob('*.py'):
used |= set(re.findall(r"InstallError\('([a-z_.]+)'", path.read_text()))
used |= set(re.findall(r"warning\('([a-z_.]+)'", path.read_text()))
used |= {f'phase.{s}.{p}' for s, p in re.findall(r"progress\('(\w+)', '(\w+)'[,)]", path.read_text())}
text = path.read_text(encoding='utf-8')
used |= set(re.findall(r"InstallError\('([a-z_.]+)'", text))
used |= set(re.findall(r"warning\('([a-z_.]+)'", text))
used |= {f'phase.{s}.{p}' for s, p in re.findall(r"progress\('(\w+)', '(\w+)'[,)]", text)}
self.assertEqual(sorted(i for i in used if i not in catalog), [])
+19 -2
View File
@@ -68,6 +68,7 @@ import time
import urllib.request
from pathlib import Path
import check_isa
import romfs_index
from build_shaders import stale as stale_shaders
from check_content import GENERATED_MARKER
@@ -114,7 +115,7 @@ if WIN:
RT_FLAGS.append('-D_WIN32_WINNT=0x0A00')
# -Xlinker passes the map path as one argument; -Wl, would split it at any comma in the build dir path
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '-Xlinker', '"--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
VMA_INCLUDE = UCRT.parent / 'include' / 'vma' # vk_mem_alloc.h; MSYS2 package vulkan-memory-allocator
else:
# -no-pie: the lifted modules' function tables (hundreds of thousands of pointers) need no load-time relocation.
@@ -150,7 +151,7 @@ def vma_include():
# --toolchain: an llvm-mingw tree. The libraries, headers and DLLs MSYS2 supplies come from the deps directory instead.
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lvulkan-1', '-lSDL3', '-lopus',
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lvulkan-1', '-lSDL3', '-lopus',
'-l:libvpx.a', '-l:libzstd.a', '-l:libc++.a', '-l:libc++abi.a', '-l:libunwind.a', '-l:libwinpthread.a']
DEFAULT_DEPS = ROOT / 'toolchain' / 'deps-mingw'
# Runtime sources compiled per install, not in the library: romfs.c includes romfs_config.h, patches.c and ov_zstd.c
@@ -715,6 +716,16 @@ def elf_sections(path):
return out, hi - (lo or 0)
def objdump_tool():
"""The disassembler for tools/check_isa.py: llvm-objdump of --toolchain, else MSYS2's or the one on PATH."""
suffix = '.exe' if WIN else ''
for d in (TOOLCHAIN_BIN, UCRT if WIN else None):
for name in ('llvm-objdump', 'objdump'):
if d and (d / (name + suffix)).is_file():
return str(d / (name + suffix))
return check_isa.find_objdump()
def mib(n):
return f'{n / (1 << 20):,.1f} MiB'
@@ -886,6 +897,12 @@ def main():
size = sum(o.stat().st_size for o in files)
print(f' {mod:8} {len(files):5} objects {mib(size):>14}')
verify_objects(out, objs, pairs is not None, main_out, pairs, base, env)
if args.cpu == 'v2':
# Inline assembly, target attributes and intrinsics pass -march=x86-64-v2 by; one vmovdqa of a YMM register
# stops a CPU without AVX (tools/check_isa.py)
fail = check_isa.check_objects(objs['runtime'], objdump_tool(), out / 'obj' / 'runtime' / 'isa-v2.json')
if fail:
sys.exit(f'build_game: {fail}')
if args.compile_only:
return
if args.make_runtime_lib:
+154
View File
@@ -0,0 +1,154 @@
"""Refuse instructions above x86-64-v2 in objects built for --cpu v2.
The v2 build is for CPUs without AVX, AVX2, FMA, BMI1/2, F16C, LZCNT and MOVBE (a Pentium Gold G6400, an older Core
i7). The compiler keeps generated code inside that set by -march=x86-64-v2, but inline assembly, target attributes and
intrinsics pass through: kern_ap_stub (runtime/kernel.c) saved YMM registers with vmovdqa in the v2 library of 0.3.1
and crashed such a CPU with an illegal instruction the first time a core was interrupted. tools/build_game.py --cpu v2
runs this over every runtime object before it links or archives them.
A function that holds such an instruction behind a runtime CPU check goes in DISPATCHED with that check named.
python tools/check_isa.py [--objdump PATH] FILE... (objects, archives or executables)
Standard library only.
"""
import argparse
import collections
import concurrent.futures
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
# Mnemonics outside x86-64-v2 that are not VEX/EVEX encoded, or VEX encoded but without a v prefix (BMI1/2).
# lzcnt and tzcnt decode as bsr and bsf on older CPUs and give other results; tzcnt is left out because the
# compiler emits it on any x86-64 level where bsf would do (a nonzero input, or one it checks first).
NON_V2 = {'andn', 'bextr', 'blsi', 'blsmsk', 'blsr', 'bzhi', 'shlx', 'shrx', 'sarx', 'rorx', 'pdep', 'pext', 'mulx',
'lzcnt', 'movbe', 'adcx', 'adox', 'sha1rnds4', 'sha1nexte', 'sha1msg1', 'sha1msg2', 'sha256rnds2',
'sha256msg1', 'sha256msg2', 'aesenc', 'aesenclast', 'aesdec', 'aesdeclast', 'aesimc', 'aeskeygenassist',
'pclmulqdq', 'rdrand', 'rdseed'}
PREFIXES = {'rep', 'repe', 'repz', 'repne', 'repnz', 'lock', 'notrack', 'data16', 'cs', 'ds', 'es', 'ss', 'fs', 'gs'}
WIDE = re.compile(r'%[yz]mm\d|%k[0-7]\b')
# Function -> the runtime check that keeps it off CPUs without the instructions.
DISPATCHED = {
'blocks_ni': 'runtime/sha256.c sha256_blocks: __builtin_cpu_supports("sha") && __builtin_cpu_supports("sse4.1")',
}
def find_objdump(hint=None):
"""llvm-objdump next to the compiler in use, else on PATH, else GNU objdump; None when there is none."""
for p in ([Path(hint)] if hint else []):
if p.is_file():
return str(p)
for name in ('llvm-objdump', 'objdump'):
found = shutil.which(name)
if found:
return found
return None
def mnemonic(line):
"""The mnemonic of one disassembly line (llvm-objdump or GNU objdump, AT&T syntax), or None."""
s = line.strip()
m = re.match(r'^[0-9a-f]+:\s*', s)
if m:
s = s[m.end():]
words = s.split()
while words and words[0] in PREFIXES:
words = words[1:]
return words[0] if words else None
def is_non_v2(line, mn):
if mn.startswith('v') and mn not in ('verr', 'verw'):
return True # VEX or EVEX: AVX, AVX2, FMA, F16C, AVX-512
if mn.startswith('k') and WIDE.search(line):
return True # AVX-512 mask registers
return mn in NON_V2 or bool(WIDE.search(line))
def scan_one(path, objdump):
"""{(file, function): Counter of mnemonics} of one file."""
hits = collections.defaultdict(collections.Counter)
r = subprocess.run([objdump, '-d', '--no-show-raw-insn', str(path)], capture_output=True, text=True,
errors='replace')
if r.returncode:
sys.exit(f'check_isa: {objdump} failed on {path}: {r.stderr.strip()[:300]}')
obj = str(path)
func = None
for line in r.stdout.splitlines():
if 'file format' in line:
obj = line.split(':\tfile format')[0].split(': file format')[0].strip()
continue
m = re.match(r'^[0-9a-f]*\s*<(.+)>:$', line)
if m:
func = m.group(1)
continue
if not line[:1].isspace():
continue
mn = mnemonic(line)
if mn and func not in DISPATCHED and is_non_v2(line, mn):
hits[(obj, func)][mn] += 1
return hits
def scan(paths, objdump):
"""{(file, function): Counter of mnemonics} for every function outside DISPATCHED with a non-v2 instruction."""
hits = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=min(16, os.cpu_count() or 1)) as pool:
for h in pool.map(lambda p: scan_one(p, objdump), paths):
hits.update(h)
return hits
def check_objects(paths, objdump, record):
"""Scan the objects changed since the last clean scan (their sizes and mtimes in the JSON file record); return
the failure text, or None. Without an objdump it warns and passes: the check guards the build, it is no part
of it."""
if not objdump:
print('check_isa: no llvm-objdump or objdump; the v2 instruction check is skipped', file=sys.stderr)
return None
try:
seen = json.loads(record.read_text())
except (OSError, ValueError):
seen = {}
stamp = {str(p): [p.stat().st_size, p.stat().st_mtime_ns] for p in paths}
todo = [p for p in paths if seen.get(str(p)) != stamp[str(p)]]
hits = scan(todo, objdump)
if hits:
record.unlink(missing_ok=True)
return report(hits)
record.write_text(json.dumps(stamp))
print(f'check_isa: {len(todo)} of {len(paths)} runtime objects scanned, none above x86-64-v2')
return None
def report(hits):
lines = [f' {f} in {o}: ' + ', '.join(f'{k} x{v}' for k, v in c.most_common(6))
for (o, f), c in sorted(hits.items())]
return ('instructions above x86-64-v2 in a --cpu v2 build (a CPU without AVX/BMI stops on them with an illegal '
'instruction); use SSE4.2 code under #ifndef __AVX__, or a runtime CPU check and an entry in '
'tools/check_isa.py DISPATCHED:\n' + '\n'.join(lines))
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('files', nargs='+', type=Path)
ap.add_argument('--objdump', help='llvm-objdump or objdump to use (default: PATH)')
args = ap.parse_args()
objdump = find_objdump(args.objdump)
if not objdump:
sys.exit('check_isa: no llvm-objdump or objdump found')
hits = scan(args.files, objdump)
if hits:
sys.exit(report(hits))
print(f'check_isa: {len(args.files)} files, no instructions above x86-64-v2')
if __name__ == '__main__':
main()
+5 -5
View File
@@ -31,10 +31,10 @@ fetch() {
echo "$name: $commit"
}
fetch LASRecomp https://github.com/peiikai/LASRecomp.git da35b83a9611d046a9a681aae668d7085954d37f
fetch MacNativeGames https://github.com/ly05010419/MacNativeGames.git b75be8cfa704df3701e86130c8ced268e79d6b11 history
fetch d2r-mobile https://github.com/jhaizhou-ops/d2r-mobile.git 53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53
fetch nxnative https://github.com/seedds/nxnative.git d5b380b3a80495609c2e751bb994911a638935ad
fetch LASRecomp https://github.com/peiikai/LASRecomp.git a6b4f03844ecfb4012f18deff96da8b410aeafcd
fetch MacNativeGames https://github.com/ly05010419/MacNativeGames.git d1509b4b62211265fad5449cf53cc9f13484ecc5 history
fetch d2r-mobile https://github.com/jhaizhou-ops/d2r-mobile.git fae8c680fe98f79179cf3fab85f7a6c14ee28600
fetch nxnative https://github.com/seedds/nxnative.git 99330eb81c873c3aafe9fc92194c9687d77fcdbb
fetch SwitchRecomp https://github.com/bgyss/SwitchRecomp.git 0f99e1c5c1e78fdc8e01e58a0f8dc8622e82976b
fetch deko3d https://github.com/devkitPro/deko3d.git 373bfd43d8b2d805fa2eb7d406dbb823032df958
fetch SavageWindRecomp https://github.com/Revivedx/SavageWindRecomp.git 575ad88e02f9a89a9a885d90039d48af33c982f4
fetch SavageWindRecomp https://github.com/Revivedx/SavageWindRecomp.git bf6146c14c37fee8781a601e5a544b93dff6d75e
+1 -1
View File
@@ -105,7 +105,7 @@ LLVM_SKIP_DIRS = {'aarch64-w64-mingw32', 'armv7-w64-mingw32', 'i686-w64-mingw32'
LLVM_SKIP_SUFFIXES = {'.idl', '.tlb'}
# tools/ files the installer steps run (and what they import), copied from the repo.
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'button_icons.py', 'check_content.py', 'extract.py', 'hostmem.py',
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'button_icons.py', 'check_content.py', 'check_isa.py', 'extract.py', 'hostmem.py',
'make_romfs_patches.py', 'pack_shader_cache.py', 'prepare_shaders.py', 'romfs_index.py', 'shader_census.py',
'shader_list.py', 'ultrawide_layout.py', 'cheats.py', 'exefs_patches.py']
TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes
+23 -2
View File
@@ -22,6 +22,19 @@ FIELD = {'x': 44, 'y': 48, 'scale_x': 68, 'scale_y': 72}
KEEP_ROOT = {'AppMenuBG_00', 'PauseMenuBG_00', 'ShopMenuParasailBG_00', 'ShortCutBG_00', 'TitleMenuBG_00', 'EventFade_00',
'EventFadeWipe_00', 'CameraShutter_00', 'PaAppPictureBookDetail_00', 'AppMenuOverlay_00'}
# Layouts whose markers the game places over a point in the world (the lock-on arrow, the name of the item or character
# in front of Link, enemy life bars and notices, speech balloons): the code moves a pane under the root to the target's
# projected position in the 1920x1080 grid, which spans the whole canvas. Under a scaled root that position would be
# scaled too, so the marker lands short of its target, toward the middle of the screen (at half the distance on 32:9).
# Their root keeps scale 1 and the scale goes one level down instead, to the panes listed here: ones the code does not
# move and no animation scales, so the markers keep their 16:9 proportions.
MARKER_ROOT = {
'AttentionBalloon_00': ('C_Capture_00', 'N_InOut_00'), # N_PosRoot_00 moves; N_InOut_00 animates y and alpha
'AttentionLockOn_00': ('N_Offset_00',), # N_PosRoot_00 moves; N_Offset_00 animates y
'EnemyInfo_00': ('Pa_EnemyLife_00', 'Pa_EnemyNotice_00'), # the parts move; N_EnemyLifeArea_* stay screen-sized
'BalloonMessage_00': ('Pa_BalloonMessage_00',),
}
def edits(k, hud_corner):
"""(layout, pane, field, value) for the aspect: k = 16/9 over the aspect (the root's scale), w = how far a 1920-wide
layout's edge moves out in its own units (960 * (1 / k - 1)). OFF moves a pane out of sight."""
@@ -222,8 +235,8 @@ def apply(blarc, members, width, height, hud_corner=True):
assert k < 1, 'only for canvases of another shape than 16:9'
root_scale = 'scale_x' if wide else 'scale_y'
changed, layouts = set(), {}
unknown = sorted(n for n in KEEP_ROOT if f'blyt/{n}.bflyt' not in members)
if unknown: raise SystemExit(f'KEEP_ROOT names no layout: {unknown}')
unknown = sorted(n for n in KEEP_ROOT | MARKER_ROOT.keys() if f'blyt/{n}.bflyt' not in members)
if unknown: raise SystemExit(f'KEEP_ROOT or MARKER_ROOT names no layout: {unknown}')
def pane_offset(layout, pane):
member = f'blyt/{layout}.bflyt'
if member not in members: raise SystemExit(f'{layout}: no layout {member} in the archive')
@@ -232,6 +245,14 @@ def apply(blarc, members, width, height, hud_corner=True):
return find_pane(layouts[member], layout, pane)
for member in sorted(m for m in members if m.startswith('blyt/') and m.endswith('.bflyt')):
if member[5:-6] in KEEP_ROOT: continue
if member[5:-6] in MARKER_ROOT: # the root stays at 1, its listed panes take the scale
layout = member[5:-6]
for pane in ('RootPane',) + MARKER_ROOT[layout]:
at = pane_offset(layout, pane) + FIELD[root_scale]
have = struct.unpack_from('<f', blarc, at)[0]
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{layout} {pane} {root_scale}: {have}, expected 1.0')
if pane != 'RootPane': struct.pack_into('<f', blarc, at, k)
continue
at = pane_offset(member[5:-6], 'RootPane') + FIELD[root_scale]
have = struct.unpack_from('<f', blarc, at)[0]
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{member[5:-6]} RootPane {root_scale}: {have}, expected 1.0')
+20 -5
View File
@@ -7,6 +7,13 @@
# Frame rate limit: 30, 40, 45, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the refresh rate of the
# window's monitor, at most 240). A higher rate gives 240: above it the game's physics runs out of memory. Above 60
# is experimental and needs realtime = 1. Changes while the game runs take effect at once.
# With mailbox or fifo presentation (vk_present, the default) a frame reaches the screen at a vertical blank, so a limit
# that does not divide the monitor's refresh rate keeps each frame on screen for a different number of refreshes. On a
# 320 Hz monitor 80, 64, 40 and 32 divide it and 90 does not: 90 fps alternates 3 and 4 refreshes per frame, which reads
# as judder while the frame counter still shows 90. Likewise on 240 Hz 60, 48 and 40 divide it and on 144 Hz 72, 48 and
# 36 do, while 60 does not. vk_present = immediate shows a frame as soon as it is presented, without this quantisation,
# at the cost of tearing. A monitor running with variable refresh (FreeSync, G-Sync) follows the frames instead, so
# there any limit inside its range is even.
# fps_limit = 60
# Older key, read only while fps_limit is unset: fps60 = 0 means a limit of 30.
# fps60 = 1
@@ -57,7 +64,10 @@
# astc = auto
# Game memory shared with the GPU: auto (mirror on a discrete GPU, import on an integrated one), import (host memory
# imported into Vulkan), mirror (pools used as buffers get a copy in video memory) or verify (mirror plus a check of
# every copy; slow).
# every copy; slow). On a discrete GPU import can be far slower than mirror, not slightly slower: the GPU reads the
# game's pools over PCIe. A 5070 Ti Laptop went from 6 ms to 135-230 ms of GPU time per frame with import (5 fps on the
# title screen, bound: gpu, every line of the perf window waiting on a fence). The log names the mode in use: "mem:
# mode import" or "mem: mode mirror".
# memory = auto
# A new texture created over a live texture's memory starts with that content instead of empty.
# tex_alias = 1
@@ -160,8 +170,8 @@
# color_saturation = 100
# color_gamma = 1
# Sun shadow map size: game (the game's own, which changes with the view and drops to about 400 near the ground, so
# leaf shadows look blocky and jump) or a fixed 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with it.
# Read at start.
# leaf shadows look blocky and jump) or a fixed 256, 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with
# it. 256 is coarser than the game's own and is for integrated GPUs short of memory bandwidth. Read at start.
# shadow_res = game
# Shadow map resolution: the game's sun cascades are 960x960 (and its static shadow maps 1440 and 720) whatever the game
@@ -185,8 +195,9 @@
# totk.exe; elsewhere the setting does nothing. The HUD is drawn after it and is not touched. Read at start.
# dlaa = 0
# What DLSS works from: dlaa (the 3D scene at full size), quality, balanced or performance (the scene at 67, 58 or
# 50 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering to match). With dlaa
# = 0 the game's own scaling is used at that size. Read at start.
# 50 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering to match). They apply
# only with dlaa = 1 and nvngx_dlss.dll next to totk.exe, on Windows; otherwise the scene follows render_scale. Read at
# start.
# dlss = dlaa
# The game's own FXAA after the post effects; with DLAA on it is not needed. Changes while the game runs take effect at
# once.
@@ -313,6 +324,10 @@
# takes longer than flight_spike_ms (0 to 60000).
# flight = 0
# flight_spike_ms = 100
# GPU pass marks: after a GPU crash (device lost) the log names the render pass or dispatch the GPU had started and the
# last one it finished. Unset: on AMD GPUs (VK_AMD_buffer_marker). 1: also on NVIDIA GPUs
# (VK_NV_device_diagnostic_checkpoints), which NVIDIA says costs a little. 0: off.
# pass_crumbs = 1
# Largest render target (bytes, 0 to 1073741824) that goes back to its game memory when the game waits for a GPU fence.
# A game that reads a larger target without invalidating it sees stale bytes at the default. With trace = gfx the log
# names each dirty target over the limit that stays on the GPU ("fence write-back skipped-for-size") and each one over