Each frame the game waits in one nvnSyncWait (1.2.1 lr 0x7000b23dfc) for the frame it presented last. On the Switch
that work is long done by then; here it is still in the replay and on the GPU, and on Great Sky Island the wait is
about 4.6 ms of a 13 ms frame. With TOTK_SYNC_AHEAD=1 that one wait waits for the frame before it instead, once the
replay thread submitted the asked value, so the game builds the next frame while the GPU runs the last one.
The game then writes guest memory that GPU work still in flight reads. Shadow storage makes that safe: a shadow update
of a pool with a reader in flight is queued and copied by the GPU in the next part's prologue, in queue order, instead
of memcpy'd under that reader (TOTK_SHADOW_DEFER=1, the default with the switch; 0 makes the replay thread wait for
the reader instead). The guard is read under the shadow lock and the wait takes that lock once after raising it, so no
sync that read the old guard is still copying when the game writes again. The switch needs shadow storage (Linux and
Wine by default, TOTK_SHADOW=1 on native Windows) and the 1.2.1 call site; otherwise it stays off with a log line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nvnWindowAcquireTexture holds the game until the image is free, which was one period after its present's display time, when the next frame reaches the display. The present is a copy queued by then, so with frame_queue = 1 the image is free from that time: the game draws the next frame while the previous one waits for its turn, and a frame that takes up to one period more still makes its slot. In the town at 4K at 60 fps on an RTX 5070, frames over 17.5 ms fell from 25-33 to 1-8 per 30 s, for up to one frame more input delay. Off by default; the menu switches it at once.
1.4.3 sets its render size once, at start (main+0x7000e67028 calls
main+0x585bd0(renderer, force)). The function-entry hook catches that call's
renderer, and a TOTK_DLSS change writes the new scene size to the patch page and
has the next nvnQueuePresentTexture call the setup again with force set, on the
game's thread between frames, with the guest registers kept. The heap is sized
for the largest scene a mode asks for. The DLSS texture bias is part of the
sampler cache key and follows the mode and the dlaa switch. The game's scaling
pass is told from the in-place FXAA by its missing depth attachment, not by the
sizes, which change a frame ahead of the drawing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
nvnTextureFlushTexels was a no-op stub. Call nvn_guest_written on CPU-visible texture storage so mirrored textures whose texels were flushed by the guest are stamped in the write-generation table for re-upload, matching nvnBufferFlushMappedRange.
With a step under 1/240 s (ratio below 0.125) 1.4.3 aborts with Havok "Out of memory" from the physics pre-solve
buffer resize (main+0x1261630) within seconds of loading some saves, so a 280 Hz monitor with fps_limit = display
crashed on load (issue #241). frame_step_present floors the averaged step at 1/240 s, and the limit, including the
monitor's rate, stops at 240; a higher fps_limit now logs and gives 240 instead of 60. Limits of 240 and below step
exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared runtime code now goes through runtime/platform/platform.h, implemented by platform/win32.c and a new
platform/linux.c: futex locks, a userfaultfd and PAGEMAP_SCAN write watch, signal-based async preemption, our own
fiber switch, and a SIGSEGV handler for the thaw and crash hooks. The window, input, menu and software keyboard get
SDL3 back ends beside the Win32 ones, with the swapchain (gfx_present.c) and input merging (input.c) shared. SHA-256
replaces BCrypt, ELF builds get their own override sections, and the build tools gain Linux branches.
On Linux the selftest and the gfx-abi, astc, nvn and window probes pass. probe-vkmem still fails: imported guest
memory cannot be write-watched on Linux, which the shadow buffers in vk_memory.c will address.
TOTK_FPS_LIMIT (fps_limit in totk.ini, live, menu row on the Game tab) replaces the 30/60 choice of TOTK_FPS60. The
pacer holds frames to the limit in nanoseconds instead of 60 Hz ticks; the real-time step has no lower bound, so game
speed stays 1.000 above 60. `display` follows the exact refresh rate of the window's monitor (QueryDisplayConfig),
measured again when the window moves or the display mode changes. A limit of 30 switches the patch set fps60 off as
before; unset, the older fps60 = 0 still means 30. Above 60 needs the real-time step and is marked experimental.
The vsync event stays at 60 Hz: on 1.4.3 the title runs at 144 fps with it, so the game does not wait for it each
frame. Field on machine A: 69-88 fps, CPU-bound (GPU 3.4 ms per frame).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cfg_set stores a value in memory and bumps cfg_version; consumers that keep a
parsed value compare the version and parse again when it changed. cfg_save
rewrites only the changed lines of totk.ini, keeps comments, and adds a missing
key under its section (after its commented example line when there is one).
The environment still wins over both.
Live consumers: volume (new TOTK_VOLUME host gain in the mixer), frame limit
(TOTK_FPS60 switches the fps60 patch set and the pacer caps at 30 or 60),
rumble strength, curve and triggers, gyro on or off, mouse sensitivity.
TOTK_MOTION=0 and rumble strength 0 now keep their hid handles and read at
rest or stay silent, so they can be turned back on. --probe-cfg covers
cfg_set and cfg_save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pacer gave each frame its display time but the present thread queued vkQueuePresentKHR as soon as the game
handed the frame over, so every present shifted with the frame's replay time: at 60.0 fps mean, 17-18 ms intervals
alternated with 15-16 ms ones. The present thread now submits the blit at once (vk_present_blit, so
vk_present_drain and queue order are unchanged) and holds the host present until the pacer's slot
(vk_present_flip). Acquire keeps waiting until the image's slot plus one period, which is now the time the next
frame really goes out.
TOTK_TRACE=present logs one line per present: host interval, the game's present-call interval, wait and lateness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>