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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 22:44:39 +01:00
Valiantsin DzerakhandClaude Opus 5.5 beda188776 Drop released textures the game has overwritten when VRAM runs short.
Under pressure only clean mirrored entries could go, so once those ran out the render targets stayed: 1.7-2.0 GiB of released images with the use still over the line. Most of them sit on storage the game has since given to another texture, and they are never taken back. The pressure path now drops those first, 30 s after their release and when no finalized slot has bound them for 600 submits. Clean entries wait 5 s, which ends the re-creation churn in fights, and both kinds stop once the use is back under the line.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:53:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 627e560648 Merge pull request #743: keep uniform updates inside a pass as scratch overlays and copy them at the pass end (ApereLieZ)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:21:26 +03:00
Claude 9069c9208f Keep uniform updates inside a pass as scratch overlays and copy them at the pass end
nvnCommandBufferUpdateUniformBuffer between draws was replayed as a buffer copy at that point, which needs a barrier
against the draws that read the old bytes, and a barrier ends the open pass. The terrain and the sky islands' meshes
update before most of their draws: about 200 updates inside passes in a field submission, each a pass end, a full
barrier and a pass begin.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BspNcezsiV8zT8wcnXrrME
2026-11-04 23:54:02 +00:00
72 changed files with 4571 additions and 486 deletions

No files matched your search

+3 -1
View File
@@ -26,6 +26,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- 16:10 (Steam Deck, handhelds), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
- In-game settings menu (F1) and performance overlay (F10)
- Play time across runs on the Game tab of the settings menu, kept in `playtime.txt` next to the saves
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
- Keyboard, mouse and gamepad with remapping, rumble and gyro
@@ -34,7 +35,8 @@ 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/mods.md](docs/mods.md))
([docs/mods.md](docs/mods.md)). HD texture packs work: the port grows the texture heap a pack asks for by itself. Tested
with the HDPacks.com 4K Texture Pack v1.0 on 1.4.3 and 1.2.1; other packs come with no guarantee
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
## Planned
+1 -1
View File
@@ -1 +1 @@
0.3.11
0.3.13
+4
View File
@@ -90,6 +90,10 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
`TOTK_UBO_SNAPSHOT`); without it, late draws read another object's matrix.
- Uniform updates. `nvnCommandBufferUpdateUniformBuffer` between the draws of a pass is replayed as an overlay: the
bytes go to scratch memory, later bindings of the range bind them, and the copies into the game's buffers are
recorded behind one barrier when the pass ends (`vk_exec_draw.c` `op_update_ub`, `TOTK_UB_OVERLAY`). A copy at the
update itself would end the pass with a full barrier, which the terrain paid before most of its draws.
- Driver data. The NVN driver keeps a per-stage constant bank (c0) that shaders read for things like point size, dispatch size and base instance. The back end fills it the way the driver does.
- Shaders: the Ryujinx translator 1.3.2 with MacNativeGames' patches and ours (`tools/xlate/patches/`), built with .NET 10 NativeAOT as `totk_xlate.exe` and run as a separate process. Its SPIR-V goes to Vulkan directly. `tools/prepare_shaders.py` and `totk.exe --prepare-shaders` translate and compile the variants of MacNativeGames' shared list ahead of play (29,587 for 1.4.3); the rest are translated on first use and cached on disk.
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it. Shader stages are compiled ahead into libraries; a draw that meets a new state links them at once (fast link), and an optimised pipeline replaces the linked one when a background thread finishes it. Pipeline keys seen in play are saved and built at boot. A draw whose pipeline is not ready is skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise, so one-shot passes are never lost; a full-screen draw is skipped as well rather than waiting (`TOTK_PIPE_SKIP`); the stage libraries a waiting draw needs are built on idle workers in parallel (`TOTK_PIPE_WAIT`). Without the extension, a missing pipeline is built on a worker and the draw waits or is skipped by the same rule.
+11 -1
View File
@@ -118,6 +118,13 @@ of them.
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`. `--prompts pc` writes
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
A build from source has no layouts for other shapes than 16:9. At an `output_res` such as 5120x1440 the game then
draws the largest 16:9 size inside it (2560x1440) and the present adds bars, instead of a wide camera over a
stretched HUD (`runtime/resolution.c`, `shape_fallback`). For the full shape make `romfs-patches-<w>x<h>` once per
size: `make_romfs_patches.py --version <ver> --aspect <w>x<h> --out build\<ver>\romfs-patches-<w>x<h>` (the sizes
the installer makes are `ASPECT_SIZES` in `installer/steps.py`); `build_game.py` copies every `romfs-patches*`
folder next to `totk.exe`.
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
for the shared shader list:
@@ -231,7 +238,10 @@ Before a commit:
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
files); a change to the settings passes `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`, which fails
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md).
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md). A change to the mods' ExeFS patches
(`tools/exefs_patches.py`, `runtime/mods.c`, the `folded.txt` of `recomp/gen.py`) passes
`.venv\Scripts\python tests\exefs_patches_test.py`, which makes its own modules and runs `gen.py` on one with a jump
table (no game files), and `totk.exe --probe-mods`.
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
change passes these, all run with `.venv`:
+5 -5
View File
@@ -14,7 +14,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|---|---|---|---|
| Left stick | left stick | W A S D | move |
| Right stick | right stick | mouse; arrow keys as a second source | camera |
| Y | west | left mouse button | attack |
| Y | west | left mouse button, T | attack |
| ZL | left trigger (over 50%) | right mouse button, Z | shield, lock on |
| ZR | right trigger (over 50%) | F, X | bow |
| X | north | Space | jump |
@@ -29,7 +29,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
| Minus | Back | M | map |
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. T duplicates Y for the game's menus, where the left mouse button is A while the pointer is shown (sort the inventory with T). The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
## Xbox button prompts
@@ -46,7 +46,7 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
| Plus, Minus | Menu, View | Menu, View |
| D-pad | cross | cross |
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters stay and the diamond is laid out as an Xbox pad: A at the bottom, B on the right, X on the left and Y at the top, the button the prompt names lit in its place, its bar reaching left from X and right from the others (`tools/make_romfs_patches.py`, `bflyt_xbox_diamond`: the panes of `blyt/DoCommand_00.bflyt` take their partner's place, A's of B and X's of Y). The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
@@ -96,7 +96,7 @@ The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
- `none` leaves the button unbound.
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above, less any key a line binds: with `bind_X = key:t`, T presses X only, not also Y. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
A pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
@@ -197,7 +197,7 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
## Mouse in the game's menus
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The middle and side mouse buttons keep their bindings (bound to L and R they turn the tabs), and T presses Y (sort). The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
+3 -1
View File
@@ -60,7 +60,9 @@ loads the saves 1.4.3 wrote.
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
tools refuse costs those cheats or patches, not the install.
tools refuse costs those cheats or patches, not the install. The patch files' writes into main's data go to
`build-cache/exefs.data`, which the game stores into main at start; after the lift they are checked against the
bytes the lifted code read as constants (`<gen>/main/folded.txt`, its jump tables).
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
hashes and the game version's patch page) and link
them with the prebuilt runtime library for the host's level (`libtotkrt.a` or `libtotkrt-v2.a`) into `totk.exe`
+19
View File
@@ -71,6 +71,10 @@ The rebuild needs the setup folder the game was installed from. When that folder
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
A patch file may also change the game's data (constants and tables in its `.rodata` and `.data`). Those bytes are
stored into the loaded game at start, before it runs. When two mods write the same bytes, the higher one wins,
as with files.
## Cheats
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
@@ -87,6 +91,21 @@ The port keys a mod by its folder name. A renamed folder counts as a new mod:
Rename mod folders before you set them up in the menu, not after.
## HD texture packs
Texture packs made for the emulators replace the game's textures with larger ones, so the heap the world's textures
stream into must grow. Such a pack grows it in its own `System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs`, sized for
the emulators' 8 GB memory option. The port reads that file at start: the texture heap and the game's memory pool grow
by the pack's amount, and the game gets the port's own heap table with that growth (the log says
`resolution: mod <name> ships a HeapDef with TextureHeap ...`). Nothing needs to be set. For a pack without such a file
that still loops the panic Blood Moon, set `tex_heap_extra_mb` under `[mods]` in `totk.ini` (0 to 4096 MiB).
Tested: **HDPacks.com Tears of the Kingdom 4K Texture Pack v1.0** (`4kTextures`, 10,407 textures, 6.5 GB) on game 1.4.3
and 1.2.1, RTX 4080 SUPER: it loads into the game from a save and runs at the same frame rate, with about 3.3 GB more of
textures in memory (4.5 GB of game memory pools against 1.2 GB without it). Other packs are not tested and come with no
guarantee. Such a pack needs a lot of video memory; on a GPU with less than 8 GB, `astc_bc = 1` under `[performance]`
keeps its textures at a quarter of the size.
## What does not work
| On the Mods tab | Why | What to do |
+91
View File
@@ -0,0 +1,91 @@
# Rules for taking a PR
## 1. What to review first
1. Fixes before features. A PR that fixes a bug, a glitch or a crash goes first; feature PRs wait.
2. Order fixes by reach: how many players the bug hits and how badly. Windows and default settings first, rare
hardware and unusual setups later.
3. Scale the depth of the checks to the reach. A crash many players hit and anything on the frame path get the full
checks. A rare case (a CPU without AVX, an unusual setup) gets the minimal fix, one targeted check and a short
note. A crash fix on rare hardware is still worth taking: cut the checks around it, not the fix.
4. Before testing, find out where the bug reproduces: OS, GPU vendor and generation, game version, settings. If the
symptom cannot appear on the hardware at hand (an NVIDIA paging bug on an AMD machine), say so and do not run
tests that cannot decide it.
5. Check a feature PR against the designs in `docs/milestones/`. A feature with a design (mods, cheats) goes in as
part of that design.
6. Several PRs on one topic: compare them before taking either. Another PR may already do it right.
## 2. How to read the diff
- The author's claim is a hypothesis. Read the whole diff: races, wrong semantics, edge cases, leaks, lock order.
- Check the behaviour against evidence: how yuzu, Ryujinx, LAS Recomp, SavageWindRecomp, nxnative and MacNativeGames
do the same thing in `refs/` (`tools/fetch_refs.sh` recreates it); the Vulkan spec; hardware docs
([docs/maxwell.md](../../docs/maxwell.md)); upstream issues.
- Weigh two things for every change: its cost on the hot path, and how it fails (device lost, hang, stale data, weak
or non-AVX CPUs, other GPU vendors, Wine/Proton).
- Read the code a hypothesis depends on before running the game: the game's functions in Ghidra or
`generated/<ver>`, our path in `runtime/`. A run confirms what the code could not rule out. It is one run, not a
series of variants.
- Order the work so the cheapest check that could refute the PR's idea runs first. Refuted: stop, write down why,
no build, no benches.
## 3. What a PR needs
- **Proof.** The bug reproduced before the fix, the same repro after, both results side by side: a screenshot, a log
line, a probe, a differential test. A fix without a reproduced failure is not proof. Where a repro is impossible
on the hardware at hand, say so; the fix is then taken only on strong static evidence in the code. Where a repro is
risky (device lost or a TDR can hang the machine), write the recipe and let the owner decide.
- **Frame-path changes** get whole runs of the heavy scene with the switch off and on, at least two each
([02-performance.md](02-performance.md)). Zero new slow frames is a hard bar.
- **A switch.** A change on the hot path or in rendering sits behind an env or `totk.ini` switch, so a player can turn
it off and we can compare.
- **Nothing changes for players outside the feature.** The DLSS PRs touched shared code; they went in once every
path that runs without DLSS was gated on DLSS being on.
- **A fix for one setup keeps the default for everyone.** The PRs that raised the low memory block from 6 to 10 GiB for
all players were rejected; the right shape is 6 GiB plus the extra where it is needed.
- **Parts we cannot run** (Linux-only code, an HDR display, RTX-only paths) are gated so they cannot touch anyone
else, or wait for a report from someone with that hardware. Note them as unrun.
- **Project tests** from [03-correctness.md](03-correctness.md): lifter differential tests, probes, `--selftest`,
`tools/check_content.py --staged`.
- **Text the player sees** goes into the catalog: `runtime/ui_text.h` ids and `runtime/ui_text.c` entries in every
language the menu has (English, Russian, Brazilian Portuguese, Simplified Chinese), and `installer/text/*.json` for
the installer. No hardcoded sentences, no second string table.
- **A new setting** is described in `totk.ini.example` and [docs/settings.md](../../docs/settings.md).
- **Licences.** MIT, BSD, ISC, zlib and Apache-2.0 code may be copied, with its header and a row in
`THIRD_PARTY_NOTICES.md`. yuzu, suyu and other GPL code is read for behaviour only; we write our own.
## 4. Reasons to reject or rework
- No test or repro for a change that is not trivial.
- Slow: a state copy per draw, a global atomic counter on the hot path, synchronous file I/O or a GPU allocation on a
game or render thread, new slow frames.
- Changes behaviour for everyone to serve one setup, or is on by default without a run in a scene where it acts.
- "Fixes" a crash by turning a feature off for a vendor. A crash that goes away with a feature off points at the bug;
the feature gets fixed.
- Breaks another feature (a preview that loses anti-aliasing).
- Fake features: labelled FSR4 or frame generation, and not that.
- Wrong measurements: percentiles over window means, counters over the wrong set.
- Generated C edited by hand instead of the generator (`recomp/`).
- A disallowed licence or pasted GPL code.
## 5. Finishing a good PR
A good PR that misses the project's scheme (hardcoded text, conflicts, layout, comment style) is brought to it while
merging, not sent back to the author for another round. When only part of a PR is good, take that part and note what
was left out and why.
A bug found outside the PR under review is not fixed on the spot: search the open PRs on that topic, report the bug
with them, and let the owner decide.
## 6. Merging, branches, commits
- Each PR or wave of PRs gets its own branch `task/<tag>` from a fresh `dev` and its own build dir. It merges into
`dev`.
- Once the review is done, the symptom is gone on one paired shot and the probes pass, merge into `dev` and hand the
build to players: they test in game faster and wider than our routes. The wider checks (other routes, validation,
fps cost) follow as a separate diff. Speed never skips the review of the diff itself.
- `main` only with the owner's explicit yes. Push only when told to.
- A commit message is a plain English sentence, no `feat:`/`fix:` prefixes.
- The version lives in the repo-root `VERSION` file; the owner decides when it moves.
- After a merge, record which PRs went in, what was reworked, what was rejected and why. The reason for a rejection
goes into the PR as a plain English comment.
+97
View File
@@ -0,0 +1,97 @@
# Performance: no lost frames on weak hardware
## Who we tune for
- The goal is many more fps on modest hardware, and Steam Deck later (Zen 2 4c/8t APU, shared LPDDR5, the exe runs
through Proton/Wine).
- The developer's machine (Ryzen 9 7900X + RX 9070) is fast and its numbers do not carry over. A laptop with an
i7-9750H and a GTX 1050 Ti is a sample of a weak machine, not the target to fit constants to.
- What weak hardware measured:
- GTX 1050 Ti (4 GiB, 214 MiB BAR, no ReBAR): the GPU read imported host pools over PCIe, 110-120 ms a frame
against 4.3 ms on the RX 9070. The VRAM mirror brought it to 10-11 ms; that machine is now CPU-bound at 25-45 fps,
replay 25-30 ms a frame.
- The same write-watch scan costs about 3x the CPU time there.
- NVIDIA and desktop AMD GPUs have no ASTC: textures decode in a compute shader. CPU decodes before that made
0.4-0.7 s load frames on the 1050 Ti.
- Wine emulates `GetWriteWatch` with `mprotect` and a fault per first write. Anything relying on cheap OS memory
tricks (write-watch, `VirtualProtect` freezing, fault handlers) is checked against Wine's implementation.
- The port uses about half of even a weak CPU, so the frame's critical path is the limit, not throughput. TotK keys
per-core data by `GetCurrentProcessorNumber` without locks, so the game's threads share three virtual cores on any
host. In a fight the main thread and five job workers keep 2.6 of the 3 busy: guest code speed sets the fps.
- Perf work builds and measures 1.4.3. A 1.2.1 build is for a change about to merge with a large gain.
## Gain ceiling first
Before writing or taking a perf change, compute its ceiling: the share of the code it removes times the share of the
frame that code holds on the critical path, on weak hardware. A ceiling of a few percent or less is not worth it.
- Use the critical path (`TOTK_CRIT` + `tools/crit_path.py`, heavy frames only), not one thread's profile. On the weak
laptop the submit thread is 55% replay, yet the critical path of heavy frames is 67% guest code, 15%
`nvnSyncWait`, 13% core waits and wake latency.
- Changes that can move the frame by 10% or more are usually design changes. Polishing a hot function from a flat
profile almost always gives zero.
## A neutral safe change stays
An optimization that measures neutral on the fast machine stays on by default when:
- upstream (`refs/`, other ports) reports a gain for it, especially on weak CPUs or GPUs;
- the static read leaves no hazard, and the probe or differential test passes;
- whole heavy-scene runs off/on x2 show no fps loss and no new slow frames;
- a switch turns it off.
"No gain here" is not a reason to drop a safe change with upstream evidence. A regression or a hazard nobody ruled
out is.
## How to measure
- **By game time, never by frame counts.** Run length, analysis windows, A/B blocks and input scripts go by the world
clock (`--seconds`, `TOTK_CRIT=w<lo>-<hi>`, `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, `TOTK_SHOTS=w<lo>-<hi>:<step>`). The
same frame count at another fps is another scene.
- **Check a scripted run reached the world.** Title menus on a slow boot eat button presses; confirm the run is in the
world before trusting its numbers.
- **Whole runs off and on, two each at least.** An in-run A/B on a moving scene lies: the A and B blocks land on the
same game seconds every run, so a content difference repeats. One in-run A/B showed "-11% replay"; whole runs gave
5.84/5.73 against 5.76/5.73 ms, nothing. In-run A/B only on a static scene (standing still).
- **Spread.** One script on near-identical builds gave 1% lows from 31.5 to 50 fps. Compare medians of three runs,
never one run against one run.
- **A quiet machine.** `tools/run_game.py --quiet` takes the machine-wide lock; no other `totk.exe`, build or test runs
meanwhile. One game at a time.
- **A warm shader cache, prepared properly.** Shader and pipeline caches are not copied between build dirs:
`totk.exe --prepare-shaders` or `--warm-shaders` in that dir. The AMD driver cache is keyed to the exe path and
limited in size: preparing another build dir can evict this one's libraries (40-220 ms per new effect in play).
- **A heavy scene.** A fight with a large enemy and a crowd, fps limit at the maximum (`TOTK_FPS_LIMIT=360`). Compare
one shot with the scene's reference before trusting numbers.
- **No base runs by habit.** Run the candidate alone and judge it against the targets: max fps, zero slow frames
(`perf: slow frame`), no black or flat frames, no new validation errors. Glitches are found by looking at shots and
frame dumps, not by diffing metrics. Run a base build only when a specific question needs a side-by-side.
| What | How |
|---|---|
| fps, worst frame, render-thread buckets, GPU busy, VRAM | `--env TOTK_TRACE=perf,sched`, then `python tools/perf_summary.py <log> <lo> <hi>` |
| the frame's critical path | `TOTK_CRIT=w<lo>-<hi>` + `TOTK_CRIT_OUT=<path>`, then `python tools/crit_path.py <file>` |
| sampling profiler | `TOTK_PROFILE=<path>` + `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `tools/prof_report.py <profile> <totk.map>` |
| A/B on a static scene | `TOTK_AB=<seconds>`, then `tools/ab_summary.py --skip <seconds>` |
| VRAM pressure on a large card | `TOTK_VRAM_LIMIT_MIB=<MiB>` caps the budget the port sees |
## What the hot path must not do (Windows host)
- **Files.** The game threads (main, job workers) and the render thread never open, read, write or flush a file, and
never wait for a thread that does. Opening a 20-byte file in a 139k-file folder: 0.1 ms idle, 11-40 ms in play while
the translator, the pipeline cache and the save wrote to the same volume. A save's `FlushFileBuffers` and
`MOVEFILE_WRITE_THROUGH`: 93-135 ms. Saving the pipeline cache under its lock on the present thread froze the replay
every 20 s. File I/O goes to a background thread; results come back in memory.
- **Waits.** Short waits spin on the counter; waits over 1 ms use the high-resolution timer. A waitable timer wakes
0.3-0.5 ms late; the SDK's 20 us sleeps took 460-515 us. A 1 ms timer thread does not help: it gave more slow frames.
- **GPU allocations.** VMA's `vkAllocateMemory` of a 256 MiB block under its lock: 8-23 ms. A new block on the render
thread is a hitch.
- **128-bit division.** `(unsigned __int128)t * num / den` compiles to the library call `__udivti3`; counter
conversions that way took a tenth of the game threads' time in a fight. Use the precomputed scale (`platform.h`
`PlatScale`).
- **Per-draw copies and counters.** A pipeline-state copy per draw, a global atomic counter, a scan of every page each
frame: on a weak CPU each is frames.
- **Memory kept per frame.** Anything allocated per texture initialization or per frame must be freed. One copy kept
per texture init grew host memory by ~44 MiB a game minute until a commit failure after hours (#670). Check private
memory over a few idle minutes for a PR that stores per-object state.
- **Scheduling.** Any timing or scheduling change is checked for slow frames. A preempt check at every host call made
one bug rarer but added 50-75 ms CPU hitches and was reverted.
+92
View File
@@ -0,0 +1,92 @@
# Correctness: checks that keep behaviour intact
## Lifter and generated C
- Generated C is never edited by hand: change the generator in `recomp/`.
- `recomp/layout.py` alone decides module bases.
- Every change to the lifter or the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) passes the differential tests
against Unicorn, all run with `.venv`:
| Test | Checks |
|---|---|
| `tests\diff\lift.py` | instructions sampled from the modules; `--coverage` lists the words the lifter rejects |
| `tests\diff\simd.py` | the whole FP/SIMD encoding space |
| `tests\diff\atomic.py` | exclusive load/store and ARMv8.1 atomics |
| `tests\diff\func.py` | whole functions as the generator emits them: loops, jump tables, calls |
| `tests\flags_check.py` | the function-local flag helpers against the packed ones |
| `tests\excl_mt.py` | the exclusive monitor under contention and ABA, on several threads |
| `tests\jumptables_check.py` | `recomp/jumptables.py` against Unicorn: 0 disagreements on 1.2.1 and 1.4.3 |
- A `--cpu v2` build (no AVX2) passes the same tests with `--cflags=-march=x86-64-v2`. Players without AVX exist.
- A compile error in generated C on a line the generator could not have written is a bit flip on disk or in memory.
Regenerate and compare hashes; leave `recomp/` alone.
## Runtime
- From the build dir: `totk.exe --selftest`, `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`
pass.
- Settings: `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`.
- Button prompts: `.venv-py314\Scripts\python tests\romfs_prompts_test.py`. Build dirs carry only `romfs-patches`; to
test PC or Xbox prompts in game, generate the overlay first:
`.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts pc --out build\<dir>\romfs-patches-pc`,
then restart the game.
- Installer: `tests\installer_gui_test.py` (event model, form, catalogs, cancel, display scale).
- Shaders: `tools\shader_census.py --version <ver> --translate`, `tools\xlate_smoke.py`. A translator patch
(`tools/xlate/patches/`) needs `build/xlate` rebuilt and changes shader hashes: anything keyed to a shader hash
breaks with it.
## GPU portability
- New Vulkan code queries the feature, extension, format or limit it uses and keeps a working path or a logged
fallback without it (provoking vertex and subgroup size pinning in `vk_pipeline.c` are examples).
- Never hardcode what one GPU reports: alignments, subgroup size (RDNA4 may run wave64, old AMD Vega is wave64 only),
depth formats, `maxPushDescriptors`.
- One queue: every submit and present takes `vk_queue_lock`. Presenting from its own thread on a second queue gave
device lost on GTX 10-series in another port.
- RDNA1/2 bugs do not reproduce on RDNA4 (another driver branch); NVIDIA bugs do not reproduce on AMD. A Ryzen 7000
iGPU is RDNA2 and runs the RDNA1/2 driver branch: a cheap testbed for those reports.
- Validation: a `--validation` run in the world shows no new messages.
## Maxwell / NVN behaviour
The port must do what the Switch GPU does, not what Vulkan does by default. The full list with sources:
[docs/maxwell.md](../../docs/maxwell.md), section 7. The traps a PR breaks most easily:
- Provoking vertex: Vulkan takes the first vertex, NVN the last.
- Unwritten vertex outputs read (0, 0, 0, 1).
- A colour target outside the fragment shader's output map keeps its texel (write mask 0). AMD writes garbage there.
- Feedback loops: the G-buffer pass reads its own targets. It needs the feedback layout and a by-region barrier.
- Depth bias argument order, depth mode, depth clamp: checked against deko3d.
- Report timestamps: the engine's GPU time comes from them, and dynamic resolution and game speed follow it.
## Timers and the real-time step
- With `realtime` set, the game steps by a measured ratio, not whole 30 Hz units. A countdown remainder in
(0, FLT_EPSILON] is never stepped again, and a consumer testing `> 0` sees it active forever. That was "weapons
cannot be changed after a Fuse until a reload" (#18).
- Symptoms of this class: stuck until a reload or teleport, locked after an action, works sometimes.
- A PR touching the frame step, the pacer or the fps limit is checked for this class.
## Threads and scheduling
- ARMv8 `stlr` then `ldar` is ordered (RCsc); C11 release/acquire lets x86 reorder that store and load. Changes to
the lifter's barriers or atomics pass `tests\diff\atomic.py` and `tests\excl_mt.py`.
- A scheduler change is judged from one `TOTK_TRACE=sched` run by how long a ready, better thread waits for a core it
may use, plus the slow-frame count. Not by statistics over many runs.
- A crash that looks like a race: rule out the save first. One reported crash was the save itself; it crashed the
same way on a real Switch.
## Files and caches
- A crash leaves zero-filled files. Caches are validated on load, not trusted because they exist.
- The pipeline cache is dropped when vendor, device or `pipelineCacheUUID` change.
- Unicode paths: W APIs with UTF-8 to UTF-16 conversion.
- A failure that happens once and does not come back: rerun before debugging it as a code bug. Broken caches and bit
flips happen.
## Game content boundary
- The repository is public. No game files, and nothing extracted or generated outside `build/` and `generated/`.
- Keys never appear in logs, commands, files, issues or comments.
- Every file the generator emits carries the `GENERATED_MARKER` defined in `tools/check_content.py`.
- `python tools/check_content.py --staged` before every commit. CI runs the same check.
+97
View File
@@ -0,0 +1,97 @@
# Knowledge base
Before investigating a symptom, or trusting a PR that "fixes" it, look for it here. Search by what the player sees
(black, magenta, flicker, slow motion, missing, stale, streaks) and by the object (sky, grass, shadow, fog, HUD).
## Invariants of recent changes
A PR that touches these areas must keep them.
- **Queue order.** When anything is submitted, every earlier part is already on the queue. Present, uploads, readbacks
from guest threads and the `vkDeviceWaitIdle` sites rely on it. The recorder thread now begins, ends and submits
the parts it records; the submitting thread still waits for that submission, so the order holds.
`TOTK_REC_END=0` restores the old split.
- **GPU-written memory is never frozen.** Storage-buffer stores, copies and fills land in imported guest memory
without a fault or a write-watch bit, so a region only the GPU writes looks idle. Such regions are never frozen
read-only, and a frozen region is thawed before a GPU write. LAS Recomp never changes the protection of imported
memory because of NVIDIA. A suspected cause of NVIDIA "write invalid" device faults (#788, #802), not proven.
- **VRAM pressure.** Under pressure, released textures whose storage the game has given to another texture go first:
30 s after release and once no finalized slot has bound them for 600 submits. Clean mirrored entries wait 5 s.
Both stop once use is back under the line. A PR touching texture lifetime is tested with `TOTK_VRAM_LIMIT_MIB`.
- **Texture pins are freed.** Each pin counts the recording copies naming it and is freed 1200 submits after its last
use unless the replay holds it as a render target. Before that, pins leaked ~44 MiB a game minute (#670).
## Fixed bug classes
If a PR touches this code, confirm the fix still holds.
| Symptom | Cause | Where the fix is |
|---|---|---|
| lime silhouettes in grass; rectangles in the sky | a target outside the output map must keep its texel; zero-filling it wiped the half-res depth | `vk_pipeline.c` `target_wmask` |
| stale grass maps in the far terrain for a second after a hit | write-back of a texture the CPU had rewritten | `vk_texture.c` `write_back` |
| cut grass stays standing; fire does not spread (#132) | CPU reads of GPU-rendered textures without an invalidate | `vk_texture.c` `vk_texture_write_back_small`, `nvn_ext.c` `nvn_pool_invalidate` |
| cloud and fog volumes empty for the session | a one-shot draw skipped instead of waiting for its pipeline | `vk_exec_draw.c` `prepare_draw` |
| tile rectangles | storage buffers bound by Ryujinx first-use order, not the c0 slot | `vk_exec_draw.c` `ssbo_slot` |
| black dialog background and loading map | 3D targets rejected, the grading LUT never written | `vk_exec.c` `resolve_target` |
| streaks on cliffs | primitive restart ignored | `vk_exec_draw.c` `restart_for` |
| streaky map screen | the newest texture picked at shared storage | `vk_texture.c` `vk_texture_by_id` |
| dark 16x16 blocks on the ground, sage spirit not drawn | G-buffer feedback loop | `vk_exec_draw.c`, `TOTK_FEEDBACK` |
| magenta cave walls, brown sky | CPU-filled textures without a flush | write-watched mirrors, `vk_memory.c`, `vk_texture.c` |
| black 3D scene | deferred texture binds | `nvn_api_cmd.c` `Bind*Deferred` |
| HUD prompt boxes, black stamina wheel | `nvnTextureCompare` ignored the channel mapping | `nvn_api_obj.c` |
| crash on world load | `GetCommandMemoryUsed` did not follow the driver's sizes; the game sizes model heaps from it | `nvn_api_cmd.c` |
| 20 fps | a yield back-off starting before 2 ms | `kernel.c` SVC 0x0B: no back-off |
| weapons locked after a Fuse (#18) | timer remainder in (0, FLT_EPSILON] with the real-time step | patch under the `realtime` setting |
| magenta/blue garbage or device lost on RDNA1/2 (#5) | writes into 3D BC block views | in `dev` |
| black window after a driver update | a pipeline cache from another driver | `vk_pipeline.c` drops the cache by UUID |
## Known open risks
A PR touching these needs stronger proof.
- Buffer textures are not replayed and bind a stand-in: light and decal lists may read 0.
- New GPU-only colour images start undefined; feedback effects may show garbage.
- Clears with a partial channel or stencil mask are skipped (`W_CLEAR_MASK`, `W_STENCIL_MASK`).
- 8-bit indices are widened on the CPU at record time; a copy in the same submission gives stale indices.
- `UpdateUniformBuffer` is a copy between full barriers and ends the render pass (fps outdoors).
- Report timestamps carry host time at fence completion: the engine's GPU time is wrong.
- The game's SVC-free spin loops burn a host core (843 in `main`).
- Images aliased onto one GPU-only pool allocation do not see each other's data on AMD.
## Past PR decisions
| PR | Decision | Why |
|---|---|---|
| #690 shrine snapshots | rejected | a pass restart per copy, a `VkPipeState` copy per draw, broken `bind_sig` skip; fps loss |
| #674 | rejected | a global STXR counter on the hot path |
| #660 | rejected | the preview loses anti-aliasing |
| #713, #576 | rejected | low memory block 10 GiB for everyone; should be 6 GiB plus the extra where needed |
| #573 | rejected | dynamic resolution off by default for everyone, the 1.4.3 site never run |
| #568, #569 | rejected | fake FSR4 and frame generation |
| #487 | rejected | wrong metrics: percentiles over window means, counters over the wrong set |
| #451, #380 | rejected | not painless to take |
| #566, #594, #534 DLSS | taken with changes | #594's shared paths gated on DLSS being on; nothing changes without DLSS |
| #489 | partly taken | the runtime part checked by a frame dump; the installer's DLL download was the owner's call, its pinned hash re-checked |
| #519, #582 amiibo | rewritten by us | took the data-format fixes; left out the features and the Linux event rework |
| #483 vs #585 | #585's mapping kept | #483's stick glyph mapping was rewritten unasked while #585 already had it right |
| #528 rain | taken after a check | code review fine, merged only after a run on a rain save |
| #767 | taken with our fix | the default was skipped with `prompts = xbox|pc` when those folders did not exist |
| #625 frame queue | taken, off by default | a new frame queue, one field run with it on |
| #743 UB overlay | waiting | +25% in a Depths fight on an RTX 4080; needs review, fight runs x2 and shots on AMD |
## Our mistakes, not to repeat
- **"-11%" was an A/B artifact** of a moving scene. Whole runs off/on showed zero.
- **Micro-optimizations from a flat profile** (1-3% of samples each) gave 26.2 against 26.4 ms on the weak laptop:
nothing. Nobody had computed a ceiling.
- **A scheduler patch merged on run statistics** added 50-75 ms hitches. Slow frames are a hard bar.
- **Ten game runs on lock theories** where one Ghidra read of the game's code narrowed it at once.
- **45 minutes of builds and benches** on an idea its own first thread census had refuted: the game pins 53 of 56
threads itself. The cheapest refuting check goes first; when it refutes, stop and write the idea down.
- **Tests of a bug that could not appear on the hardware at hand** (NVIDIA paging on an AMD machine): 245k tokens and
37 minutes for nothing. Ask first where the bug reproduces.
- **Turning the recorder off for a vendor** instead of finding its bug. Stopped: the feature gets fixed, not silenced.
- **Bisecting code when every build crashed the same way at once.** The cause was a bad page of a game file in the OS
file cache. When all builds fail identically, suspect inputs and caches first.
- **A duplicate issue closed with a bare "Same as #N"** lost the logs and links. Move everything useful to the issue
that stays open first: hardware, driver, version, when it happens, workarounds, links from comments.
+49
View File
@@ -0,0 +1,49 @@
# Per-PR checklist
## Before reading
- [ ] Fix or feature? Fixes first, ordered by how many players they reach.
- [ ] Where does the bug reproduce: OS, GPU vendor and generation, game version, settings? Can the hardware at hand
show it?
- [ ] Is the symptom in [04-knowledge.md](04-knowledge.md) or `docs/maxwell.md`?
- [ ] Another open PR on the same topic? Compare them.
- [ ] Feature: is there a design in `docs/milestones/`?
## Reading the diff
- [ ] The whole diff read, not the description: races, edge cases, lock order, leaks.
- [ ] Behaviour checked against `refs/` (Ryujinx, yuzu, LAS, SavageWind, nxnative, MacNativeGames), the Vulkan spec,
`docs/maxwell.md`.
- [ ] Hot-path cost: no file I/O, GPU allocations, waits, per-draw copies or global atomics on game or render threads.
- [ ] No memory kept per frame or per object without a free.
- [ ] Failure modes: device lost, hang, stale data, CPUs without AVX, NVIDIA/Intel/old AMD, Wine/Proton.
- [ ] New Vulkan code queries the feature and has a fallback; nothing hardcoded for one GPU.
- [ ] Invariants kept: queue order, GPU-written memory never frozen, VRAM pressure path, pins freed.
- [ ] Players without the feature see no change. The default for everyone stays for one setup's sake.
- [ ] New behaviour behind a switch. A new setting described in `totk.ini.example` and `docs/settings.md`.
- [ ] Player-visible text in the catalogs, in every menu language (`runtime/ui_text.c`, `installer/text/*.json`).
- [ ] Copied code: allowed licence, header kept, row in `THIRD_PARTY_NOTICES.md`. No GPL code.
- [ ] Generated C not edited by hand.
- [ ] Parts we cannot run (Linux, HDR, RTX-only) gated away from everyone else and noted as unrun.
## Proof
- [ ] Repro before the fix and the same repro after, side by side.
- [ ] Lifter or CPU headers: differential tests against Unicorn; `jumptables_check.py` 0 disagreements.
- [ ] Runtime: `--selftest` and the four probes pass.
- [ ] Settings: `--probe-cfg` and `tests\settings_doc_check.py`. Prompts: `romfs_prompts_test.py`, overlay generated
for a game check.
- [ ] Frame path: whole heavy-scene runs off/on, two each at least, by game time, quiet machine, warm cache prepared
in that build dir. No fps loss, zero new slow frames.
- [ ] Rendering: shots or a frame dump in a scene where the change acts; no black or flat frames; no new validation
errors.
- [ ] Perf PR: gain ceiling computed for weak hardware.
## Decision
- [ ] Taken, partly taken or rejected, with the reason written down and posted to the PR.
- [ ] What missed the project's scheme brought to it while merging.
- [ ] Bugs found outside the PR reported, not fixed on the spot.
- [ ] Merged into `dev` from `task/<tag>`. `main` only with the owner's yes.
- [ ] `python tools/check_content.py --staged` before the commit.
- [ ] Commit message: a plain English sentence, no `feat:`/`fix:`.
+32
View File
@@ -0,0 +1,32 @@
# PR review kit
Rules and background for a tester or reviewer who takes community PRs into `dev`. One goal: no PR breaks the
game's behaviour or costs frames on weak hardware.
## The short version
1. A PR description and "works for me" prove nothing. Read the diff yourself and check it against `refs/`, the
Vulkan spec and upstream issues.
2. Almost every change needs a test: a repro before and after, a probe, a differential test, or whole runs with the
switch off and on.
3. The developer's machine is fast; players run weak machines. "Neutral here" says nothing about them, in either
direction.
4. A player without the problem sees no change: the new path sits behind a switch and the old path stays as it was.
5. Nothing goes into `main` without the project owner's explicit yes. Accepted work lives in `dev` first.
## Files
| File | Covers |
|---|---|
| [01-rules.md](01-rules.md) | triage order, what a PR needs, reasons to reject, merging and commits |
| [02-performance.md](02-performance.md) | target hardware, gain ceiling, how to measure, what the hot path must not do |
| [03-correctness.md](03-correctness.md) | lifter, tests and probes, GPU portability, Maxwell behaviour, timers, threads, game content |
| [04-knowledge.md](04-knowledge.md) | invariants of recent changes, fixed bug classes, open risks, past PR decisions, our mistakes |
| [05-checklist.md](05-checklist.md) | the per-PR checklist |
The public docs this kit builds on: [docs/architecture.md](../../docs/architecture.md),
[docs/building.md](../../docs/building.md) ("Checks"), [docs/maxwell.md](../../docs/maxwell.md),
[docs/settings.md](../../docs/settings.md), [docs/tester-guide.md](../../docs/tester-guide.md), `AGENTS.md`.
Working with a coding agent: give it this folder and `AGENTS.md` before the first review.
State as of 2026-11-06. The code and `docs/` win over this text where they differ.
+7 -2
View File
@@ -64,6 +64,7 @@ Many rows below are developer tools that only act together with another setting
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `vram_limit_mib` | Pretends the device-local memory budget is at most this many MiB, for the paths that react to the budget (`tex_vram_pressure`, mirror refusal); the driver knows nothing of it. | Lets a large card drive those paths; a low value drops released textures and refuses mirrors that would have fit. | Caution |
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
@@ -73,6 +74,7 @@ Many rows below are developer tools that only act together with another setting
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
| `probe_movie_bench` | `1` makes the movie probe also time the resampler that fits a movie to a game resolution of another shape, and print the time per frame. | Only used by the movie decoder probe. | Safe |
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
| `test_pad` | `<lx>,<ly>,<rx>,<ry>` attaches a virtual gamepad whose sticks rest at these raw SDL axis values (-32768 to 32767). | The game sees a controller with that drift or deflection; a malformed value is logged and ignored. | Caution |
@@ -119,6 +121,7 @@ Many rows below are developer tools that only act together with another setting
| Setting | What it does | Effect of changing it | Safety |
| --- | --- | --- | --- |
| `block_views_3d` (default `0`) | `1` gives 3D block-compressed images block views too. | On some AMD cards a GPU write past the end of the image's memory loses the Vulkan device, and the game stops. | Risky |
| `dyn_delta` (default `1`) | Records only the pipeline dynamic states (cull mode, depth and stencil tests, vertex input, blend enables, equations and write masks, ...) whose values changed since the last draw in the command buffer, also across pipeline changes; `0` records the full set at every change of pipeline state, as before; `ab` alternates with the `ab` blocks. | Fewer commands per pipeline change on the render and recorder threads; a driver that loses dynamic state across pipeline binds would draw with stale state. `TOTK_TRACE=perf` prints the calls and commands per submission. | Caution |
| `feedback` | `0` samples a target that an earlier draw of the same pass wrote with no barrier; `restart` ends and restarts the pass behind a full barrier; otherwise the feedback-loop layout is used when the GPU supports it. | `0` brings back the old behaviour that the barrier fixed, so the picture can break in passes that read a target they draw into; `restart` costs extra passes. | Risky |
| `flush_span` (default `0`) | `1` copies one span from the lowest to the highest flushed address of a pool, as an older version did. | Copies far more data to the GPU in crowded towns: an estimated 30-50 ms of GPU time per large copy over PCIe 3.0 x8. | Caution |
| `freeze_idle` (default `60`) | Number of submissions a 2 MiB region stays unwritten before it is made read-only; `0` turns the freeze off. | A larger value keeps more regions writable and costs more write tracking; a write to a frozen region costs about 60 microseconds. | Caution |
@@ -131,9 +134,11 @@ Many rows below are developer tools that only act together with another setting
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
| `rec_end` (default `1`) | `0` lets the submitting thread end a recorded part again, as an older version did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which unused released textures are dropped, 0 to 100; `0` is off. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets a 4 GiB card spill into system memory. Also a slider in the menu. | Caution |
| `rec_end` (default `1`) | `0` lets the submitting thread begin, end and submit a recorded part again, as older versions did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which released textures are dropped, 0 to 100; `0` is off. First those the game has overwritten with a later texture and left alone for 30 s, then those that can be uploaded again. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets the card spill into system memory. Also a slider in the menu. | Caution |
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
| `ub_overlay` (default `1`) | `0` copies each uniform update inside a pass into its buffer at once, ending the pass with a full barrier; `ab` alternates with the `ab` blocks. | With `0` scenes with many updated blocks (the terrain, the sky islands) take longer on the GPU; `TOTK_TRACE=perf` prints how many updates stayed in their pass. | Caution |
| `ub_overlay_out` (default `1`) | `0` copies each uniform update between passes into its buffer at once, behind a full barrier of its own; `ab` alternates with the `ab` blocks. Needs `ub_overlay`. | With `0` a run of updates before a pass costs a full GPU barrier each; `TOTK_TRACE=perf` prints how many updates between passes were overlaid. | Caution |
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
+41 -16
View File
@@ -16,9 +16,10 @@ logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites only
in build/ recompiles only those. A change of the mods' code cheats then costs the lift (about a minute) and the chunks
it touches, not a full compile.
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in
its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; totk.exe runs it at start
when an enabled mod has code cheats the build lacks (runtime/cheats.c).
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status and exefs.data (tools/exefs_patches.py) and
rebuild.txt, the command that starts this setup in its rebuild mode on the install folder (installer/gui.py --rebuild),
one argument per line; totk.exe runs it at start when an enabled mod has code cheats the build lacks
(runtime/cheats.c).
"""
import hashlib
import http.client
@@ -855,6 +856,12 @@ def exefs_status(ctx):
return ctx.dest / 'build-cache' / 'exefs.status'
def exefs_data(ctx):
"""The data writes of the mods' ExeFS patch files (tools/exefs_patches.py --data), which runtime/mods.c stores into
main before the game runs."""
return ctx.dest / 'build-cache' / 'exefs.data'
def cheats_status(ctx):
"""The build's cheat status file (tools/cheats.py --status), which runtime/cheats.c reads: in build-cache, not in a
mod's own cheats folder, which the player (or TKMM's merge) may empty."""
@@ -869,7 +876,7 @@ class Lift(Step):
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
files = cheat_files(ctx) + exefs_files(ctx)
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
return fingerprint('lift-5', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
@@ -890,36 +897,54 @@ class Lift(Step):
if m:
rep.progress('lift', 'emit', int(m.group(1)), int(m.group(2)), 'chunks')
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets (tools/cheats.py,
# tools/exefs_patches.py); their status files tell the runtime what the build has. Both are the player's
# downloads, so files the tools refuse cost those cheats or patches, not the install: the game is built
# without them and the setup warns.
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets and data writes
# (tools/cheats.py, tools/exefs_patches.py); their status files tell the runtime what the build has. Both are
# the player's downloads, so files the tools refuse cost those cheats or patches, not the install: the game is
# built without them and the setup warns.
plain = layout.patches(game_version(ctx))
stages = [('cheats.py', 'patches-cheats.txt', cheats_status(ctx), cheat_files(ctx), 'warn.cheats_skipped'),
('exefs_patches.py', 'patches-exefs.txt', exefs_status(ctx), exefs_files(ctx), 'warn.exefs_skipped')]
# (tool, patch file it writes, its outputs for the runtime: the status first, then the data file, the files it
# reads, the warning when it fails)
stages = [('cheats.py', 'patches-cheats.txt', [cheats_status(ctx)], cheat_files(ctx), 'warn.cheats_skipped'),
('exefs_patches.py', 'patches-exefs.txt', [exefs_status(ctx), exefs_data(ctx)], exefs_files(ctx),
'warn.exefs_skipped')]
patches, skipped = plain, {}
for tool, out, status, inputs, warn in stages:
status.unlink(missing_ok=True) # no status: the runtime treats every such file as not built
def drop(outputs): # no status: the runtime treats every such file as not built
for f in outputs:
f.unlink(missing_ok=True)
for tool, out, outputs, inputs, warn in stages:
drop(outputs)
if not inputs:
continue
code, log = run_tool(ctx, 'lift', layout.tools / tool,
['--exefs', ctx.dest / 'exefs', '--mods', ctx.dest / 'mods', '--patches', patches,
'--out', ctx.temp / out, '--status', status])
'--out', ctx.temp / out, '--status', outputs[0],
*(['--data', outputs[1]] if len(outputs) > 1 else [])])
if code:
status.unlink(missing_ok=True)
drop(outputs)
skipped[warn] = log
else:
patches = ctx.temp / out
args = ['--exefs', ctx.dest / 'exefs', '--out', gen]
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', patches], parse)
if code and patches != plain:
for tool, out, status, inputs, warn in stages:
for tool, out, outputs, inputs, warn in stages:
if inputs and warn not in skipped:
status.unlink(missing_ok=True)
drop(outputs)
skipped[warn] = log
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', plain], parse)
if code:
raise InstallError('lift.failed', code=code, log=str(log))
# The mods' data writes must stay off the bytes the lifted code reads as constants (its jump tables), which only
# the lift knows: gen.py lists them in folded.txt, and a file that writes them becomes unsupported
if exefs_data(ctx).is_file():
code, log = run_tool(ctx, 'lift', layout.tools / 'exefs_patches.py',
['--exefs', ctx.dest / 'exefs', '--status', exefs_status(ctx), '--data',
exefs_data(ctx), '--folded', gen / 'main' / 'folded.txt'])
if code:
drop([exefs_status(ctx), exefs_data(ctx)])
skipped.setdefault('warn.exefs_skipped', log)
for warn, log in skipped.items():
rep.warning(warn, log=str(log))
(gen / STAMP).write_text(ctx.keys['lift'] + '\n')
+41 -2
View File
@@ -9,6 +9,9 @@ Output, under --out:
<mod>/<mod>_NNNN.c one `void f_<addr>(Cpu *c)` per guest function, about --chunk-bytes of C per file
<mod>/<mod>_table.c sorted <mod>_fn_addrs[] / <mod>_fn_ptrs[] / <mod>_fn_count for rt_dispatch
<mod>/manifest.json chunk list, function count, statistics and timings
<mod>/folded.txt the module's bytes the generated code took as constants: what the jump tables read at
lift time, as module offsets [start, end); tools/exefs_patches.py keeps a mod's data
writes off them
patches.h code patch sets for the runtime (recomp/patches.py; no sets without --patches)
Function discovery runs in this process, vectorised over the module's .text words. Function starts are the module
@@ -555,7 +558,8 @@ def _retry(fn, *args):
# ------------------------------------------------------------------------------------------------ workers
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter},
# 'reads' {name: [(start, end)]} module bytes read since the last chunk
def _worker_init(mods, starts, table, opts, jobs):
@@ -565,6 +569,7 @@ def _worker_init(mods, starts, table, opts, jobs):
_W['patches'] = table
_W['opts'] = opts # Emitter keyword arguments
_W['emitters'] = {}
_W['reads'] = {}
def _emitter(name):
@@ -572,12 +577,31 @@ def _emitter(name):
if e is None:
path, base = _W['mods'][name]
mod = Module(name, path, base, 'c')
# Module.read is how the jump tables (recomp/jumptables.py _eval) read .text and .rodata at lift time: the
# bytes it returns become part of the generated code, so <mod>/folded.txt lists them
read, reads = mod.read, _W['reads'].setdefault(name, [])
def recorded(a, n, signed=False):
reads.append((a - base, a - base + n))
return read(a, n, signed=signed)
mod.read = recorded
e = _W['emitters'][name] = Emitter(mod, set(_W['starts'][name].tolist()),
_W['patches'].for_module(name, base),
spins=_W['patches'].spins_for(name, base), **_W['opts'])
return e
def merge_ranges(ranges):
"""Sorted [start, end) ranges with overlapping and touching ones joined."""
out = []
for s, e in sorted(ranges):
if out and s <= out[-1][1]:
out[-1][1] = max(out[-1][1], e)
else:
out.append([s, e])
return out
def _emit_chunk(task):
"""Worker: write one chunk. Returns its statistics."""
name, number, path, fns = task
@@ -595,9 +619,12 @@ def _emit_chunk(task):
written = write_if_changed(Path(path), text)
except OSError as ex:
written, error = False, f'{path}: {ex}'
reads = _W['reads'].get(name, [])
folded = merge_ranges(reads)
reads.clear()
return {'module': name, 'number': number, 'file': Path(path).name, 'functions': len(fns), 'bytes': len(text),
'written': written, 'error': error, 'seconds': time.perf_counter() - t0, 'stats': dict(e.stats),
'unsupported': dict(e.unsupported)}
'unsupported': dict(e.unsupported), 'folded': folded}
# ------------------------------------------------------------------------------------------------ driver
@@ -613,6 +640,14 @@ def table_text(name, addrs):
return ''.join(lines)
def folded_text(name, ranges):
"""<mod>/folded.txt: the module offsets [start, end) of the bytes the jump tables read at lift time, in hex."""
lines = [f'# {GENERATED_MARKER}: {name} bytes the generated code took as constants (jump tables), module offsets '
f'[start, end), generated by recomp/gen.py from game code. Do not edit or commit.']
lines += [f'{s:x} {e:x}' for s, e in ranges]
return '\n'.join(lines) + '\n'
def remove_stale(moddir, name, nchunks, errors):
"""Delete chunks an earlier run wrote beyond this run's count, and temporary files an interrupted run left."""
pat = re.compile(re.escape(name) + r'_(\d{4})\.c')
@@ -714,6 +749,7 @@ def main():
# Largest chunks first, so the pool does not end on a long tail.
tasks.sort(key=lambda t: -t[0])
results = {name: [] for name in info}
folded = {name: [] for name in info}
done_at, errors = {}, []
t_emit = time.perf_counter()
ctx = mp.get_context('spawn')
@@ -724,9 +760,11 @@ def main():
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
try:
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
# Its folded bytes come with its result: a chunk emitted again reads them again in its new worker.
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
results[r['module']].append(r)
folded[r['module']] += r['folded']
if r['error']:
errors.append(r['error'])
done_at[r['module']] = time.perf_counter() - t_emit
@@ -753,6 +791,7 @@ def main():
unsupported.update(r['unsupported'])
t = time.perf_counter()
table_written = write_if_changed(moddir / f'{name}_table.c', table_text(name, d['emitted']))
write_if_changed(moddir / 'folded.txt', folded_text(name, merge_ranges(folded[name])))
removed = remove_stale(moddir, name, d['chunks'], errors)
manifest = {
'module': name,
+2
View File
@@ -111,6 +111,8 @@ site mouse_direct main 874cb4 bd042289 94c9acc7
site mouse_direct main 874d7c 6d40a3e9 94c9ac91
site mouse_direct main 18e444 f9400408 94e546db
site mouse_direct main 18f108 bd4047ec 94e543a6
# Keep native camera-distance easing when direct mouse orbit bypasses the stick integrators.
site mouse_direct main 18ea34 b9418a88 94e5454f # orbit state: ldr w8, [x20, #0x188]
site mouse_direct main 1d59460 b9004be8 94761acc
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
+111
View File
@@ -0,0 +1,111 @@
// Generated by tools/build_shaders.py from runtime/shaders/bl_deswizzle.comp; do not edit.
// source sha256 bd2065290e315e85d2fcdb609d812c15ba9a109d7506b3105850a5860677b0ef
#pragma once
#include <stdint.h>
static const uint32_t bl_deswizzle_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x000000d0, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001f, 0x00060010,
0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x0000000b, 0x0000000b,
0x0000001a, 0x00030047, 0x00000010, 0x00000002, 0x00050048, 0x00000010, 0x00000000, 0x00000023,
0x00000000, 0x00050048, 0x00000010, 0x00000001, 0x00000023, 0x00000004, 0x00050048, 0x00000010,
0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x00000010, 0x00000003, 0x00000023, 0x0000000c,
0x00050048, 0x00000010, 0x00000004, 0x00000023, 0x00000010, 0x00050048, 0x00000010, 0x00000005,
0x00000023, 0x00000014, 0x00050048, 0x00000010, 0x00000006, 0x00000023, 0x00000018, 0x00050048,
0x00000010, 0x00000007, 0x00000023, 0x0000001c, 0x00050048, 0x00000010, 0x00000008, 0x00000023,
0x00000020, 0x00050048, 0x00000010, 0x00000009, 0x00000023, 0x00000024, 0x00050048, 0x00000010,
0x0000000a, 0x00000023, 0x00000028, 0x00040047, 0x0000001f, 0x0000000b, 0x0000001b, 0x00040047,
0x000000ad, 0x00000006, 0x00000010, 0x00030047, 0x000000ae, 0x00000002, 0x00040048, 0x000000ae,
0x00000000, 0x00000019, 0x00050048, 0x000000ae, 0x00000000, 0x00000023, 0x00000000, 0x00030047,
0x000000b0, 0x00000019, 0x00040047, 0x000000b0, 0x00000021, 0x00000001, 0x00040047, 0x000000b0,
0x00000022, 0x00000000, 0x00040047, 0x000000c1, 0x00000006, 0x00000010, 0x00030047, 0x000000c2,
0x00000002, 0x00040048, 0x000000c2, 0x00000000, 0x00000018, 0x00050048, 0x000000c2, 0x00000000,
0x00000023, 0x00000000, 0x00030047, 0x000000c4, 0x00000018, 0x00040047, 0x000000c4, 0x00000021,
0x00000000, 0x00040047, 0x000000c4, 0x00000022, 0x00000000, 0x00040047, 0x000000cd, 0x0000000b,
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
0x00000020, 0x00000000, 0x00040017, 0x00000009, 0x00000006, 0x00000003, 0x00040020, 0x0000000a,
0x00000001, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000001, 0x0004002b, 0x00000006,
0x0000000c, 0x00000001, 0x00040020, 0x0000000d, 0x00000001, 0x00000006, 0x000d001e, 0x00000010,
0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006,
0x00000006, 0x00000006, 0x00000006, 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b,
0x00000011, 0x00000012, 0x00000009, 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b,
0x00000013, 0x00000014, 0x0000000a, 0x00040020, 0x00000015, 0x00000009, 0x00000006, 0x0004002b,
0x00000006, 0x00000019, 0x00000000, 0x0004002b, 0x00000006, 0x0000001d, 0x00000040, 0x0004003b,
0x0000000a, 0x0000001f, 0x00000001, 0x0004002b, 0x00000013, 0x00000024, 0x00000002, 0x00020014,
0x00000027, 0x0004002b, 0x00000013, 0x0000002d, 0x00000006, 0x0004002b, 0x00000013, 0x00000032,
0x00000007, 0x0004002b, 0x00000006, 0x00000037, 0x00000020, 0x0004002b, 0x00000006, 0x00000048,
0x00000005, 0x0004002b, 0x00000006, 0x0000004c, 0x0000001f, 0x0004002b, 0x00000013, 0x0000004f,
0x00000008, 0x0004002b, 0x00000013, 0x00000052, 0x00000009, 0x0004002b, 0x00000006, 0x0000006a,
0x00000004, 0x0004002b, 0x00000006, 0x0000006f, 0x00000002, 0x0004002b, 0x00000006, 0x00000078,
0x00000008, 0x0004002b, 0x00000006, 0x00000083, 0x00000003, 0x0004002b, 0x00000013, 0x00000093,
0x00000003, 0x0004002b, 0x00000013, 0x0000009b, 0x00000004, 0x0004002b, 0x00000013, 0x000000a4,
0x00000005, 0x00040017, 0x000000ac, 0x00000006, 0x00000004, 0x0003001d, 0x000000ad, 0x000000ac,
0x0003001e, 0x000000ae, 0x000000ad, 0x00040020, 0x000000af, 0x0000000c, 0x000000ae, 0x0004003b,
0x000000af, 0x000000b0, 0x0000000c, 0x0004002b, 0x00000013, 0x000000b1, 0x00000000, 0x0004002b,
0x00000013, 0x000000b2, 0x00000001, 0x0003001d, 0x000000c1, 0x000000ac, 0x0003001e, 0x000000c2,
0x000000c1, 0x00040020, 0x000000c3, 0x0000000c, 0x000000c2, 0x0004003b, 0x000000c3, 0x000000c4,
0x0000000c, 0x00040020, 0x000000c9, 0x0000000c, 0x000000ac, 0x0006002c, 0x00000009, 0x000000cd,
0x0000001d, 0x0000000c, 0x0000000c, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
0x000200f8, 0x00000005, 0x000300f7, 0x000000ce, 0x00000000, 0x000300fb, 0x00000019, 0x000000cf,
0x000200f8, 0x000000cf, 0x00050041, 0x0000000d, 0x0000000e, 0x0000000b, 0x0000000c, 0x0004003d,
0x00000006, 0x0000000f, 0x0000000e, 0x00050041, 0x00000015, 0x00000016, 0x00000012, 0x00000014,
0x0004003d, 0x00000006, 0x00000017, 0x00000016, 0x00050084, 0x00000006, 0x00000018, 0x0000000f,
0x00000017, 0x00050041, 0x0000000d, 0x0000001a, 0x0000000b, 0x00000019, 0x0004003d, 0x00000006,
0x0000001b, 0x0000001a, 0x00050080, 0x00000006, 0x0000001c, 0x00000018, 0x0000001b, 0x00050084,
0x00000006, 0x0000001e, 0x0000001c, 0x0000001d, 0x00050041, 0x0000000d, 0x00000020, 0x0000001f,
0x00000019, 0x0004003d, 0x00000006, 0x00000021, 0x00000020, 0x00050080, 0x00000006, 0x00000022,
0x0000001e, 0x00000021, 0x00050041, 0x00000015, 0x00000025, 0x00000012, 0x00000024, 0x0004003d,
0x00000006, 0x00000026, 0x00000025, 0x000500ae, 0x00000027, 0x00000028, 0x00000022, 0x00000026,
0x000300f7, 0x0000002a, 0x00000000, 0x000400fa, 0x00000028, 0x00000029, 0x0000002a, 0x000200f8,
0x00000029, 0x000200f9, 0x000000ce, 0x000200f8, 0x0000002a, 0x00050041, 0x00000015, 0x0000002e,
0x00000012, 0x0000002d, 0x0004003d, 0x00000006, 0x0000002f, 0x0000002e, 0x000500c4, 0x00000006,
0x00000030, 0x0000000c, 0x0000002f, 0x00050041, 0x00000015, 0x00000033, 0x00000012, 0x00000032,
0x0004003d, 0x00000006, 0x00000034, 0x00000033, 0x000500c4, 0x00000006, 0x00000035, 0x0000000c,
0x00000034, 0x00050080, 0x00000006, 0x0000003c, 0x0000002f, 0x00000034, 0x000500c4, 0x00000006,
0x0000003d, 0x00000037, 0x0000003c, 0x00050086, 0x00000006, 0x00000041, 0x00000022, 0x0000003d,
0x00050089, 0x00000006, 0x00000045, 0x00000022, 0x0000003d, 0x000500c2, 0x00000006, 0x00000049,
0x00000045, 0x00000048, 0x000500c7, 0x00000006, 0x0000004d, 0x00000045, 0x0000004c, 0x00050041,
0x00000015, 0x00000050, 0x00000012, 0x0000004f, 0x0004003d, 0x00000006, 0x00000051, 0x00000050,
0x00050041, 0x00000015, 0x00000053, 0x00000012, 0x00000052, 0x0004003d, 0x00000006, 0x00000054,
0x00000053, 0x00050084, 0x00000006, 0x00000055, 0x00000051, 0x00000054, 0x00050086, 0x00000006,
0x00000059, 0x00000041, 0x00000055, 0x00050089, 0x00000006, 0x0000005d, 0x00000041, 0x00000055,
0x00050086, 0x00000006, 0x00000062, 0x0000005d, 0x00000051, 0x00050089, 0x00000006, 0x00000067,
0x0000005d, 0x00000051, 0x00050084, 0x00000006, 0x0000006b, 0x00000067, 0x0000006a, 0x000500c2,
0x00000006, 0x0000006d, 0x0000004d, 0x0000006a, 0x000500c7, 0x00000006, 0x0000006e, 0x0000006d,
0x0000000c, 0x00050084, 0x00000006, 0x00000070, 0x0000006e, 0x0000006f, 0x00050080, 0x00000006,
0x00000071, 0x0000006b, 0x00000070, 0x000500c2, 0x00000006, 0x00000073, 0x0000004d, 0x0000000c,
0x000500c7, 0x00000006, 0x00000074, 0x00000073, 0x0000000c, 0x00050080, 0x00000006, 0x00000075,
0x00000071, 0x00000074, 0x00050084, 0x00000006, 0x00000079, 0x00000062, 0x00000078, 0x00050084,
0x00000006, 0x0000007b, 0x00000079, 0x00000030, 0x00050089, 0x00000006, 0x0000007e, 0x00000049,
0x00000030, 0x00050084, 0x00000006, 0x0000007f, 0x0000007e, 0x00000078, 0x00050080, 0x00000006,
0x00000080, 0x0000007b, 0x0000007f, 0x000500c2, 0x00000006, 0x00000082, 0x0000004d, 0x0000006f,
0x000500c7, 0x00000006, 0x00000084, 0x00000082, 0x00000083, 0x00050084, 0x00000006, 0x00000085,
0x00000084, 0x0000006f, 0x00050080, 0x00000006, 0x00000086, 0x00000080, 0x00000085, 0x000500c7,
0x00000006, 0x00000088, 0x00000045, 0x0000000c, 0x00050080, 0x00000006, 0x00000089, 0x00000086,
0x00000088, 0x00050084, 0x00000006, 0x0000008d, 0x00000059, 0x00000035, 0x00050086, 0x00000006,
0x00000090, 0x00000049, 0x00000030, 0x00050080, 0x00000006, 0x00000091, 0x0000008d, 0x00000090,
0x00050041, 0x00000015, 0x00000094, 0x00000012, 0x00000093, 0x0004003d, 0x00000006, 0x00000095,
0x00000094, 0x000500ae, 0x00000027, 0x00000096, 0x00000075, 0x00000095, 0x000400a8, 0x00000027,
0x00000097, 0x00000096, 0x000300f7, 0x00000099, 0x00000000, 0x000400fa, 0x00000097, 0x00000098,
0x00000099, 0x000200f8, 0x00000098, 0x00050041, 0x00000015, 0x0000009c, 0x00000012, 0x0000009b,
0x0004003d, 0x00000006, 0x0000009d, 0x0000009c, 0x000500ae, 0x00000027, 0x0000009e, 0x00000089,
0x0000009d, 0x000200f9, 0x00000099, 0x000200f8, 0x00000099, 0x000700f5, 0x00000027, 0x0000009f,
0x00000096, 0x0000002a, 0x0000009e, 0x00000098, 0x000400a8, 0x00000027, 0x000000a0, 0x0000009f,
0x000300f7, 0x000000a2, 0x00000000, 0x000400fa, 0x000000a0, 0x000000a1, 0x000000a2, 0x000200f8,
0x000000a1, 0x00050041, 0x00000015, 0x000000a5, 0x00000012, 0x000000a4, 0x0004003d, 0x00000006,
0x000000a6, 0x000000a5, 0x000500ae, 0x00000027, 0x000000a7, 0x00000091, 0x000000a6, 0x000200f9,
0x000000a2, 0x000200f8, 0x000000a2, 0x000700f5, 0x00000027, 0x000000a8, 0x0000009f, 0x00000099,
0x000000a7, 0x000000a1, 0x000300f7, 0x000000aa, 0x00000000, 0x000400fa, 0x000000a8, 0x000000a9,
0x000000aa, 0x000200f8, 0x000000a9, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000aa, 0x00050041,
0x00000015, 0x000000b3, 0x00000012, 0x000000b2, 0x0004003d, 0x00000006, 0x000000b4, 0x000000b3,
0x00050041, 0x00000015, 0x000000b6, 0x00000012, 0x0000009b, 0x0004003d, 0x00000006, 0x000000b7,
0x000000b6, 0x00050084, 0x00000006, 0x000000b8, 0x00000091, 0x000000b7, 0x00050080, 0x00000006,
0x000000ba, 0x000000b8, 0x00000089, 0x00050084, 0x00000006, 0x000000bd, 0x000000ba, 0x00000095,
0x00050080, 0x00000006, 0x000000be, 0x000000b4, 0x000000bd, 0x00050080, 0x00000006, 0x000000c0,
0x000000be, 0x00000075, 0x00050041, 0x00000015, 0x000000c5, 0x00000012, 0x000000b1, 0x0004003d,
0x00000006, 0x000000c6, 0x000000c5, 0x00050080, 0x00000006, 0x000000c8, 0x000000c6, 0x00000022,
0x00060041, 0x000000c9, 0x000000ca, 0x000000c4, 0x000000b1, 0x000000c8, 0x0004003d, 0x000000ac,
0x000000cb, 0x000000ca, 0x00060041, 0x000000c9, 0x000000cc, 0x000000b0, 0x000000b1, 0x000000c0,
0x0003003e, 0x000000cc, 0x000000cb, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000ce, 0x000100fd,
0x00010038,
};
+1
View File
@@ -307,6 +307,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
fflush(stderr);
fs_save_flush();
vk_pipeline_save(); // no-op without the Vulkan back end
playtime_save();
prof_write();
fflush(stderr);
plat_exit(0);
+7 -5
View File
@@ -40,8 +40,8 @@ static const char *const known[] = {
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"DESC_TEMPLATE", "DESWIZZLE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
"DUMP_LOG", "DYN_DELTA", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAME_QUEUE",
"FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
@@ -59,8 +59,8 @@ 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", "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",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UB_OVERLAY_OUT", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "VS_OUTS",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
"ZSTD_CHECK", "ZSTD_NATIVE",
};
@@ -82,6 +82,7 @@ static const struct { const char *key, *section; } sections[] = {
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
{"PCSS", "display"},
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
{"TEX_HEAP_EXTRA_MB", "mods"},
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"},
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
@@ -99,7 +100,7 @@ static const struct { const char *key, *section; } sections[] = {
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"VB_USED", "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"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
@@ -173,6 +174,7 @@ static const CfgMeta metas[] = {
// render fixes
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
{"VB_USED", "1", "0|1", RESTART},
{"DESC_TEMPLATE", "1", "0|1", RESTART},
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
{"SPLIT_US", "1000", "0..20000", RESTART},
+10
View File
@@ -230,6 +230,16 @@ int probe_cfg(void) {
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
memset(dn, 0, sizeof dn);
dn[KEY_T] = 1;
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: T is Y too (the left button is A in the menus)");
plat_setenv("TOTK_BIND_X", "key:t");
input_map_configure();
CHECK(input_map_buttons(dn) == 1ull << B_X, "bind: bind_X = key:t takes T from Y's defaults");
dn[KEY_T] = 0, dn[KEY_MOUSE_L] = 1;
CHECK(input_map_buttons(dn) == 1ull << B_Y, "bind: Y keeps the left mouse button");
plat_setenv("TOTK_BIND_X", NULL);
input_map_reset();
memset(dn, 0, sizeof dn);
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
+1
View File
@@ -72,6 +72,7 @@ void gfx_window_closed(void) {
rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting");
fs_save_flush();
vk_pipeline_save();
playtime_save();
prof_write(); // plat_exit runs no atexit handlers
if (restart) {
plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c)
+12
View File
@@ -260,6 +260,12 @@ void cam_pack_served(void); // romfs.c serves the
// ---- game resolution (resolution.c): TOTK_OUTPUT_RES, TOTK_RENDER_SCALE
uint64_t res_heap_extra_bytes(void); // added to the application pool (mem_init), 2 MiB steps
uint64_t res_heap_extra_kib(void); // added to the HeapDef texture heap and its parent; 0 = none
uint64_t res_tex_heap_extra_bytes(void); // TOTK_TEX_HEAP_EXTRA_MB: added to the application pool and
// the heap cap (mem_init), 2 MiB steps; 0 = none
int res_heapdef_tex_grown(const void *in, size_t n); // the HeapDef table (.byml.zs) grows TextureHeap over the
// shipped size (an HD texture pack's)
int res_heapdef_plain(const void *in, size_t n); // a readable HeapDef table whose TextureHeap is not
// grown (a content mod's)
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); // the HeapDef table (.byml.zs) with
// the grown heaps, malloc'd; 0 = cannot (logged)
void res_heapdef_served(void); // romfs.c serves the grown table
@@ -272,6 +278,8 @@ void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene
// until res_init switched the sets on)
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
int romfs_shaped_ready(unsigned w, unsigned h); // romfs-patches-<w>x<h> has a manifest (romfs.c; works
// before fs_init)
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
// ---- code patches (patches.c) and the frame step (frame_rate.c)
@@ -316,6 +324,10 @@ int probe_cfg(void); // cfg_probe.c
int probe_dpi(void); // dpi_probe_win.c (Windows only)
int probe_mods(void); // mods_probe.c
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
void playtime_init(void); // playtime.c: the play time thread, game runs only
void playtime_save(void); // playtime.c: writes <profile>/playtime.txt; at exit
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c: all runs and this one; 0 without a profile
int probe_playtime(void); // playtime.c: the file, the ticks and the save
int probe_discord(void); // discord_rpc.c
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
int probe_gfx_abi(void); // gfx_abi_probe.c
+24 -5
View File
@@ -35,7 +35,7 @@ typedef struct {
const char *name;
GuestFn fn;
uint64_t original; // observers: the guest definition rt_original calls
uint64_t calls;
uint64_t calls; // with TOTK_TRACE=host only (hostfn_report)
int blocking; // runs with the virtual core given up (hostfn_blocking)
} Slot;
static Slot slots[MAX_HOSTFN];
@@ -44,6 +44,21 @@ static RtHostEntry added[MAX_ADDED];
static int nadded;
static int sealed;
// The call count (hostfn_calls): one counter per calling thread, each on its own cache line, written only by that
// thread. A locked add on a per-function counter beside the read-mostly slot fields made the game threads share and
// bounce those lines on every host call (1.4.3 town: about 1% of the game threads' time in hostfn_entry).
#define MAX_COUNTERS 4096
typedef struct { _Alignas(64) uint64_t n; } Counter;
static Counter counters[MAX_COUNTERS];
static uint32_t ncounters;
static uint64_t shared_calls; // the threads past MAX_COUNTERS, counted with an atomic add
static __thread uint64_t *thread_calls;
__attribute__((noinline)) static uint64_t *counter_new(void) {
uint32_t i = __atomic_fetch_add(&ncounters, 1, __ATOMIC_RELAXED);
return thread_calls = i < MAX_COUNTERS ? &counters[i].n : &shared_calls;
}
static GuestFn table_find(const RtHostEntry *start, const RtHostEntry *stop, const char *name) {
__asm__("" : "+r"(start), "+r"(stop)); // separate objects: keep the compiler from reasoning about them
for (const RtHostEntry *p = start; p < stop; p++)
@@ -146,8 +161,11 @@ static void hostfn_entry(Cpu *c) {
uint64_t i = (c->pc - RT_HOSTFN_BASE) >> 4;
if (__builtin_expect((c->pc & 15) || i >= (uint64_t)nslots, 0)) bad_entry(c);
Slot *s = &slots[i];
uint64_t n = __atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED);
if (__builtin_expect(!n && (rt_trace & RT_TRACE_HOSTCALL), 0)) {
uint64_t *k = thread_calls;
if (__builtin_expect(!k, 0)) k = counter_new();
if (__builtin_expect(k == &shared_calls, 0)) __atomic_fetch_add(k, 1, __ATOMIC_RELAXED);
else __atomic_store_n(k, *k + 1, __ATOMIC_RELAXED);
if (__builtin_expect(rt_trace & RT_TRACE_HOSTCALL, 0) && !__atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED)) {
char lr[96];
rt_log("host call %s from %s", s->name, ldr_describe(c->x[30] - 4, lr, sizeof lr));
}
@@ -178,8 +196,9 @@ void hostfn_seal(void) {
}
uint64_t hostfn_calls(void) {
uint64_t n = 0;
for (int i = 0; i < nslots; i++) n += __atomic_load_n(&slots[i].calls, __ATOMIC_RELAXED);
uint32_t m = __atomic_load_n(&ncounters, __ATOMIC_RELAXED);
uint64_t n = __atomic_load_n(&shared_calls, __ATOMIC_RELAXED);
for (uint32_t i = 0; i < m && i < MAX_COUNTERS; i++) n += __atomic_load_n(&counters[i].n, __ATOMIC_RELAXED);
return n;
}
+7 -2
View File
@@ -623,17 +623,22 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
if (ikb_foreground()) {
uint8_t down[KEY_COUNT] = {0};
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
// (the click that refocused the window) is ignored until it is released.
// (the click that refocused the window) is ignored until it is released. While a game menu has the pointer
// (menu_cursor.c) and it is over the window, the left and right buttons are that menu's A and B, and the
// middle and side buttons keep their bindings: bound to L and R they turn the menu's tabs.
static int was_captured;
static uint8_t mouse_ignore[KEY_COUNT];
int cap = ikb_captured();
float pointer_x, pointer_y;
int menu_pointer = !cap && menu_cursor_pointer() && ikb_pointer(&pointer_x, &pointer_y);
for (int k = 1; k < KEY_COUNT; k++) {
down[k] = ikb_down(k);
kbm_used |= down[k];
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
if (cap && !was_captured) mouse_ignore[k] = down[k];
if (!down[k]) mouse_ignore[k] = 0;
if (!cap || mouse_ignore[k]) down[k] = 0;
int counts = cap || (menu_pointer && k != KEY_MOUSE_L && k != KEY_MOUSE_R);
if (!counts || mouse_ignore[k]) down[k] = 0;
}
was_captured = cap;
b |= input_map_buttons(down);
+19 -3
View File
@@ -16,7 +16,7 @@
#define DEFAULT_KEYS \
{KEY_W, B_SL_UP}, {KEY_A, B_SL_LEFT}, {KEY_S, B_SL_DOWN}, {KEY_D, B_SL_RIGHT}, /* move */ \
{KEY_UP, B_SR_UP}, {KEY_LEFT, B_SR_LEFT}, {KEY_DOWN, B_SR_DOWN}, {KEY_RIGHT, B_SR_RIGHT}, /* camera, second source */ \
{KEY_MOUSE_L, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
{KEY_MOUSE_L, B_Y}, {KEY_T, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
{KEY_SPACE, B_X}, {KEY_LSHIFT, B_B}, {KEY_E, B_A}, {KEY_Q, B_L}, {KEY_R, B_R}, \
{KEY_LCTRL, B_STICK_L}, {KEY_C, B_STICK_R}, \
{KEY_1, B_LEFT}, {KEY_2, B_UP}, {KEY_3, B_RIGHT}, {KEY_4, B_DOWN}, \
@@ -31,7 +31,7 @@
{PAD_ZR, B_ZR}
#define NINTENDO_PADS DEFAULT_PADS(B_B, B_A, B_Y, B_X)
#define XBOX_PADS DEFAULT_PADS(B_A, B_B, B_X, B_Y)
enum { DEFAULT_KEY_COUNT = 27, DEFAULT_PAD_COUNT = 16 };
enum { DEFAULT_KEY_COUNT = 28, DEFAULT_PAD_COUNT = 16 };
_Static_assert(sizeof (Binding[]){DEFAULT_KEYS} / sizeof (Binding) == DEFAULT_KEY_COUNT, "DEFAULT_KEY_COUNT");
static const InputMap defaults = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX_PADS}},
@@ -166,11 +166,27 @@ int input_map_bind(const char *button, const char *list) {
void input_map_configure(void) {
input_map_reset();
uint8_t lined[ACTION_COUNT] = {0};
for (int a = 0; a < ACTION_COUNT; a++) {
char var[32];
snprintf(var, sizeof var, "TOTK_BIND_%s", action_names[a]);
const char *v = cfg_get(var);
if (v && *v && input_map_bind(action_names[a], v)) rt_log("input: %s = %s", var + 5, v);
if (v && *v && input_map_bind(action_names[a], v)) lined[a] = 1, rt_log("input: %s = %s", var + 5, v);
}
// A key a line binds leaves the defaults of the actions without a line, as the remapping rows move a key: with
// bind_X = key:t, T presses X only, not also Y, whose defaults hold it.
uint8_t bound[KEY_COUNT] = {0}, list[INPUT_BINDING_MAX];
for (int a = 0; a < ACTION_COUNT; a++) {
if (!lined[a]) continue;
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX);
for (int i = 0; i < n; i++) bound[list[i]] = 1;
}
for (int a = 0; a < ACTION_COUNT; a++) {
if (lined[a]) continue;
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX), w = 0;
for (int i = 0; i < n; i++)
if (!bound[list[i]]) list[w++] = list[i];
if (w < n) keys_put(&input_map, a, list, w);
}
}
+1
View File
@@ -80,6 +80,7 @@ const char *input_pad_name(int pad);
// skipped; when none is valid the action keeps its bindings. 1 when applied.
int input_map_bind(const char *button, const char *list);
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
// A key a line binds leaves the defaults of the actions that have no line.
void input_map_configure(void);
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts", 0.1.9's name for the Nintendo
+22 -6
View File
@@ -7,6 +7,7 @@
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
// --probe-discord the Rich Presence IPC exchange; skips without a Discord client; no game files
// --probe-playtime the play time file, its ticks and saves (playtime.c); needs no game files
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
// --probe-gfx-abi NVN object layouts against nvn.h's reserved sizes; needs no game files
// --probe-nvn, --probe-nvn-tex, --probe-window, --probe-shader, --probe-vkmem, --probe-vkexec graphics probes
@@ -227,23 +228,34 @@ static void crash_handler(const PlatCrash *x) {
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
(unsigned long long)x->addr);
rt_log_raw("\n", 1);
if (x->cxx_type)
rt_logf(" C++ type %s%s%s\n", x->cxx_type, x->cxx_what ? ", what(): " : "", x->cxx_what ? x->cxx_what : "");
uint64_t f = x->module ? 0 : lifted_function(rip);
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
// abort and invalid parameter: where the host code called it; a fault in another module: the host calls that led
// into it
// abort and invalid parameter: where the host code called it; a fault in another module: the driver code and the
// host calls that led into it
uintptr_t exe = plat_exe_base();
for (int i = 0; i < x->nframes; i++) {
const char *m = x->frame_module[i];
if (m) {
rt_logf(" host frame %2d: %s+%#llx\n", i, *m ? m : "outside every module",
(unsigned long long)x->frame_offset[i]);
continue;
}
uint64_t g = lifted_function(x->frames[i]);
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - exe));
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
rt_log_raw("\n", 1);
}
if (x->frames_left_out) rt_logf(" (%d frames in other modules left out)\n", x->frames_left_out);
Cpu *c = kern_cpu();
if (c) {
for (int i = 0; i < 31; i++)
rt_logf(" x%-2d %016llx%s", i, (unsigned long long)c->x[i], i % 4 == 3 ? "\n" : "");
rt_logf(" sp %016llx\n pc %016llx lr %s\n", (unsigned long long)c->sp, (unsigned long long)c->pc,
ldr_describe(c->x[30], d, sizeof d));
// pc: the guest code running, or the host function a guest call entered (its name tells the host call apart)
rt_logf(" sp %016llx\n pc %016llx", (unsigned long long)c->sp, (unsigned long long)c->pc);
if (hostfn_name(c->pc)) rt_logf(" (%s)", ldr_describe(c->pc, d, sizeof d));
rt_logf(" lr %s\n", ldr_describe(c->x[30], d, sizeof d));
uint64_t fp = c->x[29];
for (int n = 0; n < 32 && fp && !(fp & 7) && readable(fp, 16); n++) {
uint64_t next = LD64(fp), lr = LD64(fp + 8);
@@ -396,7 +408,9 @@ static void check_memory(void) {
CHECK(q.state == 0 && q.base == g->alias.base && q.size == g->alias.size, "QueryMemory on the free alias region");
CHECK(mem_map_physical(a, 0x1000) == 0 && LD64(a + 0x10) == 0 && mem_unmap_physical(a, 0x1000) == 0,
"remapped physical memory is not zeroed");
CHECK(mem_map_physical(a, 0x100000000ull) == 0x10801, "MapPhysicalMemory beyond the pool is not LimitReached");
// past the pool whatever its size (TOTK_APP_POOL, TOTK_TEX_HEAP_EXTRA_MB): 4 GiB fits a grown one
CHECK(mem_map_physical(a, (g->pool + 0x100000) & ~0xfffffull) == 0x10801,
"MapPhysicalMemory beyond the pool is not LimitReached");
mem_query(RT_HOSTFN_BASE + 0x40, &q);
CHECK(q.state == 0x10 && q.base == RT_HOSTFN_BASE, "QueryMemory on the host-function window");
mem_query(g->alias.base + g->alias.size + 0x1000, &q);
@@ -463,7 +477,7 @@ static int probe_crash(int null) {
static int usage(const char *argv0) {
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-cfg | --probe-dpi | --probe-mods | --probe-playtime | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
"--prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
@@ -553,6 +567,7 @@ int main(int argc, char **argv) {
if (!mode) {
cfg.boot = cfg.seconds > 0; // --seconds starts the game like --boot, with its checkpoints
discord_init(); // the Rich Presence thread (discord_rpc.c); it reads totk.ini itself and idles while off
playtime_init(); // the play time thread (playtime.c): the Game tab's play time across runs
rt_boot();
return 0;
}
@@ -566,6 +581,7 @@ int main(int argc, char **argv) {
#endif
if (!strcmp(mode, "--probe-mods")) return probe_mods();
if (!strcmp(mode, "--probe-discord")) return probe_discord();
if (!strcmp(mode, "--probe-playtime")) return probe_playtime();
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
if (!strcmp(mode, "--probe-services")) return probe_services();
+21 -13
View File
@@ -20,11 +20,11 @@
#define PAGE 0x1000ull
#define GiB (1ull << 30)
#define LOW_SEARCH_FROM 0x100000000ull
#define LOW_SIZE (6 * GiB)
#define LOW_BASE (6 * GiB) // the stack and heap block without a texture heap extra
#define STACK_OFF 0x10000000ull
#define STACK_SIZE 0x10000000ull
#define HEAP_OFF (STACK_OFF + STACK_SIZE + 0x10000000ull)
#define HEAP_CAP (4 * GiB)
#define HEAP_CAP (4 * GiB) // without TOTK_TEX_HEAP_EXTRA_MB, which raises it by its own size (mem_init)
#define ALIAS_SIZE (64 * GiB)
#define ALIAS_ALIGN (4 * GiB)
#define ALIAS_PAGES (ALIAS_SIZE / PAGE)
@@ -48,6 +48,9 @@ enum { ATTR_PERMISSION_LOCKED = 16 };
static RtGuestLayout L;
static uint64_t mods_lo, mods_hi; // fixed module window
// The stack and heap block: LOW_BASE, plus the texture heap extra (tex_heap_extra_mb or a mod's HeapDef,
// res_tex_heap_extra_bytes) rounded up to a GiB, so only a player who grows the texture heap reserves more (mem_init).
static uint64_t low_size = LOW_BASE;
static PlatMutex lock = PLAT_MUTEX_INIT;
static uint64_t heap_size, alias_bytes;
static uint8_t *alias_pages; // 1 per mapped 4 KiB page of the alias region
@@ -78,7 +81,7 @@ static volatile int64_t frz_count[4]; // freezes; thaws by a write fault, by m
static int watched(uint64_t addr, uint64_t size) {
const RtGuestLayout *g = &L;
return (addr >= g->aslr.base && size <= LOW_SIZE && addr - g->aslr.base <= LOW_SIZE - size) ||
return (addr >= g->aslr.base && size <= low_size && addr - g->aslr.base <= low_size - size) ||
(addr >= g->alias.base && size <= ALIAS_SIZE && addr - g->alias.base <= ALIAS_SIZE - size);
}
@@ -221,10 +224,15 @@ void mem_init(void) {
// Stack and heap block, then the alias region above it, both searched below the module window.
uint64_t low = 0, alias = 0;
for (uint64_t at = LOW_SEARCH_FROM; at + LOW_SIZE <= mods_lo && !low; at += GiB)
if (reserve_at(at, LOW_SIZE, 1)) low = at;
if (!low) reexec_or_fail("a 6 GiB stack and heap block below 2^39");
for (uint64_t at = align_up(low + LOW_SIZE, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
low_size = LOW_BASE + align_up(res_tex_heap_extra_bytes(), GiB);
for (uint64_t at = LOW_SEARCH_FROM; at + low_size <= mods_lo && !low; at += GiB)
if (reserve_at(at, low_size, 1)) low = at;
if (!low) {
char what[64];
snprintf(what, sizeof what, "a %llu GiB stack and heap block below 2^39", (unsigned long long)(low_size / GiB));
reexec_or_fail(what);
}
for (uint64_t at = align_up(low + low_size, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
if (reserve_at(at, ALIAS_SIZE, 1)) alias = at;
if (!alias) reexec_or_fail("a 64 GiB alias region below 2^39");
@@ -238,11 +246,11 @@ void mem_init(void) {
L = (RtGuestLayout){
.aslr = {low, RT_ADDRESS_LIMIT - low},
.stack = {low + STACK_OFF, STACK_SIZE},
.heap = {low + HEAP_OFF, HEAP_CAP},
.heap = {low + HEAP_OFF, HEAP_CAP + res_tex_heap_extra_bytes()},
.alias = {alias, ALIAS_SIZE},
.kernel = {RT_KERNEL_BASE, RT_KERNEL_SIZE},
.hostfn = {RT_HOSTFN_BASE, RT_HOSTFN_SIZE},
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes(), // resolution.c
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes() + res_tex_heap_extra_bytes(),
};
if (!L.pool) rt_fatal("memory: TOTK_APP_POOL is 0");
}
@@ -276,7 +284,7 @@ static int inside(uint64_t a, uint64_t n, RtRange r) { return a >= r.base && n <
// ---- 0x01 SetHeapSize
uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
plat_mutex_lock(&lock);
if (size > HEAP_CAP || size % 0x200000 || size + image_bytes() > L.pool) {
if (size > L.heap.size || size % 0x200000 || size + image_bytes() > L.pool) {
plat_mutex_unlock(&lock);
return R_INVALID_STATE;
}
@@ -506,7 +514,7 @@ static void taken_gap(uint64_t a, uint64_t *base, uint64_t *size) {
uint64_t lo = 0, hi = RT_ADDRESS_LIMIT;
RtRange known[3 + MOD_COUNT + 2];
int n = 0;
known[n++] = (RtRange){L.aslr.base, LOW_SIZE};
known[n++] = (RtRange){L.aslr.base, low_size};
known[n++] = L.alias;
known[n++] = L.kernel;
known[n++] = L.hostfn;
@@ -549,14 +557,14 @@ uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
r.base = 0;
r.size = low;
r.state = ST_FREE;
} else if (a < low + LOW_SIZE) {
} else if (a < low + low_size) {
uint64_t heap = L.heap.base;
if (a >= heap && a < heap + heap_size) {
r = (RtMemoryInfo){.base = heap, .size = heap_size, .state = ST_NORMAL, .perm = PERM_RW};
} else if (a < heap) {
r = (RtMemoryInfo){.base = low, .size = heap - low, .state = ST_FREE};
} else {
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + LOW_SIZE - heap - heap_size, .state = ST_FREE};
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + low_size - heap - heap_size, .state = ST_FREE};
}
} else if (a - L.alias.base < L.alias.size) {
uint64_t p = (a - L.alias.base) / PAGE;
+248 -45
View File
@@ -15,7 +15,9 @@
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
// rebuild at start (cheats.c), and its mod stays off until then.
// rebuild at start (cheats.c), and its mod stays off until then. A file's writes into main's data (.rodata, .data,
// .bss) are lines of build-cache/exefs.data, stored into the loaded main before the game runs (mods_exefs_data), as
// emulators apply a patch file to the module before its dynamic linker runs.
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
@@ -170,22 +172,26 @@ static int same_build(const char *hex, size_t n) {
return 1;
}
// build-cache/exefs.status next to totk.exe, read once; "" when there is none.
static const char *exefs_status(void) {
static char *text;
if (text) return text;
// build-cache/<name> next to totk.exe, read once into *text; "" when there is none (mods_probe_cache sets the text).
static char *status_text, *data_text;
static const char *build_cache(const char *name, char **text) {
if (*text) return *text;
char path[2100];
uint64_t n = 0;
if (plat_exe_path(path, sizeof path)) {
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
if (back > cut) cut = back;
if (cut) snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/exefs.status");
text = (char *)plat_file_read(path, &n);
if (cut && snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/%s", name) <
(int)(sizeof path - (size_t)(cut + 1 - path)))
*text = (char *)plat_file_read(path, &n);
}
if (!text) text = calloc(1, 1);
return text;
if (!*text) *text = calloc(1, 1);
return *text;
}
static const char *exefs_status(void) { return build_cache("exefs.status", &status_text); }
static const char *exefs_data(void) { return build_cache("exefs.data", &data_text); }
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
char want[20];
@@ -194,8 +200,9 @@ static int status_line(const char *key, const char *content, char *kind, char *o
const char *end = strchr(line, '\n');
size_t len = end ? (size_t)(end - line) : strlen(line);
if (len > 18 && !memcmp(line, want, 18)) {
char copy[600];
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : sizeof copy - 1), line + 18);
char copy[600]; // the line after the two hashes, without the next line
size_t rest = len - 18;
snprintf(copy, sizeof copy, "%.*s", (int)(rest < sizeof copy ? rest : sizeof copy - 1), line + 18);
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
if (!t1 || !t2) return 0;
*t1 = *t2 = 0;
@@ -269,9 +276,14 @@ static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
size_t k = strlen(m->overrides);
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
} else if (!strcmp(kind_s, "data") && m->ndata < (int)(sizeof m->data / sizeof *m->data)) {
m->data[m->ndata++] = (uint32_t)strtoul(key, NULL, 16);
} else if (!strcmp(kind_s, "other")) {
m->other_version++;
}
unsigned long bytes; // a code or data file's data writes: exefs.data must hold them all (mods_exefs_data)
if ((!strcmp(kind_s, "code") || !strcmp(kind_s, "data")) && sscanf(note, "data %lu", &bytes) == 1)
m->data_bytes += (uint32_t)bytes;
return 1;
}
@@ -326,34 +338,62 @@ static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
int unsupported = exefs_check(m, path);
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
: unsupported ? MOD_EXEFS_UNSUPPORTED
: m->pending ? MOD_EXEFS_LATER
: m->nsets || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
: MOD_EMPTY;
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
: unsupported ? MOD_EXEFS_UNSUPPORTED
: m->pending ? MOD_EXEFS_LATER
: m->nsets || m->ndata || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
: MOD_EMPTY;
m->enabled = !listed(disabled, m->name);
nmods++;
return 1;
}
void mods_scan(void) {
static int done;
if (done) return;
done = 1;
// The mods folder into out: TOTK_MODS_DIR, else <dir of totk.exe>/mods. 0 when the exe's path is unknown.
static int mods_folder(char *out, size_t cap) {
const char *d = cfg_get("TOTK_MODS_DIR");
if (d && *d) {
snprintf(dir, sizeof dir, "%s", d);
} else {
char exe[2048];
if (!plat_exe_path(exe, sizeof exe)) return;
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
if (back > cut) cut = back;
if (!cut) return;
*cut = 0;
snprintf(dir, sizeof dir, "%s/mods", exe);
}
if (d && *d) return snprintf(out, cap, "%s", d) < (int)cap;
char exe[2048];
if (!plat_exe_path(exe, sizeof exe)) return 0;
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
if (back > cut) cut = back;
if (!cut) return 0;
*cut = 0;
return snprintf(out, cap, "%s/mods", exe) < (int)cap;
}
typedef struct {
const char *folder, *disabled;
void (*cb)(const char *name, const char *romfs, void *ctx);
void *ctx;
} Peek;
static int peek_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
const Peek *pk = ctx;
if (kind != PLAT_FS_DIR || listed(pk->disabled, name)) return 1;
char path[2100], romfs[2048];
if (snprintf(path, sizeof path, "%s/%s", pk->folder, name) >= (int)sizeof path) return 1;
find_root(path, "romfs", romfs, sizeof romfs);
if (!romfs[0]) find_root(path, "romfslite", romfs, sizeof romfs);
if (romfs[0]) pk->cb(name, romfs, pk->ctx);
return 1;
}
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx) {
char folder[2048];
if (!mods_folder(folder, sizeof folder) || !plat_dir_exists(folder)) return;
Peek pk = {folder, cfg_get("TOTK_MODS_DISABLED"), cb, ctx};
plat_fs_list(folder, peek_cb, &pk);
}
static int scanned;
void mods_scan(void) {
if (scanned) return;
scanned = 1;
if (!mods_folder(dir, sizeof dir)) return;
if (!plat_dir_exists(dir)) {
rt_log("mods: no folder %s", dir);
return;
@@ -376,18 +416,171 @@ void mods_scan(void) {
rt_log("mods: %d folders in %s", nmods, dir);
}
void mods_exefs_switch(void) {
mods_scan();
for (int i = 0; i < nmods; i++) {
const ModInfo *m = &mods[i];
if (!start_enabled[i] || m->problem != MOD_OK || !m->nsets) continue;
for (int k = 0; k < m->nsets; k++) {
char name[32];
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
int set = rt_patch_set(name);
if (set >= 0) rt_patch_switch(set, 1);
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
// ---- ExeFS data writes (build-cache/exefs.data; tools/exefs_patches.py data_line has the format)
// 1 when key names one of m's usable ExeFS patch files: one with a patch set, or one with data writes only.
static int owns(const ModInfo *m, uint32_t key) {
for (int k = 0; k < m->nsets; k++)
if (m->sets[k] == key) return 1;
for (int k = 0; k < m->ndata; k++)
if (m->data[k] == key) return 1;
return 0;
}
static int hex_digit(char c) {
return c >= '0' && c <= '9' ? c - '0'
: c >= 'a' && c <= 'f' ? c - 'a' + 10
: c >= 'A' && c <= 'F' ? c - 'A' + 10
: -1;
}
// One line: "<file hash>\t<content hash>\t<offset in main>\t<bytes>[\tbase]", all hex. 0 when malformed.
typedef struct { char key[9], content[9]; uint64_t off; uint8_t bytes[256]; int n, base; } DataRun;
static int parse_run(const char *line, size_t len, DataRun *r) {
char copy[1100], *f[6];
if (len >= sizeof copy) return 0;
memcpy(copy, line, len);
copy[len] = 0;
int nf = 0;
for (char *p = copy; p && nf < 6; nf++) {
f[nf] = p;
p = strchr(p, '\t');
if (p) *p++ = 0;
}
if (nf < 4 || nf > 5 || strlen(f[0]) != 8 || strlen(f[1]) != 8) return 0;
memcpy(r->key, f[0], 9);
memcpy(r->content, f[1], 9);
char *end;
r->off = strtoull(f[2], &end, 16);
size_t hex = strlen(f[3]);
if (!*f[2] || *end || !hex || hex % 2 || hex / 2 > sizeof r->bytes) return 0;
r->n = (int)(hex / 2);
for (int i = 0; i < r->n; i++) {
int hi = hex_digit(f[3][2 * i]), lo = hex_digit(f[3][2 * i + 1]);
if (hi < 0 || lo < 0) return 0;
r->bytes[i] = (uint8_t)(hi << 4 | lo);
}
r->base = nf == 5;
return nf == 4 || (!strcmp(f[4], "base") && r->n == 8);
}
// Stores n bytes into the loaded main at a: a read-only page (.rodata, loader.c seal) is writable for its part of the
// copy only, as cheats.c wr_main does. 0 when a page is neither readable nor writable.
static int store_main(uint64_t a, const uint8_t *from, int n) {
while (n > 0) {
uint64_t page = a & ~0xFFFull, end;
int k = (int)(page + 0x1000 - a);
if (k > n) k = n;
int prot = plat_mem_state(page, page + 0x1000, &end);
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return 0;
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return 0;
memcpy((void *)(uintptr_t)a, from, (size_t)k);
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
a += (uint64_t)k;
from += k;
n -= k;
}
return 1;
}
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
if (!store) store = store_main;
long total = 0, found = 0;
for (int pass = 0; pass < 2; pass++) { // pass 0 checks every line, pass 1 stores
// exefs.data must hold every byte the status lines count: one missing or cut short would leave the code
// patches on without their data
if (pass && found != (long)m->data_bytes) {
rt_log("mods: %s: build-cache/exefs.data holds %ld of the %u bytes its ExeFS patch files write", m->name,
found, m->data_bytes);
return -1;
}
for (const char *line = exefs_data(), *next; *line; line = next) {
const char *nl = strchr(line, '\n');
size_t len = nl ? (size_t)(nl - line) : strlen(line);
next = nl ? nl + 1 : line + len;
if (len && line[len - 1] == '\r') len--;
char key[9];
snprintf(key, sizeof key, "%.*s", (int)(len < 8 ? len : 8), line);
if (!len || *line == '#' || !owns(m, (uint32_t)strtoul(key, NULL, 16))) continue;
DataRun r;
char kind[16], over[256], note[256];
// a line whose file the status does not list as built (another content) or that leaves main's data: the
// two build-cache files and this build disagree
if (!parse_run(line, len, &r) || !status_line(r.key, r.content, kind, over, note) ||
(strcmp(kind, "code") && strcmp(kind, "data")) || r.off < code_end || r.off > size ||
(uint64_t)r.n > size - r.off) {
rt_log("mods: %s: a line of build-cache/exefs.data does not fit this build: %.*s", m->name, (int)len,
line);
return -1;
}
if (!pass) {
found += r.n;
continue;
}
if (r.base) { // a DT_RELR word holds its addend; the dynamic linker adds the module's base
uint64_t w;
memcpy(&w, r.bytes, 8);
w += base;
memcpy(r.bytes, &w, 8);
}
if (!store(base + r.off, r.bytes, r.n)) {
rt_log("mods: %s: cannot write main+%#llx", m->name, (unsigned long long)r.off);
return -1;
}
total += r.n;
}
}
return total;
}
static char *dup_text(const char *s) {
if (!s) return NULL;
size_t n = strlen(s) + 1;
char *d = malloc(n);
if (d) memcpy(d, s, n);
return d;
}
void mods_probe_cache(const char *status, const char *data) {
free(status_text);
free(data_text);
status_text = dup_text(status);
data_text = dup_text(data);
}
void mods_rescan(void) {
free(mods);
mods = NULL;
nmods = 0;
scanned = 0;
}
// Switches the patch set mod_<key> on; 0 when this build lacks it (logged).
static int set_on(const ModInfo *m, uint32_t key) {
char name[32];
snprintf(name, sizeof name, "mod_%08x", key);
int set = rt_patch_set(name);
if (set >= 0) rt_patch_switch(set, 1);
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
return set >= 0;
}
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
// bottom first: the top mod's data writes land last and win, as its RomFS files do. Patch sets are only switches
// (rt_patch_switch), so their order does not matter.
for (int i = nmods - 1; i >= 0; i--) {
const ModInfo *m = &mods[i];
if (!start_enabled[i] || m->problem != MOD_OK || !(m->nsets || m->ndata)) continue;
// the data first: a mod whose data writes cannot all be stored keeps its code patches off too, as half of a
// patch file is worse than none
long stored = size ? mods_exefs_data(m, base, size, code_end, store) : 0;
if (stored < 0) {
rt_log("mods: %s: its ExeFS patch files stay off: their data writes could not all be stored", m->name);
continue;
}
for (int k = 0; k < m->nsets; k++) set_on(m, m->sets[k]);
char over[256];
snprintf(over, sizeof over, "%s", m->overrides);
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
@@ -395,10 +588,20 @@ void mods_exefs_switch(void) {
if (set >= 0) rt_patch_switch(set, 0);
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
}
rt_log("mods: %s: %d ExeFS patch files on", m->name, m->nsets);
rt_log("mods: %s: %d ExeFS patch files on, %ld bytes of main's data stored", m->name, m->nsets + m->ndata,
stored);
}
}
void mods_exefs_switch(void) {
mods_scan();
const RtLoaded *main_mod = NULL;
for (int i = 0; ldr_module(i); i++)
if (!strcmp(ldr_module(i)->name, "main")) main_mod = ldr_module(i);
if (main_mod) mods_exefs_apply(main_mod->base, main_mod->size, main_mod->text_end, NULL);
else mods_exefs_apply(0, 0, 0, NULL);
}
int mods_exefs_pending(uint32_t *key) {
int n = 0;
for (int i = 0; i < nmods; i++) {
+27 -2
View File
@@ -2,7 +2,8 @@
// them: <mod>/romfs, <mod>/exefs and <mod>/cheats, the Atmosphere layout (atmosphere/contents/<title id>/romfs,
// atmosphere/exefs_patches/<name>/*.ips) or TKMM's merged output (romfs or romfslite, exefs, cheats). ExeFS patch
// files (.ips, .pchtxt) for the game's main are patch sets the setup builds into the game (tools/exefs_patches.py),
// switched with their mod (mods_exefs_switch). The list is scanned once at start (romfs.c overlay_init);
// switched with their mod (mods_exefs_switch), and data writes stored into main before the game runs (mods_exefs_data).
// The list is scanned once at start (romfs.c overlay_init);
// its order and switches live in totk.ini [mods] (TOTK_MODS_ORDER: names top first, separated by '|';
// TOTK_MODS_DISABLED: names switched off). The top mod wins a file two mods replace.
#pragma once
@@ -36,10 +37,17 @@ typedef struct {
uint32_t pending, pending_key; // ExeFS patch files the build lacks; FNV-1a of their names and contents
int nsets;
uint32_t sets[16]; // the built patch sets mod_<hash> of its ExeFS patch files
int ndata;
uint32_t data[16]; // the hashes of its ExeFS patch files with data writes only (no set)
uint32_t data_bytes; // the bytes its usable ExeFS patch files write into main's data, as their
// status lines count them ("data <bytes>")
char overrides[256]; // the port's own patch sets its patches replace, comma separated
} ModInfo;
void mods_scan(void); // once; later calls return at once
// Every switched-on mod folder's RomFS root, without the rest of the scan (no ExeFS checks, no order): for mem_init,
// which runs before the paths mods_scan needs are set (resolution.c mod_tex_heap_kib). cb gets the name and root.
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx);
const char *mods_dir(void); // the folder scanned (default: <dir of totk.exe>/mods)
int mods_count(void);
ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
@@ -48,8 +56,25 @@ ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
void mods_set_enabled(int i, int on);
void mods_move(int i, int delta); // -1 up, +1 down; nothing at the ends
int mods_changed(void); // 1 when the order or a switch differs from the start
// After rt_patches_init: the patch sets of the enabled mods' ExeFS patches on, the port's own sets they replace off.
// After ldr_load and rt_patches_init, before any game code: the patch sets of the enabled mods' ExeFS patches on, the
// port's own sets they replace off, and their data writes stored into main (mods_exefs_data).
void mods_exefs_switch(void);
// What mods_exefs_switch does with main's image at base (size bytes, code below code_end; size 0: no data stored), for
// every enabled mod from the bottom one up, so that the top mod's data writes win where two mods write the same bytes.
// store as for mods_exefs_data.
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
// Stores the data writes of mod m's ExeFS patch files (build-cache/exefs.data) into main's image at base: size bytes,
// code below code_end. A DT_RELR word gets base added, as the dynamic linker adds it. Every line is checked before the
// first store: one that falls outside [code_end, size) or does not belong to a usable file of m keeps them all out.
// store writes n bytes at a guest address, 1 on success; NULL: into the loaded main, whose .rodata is read-only.
// Returns the bytes stored, -1 when a line kept them out or a store failed. --probe-mods passes a buffer and memcpy.
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
// --probe-mods: the texts of build-cache/exefs.status and exefs.data instead of the files (NULL: read the file), and a
// fresh scan at the next mods_scan.
void mods_probe_cache(const char *status, const char *data);
void mods_rescan(void);
// The ExeFS patch files of the enabled mods that the build lacks, their FNV-1a mixed into *key (cheats.c starts the
// rebuild for them).
int mods_exefs_pending(uint32_t *key);
+88 -1
View File
@@ -39,6 +39,93 @@ static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
return 1;
}
static uint32_t fnv(const char *p, size_t n) {
uint32_t h = 0x811C9DC5u;
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)p[i]) * 0x01000193u;
return h;
}
static ModInfo *by_name(const char *name) {
for (int i = 0; mods_at(i); i++)
if (!strcmp(mods_at(i)->name, name)) return mods_at(i);
return NULL;
}
// mods_exefs_data's store into a buffer standing in for main
static int store_buffer(uint64_t a, const uint8_t *p, int n) {
memcpy((void *)(uintptr_t)a, p, (size_t)n);
return 1;
}
// A second scan of a folder of its own: data writes of ExeFS patch files (build-cache/exefs.data, given as text).
static void probe_data(const char *root) {
remove_tree(root);
plat_file_mkdir_p(root);
const char *bid = *mods_main_build_id() ? mods_main_build_id() : "0";
char n_ips[160], p_ips[160], q_ips[160], x_ips[160], y_ips[160];
snprintf(n_ips, sizeof n_ips, "N/exefs/%s.ips", bid);
snprintf(p_ips, sizeof p_ips, "P/exefs/%s.ips", bid);
snprintf(q_ips, sizeof q_ips, "Q/exefs/%s.ips", bid);
snprintf(x_ips, sizeof x_ips, "X/exefs/%s.ips", bid);
snprintf(y_ips, sizeof y_ips, "Y/exefs/%s.ips", bid);
const char *files[] = {n_ips, p_ips, q_ips, x_ips, y_ips};
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
uint32_t kn = fnv(n_ips, strlen(n_ips)), kp = fnv(p_ips, strlen(p_ips)), kq = fnv(q_ips, strlen(q_ips));
uint32_t kx = fnv(x_ips, strlen(x_ips)), ky = fnv(y_ips, strlen(y_ips));
uint32_t c = fnv("probe", 5);
char status[512], data[768];
snprintf(status, sizeof status,
"%08X\t%08X\tdata\t-\tdata 11\n%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 4\n"
"%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 1\n",
kn, c, kp, c, kq, c, kx, c, ky, c);
snprintf(data, sizeof data,
"# file hash, content hash, offset in main, bytes[, base]\n"
"%08X\t%08X\t10\t414243\n" // N: three bytes of data
"%08X\t%08X\t18\t0800000000000000\tbase\r\n" // N: a DT_RELR word, 8 plus main's base (a CRLF line)
"DEADBEEF\t%08X\t20\t99\n" // another file's: not N's
"%08X\t%08X\t4\t77\n" // P: below main's data
"%08X\t%08X\t30\t66\n" // Q: 1 of the 4 bytes its status counts
"%08X\t%08X\t28\tBB\n" // X: the byte Y writes too
"%08X\t%08X\t28\tAA\n", // Y, above X in the order: its byte stays
kn, c, kn, c, c, kp, c, kq, c, kx, c, ky, c);
mods_probe_cache(status, data);
cfg_set("TOTK_MODS_DIR", root);
cfg_set("TOTK_MODS_ORDER", "Y"); // Y on top, the rest by name: Y N P Q X
cfg_set("TOTK_MODS_DISABLED", "");
mods_rescan();
mods_scan();
ModInfo *n = by_name("N"), *p = by_name("P"), *q = by_name("Q");
CHECK(n && p && q && by_name("X") && by_name("Y"), "second scan: five mods");
if (n && p && q) {
CHECK(n->kind == MOD_EXEFS && n->problem == MOD_OK && n->ndata == 1 && !n->nsets,
"an ExeFS patch file with data writes only makes a usable mod");
uint8_t img[0x40];
memset(img, 0, sizeof img);
uint64_t base = (uint64_t)(uintptr_t)img, word;
long stored = mods_exefs_data(n, base, sizeof img, 8, store_buffer);
memcpy(&word, img + 0x18, 8);
CHECK(stored == 11 && !memcmp(img + 0x10, "ABC", 3) && word == base + 8 && !img[0x20],
"data writes: bytes as they are, a DT_RELR word rebased, other files' lines left");
memset(img, 0x5A, sizeof img);
int untouched = 1;
CHECK(mods_exefs_data(p, base, sizeof img, 8, store_buffer) == -1, "a line below main's data keeps all out");
CHECK(n->data_bytes == 11 && q->data_bytes == 4, "the status lines count the data bytes");
CHECK(mods_exefs_data(q, base, sizeof img, 8, store_buffer) == -1,
"a data file holding fewer bytes than the status counts keeps all out");
for (size_t i = 0; i < sizeof img; i++) untouched &= img[i] == 0x5A;
CHECK(untouched, "nothing stored when a line does not fit or bytes are missing");
// every enabled mod as at start: X and Y write the same byte, and Y, the higher one, wins as its RomFS files
// would; P and Q stay out as above
memset(img, 0, sizeof img);
CHECK(EQ(mods_at(0)->name, "Y") && EQ(mods_at(4)->name, "X"), "mods_order puts Y on top and X at the bottom");
mods_exefs_apply(base, sizeof img, 8, store_buffer);
CHECK(img[0x28] == 0xAA, "two mods write the same byte: the top mod's byte stays");
CHECK(!memcmp(img + 0x10, "ABC", 3) && !img[0x30], "the other mods' data as each one stores it alone");
}
mods_probe_cache(NULL, NULL);
remove_tree(root);
}
// s ends in tail, either path separator matching either.
static int ends_with(const char *s, const char *tail) {
size_t n = strlen(s), t = strlen(tail);
@@ -126,7 +213,7 @@ int probe_mods(void) {
CHECK(EQ(cfg_get("TOTK_MODS_DISABLED"), "A"), "switching A off writes it");
CHECK(mods_changed(), "changes seen");
}
remove_tree(root);
probe_data(root);
printf("mods: %s\n", fails ? "FAIL" : "ok");
int table = restbl_probe();
return fails || table ? 1 : 0;
+49 -16
View File
@@ -119,6 +119,7 @@ void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMo
#define HOST_MOUSE_ORBIT_BEGIN (RT_PATCH_PAGE + 0xfb0)
#define HOST_MOUSE_ORBIT_ANGLES (RT_PATCH_PAGE + 0xfa0)
#define HOST_MOUSE_HAND (RT_PATCH_PAGE + 0xf90)
#define HOST_MOUSE_ORBIT_STATE (RT_PATCH_PAGE + 0xf70)
static _Thread_local uint64_t mouse_input_object;
// Where a game version keeps what the hooks read and write (recomp/patches-v143.txt, patches-v121.txt). The camera
@@ -162,8 +163,8 @@ static void sample_mark_taken(uint64_t generation) {
static _Thread_local struct { uint64_t object; MouseSample mouse; } orbit;
typedef struct {
uint64_t object, generation, input, seen;
float pitch, original_target;
int hold_pitch, target_saved, resume;
float pitch;
int hold_pitch, resume;
} OrbitTrack;
static OrbitTrack orbit_seen[16];
@@ -268,19 +269,17 @@ static uint32_t player_state_logged(void) {
return f;
}
// main+0x191d78 builds the pitch target from +0x15c and +0x11c; the orbit update filters +0x108 toward it and adds
// (1 - +0x88) * +0x10c at output. Updating only +0x108 makes the camera spring back to the unchanged target.
// The orbit output adds (1 - +0x88) * +0x10c to its filtered pitch at +0x108.
// Hold the final angle in orbit_angles. Do not overwrite the configured target
// at +0x15c: main+0x191534 also uses it as the reference for camera distance.
// Replacing that reference with the held pitch changes the native return from
// aiming and collision close-ups, even though no distance field is written.
static float orbit_pitch_offset(uint64_t object) {
return (1 - guest_float(object + 0x88)) * guest_float(object + 0x10c);
}
static void orbit_pitch_target(uint64_t object, float pitch) {
guest_float_store(object + 0x15c, pitch - orbit_pitch_offset(object) - guest_float(object + 0x11c));
}
static void orbit_pitch_release(OrbitTrack *track) {
if (track->target_saved) guest_float_store(track->object + 0x15c, track->original_target);
track->hold_pitch = track->target_saved = track->resume = 0;
track->hold_pitch = track->resume = 0;
}
// The pitch the camera has now (the filtered pitch plus its vertical offset), as orbit_angles leaves it
@@ -288,8 +287,8 @@ static float orbit_pitch_now(uint64_t object) {
return wrap_degrees(guest_float(object + 0x108) + orbit_pitch_offset(object));
}
// The camera stays where it is now: the mouse holds the pitch the camera has (orbit_begin writes its target), so
// neither the game's default target nor the mouse's target from before an aim or a lock-on pulls it back.
// Resume from the pitch the camera reached during the handoff. orbit_angles
// holds this final angle without replacing the native configuration reference.
static void orbit_pitch_settle(OrbitTrack *track) {
track->pitch = orbit_pitch_now(track->object);
track->hold_pitch = 1, track->resume = 0;
@@ -471,7 +470,7 @@ static void orbit_begin(Cpu *c) {
memcpy(&override, (void *)(uintptr_t)(input + 0x15c), 1);
plat_mutex_lock(&sample_lock);
OrbitTrack *track = orbit_track(object);
if (track->input != input) track->hold_pitch = track->target_saved = track->resume = 0;
if (track->input != input) track->hold_pitch = track->resume = 0;
uint64_t now = plat_now_ns();
// A gamepad's vertical input gives the pitch back to the game's own target. The game's lock-on or override
// (aiming) drives the camera itself: the mouse lets go without restoring the target it had before, and
@@ -484,7 +483,6 @@ static void orbit_begin(Cpu *c) {
track->seen = now;
take = moved || track->hold_pitch;
if (take) track->input = input;
if (track->hold_pitch) orbit_pitch_target(object, track->pitch);
plat_mutex_unlock(&sample_lock);
if (moved) { // the mouse's part of this step's stick goes to the final angles instead
guest_float_store(input + 0x420, m.pad[0]);
@@ -500,6 +498,42 @@ static void orbit_begin(Cpu *c) {
c->x[8] = guest_pointer(object + 8); // displaced ldr x8, [x0, #8]
}
// The native manual-orbit state (4) integrates both pitch and distance from
// the stick. Direct mouse orbit bypasses those integrators, so entering that
// state during an aim return can latch the close-up's radius indefinitely.
// Select the native automatic-distance state (3) at the existing state decision
// point, before its transition initializer runs (main+0x18ea34, 1.4.3).
// This retains the game's own radius curve, transition easing and collision.
static void orbit_state(Cpu *c) {
uint64_t object = c->x[20];
uint32_t override;
memcpy(&override, (void *)(uintptr_t)(object + 0x188), sizeof override);
if (rt_game_version == 143 && direct_mode() && (uint32_t)c->x[10] == 4 && !override) {
MouseSample m = sample_read();
uint64_t input = guest_pointer(object + 0x20);
if (m.object == input && m.camera_enabled && plat_now_ns() - m.when < 150000000ull &&
m.pad[0] == 0 && m.pad[1] == 0 &&
!(input_buttons_last() & ((1ull << B_ZR) | (1ull << B_R)))) {
uint64_t held = guest_pointer(input + 0x148);
uint8_t flags;
memcpy(&flags, (void *)(uintptr_t)(input + 0x15c), 1);
plat_mutex_lock(&sample_lock);
OrbitTrack *track = orbit_track(object);
int owned = track->input == input && track->hold_pitch;
plat_mutex_unlock(&sample_lock);
if (owned && !(held & 0x20) && !(flags & 0x40)) {
c->x[10] = 3;
static int logged;
if (!logged) {
logged = 1;
rt_log("mouse: native distance transition retained for direct orbit");
}
}
}
}
c->x[8] = override; // displaced ldr w8, [x20, #0x188]
}
// The final pitch and yaw are on the stack (1.4.3 sp + 0x44 / 0x48, 1.2.1 sp + 0x3c / 0x40)
static void orbit_angles(Cpu *c) {
const MouseLayout *ml = mouse_layout();
@@ -526,9 +560,7 @@ static void orbit_angles(Cpu *c) {
guest_float_store(yaw_at, wrap_degrees(yaw + yaw_delta));
guest_float_store(object + 0x108, wrap_degrees(new_pitch - orbit_pitch_offset(object)));
guest_float_store(object + 0x120, wrap_degrees(guest_float(object + 0x120) + yaw_delta));
if (!track->target_saved) track->original_target = guest_float(object + 0x15c), track->target_saved = 1;
track->pitch = new_pitch; track->input = orbit.mouse.object; track->hold_pitch = 1;
orbit_pitch_target(object, new_pitch);
static int logged;
if (!logged) {
logged = 1;
@@ -1062,6 +1094,7 @@ __attribute__((constructor)) static void mouse_direct_register(void) {
rt_register_host(HOST_MOUSE_DIRECT, camera_input);
rt_register_host(HOST_MOUSE_ACTIVITY, camera_activity);
rt_register_host(HOST_MOUSE_ORBIT_BEGIN, orbit_begin);
if (rt_game_version == 143) rt_register_host(HOST_MOUSE_ORBIT_STATE, orbit_state);
rt_register_host(HOST_MOUSE_ORBIT_ANGLES, orbit_angles);
rt_register_host(HOST_MOUSE_HAND, hand_input);
rt_register_host(HOST_MOUSE_LOAD, camera_load);
+143 -17
View File
@@ -141,10 +141,13 @@ static void pool_drain(void) {
// system-managed page file, so the same request succeeds a moment later, and a stall of some frames is better than
// ending the game. Frees the pooled buffers first. NULL only after about 30 s without memory; the caller ends the game.
// Never called with a lock held.
static void big_drain(void);
static void *realloc_wait(void *old, size_t n, const char *what) {
void *p = realloc(old, n);
if (p) return p;
pool_drain();
big_drain();
static uint32_t reports;
int report = __atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8;
if (report) {
@@ -406,8 +409,10 @@ void nvn_pins_stats(uint64_t *live, uint64_t *freed) {
// adding control memory again releases the copies recorded there without walking every live copy. 4 KiB pages cost
// about 500 lookups per AddControlMemory under the exclusive lock (1.4.3 field: 53 calls a frame, 2 MiB ranges, 3.7
// copies per page), and the recording threads waited on it.
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs (pin_refs)
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins; } copy_prefix;
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs; pin_v: those pins, in
// record order, or NULL (then pin_refs walks the records); cap: bytes of the block from the large-block cache
// (copy_block), 0 for a plain heap block
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins, cap; const tex_t **pin_v; } copy_prefix;
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
@@ -429,15 +434,117 @@ static void zombie_add(copy_prefix *pf) { // copy_lock held
zombies[nzombies++] = pf;
}
static void copy_free(copy_prefix *pf) { // copy_lock held
if (pf->pins) pin_refs((const uint8_t *)(pf + 1), -1);
// Blocks of large copies are kept for the next ones. 1.4.3 makes two copies of 0.5-2 MiB a frame (the per-frame
// recordings that inline many others); above the heap's direct-allocation threshold (about 512 KiB) every malloc mapped
// fresh zero-filled pages and every free unmapped them, so the copy ran at half the speed of a small one and the free
// held copy_lock for tens of microseconds. A block is reused for a copy that fills at least half of it.
#define BIG_MIN ((size_t)256 << 10) // smaller blocks come from the heap and go back to it
#define BIG_MAX ((size_t)16 << 20) // a larger block is never kept
#define BIG_SLOTS 8
static PlatMutex big_lock = PLAT_MUTEX_INIT; // lock order: copy_lock, then big_lock
static struct { copy_prefix *p; size_t cap; } big[BIG_SLOTS];
static uint32_t nbig;
static void big_drain(void) {
plat_mutex_lock(&big_lock);
while (nbig) free(big[--nbig].p);
plat_mutex_unlock(&big_lock);
}
// A block for a copy of n bytes after its prefix; prefix.cap says where it goes back (copy_block_free). wait: a failed
// allocation waits for memory (realloc_wait), which is never done with a lock held; else it ends the game at once.
static copy_prefix *copy_block(size_t n, int wait, const char *what) {
size_t total = sizeof(copy_prefix) + n, cap = 0;
copy_prefix *pf = NULL;
if (total >= BIG_MIN && total <= BIG_MAX) {
plat_mutex_lock(&big_lock);
uint32_t best = BIG_SLOTS;
for (uint32_t i = 0; i < nbig; i++)
if (big[i].cap >= total && big[i].cap / 2 <= total && (best == BIG_SLOTS || big[i].cap < big[best].cap))
best = i;
if (best < BIG_SLOTS) {
pf = big[best].p;
cap = big[best].cap;
big[best] = big[--nbig];
}
plat_mutex_unlock(&big_lock);
if (!pf) cap = (total + total / 8 + 0xffff) & ~(size_t)0xffff; // room for the next frame's slightly larger one
}
if (!pf) pf = wait ? realloc_wait(NULL, cap ? cap : total, what) : malloc(cap ? cap : total);
if (!pf) rt_fatal("nvn: %s allocation (%zu bytes)", what, cap ? cap : total);
pf->cap = cap;
return pf;
}
static void copy_block_free(copy_prefix *pf) {
size_t cap = pf->cap;
if (cap) {
plat_mutex_lock(&big_lock);
if (nbig < BIG_SLOTS) {
big[nbig].p = pf;
big[nbig++].cap = cap;
pf = NULL;
} else {
uint32_t small = 0;
for (uint32_t i = 1; i < nbig; i++)
if (big[i].cap < big[small].cap) small = i;
if (big[small].cap < cap) {
copy_prefix *drop = big[small].p;
big[small].p = pf;
big[small].cap = cap;
pf = drop;
}
}
plat_mutex_unlock(&big_lock);
}
free(pf);
}
static void zombie_sweep(uint64_t replayed) { // copy_lock held
// Counts (delta 1) or uncounts (-1) the pins a copy names: from its list, without walking its records. A copy of a
// per-frame recording is 0.5-2 MiB of records naming a few pins; walking them again at every free cost about 40 us.
static void copy_pins_ref(const copy_prefix *pf, int delta) {
if (!pf->pins) return;
if (!pf->pin_v) {
pin_refs((const uint8_t *)(pf + 1), delta);
return;
}
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
for (uint64_t i = 0; i < pf->pins; i++) pin_ref(pf->pin_v[i], delta, now);
}
// A copy in no map and no zombie list: nothing else can reach it, so copy_lock need not be held.
static void copy_free(copy_prefix *pf) {
copy_pins_ref(pf, -1);
free((void *)pf->pin_v);
copy_block_free(pf);
}
// Copies retired under copy_lock and freed after it is released: the frees, the unpinning walk over their records and
// the large blocks kept the lock exclusive for most of release_copies, while the game's other recording threads waited
// for it in copy_touch (1.4.3 town: three threads, 0.6 ms a frame of waiting).
typedef struct { copy_prefix **v; uint32_t n, cap; } dead_list;
static __thread dead_list dead;
static void dead_add(copy_prefix *pf) {
if (dead.n == dead.cap) {
uint32_t cap = dead.cap ? dead.cap * 2 : 256;
copy_prefix **v = realloc(dead.v, sizeof *v * cap);
if (!v) { copy_free(pf); return; } // out of memory: free it here, under the lock, as before
dead.v = v;
dead.cap = cap;
}
dead.v[dead.n++] = pf;
}
static void dead_free(void) { // copy_lock not held
for (uint32_t i = 0; i < dead.n; i++) copy_free(dead.v[i]);
dead.n = 0;
}
static void zombie_sweep(uint64_t replayed) { // copy_lock held; the swept copies go to the dead list
for (uint32_t i = 0; i < nzombies;) {
if (zombies[i]->used_at <= replayed) {
copy_free(zombies[i]);
dead_add(zombies[i]);
zombies[i] = zombies[--nzombies];
} else {
i++;
@@ -477,13 +584,16 @@ static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
}
// The recording [p, p + n), OP_BEGIN to OP_END (the recording buffer is one block: recording emits no OP_JUMP), copied
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
// into one host block (copy_block: its exact size, or a kept large block). One allocation and one copy: growing the
// copy record by record cost the game's main thread about 1 ms a frame in reallocations and small copies (1.4.3,
// field). The pins the records name are counted and listed (copy_prefix.pin_v) on the way.
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
if (!out) rt_fatal("nvn: command copy allocation");
copy_prefix *block = copy_block(n, 1, "command copy");
uint8_t *out = (uint8_t *)block;
memcpy(out + sizeof(copy_prefix), p, n);
static __thread struct { const tex_t **v; uint64_t cap; } named; // the pins of this recording, reused
uint64_t pins_named = 0;
int listed = 1; // 0 after a failed growth: the copy goes without a list
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
op_hdr h;
memcpy(&h, p + at, sizeof h);
@@ -493,6 +603,14 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
const tex_t *pinned[9];
int k = op_pins(h.op, out + sizeof(copy_prefix) + at + 8, pinned);
for (int i = 0; i < k; i++) pin_ref(pinned[i], 1, 0);
if (k && listed && pins_named + (uint64_t)k > named.cap) {
uint64_t cap = named.cap ? named.cap * 2 : 64;
while (cap < pins_named + (uint64_t)k) cap *= 2;
const tex_t **v = realloc((void *)named.v, sizeof *v * cap);
if (v) named.v = v, named.cap = cap;
else listed = 0;
}
if (k && listed) memcpy((void *)(named.v + pins_named), pinned, sizeof *pinned * (size_t)k);
pins_named += (uint64_t)k;
at += h.size;
if (h.op == OP_END) {
@@ -500,7 +618,10 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
break;
}
}
*(copy_prefix *)out = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named};
const tex_t **pin_v = NULL;
if (pins_named && listed && (pin_v = malloc(sizeof *pin_v * pins_named)))
memcpy((void *)pin_v, (const void *)named.v, sizeof *pin_v * pins_named);
*block = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named, .cap = block->cap, .pin_v = pin_v};
return out + sizeof(copy_prefix);
}
@@ -550,7 +671,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
copy_bytes -= pf->bytes;
pf->magic = 0;
if (pf->used_at > replayed) zombie_add(pf); // a queued submission still reads it
else copy_free(pf);
else dead_add(pf);
b->v[i] = b->v[--b->n];
}
if (!b->n) {
@@ -560,6 +681,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
}
}
plat_mutex_unlock(&copy_lock);
dead_free();
}
// ---------- CommandBuffer: object and memory
@@ -1182,11 +1304,14 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
}
out[i] = handles[i];
if (pf->used_at > replayed && pf->used_at != now && pf->deferred != 1) {
size_t sz = sizeof *pf + pf->bytes;
copy_prefix *cl = malloc(sz);
if (!cl) rt_fatal("nvn: out of memory (recording clone, %zu bytes)", sz);
memcpy(cl, pf, sz);
if (cl->pins) pin_refs((const uint8_t *)(cl + 1), 1);
copy_prefix *cl = copy_block(pf->bytes, 0, "recording clone"); // copy_lock held
size_t cap = cl->cap;
memcpy(cl, pf, sizeof *pf + pf->bytes);
cl->cap = cap;
cl->pin_v = NULL;
if (pf->pin_v && (cl->pin_v = malloc(sizeof *cl->pin_v * pf->pins)))
memcpy((void *)cl->pin_v, (const void *)pf->pin_v, sizeof *cl->pin_v * pf->pins);
copy_pins_ref(cl, 1);
cl->magic = 0; // not a live handle: the game never sees it
cl->used_at = now;
zombie_add(cl);
@@ -1197,6 +1322,7 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
}
}
plat_mutex_unlock(&copy_lock);
dead_free();
for (int i = 0; i < n; i++) {
int nd = resolve_deferred((const uint8_t *)(uintptr_t)out[i], 0);
if (out[i] == handles[i]) ((copy_prefix *)(uintptr_t)out[i] - 1)->deferred = nd ? 2 : 1;
+7 -1
View File
@@ -929,8 +929,14 @@ static void probe_menu_text(void) {
cfg_set("TOTK_MENU_LANG", "zh-Hans");
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
cfg_set("TOTK_MENU_LANG", "");
// the system language as ui_text.c resolve_menu_lang takes it: the tag, else its base language (ru-RU: ru), else en
const char *sys_lang = ui_text_lang(plat_ui_language());
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang : "en";
char sys_base[16] = "";
size_t base_n = strcspn(sys_lang, "-");
if (sys_lang[base_n] && base_n < sizeof sys_base) memcpy(sys_base, sys_lang, base_n);
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang
: *sys_base && ui_text_defined(sys_base, UI_MENU_TITLE) ? sys_base
: "en";
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
}
+35
View File
@@ -1456,6 +1456,41 @@ int plat_open_folder(const char *path) {
void *plat_lib_open(const char *name) { return dlopen(name, RTLD_NOW | RTLD_LOCAL); }
void *plat_lib_sym(void *lib, const char *name) { return lib ? dlsym(lib, name) : NULL; }
void plat_lib_error(char *out, size_t cap) {
const char *e = dlerror();
snprintf(out, cap, "%s", e ? e : "unknown error");
}
int plat_ngx_core_path(char *out, size_t cap) { return snprintf(out, cap, "libnvidia-ngx.so.1") < (int)cap; }
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
const unsigned char *u = (const unsigned char *)in;
size_t n = 0;
while (*u) {
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
if (more == 4) return 0;
if (more) c &= 0x3F >> more;
for (unsigned k = 0; k < more; k++, u++) {
if ((*u & 0xC0) != 0x80) return 0;
c = c << 6 | (*u & 0x3F);
}
if (n + 1 >= cap) return 0;
out[n++] = (wchar_t)c;
}
out[n] = 0;
return 1;
}
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
char dir[PLAT_PATH_MAX];
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
if (!cut) return 0;
cut[1] = 0;
return utf8_to_wide(dir, out, cap);
}
// ---- machine load: /proc/stat and /proc/self/stat for the CPU, hwmon for the temperature, the amdgpu sysfs files of
// the render device for the GPU.
+21 -5
View File
@@ -222,9 +222,10 @@ _Noreturn void plat_reexec(void);
// - A fault in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
// overflow) is reported as it happens.
// - A fault in another module (a GPU driver, a system library) is reported only when nothing handles it: on Windows
// from the unhandled-exception filter, with the return addresses in this executable on the faulting thread's stack,
// so the report shows which host call ran into the driver; on Linux from the signal handler, without frames. Drivers
// that raise and catch their own exceptions never reach fn.
// from the unhandled-exception filter, with the return addresses on the faulting thread's stack in every module, so
// the report shows which driver code failed and which host call ran into it; on Linux from the signal handler,
// without frames. Drivers that raise and catch their own exceptions never reach fn. For a C++ exception thrown by
// MSVC-built code (a driver) the report names its type and, for a std::exception, its what() text.
// - On Windows abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function go there
// too, with the host frames in this executable: the C runtime ends the process for both with 0xc0000409 and no
// exception.
@@ -234,9 +235,14 @@ typedef struct {
uintptr_t rip, offset; // the faulting instruction, and its offset in its module
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
uint64_t addr; // its address when access >= 0
int nframes; // return addresses in this executable, innermost first: for abort and invalid parameter
uintptr_t frames[16]; // (rip is frames[0]) and for a fault in another module; 0 for a fault in this one
int nframes; // return addresses, innermost first: for abort and invalid parameter (rip is frames[0])
uintptr_t frames[32]; // and for a fault in another module; 0 for a fault in this one
const char *frame_module[32]; // the module of each frame: NULL for this executable, "" for none
uintptr_t frame_offset[32]; // each frame's offset in its module
int frames_left_out; // frames in other modules past the limit the walk keeps, so this executable's still fit
const char *module; // the module holding rip: NULL for this executable, "" for none (a jump to address 0)
const char *cxx_type; // a C++ exception from MSVC-built code: the thrown type's decorated name (.?AV...@@)
const char *cxx_what; // and what() when the type derives from std::exception; NULL when unknown
} PlatCrash;
void plat_crash_hook(void (*fn)(const PlatCrash *c));
int plat_mem_read(uint64_t addr, void *out, size_t n); // copies n bytes, 0 instead of faulting on unmapped memory
@@ -387,6 +393,16 @@ int plat_open_folder(const char *path); // shows the folder in
// in the system directories only. NULL when missing.
void *plat_lib_open(const char *name);
void *plat_lib_sym(void *lib, const char *name);
// Why the calling thread's last plat_lib_open failed, for a log line (the OS error code on Windows, dlerror on Linux).
void plat_lib_error(char *out, size_t cap);
// ---- NVIDIA NGX (vk_dlaa.c). The path of the driver's NGX core library into out: on Windows _nvngx.dll in the folder
// the driver records in the registry (NGXCore FullPath), on Linux libnvidia-ngx.so.1, which the dynamic loader finds
// where the driver put it. 0 when the driver records none.
int plat_ngx_core_path(char *out, size_t cap);
// The running executable's folder with a trailing separator, as the wchar_t string NGX takes for its application data
// path (UTF-16 on Windows, UTF-32 code points on Linux). 0 on failure or when out has no room.
int plat_exe_dir_wide(wchar_t *out, size_t cap);
// ---- machine load for the performance overlay (perfmon.c's sampling thread; each call can take milliseconds). A number
// the system does not give is -1.
+89 -11
View File
@@ -674,16 +674,20 @@ static const char *image_name(const char *base) {
return base + ((const IMAGE_EXPORT_DIRECTORY *)(base + d->VirtualAddress))->Name;
}
// The return addresses in [lo, lo + size) on the crashed thread's stack, innermost first: unwinds the faulting context
// The return addresses on the crashed thread's stack, innermost first, with their modules: unwinds the faulting context
// with the x64 unwind tables; a function without a table entry (a leaf, or address 0 after a call through a null
// pointer) has its return address on top of the stack. The walk stops at return address 0 (the thread's first frame)
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting.
static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t *out, int max) {
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting. A driver can
// nest deeper than the report holds, so frames in other modules stop being kept after the first 12 (counted in
// frames_left_out) and the frames in this executable, which name the host call that ran into the driver, still fit.
static void frames_of(const CONTEXT *at, PlatCrash *x) {
enum { MAX = sizeof x->frames / sizeof *x->frames, OTHER_MAX = 12 };
CONTEXT c = *at;
ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high);
int n = 0;
for (int i = 0; i < 256 && n < max && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
void *exe = GetModuleHandleW(NULL);
int other = 0;
for (int i = 0; i < 256 && x->nframes < MAX && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
DWORD64 img;
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
if (fe) {
@@ -694,9 +698,64 @@ static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t
c.Rip = *(const DWORD64 *)c.Rsp;
c.Rsp += 8;
}
if (c.Rip - lo < size) out[n++] = (uintptr_t)c.Rip;
if (!c.Rip) break;
void *base = NULL;
RtlPcToFileHeader((void *)(uintptr_t)c.Rip, &base);
if (base != exe && other++ >= OTHER_MAX) {
x->frames_left_out++;
continue;
}
int n = x->nframes++;
x->frames[n] = (uintptr_t)c.Rip;
x->frame_module[n] = base == exe ? NULL : base ? image_name(base) : "";
x->frame_offset[n] = (uintptr_t)c.Rip - (uintptr_t)base;
}
return n;
}
// MSVC's C++ throw (0xe06d7363) passes the thrown object in ExceptionInformation[1], its ThrowInfo in [2] and the image
// base the ThrowInfo's 32-bit offsets are relative to in [3] (magic 0x19930520..0x19930522 in [0]). The ThrowInfo lists
// the types a catch may match, the thrown type first; each type descriptor holds a vtable pointer, a spare pointer and
// the decorated name. A std::exception keeps its what() text in the pointer right after its vtable pointer (MSVC's
// __std_exception_data). Every read is checked: the exception may come from a damaged heap.
typedef struct { uint32_t attributes; int32_t unwind, forward_compat, catchable_types; } CxxThrowInfo;
typedef struct { uint32_t properties; int32_t type, this_mdisp, this_pdisp, this_vdisp, size, copy; } CxxCatchableType;
// Copies the string at p into buf (cut to n - 1 characters), checking each page before reading it; NULL when the first
// byte cannot be read.
static const char *copy_cstr(uintptr_t p, char *buf, size_t n) {
size_t i = 0;
for (; p && i + 1 < n; i++) {
if ((!i || !((p + i) & 4095)) && !plat_mem_readable(p + i, 1)) break;
if (!(buf[i] = ((const char *)p)[i])) return buf;
}
buf[i] = 0;
return i ? buf : NULL;
}
static void cxx_info(const EXCEPTION_RECORD *er, PlatCrash *x) {
if (er->NumberParameters < 4 || er->ExceptionInformation[0] - 0x19930520u > 2) return;
uintptr_t obj = er->ExceptionInformation[1], info = er->ExceptionInformation[2],
base = er->ExceptionInformation[3];
if (!info || !plat_mem_readable(info, sizeof(CxxThrowInfo))) return;
const CxxThrowInfo *ti = (const void *)info;
uintptr_t arr = base + (uintptr_t)(intptr_t)ti->catchable_types;
if (!ti->catchable_types || !plat_mem_readable(arr, 4)) return;
int32_t n = *(const int32_t *)arr;
if (n <= 0 || n > 64 || !plat_mem_readable(arr + 4, 4u * (uint64_t)n)) return;
static char type[128], what[256]; // the crash report runs once: crash_fn ends the process
int exception = 0;
for (int32_t i = 0; i < n; i++) {
uintptr_t ct = base + (uintptr_t)(intptr_t)((const int32_t *)(arr + 4))[i];
if (!plat_mem_readable(ct, sizeof(CxxCatchableType))) continue;
uintptr_t td = base + (uintptr_t)(intptr_t)((const CxxCatchableType *)ct)->type;
char name[64];
if (!copy_cstr(td + 16, i ? name : type, i ? sizeof name : sizeof type)) // past vtable and spare pointers
continue;
if (!i) x->cxx_type = type;
if (!strcmp(i ? name : type, ".?AVexception@std@@")) exception = 1;
}
if (exception && obj && plat_mem_readable(obj + 8, 8))
x->cxx_what = copy_cstr(*(const uintptr_t *)(obj + 8), what, sizeof what);
}
// The last-chance filter: an exception no handler took, in any module. crash_veh reports faults in this executable as
@@ -723,14 +782,12 @@ static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep) {
c.access = (int)er->ExceptionInformation[0], c.addr = er->ExceptionInformation[1];
void *base = NULL;
RtlPcToFileHeader((void *)rip, &base);
MODULEINFO mi;
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
c.offset = rip - (uintptr_t)base;
if (!base) c.module = "";
else if ((uintptr_t)base != exe) c.module = image_name(base);
if (c.module && GetModuleInformation(GetCurrentProcess(), (HMODULE)exe, &mi, sizeof mi))
c.nframes = frames_in(ep->ContextRecord, exe, mi.SizeOfImage, c.frames,
(int)(sizeof c.frames / sizeof *c.frames));
if (c.module) frames_of(ep->ContextRecord, &c);
if (code == 0xe06d7363u) cxx_info(er, &c);
crash_fn(&c);
return EXCEPTION_CONTINUE_SEARCH;
}
@@ -1389,6 +1446,27 @@ void *plat_lib_open(const char *name) {
void *plat_lib_sym(void *lib, const char *name) { return lib ? (void *)GetProcAddress((HMODULE)lib, name) : NULL; }
void plat_lib_error(char *out, size_t cap) { snprintf(out, cap, "error %lu", GetLastError()); }
int plat_ngx_core_path(char *out, size_t cap) {
wchar_t dir[WPATH];
DWORD n = sizeof dir;
char d[PLAT_PATH_MAX];
if (RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", L"FullPath", RRF_RT_REG_SZ,
NULL, dir, &n) != ERROR_SUCCESS ||
!WideCharToMultiByte(CP_UTF8, 0, dir, -1, d, (int)sizeof d, NULL, NULL))
return 0;
return snprintf(out, cap, "%s\\_nvngx.dll", d) < (int)cap;
}
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
DWORD len = GetModuleFileNameW(NULL, out, (DWORD)cap);
if (!len || len >= cap) return 0;
while (len && out[len - 1] != L'\\' && out[len - 1] != L'/') len--;
out[len] = 0;
return len != 0;
}
// ---- machine load: GetSystemTimes for the CPU, PDH (pdh.dll, loaded on first use) for the thermal zones and the GPU
// engine and adapter memory counters that Task Manager reads.
+214
View File
@@ -0,0 +1,214 @@
// Play time: how long the player has spent in the game, across runs, shown on the Game tab of the settings menu
// (ui/menu.cpp tab_game). The game itself keeps no play time a player can see, and the Switch's own play log is not
// there to read, so the port counts it.
//
// One host thread (it never holds a virtual core, as discord_rpc.c's) wakes every second and adds the time since its
// last wake when the window has the focus (gfx_window_focused) and the game presented at least one frame meanwhile
// (gfx_stats). So the shader prewarm before the first frame, a window left in the background and a hung game do not
// count; loading screens and the game's own menus do, as they present frames. One wake adds at most TICK_CAP_MS, so a
// machine that slept with the game open adds two seconds, not the night.
//
// The total lives in <profile>/playtime.txt, next to account.uid and the saves, so it stays with them when the
// install is updated or moved. It is plain text, "total_seconds <n>", written atomically (plat_file_write_atomic)
// every SAVE_EVERY_MS while it grows and once more when the game exits (playtime_save from boot.c, svc.c and
// gfx_present.c, next to prof_write); a crash loses at most SAVE_EVERY_MS. A file that does not read as that is kept
// as playtime.txt.bad and the count starts again from zero. A scripted run (--seconds) reads the total and adds nothing.
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gfx.h"
#include "host.h"
#include "platform/platform.h"
#define TICK_MS 1000
#define TICK_CAP_MS 2000
#define SAVE_EVERY_MS 30000
#define TOTAL_CAP_S (100ull * 365 * 24 * 3600) // a hundred years: anything larger is a damaged file
static PlatMutex lock = PLAT_MUTEX_INIT; // the totals and the file
static uint64_t total_ms, session_ms, saved_ms;
static int loaded; // 1 once the file was read (or found missing), -1 when there is no profile
static char path[1100];
// The seconds of a playtime.txt: "total_seconds <n>", a line end or nothing after it, a UTF-8 byte order mark before it
// (an editor may add one). 1 when the file reads so and n is at most TOTAL_CAP_S.
static int parse(const uint8_t *b, uint64_t n, uint64_t *seconds) {
if (n >= 3 && !memcmp(b, "\xEF\xBB\xBF", 3)) b += 3, n -= 3;
char text[64] = {0};
memcpy(text, b, n < sizeof text - 1 ? n : sizeof text - 1);
unsigned long long s = 0;
char tail = 0;
if (sscanf(text, "total_seconds %llu%c", &s, &tail) < 1 || (tail && tail != '\n' && tail != '\r') ||
s > TOTAL_CAP_S)
return 0;
*seconds = s;
return 1;
}
// Holding lock. dir: the profile directory.
static void load(const char *dir) {
if (loaded) return;
if (!dir || !*dir || snprintf(path, sizeof path, "%s/playtime.txt", dir) >= (int)sizeof path) {
loaded = -1;
rt_log("playtime: no profile directory; play time is not counted");
return;
}
loaded = 1;
uint64_t n = 0, s = 0;
uint8_t *b = plat_file_read(path, &n);
if (!b) {
rt_log("playtime: no %s yet; counting from zero", path);
return;
}
if (parse(b, n, &s)) {
free(b);
total_ms = saved_ms = s * 1000;
rt_log("playtime: %llu h %02llu min played before this run (%s)", (unsigned long long)(s / 3600),
(unsigned long long)(s / 60 % 60), path);
return;
}
char bad[1110];
snprintf(bad, sizeof bad, "%s.bad", path);
int kept = plat_file_write_atomic(bad, b, (size_t)n); // the whole file, for the player to recover the number
free(b);
rt_log("playtime: %s does not read as \"total_seconds <n>\"; %s, counting from zero", path,
kept ? "kept as playtime.txt.bad" : "could not keep a copy");
}
// Holding lock.
static void save_locked(void) {
if (loaded != 1 || total_ms / 1000 == saved_ms / 1000) return;
char text[48];
int n = snprintf(text, sizeof text, "total_seconds %" PRIu64 "\n", total_ms / 1000);
if (plat_file_write_atomic(path, text, (size_t)n)) saved_ms = total_ms;
else rt_log("playtime: cannot write %s", path);
}
void playtime_save(void) {
plat_mutex_lock(&lock);
save_locked();
plat_mutex_unlock(&lock);
}
int playtime_get(uint64_t *total_s, uint64_t *session_s) {
plat_mutex_lock(&lock);
int ok = loaded == 1;
*total_s = total_ms / 1000;
*session_s = session_ms / 1000;
plat_mutex_unlock(&lock);
return ok;
}
// Holding lock: the time since the last tick, d ms, when the game was played meanwhile; the file every SAVE_EVERY_MS.
static void tick_locked(uint64_t now, uint64_t d, int playing, uint64_t *last_save) {
if (d > TICK_CAP_MS) d = TICK_CAP_MS;
if (playing) total_ms += d, session_ms += d;
if (now - *last_save >= SAVE_EVERY_MS) save_locked(), *last_save = now;
}
static void playtime_main(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
uint64_t last = plat_now_ms(), last_save = last, frames = gfx_stats().frames;
for (;;) {
plat_sleep_ms(TICK_MS);
uint64_t now = plat_now_ms(), f = gfx_stats().frames;
int playing = f != frames && gfx_window_focused();
plat_mutex_lock(&lock);
tick_locked(now, now - last, playing, &last_save);
plat_mutex_unlock(&lock);
last = now, frames = f;
}
}
// The file is read at once, so the Game tab shows the total from the first frame. A scripted run (--seconds: benches,
// menu shots, input scripts) shows it but adds nothing: those runs are not play.
void playtime_init(void) {
static int32_t started;
if (plat_xchg32(&started, 1)) return;
plat_mutex_lock(&lock);
load(rt_config()->profile);
int on = loaded == 1;
plat_mutex_unlock(&lock);
if (!on) return;
if (rt_config()->seconds > 0) {
rt_log("playtime: a scripted run (--seconds); play time is shown, not counted");
return;
}
if (!plat_thread_spawn(playtime_main, NULL, "playtime", 0))
rt_log("playtime: cannot start its thread; play time is not counted");
}
// --probe-playtime: the file's parsing, a missing, a valid and a damaged file, the periodic save and the tick's cap, in
// a directory next to totk.exe. Needs no game files.
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL playtime: %s\n", what); } } while (0)
static void reset(void) { total_ms = session_ms = saved_ms = 0, loaded = 0, path[0] = 0; }
static int file_is(const char *p, const char *want) {
uint64_t n = 0;
uint8_t *b = plat_file_read(p, &n);
int ok = b && n == strlen(want) && !memcmp(b, want, n);
free(b);
return ok;
}
int probe_playtime(void) {
uint64_t s = 0;
CHECK(parse((const uint8_t *)"total_seconds 42\n", 17, &s) && s == 42, "a line with LF");
CHECK(parse((const uint8_t *)"total_seconds 42\r\n", 18, &s) && s == 42, "a line with CRLF");
CHECK(parse((const uint8_t *)"total_seconds 42", 16, &s) && s == 42, "no line end");
CHECK(parse((const uint8_t *)"\xEF\xBB\xBFtotal_seconds 7\n", 19, &s) && s == 7, "a byte order mark");
CHECK(!parse((const uint8_t *)"total_seconds 42x", 17, &s), "a character after the number");
CHECK(!parse((const uint8_t *)"total_seconds -1\n", 17, &s), "a negative number");
CHECK(!parse((const uint8_t *)"total_seconds 3153600001\n", 25, &s), "over a hundred years");
CHECK(!parse((const uint8_t *)"seconds 42\n", 11, &s), "another key");
CHECK(!parse((const uint8_t *)"", 0, &s), "an empty file");
char dir[1024];
if (!plat_exe_path(dir, sizeof dir)) return 1;
char *cut = plat_last_sep(dir);
if (!cut) return 1;
strcpy(cut + 1, "playtime-probe");
plat_file_mkdir_p(dir);
char file[1100], bad[1110];
snprintf(file, sizeof file, "%s/playtime.txt", dir);
snprintf(bad, sizeof bad, "%s.bad", file);
plat_file_delete(file);
plat_file_delete(bad);
uint64_t total, session, last_save = 0;
reset();
load(dir);
CHECK(playtime_get(&total, &session) && total == 0, "a missing file counts from zero");
tick_locked(1000, 1000, 1, &last_save);
tick_locked(2000, 1000, 0, &last_save);
CHECK(plat_file_size(file) < 0, "nothing is written before SAVE_EVERY_MS");
tick_locked(2000 + 3600000, 3600000, 1, &last_save); // a machine that slept: one tick adds TICK_CAP_MS, and saves
playtime_get(&total, &session);
CHECK(total == 3 && session == 3, "played ticks count, an idle one does not, a long one is capped");
CHECK(file_is(file, "total_seconds 3\n"), "the save after SAVE_EVERY_MS writes the total");
reset();
load(dir);
playtime_get(&total, &session);
CHECK(total == 3 && session == 0, "the next run reads the total back");
static const char damaged[] = "total_seconds twelve\n";
plat_file_write_atomic(file, damaged, sizeof damaged - 1);
reset();
load(dir);
playtime_get(&total, &session);
CHECK(total == 0, "a damaged file counts from zero");
CHECK(file_is(bad, damaged), "a damaged file is kept whole as playtime.txt.bad");
plat_file_delete(file);
plat_file_delete(bad);
reset();
fprintf(stderr, "playtime: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
return fails ? 1 : 0;
}
+164 -15
View File
@@ -39,6 +39,7 @@
#include "cfg.h"
#include "host.h"
#include "mods.h"
#include "platform/platform.h"
#define BASE_W 1920
@@ -46,6 +47,7 @@
#define SCENE_W 1600
#define SCENE_H 900
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
#define TEX_HEAP_KIB 506880u // TextureHeap, shipped (1.2.1 and 1.4.3): what a mod's own HeapDef is measured against
#define SCENE_MAX 4096 // per side, as MacNativeGames' TOTK_RENDER_RES
#define SHADOW_BASE 960 // the sun shadow map's base size (its cascades: a D16 array of 4 layers)
@@ -120,6 +122,25 @@ static double dlss_share(const char *m) {
return 0;
}
// A game resolution that is not 16:9 needs the HUD's layout for its shape (romfs-patches-<w>x<h>, romfs.c
// overlay_init), which the installer makes but a developer build only has after tools/make_romfs_patches.py --aspect.
// Without it the camera would widen while the 16:9 HUD stretched across the picture, so the game draws the largest
// 16:9 size that fits inside the asked one and the camera aspect patch stays off (res_init); gfx_present.c shows the
// picture with bars (present setting aspect). Not when TOTK_ROMFS_PATCHES names the overlay itself or the game version
// has no camera aspect patch. TOTK_OUTPUT_RES is restart-only (cfg.c, RESTART), so this one decision holds all run.
static void shape_fallback(void) {
const char *own = cfg_get("TOTK_ROMFS_PATCHES");
unsigned out[2] = {R.ow, R.oh};
if (own || !res_wants_wide_overlay(out) || romfs_shaped_ready(R.ow, R.oh)) return;
unsigned h = R.oh, w = R.ow;
if ((uint64_t)w * 9 <= (uint64_t)h * 16) h = (unsigned)((uint64_t)w * 9 / 16) & ~1u;
w = (unsigned)((uint64_t)h * 16 / 9) & ~1u;
rt_log("resolution: no romfs-patches-%ux%u for the HUD at this shape; the game draws %ux%u (16:9) and the present "
"adds bars. Make the layout with: tools/make_romfs_patches.py --version <v121|v143> --aspect %ux%u --out "
"build/<version>/romfs-patches-%ux%u", R.ow, R.oh, w, h, R.ow, R.oh, R.ow, R.oh);
R.ow = w, R.oh = h;
}
static void decide(void) {
if (R.decided) return;
R.decided = 1;
@@ -131,6 +152,7 @@ static void decide(void) {
if (sscanf(o, "%ux%u%c", &w, &h, &tail) == 2 && w >= 1280 && h >= 720 && w <= 7680 && h <= 4320) {
R.ow = w & ~1u;
R.oh = h & ~1u;
shape_fallback();
} else {
rt_log("resolution: TOTK_OUTPUT_RES=%s is not <width>x<height> in 1280x720..7680x4320; 1920x1080", o);
}
@@ -189,6 +211,33 @@ uint64_t res_heap_extra_kib(void) {
uint64_t res_heap_extra_bytes(void) { return ((res_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
// TOTK_TEX_HEAP_EXTRA_MB (0 to 4096): more room in "TextureHeap" and its parent "ゲームグラフィックス", the heap the
// world's textures stream into. HD texture packs made for emulators with 8 GB of DRAM grow it by about 3.3 GiB in their
// own HeapDef; without the room the game runs out and starts its panic Blood Moon over and over. The pool and the heap
// cap grow by the same amount (memory.c mem_init).
static uint64_t mod_tex_heap_kib(void);
static uint64_t cfg_tex_heap_kib(void) {
const char *v = cfg_get("TOTK_TEX_HEAP_EXTRA_MB");
if (!v || !*v) return 0;
char *end;
long mb = strtol(v, &end, 10);
if (end == v || *end || mb < 0 || mb > 4096) {
static int logged;
if (!logged++) rt_log("resolution: TOTK_TEX_HEAP_EXTRA_MB=%s is not 0..4096; 0", v);
return 0;
}
return (uint64_t)mb << 10;
}
// The larger of TOTK_TEX_HEAP_EXTRA_MB and what an enabled mod's own HeapDef adds to the texture heap (mod_tex_heap_kib).
static uint64_t tex_heap_extra_kib(void) {
uint64_t c = cfg_tex_heap_kib(), m = mod_tex_heap_kib();
return c > m ? c : m;
}
uint64_t res_tex_heap_extra_bytes(void) { return ((tex_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
// ---- the HeapDef table. BYML version 2..7, little endian ("YB"): a hash node is {u8 0xC1, u24 count} and count
// entries {u24 key index, u8 type, u32 value}; an int (type 0xD1) is stored in the entry, so a size changes in place.
typedef struct { uint8_t *p; size_t n; uint32_t keys; } Byml;
@@ -226,28 +275,103 @@ static size_t heap_size_field(const Byml *b, uint32_t heaps, const char *heap) {
return at && u32_at(b, at, &node) ? field(b, node, "Size", 0xd1) : 0;
}
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
uint64_t extra = res_heap_extra_kib();
*out = NULL;
if (!extra) return 0;
// A HeapDef in b's place (zstd frame in[0..n) with its content size, no dictionary): b.p malloc'd and *heaps its
// "Heaps" node; 0 when it is not one (b.p freed). why: what the log line says the caller keeps then.
static int heapdef_open(const void *in, size_t n, Byml *b, uint32_t *heaps, const char *why) {
b->p = NULL;
if (ZSTD_getDictID_fromFrame(in, n)) {
rt_log("resolution: the HeapDef table needs a zstd dictionary; the game keeps its own resolution");
rt_log("resolution: the HeapDef table needs a zstd dictionary; %s", why);
return 0;
}
unsigned long long raw = ZSTD_getFrameContentSize(in, n);
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > (64u << 20)) {
rt_log("resolution: the HeapDef table is not a zstd frame with a size; the game keeps its own resolution");
rt_log("resolution: the HeapDef table is not a zstd frame with a size; %s", why);
return 0;
}
Byml b = {malloc((size_t)raw), (size_t)raw, 0};
if (!b.p) return 0;
size_t got = ZSTD_decompress(b.p, b.n, in, n);
uint32_t root, heaps = 0;
*b = (Byml){malloc((size_t)raw), (size_t)raw, 0};
if (!b->p) return 0;
size_t got = ZSTD_decompress(b->p, b->n, in, n);
uint32_t root;
uint16_t ver = 0;
if (!ZSTD_isError(got) && got == b.n && b.n >= 16) memcpy(&ver, b.p + 2, 2);
size_t heaps_at = 0, dta_at = 0, gfx_at = 0;
if (ver >= 2 && ver <= 7 && !memcmp(b.p, "YB", 2) && u32_at(&b, 4, &b.keys) && u32_at(&b, 12, &root) &&
(heaps_at = field(&b, root, "Heaps", 0xc1)) && u32_at(&b, heaps_at, &heaps)) {
if (!ZSTD_isError(got) && got == b->n && b->n >= 16) memcpy(&ver, b->p + 2, 2);
size_t heaps_at = 0;
if (ver >= 2 && ver <= 7 && !memcmp(b->p, "YB", 2) && u32_at(b, 4, &b->keys) && u32_at(b, 12, &root) &&
(heaps_at = field(b, root, "Heaps", 0xc1)) && u32_at(b, heaps_at, heaps))
return 1;
free(b->p);
b->p = NULL;
return 0;
}
// HD texture packs made for the emulators ship their own HeapDef with "TextureHeap" and its parent grown (HDPacks.com's
// 4K pack for TotK: by 3,419,000 KiB, 3.3 GiB, for the emulators' 8 GB memory option). Served as it is, the game asks
// for that room and the application pool has none: it crashed at start (issue #673). So before mem_init sizes the pool,
// every switched-on mod's HeapDef is read (mods.c mods_peek_romfs: mods_scan needs paths main sets later) and the
// largest TextureHeap growth over the shipped size counts as TOTK_TEX_HEAP_EXTRA_MB (the larger of the two, at most
// 4 GiB). romfs.c heapdef_overlay then serves another table grown by that amount in the pack's place: the table of the
// highest switched-on mod below it whose texture heap is not grown, else the game's own, so the other heaps keep the
// sizes the content that runs needs.
#define HEAPDEF_REL "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
static struct { int done; uint64_t kib; } MT;
// "TextureHeap" of the HeapDef table in[0..n) in KiB; 0 when the table cannot be read or has no such size (the failure
// is logged with why).
static int32_t heapdef_tex_kib(const void *in, size_t n, const char *why) {
Byml b;
uint32_t heaps;
size_t th_at = 0;
int32_t th = 0;
if (!heapdef_open(in, n, &b, &heaps, why)) return 0;
if ((th_at = heap_size_field(&b, heaps, "TextureHeap"))) memcpy(&th, b.p + th_at, 4);
else rt_log("resolution: the HeapDef table has no TextureHeap size; %s", why);
free(b.p);
return th;
}
static void mod_heapdef(const char *name, const char *romfs, void *ctx) {
(void)ctx;
char path[2300], why[400];
if (snprintf(path, sizeof path, "%s/%s", romfs, HEAPDEF_REL) >= (int)sizeof path) return;
uint64_t n = 0;
uint8_t *in = plat_file_read(path, &n);
if (!in) return;
snprintf(why, sizeof why, "mod %s: its texture heap growth is left out", name);
int32_t th = heapdef_tex_kib(in, (size_t)n, why);
free(in);
if (!th) return;
int64_t grow = (int64_t)th - (int64_t)TEX_HEAP_KIB;
if (grow > (int64_t)(4096u << 10)) grow = 4096u << 10;
if (grow > 0 && (uint64_t)grow > MT.kib) MT.kib = (uint64_t)grow;
rt_log("resolution: mod %s ships a HeapDef with TextureHeap %d KiB (the game's %u KiB)%s", name, th, TEX_HEAP_KIB,
grow > 0 ? ": the port grows the texture heap and the pool by the difference" : "");
}
static uint64_t mod_tex_heap_kib(void) {
if (!MT.done) MT.done = 1, mods_peek_romfs(mod_heapdef, NULL);
return MT.kib;
}
// Only such a table is swapped for the game's own (romfs.c heapdef_overlay): a mod table that changes other heaps
// keeps them. A table it cannot read stays the base (logged).
int res_heapdef_tex_grown(const void *in, size_t n) {
return heapdef_tex_kib(in, n, "the mod's table stays the base") > (int32_t)TEX_HEAP_KIB;
}
// A table that can stand in for an HD pack's (romfs.c heapdef_overlay): readable, with TextureHeap at most the shipped
// size. A content mod's table (The Second Quest grows RSDB and UI for its own files) is one.
int res_heapdef_plain(const void *in, size_t n) {
int32_t th = heapdef_tex_kib(in, n, "it does not stand in for the texture pack's table");
return th > 0 && th <= (int32_t)TEX_HEAP_KIB;
}
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
uint64_t extra = res_heap_extra_kib(), tex = tex_heap_extra_kib();
*out = NULL;
if (!extra && !tex) return 0;
Byml b;
uint32_t heaps = 0;
size_t dta_at = 0, gfx_at = 0;
if (heapdef_open(in, n, &b, &heaps, "the game keeps its own resolution")) {
dta_at = heap_size_field(&b, heaps, "DynamicTextureAllocatorMem");
gfx_at = heap_size_field(&b, heaps, "Graphics");
}
@@ -263,6 +387,26 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
int32_t dta2 = dta + (int32_t)extra, gfx2 = gfx + (int32_t)extra;
memcpy(b.p + dta_at, &dta2, 4);
memcpy(b.p + gfx_at, &gfx2, 4);
int32_t th = 0, th2 = 0, gg = 0, gg2 = 0;
if (tex) { // TOTK_TEX_HEAP_EXTRA_MB: the world texture heap and its parent grow by the same amount
static const char parent[] = "\xe3\x82\xb2\xe3\x83\xbc\xe3\x83\xa0\xe3\x82\xb0\xe3\x83\xa9\xe3\x83\x95"
"\xe3\x82\xa3\xe3\x83\x83\xe3\x82\xaf\xe3\x82\xb9"; // ゲームグラフィックス
size_t th_at = heap_size_field(&b, heaps, "TextureHeap"), gg_at = heap_size_field(&b, heaps, parent);
if (th_at && gg_at) {
memcpy(&th, b.p + th_at, 4);
memcpy(&gg, b.p + gg_at, 4);
th2 = th + (int32_t)tex, gg2 = gg + (int32_t)tex;
memcpy(b.p + th_at, &th2, 4);
memcpy(b.p + gg_at, &gg2, 4);
} else {
rt_log("resolution: the HeapDef table has no TextureHeap sizes; TOTK_TEX_HEAP_EXTRA_MB left out");
tex = 0;
if (!extra) {
free(b.p);
return 0;
}
}
}
size_t cap = ZSTD_compressBound(b.n);
void *z = malloc(cap);
size_t zn = z ? ZSTD_compress(z, cap, b.p, b.n, 9) : 0;
@@ -273,7 +417,12 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
}
*out = z;
*out_n = zn;
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx, gfx2);
if (extra)
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx,
gfx2);
if (tex)
rt_log("resolution: HeapDef TextureHeap %d -> %d KiB and its parent %d -> %d KiB (TOTK_TEX_HEAP_EXTRA_MB or a "
"mod's HeapDef)", th, th2, gg, gg2);
return 1;
}
+71 -4
View File
@@ -533,6 +533,22 @@ static void added_init(void) {
rt_log("RomFS overlay: %zu files added to the RomFS", nadded);
}
// RomFS file `rel` as the image holds it, whatever overlays replace it. malloc'd; NULL when it is missing, unreadable
// or 16 MiB or more (logged with `keeps`, what the game does without it).
static void *read_original(const char *rel, size_t *n, const char *keeps) {
uint64_t i = lower(rel);
if (i >= file_count || strcmp(entry_name(entry(i)), rel)) return NULL;
uint64_t size = entry(i).size;
void *in = size && size < (16u << 20) ? malloc((size_t)size) : NULL;
if (!in || read_at(image, in, (uint32_t)size, entry(i).offset) != (int64_t)size) {
free(in);
rt_log("RomFS: cannot read %s; %s", rel, keeps);
return NULL;
}
*n = (size_t)size;
return in;
}
// RomFS file `rel` as the game would read it: from an overlay that replaces it (*base), else from the image. malloc'd;
// NULL when it is missing, unreadable or 16 MiB or more (logged with `keeps`, what the game does without it).
static void *read_served(const char *rel, Overlay **base, size_t *n, const char *keeps) {
@@ -829,17 +845,55 @@ static void restbl_overlay(void) {
members, packs, (now_s() - t0) * 1000);
}
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
// base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS original; the grown table is
// written to <profile>/resolution/ and served in the base's place.
// The HeapDef table a bigger game resolution or TOTK_TEX_HEAP_EXTRA_MB needs (resolution.c): its texture heaps and
// their parents grow. The base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS
// original; the grown table is written to <profile>/resolution/ and served in the base's place. A served mod table
// whose own TextureHeap is grown (an HD texture pack's, resolution.c res_heapdef_tex_grown) is not the base: its
// growth is already in the amount (resolution.c mod_tex_heap_kib). In its place the table of the highest switched-on
// mod below it that changes other heaps only grows by the amount (plain_mod_heapdef: with the pack above The Second
// Quest, the default order for HDPacks.com's "4kTextures", the game's own sizes for RSDB and UI were too small for
// that mod's files and the game crashed loading a save), else the RomFS original; either is served in the pack's
// place. A mod table that changes other heaps only and is served itself stays the base.
#define HEAPDEF "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
// The HeapDef table of the highest switched-on mod that changes other heaps only (malloc'd, *n, *who its name); NULL
// when no mod has one. Mods above the served table ship none, so the first found is the highest below it.
static void *plain_mod_heapdef(size_t *n, const char **who) {
for (int i = 0; i < mods_count(); i++) {
const ModInfo *m = mods_at(i);
char path[2300];
if (!m->enabled || !m->romfs[0] ||
snprintf(path, sizeof path, "%s/%s", m->romfs, HEAPDEF) >= (int)sizeof path)
continue;
uint64_t fn = 0;
uint8_t *f = plat_file_read(path, &fn);
if (!f) continue;
if (fn < (16u << 20) && res_heapdef_plain(f, (size_t)fn)) {
*n = (size_t)fn;
*who = m->name;
return f;
}
free(f);
}
return NULL;
}
static void heapdef_overlay(void) {
if (!res_heap_extra_kib()) return;
if (!res_heap_extra_kib() && !res_tex_heap_extra_bytes()) return;
static const char keeps[] = "the game keeps its own resolution";
Overlay *base;
size_t n;
void *in = read_served(HEAPDEF, &base, &n, keeps), *out = NULL;
size_t out_n = 0;
if (in && base && base->mod && res_heapdef_tex_grown(in, n)) {
free(in);
const char *who = NULL;
if ((in = plain_mod_heapdef(&n, &who)))
rt_log("RomFS: %s: the HeapDef of %s with the texture heap growth in place of the mod's table",
mods_at(base->mod - 1)->name, who);
else if ((in = read_original(HEAPDEF, &n, keeps)))
rt_log("RomFS: %s: the game's own HeapDef with the mod's texture heap growth in place of the mod's table",
mods_at(base->mod - 1)->name);
}
int ok = in && res_heapdef_build(in, n, &out, &out_n);
free(in);
if (ok && serve_generated(HEAPDEF, base, out, out_n, "resolution", "grown heaps", keeps)) res_heapdef_served();
@@ -1026,6 +1080,19 @@ static void index_path(char *out, size_t cap) {
strcpy(out + k, "romfs-index.bin");
}
// 1 when the game resolution w x h has the UI layout of its shape: romfs-patches-<w>x<h> next to romfs-index.bin with a
// manifest, the folder overlay_init looks for. Works before fs_init: index_dir is not set that early (res_init and
// resolution.c decide run first), so the index path is worked out again here.
int romfs_shaped_ready(unsigned w, unsigned h) {
char dir[2100], path[2200];
index_path(dir, sizeof dir);
char *sep = plat_last_sep(dir);
if (sep) *sep = 0;
else strcpy(dir, ".");
return snprintf(path, sizeof path, "%s/romfs-patches-%ux%u/manifest.tsv", dir, w, h) < (int)sizeof path &&
plat_file_exists(path);
}
static void load_index(void) {
char path[2100];
index_path(path, sizeof path);
+37
View File
@@ -0,0 +1,37 @@
#version 450
// Block-linear to linear copy of one texture level and layer on the GPU (vk_deswizzle.c), the same layout as
// tex_deswizzle.c nvn_bl_offset: GOB = 64 bytes x 8 rows; a block is 1 GOB wide, 2^th GOBs high and 2^td deep, its
// GOBs ordered y first, then z; blocks fill the level x first, then y, then z.
// One invocation per 16-byte segment of the block-linear storage, in storage order, so the reads (from the pool's
// imported buffer: guest memory, over PCIe on a discrete GPU) are contiguous; the writes go to a device-local buffer
// with rows of pitch16 segments. Segments outside the level's rows, slices or row width are padding and skipped.
layout(local_size_x = 64) in;
layout(std430, set = 0, binding = 0) readonly buffer Src { uvec4 src[]; };
layout(std430, set = 0, binding = 1) writeonly buffer Dst { uvec4 dst[]; };
layout(push_constant) uniform Push {
uint src_base; // first segment of the level and layer in the src binding
uint dst_base; // first segment of its linear copy in the dst binding
uint segs; // segments to visit: whole blocks covering the level
uint pitch16; // segments per linear row (row bytes rounded up to 16)
uint rows, slices, th, td;
uint gobs_x, blocks_y;
uint nx; // workgroups along x (the dispatch is 2D when one dimension is not enough)
} pc;
void main() {
uint s = (gl_WorkGroupID.y * pc.nx + gl_WorkGroupID.x) * 64u + gl_LocalInvocationID.x;
if (s >= pc.segs) return;
uint bh = 1u << pc.th, bd = 1u << pc.td;
uint per_block = 32u << (pc.th + pc.td); // 512-byte GOBs of 32 segments each
uint block = s / per_block, inb = s % per_block;
uint gob = inb >> 5u, ing = inb & 31u;
uint per_layer = pc.gobs_x * pc.blocks_y;
uint bz = block / per_layer, rem = block % per_layer;
uint by = rem / pc.gobs_x, bx = rem % pc.gobs_x;
// Inside a GOB, segment bits: 4 = x % 64 / 32, 3..2 = y % 8 / 2, 1 = x % 32 / 16, 0 = y % 2.
uint xseg = bx * 4u + ((ing >> 4u) & 1u) * 2u + ((ing >> 1u) & 1u);
uint y = by * 8u * bh + (gob % bh) * 8u + ((ing >> 2u) & 3u) * 2u + (ing & 1u);
uint z = bz * bd + gob / bh;
if (xseg >= pc.pitch16 || y >= pc.rows || z >= pc.slices) return;
dst[pc.dst_base + (z * pc.rows + y) * pc.pitch16 + xseg] = src[pc.src_base + s];
}
+1
View File
@@ -216,6 +216,7 @@ static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
rt_log("guest ExitProcess at %s; exiting", ldr_describe(pc, at, sizeof at));
fs_save_flush();
vk_pipeline_save();
playtime_save();
prof_write();
fflush(NULL);
if (svc_test_exit_hook) return svc_test_exit_hook(0);
+17 -2
View File
@@ -57,6 +57,7 @@ extern const int rt_game_version;
extern const char rt_port_version[];
void cheats_frame(uint64_t frame); // cheats.c
bool tab_cheats(float scale); // ui/cheats_tab.cpp; true: restart into the rebuild
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c
}
// Roboto Medium (Apache-2.0, ui/fonts/LICENSE-Roboto.txt) with Latin and Cyrillic glyphs.
@@ -698,6 +699,19 @@ static void tab_screen(float scale) {
opt_end();
}
// Play time across runs (playtime.c), read only. Hidden while it is not counted (no profile directory).
static void row_play_time(void) {
uint64_t total, session;
if (!playtime_get(&total, &session)) return;
char value[64], run[64], note[96];
snprintf(value, sizeof value, menu_text(UI_MENU_PLAY_TIME_HM), (unsigned long long)(total / 3600),
(unsigned)(total / 60 % 60));
snprintf(run, sizeof run, menu_text(UI_MENU_PLAY_TIME_HM), (unsigned long long)(session / 3600),
(unsigned)(session / 60 % 60));
snprintf(note, sizeof note, menu_text(UI_MENU_PLAY_TIME_SESSION), run);
opt_text_row("##play_time", menu_text(UI_MENU_PLAY_TIME), value, note, menu_text(UI_MENU_PLAY_TIME_DESC), false);
}
static void tab_game(float scale) {
opt_begin("##game", scale);
row_fps_limit();
@@ -706,6 +720,7 @@ static void tab_game(float scale) {
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
opt_toggle("TOTK_ASSUME_FOCUS", UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC);
row_play_time();
opt_end();
}
@@ -1094,8 +1109,8 @@ static void tab_mods(float scale) {
char code[512] = "", problem[512] = "";
if (m->nsets && m->overrides[0])
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_REPLACES_BUILTIN), m->nsets, m->overrides);
else if (m->nsets)
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets);
else if (m->nsets || m->ndata)
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets + m->ndata);
switch (m->problem) {
case MOD_EMPTY: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EMPTY)); break;
case MOD_PLUGIN: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_SUBSDK)); break;
+23
View File
@@ -192,6 +192,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
"window loses focus, as on the Switch when Home is pressed; a load interrupted that "
"way can stop for good.",
[UI_MENU_PLAY_TIME] = "Play time",
[UI_MENU_PLAY_TIME_DESC] = "Time in the game over all runs, counted while the game window has the focus and the game "
"draws frames. Kept in playtime.txt in the profile folder, next to the saves.",
[UI_MENU_PLAY_TIME_HM] = "%llu h %02u min", [UI_MENU_PLAY_TIME_SESSION] = "this run: %s",
[UI_MENU_MODS_BUILTIN] = "Built into the port", [UI_MENU_MODS_INSTALLED] = "Installed mods",
[UI_MENU_MODS_NONE] = "No mods yet. Unpack a mod into the mods folder, one folder per mod with its romfs folder "
"inside, and restart the game. TKMM can export its merged mods into a folder there too.",
@@ -662,6 +666,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
"когда окно теряет фокус, как на Switch при нажатии Home; прерванная так загрузка "
"может остановиться насовсем.",
[UI_MENU_PLAY_TIME] = "Время в игре",
[UI_MENU_PLAY_TIME_DESC] = "Время в игре за все запуски. Считается, пока окно игры в фокусе и игра рисует кадры. "
"Хранится в playtime.txt в папке профиля, рядом с сохранениями.",
[UI_MENU_PLAY_TIME_HM] = "%llu ч %02u мин", [UI_MENU_PLAY_TIME_SESSION] = "в этом запуске: %s",
[UI_MENU_MODS_BUILTIN] = "Встроенные в порт", [UI_MENU_MODS_INSTALLED] = "Установленные моды",
[UI_MENU_MODS_NONE] = "Модов пока нет. Распакуйте мод в папку mods, каждый мод в свою папку с папкой romfs внутри, и "
"перезапустите игру. TKMM тоже может выгрузить туда объединённые моды отдельной папкой.",
@@ -983,6 +991,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_TAB_SCREEN] = "Tela", [UI_MENU_TAB_GAME] = "Jogo", [UI_MENU_TAB_SOUND] = "Som",
[UI_MENU_TAB_CONTROLS] = "Controles", [UI_MENU_TAB_AMIIBO] = "Amiibo", [UI_MENU_TAB_MENU] = "Menu", [UI_MENU_TAB_ADVANCED] = "Avançado",
[UI_MENU_TAB_MODS] = "Mods",
[UI_MENU_PLAY_TIME] = "Tempo de jogo",
[UI_MENU_PLAY_TIME_DESC] = "Tempo no jogo somando todas as sessões, contado enquanto a janela do jogo está em foco e o "
"jogo desenha quadros. Fica em playtime.txt na pasta do perfil, junto dos saves.",
[UI_MENU_PLAY_TIME_HM] = "%llu h %02u min", [UI_MENU_PLAY_TIME_SESSION] = "nesta sessão: %s",
[UI_MENU_MODS_BUILTIN] = "Embutidos no port", [UI_MENU_MODS_INSTALLED] = "Mods instalados",
[UI_MENU_MODS_NONE] = "Nenhum mod ainda. Descompacte um mod na pasta mods, uma pasta por mod com a pasta romfs dentro, "
"e reinicie o jogo. O TKMM também pode exportar os mods mesclados para uma pasta ali.",
@@ -1088,6 +1100,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_MODE_DOCKED] = "Modo TV", [UI_MENU_MODE_HANDHELD] = "Modo portátil",
[UI_MENU_VOLUME] = "Volume", [UI_MENU_VOLUME_DESC] = "Volume geral do jogo.",
[UI_MENU_AUDIO_CHANNELS] = "Alto-falantes",
[UI_MENU_AUDIO_CHANNELS_DESC] = "Saída estéreo ou surround 5.1. Automático escolhe 5.1 quando a configuração de "
"alto-falantes do Windows tem seis canais ou mais. Vale após reiniciar.",
[UI_MENU_CHANNELS_AUTO] = "Automático", [UI_MENU_CHANNELS_STEREO] = "Estéreo", [UI_MENU_CHANNELS_51] = "Surround 5.1",
[UI_MENU_MOTION] = "Controles de movimento", [UI_MENU_MOTION_DESC] = "Mirar com o giroscópio do controle.",
[UI_MENU_RUMBLE_STRENGTH] = "Intensidade da vibração",
@@ -1492,6 +1508,10 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_TAB_MENU] = "菜单",
[UI_MENU_TAB_ADVANCED] = "高级",
[UI_MENU_TAB_MODS] = "模组",
[UI_MENU_PLAY_TIME] = "游戏时间",
[UI_MENU_PLAY_TIME_DESC] = "所有运行累计的游戏时间,仅在游戏窗口处于焦点且游戏正在绘制画面时计时。"
"保存在配置文件夹的 playtime.txt 中,与存档放在一起。",
[UI_MENU_PLAY_TIME_HM] = "%llu 小时 %02u 分钟", [UI_MENU_PLAY_TIME_SESSION] = "本次运行:%s",
[UI_MENU_MODS_BUILTIN] = "移植版内置", [UI_MENU_MODS_INSTALLED] = "已安装的模组",
[UI_MENU_MODS_NONE] = "还没有模组。把模组解压到 mods 文件夹,每个模组一个文件夹,里面放 romfs 文件夹,然后重启游戏。"
"TKMM 也可以把合并后的模组导出到那里的一个文件夹。",
@@ -1631,6 +1651,9 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
"这样被打断的读取可能会永久卡住。",
[UI_MENU_VOLUME] = "音量",
[UI_MENU_VOLUME_DESC] = "游戏的主音量。",
[UI_MENU_AUDIO_CHANNELS] = "扬声器",
[UI_MENU_AUDIO_CHANNELS_DESC] = "立体声或 5.1 环绕声输出。选择自动时,若 Windows 扬声器设置有六个或更多声道则使用 5.1。重启后生效。",
[UI_MENU_CHANNELS_AUTO] = "自动", [UI_MENU_CHANNELS_STEREO] = "立体声", [UI_MENU_CHANNELS_51] = "5.1 环绕声",
[UI_MENU_MOTION] = "体感操作",
[UI_MENU_MOTION_DESC] = "用手柄的陀螺仪瞄准。",
[UI_MENU_RUMBLE_STRENGTH] = "振动强度",
+3
View File
@@ -70,6 +70,9 @@ typedef enum {
UI_MENU_GAME_LANG, UI_MENU_GAME_LANG_DESC, UI_MENU_GAME_LANG_AUTO,
UI_MENU_OPERATION_MODE, UI_MENU_OPERATION_MODE_DESC, UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD,
UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC,
UI_MENU_PLAY_TIME, UI_MENU_PLAY_TIME_DESC,
UI_MENU_PLAY_TIME_HM, // hours and minutes: "%llu h %02u min" (unsigned long long hours, unsigned minutes)
UI_MENU_PLAY_TIME_SESSION, // note under the total: "this run: %s" (the hours and minutes above)
// Mods: the port's own patches and the player's mods folder (mods.c)
UI_MENU_MODS_BUILTIN, UI_MENU_MODS_INSTALLED,
+48 -3
View File
@@ -329,6 +329,15 @@ void vk_defer_image(VkImage img, VmaAllocation alloc); // alloc NULL: vkDestro
void vk_defer_view(VkImageView view);
void vk_exec_image_gone(VkImage image); // destroy_tex, texture lock held: the replay's views of image go with it
void vk_defer_buffer(VkBuffer buf, VmaAllocation alloc);
// free(p) once the GPU is past the submission being recorded, like the deferrals above: host records that a recording
// may still read through a pointer taken before another thread dropped them (vk_texture.c destroy_tex).
void vk_defer_free(void *p);
// Device-local scratch for the upload batch (storage, transfer src and dst), from a pool of its own (vk_memory.c).
VkResult vk_scratch_buffer(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc);
// Staging and scratch bytes the open upload batch holds until it completes. Large texture loads submit the batch early
// (vk_texture.c UPLOAD_HELD_MAX) so the GPU starts on it and its buffers come back before the pools need new blocks.
VkDeviceSize vk_upload_held(void);
void vk_upload_pools_init(void); // vk_device.c, after the allocator: the staging and scratch pools and their blocks
// ---- ASTC (vk_astc.c). TOTK_ASTC = auto | gpu | native | cpu | check; see vk_astc.c.
enum { VK_ASTC_CPU, VK_ASTC_GPU, VK_ASTC_NATIVE, VK_ASTC_CHECK };
int vk_astc_mode(void); // decided once per device (logged); VK_ASTC_CPU before vk_init
@@ -352,7 +361,31 @@ void vk_astc_time_enable(int on);
int vk_astc_time_get(uint32_t bw, uint32_t bh, uint64_t *texels, uint64_t *calls, double *us); // totals so far
void vk_astc_time_summary(void); // logs the totals
void vk_astc_release(void); // vk_device.c, before the device goes: frees the decoder's objects
void vk_mem_note_submit(uint64_t timeline_value); // vk_sync.c: after each submit that signals ctx->timeline with this value
// ---- Block-linear deswizzle on the GPU (vk_deswizzle.c). TOTK_DESWIZZLE = auto | gpu | cpu | check.
enum { VK_DESWZ_CPU, VK_DESWZ_GPU, VK_DESWZ_CHECK };
int vk_deswz_mode(void); // decided once per device (logged); VK_DESWZ_CPU before vk_init
// One level and layer: its storage starts at byte src of the bound source range and covers segs 16-byte segments
// (whole blocks, cut at the storage end); its linear copy goes to byte dst of the destination, rows of pitch16 segments,
// rows x slices of them. th, td, gobs_x, blocks_y: as tex_deswizzle.c nvn_bl_offset.
typedef struct {
VkDeviceSize src, dst;
uint32_t segs, pitch16, rows, slices, th, td, gobs_x, blocks_y;
} VkDeswzRegion;
// Records into cb a dispatch per region reading [src_off, src_off + src_size) of src (a storage buffer) and writing
// dst (device-local, dst_size bytes). The caller records the barriers around it. 0 when nothing was recorded: no
// pipeline, src_off not 16-byte aligned, a range over the device's storage-buffer limit, a region outside the ranges.
int vk_deswz_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_off, VkDeviceSize src_size, VkBuffer dst,
VkDeviceSize dst_size, const VkDeswzRegion *rg, int n);
// 1 when vk_deswz_record will take these regions (their dst offsets aside) with destinations of up to max_dst bytes.
int vk_deswz_fits(VkDeviceSize src_off, VkDeviceSize src_size, const VkDeswzRegion *rg, int n, VkDeviceSize max_dst);
void vk_deswz_release(void); // vk_device.c, before the device goes
// 1 when sp (from vk_mem_resolve) is an imported pool's buffer over guest memory itself, so a GPU read in a submission
// sees every CPU write made before the submit; 0 for device memory and shadow storage (synced after the upload batch).
int vk_mem_span_live(const VkMemSpan *sp);
// vk_sync.c, when it starts recording the submission that will signal ctx->timeline with this value: objects destroyed
// from then on are freed once the timeline reaches it.
void vk_mem_note_fence(uint64_t timeline_value);
void vk_mem_fault_log(void); // vk_device_fault_report: the deferred-free state, lock-free
void vk_mem_collect(void); // frees what has completed
typedef struct { uint64_t imported_pools, physical_pools, virtual_pools, import_failures, upload_batches; } VkMemStats;
void vk_mem_stats(VkMemStats *s);
@@ -384,6 +417,9 @@ typedef struct {
uint8_t scale; // 1, or TOTK_SHADOW_SCALE for a shadow map made larger than the game's (vk_texture.c
// shadow_scale_of): width and height above are the larger ones, the game's regions
// are multiplied by it
uint8_t gone; // the entry was dropped (vk_texture.c destroy_tex, under the texture lock): image and
// record live until the submission being recorded is done, and nothing may be cached
// by its image handle, which the driver can hand out again after that
} VkTexImage;
// The window texture the backend presents (vk_backend.c): *image and *format become the float picture the game copied
// into it, when the last draw into it was that copy and the swapchain is HDR10.
@@ -428,6 +464,7 @@ void vk_texture_stats(uint64_t *images, uint64_t *uploads, uint64_t *reuploads);
// (images not bound into pool memory).
typedef struct { uint64_t live, released, own_live, own_released, bytes_live, bytes_released; } VkTexCensus;
void vk_texture_census(VkTexCensus *s);
void vk_texture_released_log(uint64_t frame); // mem: released lines of vram_log
void vk_texture_shutdown(void); // vk_mem_shutdown calls it
void vk_texture_new_submit(void); // a queue submit starts: draw bindings check storage again
uint32_t vk_texture_epoch(void); // the write-watch snapshot epoch vk_texture_new_submit started
@@ -504,6 +541,7 @@ void vk_pipe_state_defaults(VkPipeState *s); // NVN defaults of every state ob
enum { VK_PIPE_SUPPORT = GFX_RES_KINDS }; // VkPipeBinding.kind of the support buffer
#define VK_PIPE_ZERO_BINDING 16 // vertex binding of shader inputs without an attribute: bind a zeroed
// buffer of at least 16 bytes there (stride 0)
#define VK_PIPE_ALL_BINDINGS 0x1ffffu // every vertex binding, streams 0-15 and the zero binding
typedef struct {
uint16_t binding; // in its set
uint8_t stage; // GFX_STAGE_* that declared it (support buffer: the first stage)
@@ -556,8 +594,15 @@ VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info);
// has them dynamic, depth clamp, polygon mode, blend enables, equations, write masks and alpha to coverage. The replay
// sets the rest: vkCmdSetViewportWithCount, vkCmdSetScissorWithCount, depth bias, blend constants, stencil compare and
// write masks and references; vertex buffers with vkCmdBindVertexBuffers2 (strides NULL when
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream).
void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream). Returns the vertex bindings (bit per binding) the
// state's vertex input reads, whether set here or made into the pipeline.
uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
// The replay's variant (one thread, TOTK_DYN_DELTA): valid 1 says every graphics pipeline bound in cb since the last call
// was one of vk_pipeline_graphics*'s, which all declare the same dynamic states, and nothing else set them, so cb still
// holds what that call recorded (Vulkan keeps dynamic state across such binds for the life of the command buffer);
// then only the commands whose values differ are recorded. valid 0 (a new command buffer, or one whose state another
// recorder changed) records everything. *cmds: the commands recorded (valid 0: 1). Returns as vk_pipeline_set_dynamic.
uint32_t vk_pipeline_set_dynamic_delta(VkCommandBuffer cb, const VkPipeState *s, int valid, uint32_t *cmds);
int vk_pipeline_vertex_input_dynamic(void); // 1: vertex input is VK_EXT_vertex_input_dynamic_state
// Translator answers from the bound state: attr_type from the vertex attribute formats (vs), frag_out_type from the
// colour target formats (fs). Other fields are left as they are; either pointer may be NULL.
+1 -5
View File
@@ -463,13 +463,9 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
cap_in = (cap_in + al - 1) / al * al;
cap_out = (cap_out + al - 1) / al * al;
if (!bc) cap_bc = 0;
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = cap_in + cap_out + cap_bc,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
VkBuffer sb;
VmaAllocation sa;
if (vmaCreateBuffer(c->vma, &bi, &aci, &sb, &sa, NULL) != VK_SUCCESS) {
if (vk_scratch_buffer(cap_in + cap_out + cap_bc, &sb, &sa) != VK_SUCCESS) {
if (plat_inc32(&n_fail) <= 16)
rt_log("vk: ASTC scratch buffer of %llu bytes failed: texture left undecoded",
(unsigned long long)(cap_in + cap_out));
+176
View File
@@ -0,0 +1,176 @@
// Block-linear textures deswizzled by the GPU (runtime/shaders/bl_deswizzle.comp): vk_texture.c upload binds the
// texture's storage in its pool's imported buffer and lets a compute pass write the linear copy that a buffer-to-image
// copy (or the ASTC decoder) then takes, instead of tex_deswizzle.c walking every 16 bytes on the replay thread
// (50-236 ms frames on entering new areas, 2026-11-07 play sessions). The same design as MacNativeGames' TotK port
// (d2r_gfx nvn_mtl.m: deswizzle from the pool at GPU execution time), written for Vulkan from our CPU layout code.
//
// TOTK_DESWIZZLE = auto (default) / gpu: the compute pass where the storage is an imported pool (vk_mem_span_live),
// the CPU otherwise; cpu: always the CPU; check: gpu, and every upload is also deswizzled on the CPU and compared
// (waits for the GPU; for testing only).
#include <stdlib.h>
#include <string.h>
#include "bl_deswizzle_spv.h"
#include "cfg.h"
#include "host.h"
#include "vk.h"
#include "vk_rec.h"
#include "platform/platform.h"
typedef struct {
uint32_t src_base, dst_base, segs, pitch16, rows, slices, th, td, gobs_x, blocks_y, nx;
} Push;
static PlatMutex g_lock = PLAT_MUTEX_INIT;
static struct {
VkDevice dev;
int mode; // VK_DESWZ_*; -1 before the first vk_deswz_mode with a device
VkDescriptorSetLayout dsl;
VkPipelineLayout layout;
VkPipeline pipe;
PFN_vkCmdPushDescriptorSetKHR push;
} G = {.mode = -1};
static volatile int64_t n_regions, n_bytes;
static int create_pipeline(VkCtx *c) {
G.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(c->device, "vkCmdPushDescriptorSetKHR");
if (!G.push) return 0;
VkDescriptorSetLayoutBinding b[2];
for (int i = 0; i < 2; i++)
b[i] = (VkDescriptorSetLayoutBinding){.binding = (uint32_t)i, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT};
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
.bindingCount = 2, .pBindings = b};
if (vkCreateDescriptorSetLayout(c->device, &dci, NULL, &G.dsl) != VK_SUCCESS) return 0;
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push)};
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
.pSetLayouts = &G.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
if (vkCreatePipelineLayout(c->device, &li, NULL, &G.layout) != VK_SUCCESS) return 0;
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof bl_deswizzle_spv,
.pCode = bl_deswizzle_spv};
VkShaderModule m = VK_NULL_HANDLE;
if (vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) return 0;
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
.layout = G.layout};
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &G.pipe);
vkDestroyShaderModule(c->device, m, NULL);
if (r != VK_SUCCESS) G.pipe = VK_NULL_HANDLE;
return r == VK_SUCCESS;
}
int vk_deswz_mode(void) {
VkCtx *c = vk_ctx();
if (!c) return VK_DESWZ_CPU;
plat_mutex_lock_shared(&g_lock);
int m = G.dev == c->device ? G.mode : -1;
plat_mutex_unlock_shared(&g_lock);
if (m >= 0) return m;
plat_mutex_lock(&g_lock);
if (G.dev != c->device) { // the old device's objects went with it (probes recreate the device)
memset(&G, 0, sizeof G);
G.dev = c->device;
G.mode = -1;
}
if (G.mode < 0) {
const char *e = cfg_get("TOTK_DESWIZZLE");
if (!e || !*e) e = "auto";
int want = !strcmp(e, "cpu") || !strcmp(e, "0") ? VK_DESWZ_CPU : !strcmp(e, "check") ? VK_DESWZ_CHECK : VK_DESWZ_GPU;
if (strcmp(e, "auto") && strcmp(e, "gpu") && strcmp(e, "1") && strcmp(e, "cpu") && strcmp(e, "0") &&
strcmp(e, "check"))
rt_log("vk: TOTK_DESWIZZLE=%s is not auto, gpu, cpu or check: auto", e);
if (want != VK_DESWZ_CPU && !create_pipeline(c)) {
rt_log("vk: the deswizzle compute pass is unavailable on %s: textures deswizzle on the CPU", c->props.deviceName);
want = VK_DESWZ_CPU;
}
G.mode = want;
rt_log("vk: block-linear textures deswizzle on the %s%s", want == VK_DESWZ_CPU ? "CPU" : "GPU where the storage is "
"an imported pool", want == VK_DESWZ_CHECK ? ", checked against the CPU (TOTK_DESWIZZLE=check)" : "");
}
m = G.mode;
plat_mutex_unlock(&g_lock);
return m;
}
static uint32_t max_groups_x(const VkCtx *c) {
uint32_t m = c->props.limits.maxComputeWorkGroupCount[0];
return m > 65535 ? 65535 : m;
}
// The bound source range starts at the alignment below src_off.
static VkDeviceSize src_lo(const VkCtx *c, VkDeviceSize src_off) {
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment;
if (al < 16) al = 16;
return src_off / al * al;
}
int vk_deswz_fits(VkDeviceSize src_off, VkDeviceSize src_size, const VkDeswzRegion *rg, int n, VkDeviceSize max_dst) {
VkCtx *c = vk_ctx();
if (!c || !n || (src_off & 15) || vk_deswz_mode() == VK_DESWZ_CPU) return 0;
VkDeviceSize range = src_off + src_size - src_lo(c, src_off), lim = c->props.limits.maxStorageBufferRange;
if (range > lim || max_dst > lim) return 0;
uint32_t maxg = max_groups_x(c);
for (int i = 0; i < n; i++) { // the 32-bit segment indices of the shader stay inside the bindings
uint64_t groups = ((uint64_t)rg[i].segs + 63) / 64, out = (uint64_t)rg[i].pitch16 * rg[i].rows * rg[i].slices;
if ((rg[i].src & 15) || rg[i].src + (uint64_t)rg[i].segs * 16 > src_size || !rg[i].segs ||
(src_off - src_lo(c, src_off) + rg[i].src) / 16 + rg[i].segs > UINT32_MAX || out > UINT32_MAX ||
out * 16 > max_dst || (groups + maxg - 1) / maxg > c->props.limits.maxComputeWorkGroupCount[1])
return 0;
}
return 1;
}
int vk_deswz_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_off, VkDeviceSize src_size, VkBuffer dst,
VkDeviceSize dst_size, const VkDeswzRegion *rg, int n) {
VkCtx *c = vk_ctx();
if (!vk_deswz_fits(src_off, src_size, rg, n, dst_size)) return 0;
for (int i = 0; i < n; i++)
if ((rg[i].dst & 15) || rg[i].dst + (uint64_t)rg[i].pitch16 * 16 * rg[i].rows * rg[i].slices > dst_size) return 0;
plat_mutex_lock_shared(&g_lock);
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE;
plat_mutex_unlock_shared(&g_lock);
if (!pipe) return 0;
VkDeviceSize lo = src_lo(c, src_off), range = src_off + src_size - lo;
uint32_t maxg = max_groups_x(c);
VkDescriptorBufferInfo dbi[2] = {{src, lo, range}, {dst, 0, dst_size}};
VkWriteDescriptorSet w[2];
for (int i = 0; i < 2; i++)
w[i] = (VkWriteDescriptorSet){.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = (uint32_t)i,
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.pBufferInfo = &dbi[i]};
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
rec_push(G.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, G.layout, 0, 2, w);
uint64_t bytes = 0;
for (int i = 0; i < n; i++) {
const VkDeswzRegion *r = &rg[i];
uint64_t groups = ((uint64_t)r->segs + 63) / 64;
uint32_t nx = groups < maxg ? (uint32_t)groups : maxg, ny = (uint32_t)((groups + nx - 1) / nx);
Push pc = {.src_base = (uint32_t)((src_off - lo + r->src) / 16), .dst_base = (uint32_t)(r->dst / 16),
.segs = r->segs, .pitch16 = r->pitch16, .rows = r->rows, .slices = r->slices, .th = r->th, .td = r->td,
.gobs_x = r->gobs_x, .blocks_y = r->blocks_y, .nx = nx};
vkCmdPushConstants(cb, G.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
vkCmdDispatch(cb, nx, ny, 1);
bytes += (uint64_t)r->segs * 16;
}
int64_t before = plat_add64(&n_regions, n) - n;
plat_add64(&n_bytes, (int64_t)bytes);
if (!before || (before + n) / 4096 != before / 4096)
rt_log("vk: deswizzle: %lld regions, %.1f MiB of block-linear storage deswizzled on the GPU so far",
(long long)(before + n), (double)n_bytes / 1048576.0);
return 1;
}
void vk_deswz_release(void) {
VkCtx *c = vk_ctx();
plat_mutex_lock(&g_lock);
if (c && G.dev == c->device) {
if (G.pipe) vkDestroyPipeline(c->device, G.pipe, NULL);
if (G.layout) vkDestroyPipelineLayout(c->device, G.layout, NULL);
if (G.dsl) vkDestroyDescriptorSetLayout(c->device, G.dsl, NULL);
}
memset(&G, 0, sizeof G);
G.mode = -1;
plat_mutex_unlock(&g_lock);
}
+90 -9
View File
@@ -16,9 +16,69 @@ static VkCtx g_vk;
static int g_ready;
static PlatMutex g_lock = PLAT_MUTEX_INIT;
// ---- GPU address map for device faults
// VK_EXT_device_address_binding_report: the driver reports each bind and unbind of an object's GPU virtual range through
// the debug messenger. The newest AB_N events stay in a ring written without a lock (one atomic add each), so a device
// fault names the object behind its address and whether it was unbound before the fault, which is memory freed while
// a submission still used it. One event per image, buffer or memory block made or freed, none per draw.
// TOTK_ADDR_REPORT=0 turns it off.
#define AB_N (1u << 15)
typedef struct { uint64_t seq, base, size, handle, issued, done; int32_t type; uint32_t unbind; } AbEvent;
static AbEvent *ab_ring;
static uint64_t ab_head;
static int ab_want; // the instance has the messenger for it (create_instance)
static void ab_record(const VkDebugUtilsMessengerCallbackDataEXT *d) {
const VkDeviceAddressBindingCallbackDataEXT *b = NULL;
for (const VkBaseInStructure *p = d->pNext; p; p = p->pNext)
if (p->sType == VK_STRUCTURE_TYPE_DEVICE_ADDRESS_BINDING_CALLBACK_DATA_EXT) b = (const void *)p;
if (!b || !ab_ring) return;
uint64_t i = __atomic_fetch_add(&ab_head, 1, __ATOMIC_RELAXED);
AbEvent *e = &ab_ring[i & (AB_N - 1)];
__atomic_store_n(&e->seq, 0, __ATOMIC_RELAXED);
e->base = b->baseAddress, e->size = b->size;
e->handle = d->objectCount ? d->pObjects[0].objectHandle : 0;
e->type = d->objectCount ? (int32_t)d->pObjects[0].objectType : 0;
e->unbind = b->bindingType == VK_DEVICE_ADDRESS_BINDING_TYPE_UNBIND_EXT;
e->issued = vk_sync_issued(), e->done = vk_sync_fence_done();
__atomic_store_n(&e->seq, i + 1, __ATOMIC_RELEASE);
}
static const char *ab_type_name(int32_t t, char *buf, size_t n) {
switch (t) {
case VK_OBJECT_TYPE_IMAGE: return "image";
case VK_OBJECT_TYPE_BUFFER: return "buffer";
case VK_OBJECT_TYPE_DEVICE_MEMORY: return "memory";
default: snprintf(buf, n, "object type %d", (int)t); return buf;
}
}
// The newest events whose range meets the precision-aligned range around a fault address, newest first.
static void ab_report(uint64_t addr, uint64_t precision) {
if (!ab_ring) return;
uint64_t lo = precision > 1 ? addr & ~(precision - 1) : addr, hi = lo + (precision > 1 ? precision : 1);
uint64_t head = __atomic_load_n(&ab_head, __ATOMIC_ACQUIRE), n = head < AB_N ? head : AB_N;
int found = 0;
for (uint64_t k = 1; k <= n && found < 6; k++) {
const AbEvent *e = &ab_ring[(head - k) & (AB_N - 1)];
if (__atomic_load_n(&e->seq, __ATOMIC_ACQUIRE) != head - k + 1) continue;
if (e->base >= hi || e->base + e->size <= lo) continue;
found++;
char tn[32];
rt_log("vk: fault address %#llx: %s %#llx [%#llx, +%#llx] %s %llu address events before the fault, "
"submitted %llu, done %llu", (unsigned long long)addr, ab_type_name(e->type, tn, sizeof tn),(unsigned long long)e->handle,
(unsigned long long)e->base, (unsigned long long)e->size, e->unbind ? "unbound" : "bound",
(unsigned long long)k, (unsigned long long)e->issued, (unsigned long long)e->done);
}
if (!found) rt_log("vk: fault address %#llx: no binding in the last %llu address events", (unsigned long long)addr,
(unsigned long long)n);
}
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_cb(VkDebugUtilsMessageSeverityFlagBitsEXT sev, VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT *d, void *user) {
(void)type, (void)user;
(void)user;
if (type & VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT) { ab_record(d); return VK_FALSE; }
if (sev < VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) return VK_FALSE; // info, asked for the address events
int err = (sev & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0;
if (err) plat_inc32(&g_vk.validation_errors);
static volatile int32_t logged;
@@ -76,7 +136,10 @@ static int create_instance(void) {
rt_log("vk: TOTK_VK_VALIDATION set but %s is not installed; continuing without", layers[0]);
validate = 0;
}
if (validate) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
const char *ar = cfg_get("TOTK_ADDR_REPORT");
ab_want = !(ar && !strcmp(ar, "0")) && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
if (ab_want && !ab_ring && !(ab_ring = calloc(AB_N, sizeof *ab_ring))) ab_want = 0;
if (validate || ab_want) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME;
vk_dlaa_instance_exts(ext, &n_ext, 12);
@@ -89,16 +152,18 @@ static int create_instance(void) {
VkResult r = vkCreateInstance(&ci, NULL, &g_vk.instance);
if (r != VK_SUCCESS) { rt_log("vk: vkCreateInstance failed (%d); is a Vulkan 1.3 driver installed?", (int)r); return 0; }
g_vk.validation = validate;
if (validate) {
if (validate || ab_want) {
PFN_vkCreateDebugUtilsMessengerEXT f =
(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(g_vk.instance, "vkCreateDebugUtilsMessengerEXT");
VkDebugUtilsMessengerCreateInfoEXT mi = {
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
(ab_want ? VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT : 0),
.messageType = (validate ? VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT : 0) |
(ab_want ? VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT : 0),
.pfnUserCallback = debug_cb};
if (f) f(g_vk.instance, &mi, NULL, &g_vk.messenger);
if (!f || f(g_vk.instance, &mi, NULL, &g_vk.messenger) != VK_SUCCESS) ab_want = 0;
}
return 1;
}
@@ -217,6 +282,7 @@ typedef struct {
VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT gpl;
VkPhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT fbl;
VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR lr;
VkPhysicalDeviceAddressBindingReportFeaturesEXT abr;
} FeatureChain;
static int create_device(void) {
@@ -251,6 +317,13 @@ static int create_device(void) {
// Ryujinx's SPIR-V declares fragment shader interlock whenever the host reports it (vk_pipeline.c spirv_fixup).
OPTIONAL_EXT(has_fragment_shader_interlock, VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, fsi,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT);
// The GPU address map for device faults (ab_record), with the messenger made for it
int has_abr = 0;
if (ab_want && has_ext(ep, ne, VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME)) {
has_abr = 1;
ext[n_ext++] = VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME;
CHAIN(abr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ADDRESS_BINDING_REPORT_FEATURES_EXT);
}
// Feedback loops (vk.h VkCtx.feedback): the layout, and barriers inside dynamic rendering
int has_fbl = 0, has_lr = 0;
if (has_ext(ep, ne, VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME) &&
@@ -314,6 +387,10 @@ static int create_device(void) {
else if (!h->textureCompressionBC) miss = "textureCompressionBC";
if (miss) { rt_log("vk: required feature missing on %s: %s", g_vk.props.deviceName, miss); free(c); return 0; }
if (!c->fault.deviceFault) g_vk.has_device_fault = 0; // vkGetDeviceFaultInfoEXT needs the feature, not just the extension
rt_log("vk: GPU address map for device faults: %s",
has_abr && c->abr.reportAddressBinding ? "on (VK_EXT_device_address_binding_report; TOTK_ADDR_REPORT=0 turns it off)"
: !ab_want ? "off (TOTK_ADDR_REPORT=0 or no VK_EXT_debug_utils)"
: "unavailable (no VK_EXT_device_address_binding_report)");
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
{
@@ -376,6 +453,7 @@ static void destroy_all(void) {
gfx_window_release_vk();
if (g_vk.device) vkDeviceWaitIdle(g_vk.device);
if (g_vk.device) vk_astc_release();
if (g_vk.device) vk_deswz_release();
if (g_vk.device) vk_crumbs_release();
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
vk_headroom_stop();
@@ -397,7 +475,7 @@ int vk_init(void) {
plat_mutex_lock(&g_lock);
if (!g_ready) {
memset(&g_vk, 0, sizeof g_vk);
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_headroom_start(), vk_dlaa_init();
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
else destroy_all();
}
int ok = g_ready;
@@ -437,6 +515,7 @@ void vk_device_fault_report(VkResult r) {
done = 1;
vk_crumbs_report();
vk_pass_marks_report();
vk_mem_fault_log();
VkCtx *c = vk_ctx();
PFN_vkGetDeviceFaultInfoEXT get =
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
@@ -461,9 +540,11 @@ void vk_device_fault_report(VkResult r) {
rt_log("vk: device fault: \"%s\" (%u address infos, %u vendor infos, vendor binary %llu bytes)", info.description,
n.addressInfoCount, n.vendorInfoCount, (unsigned long long)binary);
// The faulting access lies in the precision-aligned range around the reported address.
for (uint32_t i = 0; i < n.addressInfoCount; i++)
for (uint32_t i = 0; i < n.addressInfoCount; i++) {
rt_log("vk: device fault address %u: %s at %#llx, precision %#llx", i, fault_address_type(ai[i].addressType),
(unsigned long long)ai[i].reportedAddress, (unsigned long long)ai[i].addressPrecision);
ab_report(ai[i].reportedAddress, ai[i].addressPrecision);
}
for (uint32_t i = 0; i < n.vendorInfoCount; i++)
rt_log("vk: device fault vendor info %u: \"%s\" code %#llx data %#llx", i, vi[i].description,
(unsigned long long)vi[i].vendorFaultCode, (unsigned long long)vi[i].vendorFaultData);
+7 -54
View File
@@ -11,9 +11,6 @@
// layout with a set-then-get round trip before anything relies on it.
//
// Where DLAA runs in the frame, the jitter and the motion vectors: see the sections below.
#ifdef _WIN32
#include <windows.h>
#endif
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@@ -115,31 +112,19 @@ int vk_dlaa_wanted(void) {
static int core_load(void) {
if (N.tried) return N.loaded;
N.tried = 1;
#ifdef _WIN32
char dir[MAX_PATH] = {0}, path[MAX_PATH + 32];
DWORD n = sizeof dir;
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", "FullPath", RRF_RT_REG_SZ,
NULL, dir, &n) != ERROR_SUCCESS) {
char path[PLAT_PATH_MAX];
if (!plat_ngx_core_path(path, sizeof path)) {
rt_log("dlaa: the driver has no NGX core (registry NGXCore missing): DLAA off");
return 0;
}
snprintf(path, sizeof path, "%s\\_nvngx.dll", dir);
N.core = LoadLibraryA(path);
if (!N.core) {
rt_log("dlaa: cannot load %s (error %lu): DLAA off", path, GetLastError());
return 0;
}
#define FN(f, name) N.f = (void *)GetProcAddress((HMODULE)N.core, name)
#else
// The driver's NGX core; the dynamic loader finds it where the driver put it (/usr/lib, /usr/lib/x86_64-linux-gnu).
const char *path = "libnvidia-ngx.so.1";
N.core = plat_lib_open(path);
if (!N.core) {
rt_log("dlaa: cannot load %s (not an NVIDIA driver, or one without NGX): DLAA off", path);
char why[256];
plat_lib_error(why, sizeof why);
rt_log("dlaa: cannot load %s (%s; not an NVIDIA driver, or one without NGX): DLAA off", path, why);
return 0;
}
#define FN(f, name) N.f = plat_lib_sym(N.core, name)
#endif
FN(init, "NVSDK_NGX_VULKAN_Init_Ext2");
FN(req_ext, "NVSDK_NGX_VULKAN_RequiredExtensions");
FN(caps, "NVSDK_NGX_VULKAN_GetCapabilityParameters");
@@ -203,46 +188,14 @@ static int vtable_ok(void) {
return ok;
}
#ifndef _WIN32
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
const unsigned char *u = (const unsigned char *)in;
size_t n = 0;
while (*u) {
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
if (more == 4) return 0;
if (more) c &= 0x3F >> more;
for (unsigned k = 0; k < more; k++, u++) {
if ((*u & 0xC0) != 0x80) return 0;
c = c << 6 | (*u & 0x3F);
}
if (n + 1 >= cap) return 0;
out[n++] = (wchar_t)c;
}
out[n] = 0;
return 1;
}
#endif
void vk_dlaa_init(void) {
if (!vk_dlaa_wanted() || !core_load()) return;
#ifdef _WIN32
wchar_t wdir[MAX_PATH];
DWORD len = GetModuleFileNameW(NULL, wdir, MAX_PATH);
while (len && wdir[len - 1] != L'\\') len--;
wdir[len] = 0;
#else
// The executable's folder, where NGX looks for libnvidia-ngx-dlss.so and writes its logs, as a wchar_t string:
// 32-bit code points here, as the Linux core expects.
char dir[PLAT_PATH_MAX];
// The executable's folder, where NGX looks for the DLSS model library and writes its logs.
wchar_t wdir[PLAT_PATH_MAX];
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
if (!cut || !utf8_to_wide((cut[1] = 0, dir), wdir, PLAT_PATH_MAX)) {
if (!plat_exe_dir_wide(wdir, PLAT_PATH_MAX)) {
rt_log("dlaa: cannot name the executable's folder for NGX: DLAA off");
return;
}
#endif
// 231313132: the application id NVIDIA's DLSS samples use for builds without an NVIDIA-issued id
const VkCtx *c = vk_ctx();
NgxResult r = N.init(231313132ull, wdir, c->instance, c->phys, c->device, vkGetInstanceProcAddr,
+7
View File
@@ -86,6 +86,10 @@ void vk_exec_frame_log(const tex_t *p, uint64_t frame) {
nfw = 0;
}
WEAK void vk_exec_ub_flush(VkExec *ex) { (void)ex; }
WEAK void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n) { (void)ex, (void)addr, (void)n; }
WEAK void vk_exec_ub_perf(void) {}
WEAK void vk_exec_op_draw(VkExec *ex, uint32_t op, const void *payload) {
(void)ex, (void)payload;
rt_fatal("vk: op %s is not replayed yet", nvn_op_name(op));
@@ -179,6 +183,7 @@ static void memory_barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAcce
// be made (logged). Replay thread.
static int bc_standin(const VkTexImage *img, uint32_t level, VkFormat fmt, BcStandIn *out) {
static int warned;
if (img->gone) return 0; // dropped since the lookup: a stand-in kept by its handle would outlive it
plat_mutex_lock(&bcs_lock);
int full = nbcs == BC_STANDINS;
for (int i = 0; i < nbcs; i++)
@@ -557,6 +562,7 @@ static VkImageMemoryBarrier2 layout_barrier(VkImage img, VkImageAspectFlags aspe
// go back to GENERAL and the new ones leave it, in the same command.
static void barrier_to(VkExec *ex, const VkImage *want, const VkImageAspectFlags *wasp, int nwant) {
vk_exec_pass_end(ex);
vk_exec_ub_flush(ex); // uniform updates overlaid in the pass reach their buffers before the barrier below
VkImageMemoryBarrier2 ib[18];
uint32_t n = 0;
for (int i = 0; i < ex->nfbl; i++)
@@ -967,6 +973,7 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
}
ex->cb = VK_NULL_HANDLE;
if (n) n_subs++;
if (n && (rt_trace & RT_TRACE_PERF)) vk_exec_ub_perf();
if ((rt_trace & RT_TRACE_PERF) && n_subs >= 240) {
rt_log("perf: replay command buffer parts per submission %.2f", (double)(n_subs + n_parts) / (double)n_subs);
n_subs = n_parts = 0;
+6
View File
@@ -122,6 +122,12 @@ void vk_exec_pass_end(VkExec *ex);
// Full memory barrier (all commands, all memory); ends the open pass first. Copies and dispatches order themselves with
// it. Skipped when nothing was recorded since the last one.
void vk_exec_barrier(VkExec *ex);
// UpdateUniformBuffer overlays (vk_exec_draw.c): the copies into the real buffers that updates inside a pass left for
// the next barrier (barrier_to records them first), and a GPU write of another kind into [addr, addr+n) of this
// submission (a copy, a fill, a storage write), which retires the overlays it touches.
void vk_exec_ub_flush(VkExec *ex);
void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n);
void vk_exec_ub_perf(void);
// vk_texture.c (TOTK_TEX_ALIAS): commands that must run at the replay's current position. vk_exec_inline_begin returns
// the command buffer being recorded after a full barrier (an open pass is ended; *reopen says so), or VK_NULL_HANDLE
// when this thread is not recording a replay. vk_exec_inline_end begins the pass again when it was open.
+361 -12
View File
@@ -99,6 +99,10 @@ static struct {
VkPipeline pipe[2]; // bound per bind point: 0 graphics, 1 compute
VkPipeState st; // the state vk_pipeline_set_dynamic last recorded
int st_valid;
int dyn_valid; // vk_pipeline_set_dynamic_delta's record of this command buffer's dynamic state
// holds (TOTK_DYN_DELTA); pipeline binds keep it, the memset clears it
uint32_t vmask; // the vertex bindings st's vertex input reads (valid with st_valid)
uint32_t vres; // the streams whose vb entry was resolved from vin (others: the zero buffer)
VkBuffer vb[17]; // vertex buffers last bound
VkDeviceSize vo[17], vsz[17], vstr[17];
int vcls[17]; // their pool class and pool record, for the census
@@ -137,6 +141,10 @@ static struct {
uint64_t draws, bind[2], desc[2], push, push_skip, set_new, set_reuse, ac_miss, same_prog, same_all, bind_skip;
} BS = {-1};
// TOTK_DYN_DELTA (prepare_draw): the mode and, for the perf line, the set_dynamic calls, the commands they recorded
// (a full call counts 1) and the calls that recorded none.
static struct { int mode; uint64_t calls, cmds, empty; } DD = {-1};
// The inputs of the last graphics bind_resources (bind_sig) and what its build counted for the traffic census, which a
// skipped build counts again. RS_BYTES bounds the bound-slot bytes a pipeline reads (RsList): per binding at most one
// c0 storage table, otherwise one bound range or handle per descriptor.
@@ -158,6 +166,36 @@ static struct {
uint8_t ck[DS_SIG_BYTES];
} RS = {-1};
// UpdateUniformBuffer overlays of the submission being replayed (op_update_ub, TOTK_UB_OVERLAY). Here because
// bind_sig reads them (ov_at).
#define OV_N 2048
#define OV_FOREIGN 4096
static struct {
int mode; // rt_knob: 0 off, 1 on, 2 ab; -1 unread
uint64_t fence; // the submission the lists belong to
uint64_t serial; // numbers the overlays, so bind_sig tells one from the next at the same range
uint64_t lo, hi; // the span of the overlays: most bindings lie outside it
uint32_t n, np, nf;
int f_over; // more foreign writes than f holds: no more overlays in this submission
struct { uint64_t lo, hi, serial; VkBuffer buf; VkDeviceSize off; } o[OV_N]; // oldest first, no two equal
struct { uint64_t dst; VkBuffer src; VkDeviceSize soff; VkMemSpan sp; uint64_t size; } p[OV_N]; // to copy
struct { uint64_t lo, hi; } f[OV_FOREIGN]; // GPU writes of other kinds
// since the last perf line (vk_exec_ub_perf)
uint64_t c_update, c_overlaid, c_overlaid_out, c_flush, c_bind, c_merged, c_mixed, c_inpass, c_outside, c_early,
c_order;
} OV = {.mode = -1};
// A binding without a size reaches to the end of its backing: as far as the overlays go, for ov_at.
static uint64_t ov_rest(uint64_t addr) { return addr < OV.hi ? OV.hi - addr : 1; }
// The serial of the newest overlay a binding of [addr, addr+n) reads from, 0 for none (bind_sig).
static uint64_t ov_at(const VkExec *ex, uint64_t addr, uint64_t n) {
if (!OV.n || OV.fence != ex->fence || addr >= OV.hi || addr + n <= OV.lo) return 0;
for (uint32_t i = OV.n; i-- > 0;)
if (addr < OV.o[i].hi && addr + n > OV.o[i].lo) return OV.o[i].serial;
return 0;
}
// The bound-slot bytes of VkExec a pipeline's descriptors read, as ranges merged where they touch, built once per
// pipeline info: c0's storage table, the uniform and storage ranges of the bound slots and the texture and image
// handles. sup and c0: the support inputs and the base instance count too. p: storage buffers whose address the shader
@@ -269,6 +307,9 @@ static uint8_t *scratch(VkExec *ex, uint64_t size, VkBuffer *buf, VkDeviceSize *
return p;
}
static int ub_bind(VkExec *ex, uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out);
static void ub_written(VkExec *ex, uint64_t addr, uint64_t n);
static void note_written(VkExec *ex, uint64_t addr, uint64_t size) {
CbRes *r = cb_res(ex);
for (int i = 0; i < r->nwritten; i++)
@@ -552,6 +593,10 @@ static VkImageView get_view(VkExec *ex, const VkTexImage *img, const view_t *v,
if (img->block_views) ci.pNext = &use; // the image's COLOR_ATTACHMENT usage is its block views' (vk_texture.c)
VkImageView view;
if (vkCreateImageView(vk_ctx()->device, &ci, NULL, &view) != VK_SUCCESS) return VK_NULL_HANDLE;
if (img->gone) { // dropped since the lookup: its views were purged and a new one would outlive the image
vk_defer_view(view);
return view;
}
ViewNode *n = malloc(sizeof *n);
if (!n) rt_fatal("vk: out of memory");
*n = (ViewNode){nvn_hm_get(&views_by_image, &img->image), k, view};
@@ -578,11 +623,15 @@ VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageV
if (blocks) ci.pNext = &use; // not the BC image's sampling and transfer usages
if (vkCreateImageView(vk_ctx()->device, &ci, NULL, &view) != VK_SUCCESS)
rt_fatal("vk: render target view (format %d) could not be created", (int)format);
ViewNode *n = malloc(sizeof *n);
if (!n) rt_fatal("vk: out of memory");
*n = (ViewNode){nvn_hm_get(&views_by_image, &img->image), k, view};
nvn_hm_put(&views_by_image, &img->image, n);
nvn_hm_put(&view_of, &k, (void *)view);
if (img->gone) { // dropped since resolve_target (get_view)
vk_defer_view(view);
} else {
ViewNode *n = malloc(sizeof *n);
if (!n) rt_fatal("vk: out of memory");
*n = (ViewNode){nvn_hm_get(&views_by_image, &img->image), k, view};
nvn_hm_put(&views_by_image, &img->image, n);
nvn_hm_put(&view_of, &k, (void *)view);
}
}
vk_texture_unlock();
return view;
@@ -807,10 +856,15 @@ static int bind_range(VkExec *ex, uint64_t addr, uint64_t size, uint64_t align,
uint64_t n = size && size < sp.avail ? size : sp.avail;
if (n > max) n = max;
if (!n) return 0;
if (!written && (use == VK_MUSE_UBO || use == VK_MUSE_SSBO) && ub_bind(ex, addr, n, align, out)) {
count_use(VK_MCLS_SCRATCH, use, n);
return 1;
}
if (sp.offset % align == 0) {
VK_MEM_USE(sp);
if (written) ub_written(ex, addr, n);
*out = (VkDescriptorBufferInfo){sp.buffer, sp.offset, n};
if (written) vk_mem_gpu_wrote_shader(&sp, n), vk_usnap_drop(addr, n);
if (written) vk_mem_gpu_wrote_shader(&sp, n), vk_usnap_drop(addr, n), vk_exec_ub_foreign(ex, addr, n);
count_use(sp.cls, use, n);
VK_MEM_BOUND(sp);
return 1;
@@ -920,6 +974,10 @@ static VkDescriptorBufferInfo ubo_info(VkExec *ex, int st, int slot) {
}
} else if (slot >= 3 && slot - 3 < 18) {
const p_bind_buf *b = &ex->ubo[st][slot - 3];
if (b->size && ub_bind(ex, b->addr, b->size < max ? b->size : max, align, &bi)) { // updated in this pass
count_use(VK_MCLS_SCRATCH, VK_MUSE_UBO, bi.range);
return bi;
}
if (b->size && vk_usnap_find(b->addr, b->size < max ? b->size : max, align, &bi)) { // as of the submit call
count_use(VK_MCLS_SCRATCH, VK_MUSE_UBO, bi.range);
return bi;
@@ -983,6 +1041,16 @@ static int upd_read(const VkExec *ex, uint64_t a, void *out, size_t n) {
return 0;
}
// Applies the updates of this submission that touch [a, a+n) onto out, oldest first (ub_bind's merged copies).
static void upd_apply(const VkExec *ex, uint64_t a, uint8_t *out, size_t n) {
if (UPD.fence != ex->fence) return;
for (uint32_t i = UPD.n > UPD_N ? UPD.n - UPD_N : 0; i < UPD.n; i++) {
const typeof(UPD.u[0]) *e = &UPD.u[i % UPD_N];
uint64_t lo = e->lo > a ? e->lo : a, hi = e->hi < a + n ? e->hi : a + n;
if (lo < hi) memcpy(out + (lo - a), e->data + (lo - e->lo), hi - lo);
}
}
// That address as the bound block holds it at this point of the replay; 0 when the block is unbound or too short. The
// block has no size field.
static uint64_t ubo_ptr(const VkExec *ex, int st, const GfxShaderRes *r) {
@@ -1698,6 +1766,30 @@ static int bind_sig(const VkExec *ex, const VkPipeInfo *info) {
}
if (l->c0) PUT(c0_binst);
for (int i = 0; i < l->np; i++) PUT(ubo_ptr(ex, l->p[i].st, l->p[i].res));
// The uniform overlays the draw's uniform and storage bindings read (op_update_ub): a draw of an updated block binds
// other bytes after each update, every other draw is left to skip
if (OV.n) {
uint64_t ov = 0;
const size_t lo = offsetof(VkExec, ubo), hi = offsetof(VkExec, ssbo) + sizeof ex->ssbo;
for (uint32_t i = 0; i < l->n; i++) {
const RsRange *r = &l->r[i];
if (r->off + r->len <= lo || r->off >= hi) continue;
for (uint32_t o = 0; o + sizeof(p_bind_buf) <= r->len; o += sizeof(p_bind_buf)) {
size_t at = r->off + o;
if (at < lo || at + sizeof(p_bind_buf) > hi) continue;
if ((at < offsetof(VkExec, ssbo) ? at - lo : at - offsetof(VkExec, ssbo)) % sizeof(p_bind_buf)) continue;
const p_bind_buf *b = (const p_bind_buf *)((const uint8_t *)ex + at);
if (b->addr) ov = ov * 1099511628211ull ^ ov_at(ex, b->addr, b->size ? b->size : ov_rest(b->addr));
}
}
for (int i = 0; i < l->np; i++) { // storage read through an address in a uniform block: no size either
uint64_t a = ubo_ptr(ex, l->p[i].st, l->p[i].res);
if (a) ov = ov * 1099511628211ull ^ ov_at(ex, a, ov_rest(a));
}
PUT(ov);
} else {
PUT(0);
}
#undef PUT
diff |= n != RS.hn;
RS.hn = n;
@@ -1971,21 +2063,34 @@ static void record_fixed(VkExec *ex) {
ex->dirty = 0;
}
// TOTK_VB_USED=0: every bound stream is resolved and bound, also those the draw's vertex input does not read.
static int vb_used_on(void) {
static int on = -1;
if (on < 0) on = cfg_bool("TOTK_VB_USED");
return on;
}
static void bind_vertex_buffers(VkExec *ex) {
VkBuffer vb[17];
VkDeviceSize vo[17], vsz[17], vstr[17];
int vcls[17] = {0};
void *vpool[17] = {0};
int dyn = vk_pipeline_vertex_input_dynamic();
// Only the streams the draw's vertex input reads (B.vmask, from vk_pipeline_set_dynamic): the game keeps about ten
// bound and a draw reads a few. The others get the zero buffer, never a stale one: a buffer bound but not read must
// still be a live handle.
uint32_t used = vb_used_on() && B.st_valid ? B.vmask : VK_PIPE_ALL_BINDINGS;
// A stream whose range is the one its bound buffer was resolved from, with no pool or mirror change since
// (vk_mem_gen, read before any resolve), resolves to that buffer again: most draws change one stream or none.
uint64_t gen = vk_mem_gen();
int same_gen = B.vb_valid && B.vgen == gen;
uint32_t res = 0;
for (int b = 0; b < 17; b++) {
vb[b] = Z.zbuf, vo[b] = 0, vsz[b] = ZERO_BYTES, vstr[b] = b < 16 ? ex->pipe.vstream[b].stride : 0;
if (b == VK_PIPE_ZERO_BINDING) continue;
if (b == VK_PIPE_ZERO_BINDING || !(used >> b & 1)) continue;
res |= 1u << b;
const NVNbufferRange *r = &ex->vbuf[b];
if (same_gen && B.vin[b].address == r->address && B.vin[b].size == r->size) {
if (same_gen && (B.vres >> b & 1) && B.vin[b].address == r->address && B.vin[b].size == r->size) {
vb[b] = B.vb[b], vo[b] = B.vo[b], vsz[b] = B.vsz[b], vcls[b] = B.vcls[b], vpool[b] = B.vpool[b];
continue;
}
@@ -1997,7 +2102,7 @@ static void bind_vertex_buffers(VkExec *ex) {
}
memcpy(B.vcls, vcls, sizeof vcls), memcpy(B.vpool, vpool, sizeof vpool);
memcpy(B.vin, ex->vbuf, sizeof B.vin);
B.vgen = gen;
B.vgen = gen, B.vres = res;
if (B.vb_valid && !memcmp(B.vb, vb, sizeof vb) && !memcmp(B.vo, vo, sizeof vo) && !memcmp(B.vsz, vsz, sizeof vsz) &&
(dyn || !memcmp(B.vstr, vstr, sizeof vstr)))
return;
@@ -2199,8 +2304,20 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
if (!p || !info) return warn_once(W_PIPELINE, "a graphics pipeline could not be created: draws with it are skipped"), 0;
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
if (!B.st_valid || memcmp(&B.st, &s, sizeof s)) {
vk_pipeline_set_dynamic(ex->cb, &s);
ex->recorded++;
// TOTK_DYN_DELTA: only the dynamic states whose values changed since the last call in this command buffer. A
// pipeline bind clears st_valid but not dyn_valid: every pipeline here declares the same dynamic states, which
// the bind leaves as they were (the viewport, scissor and stencil values of record_fixed rely on that too).
if (DD.mode < 0) DD.mode = rt_knob("DYN_DELTA", "1");
uint32_t cmds = 1;
if (RT_KNOB_ON(DD.mode)) {
B.vmask = vk_pipeline_set_dynamic_delta(ex->cb, &s, B.dyn_valid, &cmds);
B.dyn_valid = 1;
} else {
B.vmask = vk_pipeline_set_dynamic(ex->cb, &s);
B.dyn_valid = 0; // the delta's record is stale from here on
}
DD.calls++, DD.cmds += cmds, DD.empty += !cmds;
ex->recorded += cmds != 0;
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;
}
@@ -3159,14 +3276,246 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, &(p_dispatch){0, 0, 0, d->caller});
}
// ---- UpdateUniformBuffer inside a pass (TOTK_UB_OVERLAY)
// nvnCommandBufferUpdateUniformBuffer writes a few hundred bytes of constants between draws: the draws recorded after
// it read the new bytes, those before it the old ones. Replayed as a buffer copy at that point it needs a barrier
// against the earlier draws, and a barrier ends the open pass; the terrain and the sky islands' meshes update before
// most of their draws, about 200 updates inside passes in a field submission, each a pass end, a full barrier and a
// pass begin. Here an update inside a pass, and with TOTK_UB_OVERLAY_OUT one between passes, goes into the scratch
// buffer as an overlay: the later bindings of a range it covers bind the scratch bytes (ub_bind, from ubo_info and
// bind_range), and the copies into the real buffers wait for the next full barrier, where the pass has ended anyway
// (vk_exec_ub_flush from vk_exec.c barrier_to: a pass end, a dispatch, a transfer, the end of the submission), behind
// one barrier of their own. Each draw reads the bytes it would have read with the copy in between.
// - A GPU write of another kind (a copy, a fill, a shader's storage write) is ordered with the copies only by the
// command buffer. vk_exec_ub_foreign notes it and retires the overlays it touches; an update over such bytes takes
// the old path. A shader that writes a range with copies pending gets them first (ub_written: the pass restarts).
// - An update that takes the old path (outside a pass, a full list) retires the overlays it covers (ov_retire).
// - A binding an overlay covers only in part, or at an offset the binding's alignment refuses, gets a merged scratch
// copy: the snapshot's bytes (vk_usnap.c) or guest memory, with every update of the submission so far (UPD) on top.
// - The descriptor build skip (bind_sig) hashes the overlay each uniform and storage binding of the draw reads
// (ov_at), so a draw of an updated block binds the newest bytes and the other draws still skip. The lists are per submission and keep one entry per range (ov_add), and a binding outside their span is
// answered in one comparison; when one is full, updates take the old path.
static int ov_on(void) {
if (OV.mode < 0) OV.mode = rt_knob("UB_OVERLAY", "1");
return RT_KNOB_ON(OV.mode);
}
// TOTK_UB_OVERLAY_OUT: updates between passes become overlays too. Copied at once, each needs a full barrier of its
// own, and the copy it records makes the next update or pass begin need another: a run of k updates before a pass cost
// k + 1 barriers. Every GPU reader of the real buffer after them comes behind barrier_to, which records the copies
// first (a pass begin, a dispatch, a transfer, the end of the submission), so they cost one barrier for the run.
static int ov_out_on(void) {
static int mode = -1;
if (mode < 0) mode = rt_knob("UB_OVERLAY_OUT", "1");
return RT_KNOB_ON(mode);
}
static void ov_sub(const VkExec *ex) { // the submission being replayed
if (OV.fence == ex->fence) return;
if (OV.np) // barrier_to records them at the latest at the submission's closing barrier
rt_log("vk: %u overlaid uniform updates of submission %llu were never copied into their buffers", OV.np,
(unsigned long long)OV.fence);
OV.fence = ex->fence, OV.n = OV.np = OV.nf = 0, OV.f_over = 0, OV.lo = UINT64_MAX, OV.hi = 0;
}
static void ov_span(void) {
OV.lo = UINT64_MAX, OV.hi = 0;
for (uint32_t i = 0; i < OV.n; i++) {
if (OV.o[i].lo < OV.lo) OV.lo = OV.o[i].lo;
if (OV.o[i].hi > OV.hi) OV.hi = OV.o[i].hi;
}
}
// A new overlay of [lo, hi), and its copy. An older overlay or pending copy of exactly that range is dropped: the new
// one covers it, and the terrain rewrites one block before each draw, so the lists stay as short as the blocks.
static void ov_add(uint64_t lo, uint64_t hi, VkBuffer b, VkDeviceSize o, const VkMemSpan *sp) {
for (uint32_t i = 0; i < OV.n; i++)
if (OV.o[i].lo == lo && OV.o[i].hi == hi) {
memmove(&OV.o[i], &OV.o[i + 1], sizeof OV.o[0] * (OV.n - i - 1));
OV.n--;
break;
}
for (uint32_t i = 0; i < OV.np; i++)
if (OV.p[i].dst == lo && OV.p[i].size == hi - lo) {
memmove(&OV.p[i], &OV.p[i + 1], sizeof OV.p[0] * (OV.np - i - 1));
OV.np--;
break;
}
OV.o[OV.n++] = (typeof(OV.o[0])){lo, hi, ++OV.serial, b, o};
OV.p[OV.np++] = (typeof(OV.p[0])){lo, b, o, *sp, hi - lo};
if (lo < OV.lo) OV.lo = lo;
if (hi > OV.hi) OV.hi = hi;
}
static int ov_foreign_hits(uint64_t lo, uint64_t hi) {
if (OV.f_over) return 1;
for (uint32_t i = 0; i < OV.nf; i++)
if (lo < OV.f[i].hi && hi > OV.f[i].lo) return 1;
return 0;
}
// The overlays over [lo, hi) no longer hold the newest bytes: the real buffer does. Their copies were recorded already
// (the write came after a barrier, or ub_written brought them forward), so they only stop serving bindings.
static void ov_retire(uint64_t lo, uint64_t hi) {
if (!OV.n || lo >= OV.hi || hi <= OV.lo) return;
uint32_t k = 0;
for (uint32_t i = 0; i < OV.n; i++)
if (!(lo < OV.o[i].hi && hi > OV.o[i].lo)) OV.o[k++] = OV.o[i];
if (k == OV.n) return;
OV.n = k;
ov_span();
}
// A GPU write of another kind into [addr, addr+n) in this submission (vk_exec_xfer.c note_gpu_write, storage bindings).
void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n) {
if (OV.mode <= 0 || !n) return;
ov_sub(ex);
if (OV.nf == OV_FOREIGN) OV.f_over = 1;
else OV.f[OV.nf].lo = addr, OV.f[OV.nf].hi = addr + n, OV.nf++;
ov_retire(addr, addr + n);
}
// Records the copies of the updates overlaid so far, behind a barrier against the draws that read the old bytes.
void vk_exec_ub_flush(VkExec *ex) {
if (!OV.np || OV.fence != ex->fence) return;
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
vkCmdPipelineBarrier2(ex->cb, &di);
ex->recorded++;
// Copies without a barrier between them may run in any order, and ov_add only drops an older copy of exactly the
// same range: a copy that overlaps one recorded since the last barrier waits for it, so the newer bytes land last.
VkMemoryBarrier2 ww = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT};
VkDependencyInfo wdi = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &ww};
uint32_t since = 0; // the first copy recorded after the last barrier
for (uint32_t i = 0; i < OV.np; i++) {
for (uint32_t k = since; k < i; k++)
if (OV.p[i].dst < OV.p[k].dst + OV.p[k].size && OV.p[k].dst < OV.p[i].dst + OV.p[i].size) {
vkCmdPipelineBarrier2(ex->cb, &wdi);
ex->recorded++;
OV.c_order++;
since = i;
break;
}
VkBufferCopy cp = {OV.p[i].soff, OV.p[i].sp.offset, OV.p[i].size};
vkCmdCopyBuffer(ex->cb, OV.p[i].src, OV.p[i].sp.buffer, 1, &cp);
ex->recorded++;
}
OV.np = 0;
// The full barrier barrier_to records next makes the copied bytes visible: later bindings read the real buffer,
// as after an update copied at once, and need no overlay lookups or merged copies (ov_at answers 0 for them, so
// the descriptor build skip rebuilds the draws that bound an overlay)
OV.n = 0;
ov_span();
OV.c_flush++;
}
// TOTK_TRACE=perf, every 600 submissions: per submission, the updates and how many stayed in their pass.
void vk_exec_ub_perf(void) {
static unsigned subs, dsubs;
if (DD.mode >= 0 && ++dsubs % 600 == 0 && DD.calls) {
rt_log("perf: dynamic state per submission: %.1f calls, %.1f commands (a full set counts 1), %.1f calls with "
"nothing changed%s", DD.calls / 600.0, DD.cmds / 600.0, DD.empty / 600.0,
DD.mode == 2 ? " (TOTK_DYN_DELTA=ab)" : DD.mode ? "" : " (TOTK_DYN_DELTA=0)");
DD.calls = DD.cmds = DD.empty = 0;
}
if (OV.mode <= 0 || ++subs % 600) return;
const double k = 1 / 600.0;
rt_log("perf: uniform updates per submission %.1f: %.1f overlaid (%.1f of them between passes; copied in %.2f "
"batches), %.1f copied in a pass, %.1f outside one; bindings from an overlay %.1f (%.1f merged, %.1f over "
"other GPU writes), %.2f batches brought forward for a storage write, %.2f barriers between overlapping copies",
OV.c_update * k, OV.c_overlaid * k, OV.c_overlaid_out * k, OV.c_flush * k, OV.c_inpass * k, OV.c_outside * k,
OV.c_bind * k, OV.c_merged * k, OV.c_mixed * k, OV.c_early * k, OV.c_order * k);
OV.c_update = OV.c_overlaid = OV.c_overlaid_out = OV.c_flush = OV.c_bind = OV.c_merged = OV.c_mixed = 0;
OV.c_inpass = OV.c_outside = OV.c_early = OV.c_order = 0;
}
// The scratch bytes of a uniform or storage binding [addr, addr+n) an overlay of this submission touches: 1 with *out
// set, 0 when none does.
static int ub_bind(VkExec *ex, uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out) {
if (OV.mode <= 0 || OV.fence != ex->fence || !OV.n || addr >= OV.hi || addr + n <= OV.lo) return 0;
uint64_t hi = addr + n;
int newest = -1;
for (int i = (int)OV.n; i-- > 0;)
if (addr < OV.o[i].hi && hi > OV.o[i].lo) { newest = i; break; }
if (newest < 0) return 0;
OV.c_bind++;
if (OV.o[newest].lo <= addr && OV.o[newest].hi >= hi) {
VkDeviceSize off = OV.o[newest].off + (addr - OV.o[newest].lo);
if (off % align == 0) {
*out = (VkDescriptorBufferInfo){OV.o[newest].buf, off, n};
return 1;
}
}
// Merged: the bytes as of the submit call (the snapshot, else guest memory), then this submission's updates in
// order. A transient copy, so the descriptor build skip leaves the draw after it alone (RS.volatile_). Bytes a copy
// or a storage write of this submission produced are not in either source: counted.
VkBuffer b;
VkDeviceSize o;
uint8_t *dst = scratch(ex, n, &b, &o);
RS.volatile_ = 1;
const uint8_t *base = vk_usnap_bytes(addr, n);
VkMemSpan sp;
if (base) memcpy(dst, base, n);
else if (vk_mem_resolve(addr, n, &sp)) memcpy(dst, (const void *)(uintptr_t)addr, n);
else memset(dst, 0, n);
if (ov_foreign_hits(addr, hi)) OV.c_mixed++;
upd_apply(ex, addr, dst, n);
*out = (VkDescriptorBufferInfo){b, o, n};
OV.c_merged++;
return 1;
}
// Before a binding the shader writes: overlaid updates whose copies are still pending go to the buffer first, behind a
// barrier, and the pass restarts, so the shader's writes land after them as they did when every update was copied at
// once.
static void ub_written(VkExec *ex, uint64_t addr, uint64_t n) {
if (OV.mode <= 0 || OV.fence != ex->fence || !OV.np) return;
for (uint32_t i = 0; i < OV.np; i++)
if (addr < OV.p[i].dst + OV.p[i].size && OV.p[i].dst < addr + n) {
int reopen = ex->in_pass;
vk_exec_barrier(ex); // records the pending copies (vk_exec_ub_flush) before its barrier
if (reopen) vk_exec_pass_begin(ex);
OV.c_early++;
return;
}
}
static void op_update_ub(VkExec *ex, const p_update_ub *u) {
if (!u->size) return;
VkMemSpan sp;
if (!vk_mem_resolve(u->dst, u->size, &sp)) return warn_once(W_UB, "UpdateUniformBuffer to %#llx is not backed: dropped", (unsigned long long)u->dst);
cb_res(ex);
vk_exec_barrier(ex); // earlier draws finished with the old contents
VkBuffer b;
VkDeviceSize o;
if (ov_on()) {
ov_sub(ex);
OV.c_update++;
int here = ex->in_pass || ov_out_on();
if (here && OV.n < OV_N && OV.np < OV_N && !ov_foreign_hits(u->dst, u->dst + u->size)) {
uint8_t *d = scratch(ex, u->size, &b, &o);
memcpy(d, u->data, u->size);
VK_MEM_PICK(sp);
ov_add(u->dst, u->dst + u->size, b, o, &sp);
vk_mem_gpu_wrote(&sp, u->size);
vk_usnap_drop(u->dst, u->size);
vk_mem_count(sp.cls, VK_MUSE_GPU_WRITE, u->size);
note_written(ex, u->dst, u->size);
upd_note(ex, u);
OV.c_overlaid++;
if (!ex->in_pass) OV.c_overlaid_out++;
return;
}
if (ex->in_pass) OV.c_inpass++;
else OV.c_outside++;
ov_retire(u->dst, u->dst + u->size);
}
vk_exec_barrier(ex); // earlier draws finished with the old contents; overlaid updates' copies go first
memcpy(scratch(ex, u->size, &b, &o), u->data, u->size);
VK_MEM_PICK(sp);
VkBufferCopy cp = {o, sp.offset, u->size};
+1
View File
@@ -73,6 +73,7 @@ static void guest_written_cb(void *arg) {
// The GPU wrote [addr, addr+size) of CPU-visible memory: tell the texture cache once the fence is reached.
static void note_gpu_write(VkExec *ex, uint64_t addr, uint64_t size) {
vk_usnap_drop(addr, size);
vk_exec_ub_foreign(ex, addr, size);
if (nvn_gpu_only(addr)) return;
Range *r = malloc(sizeof *r);
if (!r) rt_fatal("vk: out of memory");
+1 -1
View File
@@ -272,7 +272,7 @@ void vk_hdr_draw(VkExec *ex, const VkPipeInfo *info) {
}
if (ntex == 1 && slot >= 0 && slot < 128 && ex->tex[1][slot]) {
const VkTexImage *s = vk_texture_by_id(NVN_HANDLE_TEX(ex->tex[1][slot]), NULL, NULL);
if (s && is_float(s->format) && s->width == t->width && s->height == t->height && s->samples == 1)
if (s && !s->gone && is_float(s->format) && s->width == t->width && s->height == t->height && s->samples == 1)
src = s->image, fmt = s->format;
}
plat_mutex_lock(&rec_lock);
+183 -27
View File
@@ -78,8 +78,9 @@ static int shadow_on; // shadow mode: imported pools get shad
static int shadow_pool(VkCtx *c, PoolRec *r, uint64_t addr, uint64_t size);
static void shadow_touch(PoolRec *r);
static void shadow_forget(PoolRec *r);
static void mirror_retiring(PoolRec *r); // vk_pool_fini: kept until mirror_retire (or vk_mem_shutdown)
static void mirror_retire(void *arg); // completion thread: destroys a finalized mirrored pool's record
static void pool_retiring(PoolRec *r); // vk_pool_fini: kept until pool_retire (or vk_mem_shutdown)
static void pool_retire(void *arg); // completion thread: destroys a finalized pool's record
static uint64_t retire_value(void); // the timeline value after which a finalized pool's record may go
static void grow(void **arr, int *cap, size_t elem);
static PlatMutex pool_lock = PLAT_MUTEX_INIT;
@@ -333,10 +334,14 @@ void vk_pool_fini(pool_t *p) {
vk_queue_unlock();
}
vk_mem_collect();
// A mirrored pool's record and buffers outlive the submission being recorded, whose sync copies and write-backs may
// name them (the game frees a pool only once the GPU is done with it, but its CPU writes may still be queued).
if (r->mbuf) mirror_retiring(r), vk_completion_add(vk_sync_issued() + 1, mirror_retire, r);
else destroy_rec(c, r);
// The record, its buffers and its memory outlive the submission being recorded, of any pool kind. The wait above
// covers submitted work only. With the synchronous replay another game thread may be recording right now, and that
// recording may already name this pool: a mirrored pool's sync copies and write-backs, an import pool's buffer
// through a texture upload or a released cache entry (vk_texture.c slot_resolve), a physical pool's memory under
// the images vk_texture_range_gone just deferred to that submission. The game frees a pool only once its own GPU
// work is done, so only the port's work can name it. Freed here, the submission ran with freed memory.
pool_retiring(r);
vk_completion_add(retire_value(), pool_retire, r);
}
// Requests were checked by the front end (nvn_api_core.c, MNG totk_vpool.c); apply them in order.
@@ -416,6 +421,10 @@ int vk_mem_resolve(uint64_t addr, uint64_t size, VkMemSpan *out) {
return resolve_locked(addr, size, out);
}
int vk_mem_span_live(const VkMemSpan *sp) {
return sp->buffer && sp->pool && !sp->alloc && !shadow_on && ((const PoolRec *)sp->pool)->kind == POOL_IMPORT;
}
// The pool index lookup under the lock; a non-overlapped import or physical pool goes into the address's cache slot.
static int resolve_locked(uint64_t addr, uint64_t size, VkMemSpan *out) {
memset(out, 0, sizeof *out);
@@ -507,6 +516,9 @@ static void dirty_frame(uint64_t frame);
static void mirror_log(uint64_t frame);
static void shadow_log(uint64_t frame);
static int dead_pending(void);
// Deferrals made off the recording thread while another thread recorded a submission (cmd_lock, defer), the newest
// one's stamp: the case the stamp from the recording start covers. In the census line and the device fault report.
static uint64_t n_cross_defer, cross_defer_tl;
static void vram_log(uint64_t frame);
@@ -542,10 +554,11 @@ void vk_mem_census(uint64_t frame) {
}
plat_mutex_unlock_shared(&pool_lock);
rt_log("mem: census at frame %llu: import %llu pools %.1f MiB (%d without a GPU buffer), physical %llu pools %.1f MiB, "
"virtual %llu pools %.1f MiB of address space",
"virtual %llu pools %.1f MiB of address space; %llu objects deferred off the recording thread during a recording",
(unsigned long long)frame, (unsigned long long)count[POOL_IMPORT], total[POOL_IMPORT] / 1048576.0,
n_import_failures, (unsigned long long)count[POOL_PHYS], total[POOL_PHYS] / 1048576.0,
(unsigned long long)count[POOL_VIRT], total[POOL_VIRT] / 1048576.0);
(unsigned long long)count[POOL_VIRT], total[POOL_VIRT] / 1048576.0,
(unsigned long long)__atomic_load_n(&n_cross_defer, __ATOMIC_RELAXED));
static const char *const kinds[] = {"", "import", "physical", "virtual"};
for (int j = 0; j < ng; j++) {
char f[160];
@@ -565,10 +578,23 @@ void vk_mem_census(uint64_t frame) {
window_start = frame;
}
// TOTK_VRAM_LIMIT_MIB: pretend the device-local budget is at most this many MiB, so the paths that react to the budget
// can be driven on a card larger than the one a report came from. The driver knows nothing of it.
static uint64_t vram_limit(void) {
static int64_t limit = -1;
if (limit < 0) {
const char *s = cfg_get("TOTK_VRAM_LIMIT_MIB");
limit = s && *s ? (int64_t)strtoull(s, NULL, 10) << 20 : 0;
if (limit) rt_log("mem: device-local budget limited to %lld MiB (TOTK_VRAM_LIMIT_MIB)", (long long)(limit >> 20));
}
return (uint64_t)limit;
}
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
VkCtx *c = vk_ctx();
*used = *budget = 0, *exact = 0;
if (!c) return 0;
uint64_t limit = vram_limit();
const VkPhysicalDeviceMemoryProperties *mp = &c->mem_props;
if (c->has_memory_budget) {
VkPhysicalDeviceMemoryBudgetPropertiesEXT b = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
@@ -578,6 +604,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
if (p2.memoryProperties.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
*used += b.heapUsage[h], *budget += b.heapBudget[h];
*exact = 1;
if (limit && *budget > limit) *budget = limit;
return *budget != 0;
}
VmaBudget vb[VK_MAX_MEMORY_HEAPS];
@@ -585,6 +612,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
for (uint32_t h = 0; h < mp->memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
if (mp->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
*used += vb[h].statistics.blockBytes, *budget += mp->memoryHeaps[h].size;
if (limit && *budget > limit) *budget = limit;
return *budget != 0;
}
@@ -633,6 +661,17 @@ static uint32_t reg_tried[RT_ADDRESS_LIMIT >> REG_SHIFT]; // gen_lock: ww_ep o
static uint32_t *fcand; // gen_lock: regions to try at the next freeze pass
static int nfcand, cfcand;
static uint64_t n_frz_refused; // gen_lock: candidates mem_freeze refused
// Regions the GPU writes. Storage-buffer stores, buffer copies and fills land in imported guest memory, directly (import
// mode) or through a pool's mirror that vk_mem_mirror_end copies or merges into the imported buffer. They are not CPU
// writes: no fault thaws the region and no write-watch bit is set, so a region only the GPU writes looked idle, was
// frozen, and the GPU went on writing pages the CPU side had made read-only. LAS Recomp never changes the protection of
// imported memory: on NVIDIA, VirtualProtect on pages imported with VK_EXT_external_memory_host left the GPU reading
// wrong data (runtime/nvn/vulkan/exec.c, the comment on its write watch); a GPU write there is the likely cause of the
// device faults "write invalid" at a page of guest memory on NVIDIA (#788, #802). reg_gpu holds the timeline value of
// the newest submission that writes into the region (gpu_writes): freeze_pass leaves the region alone until the GPU is
// past that value, and a frozen region is thawed when a write into it is recorded.
static uint64_t reg_gpu[RT_ADDRESS_LIMIT >> REG_SHIFT]; // replay thread
static volatile int64_t n_frz_gpu, n_thaw_gpu; // freeze candidates kept for a GPU write; thaws for one (mirror_log)
static uint32_t *gen_slot(uint64_t page_addr, int create) {
uint64_t ci = page_addr >> GEN_CHUNK_SHIFT;
@@ -1364,19 +1403,19 @@ static void mirror_forget(PoolRec *r) {
}
}
// Finalized mirrored pools waiting for mirror_retire; vk_mem_shutdown destroys those whose callback never ran.
// Finalized pools waiting for pool_retire; vk_mem_shutdown destroys those whose callback never ran.
static PoolRec **retiring;
static int nretiring, cretiring;
static PlatMutex retire_lock = PLAT_MUTEX_INIT;
static void mirror_retiring(PoolRec *r) {
static void pool_retiring(PoolRec *r) {
plat_mutex_lock(&retire_lock);
if (nretiring == cretiring) grow((void **)&retiring, &cretiring, sizeof *retiring);
retiring[nretiring++] = r;
plat_mutex_unlock(&retire_lock);
}
static void mirror_retire(void *arg) {
static void pool_retire(void *arg) {
plat_mutex_lock(&retire_lock);
for (int i = 0; i < nretiring; i++)
if (retiring[i] == arg) { retiring[i] = retiring[--nretiring]; break; }
@@ -1693,8 +1732,26 @@ static void snap_free(void *arg) { // completion thread: the submission is don
free(f);
}
// Record time: the submission being recorded writes [off, off+size) of r's guest range, directly into the imported
// buffer or into the mirror whose bytes go back at its end. Keeps the regions unfrozen until the GPU is past it
// (reg_gpu). Shadow storage needs nothing: the GPU writes the shadow and the CPU copies it back, thawing as it goes.
static void gpu_writes(const PoolRec *r, uint64_t off, uint64_t size) {
if (!freeze_idle || r->smap) return;
uint64_t a = r->import_lo + off, b = a + size;
if (b <= a || b > RT_ADDRESS_LIMIT) return;
uint64_t f = vk_exec_recording_fence();
if (!f) f = vk_sync_issued() + 1;
int frozen = 0;
for (uint64_t j = a >> REG_SHIFT; j <= (b - 1) >> REG_SHIFT; j++) {
if (__atomic_load_n(&reg_gpu[j], __ATOMIC_RELAXED) < f) __atomic_store_n(&reg_gpu[j], f, __ATOMIC_RELAXED);
frozen |= mem_frozen(j << REG_SHIFT);
}
if (frozen) mem_thaw(a, b - a), plat_inc64(&n_thaw_gpu);
}
static void gpu_wrote(const VkMemSpan *sp, uint64_t size, int shader) {
PoolRec *r = sp->pool;
if (r && size) gpu_writes(r, sp->offset, size);
if (!r || r->mstate != MS_ON || sp->buffer != r->mbuf || !size) return;
// A binding to the end of its pool (no size) can span hundreds of MiB: those stay copies.
if (shader && !(sp->offset & 3) && !(size & 3) && size <= MERGE_MAX && merging() && snap_add(r, sp->offset, size))
@@ -1778,9 +1835,11 @@ void nvn_pool_flushed(const pool_t *p, uint64_t off, uint64_t size) {
// gen_lock exclusive: freezes up to FREEZE_PER_SUB candidate regions still idle. The query after the
// protection collects the writes made before it took effect.
static void freeze_pass(void) {
uint64_t gpu_done = vk_sync_fence_done();
for (int done = 0; nfcand && done < FREEZE_PER_SUB;) {
uint64_t j = fcand[--nfcand], a = j << REG_SHIFT;
if (ww_ep - reg_last[j] < (uint32_t)freeze_idle || mem_frozen(a)) continue;
if (__atomic_load_n(&reg_gpu[j], __ATOMIC_RELAXED) > gpu_done) { plat_inc64(&n_frz_gpu); continue; } // gpu_writes
done++;
if (!mem_freeze(a)) { n_frz_refused++; continue; }
query_stamp(a, a + (1ull << REG_SHIFT), ++gen_now);
@@ -1998,6 +2057,10 @@ static void mirror_log(uint64_t frame) {
"or mapping change %llu, refused %llu",
(unsigned long long)frame, wc / wf, wb / wf / 1048576.0, qpc_ms(wt) / wf, dc / wf, wp / wf, qpc_ms(dt) / wf,
fz * 2.0, (unsigned long long)ff, (unsigned long long)ft, (unsigned long long)fo, (unsigned long long)nr);
int64_t kg = plat_xchg64(&n_frz_gpu, 0), tg = plat_xchg64(&n_thaw_gpu, 0);
if (kg || tg) // gpu_writes: before this, every count here was a region frozen under a GPU write
rt_log("mem: frame %llu: regions the GPU writes, since the last line: %lld kept from freezing, %lld frozen ones "
"thawed", (unsigned long long)frame, (long long)kg, (long long)tg);
if (!vk_mirror_on) return;
plat_mutex_lock(&mirror_stat_lock);
MirrorAcc a = macc;
@@ -2050,6 +2113,7 @@ static void vram_log(uint64_t frame) {
alloc_bytes / M, (unsigned long long)t.live, (unsigned long long)t.own_live, t.bytes_live / M,
(unsigned long long)t.released, (unsigned long long)t.own_released, t.bytes_released / M, (long long)m_live_n,
m_live_bytes / M, dead_pending());
vk_texture_released_log(frame);
}
// Device class: INTEGRATED -> uma; a discrete GPU with a DEVICE_LOCAL heap of 1 GiB or more beside a
@@ -2101,9 +2165,10 @@ typedef struct {
uint64_t seq;
Staging *stg;
int nstg, cap;
VkDeviceSize held; // staging and scratch bytes freed when the batch completes (vk_upload_held)
} Batch;
enum { DEAD_IMAGE = 1, DEAD_IMAGE_VMA, DEAD_VIEW, DEAD_BUFFER_VMA };
enum { DEAD_IMAGE = 1, DEAD_IMAGE_VMA, DEAD_VIEW, DEAD_BUFFER_VMA, DEAD_HOST }; // DEAD_HOST: free(handle)
typedef struct { int kind; uint64_t handle; VmaAllocation alloc; uint64_t seq, tl; } Dead;
static PlatRMutex cmd_lock;
@@ -2118,7 +2183,8 @@ static int nspare, cap_spare;
static uint64_t next_seq, done_seq; // batches handed out / completed (all batches up to done_seq)
static Dead *dead;
static int ndead, cap_dead;
static uint64_t tl_noted; // newest timeline value vk_sync.c reported submitted
static uint64_t tl_noted; // value of the submission being recorded, else of the last one (vk_mem_note_fence)
static _Thread_local uint64_t tl_noted_here; // the value this thread noted: it records that submission
static void cmd_init(void *p) {
(void)p;
@@ -2161,6 +2227,10 @@ 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; }
if (d->kind == DEAD_HOST) {
free((void *)(uintptr_t)d->handle);
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) {
@@ -2254,10 +2324,68 @@ void *vk_staging(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
else {
if (open_b.nstg == open_b.cap) grow((void **)&open_b.stg, &open_b.cap, sizeof *open_b.stg);
open_b.stg[open_b.nstg++] = (Staging){b, a};
open_b.held += bi.size;
}
return info.pMappedData;
}
// Device-local scratch of the upload batch (the GPU deswizzle's linear copies, the ASTC decoder's blocks and texels):
// a pool of its own, like staging, so the replay thread neither waits on the default pool's lock while another thread
// makes a block there (the headroom thread: about 8 ms for 256 MiB) nor makes one itself for a few MiB. One block
// of SCR_BLOCK is kept from the first use; larger buffers than half a block go to the default pool.
#define SCR_BLOCK (64ull << 20)
static VmaPool scr_pool;
static int scr_pool_tried;
VkResult vk_scratch_buffer(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
VkCtx *c = vk_ctx();
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = size ? size : 16,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (!__atomic_load_n(&scr_pool_tried, __ATOMIC_ACQUIRE)) {
plat_once(&cmd_once, cmd_init, NULL);
plat_rmutex_lock(&cmd_lock);
if (!scr_pool_tried) {
// two blocks with 8 GiB or more of device-local memory: fight2's first world frame deswizzles about
// 280 MiB, more than the GPU finishes while the replay records, and the pool grew by four blocks there
VkDeviceSize vram = 0;
for (uint32_t h = 0; h < c->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
if (c->mem_props.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT && c->mem_props.memoryHeaps[h].size > vram)
vram = c->mem_props.memoryHeaps[h].size;
uint32_t type;
VmaPoolCreateInfo pi = {.blockSize = SCR_BLOCK, .minBlockCount = vram >= (8ull << 30) ? 2 : 1};
if (vmaFindMemoryTypeIndexForBufferInfo(c->vma, &bi, &aci, &type) == VK_SUCCESS) {
pi.memoryTypeIndex = type;
if (vmaCreatePool(c->vma, &pi, &scr_pool) != VK_SUCCESS) scr_pool = NULL;
}
rt_log("vk: upload scratch pool %s (%u blocks of %llu MiB kept)", scr_pool ? "created" : "unavailable: default pool",
(unsigned)pi.minBlockCount, (unsigned long long)(SCR_BLOCK >> 20));
__atomic_store_n(&scr_pool_tried, 1, __ATOMIC_RELEASE);
}
plat_rmutex_unlock(&cmd_lock);
}
if (scr_pool && bi.size <= SCR_BLOCK / 2) aci.pool = scr_pool;
VkResult r = vmaCreateBuffer(c->vma, &bi, &aci, buf, alloc, NULL);
if (r == VK_SUCCESS && open_active) open_b.held += bi.size; // the caller defers its free to the open batch
return r;
}
VkDeviceSize vk_upload_held(void) { return open_active ? open_b.held : 0; }
// At device creation: the staging and scratch pools with their kept blocks, made through a first small buffer each
// (freed at once), so the first world frame does not allocate them (staging was first used there once the GPU took
// the early texture uploads).
void vk_upload_pools_init(void) {
VkCtx *c = vk_ctx();
if (!c) return;
VkBuffer b;
VmaAllocation a;
vk_staging(16, &b, &a);
vmaDestroyBuffer(c->vma, b, a);
if (vk_scratch_buffer(16, &b, &a) == VK_SUCCESS) vmaDestroyBuffer(c->vma, b, a);
}
uint64_t vk_upload_flush(int wait) {
plat_once(&cmd_once, cmd_init, NULL);
plat_rmutex_lock(&cmd_lock);
@@ -2295,20 +2423,21 @@ static void defer(int kind, uint64_t handle, VmaAllocation alloc) {
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. 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;
// submission that names it, including the one being recorded, which may name it and is not submitted yet:
// vk_texture.c slot_resolve hands a draw a released entry, and entry_of, sweep_released and the VRAM pressure drop
// destroy released entries. tl_noted is that submission's value from the start of its recording (vk_sync.c
// vk_sync_submit), whichever thread destroys the object. With the synchronous replay a game thread finalizing a
// texture (vk_backend.c vk_be_texture_release) destroys entries while another game thread records; stamped with
// the last submitted value, as the recording thread's own fence was the only one seen, the object was freed once
// the GPU reached that value, before the submission naming it ran: "write invalid" device faults on NVIDIA. LAS
// Recomp and Savage Wind Recomp stamp every deferral with the submission being recorded (runtime/nvn/vulkan/
// textures.c defer: vk_pending_seq).
uint64_t tl = tl_noted;
if (kind != DEAD_HOST && tl > vk_sync_issued() && tl_noted_here != tl) n_cross_defer++, cross_defer_tl = tl;
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());
if (kind != DEAD_HOST)
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);
}
@@ -2327,14 +2456,38 @@ void vk_defer_view(VkImageView view) { if (view) defer(DEAD_VIEW, (uint64_t)(uin
void vk_defer_buffer(VkBuffer buf, VmaAllocation alloc) {
if (buf) defer(DEAD_BUFFER_VMA, (uint64_t)(uintptr_t)buf, alloc);
}
void vk_defer_free(void *p) { if (p) defer(DEAD_HOST, (uint64_t)(uintptr_t)p, NULL); }
void vk_mem_note_submit(uint64_t timeline_value) {
// The device fault report: no lock, since a thread that holds cmd_lock may be waiting for the report's own lock
// (vk_upload_flush reports a failed submit with cmd_lock held).
void vk_mem_fault_log(void) {
rt_log("vk: deferred frees at the fault: %d pending, stamp %llu, submitted %llu, done %llu; %llu deferred off the "
"recording thread during a recording (newest stamp %llu)",
__atomic_load_n(&ndead, __ATOMIC_RELAXED), (unsigned long long)__atomic_load_n(&tl_noted, __ATOMIC_RELAXED),
(unsigned long long)vk_sync_issued(), (unsigned long long)vk_sync_fence_done(),
(unsigned long long)__atomic_load_n(&n_cross_defer, __ATOMIC_RELAXED),
(unsigned long long)__atomic_load_n(&cross_defer_tl, __ATOMIC_RELAXED));
}
void vk_mem_note_fence(uint64_t timeline_value) {
plat_once(&cmd_once, cmd_init, NULL);
plat_rmutex_lock(&cmd_lock);
if (timeline_value > tl_noted) tl_noted = timeline_value;
tl_noted_here = timeline_value;
plat_rmutex_unlock(&cmd_lock);
}
// The later of the submission being recorded (tl_noted, noted from its start: vk_sync.c vk_sync_submit) and the next
// one to be submitted.
static uint64_t retire_value(void) {
plat_once(&cmd_once, cmd_init, NULL);
plat_rmutex_lock(&cmd_lock);
uint64_t v = vk_sync_issued() + 1;
if (tl_noted > v) v = tl_noted;
plat_rmutex_unlock(&cmd_lock);
return v;
}
void vk_mem_collect(void) {
VkCtx *c = vk_ctx();
if (!c) return;
@@ -2383,6 +2536,9 @@ void vk_mem_shutdown(void) {
if (stg_pool) vmaDestroyPool(c->vma, stg_pool); // after retire: no staging buffer is left in it
stg_pool = NULL;
stg_pool_tried = 0;
if (scr_pool) vmaDestroyPool(c->vma, scr_pool); // deferred scratch buffers went with collect_locked above
scr_pool = NULL;
scr_pool_tried = 0;
plat_rmutex_unlock(&cmd_lock);
plat_mutex_lock(&pool_lock);
for (int i = 0; i < pool_index.n; i++) destroy_rec(c, pool_index.e[i].buf);
+170 -15
View File
@@ -101,6 +101,12 @@ static struct {
VkPipeStats stats;
} P = {.lock = PLAT_MUTEX_INIT};
// Builds into P.cache take it shared, vk_pipeline_save takes it to copy the cache out. Vulkan lets
// vkGetPipelineCacheData run alongside builds into a cache created without the EXTERNALLY_SYNCHRONIZED bit, but
// Intel's Windows driver (101.7085, Iris Xe) faults in it (igvk64.dll, reading 0x170) when the first autosave meets the
// boot's pipeline builds (#681). Nothing is taken while it is held.
static PlatMutex cache_use = PLAT_MUTEX_INIT;
typedef struct Layout {
struct Layout *next;
uint64_t hash;
@@ -1440,7 +1446,9 @@ static VkPipeline create_graphics(const VkPipeState *s, const GKey *k, const Lay
if (k->fbl & 1) pi.flags |= VK_PIPELINE_CREATE_COLOR_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
if (k->fbl & 2) pi.flags |= VK_PIPELINE_CREATE_DEPTH_STENCIL_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
uint64_t p0 = perf_begin();
plat_mutex_lock_shared(&cache_use);
VkResult r = vkCreateGraphicsPipelines(P.device, P.cache, 1, &pi, NULL, &pipe);
plat_mutex_unlock_shared(&cache_use);
perf_end(PERF_PIPE, p0); // counts on the render thread only, so background builds stay out of it
if (r != VK_SUCCESS) {
rt_log("vk_pipeline: vkCreateGraphicsPipelines failed (%d)", (int)r);
@@ -1591,7 +1599,9 @@ static VkPipeline lib_create(int kind, const VkPipeState *s, const GKey *k, VkPi
ri.depthAttachmentFormat = (VkFormat)k->depth, ri.stencilAttachmentFormat = (VkFormat)k->stencil;
pi.pColorBlendState = &cb, pi.pMultisampleState = &ms;
}
plat_mutex_lock_shared(&cache_use);
if (vkCreateGraphicsPipelines(P.device, P.cache, 1, &pi, NULL, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
plat_mutex_unlock_shared(&cache_use);
out:
if (m) vkDestroyShaderModule(P.device, m, NULL);
free(patched);
@@ -1803,7 +1813,8 @@ static VkPipeline gpl_link(const VkPipeState *s, const GKey *k, const Layout *lc
// shaders (gfx_shader_get results) live for the process (gfx_shader_drop_memory is the shader probe's, which builds no
// pipelines), layouts until vk_pipeline_shutdown, which drains the queue first. Workers call vkCreateGraphicsPipelines
// without P.lock: P.cache is created without VK_PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED_BIT, so the driver
// synchronises it, vkGetPipelineCacheData in vk_pipeline_save included. Compute pipelines stay synchronous: a skipped
// synchronises the builds among themselves. vk_pipeline_save still keeps them apart from vkGetPipelineCacheData (see
// cache_use). Compute pipelines stay synchronous: a skipped
// dispatch can change what the game computes.
//
// Two queues: draws' misses (A.head) and the boot prebuild (A.low, see "pipeline keys" below). Workers take the low
@@ -2512,7 +2523,7 @@ static void prebuild_start(void) {
plat_mutex_unlock(&P.lock);
}
// A compute pipeline for cs in layout l; VK_NULL_HANDLE on failure. No lock needed (P.cache is driver-synchronised).
// A compute pipeline for cs in layout l; VK_NULL_HANDLE on failure. Takes cache_use shared, like the graphics builds.
static VkPipeline compute_create(const GfxShader *cs, const Layout *l) {
uint64_t words;
const uint32_t *code = gfx_shader_spirv(cs, &words);
@@ -2525,7 +2536,9 @@ static VkPipeline compute_create(const GfxShader *cs, const Layout *l) {
.layout = l->layout};
pin_subgroup32(&ci.stage, &rqc, code, words);
VkPipeline pipe = VK_NULL_HANDLE;
plat_mutex_lock_shared(&cache_use);
if (vkCreateComputePipelines(P.device, P.cache, 1, &ci, NULL, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
plat_mutex_unlock_shared(&cache_use);
vkDestroyShaderModule(P.device, m, NULL);
return pipe;
}
@@ -3113,8 +3126,8 @@ int vk_pipeline_warm_shaders(void) {
// ---- dynamic state
void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
if (!ensure_init()) return;
uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
if (!ensure_init()) return VK_PIPE_ALL_BINDINGS;
const poly_st *pl = &s->poly;
const ds_st *d = &s->ds;
// The front face maps unchanged: TOTK sets the upper-left window origin, and the y-flipped viewport (y_flip) puts
@@ -3137,16 +3150,21 @@ void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
vkCmdSetDepthBiasEnable(cb, (pl->offset_en >> mode & 1) != 0);
vkCmdSetRasterizerDiscardEnable(cb, s->raster_discard != 0);
vkCmdSetPrimitiveRestartEnable(cb, s->prim_restart != 0);
if (P.dyn_vi && s->vs) {
// The vertex bindings the draw reads: those of its vertex input, set here or made into the pipeline from the same
// state by the same vertex_input.
uint32_t streams = VK_PIPE_ALL_BINDINGS;
if (s->vs) {
VtxInput v;
vertex_input(s, &v);
rec_vertex_input(P.set_vi, cb, v.n_bind, v.b, v.n_attr, v.a);
if (P.dyn_vi) rec_vertex_input(P.set_vi, cb, v.n_bind, v.b, v.n_attr, v.a);
streams = 0;
for (uint32_t i = 0; i < v.n_bind; i++) streams |= 1u << v.b[i].binding;
}
if (P.dyn_clamp) rec_bool(P.set_clamp, cb, s->depth_clamp != 0);
if (P.dyn_poly) rec_polygon_mode(P.set_poly, cb, polygon_mode(pl->mode));
if (P.dyn_a2c) rec_bool((PFN_vkCmdSetDepthClampEnableEXT)P.set_a2c, cb, s->ms.a2c != 0); // the same signature
int n = target_count(s);
if (!n) return;
if (!n) return streams;
VkBool32 en[8];
VkColorBlendEquationEXT eq[8];
VkColorComponentFlags wm[8];
@@ -3160,6 +3178,119 @@ void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
if (P.dyn_blend_en) rec_blend_enable(P.set_blend_en, cb, 0, (uint32_t)n, en);
if (P.dyn_blend_eq) rec_blend_equation(P.set_blend_eq, cb, 0, (uint32_t)n, eq);
if (P.dyn_wmask) rec_write_mask(P.set_wmask, cb, 0, (uint32_t)n, wm);
return streams;
}
// The values vk_pipeline_set_dynamic records for a state, as the commands take them.
typedef struct {
VkCullModeFlags cull;
VkFrontFace front;
VkPrimitiveTopology topo;
VkBool32 dt, dw, st, bias, discard, restart, clamp, a2c;
VkCompareOp df, scmp[2];
VkStencilOp sop[2][3]; // fail, pass, depth fail; front, back
VkPolygonMode poly;
int n; // colour attachments en, eq and wm hold
VkBool32 en[8];
VkColorBlendEquationEXT eq[8];
VkColorComponentFlags wm[8];
} DynVals;
static void dyn_vals(const VkPipeState *s, DynVals *o) {
const poly_st *pl = &s->poly;
const ds_st *d = &s->ds;
memset(o, 0, sizeof *o);
o->cull = (VkCullModeFlags)(pl->cull & 3);
o->front = pl->front == NVN_FRONT_FACE_CW ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE;
o->topo = vk_pipeline_topology(s->prim);
o->dt = d->dt, o->dw = d->dw, o->st = d->st;
o->df = compare_op(d->df);
o->sop[0][0] = stencil_op(d->fail_f), o->sop[0][1] = stencil_op(d->zpass_f), o->sop[0][2] = stencil_op(d->zfail_f);
o->sop[1][0] = stencil_op(d->fail_b), o->sop[1][1] = stencil_op(d->zpass_b), o->sop[1][2] = stencil_op(d->zfail_b);
o->scmp[0] = compare_op(d->sf_f), o->scmp[1] = compare_op(d->sf_b);
unsigned mode = pl->mode <= NVN_POLYGON_MODE_FILL ? pl->mode : NVN_POLYGON_MODE_FILL;
o->bias = (pl->offset_en >> mode & 1) != 0;
o->discard = s->raster_discard != 0, o->restart = s->prim_restart != 0;
o->clamp = s->depth_clamp != 0, o->a2c = s->ms.a2c != 0;
o->poly = polygon_mode(pl->mode);
o->n = target_count(s);
for (int i = 0; i < o->n; i++) {
const blend_st *b = &s->blend[i];
o->en[i] = (s->color.blend_en >> i & 1) && blendable(target_format(s->color_fmt[i], NULL, NULL));
o->eq[i] = (VkColorBlendEquationEXT){blend_factor(b->src_rgb), blend_factor(b->dst_rgb), blend_op(b->eq_rgb),
blend_factor(b->src_a), blend_factor(b->dst_a), blend_op(b->eq_a)};
o->wm[i] = target_wmask(s, i);
}
}
// What the replay's command buffer holds, as vk_pipeline_set_dynamic_delta last recorded it (the replay thread only):
// the values, the inputs of the vertex input and the vertex bindings it reads. Invariant while the caller passes
// valid 1: cb holds DC.v for every dynamic state of P.dyn, colour attachments [0, DC.v.n) included.
static struct {
DynVals v;
const GfxShader *vs;
uint32_t vattr[16];
vs_st vstream[16];
uint32_t streams;
} DC;
uint32_t vk_pipeline_set_dynamic_delta(VkCommandBuffer cb, const VkPipeState *s, int valid, uint32_t *cmds) {
*cmds = 0;
if (!ensure_init()) return VK_PIPE_ALL_BINDINGS;
if (!valid) { // a new command buffer, or one whose dynamic state someone else set: everything, as before
uint32_t streams = vk_pipeline_set_dynamic(cb, s);
dyn_vals(s, &DC.v);
DC.vs = s->vs, DC.streams = streams;
memcpy(DC.vattr, s->vattr, sizeof DC.vattr), memcpy(DC.vstream, s->vstream, sizeof DC.vstream);
*cmds = 1;
return streams;
}
DynVals v;
dyn_vals(s, &v);
const DynVals *o = &DC.v;
uint32_t k = 0;
if (v.cull != o->cull) vkCmdSetCullMode(cb, v.cull), k++;
if (v.front != o->front) vkCmdSetFrontFace(cb, v.front), k++;
if (v.topo != o->topo) vkCmdSetPrimitiveTopology(cb, v.topo), k++;
if (v.dt != o->dt) vkCmdSetDepthTestEnable(cb, v.dt), k++;
if (v.dw != o->dw) vkCmdSetDepthWriteEnable(cb, v.dw), k++;
if (v.df != o->df) vkCmdSetDepthCompareOp(cb, v.df), k++;
if (v.st != o->st) vkCmdSetStencilTestEnable(cb, v.st), k++;
for (int f = 0; f < 2; f++)
if (memcmp(v.sop[f], o->sop[f], sizeof v.sop[f]) || v.scmp[f] != o->scmp[f])
vkCmdSetStencilOp(cb, f ? VK_STENCIL_FACE_BACK_BIT : VK_STENCIL_FACE_FRONT_BIT, v.sop[f][0], v.sop[f][1],
v.sop[f][2], v.scmp[f]),
k++;
if (v.bias != o->bias) vkCmdSetDepthBiasEnable(cb, v.bias), k++;
if (v.discard != o->discard) vkCmdSetRasterizerDiscardEnable(cb, v.discard), k++;
if (v.restart != o->restart) vkCmdSetPrimitiveRestartEnable(cb, v.restart), k++;
// The vertex input is made from the vertex shader's inputs and the attribute and stream states alone
// (vertex_input): a draw that keeps all three keeps it, and the bindings it reads.
if (s->vs != DC.vs || memcmp(s->vattr, DC.vattr, sizeof DC.vattr) || memcmp(s->vstream, DC.vstream, sizeof DC.vstream)) {
uint32_t streams = VK_PIPE_ALL_BINDINGS;
if (s->vs) {
VtxInput vi;
vertex_input(s, &vi);
if (P.dyn_vi) rec_vertex_input(P.set_vi, cb, vi.n_bind, vi.b, vi.n_attr, vi.a), k++;
streams = 0;
for (uint32_t i = 0; i < vi.n_bind; i++) streams |= 1u << vi.b[i].binding;
}
DC.vs = s->vs, DC.streams = streams;
memcpy(DC.vattr, s->vattr, sizeof DC.vattr), memcpy(DC.vstream, s->vstream, sizeof DC.vstream);
}
if (P.dyn_clamp && v.clamp != o->clamp) rec_bool(P.set_clamp, cb, v.clamp), k++;
if (P.dyn_poly && v.poly != o->poly) rec_polygon_mode(P.set_poly, cb, v.poly), k++;
if (P.dyn_a2c && v.a2c != o->a2c) rec_bool((PFN_vkCmdSetDepthClampEnableEXT)P.set_a2c, cb, v.a2c), k++;
// Attachments past o->n may hold nothing yet in this command buffer: a state with more targets sets all of them.
// One with fewer leaves the ones past its count as they were, which no draw of it reads.
uint32_t n = (uint32_t)v.n;
int all = v.n > o->n;
if (n && P.dyn_blend_en && (all || memcmp(v.en, o->en, n * sizeof *v.en))) rec_blend_enable(P.set_blend_en, cb, 0, n, v.en), k++;
if (n && P.dyn_blend_eq && (all || memcmp(v.eq, o->eq, n * sizeof *v.eq))) rec_blend_equation(P.set_blend_eq, cb, 0, n, v.eq), k++;
if (n && P.dyn_wmask && (all || memcmp(v.wm, o->wm, n * sizeof *v.wm))) rec_write_mask(P.set_wmask, cb, 0, n, v.wm), k++;
DC.v = v; // the invariant now covers [0, v.n) only
*cmds = k;
return DC.streams;
}
// ---- statistics, persistence, teardown
@@ -3172,7 +3303,8 @@ void vk_pipeline_stats(VkPipeStats *s) {
static PlatMutex save_lock = PLAT_MUTEX_INIT; // one cache save at a time; shutdown takes it to destroy the cache
void vk_pipeline_save(void) {
// One cache save; wait: queue for cache_use (the exit saves) rather than try for it (the autosave).
static void cache_save(int wait) {
if (P.ready) keys_flush();
if (P.ready && P.gpl) {
rt_log("vk_pipeline: gpl: %llu fast links (%llu by workers; avg %.2f ms, max %.2f ms, %llu failed), %llu draws "
@@ -3199,23 +3331,46 @@ void vk_pipeline_save(void) {
}
gfx_shader_record_flush(1);
if (!P.ready || !P.cache || !P.cache_path[0]) return;
// Not under P.lock: the driver synchronises P.cache, and holding P.lock through the copy and the file write (about
// 60 ms on the 1.4.3 fight) stopped every pipeline lookup of the replay, a freeze at each 20 s autosave.
// Not under P.lock: holding it through the copy and the file write (about 60 ms on the 1.4.3 fight) stopped every
// pipeline lookup of the replay, a freeze at each 20 s autosave. Only the copy takes cache_use, so no pipeline is
// being built into P.cache while the driver reads it.
plat_mutex_lock(&save_lock);
if (!P.cache) { // vk_pipeline_shutdown destroyed it
plat_mutex_unlock(&save_lock);
return;
}
uint64_t t0 = qpc();
if (wait) {
plat_mutex_lock(&cache_use);
} else {
// The autosave never queues for cache_use: a queued exclusive request on an SRW lock holds back new shared
// ones, so the drawing thread's next build would wait for a worker's build to end. It tries for about 2 s
// and otherwise saves on its next round.
int got = 0;
for (int i = 0; i < 1000 && !(got = plat_mutex_trylock(&cache_use)); i++) plat_sleep_ms(2);
if (!got) {
rt_log("vk_pipeline: cache save put off, pipelines were being built for %.0f ms", ms_since(t0));
plat_mutex_unlock(&save_lock);
return;
}
}
uint64_t t1 = qpc();
size_t size = 0;
void *data = NULL;
if (vkGetPipelineCacheData(P.device, P.cache, &size, NULL) == VK_SUCCESS && size && (data = malloc(size)) &&
vkGetPipelineCacheData(P.device, P.cache, &size, data) == VK_SUCCESS) {
if (!plat_file_write_atomic(P.cache_path, data, size)) rt_log("vk_pipeline: cannot write %s", P.cache_path);
}
int ok = vkGetPipelineCacheData(P.device, P.cache, &size, NULL) == VK_SUCCESS && size && (data = malloc(size)) &&
vkGetPipelineCacheData(P.device, P.cache, &size, data) == VK_SUCCESS;
plat_mutex_unlock(&cache_use);
// The copy time is how long a pipeline build on the drawing thread (a compute miss, under P.lock) can wait for
// this save; the wait is how long the save waited for builds in flight.
rt_log("vk_pipeline: cache %s: %.1f MB copied in %.1f ms, after %.1f ms waiting for builds", wait ? "save" : "autosave",
(double)size / 1e6, ms_since(t1), (double)(t1 - t0) / 1e6);
if (ok && !plat_file_write_atomic(P.cache_path, data, size)) rt_log("vk_pipeline: cannot write %s", P.cache_path);
free(data);
plat_mutex_unlock(&save_lock);
}
void vk_pipeline_save(void) { cache_save(1); }
// The game never shuts the device down (boot.c and the window end the process), so the cache is saved while it
// runs: at most every 20 s, and only after new pipelines were built. New pipeline keys are appended every 2 s.
static void autosave_once(void) {
@@ -3229,7 +3384,7 @@ static void autosave_once(void) {
vk_pipeline_stats(&s);
if (s.graphics + s.compute == last_n) return;
last_n = s.graphics + s.compute, last_ms = now;
vk_pipeline_save();
cache_save(0);
}
// On its own thread: the save (vkGetPipelineCacheData and the file write, about 60 ms) and the key appends (a flush to
+156 -48
View File
@@ -3,7 +3,8 @@
// call's arguments; its pointers point into the packet itself, so the recorder passes the payload to the driver as
// it is. A packet never wraps: one that does not fit before the end is preceded by a pad. The producer publishes head
// after each packet; the recorder makes the calls and advances tail, which the producer waits on when the ring is
// full and in rec_drain. While a part is recorded the recorder spins on head; idle, it sleeps on an event.
// full and in rec_drain. While a part is recorded the recorder spins on head; idle, it sleeps on an event. Packets also
// begin, end and submit the parts (rec_begin, rec_end_cb, rec_submit), so each part stays on the recorder throughout.
// Design: Savage Wind Recomp #189 and LAS Recomp runtime/nvn/vulkan/record.c (https://github.com/peiikai/LASRecomp
// @ a4b53a3, MIT, Copyright (c) 2026 Peiikai); the code is ours.
#define VK_REC_IMPL
@@ -18,21 +19,35 @@
#define RING_WORDS (1u << 21) // 16 MiB: a part of a submission is ~1-2 MiB of packets in the town
static uint64_t ring[RING_WORDS];
static volatile uint64_t head, tail; // words published by the producer / consumed by the recorder
static volatile int32_t sleeping; // the recorder waits for wake_ev
// The queue state, a cache line per writer. The recorder spins on head while it waits for packets, so a line the
// producer writes on every packet and the recorder reads or writes goes back and forth between their cores each time:
// with tail, Q.put_h, head and the producer's statistics in one line (the linker's order of these statics), handing
// packets over took 6% of the replay thread in the fort, more than building the descriptors.
static struct {
_Alignas(64) volatile uint64_t head; // words published by the producer; the recorder reads it
_Alignas(64) volatile uint64_t tail; // words consumed by the recorder; the producer reads it when it seems full
_Alignas(64) volatile int32_t sleeping; // the recorder waits for wake_ev; written only around a sleep
_Alignas(64) uint64_t put_h; // producer only: words written
uint64_t pub_h; // the last head published
uint64_t tail_seen; // tail as last read, at most the real one: the room it leaves is there
uint64_t pub_words; // TOTK_REC_PUB: publish head once this many words wait (pub)
} Q;
static PlatEvent wake_ev; // auto-reset
static uint64_t put_h; // producer: words written (published up to head)
static VkCommandBuffer rec_cb; // the part being recorded (set while the recorder is idle)
static VkCommandBuffer rec_cb; // producer: the part being recorded, whose vkCmd* become packets
static VkCommandBuffer r_cb; // recorder: the command buffer its packets go into (set by a BeginCb packet)
static int rec_live; // producer: rec_begin started the recorder for rec_cb
static int end_mode = -1; // TOTK_REC_END (rec_own)
static int sub_pending; // producer: the recorder ended a part that rec_submit has not handed over yet
static volatile VkResult r_sub_res; // recorder: the result of its last vkQueueSubmit2, read after the drain
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_h0; // Q.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_;
static struct { _Alignas(64) uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on, submits; } RS_;
enum {
R_PAD, R_BeginQuery, R_BeginRendering, R_BindDescriptorSets, R_BindIndexBuffer, R_BindPipeline, R_BindVertexBuffers2,
@@ -44,7 +59,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,
R_PassMark, R_BeginCb, R_Submit,
};
static uint64_t now_ns(void) { return plat_now_ns(); }
@@ -58,14 +73,14 @@ static void rec_main(void *arg) {
plat_thread_priority(rec_prio);
uint64_t t = 0;
for (;;) {
uint64_t h = __atomic_load_n(&head, __ATOMIC_ACQUIRE);
uint64_t h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
if (t == h) {
for (int i = 0; i < 4096 && t == h; i++) _mm_pause(), h = __atomic_load_n(&head, __ATOMIC_ACQUIRE);
for (int i = 0; i < 4096 && t == h; i++) _mm_pause(), h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
if (t == h) {
__atomic_store_n(&sleeping, 1, __ATOMIC_SEQ_CST);
h = __atomic_load_n(&head, __ATOMIC_SEQ_CST);
__atomic_store_n(&Q.sleeping, 1, __ATOMIC_SEQ_CST);
h = __atomic_load_n(&Q.head, __ATOMIC_SEQ_CST);
if (t == h) plat_event_wait_ns(&wake_ev, 2000000); // a missed wake costs at most 2 ms
__atomic_store_n(&sleeping, 0, __ATOMIC_RELAXED);
__atomic_store_n(&Q.sleeping, 0, __ATOMIC_RELAXED);
continue;
}
}
@@ -77,9 +92,9 @@ static void rec_main(void *arg) {
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
if (op != R_PAD) exec_packet(op, &ring[t % RING_WORDS + 1]);
t += n;
if ((k & 63) == 63) __atomic_store_n(&tail, t, __ATOMIC_RELEASE);
if ((k & 63) == 63) __atomic_store_n(&Q.tail, t, __ATOMIC_RELEASE);
}
__atomic_store_n(&tail, t, __ATOMIC_RELEASE);
__atomic_store_n(&Q.tail, t, __ATOMIC_RELEASE);
}
return;
}
@@ -88,54 +103,85 @@ static void rec_main(void *arg) {
static void wake(void) {
__atomic_thread_fence(__ATOMIC_SEQ_CST);
if (__atomic_load_n(&sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
}
// Room for need words: the tail read last leaves it in all but about one packet in 16 MiB of them.
static void wait_space(uint64_t need) {
if (put_h + need - __atomic_load_n(&tail, __ATOMIC_ACQUIRE) <= RING_WORDS) return;
if (Q.put_h + need - Q.tail_seen <= RING_WORDS) return;
Q.tail_seen = __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE);
if (Q.put_h + need - Q.tail_seen <= RING_WORDS) return;
RS_.full_waits++;
__atomic_store_n(&head, put_h, __ATOMIC_RELEASE);
Q.pub_h = Q.put_h;
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
wake();
while (put_h + need - __atomic_load_n(&tail, __ATOMIC_ACQUIRE) > RING_WORDS) _mm_pause();
while (Q.put_h + need - (Q.tail_seen = __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE)) > RING_WORDS) _mm_pause();
}
// Room for a packet of bytes of arguments; the caller fills it and calls pub().
static void *put(uint32_t op, size_t bytes) {
uint64_t n = 1 + (bytes + 7) / 8, pos = put_h % RING_WORDS;
uint64_t n = 1 + (bytes + 7) / 8, pos = Q.put_h % RING_WORDS;
if (n > RING_WORDS / 4) rt_fatal("vk_rec: a packet of %llu bytes", (unsigned long long)bytes);
if (pos + n > RING_WORDS) {
uint64_t pad = RING_WORDS - pos;
wait_space(pad);
ring[pos] = (uint64_t)R_PAD | pad << 32;
put_h += pad, pos = 0;
Q.put_h += pad, pos = 0;
}
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;
Q.put_h += n;
RS_.packets++, RS_.words += n;
return &ring[pos + 1];
}
// After a packet: head moves once Q.pub_words words wait, so the recorder reads a line the producer is still filling
// once per batch instead of once per packet. Whatever the producer waits for publishes everything first (wait_space,
// rec_drain), and the packets that begin and end a part go out at once (pub_now), so the recorder begins and ends
// command buffers as early as before; TOTK_REC_CHECK publishes every packet, since rec_check_handle searches the ring
// up to head.
static void pub_now(void) {
Q.pub_h = Q.put_h;
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
}
static void pub(void) {
__atomic_store_n(&head, put_h, __ATOMIC_RELEASE);
if (__atomic_load_n(&sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
if (Q.put_h - Q.pub_h >= Q.pub_words) pub_now();
}
void rec_drain(void) {
if (!rec_thread || __atomic_load_n(&tail, __ATOMIC_ACQUIRE) == put_h) return;
if (!rec_thread || __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE) == Q.put_h) return;
uint64_t t0 = now_ns();
__atomic_store_n(&head, put_h, __ATOMIC_RELEASE);
Q.pub_h = Q.put_h;
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
wake();
for (int i = 0; __atomic_load_n(&tail, __ATOMIC_ACQUIRE) != put_h; i++)
for (int i = 0; __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE) != Q.put_h; i++)
if (i < 20000) _mm_pause();
else plat_yield();
RS_.drains++, RS_.drain_ns += now_ns() - t0;
}
void rec_begin(VkCommandBuffer cb, uint64_t fence) {
static int rec_own(void) {
if (end_mode < 0) end_mode = rt_knob("REC_END", "1");
return end_mode;
}
// vkBeginCommandBuffer (one submission) and the breadcrumb, on the thread that records cb.
static void begin_now(VkCommandBuffer cb, int crumb, uint64_t n) {
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(cb, &bi);
if (crumb) vk_crumb(cb, crumb, n);
}
// The command buffer the following packets go into; begin: the recorder begins it (TOTK_REC_END=1).
typedef struct { VkCommandBuffer cb; int begin, crumb; uint64_t n; } A_BeginCb;
void rec_begin(VkCommandBuffer cb, uint64_t fence, int crumb, uint64_t n) {
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
@@ -154,6 +200,9 @@ void rec_begin(VkCommandBuffer cb, uint64_t fence) {
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;
const char *pw = cfg_get("TOTK_REC_PUB"); // words per publish (pub); 0: every packet
Q.pub_words = rec_chk ? 0 : pw && *pw ? (uint64_t)atoi(pw) : 64;
if (Q.pub_words > RING_WORDS / 8) Q.pub_words = RING_WORDS / 8;
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");
@@ -162,17 +211,29 @@ void rec_begin(VkCommandBuffer cb, uint64_t fence) {
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 (Q.pub_words != 64) rt_log("vk_rec: TOTK_REC_PUB: head published every %llu words",
(unsigned long long)Q.pub_words);
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));
}
}
}
RS_.parts++;
if (!RT_KNOB_ON(mode)) return;
int on = RT_KNOB_ON(mode), own = on && rec_own();
if (!own) {
// The submitting thread begins cb. The part pool is the recorder's while it has packets (an A/B switch
// between parts): it finishes them first.
rec_drain();
begin_now(cb, crumb, n);
if (!on) 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_h0, Q.put_h, __ATOMIC_RELEASE);
__atomic_store_n(&part_fence, fence, __ATOMIC_RELEASE);
A_BeginCb *a = put(R_BeginCb, sizeof *a);
*a = (A_BeginCb){cb, own, crumb, n};
pub_now();
rec_cb = cb;
rec_live = 1;
}
@@ -182,42 +243,82 @@ void rec_end(void) {
rec_live = 0;
}
// TOTK_REC_END (default 1): the thread that recorded the part also ends it, after the breadcrumb crumb (kind 0: none)
// that vk_sync.c writes at the end of a submission. On an RX 6700 XT (driver 26.10.1, issue #5) parts the recorder
// recorded and the submitting thread ended broke when a submission was split (vk_sync.c vk_sync_split): the GPU hung
// or the scene turned green, then white. The handoff in rec_drain orders the two threads, so Vulkan allows the old
// split; the driver did not cope with it. TOTK_REC_END=0 brings the old split back.
// TOTK_REC_END (default 1): a part the recorder records is the recorder's from its begin to its submission: it begins
// it, makes every command, ends it after the breadcrumb crumb (kind 0: none) that vk_sync.c writes at the end of a
// submission, and submits it. The submitting thread touches the part pool only while the recorder is idle. That is
// LAS Recomp's and Savage Wind Recomp's model (runtime/nvn/vulkan/record.h: command buffers allocated once, then only
// the recorder begins, records, ends and submits them). Before, the submitting thread began and submitted parts the
// recorder recorded and, before that, also ended them. The handoff in rec_drain orders the two threads, so Vulkan
// allows that split, but on an RX 6700 XT (driver 26.10.1, issue #5) parts ended by the other thread broke when a
// submission was split (vk_sync.c vk_sync_split): the GPU hung or the scene turned green, then white. Moving the end
// fixed that; begin and submit followed. TOTK_REC_END=0 brings the old split back: the submitting thread begins,
// ends and submits every part.
// The line is written only when a part was recorded by the recorder: with the recorder off (the default on AMD) the
// switch changes nothing, and reports read an unconditional line as proof that the recorder was running (#315).
typedef struct { int kind; uint64_t n; } A_EndCb;
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n) {
static int mode = -1, said;
if (mode < 0) mode = rt_knob("REC_END", "1");
static int said;
int own = rec_own();
if (rec_live && cb == rec_cb && !said) {
said = 1;
rt_log("vk_rec: a part the recorder records is ended by %s (TOTK_REC_END=%d)",
mode ? "the recorder" : "the submitting thread", mode);
rt_log("vk_rec: a part the recorder records is begun, ended and submitted by %s (TOTK_REC_END=%d)",
own ? "the recorder" : "the submitting thread", own);
}
if (!rec_live || cb != rec_cb || !mode) {
if (!rec_live || cb != rec_cb || !own) {
rec_end();
if (crumb) vk_crumb(cb, crumb, n);
return 0;
}
A_EndCb *a = put(R_EndCb, sizeof *a);
*a = (A_EndCb){crumb, n};
pub();
rec_drain();
pub_now();
rec_live = 0;
sub_pending = 1;
return 1;
}
// At most two command buffers and one signal: the shape vk_sync.c submits.
typedef struct { uint32_t ncb, nsig; VkCommandBufferSubmitInfo cbi[2]; VkSemaphoreSubmitInfo sig; } A_Submit;
VkResult rec_submit(const VkSubmitInfo2 *si) {
if (!sub_pending) {
vk_queue_lock();
VkResult r = vkQueueSubmit2(vk_ctx()->gfx_queue, 1, si, VK_NULL_HANDLE);
vk_queue_unlock();
return r;
}
sub_pending = 0;
if (si->commandBufferInfoCount > 2 || si->signalSemaphoreInfoCount > 1 || si->waitSemaphoreInfoCount || si->pNext)
rt_fatal("vk_rec: a submission of %u command buffers, %u waits and %u signals the recorder cannot carry",
si->commandBufferInfoCount, si->waitSemaphoreInfoCount, si->signalSemaphoreInfoCount);
A_Submit *a = put(R_Submit, sizeof *a);
memset(a, 0, sizeof *a);
a->ncb = si->commandBufferInfoCount, a->nsig = si->signalSemaphoreInfoCount;
for (uint32_t i = 0; i < a->ncb; i++) a->cbi[i] = si->pCommandBufferInfos[i];
if (a->nsig) a->sig = si->pSignalSemaphoreInfos[0];
pub();
// Waited for here, so the queue holds every earlier submission when the caller goes on, as with the recorder off:
// the present, uploads, readbacks and device waits on other threads rely on that order.
rec_drain();
RS_.submits++;
return r_sub_res;
}
static void submit_run(const A_Submit *a) {
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = a->ncb,
.pCommandBufferInfos = a->cbi, .signalSemaphoreInfoCount = a->nsig,
.pSignalSemaphoreInfos = a->nsig ? &a->sig : NULL};
vk_queue_lock();
r_sub_res = vkQueueSubmit2(vk_ctx()->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
}
void rec_stats_log(void) {
if (!(rt_trace & RT_TRACE_PERF) || !RS_.parts) return;
rt_log("vk_rec: %llu of %llu parts recorded by the recorder: %llu packets (%.1f MiB), %llu drains (%llu for a call "
"without a packet) waited %.2f ms in all, ring full %llu times",
"without a packet) waited %.2f ms in all, ring full %llu times, %llu submissions made by the recorder",
(unsigned long long)RS_.parts_on, (unsigned long long)RS_.parts, (unsigned long long)RS_.packets,
RS_.words * 8 / 1048576.0, (unsigned long long)RS_.drains, (unsigned long long)RS_.fallbacks,
RS_.drain_ns / 1e6, (unsigned long long)RS_.full_waits);
RS_.drain_ns / 1e6, (unsigned long long)RS_.full_waits, (unsigned long long)RS_.submits);
memset(&RS_, 0, sizeof RS_);
}
@@ -235,7 +336,7 @@ 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 h = __atomic_load_n(&Q.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]);
@@ -246,7 +347,7 @@ void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint6
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);
int made = p < __atomic_load_n(&Q.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)
@@ -642,7 +743,14 @@ TARGET_CMD(rec_write_mask, R_WriteMask, PFN_vkCmdSetColorWriteMaskEXT, VkColorCo
// ---- the recorder's side
static void exec_packet(uint32_t op, void *p) {
VkCommandBuffer cb = rec_cb;
if (op == R_BeginCb) {
A_BeginCb *a = p;
r_cb = a->cb;
if (a->begin) begin_now(r_cb, a->crumb, a->n);
return;
}
if (op == R_Submit) { submit_run(p); return; }
VkCommandBuffer cb = r_cb;
switch (op) {
#define F(name, ...) case R_##name: { A_##name *a = p; vkCmd##name(cb, __VA_ARGS__); break; }
F(BeginQuery, a->p, a->q, a->f)
+17 -11
View File
@@ -1,11 +1,12 @@
// The recorder thread (vk_rec.c): the Vulkan calls the replay makes into its command buffer are made by a thread of
// their own. vk_sync.c brackets each part of a submission with rec_begin (after vkBeginCommandBuffer) and rec_end
// (before vkEndCommandBuffer). In between, every vkCmd* on that command buffer copies its arguments into a packet of a
// ring, in call order, and the recorder makes the real call in that order; calls into any other command buffer (uploads,
// the window, probes) are made at once. A call the ring has no packet for (a structure with a pNext chain) waits for
// the recorder to empty the ring and is then made at once on the calling thread. rec_end returns once the recorder made
// every call. vk_sync.c ends a part through rec_end_cb: the recorder makes the last calls into a part it recorded,
// vkEndCommandBuffer included; submitting and the command pool stay on the submitting thread.
// The recorder thread (vk_rec.c): the Vulkan calls the replay makes into its command buffers are made by a thread of
// their own. vk_sync.c begins each part of a submission (and the mirror prologue) with rec_begin, ends it with
// rec_end_cb and submits it with rec_submit. In between, every vkCmd* on that command buffer copies its arguments
// into a packet of a ring, in call order, and the recorder makes the real call in that order; calls into any other
// command buffer (uploads, the window, probes) are made at once. A call the ring has no packet for (a structure with a
// pNext chain) waits for the recorder to empty the ring and is then made at once on the calling thread. With
// TOTK_REC_END=1 (default) the recorder also begins, ends and submits the parts it records, so a part's command buffer
// never changes threads; rec_submit returns once the recorder submitted it. The submitting thread allocates from the
// part pool only while the recorder is idle (vk_sync.c claim_slot).
//
// Every vkCmd* the runtime calls through vk.h goes through the rc_* wrappers below (the macros at the end); the
// extension commands called through function pointers have rec_* wrappers of their own. TOTK_VK_RECORDER=0 makes every
@@ -15,11 +16,16 @@
// 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, uint64_t fence); // the submitting thread, sub_lock held; fence: the part's value
// The submitting thread, sub_lock held: vkBeginCommandBuffer (one submission) on cb and the breadcrumb crumb (0: none,
// vk_crumbs.c) with number n, made by the recorder when it records the part, else at once; fence: the part's value.
void rec_begin(VkCommandBuffer cb, uint64_t fence, int crumb, uint64_t n);
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).
// The end of part cb: the breadcrumb crumb, then vkEndCommandBuffer, both queued for the recorder when it records cb
// (1: rec_submit hands the part over); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
// vkQueueSubmit2 of si (at most two command buffers, one signal, no wait, no fence) under vk_queue_lock: by the
// recorder after a part it ended, waited for, else at once.
VkResult rec_submit(const VkSubmitInfo2 *si);
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
+11 -23
View File
@@ -309,7 +309,7 @@ void vk_sync_start(void) {
if (reserved < now) reserved = now;
stop = 0;
plat_mutex_unlock(&st_lock);
vk_mem_note_submit(now);
vk_mem_note_fence(now);
completion_thread = plat_thread_new(completion_main, c, "vk completion");
if (!completion_thread) rt_fatal("vk: cannot start the completion thread");
__atomic_store_n(&started, 1, __ATOMIC_RELEASE);
@@ -328,6 +328,7 @@ static int claim_slot(VkCtx *c, uint64_t fence) {
slots[i].fence = fence;
return i;
}
rec_drain(); // the part pool is the recorder's while it has packets (vk_rec.h): it is idle for the allocation
CmdSlot *n = realloc(slots, sizeof *n * (size_t)(nslots + 1));
if (!n) rt_fatal("vk: out of memory (command buffers)");
slots = n;
@@ -353,13 +354,11 @@ static VkCommandBuffer prologue(VkCtx *c, uint64_t fence) {
if (!vk_mem_mirror_pending()) return VK_NULL_HANDLE;
int i = claim_slot(c, fence); // before reading slots: the claim may move it
VkCommandBuffer cb = slots[i].cb;
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(cb, &bi);
rec_begin(cb, fence, 0, 0); // a command buffer of the part pool: the recorder's like the parts
int pair = ts_open(cb);
vk_mem_mirror_prologue(cb);
ts_close(cb, pair, fence);
vkEndCommandBuffer(cb);
if (!rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb);
return cb;
}
@@ -375,21 +374,14 @@ VkCommandBuffer vk_sync_split(void) {
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = pro ? 2 : 1,
.pCommandBufferInfos = pro ? cbi : cbi + 1};
ts_submitted();
vk_queue_lock();
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
VkResult r = rec_submit(&si); // by the recorder when it recorded the part
if (r != VK_SUCCESS) {
vk_device_fault_report(r);
rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
}
vk_mem_note_submit(cur_fence); // objects destroyed from here on may be in use by the part just submitted
CmdSlot *s = take_slot(c, cur_fence); // the submitted part keeps its slot until the value is reached
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
rec_begin(s->cb, cur_fence, VK_CRUMB_SUBMIT, cur_fence);
ts_begin(s->cb);
rec_begin(s->cb, cur_fence);
return s->cb;
}
@@ -411,14 +403,13 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
vk_mem_dirty_submit(); // TOTK_TRACE=perf: what a mirror would upload before this submit
uint64_t fence = __atomic_load_n(&issued, __ATOMIC_ACQUIRE) + 1;
cur_fence = fence;
// Before the first object is resolved: what any thread destroys from here on waits for this value (vk_memory.c
// defer), since the recording may already name it.
vk_mem_note_fence(fence);
CmdSlot *s = take_slot(c, fence);
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
uint64_t t0 = perf_begin();
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
rec_begin(s->cb, fence, VK_CRUMB_SUBMIT, fence); // begun by the recorder when it records the part
ts_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);
@@ -439,15 +430,12 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
.pCommandBufferInfos = pro ? cbi : cbi + 1, .signalSemaphoreInfoCount = 1,
.pSignalSemaphoreInfos = &sig};
ts_submitted();
vk_queue_lock();
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
VkResult r = rec_submit(&si); // by the recorder when it recorded the part
if (r != VK_SUCCESS) {
vk_device_fault_report(r);
rt_fatal("vk: vkQueueSubmit2 of value %llu failed (%d)", (unsigned long long)fence, (int)r);
}
s->fence = recorded ? fence : 0;
vk_mem_note_submit(fence);
plat_mutex_lock(&st_lock);
__atomic_store_n(&issued, fence, __ATOMIC_RELEASE);
plat_cond_broadcast(&cv_work);
+455 -17
View File
@@ -61,6 +61,10 @@ typedef struct Tex {
uint8_t *snap; // a texture that fills no page: its storage bytes when the GPU write was recorded
uint32_t wt_n; // WriteTexels into the storage since the last upload
struct Tex *rel_prev, *rel_next; // while released: the list of released entries, oldest first
uint64_t rel_ms; // while released: when (coarse_ms)
uint64_t use_seq; // order of the last creation or take-back among all entries
uint64_t rel_seq; // while released: use_seq of the newest entry placed before this release; an entry
// with a later use_seq on overlapping storage replaced this one's bytes
} Tex;
// A slot keeps what vk_texture_by_id resolved for it, so a draw binding skips the key lookup and the address chain: the
@@ -540,6 +544,46 @@ static void rel_unlink(Tex *e) {
e->rel_prev = e->rel_next = NULL;
}
// A clock read per submit, for stamps on paths that run many times a frame.
static uint64_t coarse_ms;
static uint64_t use_counter;
// How long released entries wait until the game takes them back, split into those holding GPU-written content and the
// others (TOTK_TRACE=perf, vk_texture_released_log): where transient textures end and the tail that can go begins.
enum { AGE_1S, AGE_5S, AGE_30S, AGE_120S, AGE_LONG, AGE_N };
static const char *const age_name[AGE_N] = {"<1s", "<5s", "<30s", "<120s", ">=120s"};
static uint64_t takeback_n[2][AGE_N];
static int age_bucket(uint64_t ms) {
return ms < 1000 ? AGE_1S : ms < 5000 ? AGE_5S : ms < 30000 ? AGE_30S : ms < 120000 ? AGE_120S : AGE_LONG;
}
// Keys of entries the pressure path dropped, to count the ones the game asks for again soon after: a drop that buys
// nothing and, for GPU-written content, loses it. Direct-mapped, a lost record only undercounts.
#define DROPPED_SLOTS 4096u
static struct { uint64_t hash, ms; } dropped[DROPPED_SLOTS];
static uint64_t n_recreated[2], n_recreated_written[2]; // index: within 10 s, later
static uint64_t key_hash(const TexKey *k) {
uint64_t h = 0xcbf29ce484222325ull;
const uint8_t *b = (const uint8_t *)k;
for (size_t i = 0; i < sizeof *k; i++) h = (h ^ b[i]) * 0x100000001b3ull;
return h | 1;
}
static void dropped_note(const Tex *e) {
uint64_t h = key_hash(&e->key);
dropped[h >> 3 & (DROPPED_SLOTS - 1)].hash = h ^ (uint64_t)(e->pub.gpu_owned != 0) << 1;
dropped[h >> 3 & (DROPPED_SLOTS - 1)].ms = coarse_ms;
}
static void dropped_check(const TexKey *k) {
uint64_t h = key_hash(k);
unsigned i = h >> 3 & (DROPPED_SLOTS - 1);
uint64_t was = dropped[i].hash;
if (!was || (was & ~2ull) != (h & ~2ull)) return;
int later = coarse_ms - dropped[i].ms >= 10000;
n_recreated[later]++;
if (was != h) n_recreated_written[later]++;
dropped[i].hash = 0;
}
static void destroy_tex(Tex *e) {
own_remove(e);
tex_gen_bump(e->key.addr);
@@ -553,10 +597,16 @@ static void destroy_tex(Tex *e) {
if (p->next_at == e) { p->next_at = e->next_at; break; }
}
if (e->released) nreleased--, rel_unlink(e);
// Another thread may hold &e->pub, taken under the lock and used after it (vk_texture_get, vk_texture_by_id: render
// targets for a whole pass, vk_exec.c resolve_target, vk_texture_storage_bound_locked writes through it). Freed
// here, that thread read a stale VkImage from freed memory, made views of it and cached them by that handle. The
// record goes with the image, once the submission being recorded is done; gone keeps views of it out of the caches.
e->pub.gone = 1;
vk_exec_image_gone(e->pub.image);
vk_defer_image(e->pub.image, e->alloc);
free(e->snap);
free(e);
e->snap = NULL;
vk_defer_free(e);
}
// Released entries with another descriptor at the same address are replaced; past RELEASED_MAX released entries the
@@ -569,15 +619,112 @@ static void sweep_released(void) {
while (nreleased > RELEASED_MAX && rel_head) destroy_tex(rel_head);
}
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each submit drops up to
// PRESSURE_STEP of the oldest released entries, so a 4 GiB card does not keep RELEASED_MAX released images (each ASTC
// one decoded to RGBA8) while the heap spills into system memory. A few per submit, for the reason above. Only entries
// that come back intact are dropped: mirrored ones the GPU never wrote, re-uploaded from their storage. A render target
// lives only in its image; dropping it brought the texture back empty (black flashes in a Depths boss fight, ~170
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each budget query drops up
// to PRESSURE_STEP released entries, oldest first, until the use is back under the line; a few at a time, for the reason
// above. Two kinds come back without loss:
//
// Overwritten: the game placed another texture on the entry's storage after releasing it, so on the Switch its bytes are
// gone too, and it has waited DEAD_AGE_MS. Transient render targets are taken back within a second or two (a stand-still
// field window: about 19000 take-backs under 1 s, 2 later than that, none after 30 s), while their leftovers at
// addresses the game's allocator moved on from stay released for good: 1.9 GiB of 2.0 GiB, nearly all of it render targets
// no storage can refill. Not bound through a finalized slot for DEAD_QUIET_SUBMITS either: such a slot still samples the
// entry (slot_resolve).
//
// Clean: mirrored ones the GPU never wrote, re-uploaded from their storage, at the cost of the upload, so after
// CLEAN_AGE_MS: without the wait a fight re-created about 500 of them per 10 s. Any other render target lives only in its
// image: dropping one that was merely old brought the texture back empty (black flashes in a Depths boss fight, ~170
// "left uninitialised" aliases a minute).
#define PRESSURE_STEP 32
#define CLEAN_AGE_MS 5000u
#define DEAD_AGE_MS 30000u
#define DEAD_QUIET_SUBMITS 600u
#define DEAD_RETRY_QUERIES 8 // budget queries to skip after a search that found nothing
static int vram_pressure = -1;
static uint64_t n_pressure_drops;
static uint64_t n_pressure_drops, n_pressure_overwritten;
static int tex_by_addr(const void *a, const void *b) {
const Tex *x = *(Tex *const *)a, *y = *(Tex *const *)b;
return x->pub.addr < y->pub.addr ? -1 : x->pub.addr > y->pub.addr;
}
// All entries sorted by storage address, with the highest storage end up to each one: the input of storage_taken.
typedef struct { Tex **e; uint64_t *end; int n; } AddrIndex;
static int addr_index(AddrIndex *x) {
x->n = 0;
x->e = malloc(sizeof *x->e * (size_t)(by_key.n + 1));
x->end = malloc(sizeof *x->end * (size_t)(by_key.n + 1));
if (!x->e || !x->end) { free(x->e), free(x->end); return 0; }
for (uint32_t i = 0; i < by_key.cap; i++)
if (by_key.vals[i]) x->e[x->n++] = by_key.vals[i];
qsort(x->e, (size_t)x->n, sizeof *x->e, tex_by_addr);
uint64_t top = 0;
for (int i = 0; i < x->n; i++) {
uint64_t end = x->e[i]->pub.addr + x->e[i]->t.L.size;
x->end[i] = top = end > top ? end : top;
}
return 1;
}
static void addr_index_free(AddrIndex *x) { free(x->e), free(x->end); }
// A texture placed after the release of x->e[i] overlaps its storage.
static int storage_taken(const AddrIndex *x, int i) {
const Tex *e = x->e[i];
for (int j = i; --j >= 0 && x->end[j] > e->pub.addr;)
if (x->e[j]->pub.addr + x->e[j]->t.L.size > e->pub.addr && x->e[j]->use_seq > e->rel_seq) return 1;
for (int j = i; ++j < x->n && x->e[j]->pub.addr < e->pub.addr + e->t.L.size;)
if (x->e[j]->use_seq > e->rel_seq) return 1;
return 0;
}
static uint64_t own_bytes(const Tex *e) {
VkCtx *c = vk_ctx();
if (!e->alloc || !c) return 0;
VmaAllocationInfo ai;
vmaGetAllocationInfo(c->vma, e->alloc, &ai);
return ai.size;
}
static int dead_candidate(const Tex *e) {
return e->alloc && (!e->checked || submit_epoch - e->checked >= DEAD_QUIET_SUBMITS);
}
// Drops overwritten entries until `need` bytes are free or the step is used up. The sort runs only under pressure, while
// an old enough entry exists, and rests for a while when it finds nothing to drop. Returns the bytes freed.
static uint64_t pressure_search_ns; // longest search since the last mem: released line
static uint64_t drop_overwritten(uint64_t need, int *dropped) {
static int rest;
if (rest > 0) { rest--; return 0; }
if (!rel_head || coarse_ms - rel_head->rel_ms < DEAD_AGE_MS) return 0;
uint64_t t0 = plat_now_ns(), freed = 0;
AddrIndex x;
if (!addr_index(&x)) return 0;
Tex *cand[PRESSURE_STEP];
int nc = 0;
for (Tex *e = rel_head; e && nc < PRESSURE_STEP && coarse_ms - e->rel_ms >= DEAD_AGE_MS; e = e->rel_next) {
if (!dead_candidate(e)) continue;
int lo = 0, hi = x.n; // the first entry at e's address, then e among those
while (lo < hi) {
int m = (lo + hi) / 2;
if (x.e[m]->pub.addr < e->pub.addr) lo = m + 1;
else hi = m;
}
while (lo < x.n && x.e[lo] != e) lo++;
if (lo < x.n && storage_taken(&x, lo)) cand[nc++] = e;
}
addr_index_free(&x);
for (int i = 0; i < nc && freed < need; i++) {
freed += own_bytes(cand[i]);
dropped_note(cand[i]);
destroy_tex(cand[i]);
(*dropped)++, n_pressure_drops++, n_pressure_overwritten++;
}
if (!nc) rest = DEAD_RETRY_QUERIES;
uint64_t ns = plat_now_ns() - t0;
if (ns > pressure_search_ns) pressure_search_ns = ns;
return freed;
}
static void sweep_pressure(void) { // tex_lock held
if (vram_pressure < 0) {
@@ -589,11 +736,16 @@ static void sweep_pressure(void) { // tex_lock held
if (!vram_pressure || !rel_head || submit_epoch & 7) return; // the budget query every 8 submits
uint64_t used, budget;
int exact;
if (!vk_mem_vram(&used, &budget, &exact) || used <= budget / 100 * (uint64_t)vram_pressure) return;
if (!vk_mem_vram(&used, &budget, &exact)) return;
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
if (used <= line) return;
int dropped = 0;
for (Tex *e = rel_head, *next; e && dropped < PRESSURE_STEP; e = next) {
uint64_t need = used - line, freed = drop_overwritten(need, &dropped);
for (Tex *e = rel_head, *next; e && freed < need && dropped < PRESSURE_STEP && coarse_ms - e->rel_ms >= CLEAN_AGE_MS; e = next) {
next = e->rel_next;
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
freed += own_bytes(e);
dropped_note(e);
destroy_tex(e);
dropped++, n_pressure_drops++;
}
@@ -673,15 +825,20 @@ static void astc_check(const Tex *e, const uint8_t *raw, const VkAstcRegion *ar,
// large ASTC array decodes to RGBA8 in one go: a staging buffer of 676682336 bytes failed (-2) on an Intel UHD with
// 8 GB of shared memory and the game closed before the title screen.
#define UPLOAD_PART (32ull << 20) // fits the staging pool (vk_staging takes up to half of its 64 MiB block)
// The open upload batch is submitted between chunks (and parts of the CPU path) once its staging and scratch pass this:
// on fight2's first world frame one batch took hundreds of MiB, and the staging and scratch pools grew by 64 MiB blocks
// on the replay thread. Submitting earlier keeps the queue order (the batch still precedes the part that samples the
// images) and lets the GPU finish and return the buffers sooner.
#define UPLOAD_HELD_MAX (48ull << 20)
// Copies the whole storage into the image (every level and layer the image has).
static void upload(Tex *e) {
static int upload_gpu(Tex *e);
// The CPU side of upload: deswizzles (and decodes or expands) into staging buffers, then copies them into the image.
static void upload_cpu(Tex *e) {
static volatile int32_t n_bad, n_undecoded, n_split;
const tex_t *t = &e->t;
const nvn_texdesc *d = &t->d;
int bw, bh, bpb = nvn_fmt_bpb(d->format, &bw, &bh);
if (!bpb || !t->mem) return;
uint64_t p0 = perf_begin(), u0 = kern_now_ns();
int linear = nvn_tex_is_linear(d), is3d = d->target == NVN_TEXTURE_TARGET_3D, one_d = e->pub.type == VK_IMAGE_TYPE_1D;
uint32_t levels = linear ? 1 : e->pub.levels, layers = linear ? 1 : e->pub.layers;
int astc = e->f.astc_bw != 0, rgb = e->f.expand_rgb;
@@ -762,6 +919,7 @@ static void upload(Tex *e) {
}
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
vk_upload_end();
if (!acheck && vk_upload_held() > UPLOAD_HELD_MAX) vk_upload_flush(0);
}
if (parts > 1)
log_limited(&n_split, "texture %#llx fmt %#x %ux%u, %u levels, %u layers: %.1f MiB staged in %d parts",
@@ -778,7 +936,201 @@ static void upload(Tex *e) {
free(raw_copy);
free(ar);
free(rg);
}
// Block-linear storage in an imported pool, deswizzled by the GPU (vk_deswizzle.c): the replay thread only records
// the dispatches, the image copies and (ASTC) the decode into the upload batch the CPU path also uses, which runs before
// the part that samples the image. The GPU reads the pool when the batch runs, as the Switch's GPU reads it at
// execution; a CPU write after make_fresh's write-watch reset still marks the pages and brings another upload. Each
// chunk of regions gets a device-local scratch buffer of at most DESWZ_CHUNK bytes (one region, when larger), freed
// once the batch completes. TOTK_DESWIZZLE=check compares every chunk with the CPU deswizzle (waits for the GPU).
// 0 when not handled, before anything is recorded: the CPU path takes the texture.
#define DESWZ_CHUNK (32ull << 20)
// Uploads by path since the last perf summary (TOTK_TRACE=perf): 0 GPU, else why the CPU took it.
enum { UP_GPU, UP_MODE, UP_LINEAR, UP_RGB, UP_ASTC_CPU, UP_BPB, UP_NOT_LIVE, UP_GEOM, UP_LIMITS, UP_SCRATCH, UP_WHY };
static const char *const up_why_name[UP_WHY] = {"gpu", "cpu mode", "linear", "rgb", "astc on cpu", "bytes per block",
"not an imported pool", "layout", "device limits", "scratch"};
static uint64_t up_n[UP_WHY], up_bytes[UP_WHY], up_ns[UP_WHY];
static int up_why; // upload_gpu's reason for returning 0 (texture lock held)
static int upload_gpu(Tex *e) {
static volatile int32_t n_scratch_fail, n_undecoded, n_bad;
static volatile int64_t n_checked, n_differ;
const tex_t *t = &e->t;
const nvn_texdesc *d = &t->d;
int mode = vk_deswz_mode();
up_why = mode == VK_DESWZ_CPU ? UP_MODE : nvn_tex_is_linear(d) ? UP_LINEAR : e->f.expand_rgb ? UP_RGB : UP_GPU;
if (up_why) return 0;
int astc = e->f.astc_bw != 0, agpu = astc && vk_astc_gpu();
if (astc && (!agpu || vk_astc_mode() == VK_ASTC_CHECK)) return up_why = UP_ASTC_CPU, 0; // or ASTC's check mode
int bw, bh, bpb = nvn_fmt_bpb(d->format, &bw, &bh);
if (!bpb || 16 % bpb) return up_why = UP_BPB, 0; // the rows of the linear copy are padded to 16 bytes
VkMemSpan sp;
if (!vk_mem_resolve(e->pub.addr, t->L.size, &sp) || !vk_mem_span_live(&sp)) return up_why = UP_NOT_LIVE, 0;
int one_d = e->pub.type == VK_IMAGE_TYPE_1D, is3d = d->target == NVN_TEXTURE_TARGET_3D;
uint32_t levels = e->pub.levels, layers = e->pub.layers, n = levels * layers, k = 0;
VkDeswzRegion *dr = calloc(n, sizeof *dr);
VkBufferImageCopy *rg = calloc(n, sizeof *rg);
VkAstcRegion *ar = agpu ? calloc(n, sizeof *ar) : NULL;
if (!dr || !rg || (agpu && !ar)) rt_fatal("vk: out of memory (texture upload regions)");
int ok = 1;
VkDeviceSize max_dst = DESWZ_CHUNK;
for (uint32_t lv = 0; ok && lv < levels; lv++)
for (uint32_t ly = 0; ok && ly < layers; ly++) {
uint32_t row_bytes, rows, slices, gbpb;
uint64_t base;
unsigned th, td;
uint32_t w = mip(d->width, lv), h = one_d ? 1 : mip(d->height, lv), sl = is3d ? mip(d->depth, lv) : 1;
if (nvn_deswizzle_geom(d, lv, ly, &row_bytes, &rows, &slices, &base, &th, &td, &gbpb) || gbpb != (uint32_t)bpb ||
base >= t->L.size || rows != (h + bh - 1) / bh || slices != sl) {
ok = 0;
break;
}
uint64_t bhg = 1ull << th, bdg = 1ull << td, gobs_x = (row_bytes + 63) / 64;
uint64_t blocks_y = (rows + 8 * bhg - 1) / (8 * bhg), blocks_z = (slices + bdg - 1) / bdg;
uint64_t segs = gobs_x * blocks_y * blocks_z * (32ull << (th + td)), room = (t->L.size - base) / 16;
if (segs > room) segs = room;
uint32_t pitch16 = (row_bytes + 15) / 16;
VkDeviceSize out = (VkDeviceSize)pitch16 * 16 * rows * slices;
if (segs > UINT32_MAX || gobs_x * blocks_y > UINT32_MAX) { ok = 0; break; }
if (out > max_dst) max_dst = out;
dr[k] = (VkDeswzRegion){.src = base, .segs = (uint32_t)segs, .pitch16 = pitch16, .rows = rows, .slices = slices,
.th = th, .td = td, .gobs_x = (uint32_t)gobs_x, .blocks_y = (uint32_t)blocks_y};
rg[k] = (VkBufferImageCopy){.bufferRowLength = pitch16 * 16 / (uint32_t)bpb * (uint32_t)bw,
.bufferImageHeight = rows * (uint32_t)bh,
.imageSubresource = {e->pub.aspect, lv, ly, 1}, .imageExtent = {w, h, sl}};
if (agpu) ar[k] = (VkAstcRegion){.w = w, .h = h, .slices = sl, .level = lv, .layer = ly};
k++;
}
if (!ok || k != n || !vk_deswz_fits(sp.offset, t->L.size, dr, (int)n, max_dst)) {
free(dr), free(rg), free(ar);
return up_why = !ok || k != n ? UP_GEOM : UP_LIMITS, 0;
}
VkCtx *c = vk_ctx();
VkCommandBuffer cb = vk_upload_begin();
// Earlier GPU writes into the pool (copies, shaders) before the reads; the image's earlier uses before its copy.
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
for (uint32_t k0 = 0, k1; k0 < n; k0 = k1) {
VkDeviceSize size = 0;
for (k1 = k0; k1 < n; k1++) {
VkDeviceSize out = (VkDeviceSize)dr[k1].pitch16 * 16 * dr[k1].rows * dr[k1].slices;
if (k1 > k0 && size + out > DESWZ_CHUNK) break;
dr[k1].dst = size, rg[k1].bufferOffset = size;
if (agpu) ar[k1].src = size;
size += out;
}
VkBuffer sb;
VmaAllocation sa;
if (vk_scratch_buffer(size, &sb, &sa) != VK_SUCCESS) {
log_limited(&n_scratch_fail, "texture %#llx: deswizzle scratch of %llu bytes failed%s",
(unsigned long long)e->pub.addr, (unsigned long long)size,
k0 ? "; its later levels left as they were" : "; deswizzled on the CPU");
if (!k0) {
vk_upload_end();
free(dr), free(rg), free(ar);
return up_why = UP_SCRATCH, 0;
}
break;
}
if (!vk_deswz_record(cb, sp.buffer, sp.offset, t->L.size, sb, size, dr + k0, (int)(k1 - k0))) {
log_limited(&n_bad, "texture %#llx: deswizzle of levels %u-%u not recorded; left as they were",
(unsigned long long)e->pub.addr, k0, k1 - 1); // vk_deswz_fits passed: not expected
vk_defer_buffer(sb, sa);
break;
}
barrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
if (agpu) {
uint64_t a0 = perf_begin();
if (!vk_astc_record(cb, sb, size, ar + k0, (int)(k1 - k0), e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb,
e->f.astc_bc, e->pub.image, e->pub.aspect, VK_NULL_HANDLE))
log_limited(&n_undecoded, "ASTC texture %#llx (fmt %#x %ux%u) not decoded on the GPU: left as it was",
(unsigned long long)e->pub.addr, d->format, d->width, d->height);
perf_end(PERF_ASTC, a0);
} else {
vkCmdCopyBufferToImage(cb, sb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, k1 - k0, rg + k0);
}
if (mode == VK_DESWZ_CHECK) { // the linear copy back to the host, compared with the CPU deswizzle
VkBuffer rb;
VmaAllocation ra;
const uint8_t *rmap = vk_staging(size, &rb, &ra);
VkBufferCopy cp = {0, 0, size};
vkCmdCopyBuffer(cb, sb, rb, 1, &cp);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT,
VK_ACCESS_2_HOST_READ_BIT);
vk_defer_buffer(sb, sa);
vk_upload_end();
vk_upload_flush(1);
vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE);
for (uint32_t i = k0; i < k1; i++) {
const VkDeswzRegion *r = &dr[i];
size_t pitch = (size_t)r->pitch16 * 16, bytes = pitch * r->rows * r->slices;
uint8_t *cpu = malloc(bytes);
if (!cpu) rt_fatal("vk: out of memory (deswizzle check)");
uint32_t row_bytes = 0, rows, slices, gbpb;
uint64_t base;
unsigned th, td;
nvn_deswizzle_geom(d, rg[i].imageSubresource.mipLevel, rg[i].imageSubresource.baseArrayLayer, &row_bytes,
&rows, &slices, &base, &th, &td, &gbpb);
nvn_deswizzle(d, rg[i].imageSubresource.mipLevel, rg[i].imageSubresource.baseArrayLayer, t->mem, cpu, pitch);
uint64_t bad = 0, first = UINT64_MAX;
for (uint64_t row = 0; row < (uint64_t)r->rows * r->slices; row++)
if (memcmp(cpu + row * pitch, rmap + r->dst + row * pitch, row_bytes)) {
bad++;
if (first == UINT64_MAX) first = row;
}
free(cpu);
plat_add64(&n_checked, 1);
if (bad) {
plat_add64(&n_differ, 1);
rt_log("vk: deswizzle check: texture %#llx fmt %#x %ux%u level %u layer %u: %llu of %u rows differ, the "
"first %llu (th %u td %u, %u rows of %u bytes, %u slices)", (unsigned long long)e->pub.addr,
d->format, d->width, d->height, rg[i].imageSubresource.mipLevel,
rg[i].imageSubresource.baseArrayLayer, (unsigned long long)bad, r->rows * r->slices,
(unsigned long long)first, r->th, r->td, r->rows, row_bytes, r->slices);
}
}
vmaDestroyBuffer(c->vma, rb, ra);
if ((n_checked & 1023) == 0 || n_checked < 2)
rt_log("vk: deswizzle check: %lld regions compared, %lld differ", (long long)n_checked, (long long)n_differ);
cb = vk_upload_begin();
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
continue;
}
vk_defer_buffer(sb, sa);
if (k1 < n && vk_upload_held() > UPLOAD_HELD_MAX) { // submit what is recorded, go on in a new batch
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL_RW);
vk_upload_end();
vk_upload_flush(0);
cb = vk_upload_begin();
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
}
}
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL_RW);
vk_upload_end();
if (vk_upload_held() > UPLOAD_HELD_MAX) vk_upload_flush(0);
free(dr), free(rg), free(ar);
return 1;
}
// Copies the whole storage into the image (every level and layer the image has): on the GPU where it can
// (upload_gpu), else on the CPU.
static void upload(Tex *e) {
const tex_t *t = &e->t;
const nvn_texdesc *d = &t->d;
int bw, bh;
if (!nvn_fmt_bpb(d->format, &bw, &bh) || !t->mem) return;
uint64_t p0 = perf_begin(), u0 = kern_now_ns();
int why = UP_GPU;
if (!upload_gpu(e)) why = up_why, upload_cpu(e);
uint64_t dt = kern_now_ns() - u0;
up_n[why]++, up_bytes[why] += t->L.size, up_ns[why] += dt;
if (e->uploaded) {
n_reuploads++, perf_count(PERF_CNT_TEX_REUP);
ru_n++, ru_bytes += t->L.size, ru_ns += dt;
@@ -947,6 +1299,8 @@ static Tex *entry_of(const tex_t *t) {
}
if (!(e = create_tex(t, &k, tex_alias))) return NULL;
perf_count(PERF_CNT_TEX_NEW);
dropped_check(&k);
e->use_seq = ++use_counter;
// released entries of another descriptor at this address are replaced by the new one; a live one holding
// GPU-written content is the alias source, preferring the one that differs only in swizzle (same_but_swizzle)
int alias = 0;
@@ -982,7 +1336,11 @@ static Tex *entry_of(const tex_t *t) {
nvn_hm_put(&by_key, &k, e);
tex_gen_bump(k.addr);
}
if (e->released) e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(k.addr);
if (e->released) {
takeback_n[e->pub.gpu_owned != 0][age_bucket(coarse_ms - e->rel_ms)]++;
e->use_seq = ++use_counter;
e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(k.addr);
}
e->t = *t;
return e;
}
@@ -1073,6 +1431,8 @@ void vk_texture_release(const tex_t *t) {
Tex *e = nvn_hm_get(&by_key, &k);
if (e && !e->released) {
e->released = 1;
e->rel_ms = coarse_ms;
e->rel_seq = use_counter;
nreleased++;
rel_append(e);
tex_gen_bump(k.addr);
@@ -1236,6 +1596,7 @@ uint32_t vk_texture_epoch(void) { return __atomic_load_n(&submit_epoch, __ATOMIC
void vk_texture_new_submit(void) {
lock();
if (!++submit_epoch) submit_epoch = 1;
coarse_ms = plat_now_ms();
sweep_pressure();
static uint64_t log_at;
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) / 60;
@@ -1249,15 +1610,24 @@ void vk_texture_new_submit(void) {
(unsigned long long)f * 60, (unsigned long long)ru_n, ru_bytes / 1048576.0, ru_ns / 1e6,
(unsigned long long)ru_wt_n, ru_wt_bytes / 1048576.0, ru_wt_ns / 1e6);
ru_n = ru_bytes = ru_ns = ru_wt_n = ru_wt_bytes = ru_wt_ns = 0;
char ub[512];
size_t uo = 0;
for (int i = 0; i < UP_WHY; i++)
if (up_n[i] && uo < sizeof ub)
uo += (size_t)snprintf(ub + uo, sizeof ub - uo, "%s %s %llu (%.1f MiB, %.2f ms)", uo ? "," : "",
up_why_name[i], (unsigned long long)up_n[i], up_bytes[i] / 1048576.0, up_ns[i] / 1e6);
if (uo) rt_log("perf: frame %llu: texture uploads by path:%s", (unsigned long long)f * 60, ub);
memset(up_n, 0, sizeof up_n), memset(up_bytes, 0, sizeof up_bytes), memset(up_ns, 0, sizeof up_ns);
uint64_t ck, calls, untr;
vk_mem_snap_stats(&ck, &calls, &untr);
rt_log("perf: frame %llu: binding storage checks %llu (texture %llu), write-watch snapshot queries %llu, untracked %llu",
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
(unsigned long long)untr);
n_checks = 0;
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu", (unsigned long long)f * 60,
nreleased, (unsigned long long)n_pressure_drops);
n_pressure_drops = 0;
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu (%llu overwritten)",
(unsigned long long)f * 60, nreleased, (unsigned long long)n_pressure_drops,
(unsigned long long)n_pressure_overwritten);
n_pressure_drops = n_pressure_overwritten = 0;
}
unlock();
}
@@ -1963,6 +2333,74 @@ void vk_texture_census(VkTexCensus *s) {
unlock();
}
// TOTK_TRACE=perf, with mem: vram: what the released entries in own memory hold and how long they waited.
void vk_texture_released_log(uint64_t frame) {
VkCtx *c = vk_ctx();
if (!c) return;
enum { CL_CLEAN, CL_BACKED, CL_DIRTY, CL_POOLS, CL_N };
uint64_t cl_n[CL_N] = {0}, cl_b[CL_N] = {0}, age_b[AGE_N] = {0}, dead_n = 0, dead_b = 0, dead_dirty_b = 0;
uint64_t bound_n = 0, bound_b = 0, ded_n = 0, ded_b = 0;
typedef struct { uint32_t w, h, fmt, n; uint64_t b; } Group;
Group gr[256];
int ng = 0;
lock();
AddrIndex x = {0};
int indexed = addr_index(&x);
for (int i = 0; indexed && i < x.n; i++) {
Tex *e = x.e[i];
if (!e->released || !e->alloc) continue;
VmaAllocationInfo2 ai2;
vmaGetAllocationInfo2(c->vma, e->alloc, &ai2);
const VmaAllocationInfo ai = ai2.allocationInfo;
if (ai2.dedicatedMemory) ded_n++, ded_b += ai.size;
int cl = !e->pub.mirrored ? CL_POOLS : e->gpu_dirty ? CL_DIRTY : e->pub.gpu_owned ? CL_BACKED : CL_CLEAN;
cl_n[cl]++, cl_b[cl] += ai.size;
if (cl != CL_CLEAN) age_b[age_bucket(coarse_ms - e->rel_ms)] += ai.size;
if (e->checked && submit_epoch - e->checked < 600) bound_n++, bound_b += ai.size;
if (storage_taken(&x, i)) dead_n++, dead_b += ai.size, dead_dirty_b += cl != CL_CLEAN ? ai.size : 0;
int g = 0;
while (g < ng && (gr[g].w != e->t.d.width || gr[g].h != e->t.d.height || gr[g].fmt != (uint32_t)e->t.d.format)) g++;
if (g == ng && ng < 256) gr[ng++] = (Group){e->t.d.width, e->t.d.height, (uint32_t)e->t.d.format, 0, 0};
if (g < ng) gr[g].n++, gr[g].b += ai.size;
}
if (indexed) addr_index_free(&x);
const double M = 1048576.0;
rt_log("mem: released frame %llu: own memory: clean %llu %.0f MiB, GPU-written with storage current %llu %.0f MiB, "
"GPU-written image only %llu %.0f MiB, not mirrored %llu %.0f MiB; all but clean by age <1s %.0f <5s %.0f <30s %.0f "
"<120s %.0f older %.0f MiB; storage taken by a later texture %llu %.0f MiB (%.0f not clean); bound through a slot "
"in the last 600 submits %llu %.0f MiB; in dedicated memory, returned to the driver when dropped, %llu %.0f MiB; "
"longest search %.2f ms",
(unsigned long long)frame, (unsigned long long)cl_n[CL_CLEAN], cl_b[CL_CLEAN] / M, (unsigned long long)cl_n[CL_BACKED],
cl_b[CL_BACKED] / M, (unsigned long long)cl_n[CL_DIRTY], cl_b[CL_DIRTY] / M, (unsigned long long)cl_n[CL_POOLS],
cl_b[CL_POOLS] / M, age_b[AGE_1S] / M, age_b[AGE_5S] / M, age_b[AGE_30S] / M, age_b[AGE_120S] / M,
age_b[AGE_LONG] / M, (unsigned long long)dead_n, dead_b / M, dead_dirty_b / M, (unsigned long long)bound_n,
bound_b / M, (unsigned long long)ded_n, ded_b / M, pressure_search_ns / 1e6);
pressure_search_ns = 0;
for (int k = 0; k < 2; k++) {
char line[256];
size_t o = 0;
for (int a = 0; a < AGE_N; a++) {
o += (size_t)snprintf(line + o, sizeof line - o, " %s %llu", age_name[a], (unsigned long long)takeback_n[k][a]);
takeback_n[k][a] = 0;
}
rt_log("mem: released frame %llu: taken back after,%s:%s", (unsigned long long)frame, k ? " GPU-written" : " others", line);
}
rt_log("mem: released frame %llu: dropped under pressure and created again: within 10 s %llu (%llu GPU-written), later %llu (%llu)",
(unsigned long long)frame, (unsigned long long)n_recreated[0], (unsigned long long)n_recreated_written[0],
(unsigned long long)n_recreated[1], (unsigned long long)n_recreated_written[1]);
n_recreated[0] = n_recreated[1] = n_recreated_written[0] = n_recreated_written[1] = 0;
for (int top = 0; top < 4; top++) {
int best = -1;
for (int g = 0; g < ng; g++)
if (gr[g].n && (best < 0 || gr[g].b > gr[best].b)) best = g;
if (best < 0) break;
rt_log("mem: released frame %llu: largest %d: %ux%u fmt %#x: %u entries %.0f MiB", (unsigned long long)frame, top + 1,
gr[best].w, gr[best].h, gr[best].fmt, gr[best].n, gr[best].b / M);
gr[best].n = 0;
}
unlock();
}
void vk_texture_shutdown(void) {
VkCtx *c = vk_ctx();
lock();
+258
View File
@@ -0,0 +1,258 @@
"""tools/exefs_patches.py on modules it makes itself: code writes become sites, data writes become runs of the data file
as emulators apply a patch file before the dynamic linker (plain bytes, DT_RELR words rebased, writes into words an
explicit-addend relocation fills dropped), the writes it refuses (outside the image, into the linker's tables or a
lazily bound pointer), the status and data files, and the check after the lift against the bytes recomp/gen.py lists in
folded.txt, with gen.py run on a module with one jump table. No game files.
Run with .venv (lz4 and numpy, as recomp/): python tests/exefs_patches_test.py
"""
import struct
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'tools'))
import exefs_patches as ep # noqa: E402
BUILD_ID = bytes.fromhex('0123456789abcdef') + bytes(24)
BID_NAME = '0123456789ABCDEF' # an IPS file name may leave the build id's trailing zeros out
MOV_W8_100, MOV_W8_200, RET = 0x52802008, 0x52804008, 0xD65F03C0
R_RELATIVE, R_GLOB_DAT, R_JUMP_SLOT = 1027, 1025, 1026
def nso(text, ro, data, bss=0x1000):
"""An uncompressed NSO with .text at 0, .rodata at 0x1000 and .data at 0x2000."""
segs = [(0x0, text), (0x1000, ro), (0x2000, data)]
hdr = bytearray(0x100)
hdr[0:4] = b'NSO0'
off = 0x100
for i, (mem, d) in enumerate(segs):
struct.pack_into('<IIII', hdr, 0x10 + 0x10 * i, off, mem, len(d), bss if i == 2 else 0)
struct.pack_into('<I', hdr, 0x60 + 4 * i, len(d))
off += len(d)
hdr[0x40:0x60] = BUILD_ID
return bytes(hdr) + b''.join(d for _, d in segs)
def put(buf, off, data):
buf[off:off + len(data)] = data
def reloc_module():
"""fn at 0x8 (mov w8, #0x100; ret); .rodata: constants at 0x1000, MOD0 at 0x1100 and the relocation tables at
0x1200 (RELA: a RELATIVE word at 0x2100, a GLOB_DAT word at 0x2108), 0x1300 (JMPREL: a JUMP_SLOT word at 0x2110)
and 0x1400 (RELR: the word at 0x2118, addend 0x30); .data: .dynamic at 0x2000, plain bytes at 0x2200."""
text = struct.pack('<4I', 0x14000002, 0x1100, MOV_W8_100, RET)
ro = bytearray(0x500)
put(ro, 0x000, b'constant bytes!!')
put(ro, 0x100, b'MOD0' + struct.pack('<6i', 0x2000 - 0x1100, 0, 0, -0x1100, -0x1100, 0))
put(ro, 0x200, struct.pack('<QQq', 0x2100, R_RELATIVE, 0x8) + struct.pack('<QQq', 0x2108, R_GLOB_DAT, 0))
put(ro, 0x300, struct.pack('<QQq', 0x2110, R_JUMP_SLOT, 0))
put(ro, 0x400, struct.pack('<Q', 0x2118))
data = bytearray(0x300)
dyn = [(7, 0x1200), (8, 48), (23, 0x1300), (2, 24), (0x24, 0x1400), (0x23, 8), (0, 0)]
put(data, 0x000, b''.join(struct.pack('<QQ', t, v) for t, v in dyn))
put(data, 0x118, struct.pack('<Q', 0x30))
put(data, 0x200, b'ABCDEFGH')
return nso(text, bytes(ro), bytes(data))
# A module with one jump table (clang's compressed form), assembled from:
# b fn ; 0x0
# .word mod0 ; 0x4: the MOD0 offset
# fn: cmp w0, #3 ; 0x8
# b.hi Ldefault
# adrp x8, table ; table = 0x1020: .byte 0, 2, 4, 6
# add x8, x8, #0x20
# adr x9, L0
# ldrb w10, [x8, w0, uxtw]
# add x9, x9, x10, lsl #2
# br x9
# L0..L3: mov w0, #10/20/30/40; ret ; Ldefault: mov w0, #0; ret
# .rodata: 32 bytes of text before the table, 21 after it, MOD0 at 0x1040 and a RELATIVE relocation to fn (the
# lifter's function seed); .data: .dynamic (DT_RELA) and the relocated word.
JT_TEXT = bytes.fromhex(
'02000014401000001f0c0071e8010054080000b008810091890000100a49603829090a8b20011fd640018052c0035fd680028052'
'c0035fd6c0038052c0035fd600058052c0035fd600008052c0035fd6')
JT_RO = bytes.fromhex(
'756e746f7563686564206279746573206265666f726520746865207461626c6500020406627974657320616674657220746865'
'207461626c65000000000000004d4f4430c00f00000000000000000000c0efffffc0efffff00000000000000003020000000000000'
'03040000000000000800000000000000')
JT_DATA = struct.pack('<7Q', 7, 0x1060, 8, 24, 0, 0, 0) # DT_RELA, DT_RELASZ, DT_NULL; the word the RELATIVE fills
def ips32(writes):
"""An IPS32 file of {image offset: bytes} (offsets count the NSO header, as emulators read them)."""
body = b''.join((off + ep.NSO_HEADER).to_bytes(4, 'big') + len(d).to_bytes(2, 'big') + d
for off, d in sorted(writes.items()))
return b'IPS32' + body + b'EEOF'
class Classify(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.TemporaryDirectory()
path = Path(cls.tmp.name) / 'main'
path.write_bytes(reloc_module())
cls.m = ep.Main(path)
cls.md = ep.MainData(cls.m)
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
def run_file(self, writes, md=True):
return ep.classify(self.m, self.md if md else None, self.m.nso.build_id, Path(f'{BID_NAME}.ips'),
ips32(writes))
def test_relocated_words(self):
self.assertEqual(self.md.rela, {0x2100: R_RELATIVE, 0x2108: R_GLOB_DAT, 0x2110: R_JUMP_SLOT})
self.assertEqual(self.md.relr, {0x2118})
def test_code_only(self):
kind, note, sites, runs, dropped = self.run_file({0x8: struct.pack('<I', MOV_W8_200)})
self.assertEqual((kind, note, runs, dropped), ('code', '', [], 0))
self.assertEqual(sites, {0x8: [MOV_W8_200]})
def test_data_only(self):
kind, note, sites, runs, dropped = self.run_file({0x1000: b'\xAA\xBB', 0x2200: b'XY'})
self.assertEqual((kind, sites, dropped), ('data', {}, 0))
self.assertEqual(runs, [(0x1000, b'\xAA\xBB', False), (0x2200, b'XY', False)])
def test_code_and_data(self):
kind, _, sites, runs, _ = self.run_file({0x8: struct.pack('<I', MOV_W8_200), 0x1004: b'\x01'})
self.assertEqual(kind, 'code')
self.assertEqual(sites, {0x8: [MOV_W8_200]})
self.assertEqual(runs, [(0x1004, b'\x01', False)])
def test_same_bytes_are_nothing(self):
kind, _, _, runs, _ = self.run_file({0x1000: b'constant'})
self.assertEqual((kind, runs), ('empty', []))
def test_explicit_addend_words_change_nothing(self):
kind, _, _, runs, dropped = self.run_file({0x2100: b'\x11\x22\x33\x44', 0x2108: b'\x55'})
self.assertEqual((kind, runs, dropped), ('empty', [], 5))
def test_relr_word_is_rebased(self):
kind, _, _, runs, _ = self.run_file({0x2118: b'\x31'})
self.assertEqual(kind, 'data')
self.assertEqual(runs, [(0x2118, struct.pack('<Q', 0x31), True)])
def test_long_runs_are_split(self):
_, _, _, runs, _ = self.run_file({0x2200: bytes(i % 255 + 1 for i in range(ep.RUN_MAX)) + b'\xFF'})
self.assertEqual([(o, len(d)) for o, d, _ in runs], [(0x2200, ep.RUN_MAX), (0x2200 + ep.RUN_MAX, 1)])
def test_refused_writes(self):
for writes, why in (({0x2110: b'\x01'}, 'binds lazily'), ({0x2000: b'\x01'}, 'linker\'s tables'),
({0x1208: b'\x01'}, 'linker\'s tables'), ({0x4000: b'\x01'}, 'outside main\'s image'),
({-0x80: b'\x01'}, 'outside main\'s image')):
kind, note, sites, runs, _ = self.run_file({0x8: struct.pack('<I', MOV_W8_200), **writes})
self.assertEqual((kind, sites, runs), ('unsupported', {}, []), note)
self.assertIn(why, note)
def test_data_without_a_data_file(self):
kind, note, _, _, _ = self.run_file({0x1000: b'\xAA'}, md=False)
self.assertEqual(kind, 'unsupported')
self.assertIn('only code patches are supported', note)
def test_pchtxt(self):
text = (f'@nsobid-{BID_NAME}\n@flag offset_shift 0x100\n@enabled\n'
f'00001000 AABB\n00002200 "XY"\n@disabled\n00002204 CC\n')
kind, _, _, runs, _ = ep.classify(self.m, self.md, self.m.nso.build_id, Path('x.pchtxt'), text.encode())
self.assertEqual(kind, 'data')
self.assertEqual(runs, [(0x1000, b'\xAA\xBB', False), (0x2200, b'XY', False)])
class Tool(unittest.TestCase):
"""The command line: build, then the check after the lift."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.d = Path(self.tmp.name)
(self.d / 'exefs').mkdir()
(self.d / 'patches.txt').write_text('# no sets of the port\n')
def tearDown(self):
self.tmp.cleanup()
def mod_file(self, mod, writes):
f = self.d / 'mods' / mod / 'exefs' / f'{BID_NAME}.ips'
f.parent.mkdir(parents=True, exist_ok=True)
f.write_bytes(ips32(writes))
def tool(self, *args):
r = subprocess.run([sys.executable, str(ROOT / 'tools' / 'exefs_patches.py'), '--exefs', str(self.d / 'exefs'),
*map(str, args)], capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
return r.stdout
def build(self):
self.tool('--mods', self.d / 'mods', '--patches', self.d / 'patches.txt', '--out', self.d / 'out.txt',
'--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data')
status = {f[0]: f for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
data = [line.split('\t') for line in (self.d / 'exefs.data').read_text().splitlines()
if not line.startswith('#')]
return status, data
def key(self, mod):
return ep.fnv(f'{mod}/exefs/{BID_NAME}.ips'.encode())
def test_build_and_check(self):
(self.d / 'exefs' / 'main').write_bytes(reloc_module())
self.mod_file('Code', {0x8: struct.pack('<I', MOV_W8_200), 0x2200: b'Q'})
self.mod_file('Data', {0x1000: b'\xAA', 0x2118: b'\x31'})
self.mod_file('Bad', {0x2110: b'\x01'})
status, data = self.build()
code, dat, bad = self.key('Code'), self.key('Data'), self.key('Bad')
self.assertEqual([status[k][2] for k in (code, dat, bad)], ['code', 'data', 'unsupported'])
self.assertEqual([status[k][4] for k in (code, dat)], ['data 1', 'data 9']) # bytes its data lines hold
self.assertIn(f'set mod_{code.lower()} TOTK_MODPATCH_{code} 0', (self.d / 'out.txt').read_text())
self.assertEqual(sorted((f[0], f[2], f[3], f[4:]) for f in data),
sorted([(code, '2200', '51', []), (dat, '1000', 'aa', []),
(dat, '2118', '3100000000000000', ['base'])]))
# after the lift: .rodata bytes the lifted code read make the Data file unsupported; the Code file's data is
# in .data, which the lifter never reads
folded = self.d / 'folded.txt'
folded.write_text('# test\n1000 1001\n')
self.tool('--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data', '--folded', folded)
status2 = {f[0]: f for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
self.assertEqual(status2[code][2], 'code')
self.assertEqual(status2[dat][2:4], ['unsupported', '-'])
self.assertIn('jump table', status2[dat][4])
left = [line.split('\t')[0] for line in (self.d / 'exefs.data').read_text().splitlines()
if not line.startswith('#')]
self.assertEqual(left, [code])
def test_check_without_the_lift_list(self):
(self.d / 'exefs' / 'main').write_bytes(reloc_module())
self.mod_file('Ro', {0x1000: b'\xAA'})
self.mod_file('Rw', {0x2200: b'Q'})
self.build()
self.tool('--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data', '--folded',
self.d / 'missing.txt')
status = {f[0]: f[2] for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
self.assertEqual((status[self.key('Ro')], status[self.key('Rw')]), ('unsupported', 'data'))
def test_jump_table_through_the_lift(self):
"""recomp/gen.py lists the jump table's bytes in folded.txt; a write into them is refused, one beside them
is kept."""
(self.d / 'exefs' / 'main').write_bytes(nso(JT_TEXT, JT_RO, JT_DATA))
r = subprocess.run([sys.executable, str(ROOT / 'recomp' / 'gen.py'), '--exefs', str(self.d / 'exefs'),
'--out', str(self.d / 'gen'), '--modules', 'main', '--jobs', '1'],
capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
folded = self.d / 'gen' / 'main' / 'folded.txt'
self.assertEqual(ep.read_ranges(folded), [(0x1020, 0x1024)])
self.mod_file('Table', {0x1022: b'\x00'})
self.mod_file('Beside', {0x1024: b'B'})
self.build()
self.tool('--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data', '--folded', folded)
status = {f[0]: f[2] for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
self.assertEqual((status[self.key('Table')], status[self.key('Beside')]), ('unsupported', 'data'))
if __name__ == '__main__':
unittest.main()
+9 -3
View File
@@ -377,13 +377,19 @@ class DpiTest(unittest.TestCase):
code = ('import tkinter\n'
'from installer import dpi, gui\n'
'root = tkinter.Tk()\n'
'print(dpi.awareness(), dpi.scale(), root.tk.call("tk", "scaling"), gui.PAD, gui.WRAP)\n'
'print(dpi.awareness(), dpi.scale(), root.tk.call("tk", "scaling"), gui.PAD, gui.WRAP, '
'root.winfo_screenwidth())\n'
'root.destroy()\n')
r = subprocess.run([sys.executable, '-c', code], cwd=ROOT, capture_output=True, text=True, timeout=120)
self.assertEqual(r.returncode, 0, r.stderr[-2000:])
awareness, scale, scaling, pad, wrap = r.stdout.split()
awareness, scale, scaling, pad, wrap, width = r.stdout.split()
self.assertGreaterEqual(int(awareness), 1) # 1 system aware, 2 per-monitor aware
self.assertAlmostEqual(float(scaling), float(scale) * 96 / 72, places=6)
# Tk's win/tkWinX.c TkWinDisplayChanged rounds the nominal screen width to whole millimetres.
# Its generic/tkCmds.c ScalingCmd converts that width back to pixels per point, so a 120 DPI display
# can report 120.118 DPI. Keep the exact comparison, accounting for that round trip.
width_mm = int(25.4 * int(width) / (float(scale) * 96) + 0.5)
expected_scaling = 25.4 * int(width) / (72 * width_mm)
self.assertAlmostEqual(float(scaling), expected_scaling, places=6)
self.assertEqual((int(pad), int(wrap)), (round(14 * float(scale)), round(620 * float(scale))))
+69
View File
@@ -315,6 +315,75 @@ class LiftKeyTest(unittest.TestCase):
self.assertEqual(self.key(), before)
class LiftRunTest(unittest.TestCase):
"""Lift.run with fake tools: the ExeFS patch stage writes the status and data files, the lift lists the bytes it
read, and the data writes are checked against that list after it; a failed lift or check drops both files."""
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
base = Path(tmp.name)
self.dest, temp = base / 'install', base / 'temp'
self.dest.mkdir()
temp.mkdir()
layout = SimpleNamespace(recomp=ROOT / 'recomp', tools=ROOT / 'tools',
patches=lambda version: base / 'patches-v143.txt')
self.ctx = SimpleNamespace(dest=self.dest, temp=temp, layout=layout, keys={'lift': 'k'},
state={'version': '1.4.3'}, reporter=Reporter(out=io.StringIO()))
self.addCleanup(self.ctx.reporter.close)
mod = self.dest / 'mods' / 'SQ' / 'exefs'
mod.mkdir(parents=True)
(mod / '277178B7DBA1B6D4.ips').write_bytes(b'IPS32EEOF')
self.calls, self.fail = [], set()
saved = steps.run_tool
self.addCleanup(lambda: setattr(steps, 'run_tool', saved))
steps.run_tool = self.run_tool
def run_tool(self, ctx, step, script, args, parse=None, cwd=None):
args = [str(a) for a in args]
name = Path(script).name + (' --folded' if '--folded' in args else '')
self.calls.append((name, args))
log = ctx.temp / 'tool.log'
log.write_text('')
if name in self.fail:
return 1, log
if name == 'exefs_patches.py':
for flag in ('--status', '--data', '--out'):
p = Path(args[args.index(flag) + 1])
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(flag)
elif name == 'gen.py':
out = Path(args[args.index('--out') + 1]) / 'main'
out.mkdir(parents=True, exist_ok=True)
(out / 'folded.txt').write_text('# test\n')
return 0, log
def outputs(self):
return [p.is_file() for p in (steps.exefs_status(self.ctx), steps.exefs_data(self.ctx))]
def test_data_file_and_the_check_after_the_lift(self):
steps.Lift().run(self.ctx)
self.assertEqual([c[0] for c in self.calls], ['exefs_patches.py', 'gen.py', 'exefs_patches.py --folded'])
build = self.calls[0][1]
self.assertEqual(build[build.index('--data') + 1], str(steps.exefs_data(self.ctx)))
check = self.calls[2][1]
self.assertEqual(check[check.index('--folded') + 1], str(self.ctx.temp / 'gen' / 'main' / 'folded.txt'))
self.assertEqual(check[check.index('--status') + 1], str(steps.exefs_status(self.ctx)))
self.assertEqual(self.outputs(), [True, True])
def test_failed_lift_drops_the_data_file(self):
self.fail = {'gen.py'}
with self.assertRaises(steps.InstallError):
steps.Lift().run(self.ctx)
self.assertEqual(self.outputs(), [False, False])
def test_failed_check_drops_both_files(self):
self.fail = {'exefs_patches.py --folded'}
steps.Lift().run(self.ctx)
self.assertEqual(self.outputs(), [False, False])
self.assertIn('warn.exefs_skipped', self.ctx.reporter.out.getvalue())
class IniTest(unittest.TestCase):
EXAMPLE = '[display]\n# vk_present = mailbox\n\n[language]\n# locale = en-US\n# lang = en-US\n# x = 1\n'
+74 -12
View File
@@ -18,7 +18,7 @@ const int rt_game_version = TEST_GAME_VERSION;
static int raw_x, raw_y, takes, captured=1, foreground=1, allowed=1, mode=1, patched=1, selecting;
static float ratio=0.5f;
static uint64_t now=1000000000;
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_hook;
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_hook, state_hook;
const char *cfg_get(const char *key) { (void)key; return mode ? "direct" : "stick"; }
const char *cfg_value(const char *key) { return cfg_get(key); }
double cfg_double(const char *key) { (void)key; return 1; }
@@ -40,13 +40,14 @@ void *rt_patch_page(void) { return &ratio; }
void rt_register_host(uint64_t addr, GuestFn fn) {
if(addr==HOST_MOUSE_DIRECT) hook=fn; else if(addr==HOST_MOUSE_ACTIVITY) activity_hook=fn;
else if(addr==HOST_MOUSE_ORBIT_BEGIN) begin_hook=fn; else if(addr==HOST_MOUSE_ORBIT_ANGLES) angles_hook=fn;
else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else if(addr==HOST_MOUSE_LOAD) load_hook=fn; else abort();
else if(addr==HOST_MOUSE_ORBIT_STATE) state_hook=fn; else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else if(addr==HOST_MOUSE_LOAD) load_hook=fn; else abort();
}
void plat_mutex_lock(PlatMutex *m) { (void)m; }
void plat_mutex_unlock(PlatMutex *m) { (void)m; }
void rt_log(const char *fmt, ...) { (void)fmt; }
float cam_speed_applied(void) { return 1; }
uint64_t input_buttons_last(void) { return 0; }
static uint64_t buttons;
uint64_t input_buttons_last(void) { return buttons; }
int mem_guest_readable(uint64_t a, uint64_t n) { (void)a; (void)n; return 0; } // no player: on-foot gains
static float scalar(__uint128_t v) { float f; memcpy(&f,&v,4); return f; }
#if TEST_GAME_VERSION == 143
@@ -109,6 +110,7 @@ int main(void) {
uint64_t input_address=(uintptr_t)target; memcpy(camera+0x20,&input_address,8);
for(int rate=0;rate<3;rate++) {
memset(orbit_seen,0,sizeof orbit_seen);
guest_float_store((uintptr_t)camera+0x15c,0);
ratio=30/rates[rate]; c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0;
raw_x=100; raw_y=-50; hook(&c);
guest_float_store((uintptr_t)camera+0x108,30); guest_float_store((uintptr_t)camera+0x120,40);
@@ -118,16 +120,16 @@ int main(void) {
c.x[20]=(uintptr_t)camera; c.sp=(uintptr_t)frame; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-(30-50*DEG_PER_COUNT))<0.0001f);
assert(fabsf(guest_float((uintptr_t)frame+0x48)-(40-100*DEG_PER_COUNT))<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(30-50*DEG_PER_COUNT))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
c.x[0]=(uintptr_t)camera; begin_hook(&c); angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x48)-(40-100*DEG_PER_COUNT))<0.0001f); // one sample cannot repeat next update
// The next input frame is stationary and the game's parameter update rewrites the old default target.
// Restoring the mouse's desired target makes the camera filter stay at the new pitch, not spring to zero.
// The native target remains available for the distance controller.
// Holding pitch happens at the final output, after the native filter.
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=raw_y=0; hook(&c);
guest_float_store((uintptr_t)camera+0x15c,0);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
float current=guest_float((uintptr_t)camera+0x108), goal=guest_float((uintptr_t)camera+0x15c);
assert(fabsf(goal-(30-50*DEG_PER_COUNT))<0.0001f && fabsf((current+goal)*0.5f-(30-50*DEG_PER_COUNT))<0.0001f);
assert(goal==0 && fabsf(current-(30-50*DEG_PER_COUNT))<0.0001f);
// A later game filter overwrites the pitch after the begin hook. Neither a stationary frame nor the next
// tiny mouse delta may integrate from that recentered result.
guest_float_store((uintptr_t)camera+0x108,0);
@@ -137,7 +139,7 @@ int main(void) {
guest_float_store((uintptr_t)frame+0x44,0);
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-(30-50*DEG_PER_COUNT))<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-((30-50*DEG_PER_COUNT)-19))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
assert(fabsf(guest_float((uintptr_t)camera+0x108)-((30-50*DEG_PER_COUNT)-14))<0.0001f);
for(int i=0;i<100;i++) {
guest_float_store((uintptr_t)frame+0x44,0); angles_hook(&c);
@@ -161,16 +163,76 @@ int main(void) {
guest_float_store((uintptr_t)camera+0x108,3); // the game left the pitch at 3 + the offset 14
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(17-14-5))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
guest_float_store((uintptr_t)frame+0x44,0);
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-17)<0.0001f);
guest_float_store((uintptr_t)camera+0x15c,0); // the game's parameter update writes its default target
c.x[20]=(uintptr_t)target; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(17-14-5))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
// Aim/close-up handoff: the native camera can replace its parameter
// reference on every frame. Mouse pitch hold must never overwrite it,
// nor restore a stale reference when a controller takes over later.
for(int i=0;i<20;i++) {
float native_target=5+i, native_radius=2+i*0.1f;
guest_float_store((uintptr_t)camera+0x15c,native_target);
guest_float_store((uintptr_t)camera+0x164,native_radius);
guest_float_store((uintptr_t)camera+0x130,native_radius-0.5f);
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(guest_float((uintptr_t)camera+0x15c)==native_target);
guest_float_store((uintptr_t)frame+0x44,0); // native pitch filter
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-17)<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==native_target);
assert(guest_float((uintptr_t)camera+0x164)==native_radius);
assert(guest_float((uintptr_t)camera+0x130)==native_radius-0.5f);
}
float pad_pitch=0.5f; c.x[20]=(uintptr_t)target;
memcpy(&c.v[10],&pad_pitch,4); hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(guest_float((uintptr_t)camera+0x15c)==24);
c.v[10]=0;
memset(camera+0x88,0,4); memset(camera+0x10c,0,4); memset(camera+0x11c,0,4);
}
// The native manual-orbit branch keeps the distance it inherited from
// aiming. Mouse-owned orbit must select the native automatic-distance
// transition, before initialization; controller and scripted modes keep
// their own state. The host hook never writes a distance or position.
{
assert(state_hook);
c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0;
raw_x=1; raw_y=0; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
c.x[20]=(uintptr_t)camera; angles_hook(&c);
uint32_t manual=4, override=0;
memcpy(camera+0x180,&manual,4); memcpy(camera+0x188,&override,4);
c.x[10]=4; expected=c; state_hook(&c);
expected.x[8]=0; expected.x[10]=3;
assert(!memcmp(&expected,&c,sizeof c));
uint32_t unchanged; memcpy(&unchanged,camera+0x180,4); assert(unchanged==4);
// The game's transition initializer, rather than this hook, owns the state.
for(int state=0;state<=5;state++) if(state!=4) {
c.x[10]=state; state_hook(&c); assert(c.x[10]==(uint64_t)state);
}
buttons=1ull<<B_ZR; c.x[10]=4; state_hook(&c); assert(c.x[10]==4);
buttons=1ull<<B_R; state_hook(&c); assert(c.x[10]==4); buttons=0;
uint8_t forced=0x40; memcpy(target+0x15c,&forced,1);
state_hook(&c); assert(c.x[10]==4); forced=0; memcpy(target+0x15c,&forced,1);
uint64_t recenter=0x20; memcpy(target+0x148,&recenter,8);
state_hook(&c); assert(c.x[10]==4); recenter=0; memcpy(target+0x148,&recenter,8);
override=1; memcpy(camera+0x188,&override,4);
state_hook(&c); assert(c.x[10]==4 && c.x[8]==1);
override=0; memcpy(camera+0x188,&override,4);
// Any real controller input retains the native manual-distance behaviour.
c.x[20]=(uintptr_t)target; float pad=0.25f;
memcpy(&c.v[9],&pad,4); c.v[10]=0; hook(&c);
c.x[20]=(uintptr_t)camera; c.x[10]=4; state_hook(&c); assert(c.x[10]==4);
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
c.x[20]=(uintptr_t)camera; c.x[10]=4; now+=150000000ull;
state_hook(&c); assert(c.x[10]==4); now++;
}
// A gamepad-only step (no mouse motion, no pitch hold) leaves the camera input and the angles as the game made them.
memset(orbit_seen,0,sizeof orbit_seen);
c.x[20]=(uintptr_t)target; c.x[21]=0; raw_x=raw_y=0; c.v[9]=c.v[10]=0;
@@ -233,7 +295,7 @@ int main(void) {
assert(takes==before && raw_x==42); selecting=0; raw_x=0;
puts("direct mouse: register contract, routing, linearity, frame ratio and capture checks passed");
puts("direct mouse: final orbit angles, single-count motion, equal axes and held-object routing passed");
puts("direct mouse: persistent pitch target, radial cursor hold and gamepad-only steps untouched passed");
puts("direct mouse: pitch hold with native distance reference, radial cursor hold and gamepad-only steps untouched passed");
puts("direct mouse: final-pitch recenter rejection, explicit handoff and bounded quick-list steps passed");
// A shake: two direction changes of swings of at least 10 degrees within 0.35 s, then the stick follows the mouse
// side by side and lets go when it rests; nothing plays on after the mouse
@@ -437,7 +499,7 @@ int main(void) {
#else
// 1.2.1: the same hooks with that version's registers and stack slots (MouseLayout layout_121) and its load sites
int main(void) {
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook);
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook && !state_hook);
(void)takes;
Cpu c, expected;
unsigned char target[0x500]={0}, camera[0x300]={0}, hand[0x300]={0};
+1 -1
View File
@@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'tools'))
import worktree # noqa: E402
IGNORED = '\n'.join('/' + d + '/' for d in worktree.LINK_DIRS) + '\n/' + worktree.LINK_FILES[0] + '\n/.worktrees/\n'
IGNORED = ''.join(f'/{p}\n' for p in [d + '/' for d in worktree.LINK_DIRS] + worktree.LINK_FILES + ['.worktrees/'])
def git(cwd, *a):
+35 -9
View File
@@ -271,10 +271,13 @@ def write_if_changed(path, text):
def copy_romfs_patches(version, out):
"""totk.exe loads romfs-patches beside romfs-index.bin (runtime/romfs.c overlay_init). tools/make_romfs_patches.py
writes them into build/<version> only, so a build in another directory ran without the 60 fps overlay (the quick
menus' captured background turned black). Copies them when their manifest differs; romfs-patches-xbox,
romfs-patches-xbox-label and romfs-patches-pc (the same with Xbox or keyboard and mouse button prompts,
make_romfs_patches.py --prompts) the same way when they were made."""
for name in ('romfs-patches', 'romfs-patches-xbox', 'romfs-patches-xbox-label', 'romfs-patches-pc'):
menus' captured background turned black). Copies every romfs-patches* folder of build/<version> that has a
manifest, the same way when it differs: romfs-patches, romfs-patches-xbox, romfs-patches-xbox-label and
romfs-patches-pc (the same with Xbox or keyboard and mouse button prompts, make_romfs_patches.py --prompts) and
the ones of another shape, romfs-patches-<w>x<h> and romfs-patches-pc-<w>x<h> (make_romfs_patches.py --aspect),
which runtime/romfs.c overlay_init needs for a game resolution that is not 16:9."""
names = sorted(p.name for p in (ROOT / 'build' / version).glob('romfs-patches*') if p.is_dir())
for name in names:
src = ROOT / 'build' / version / name
dst = out / name
if src.resolve() == dst.resolve() or not (src / 'manifest.tsv').is_file():
@@ -320,6 +323,26 @@ def runtime_sources(skip=()):
return sorted(p for pat in pats for p in RUNTIME.glob(pat) if p.name not in skip and not other_os(p.name))
def install_stamp(out, runtime_lib):
"""Write obj/runtime/inputs.sha256, one hash over the runtime library, the INSTALL_SIDE sources and every runtime
header, and return its path. The release zip dates every file 1980-01-01 (make_release.py write_zip), so a new
release extracted over an old Setup folder has sources and a library older than the objects and the totk.exe the
last install left there. Ninja compares mtimes only: it kept the old install-side objects and linked them with the
new library (undefined symbols), or skipped the link and kept the old totk.exe. The install-side objects and the
link depend on this file, which is rewritten only when the hash changes, so new contents rebuild them whatever the
file dates are."""
files = sorted((p for p in RUNTIME.rglob('*') if p.is_file() and
(p.suffix in ('.h', '.inc') or (p.parent == RUNTIME and p.name in INSTALL_SIDE))),
key=lambda p: p.relative_to(RUNTIME).as_posix())
h = hashlib.sha256()
for p in files:
h.update(f'{p.relative_to(RUNTIME).as_posix()} {file_sha256(p)}\n'.encode())
h.update(f'{LIB_NAME} {file_sha256(runtime_lib)}\n'.encode())
path = out / 'obj' / 'runtime' / 'inputs.sha256'
write_if_changed(path, h.hexdigest() + '\n')
return path
def runtime_object(out, src):
"""obj/runtime/<path below runtime>.o, so platform/win32.c cannot collide with a top-level win32.c."""
src = Path(src)
@@ -365,10 +388,11 @@ def port_version():
return path.read_text(encoding='utf-8').strip() if path.is_file() else 'unknown'
def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=None, make_lib=False):
def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=None, make_lib=False, stamp=None):
"""build.ninja text, and the object lists per module. With prebuilt the generated objects already exist in
out/obj and are link inputs without a compile edge. runtime_lib: a prebuilt library replaces the runtime objects
except INSTALL_SIDE. make_lib: build only that library, from the runtime objects except INSTALL_SIDE."""
except INSTALL_SIDE. make_lib: build only that library, from the runtime objects except INSTALL_SIDE. stamp: the
install_stamp file, an extra input of the runtime objects and of the link."""
inc = f'-I {cmd_path(RUNTIME)} -I {cmd_path(gen)}'
vma = f'{cmd_path(DEPS / "include")} -I {cmd_path(DEPS / "include" / "vma")}' if DEPS else cmd_path(vma_include())
imgui = cmd_path(RUNTIME / 'third_party' / 'imgui')
@@ -437,7 +461,8 @@ def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=
lines.append(f'build {ninja_path(o)}: rc {ninja_path(src)} | {ninja_path(RUNTIME / "totk.manifest")} '
f'{ninja_path(RUNTIME / "totk.ico")}')
else:
lines.append(f'build {ninja_path(o)}: cc_rt {ninja_path(src)}{pgo_dep}')
stamp_dep = (' | ' if not pgo_dep else ' ') + ninja_path(stamp) if stamp else ''
lines.append(f'build {ninja_path(o)}: cc_rt {ninja_path(src)}{pgo_dep}{stamp_dep}')
rt.append(o)
objs['runtime'] = rt
every = [o for v in objs.values() for o in v]
@@ -456,7 +481,7 @@ def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=
f' command = $cc $ldflags @$out.rsp -Wl,--whole-archive {cmd_path(runtime_lib)} -Wl,--no-whole-archive $libs -o $out',
' rspfile = $out.rsp', ' rspfile_content = $in_newline', ' description = LINK $out']
lines += ['', f'build {ninja_path(exe)}: {rule} ' + ' '.join(ninja_path(o) for o in every) +
(f' | {ninja_path(runtime_lib)}' if runtime_lib else ''),
(f' | {ninja_path(runtime_lib)}' if runtime_lib else '') + (f' {ninja_path(stamp)}' if stamp else ''),
'build objects: phony ' + ' '.join(ninja_path(o) for o in every), f'default {ninja_path(exe)}', '']
return '\n'.join(lines), objs, exe
@@ -923,8 +948,9 @@ def main():
if runtime_lib and missing:
sys.exit(f'build_game: {", ".join(missing)} missing from {flag_path(RUNTIME)}: the release must ship every '
f'INSTALL_SIDE source beside {runtime_lib.name}')
stamp = install_stamp(out, runtime_lib) if runtime_lib else None
text, objs, exe = ninja_file(gen, out, mods, jobs, skip, pairs is not None, runtime_lib,
args.make_runtime_lib)
args.make_runtime_lib, stamp)
write_if_changed(out / 'build.ninja', text)
env = dict(os.environ)
+2
View File
@@ -164,6 +164,8 @@ class Main:
def __init__(self, path):
n = Nso(str(path))
self.image = bytes(n.image)
n.image = self.image # one copy of the image; tools/exefs_patches.py reads nso's dynamic info
self.nso = n
self.text = n.text # (offset, size)
self.code_end = n.rodata[0] # .text and its padding up to .rodata, where code caves go
self.lifter = Lifter(BASE, 'c')
+256 -44
View File
@@ -1,11 +1,16 @@
"""ExeFS patches of the mods folder to code patch sets: IPS, IPS32 and IPSwitch (.pchtxt) files for the game's main.
"""ExeFS patches of the mods folder to code patch sets and data writes: IPS, IPS32 and IPSwitch (.pchtxt) files for
the game's main.
Lifted code cannot be byte-patched when the game runs, so a patch file's writes into main's code become `site` lines
(recomp/patches.py), one patch set per file, which the runtime switches on while its mod is on (runtime/mods.c). The
writes go through the same rules as the mods' code cheats (tools/cheats.py convert): an instruction replaced in place,
a branch into a code cave the file writes into main's zero padding inlined at the branch.
a branch into a code cave the file writes into main's zero padding inlined at the branch. Writes into main's data
(.rodata, .data, .bss) need no lifting: the runtime stores them into the loaded module before the game runs, as
emulators and Atmosphere apply a patch file to the module before its dynamic linker runs (data_runs).
python tools/exefs_patches.py --exefs <dir> --mods <mods folder> --patches <patch file> --out <patch file> --status <file>
python tools/exefs_patches.py --exefs <dir> --mods <mods folder> --patches <patch file> --out <patch file>
--status <file> --data <file>
python tools/exefs_patches.py --exefs <dir> --status <file> --data <file> --folded <gen>/main/folded.txt
The files are <mod>/exefs/*.ips and *.pchtxt (or the Atmosphere places of the exefs folder, as runtime/mods.c finds
them) and <mod>/atmosphere/exefs_patches/<name>/*.ips. A file is for main when its build id matches main's: an IPS
@@ -13,19 +18,37 @@ file's name, a pchtxt file's @nsobid line (trailing zeros may be left out). Offs
offsets count the NSO header (0x100 bytes) before main's image; a pchtxt offset adds its `@flag offset_shift` first.
A pchtxt patch line applies between @enabled and @disabled; @stop ends the file.
--out gets the --patches file followed by one set per usable file (`mod_<hash>`, switch TOTK_MODPATCH_<HASH>, default
off) and its sites. --status gets one line per file, mods by name, tab separated:
--out gets the --patches file followed by one set per file with sites (`mod_<hash>`, switch TOTK_MODPATCH_<HASH>,
default off) and its sites. --status gets one line per file, mods by name, tab separated:
<file hash> <content hash> <kind> <built-in sets> <note>
file hash: FNV-1a 32 of "<mod folder name>/<path in the mod>" (forward slashes); content hash: the same over the file's
bytes, so the runtime sees a file that changed after the build. kind:
code a set of sites
code a set of sites, and data writes when the file has some
data data writes only
other for another game version or module: not used
empty no write into main for this version (or only the game's own words)
unsupported <note> says why: a write into main's data, a write the site rules cannot express
empty no write into main for this version (or only the game's own bytes)
unsupported <note> says why: a write the site rules cannot express, a write outside main's image, into the dynamic
linker's tables or into a pointer it binds lazily, or (after the lift) into bytes the lifted code reads
as constants
built-in sets: the port's own sets (the --patches file's sets that are no cheats) with a site this file patches too,
comma separated, or "-": the runtime switches them off while the mod is on, so the mod's code runs.
comma separated, or "-": the runtime switches them off while the mod is on, so the mod's code runs. The note of a code
or data file with data writes is `data <bytes>`, the bytes its data lines hold: the runtime keeps the mod's patches off
when the data file does not hold them all.
--data gets one line per run of data bytes of the code and data files, tab separated (runtime/mods.c reads it):
<file hash> <content hash> <offset in main, hex> <bytes, hex> [base]
`base` marks a DT_RELR word: 8 bytes, little-endian, the word before relocation; the runtime adds main's base, as the
dynamic linker does. Without --data a data write makes its file unsupported.
The lifted code reads some of main's .text and .rodata at lift time instead of at run time: the jump tables
(recomp/jumptables.py), whose targets become labels of the generated C. A data write over such bytes would not reach
the lifted code. recomp/gen.py lists the ranges it read in <gen>/<module>/folded.txt; the second form above, run after
the lift, makes every file with a data write into those ranges unsupported (its status line says so and its data lines
go). Without that list, every data write into .rodata counts as one into read bytes.
A file the rules cannot express is unsupported as a whole: half of a patch is worse than none.
@@ -39,9 +62,12 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from cheats import TITLE, Main, convert, fnv # noqa: E402
from nso import (DT_JMPREL, DT_NULL, DT_PLTRELSZ, DT_RELA, DT_RELASZ, DT_RELR, DT_RELRSZ, DT_STRSZ, # noqa: E402
DT_STRTAB, DT_SYMTAB, R_AARCH64_JUMP_SLOT)
NSO_HEADER = 0x100
EXEFS_PLACES = ('exefs', f'atmosphere/contents/{TITLE}/exefs', f'contents/{TITLE}/exefs', f'{TITLE}/exefs')
RUN_MAX = 256 # bytes per line of the data file (runtime/mods.c reads lines of at most 1024 characters)
def patch_files(mods):
@@ -159,6 +185,99 @@ def word_writes(main, writes):
return words
# ---- writes into data
def relocated(n):
"""({offset: type} of the words main's explicit-addend relocations (DT_RELA, DT_JMPREL) fill, {offset} of its
DT_RELR words): the 8-byte words the dynamic linker writes at start (runtime/loader.c relocate_table,
relocate_relr)."""
rela = {}
for table, size in ((DT_RELA, DT_RELASZ), (DT_JMPREL, DT_PLTRELSZ)):
if table in n.dyn:
for i in range(n.dyn[size] // 24):
off, info, _ = struct.unpack_from('<QQq', n.image, n.dyn[table] + 24 * i)
rela[off] = info & 0xFFFFFFFF
relr, where = set(), 0
for i in range(n.dyn.get(DT_RELRSZ, 0) // 8):
e = struct.unpack_from('<Q', n.image, n.dyn[DT_RELR] + 8 * i)[0]
if not e & 1: # an address; the bitmap after it covers the 63 words that follow
relr.add(e)
where = e + 8
else:
relr.update(where + 8 * k for k in range(63) if e >> (k + 1) & 1)
where += 63 * 8
return rela, relr
def linker_tables(n):
"""[(start, end)] of main's .dynamic, relocation, symbol and string tables: the dynamic linker reads them at start,
so a write into them would change what it does, which no store into the loaded module can follow."""
end = n.dynamic
while struct.unpack_from('<Q', n.image, end)[0] != DT_NULL:
end += 16
out = [(n.dynamic, end + 16)]
for table, size in ((DT_RELA, DT_RELASZ), (DT_JMPREL, DT_PLTRELSZ), (DT_RELR, DT_RELRSZ), (DT_STRTAB, DT_STRSZ)):
if table in n.dyn and size in n.dyn:
out.append((n.dyn[table], n.dyn[table] + n.dyn[size]))
if DT_SYMTAB in n.dyn:
out.append((n.dyn[DT_SYMTAB], n.dyn[DT_SYMTAB] + 24 * n.symbol_count()))
return out
class MainData:
"""What a write into main's data meets: main's image from .rodata to the end of .bss, the words the dynamic linker
fills and the tables it reads."""
def __init__(self, main):
n = main.nso
self.image, self.lo, self.size, self.data = main.image, main.code_end, n.size, n.data[0]
self.rela, self.relr = relocated(n)
self.linker = linker_tables(n)
def data_runs(md, writes):
"""The data part of one file ({offset: byte} outside main's code), applied as emulators and Atmosphere apply a patch
file: to the module before its dynamic linker runs. Returns ([(offset, bytes, rebase)], dropped):
- a byte no relocation covers is stored as it is (runs of consecutive bytes, at most RUN_MAX long);
- a DT_RELR word holds its addend: the word with the file's bytes in it, rebase set (main's base gets added);
- the linker overwrites a word an explicit-addend relocation fills whatever the file wrote there: such bytes
change nothing and only count in dropped.
Bytes equal to main's are left out. ValueError for a write outside main's image (below .rodata: the NSO header),
into the dynamic linker's tables, or into a function pointer it binds lazily (JUMP_SLOT): on the Switch it holds
the file's value plus main's base until the first call binds it, which no store before the start can reproduce."""
plain, words, dropped = {}, {}, 0
for off in sorted(writes):
if not md.lo <= off < md.size:
raise ValueError(f'it writes outside main\'s image at main+{off:X}')
if any(a <= off < b for a, b in md.linker):
raise ValueError(f'it writes the dynamic linker\'s tables at main+{off:X}')
w = next((w for w in range(off - 7, off + 1) if w in md.rela or w in md.relr), None)
if w is None:
plain[off] = writes[off]
elif w in md.relr:
words.setdefault(w, bytearray(md.image[w:w + 8]))[off - w] = writes[off]
elif md.rela[w] == R_AARCH64_JUMP_SLOT:
raise ValueError(f'it writes a function pointer the dynamic linker binds lazily at main+{w:X}')
else:
dropped += 1
runs = []
for off in sorted(o for o, b in plain.items() if b != md.image[o]):
last = runs[-1] if runs else None
if last and last[0] + len(last[1]) == off and len(last[1]) < RUN_MAX:
runs[-1] = (last[0], last[1] + bytes([plain[off]]), False)
else:
runs.append((off, bytes([plain[off]]), False))
runs += [(w, bytes(b), True) for w, b in words.items() if bytes(b) != md.image[w:w + 8]]
return sorted(runs), dropped
def data_line(key, content, run):
off, data, rebase = run
return f'{key}\t{content}\t{off:x}\t{data.hex()}' + ('\tbase' if rebase else '')
def builtin_sites(text):
"""{offset in main: [set name]} of the patch file's sets that are no cheats or mod patches."""
out = {}
@@ -169,41 +288,45 @@ def builtin_sites(text):
return out
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--exefs', type=Path, required=True)
ap.add_argument('--mods', type=Path, required=True, help='the mods folder next to totk.exe')
ap.add_argument('--patches', type=Path, required=True, help='the patch file so far')
ap.add_argument('--out', type=Path, required=True, help='the patch file with the mods\' ExeFS patches added')
ap.add_argument('--status', type=Path, required=True)
args = ap.parse_args()
sys.stdout.reconfigure(errors='replace')
def classify(m, md, bid, path, raw):
"""(kind, note, sites, runs, dropped) of one patch file: path gives its name and suffix, raw its bytes, bid main's
build id; md None: no data file, so a write into main's data makes the file unsupported."""
try:
if path.suffix.lower() == '.ips':
for_main = same_build(path.stem, bid)
writes = parse_ips(raw) if for_main else {}
else:
pid, writes = parse_pchtxt(raw.decode('utf-8-sig', errors='replace'))
for_main = pid is not None and same_build(pid, bid)
if not for_main:
return 'other', '', {}, [], 0
code = {o: b for o, b in writes.items() if m.in_code(o - o % 4)}
data = {o: b for o, b in writes.items() if o not in code}
if data and md is None:
raise ValueError(f'it writes main\'s data at main+{min(data):X} (only code patches are supported)')
sites = convert(m, word_writes(m, code)) if code else {}
runs, dropped = data_runs(md, data) if data else ([], 0)
return 'code' if sites else 'data' if runs else 'empty', '', sites, runs, dropped
except ValueError as e:
return 'unsupported', str(e), {}, [], 0
DATA_HEADER = '# file hash, content hash, offset in main, bytes[, base] (tools/exefs_patches.py)'
def build(args):
m = Main(args.exefs / 'main')
raw = (args.exefs / 'main').read_bytes()
bid = raw[0x40:0x60].hex()
md = MainData(m) if args.data else None
bid = m.nso.build_id
text = args.patches.read_text()
builtin = builtin_sites(text)
files = patch_files(args.mods)
sets, sites, status, kinds = [], [], [], {}
sets, sites, status, data_lines, kinds = [], [], [], [], {}
for mod, rel, f in files:
data = f.read_bytes()
key, content = fnv(f'{mod}/{rel}'.encode()), fnv(data)
kind, note, over, file_sites = 'code', '', [], {}
try:
if f.suffix.lower() == '.ips':
for_main = same_build(f.stem, bid)
writes = parse_ips(data) if for_main else {}
else:
pid, writes = parse_pchtxt(data.decode('utf-8-sig', errors='replace'))
for_main = pid is not None and same_build(pid, bid)
if not for_main:
kind = 'other'
else:
file_sites = convert(m, word_writes(m, writes))
if not file_sites:
kind = 'empty'
except ValueError as e:
kind, note, file_sites = 'unsupported', str(e), {}
raw = f.read_bytes()
key, content = fnv(f'{mod}/{rel}'.encode()), fnv(raw)
kind, note, file_sites, runs, dropped = classify(m, md, bid, f, raw)
over = []
if file_sites:
name = f'mod_{key.lower()}'
label = f'{mod}/{rel}'.encode('ascii', 'replace').decode()
@@ -211,19 +334,108 @@ def main():
for a, repl in sorted(file_sites.items()):
sites.append(f'site {name} main {a:x} {m.word(a):08x} {" ".join(f"{w:08x}" for w in repl)}')
over += [s for s in builtin.get(a, []) if s not in over]
data_lines += [data_line(key, content, r) for r in runs]
data_bytes = sum(len(r[1]) for r in runs)
kinds[kind] = kinds.get(kind, 0) + 1
status.append(f'{key}\t{content}\t{kind}\t{",".join(over) or "-"}\t{note.replace(chr(9), " ")}')
print(f'exefs: {kind:11} {mod}/{rel}' + (f' ({note})' if note else '')
+ (f' ({len(file_sites)} sites, over {", ".join(over)})' if over else
f' ({len(file_sites)} sites)' if file_sites else ''))
status_note = f'data {data_bytes}' if runs else note.replace(chr(9), ' ')
status.append(f'{key}\t{content}\t{kind}\t{",".join(over) or "-"}\t{status_note}')
parts = [note] if note else []
if file_sites:
parts.append(f'{len(file_sites)} sites' + (f', over {", ".join(over)}' if over else ''))
if runs:
parts.append(f'{data_bytes} bytes of main\'s data')
if dropped:
parts.append(f'{dropped} bytes into words the dynamic linker fills, which change nothing')
print(f'exefs: {kind:11} {mod}/{rel}' + (f' ({"; ".join(parts)})' if parts else ''))
if sets:
text += ('\n# ExeFS patches of the mods folder (tools/exefs_patches.py)\n' + '\n'.join(sets) + '\n'
+ '\n'.join(sites) + '\n')
args.out.write_text(text)
args.status.parent.mkdir(parents=True, exist_ok=True)
args.status.write_text('\n'.join(status) + ('\n' if status else ''), encoding='utf-8')
if args.data:
args.data.parent.mkdir(parents=True, exist_ok=True)
args.data.write_text('\n'.join([DATA_HEADER] + data_lines) + '\n', encoding='utf-8')
print(f'exefs: {len(files)} patch files: ' + ', '.join(f'{n} {k}' for k, n in sorted(kinds.items()))
+ f'; {len(sites)} sites')
+ f'; {len(sites)} sites, {len(data_lines)} data runs')
def read_ranges(path):
"""[(start, end)] of a folded.txt (recomp/gen.py): hex module offsets, one range per line, '#' comments."""
out = []
for line in path.read_text().splitlines():
f = line.split('#', 1)[0].split()
if len(f) == 2:
out.append((int(f[0], 16), int(f[1], 16)))
return sorted(out)
def data_offset(main_path):
"""The offset of main's .data segment in its image, from the NSO header (no decompression)."""
with open(main_path, 'rb') as f:
head = f.read(0x40)
return struct.unpack_from('<III', head, 0x30)[1]
def recheck(args):
"""After the lift: every file with a data write into bytes the lifted code read at lift time becomes unsupported."""
if not args.status.is_file() or not args.data.is_file():
print('exefs: no status or data file: nothing to check')
return
folded = read_ranges(args.folded) if args.folded.is_file() else None
data_off = data_offset(args.exefs / 'main')
lines = args.data.read_text(encoding='utf-8').splitlines()
bad = {}
for line in lines:
f = line.split('\t')
if line.startswith('#') or len(f) < 4 or f[0] in bad:
continue
a = int(f[2], 16)
b = a + len(f[3]) // 2
if folded is None and a < data_off:
bad[f[0]] = (f'it writes main+{a:X} in .rodata, and the lift left no list of the bytes it reads as '
f'constants')
elif folded is not None:
hit = next((s for s, e in folded if s < b and a < e), None)
if hit is not None:
bad[f[0]] = (f'it writes main+{max(a, hit):X}, which the lifted code reads when the game is built '
f'(a jump table): the change would not reach it')
if not bad:
print(f'exefs: data writes checked against {"the bytes the lift read" if folded is not None else ".rodata"}: '
f'none meets them')
return
status = []
for line in args.status.read_text(encoding='utf-8').splitlines():
f = line.split('\t')
if len(f) >= 5 and f[0] in bad:
line = '\t'.join([f[0], f[1], 'unsupported', '-', bad[f[0]]])
print(f'exefs: unsupported file {f[0]} ({bad[f[0]]})')
status.append(line)
args.status.write_text('\n'.join(status) + '\n', encoding='utf-8')
kept = [line for line in lines if line.split('\t')[0] not in bad]
args.data.write_text('\n'.join(kept) + '\n', encoding='utf-8')
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--exefs', type=Path, required=True)
ap.add_argument('--mods', type=Path, help='the mods folder next to totk.exe')
ap.add_argument('--patches', type=Path, help='the patch file so far')
ap.add_argument('--out', type=Path, help='the patch file with the mods\' ExeFS patches added')
ap.add_argument('--status', type=Path, required=True)
ap.add_argument('--data', type=Path, help='the data writes of the mods\' patch files (runtime/mods.c reads them)')
ap.add_argument('--folded', type=Path, help='after the lift: <gen>/main/folded.txt; checks --status and --data '
'against it instead of building')
args = ap.parse_args()
sys.stdout.reconfigure(errors='replace')
if args.folded:
if not args.data:
ap.error('--folded needs --data')
recheck(args)
else:
if not (args.mods and args.patches and args.out):
ap.error('--mods, --patches and --out are needed to build')
build(args)
if __name__ == '__main__':
+93 -3
View File
@@ -35,8 +35,9 @@ from the game's own files on the player's machine, nothing added (tools/button_i
stays right.
- every other prompt: the glyphs U+E040 A, U+E041 B, U+E042 X, U+E043 Y of the button font trade places.
- xbox-label, for pad_layout = xbox and auto. Those layouts put Switch A on the button labelled A (auto picks xbox for
every pad not labelled the Nintendo way), so the face buttons keep their letters; the action prompt's diamond keeps
the Switch positions (A on the right).
every pad not labelled the Nintendo way), so the face buttons keep their letters. The action prompt's diamond is
laid out as an Xbox pad's (bflyt_xbox_diamond): A at the bottom, B on the right, X on the left, Y at the top, the
button the prompt names lit in its place with its bar.
--xbox-prompts is the older name of --prompts xbox.
--prompts pc: keyboard and mouse prompts, for prompts = pc (romfs-patches-pc, or romfs-patches-pc-<w>x<h> with
--aspect): every button of the button font is drawn as the key or mouse button the keyboard defaults put on it
@@ -170,6 +171,92 @@ def bflyt_letters(data, base, end, swap):
return changed
def bflyt_panes(data, base, end):
"""name -> absolute offset of each pane section (pan1, pic1, txt1, wnd1, ...) of the BFLYT at data[base:end], and
name -> absolute offset of each material of its mat1, and its texture names (txl1)."""
assert data[base:base + 4] == b'FLYT'
pos, panes, mats, textures = base + struct.unpack_from('<H', data, base + 6)[0], {}, {}, []
while pos + 8 <= end:
magic, size = data[pos:pos + 4], struct.unpack_from('<I', data, pos + 4)[0]
if magic in (b'pan1', b'pic1', b'txt1', b'wnd1', b'bnd1', b'prt1', b'ali1', b'scr1'):
panes[data[pos + 12:pos + 36].split(b'\0')[0].decode()] = pos
elif magic == b'mat1':
for i in range(struct.unpack_from('<H', data, pos + 8)[0]):
at = pos + struct.unpack_from('<I', data, pos + 12 + 4 * i)[0]
mats[data[at:at + 28].split(b'\0')[0].decode()] = at
elif magic == b'txl1':
for i in range(struct.unpack_from('<H', data, pos + 8)[0]):
at = pos + 12 + struct.unpack_from('<I', data, pos + 12 + 4 * i)[0]
textures.append(data[at:data.index(0, at)].decode())
if not size:
break
pos += size
return panes, mats, textures
# The action prompt's diamond (blyt/DoCommand_00.bflyt) for pad_layout = xbox and auto, where Switch A, B, X and Y are
# the buttons labelled so on an Xbox pad: A at the bottom, B on the right, X on the left, Y at the top. The layout has
# one diamond per button the prompt can name (N_A_00, N_B_00, N_X_00, N_Y_00; N_StopA_00 is A's while held), each
# with its button and bar (N_Btn<K>_00), the three other letters (T_<letter>_<n>) and a base image (P_Base<K>_00)
# that lights the button's side. A Switch button's place on an Xbox pad is that of its partner on a Switch (A of B,
# X of Y), so each piece takes the place and look its partner's piece has: the bar of a button on the left reaches
# left (Y's on a Switch), the others reach right.
DIAMOND_SLOTS = {'A': (39.0, 1.0), 'B': (2.0, -39.0), 'X': (1.0, 39.0), 'Y': (-38.0, 0.0)} # the letters' places
DIAMOND_PARTNER = {'A': 'B', 'B': 'A', 'X': 'Y', 'Y': 'X'}
# (pane, the pane whose place it takes): the buttons with their bars, and the flashes of a repeated press (Rush)
DIAMOND_MOVES = [('N_BtnA_00', 'N_BtnB_00'), ('N_BtnA_01', 'N_BtnB_00'), ('N_CompanionBtnA_00', 'N_BtnB_00'),
('N_BtnB_00', 'N_BtnA_00'), ('N_BtnX_00', 'N_BtnY_00'), ('N_BtnY_00', 'N_BtnX_00')]
DIAMOND_MOVES += [(f'P_Capture{k}_{n:02}', f'P_Capture{DIAMOND_PARTNER[k]}_{n:02}') for k in 'ABXY' for n in (1, 2, 3)]
# X and Y trade the side their bar reaches to: the flash, the bar (Y's mirrored) and the text (Y's right-aligned)
DIAMOND_MIRROR = [('P_CaptureX_00', 'P_CaptureY_00'), ('W_BaseX_00', 'W_BaseY_00'), ('T_GuideX_00', 'T_GuideY_00'),
('T_GuideX_TW', 'T_GuideY_TW')]
# the base image: its texture (by its material) and its rotation and scale
DIAMOND_BASES = [('P_BaseA_00', 'P_BaseB_00'), ('P_BaseA_01', 'P_BaseB_00'), ('P_BaseB_00', 'P_BaseA_00'),
('P_BaseX_00', 'P_BaseY_00'), ('P_BaseY_00', 'P_BaseX_00')]
def bflyt_xbox_diamond(data, base, end):
"""Lay the action prompt's diamond in the BFLYT at data[base:end] (blyt/DoCommand_00.bflyt) out as an Xbox pad's
face buttons (DIAMOND_* above); returns the panes changed. A pane section holds its origin at +0x09, translation
(3 floats) at +0x2C, rotation (3) at +0x38 and scale (2) at +0x44; a text pane its text position at +0x5C; a
picture pane its material index at +0x64, and a material its first texture's index at +0x2C."""
panes, mats, textures = bflyt_panes(data, base, end)
old = bytes(data) # every piece takes its partner's place as the game has it
changed = []
for name, at in panes.items():
m = re.fullmatch(r'T_([ABXY])_\d+', name)
if m: # a letter around a diamond: its partner's place
struct.pack_into('<2f', data, at + 0x2C, *DIAMOND_SLOTS[DIAMOND_PARTNER[m[1]]])
changed.append(name)
for name, like in DIAMOND_MOVES:
data[panes[name] + 0x2C:panes[name] + 0x34] = old[panes[like] + 0x2C:panes[like] + 0x34]
changed.append(name)
# A's hold flashes (N_LoopA_03) sit 129 right of the diamond, as A's repeated-press ones: the same move as those
dx, dy = (a - b for a, b in zip(struct.unpack_from('<2f', old, panes['P_CaptureB_01'] + 0x2C),
struct.unpack_from('<2f', old, panes['P_CaptureA_01'] + 0x2C)))
x, y = struct.unpack_from('<2f', old, panes['N_LoopA_03'] + 0x2C)
struct.pack_into('<2f', data, panes['N_LoopA_03'] + 0x2C, x + dx, y + dy)
changed.append('N_LoopA_03')
for a, b in DIAMOND_MIRROR:
pa, pb = panes[a], panes[b]
for lo, hi in ((0x09, 0x0A), (0x2C, 0x4C)) + (((0x5C, 0x5D),) if old[pa:pa + 4] == b'txt1' else ()):
data[pa + lo:pa + hi], data[pb + lo:pb + hi] = old[pb + lo:pb + hi], old[pa + lo:pa + hi]
changed += [a, b]
in_order = list(mats.values())
def material(name): # the offset of a picture pane's material
return in_order[struct.unpack_from('<H', old, panes[name] + 0x64)[0]]
for name, like in DIAMOND_BASES:
assert old[panes[name]:panes[name] + 4] == b'pic1', name
data[panes[name] + 0x38:panes[name] + 0x4C] = old[panes[like] + 0x38:panes[like] + 0x4C]
mine, theirs = material(name), material(like)
data[mine + 0x2C:mine + 0x2E] = old[theirs + 0x2C:theirs + 0x2E]
assert textures[struct.unpack_from('<H', data, mine + 0x2C)[0]].startswith('Nt_KeyBtnSubBase'), name
changed.append(name)
return changed
def msbt_letters(data, swap):
"""Swap the one-letter messages labelled T_<letter>_<n> (A, B, X or Y) in the UTF-16 MSBT data (a bytearray) by
swap; the labels changed. The game sets a layout's text panes from the messages of the same name in
@@ -319,6 +406,9 @@ if args.prompts:
letters = {}
if by_position:
bntx_swap(blarc, bntx, KEY_TEXTURES)
diamond = []
if args.prompts == 'xbox-label':
diamond = bflyt_xbox_diamond(blarc, *members['blyt/DoCommand_00.bflyt'])
if letter_panes:
for member, (lo, hi) in members.items():
if member.endswith('.bflyt'):
@@ -350,7 +440,7 @@ if args.prompts:
if grown:
outputs[RESTBL] = icons.grow_restbl(unpack(RESTBL), grown)
prompts = dict(style=args.prompts, textures=KEY_TEXTURES if by_position else [], letters=letters,
messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
diamond=len(diamond), messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
lines = []
for path, contents in outputs.items():
+16 -7
View File
@@ -93,13 +93,14 @@
# fix_zero = 1
# Optimisations, 0 turns each off: skip bindings that did not change, remember shader lookups, reuse the previous
# draw's pipeline when its state repeats, remember texture descriptors between draws, update descriptors in bulk,
# skip push constants that did not change.
# skip push constants that did not change, bind only the vertex buffers a draw reads.
# bind_skip = 1
# shader_memo = 1
# pipe_memo = 1
# tex_memo = 1
# desc_template = 1
# push_skip = 1
# vb_used = 1
# Submit GPU work in parts every N draws between passes so the GPU starts earlier (0 to 1000; 0 = off), each part at
# least split_us microseconds long (0 to 20000).
# split_draws = 64
@@ -145,12 +146,12 @@
# the Steam Deck's 1280x800 (16:10), gives the camera's view that shape and lays the HUD out for it. The installer
# prepares the UI layout for fifteen sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160 (21:9), 3840x1080, 5120x1440
# (32:9), 1280x800, 1680x1050, 1920x1200, 2560x1600 (16:10), 1920x1280, 2160x1440, 2256x1504, 2880x1920 and 3000x2000
# (3:2). Any other size
# gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
# tools/make_romfs_patches.py --aspect WxH. Read at start. Default: 1920x1080, or on a primary display of one of those
# shapes the smallest of its sizes at least 1080 high that fits the display (1920x1280 on a 3:2 laptop, 2560x1080 on
# a 21:9 monitor), so the picture fills the screen without black bars; with prompts = xbox or pc it stays 1920x1080,
# the only shape the setup builds those prompts for.
# (3:2). For any other size, or a source build that has not made the layout, the game draws the largest 16:9 size that
# fits inside it (3440x1080 gives 1920x1080) and the present adds bars; build the layout with
# tools/make_romfs_patches.py --aspect WxH to use the full shape. Read at start. Default: 1920x1080, or on a primary
# display of one of those shapes the smallest of its sizes at least 1080 high that fits the display (1920x1280 on a 3:2
# laptop, 2560x1080 on a 21:9 monitor), so the picture fills the screen without black bars; with prompts = xbox or pc it
# stays 1920x1080, the only shape the setup builds those prompts for.
# output_res = 1920x1080
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
@@ -366,6 +367,14 @@
# the setup's rebuild at start (a few minutes) and starts again after it. off: the game starts with the code it has.
# A rebuild that failed or was cancelled is not started again for the same cheats. Default: auto.
# mods_rebuild = auto
# Extra room in MiB (0 to 4096) for the heap the world's textures stream into, for HD texture packs. Packs made for
# emulators with 8 GB of DRAM grow it in their own HeapDef table (System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs):
# the port reads that growth at start and applies it itself (about 3339 MiB for the HDPacks 4K pack), so such a pack
# needs nothing here. Set it for a pack that ships textures without a HeapDef. Without the room the game keeps starting
# its panic Blood Moon. The memory pool grows by the same amount; the larger of this and a mod's growth counts.
# Read at start. Default: 0. Such a pack also needs much more video memory: astc_bc = 1 ([performance]) keeps its
# textures at a quarter of it.
# tex_heap_extra_mb = 0
[files]
# Developer overlay: a folder with RomFS files and a manifest.tsv (tools/make_romfs_patches.py format). Players put