The climb velocity step (1.4.3 main+0x1f49754, 1.2.1 main+0x1918220) multiplies the requested velocity by Nintendo's
factor for a movement loss that grows with the frame rate. 1.4.3 fits it at 30 and 60 fps and caps it at 2 from about
77 fps (63% climb speed at 120, 34% at 240); 1.2.1 has no 60 fps branch (78% at 60). Below a 1/60 s step the set climb
continues the factor with a curve fitted to a replayed ladder climb, taking the patch page's frame ratio without the
time scale, so 30 and 60 fps keep the shipping factor bit for bit, slow motion included. Only the player's climb
modules reach this code. Its own setting, climb_fix under [frame rate], with a menu toggle; off without the real-time
step. Ladder replay, 1.4.3: 1.665 / 1.663 / 1.669 / 1.681 m/s at 30 / 60 / 120 / 240 (shipping 120 1.046, 240 0.563).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The heap bound in scene_for counts the output's own growth against the 2 GiB limit, so a 7680x4320 output at
render_scale game went from the 4096x2304 scene dev renders to 2712x1526, with no setting changed. The bound now
shrinks no scene below 4096 a side, the size dev gave; scenes the 4096 cap kept smaller still grow up to the bound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With dynamic_res at 0 by default the fixres set held the scene at 100 % for every player, the reason #573 was turned
down. The default is now 1, so the set stays off unless a player turns dynamic resolution off in the menu or
totk.ini, and the Steam Deck profile no longer needs its own entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ActorCheckRotateModule's update compared the angle a gear turned since the last update with one 30 Hz frame's worth,
so above 30 fps gears and water wheels read as stopped and the Riogok, Turakamik and Wind Temple doors stayed shut.
The new set gears keeps the update context's +8 (frames elapsed in 30 Hz units, the value 1.4.3 divides by) and
multiplies the threshold by it. Measured: the context is 0.25 at 120 fps with the real-time step and 0.5 at 60 fps
without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 1.2.1 the game asks a compound physics shape for its first child's material at main+0xe056d0 and reads the result
without a check. At the reported crash the shape had none of its 125 children left, although every child is filled
when the shape is built. The crash handler now prints that shape's live bits and each record's shape pointer and name,
which tells removed children from switched-off ones and names the object. It runs only after the crash.
--probe-kernel builds a fake shape in a crashing child and checks the listing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UHD Graphics (i3-1215U, i3-1305U) and Iris Xe drew coloured noise on foliage with dynamic alpha to coverage and black
patches over the scenery with dynamic vertex input; players confirmed that vk_dyn_a2c = 0 and vk_dyn_vertex = 0 clear
both. vk_caps.intel_igpu_win marks Intel's proprietary Windows driver on an integrated GPU, and both defaults follow it.
An explicit ini value still wins. Arc discrete GPUs and Mesa's ANV keep both on: no report there, and the ANV report
(#989) had both off with its glitch still present.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDL 3.4.20 uses default Switch controller stick and IMU calibration when the
pad has none, which fixes the infinite gyro readings on the Beitong Asura 2 Pro+.
The pins in tools/make_release.py (version, zip and devel sha256, SDL3.dll sha256)
and the Windows rows of THIRD_PARTY_NOTICES.md follow. The sniper sysroot row stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VM refused C0 (register conditional), so cheats like C046B400 FFFFFFFF were greyed out as unsupported. C0 now
runs with dmnt's operand types 0-5, widths and conditions; C1 and C2 save, restore and clear registers, C3 moves
registers to and from the static registers. tools/cheats.py gives the same opcode lengths and accepts the same opcodes.
skip() took the end of a nested block at depth 1 for the else of the failed outer block; it now counts depth the way
dmnt does, and 2x000000 is an else only for x = 1.
tests/cheats_vm_check.py runs tools/cheats.py and runtime/cheats.c on cheats of its own, the two from the issue
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The floating-point helpers in simd.h and simd_fp.h inline into every lifted function before llvm-profdata matches
the profile, so the fpcheap change left pgo-v143 with a fifth of its weight while recomp/ stayed the same and
make_release saw nothing. pgo.py merge now reads the instrumented build's Ninja deps log and stores the digests of
the runtime/ headers the generated objects include (cpu.h, simd.h, simd_fp.h); make_release warns when one differs
or a record has none. docs/building.md says to retrain after a change to those headers, and build_game.py names the
libiconv-2.dll that MSYS2's SDL3.dll needs in deps-mingw/bin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.4.3 scales the scene size it reads from the patch page by canvas width over the console mode's base width, which is
1280 in handheld, not 1920. A handheld 1280x720 output at render_scale 100 put 1920x1080 on the page, so the game
rendered a 1920x1080 scene while the DynamicTextureAllocator heap was sized for 1280x720, and heavy scenes ran it out
(crash at main+0x1c2590). scene_scale now uses the mode's base for res_init and res_dlss_mode. In handheld the game
composes a 1280x720 canvas unless the output patch is on, so the output patch is switched on with the scene patch there
and the canvas is the output the factor and the heap assume. Docked is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the textures moved to a pool of their own, the default device-local
blocks hold mirrors and other buffers. With VMA's 256 MiB blocks one block
held 53 MiB of them after 10 minutes in the field, and the headroom thread
kept half a block of each device-local type ahead. TOTK_VMA_BLOCK_MIB
(default 64, 0 = VMA's 256) sets the size; the headroom spare follows it.
On the RX 9070, 10 minutes in the field: VMA blocks 3582 -> 3252 MiB,
process commit 8614 -> 8327 MB, slow frames 13 (2.8 s) -> 9 (1.1 s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: the image retry goes through vk_texture_image, sweep_pressure keeps the released-texture cap on the new
drop helpers, and vk_headroom_release also frees the texture pool's anchor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Textures shared the default 256 MiB blocks with the mirrors and other buffers, and a block went back to the driver
only once everything in it was gone. On the RX 9070 after a few minutes of roaming the field, 3 of 9 device-local
blocks were 13-27% full, held by a hundred small textures and buffers each, and blocks held 0.8-1.4 GB more than their
allocations; where device memory takes process commit, that commit backs nothing.
Own-memory textures now go to a VMA pool with 64 MiB blocks (TOTK_TEX_BLOCK_MIB, 0 = the default pools). Images of a
quarter block or more, images the driver wants a dedicated allocation for, and images the pool's memory type does not
take get memory of their own; the requirements are read before the image is made, since VMA checks neither for a
custom pool. The headroom thread keeps the pool's next block ahead like the default pools'.
Same 10-minute route: 807 MiB of blocks beyond their allocations instead of 1037, most pool blocks 75-100% full, slow
frames not up. Probes pass on v143.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79c6951 left the closing brace of a debug block in the per-heap loop, so the function ended early and the build failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Released textures stayed until 2048 of them were kept or VRAM passed 90% of the budget. On the RX 9070 a 10-minute
field run kept 818 MiB of them, while almost every take-back came within a second. TOTK_TEX_RELEASED_MIB (default
256) runs the same drops as under VRAM pressure while the released entries hold more own memory than that. On the
same route the run ended with 1.1 GB less process commit and 1 GB less in VMA blocks; 15 dropped textures were
created again and the slow frames did not change.
Also documents TOTK_MEM_RESERVE_MIB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crash logs ended in fatal errors when the system's commit ran out for a moment: a staging buffer (-2), a guest commit
with the system at 24442 of 24447 MB, image creation (-2). Windows grows a system-managed page file and some drivers
charge device memory to the process commit, so the same request often succeeds a moment later.
oom.c holds the wait nvn_api_cmd.c realloc_wait already used: it requests a release, retries every 10 ms for up to
30 s, logs the first 8 with the commit numbers. rt_oom_request sets a flag that vk_texture_new_submit takes (one atomic
load per submit): it drops up to 256 released textures of the two kinds sweep_pressure drops (overwritten, clean
mirrored), never GPU-written content, and frees the headroom blocks. vk_oom_release does at once what needs no submit:
headroom blocks, finished upload batches and deferred objects.
- memory.c commit: waits and retries without frz_lock between tries; every caller waits, boot ones too.
- vk_staging and the draw-data chunks (vk_exec_draw.c scratch): release, then wait; the open upload batch is not
submitted, its caller still records into it.
- create_tex: release and one retry; no wait under the texture lock, the texture is asked for again at its next use.
- the texture write-back buffer: release, then wait.
- the headroom thread keeps no spare block for 30 s after a failure.
TOTK_TEST_OOM=<site>:<n> (commit, staging, image, chunk) fails the first n attempts; probe-kernel forces 3 refused
commits, probe-vkexec 2 staging buffers, 1 image and 1 chunk, and both pass through them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79c6951 left the closing brace of a debug block in the per-heap loop, so the function ended early and the build failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c838d96 counted system memory for every discrete GPU but NVIDIA's, an assumption. An RTX 5080 log (#398) showed 10.4 GB
of process commit at frame 1200, about what the RX 9070 holds with its VRAM counted, and the RTX 3060 Ti in #670 ran
out of system commit (17.8 GB) at 4K with its VRAM full. vk_mem_host_probe now allocates 128 MiB of device-local memory
once after device creation and compares the process commit before and after; half the size or more turns the system
memory cap on. The log line carries the measured amount, so user logs show what each driver does.
RX 9070: "a 128 MiB device-local allocation took 128 MiB of process commit".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The size rule for TOTK_ASTC_BC looked at the largest device-local heap only. On an integrated GPU that heap is system
RAM reported as VRAM (10.6 GiB on the Ally X, 14.9 GiB on the 880M), so RGBA8 stayed on exactly where memory is
shortest. vk_mem_uma() now turns BC3 on there as well.
7900X iGPU, field save, same exe, frame 1800: RGBA8 commit 7047 MB, GPU 49-53 ms, 16.8-18.4 fps; BC3 commit 6155 MB,
GPU 42.6 ms, 21.6-21.8 fps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headroom woke for every memory type that got new device memory, the staging pool's own 64 MiB blocks included, and
then kept a 256 MiB spare block in that type's default pool. On the RX 9070 the default pools of host memory types 1
and 3 held 4 MiB, beside 512 MiB of spare blocks. Textures and mirrors, whose block allocations headroom moves off the
render thread, live in device-local types.
RX 9070, field save, 60 s: heap 0 VMA blocks 192 MiB instead of 704, driver usage 1384 MiB instead of 1896; 60 fps,
1% low 51.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The paths that give memory back under pressure (headroom's spare block, released textures, pool mirrors) watched the
Vulkan heap budget only. On a UMA GPU that budget is gigabytes of the same RAM the game runs in, and on Windows the
AMD driver charges device-local allocations to the process commit one to one (RX 9070: VRAM use and private bytes
grew by the same 1164, 256 and 256 MiB). Crash logs show the result: the process at 4.4-10.5 GB of commit, 16 GB
machines at 80-94% RAM, std::bad_alloc inside amdvlk64.dll (#285), allocation failures on Intel UHD.
vk_mem_host_room (vk_memory.c) is what the system can still give beyond a reserve (TOTK_MEM_RESERVE_MIB, 1536):
commit, and on a UMA GPU physical memory too; Linux overcommits, so there only a UMA GPU's available memory counts.
NVIDIA is left out, not measured. vk_mem_vram caps the budget at use plus that room, and headroom keeps its spare
block only while four blocks fit in it.
RX 9070, field save, 60 s with the room near zero from the start (TOTK_MEM_RESERVE_MIB=21000): commit 7124 MB instead
of 8965, VRAM 2631 MiB instead of 3781, released textures 153 instead of 1866; mirrors refused for 423 MiB of pools,
GPU 4.45 ms a frame instead of 3.34 ms, 60 fps either way. Without pressure nothing changes. The census logs the
room, the device class line the mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A driver that runs out of memory faults or throws std::bad_alloc instead of returning an error: #285's log shows
bad_alloc inside amdvlk64.dll under gpl_link, and nothing in the report told that apart from a driver bug. The crash
report now prints the process and system commit, as rt_fatal does for our own failed allocations; --probe-kernel
checks the line.
The census at frames 600 and 1800 logs the commit as a baseline and, per heap, VMA's blocks and allocations beside
the driver's usage and budget, so a report shows whose memory grew.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The crash report named the lifted function and printed the guest registers, but the lifted code keeps guest
registers in host locals and writes them back only at calls, so the dump showed stale values and the faulting
instruction stayed unknown. Logs from #1005, #967 and #998 could not be traced further than the function.
The report now adds the x86 registers of the faulting thread (Windows and Linux), 64 bytes of machine code around
rip with the offset into the lifted function, so the instruction can be found in a local build of the same version,
64 bytes of memory around every register value that points at readable memory, and the first 128 bytes of the
guest stack. --probe-kernel checks the new lines with a child that faults with known values in rbx and x19.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ported onto vk_dlaa_plan (#794): the scene end in the inventory also records its submission and op (end_fence,
end_pos) so chunked replays see it at the same point. DLSS-only: everything sits behind dl_active().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
replay_workers stays 0. On the 7900X 4 threads cut the replay by a quarter (town 6.7 -> 5.0 ms, fight2 5.3 -> 3.9 ms)
with no fps change: the game's own threads bound the frame there. Weak CPUs may gain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
entry_of decides at record time whether a new texture takes the content of a live GPU-written texture at its storage
(alias_copy) or the storage bytes. In a chunked replay the source's GPU ownership is set when a chunk records its
render, so the choice followed the order the threads got to the chunks: a later chunk could take the old bytes before
an earlier chunk recorded the render, and a mirrored texture keeps them. Now a chunk that makes an entry while a live
colour texture of its size sits at the storage waits until the chunks before it are recorded (vk_exec_chunk_settle,
the texture lock released for the wait), skips sources the scan revives at a later op, and ignores GPU ownership a
later chunk set in this submission. slot_resolve and vk_texture_by_id_locked no longer read tslots through a pointer
taken before the wait.
TOTK_REPLAY_SKEW_US (debug) makes the even chunks wait before they record, so the chunk after each one records
first in every submission. With it at 3000 us and 4 threads the field save runs clean (grass drawn, 0 validation errors); the alias path
did not fire there (0 copied, 0 filled from storage), so this does not reproduce the missing grass reported on
RADV, and the fix there stays unconfirmed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FCMP and the vector compares no longer flush denormal operands under FPCR.FZ: rt_fpcr_sync keeps MXCSR.DAZ equal to
FPCR.FZ, and x86 compares under DAZ read a denormal as a zero of its sign, as ARM does under FZ. The directed RMode
check of FN(op) and FN(vop) moved into the result check's branch, and min/max skip it, since they do not round.
Hot lifted functions shrink by about 10% of their x86 instructions; the town camera-pan bench (1080p, two runs each)
goes from 10.25 to 9.78 ms per frame. simd.py (60000 encodings), lift.py and func.py report 0 failures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With vb_used the streams a draw does not read got the zero buffer, so draws in a row that read different streams
bound all 17 slots again almost every draw. On an RTX 5060 Laptop that cost 16 % of the frame rate (issue #974). Now
only the read streams are compared, one bind covers the changed ones, and the others keep the buffer bound earlier
in the command buffer, which the deferred frees keep alive until the submission is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An i5-1235U or Core 3 100U has 2 P-cores. Binding the five frame-critical threads there, with a spinning recorder
among them, crowds two cores and their hyperthreads while the E-cores stay idle. plat_perf_core_setup now counts the
allowed physical P-cores and selects nothing below PC_MIN_CORES (4), so the scheduler places those threads as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rec_submit queued each part's submission and then drained the ring until the recorder had submitted it: 0.29 ms a
frame in the town on a Ryzen 7900X, 0.34 ms on 4 cores. Now it publishes the submission and returns
(TOTK_REC_ASYNC=1, default). The queue order the present, uploads, readbacks and device waits rely on is kept by
vk_queue_lock: a thread's first lock waits until the recorder submitted what was queued before (rec_queue_wait).
The recorder itself and a thread that already holds the lock skip the wait. rec_submit reports the first failed
submission the recorder made. Town, in-run A/B: replay -4.1% on all cores, -2.9% on 4 cores / 8 threads; queue
users waited about 0.02 ms a frame. fight2 with TOTK_REC_CHECK and the town under sync validation are clean.
Where the recorder does not poll (fewer than 6 usable logical processors) the producer now publishes the ring head
every 8192 words instead of 64, as Savage Wind Recomp does (runtime/nvn/vulkan/record.c rec_init): each publish
there costs an event set and a wake-up. Town on 4 logical processors: frame interval -1.3%. TOTK_REC_PUB=ab
compares both; the perf log counts the recorder's wake-ups and the queue waits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an Intel CPU with P-cores and E-cores a thread on an E-core runs at about 60% of a P-core's speed, and the frame
waits for its slowest critical thread. The game's main thread, the thread that presents, and the vk replay, recorder
and present threads now select the CPU sets of the P-core logical processors the process affinity allows. Nothing is
selected on a CPU with one efficiency class or when the affinity leaves only one kind of core. TOTK_PCORE=0 turns it
off; "power:" log lines say what was selected and which threads took it. LAS Recomp does the same for its game,
render and vsync threads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The executable exports NvOptimusEnablement and AmdPowerXpressRequestHighPerformance, which NVIDIA Optimus and AMD
switchable graphics read at start to run it on the dedicated GPU; Optimus can otherwise hide its GPU from a program
it put on the integrated one. At start the process opts out of EcoQoS (E-cores and low clocks on hybrid Intel CPUs
when Windows deems it in the background) and of Windows 11 ignoring its timer resolution while the window is hidden.
One "power:" log line says whether Windows accepted it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guest waits and waits for a virtual core usually end within microseconds. A thread now polls for 20 us with the
global lock released before it sleeps on its condition variable, so a quick wake costs no OS wake-up
(TOTK_KSPIN_US, 0 turns it off). The recorder polls by the clock for 60 us instead of 4096 pauses, which last a few
us on Zen 4 and about 140 us on Coffee Lake (TOTK_REC_SPIN_US). Both stay off with fewer than 6 usable logical
processors. Savage Wind Recomp polls the same way before it parks.
Machine A, town save, in-run A/B: frame interval -1.8% on average for the kernel poll (5 of 7 block pairs), -0.7%
with the process limited to 4 cores / 8 threads; the recorder poll is neutral there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_pool_fini freed import, physical and virtual pools right after vkQueueWaitIdle, which covers submitted work only;
with the synchronous replay another game thread may be recording a submission that names the pool. Every pool kind
now retires through the completion queue at the later of the recording's value and the next one.
destroy_tex freed the texture record while other threads still held &e->pub (render targets for a pass, storage
bindings writing wr_fence through it): they read a stale VkImage and cached views by that handle. The record is now
freed with its image's stamp (vk_defer_free), and views of a dropped entry are deferred at once instead of cached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the synchronous replay (the default since 7be68ec) a game thread finalizing a texture destroys released texture
cache entries while another game thread records a submission that slot_resolve may already have handed one of them.
defer() saw the recording's value only on the recording thread and stamped the others with the last submitted value,
so the completion thread freed the image before the submission naming it ran: "write invalid" device faults on NVIDIA
(#788, #802). vk_sync_submit now notes the value at the start of the recording, and every deferral made from then on
waits for it. A fight run counted 10 such deferrals during the world load.
Device faults now log the deferred-free state, and with VK_EXT_device_address_binding_report the object that held the
faulting address and whether it was unbound before the fault (TOTK_ADDR_REPORT=0 turns the map off). The census line
counts deferrals made off the recording thread during a recording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy time bounds how long a compute miss on the drawing thread can wait for the save now that builds and the copy exclude each other; a put-off autosave is logged too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The replay thread deswizzled every block-linear texture on the CPU, and entering a new area stalled it for 50-236 ms. Now a compute pass reads the texture straight from the pool's imported buffer and writes a linear copy into a device-local scratch pool; the upload batch copies it into the image, or the ASTC decoder takes it. Large texture loads submit the batch early so the staging and scratch pools do not grow on the replay thread, and both pools are made at device creation.
On fight2 the render thread waits about 270 ms on textures per run instead of 900, and the load frame drops from 145 to about 80 ms. Mean fps is unchanged. TOTK_DESWIZZLE=cpu turns it off and check compares every upload with the CPU result (0 of 196k regions differ). Shadow mode (Linux/Wine) and textures outside the device's limits stay on the CPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An exception no handler took (a fault in a GPU driver, a C++ exception a driver let escape) listed only the return addresses in totk.exe, and those match only the reporter's own build. #879 and #285 crash in the AMD driver with nothing in the log saying which driver code failed or what it threw.
The unhandled-exception filter now lists the frames of every module on the faulting thread with the module name and offset, up to 12 outside totk.exe so this executable's frames still fit. For a C++ exception thrown by MSVC-built code it reads the thrown type's decorated name from the ThrowInfo, and what() for a std::exception, with every read checked. The guest pc of a host function call is printed with that host function's name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a game menu has the pointer the left mouse button is A, and it was Y's only default key, so keyboard and mouse players could not sort the inventory or press Y anywhere in the menus. T is now Y's second default key.
A bind line in totk.ini now also removes its keys from the defaults of buttons without a line, as the remapping rows already do: with bind_X = key:t, T presses X only, not X and Y.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bindings dropped every mouse button while the cursor was not captured, and the menu pointer releases the capture,
so side buttons bound to L and R turned the menu's tabs only in the frames before the pointer appeared. While the menu
pointer is shown and over the window, the middle and side buttons now count; the left and right buttons stay the
menu's A and B.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release zip dates every file 1980-01-01, so a new release extracted over an old Setup folder has sources and a
libtotkrt.a older than the objects and the totk.exe the last install left in temp\<name>\build. Ninja compares mtimes
only: it kept the old mouse_direct.o and romfs.o and linked them with the new library (undefined symbol
input_direct_mouse_shake on 0.3.11, romfs_shaped_ready on 0.3.12, #795), or skipped the link and kept the old exe
when the version string did not change.
With --runtime-lib, build_game.py now writes obj/runtime/inputs.sha256, one hash over the library, the INSTALL_SIDE
sources and every runtime header, rewritten only when the hash changes. The install-side objects and the link depend
on it. Builds without --runtime-lib are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>