98 Commits
Author SHA1 Message Date
Valiantsin DzerakhandClaude Opus 5.5 f469c5a774 Raise the version to 0.2.8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:17:15 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1d6baac9d2 Merge pull request #233: Add colour profiles and brightness, contrast, saturation and gamma for the picture (Lucas-blip-png)
With TOTK_COLOR_* away from their defaults, present_image runs one compute pass (vk_color.c) that writes the game
picture's crop, adjusted, into an RGBA16F image, which FSR 1 or the blit then scales. At the defaults nothing is
recorded and the present takes the same source, format and rectangle as before. The Screen tab gets a Colour group
(profile and four sliders) after the field-of-view slider of #248; cfg.c and totk.ini.example keep both settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:15:35 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b768567f8d Merge pull request #248: Add a field-of-view setting for 1.4.3 (Lucas-blip-png)
TOTK_FOV (70..140 percent, Screen tab, live) multiplies the camera's vertical view angle through the new patch set fov
at the two camera routines the aspect set patches; page +60 holds the factor. At the default 100 the set is off and the
camera runs the game's own instructions. Both fov sites and the rest of the 1.4.3 table pass patches.check against the
1.4.3 main. 1.2.1 and 1.1.1 have no fov sites and log that the setting does nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:14:50 +03:00
lunar-meandClaude Opus 5.5 bdad89e0e4 Say "an update file other than the recorded one" in the installer guide (#247)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:14:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 db4d04a6e5 Merge pull request #269: Trust the narrowed shader walk when it finds the version's shaders (shenjackyuanjie)
The shader list covers several versions, so most of its rows are absent from any one game and the old test fell back to
reading the whole RomFS on every install: 23,825 of 41,414 rows on 1.2.1, 23,370 on 1.4.3. The fallback now runs only
when the narrowed walk finds next to nothing. On 1.2.1 and 1.4.3 the narrowed walk finds the same shaders as the full
one (27,576 and 28,667, none only in the full walk) in 2 s instead of 51 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:14:34 +03:00
Valiantsin DzerakhandClaude Opus 5.5 407c6915b5 Merge pull request #220: Support the 1.1.1 update as a third game version (shenjackyuanjie)
1.1.1 (update 131072) gets its boot constants, expected hashes, the 60 fps
RomFS overlay, its own code patch table, the scene module slot, loader probe
values and shader census lookup, and the setup accepts it. 1.2.1 and 1.4.3
keep every value: the runtime branches on RT_V111, and an install without a
recorded version still falls back to 1.2.1.

1.1.1 has no aspect patch set; outputs other than 16:9 are scaled to the
window on that version.

Fixes #173

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:03:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f408f26363 Merge task/savecommit: no save or backup for a Commit that changed nothing, a short swap retry (#260, #263)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:03:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d7266301d2 Skip the save and backup of a Commit that changed no byte, and retry the swap briefly
At start the game rewrites its save files with the same bytes and commits
twice; each of those commits made a new committed save and a backup, so one
start pushed every older backup out of save-backup. A Commit whose copy is
the newest save file for file (all hard links, same names) now only retries
a waiting swap.

Renaming the committed save away is retried for 200 ms instead of 2 s: a
failure no longer loses the commit, the next Commit or start retries, and the
game's next change to its save data waits for the commit thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:00:01 +03:00
shenjackanddeepseek-v4.1-flash-1017 55c7411de0 Give the loader probe and the shader census the 1.1.1 values
Three places still assumed the two older versions:

- loader.c compared a 1.1.1 load against 1.2.1's expectations, so --probe-loader failed every time:
  main has 10,668 constructors against 1.2.1's 10,673, and its relocation kinds and totals differ.
  1.1.1 has its own table; subsdk0 and sdk match 1.2.1 down to every count.
- gfx_shader_probe.c looked for build/v121 and build/v143 only, so a census built for 1.1.1 was never
  found and --probe-shader failed on a checkout that has only that version. It looks for the running
  version first, then the other two.
- layout.py put 1.1.1 first in SUPPORTED while steps.py took SUPPORTED[0] as the version of an install
  made before versions were recorded, and such an install is 1.2.1: updating one picked 1.1.1's hashes
  and invalidated its extraction. The tuple keeps 1.2.1 first and the fallback names 1.2.1 outright.

--probe-loader now reports 0 failures on a 1.1.1 install.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:40 +08:00
shenjackanddeepseek-v4.1-flash-1017 1ea8813bed Read the scene module of 1.1.1 from its own slot
frame_rate.c finds the player through a global pointer in main's data segment: 1.4.3 keeps it at
main+0x3add238, 1.2.1 at main+0x4728538, and 1.1.1 was borrowing 1.2.1's address, which points at
something else in that build.

The 1.1.1 slot is main+0x4630a68. It was measured from the build itself: one function there walks
the whole chain this code walks (module+0x1e8 scene, the 14-component check at +0x50, component 13
at +0x58, the 0x70 link stride, the name compare) and 181 scene accesses read the same slot, which
makes it the scene module and not one of the single-instance pointers next to it.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:39 +08:00
shenjackanddeepseek-v4.1-flash-1017 6b10489e59 Let the setup take the 1.1.1 update
The update's hashes, the patch table and the boot constants are in place; the setup itself still refused the
version. SUPPORTED lists it now, so a player with that update can build.

The labels and messages that named the two supported updates name this one as well, so a player whose update is
1.1.1 is no longer asked for a file they do not have.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:39 +08:00
shenjackanddeepseek-v4.1-flash-1017 6d27cf8202 Add the 1.1 code patch table with its spin waits, hair and cloth sites
The 60 fps frame step and ratio, the render resolution, the slow-motion check and the actor animation clock, at the
addresses this build has them: each site was located again by instruction-window matching and its words checked against
the 1.1.1 main. The spin-wait back-edges are the ones that only another thread can end, and the last five sites are the
two inlined clocks of the animation update with the shared call they hand their seconds to.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:38 +08:00
shenjackanddeepseek-v4.1-flash-1017 93eeb8b29f Record the expected hashes of the 1.1 update
The ExeFS digests, the merged RomFS size and hash and the six shared fonts, so extract.py can tell this update from any
other. No rewritten_sha256: that field names MacNativeGames' NSO rewriter output, this dump never went through it, and
nothing reads it.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:38 +08:00
shenjackanddeepseek-v4.1-flash-1017 d8a211af7f Write the 60 fps RomFS overlay for 1.1
Its Common archive carries the same version number as 1.2.1, so make_romfs_patches.py takes v111 and reads
Common.Product.110, moving the quick-menu background captures to animation frame 0 where the world still draws.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:37 +08:00
shenjackanddeepseek-v4.1-flash-1017 2b16578f69 Boot the 1.1 game: its init_array count, checkpoint addresses and version name
The build knows the player's update as v111: DISPLAY_VERSIONS maps it for the version line of the settings menu, and
boot.c carries its two checkpoint return addresses (0xeb8c14 for SetFocusHandlingMode, 0xebcdec for the first
nvnBootstrapLoader call) beside the 1.2.1 and 1.4.3 ones, with init_array counts of 10668, 45 and 116 for main, subsdk0
and sdk. The build guide lists v111 among the versions every step takes.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:37 +08:00
shenjackanddeepseek-v4.1-flash-1017 157e619084 Trust the narrowed shader walk when it finds the version's shaders
The fallback read the whole RomFS whenever the shader list named shaders this version does not have. The list covers
several versions, and 1.1.1 holds 17,587 of its 41,414 rows, so every 1.1.1 install read all 304,469 files again:
23827 of the 41414 listed variants are not among the 27576 shaders found, and the narrowed walk costs 2.8 s against
70.6 s for the whole RomFS.

What says the walk fell short is a walk that found next to nothing, which is when the shader directories have moved.
Counting absent list rows does not: for 1.1.1 most of them are absent because the game does not have them.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:32 +08:00
Valiantsin DzerakhandClaude Opus 5.5 47485da7d9 Merge pull request #262: Linux fixes (wdiasjunior)
Shader preparation reads MacNativeGames' whole shader list history,
release downloads resume from a .part file, appimagetool and the type-2
runtime come from release tags, and the preparation summary counts the
vertex and fragment variants it skips with the graphics pipeline library
off.

The gettid cache in runtime/platform/linux.c conflicted with the same fix
already on dev (8dd7e20, #236); dev's version is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:45:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1ba0424774 Merge task/savecommit: a save commit that cannot replace the committed save waits in save-tx instead of being lost (#260, #263)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:35:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 65fd631fea Keep a save commit that cannot replace the committed save instead of dropping it
Windows refuses to rename a directory while any file under it is open, so a
virus scanner, Explorer's preview or a sync tool holding a file of
save/<uid> made every Commit of a session fail after its 2 s of retries.
The game had its Commit return 0 and showed the new slots from the working
copy, and the next start deleted that copy: hours of play were gone (#260,
#263, log line "cannot move the committed save (error 5)").

A Commit now renames its finished staging copy to save-tx/ready-<n> before it
touches the committed save. That rename is the commit point. When the swap
into save/<uid> fails, ready-<n> stays the newest save: later working copies
and the next start are made from it, and the next Commit or start retries the
swap. The failure log names the programs holding files in the committed save
(Restart Manager) or says the folder refuses the rename (no DELETE access).
--probe-fs holds a committed file open across two Commits and restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:20:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8a4ab178ec Raise the version to 0.2.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:04:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8041ded696 Merge task/crashdiag: menu without the core gate, system font check, abort reports, command buffer diagnostics (#212, #176, #94)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:57:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e84c61f16d Say why a command recording buffer could not grow
A player's game ended with "nvn: command recording buffer of 1048576 bytes" during a dive into the Depths (#94):
realloc refused 1 MiB with commit to spare. That leaves a buffer pointer that is no block of ours, from a command
buffer object the game overwrote or initialized again without Finalize. The fatal line now gives the object, its
magic, the old buffer and its size, the bytes used and asked, and Initialize logs (eight times at most) when it
replaces an object that was not finalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:57:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e451ce6a0d Report abort and invalid C runtime parameters as crashes on Windows
abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function raise no exception: the
C runtime ends the process with 0xc0000409 and the log stops without a FATAL line. A SIGABRT handler and an
invalid-parameter handler now send both to the crash hook with the host return addresses in totk.exe, and the
report lists them as totk+offset frames after the guest registers. Checked with win32.c in a test program: assert,
abort and fclose(NULL) each give one report whose innermost frame is the failing call.

lifted_function no longer names the nearest lifted function for an address above the lifted code, so a host frame
of the runtime is not reported as game code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:57:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 637b98dea7 Check a system font before the menu's font atlas takes it
The menu merges the Japanese, Korean and Chinese language names from a system font. Dear ImGui asserts on a font
stb_truetype cannot parse, and the assert ends the process with 0xc0000409 (exit code 127 in a shell) and no line in
the log, a few seconds after the window opens: the report of an AMD laptop on Windows 11 Home Single Language (#176)
matches that, and a test build of our ImGui with a damaged font exits the same way. A cut-off font passes the parse
and faults later, when the glyphs are made, because stb_truetype reads tables without bounds checks.

fonts_up now runs each system font through a private copy of stb_truetype whose asserts only mark the font bad, after
checking that every table of its directory lies inside the file, and leaves out a font that fails with a log line.
--probe-window checks that the test takes the embedded Roboto and refuses a cut-off copy, one without a character
map and one with an unknown tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:56:57 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a554a7c13c Take the virtual core gate out of the settings menu
The row was labelled "Virtual core gate" with the text "Lets only one thread run per virtual core, as on the
console", but switching it on wrote free_cores = 1, which lifts the gate. Without the gate guest threads run in
parallel on one virtual core, and TotK keeps its per-core data without locks: an Arc B580 player who turned it on
crashed at every start with "CallCommands: command handle ... is not a live recording" (#212). free_cores stays an
ini key for experiments; the log now says when it is set, and totk.ini.example warns that the game can crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:56:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c346c7fde2 Merge task/camlabel: write back GPU renders into textures that share their page (#13, #63, #118, #252)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:55:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 de3ed42a4c Write back GPU renders into textures that share their page when the texture's own bytes are unchanged (#13, #63, #118, #252)
The page write tracking dropped the render of any texture smaller than a page whenever the CPU wrote another
part of that page. The Purah Pad camera renders the ID, screen rect and depth of what is under the reticle into
4x4 targets in two slots and reads them a frame later. On 1.4.3 one slot's rect target shares its page with data
the CPU writes every frame, so the game read a zero rect every other frame and the target's name label blinked; on
1.2.1 the targets of both slots were dropped and nothing was recognised. A texture that fills no page now keeps a
copy of its storage bytes from the GPU write, and a page write counts only when those bytes changed.
TOTK_WB_EXACT=0 restores the old decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:55:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 da6cd4009e List the bundled libXft and libXrender in the third-party notices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:05:30 +03:00
Valiantsin Dzerakh 14cc5f1d9b Bundle libXft and libXrender with the Xft Tk so the installer opens on hosts without them 2026-10-31 18:56:40 +03:00
Valiantsin Dzerakh e4eecef0f7 Pick the installer log font next to the UI font 2026-10-31 18:52:38 +03:00
Valiantsin Dzerakh ec34382f0a Use the platform UI font for the idle step rows in the installer 2026-10-31 18:52:38 +03:00
Valiantsin Dzerakh f8d8a4c553 Merge pull request #230: Restore Russian text in the Linux installer with Xft (lcd1232) 2026-10-31 18:52:09 +03:00
Wellington Dias 9865e7ab9a Count the vertex and fragment variants preparation skips with the graphics pipeline library off, and say so in its summary instead of reporting them as built. 2026-10-31 12:48:36 -03:00
Valiantsin Dzerakh e34909b40d Merge pull request #229: Find the shaders by their directories instead of reading the whole RomFS (shenjackyuanjie) 2026-10-31 18:48:09 +03:00
Wellington Dias 02d79bd378 Read a thread's id once on Linux, where gettid is a system call and every recursive mutex lock asked for it. 2026-10-31 12:41:49 -03:00
Wellington Dias 42e5c98395 Pin appimagetool 1.9.1 and the type-2 runtime 20251108 from their release tags, so a new upload under continuous no longer stops the AppImage build. 2026-10-31 12:41:49 -03:00
Valiantsin DzerakhandClaude Opus 5.5 e38eaa8224 Turn the recorder thread back on by default on AMD GPUs
The AMD device losses with the recorder (#5, #40) stopped after objects
were kept until their submission completed and the recorder began ending
its own parts; RDNA 1 and 2 cards in #5 ran with vk_recorder on and off.
vk_recorder = 0 still turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:41:38 +03:00
Wellington Dias 522c69e951 Download a cached release file again when its hash no longer matches its pin, in the same run, instead of stopping. 2026-10-31 12:40:55 -03:00
Wellington Dias e8f56106de Read the MacNativeGames pin in build_xlate.py from a fetch line with the history word too. 2026-10-31 12:39:41 -03:00
Wellington Dias 8f6664d758 Download the release's pinned files into a .part file that a rerun resumes, and delete a file whose hash is wrong, so a cut download no longer fails every later run. 2026-10-31 12:38:33 -03:00
Wellington Dias ec7bb5db0a Fetch MacNativeGames with its history and point origin/HEAD at each pinned commit, and stop shader_list.py on a shallow clone, so shader preparation reads the whole list instead of failing or quietly preparing a fraction of it. 2026-10-31 12:38:09 -03:00
Valiantsin Dzerakh bf7c507e4c Merge branch 'task/tidcache' into dev 2026-10-31 18:26:01 +03:00
8dd7e2041e Cache the thread id per thread on Linux (#236)
plat_thread_id made a gettid syscall on every call, and plat_rmutex_lock
calls it on every acquire: about 685,000 syscalls a second in the open
world on 1.4.3. A thread's id never changes and the runtime starts child
processes with posix_spawn only, so the id now comes from a __thread cache
after the first call. Windows is unchanged: GetCurrentThreadId reads the TEB.

Found, measured and fixed by H.A. Forster in issue #236 (RTX 2060 laptop:
36.3 to 46.0 fps with replay_async). SchulzCode confirmed it on an RTX 2070
Super with a 7800X3D: 98 to 120 fps at game resolution, 76 to 90 fps at
render_scale 150.

Co-Authored-By: H.A. Forster <154697759+h-a-forster@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:10:26 +03:00
Lucas-blip-png 459d414c29 Add a field-of-view setting for 1.4.3
The camera update and setup routines that the aspect set patches load the camera's vertical view angle a few words
before the ratio and pass it to sead's perspective projection. The new set fov loads the angle as before and multiplies
it by a factor the runtime writes to patch page +60, so TOTK_FOV (70..140 percent, menu Screen tab, live) widens or
narrows the view. The game's own zooms (bow, scope) scale with it; 100 switches the set off.

Checked on 1.4.3 at 1920x1080 at 70, 100, 130 and 140: the view narrows and widens around Link, the HUD is unchanged,
no objects appear or vanish at the edges, no validation errors. 1.2.1 has no fov sites yet (its original words are
unverified here); the setting logs that and does nothing there.
2026-10-31 11:08:06 -04:00
f482217350 Step the 1.2.1 equipment-switch cooldown until it is no longer positive (#18)
1.2.1 has the same mismatch as 1.4.3 at the same player state field: the
update steps +0x109c only while it is above FLT_EPSILON and the equipment
gate blocks changes while it is above 0. The realtime set now compares with
0 in the update. Ported from the 1.4.3 patch by JieML1100 in issue #18.

Co-Authored-By: JieML1100 <183249798+JieML1100@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:00:48 +03:00
a80c78f08d Step the 1.4.3 equipment-switch cooldown until it is no longer positive (#18)
The player update steps the countdown at player state +0x109c only while it
is above FLT_EPSILON, while the equipment gate blocks weapon, shield and bow
changes while it is above 0. With the real-time step a rounding remainder in
(0, FLT_EPSILON] stays there and locks equipment after a Fuse until a load.
The realtime set now steps the countdown while it is above 0. Root cause and
patch by JieML1100 in issue #18.

Co-Authored-By: JieML1100 <183249798+JieML1100@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:59:40 +03:00
Lucas-blip-pngandSTS78721 c0567191f0 Add colour profiles and brightness, contrast, saturation and gamma for the picture
Issue #136. With TOTK_COLOR_* away from their defaults, present_image runs one compute pass (vk_color.c, shaders/color_grade.comp) that writes the game picture's crop, adjusted, into an RGBA16F image, and FSR 1 or the blit scale that one; at the defaults nothing runs. The Screen tab gets a Colour group: a profile (Game's own, Vivid, Clarity, Custom) that sets the four values, and the four sliders. The adjustment, its order, ranges and presets are STS78721's patch from the issue, reworked onto dev: it now applies with FSR 1 scaling as well as the blit, its menu texts come from the catalog in English and Russian, and the patch's unused accessors and build_game.py change are left out.

Co-authored-by: STS78721 <134802927+STS78721@users.noreply.github.com>
2026-10-31 10:02:16 -04:00
shenjack 6813b60ed9 Find the shaders by their directories instead of reading the whole RomFS
inventory() read every RomFS file into memory and decompressed it before deciding whether the file can hold a
shader. On 1.1.1 that is 304,469 entries and 15.86 GiB, and the shaders live in six directories: 1,996 entries,
147 MiB, 0.9% of the bytes. The walk visits those directories and reads everything else only when the shader list
names keys it did not find there.

Measured on one install: 66.50 s -> 3.35 s for the same 27,576 shaders, key for key.

pack_shader_cache.py also rewrote its pack after every prepare although there was nothing to fold in: 2.6 s of
copying 1.37 GiB over the same entries, and a file system that then releases the old pack's blocks. It returns at
once when the cache holds no loose files.
2026-10-31 21:42:04 +08:00
lcd1232 d0e27b95c4 Restore Cyrillic rendering in the Linux installer with Xft 2026-10-31 17:40:54 +04:00
Valiantsin DzerakhandClaude Opus 5.5 7d62cbfa85 Pin the current appimagetool build in make_release_linux.py
The continuous tag got a new upload, so the old sha256 stopped the AppImage build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:37:37 +03:00
Valiantsin Dzerakh 9935d97b3f Bump version 2026-10-31 15:14:18 +03:00
Valiantsin DzerakhandClaude Opus 5.5 98a657dfd1 Give 1.2.1 the camera aspect ratio and the HUD layouts of other shapes
patches-v121.txt takes the aspect set at upstream's 1.2.1 sites (game_patches.c aspect_install): the same two camera
loads and two viewport divisions as 1.4.3, with the same original words and the same replacement from page +56. The
installer now builds the romfs-patches-<w>x<h> overlays for 1.2.1 too; ultrawide_layout.py passes on its
Common.Product.110 at all ten sizes. Checked in game at 3440x1440 (camera 2.3889) and 1920x1200 (1.6000): wider or
taller view, HUD at the corners or edges, no validation errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:23:58 +03:00
Valiantsin DzerakhandClaude Opus 5.5 cbc3cc94d8 Set the probe's environment through plat_setenv so astc_probe.c builds on Linux
astc_probe.c came from dev with _putenv_s, which the Linux build of the merged runtime rejects. check_platform.py now
flags _putenv and _putenv_s in shared files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:59:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0db841c297 Lay the HUD out for 16:10 game resolutions on 1.4.3
ultrawide_layout.py handled canvases wider than 16:9 only, so the Steam Deck's 1280x800 got the 16:10 camera with
the 16:9 HUD stretched 11% over it. A narrower canvas now scales each layout's root pane in y by the aspect over 16/9,
stretches full-screen backgrounds back and moves the HUD to the top and bottom edges. The pane list and distances are
the TALL table of tools/aspect_layouts.py from pull request #60, in the format of the wide edits; the minimap,
discovery banner and dragon's tears keys are found by walking the animation's curves. Every value is checked against
the shipped one before it is written: 1.4.3's Common.Product.140 and 1.2.1's Common.Product.110 pass at 1280x800 and
1920x1200, and the 3440x1440 overlay is byte-identical to the one before.

The settings menu offers 1280x800, 1680x1050, 1920x1200 and 2560x1600, and the installer builds their overlays with the
21:9 and 32:9 ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:31:39 +03:00
Valiantsin DzerakhandClaude Opus 5.5 43a18a9bb1 Merge pull request #60: Linux port (AppImage) and Steam Deck support (wdiasjunior)
The branch forked 79 commits before main and 98 before dev. Resolutions:

1. Aspect ratio: dev's implementation stays (camera patch at page +56 on 1.4.3, tools/ultrawide_layout.py layouts per
   size in romfs-patches-<w>x<h>, the aspect = fit|stretch setting). The PR's second implementation is dropped: the
   aspect sets of patches-v121.txt and patches-v143.txt (the 1.4.3 one patched the same viewport sites as dev's),
   tools/aspect_layouts.py, the resolution.c shapes that limited game resolutions to 16:9, 16:10 and 21:9, and the
   picture_fit setting. Its letterbox code (gfx_fit_rect) is kept and reads TOTK_ASPECT.
2. Code dev changed where the PR moved it: the stick dead zone, the test gamepad and the paddles go to input.c; the
   DPI awareness, keep-awake and IME detach of gfx_window.c go to gfx_window_win.c; device fault reports and GPU
   breadcrumbs to gfx_present.c. gfx_window_dpi gets an SDL version.
3. dev's Win32 calls in shared code go through platform.h: the shader cache pack reader (PlatFile), the romfs
   overlay checks, the audio pacing waits, the motion, ASTC and device fault locks. plat_cpu_usable and
   plat_commit_info get Linux versions; plat_cpu_count counts the machine's processors on both. dpi_probe.c becomes
   dpi_probe_win.c.
4. assume_focus defaults to 1 as the PR has it (a load interrupted by a focus loss can stop for good) and stays live,
   with dev's Game tab row.
5. The frame limits 40 and 45 join dev's list. The BC 3D storage fallback for Vega only runs with
   TOTK_BLOCK_VIEWS_3D=1, since dev makes 3D BC images without block views by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:26:42 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0aa529b4ce Offer the dynamic alpha to coverage switch in the menu and fix stale comments
TOTK_VK_DYN_A2C from pull request #188 gets the settings row, the Advanced tab choice and the totk.ini.example
line that TOTK_VK_DYN_VERTEX has. The IME comment in gfx_window.c window_main named a walk key the runtime does not
have, and ov_system.c still said TOTK_ASSUME_FOCUS opens the window without focus, which TOTK_BACKGROUND_WINDOW does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:54:05 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5cbabc4e77 Merge pull request #187: Offer the 32:9 sizes 3840x1080 and 5120x1440 with their HUD layout (Lucas-blip-png)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5f1a94c5e3 Merge pull request #196: Size thread pools by the CPUs of the process's affinity mask (Lucas-blip-png)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 833d04be17 Merge pull request #195: Offer the keep-running-out-of-focus switch in the Game tab (shenjackyuanjie)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:47 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4f7403550d Merge pull request #194: Take a compiler cache in front of clang from SCCACHE (shenjackyuanjie)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:46 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1681ad1bd6 Merge pull request #193: Detach the game window from the input method (shenjackyuanjie)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 577d8efbaf Merge pull request #190: Document how to connect a Switch 2 Pro Controller with motion (applezra)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6c445ede2b Merge pull request #188: Take alpha to coverage out of the dynamic state on AMD's RDNA 2-and-older driver (fixes #67) (tuwary)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:43 +03:00
Valiantsin DzerakhandClaude Opus 5.5 59e7884174 Merge pull request #184: Keep long menu descriptions whole in the description pane (Lucas-blip-png)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:51:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4b6158f883 Merge task/rdna2hires: depth images that outgrow their NVN storage get their own memory (RDNA2 garbage above 1080p)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:05:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c33af6020b Give a depth image its own memory when it needs more bytes than its NVN storage
AMD's RDNA2 driver places the depth compression metadata after the depth data. Above 1080p that tail of a depth
target bound into the game's texture heap lay over the next texture, and every depth write scrambled it: a garbage
band at the top of the screen, a pink glow, blocks in the grass and a blue checkered scene in the menu (RX 6600 to
6800, issue #185). Reproduced at 2560x1440 on an RDNA2 iGPU with driver 26.10.1; all 63 oversize images there were
D16 or D32 depth targets. Colour images keep their place in the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:01:24 +03:00
Lucas-blip-png 91c5312932 Size thread pools by the CPUs of the process's affinity mask
Eight places sized a pool (pipeline workers, library prebuild, translator prewarm and seeding, the multithreaded ASTC decoder) by GetActiveProcessorCount, every logical processor of the machine. A process limited with start /affinity, Task Manager or a launcher then started a pool for 16 CPUs on 4. plat_cpu_usable counts the affinity mask (all active processors for a process that spans processor groups); plat_cpu_count, the machine's count, stays for the system info. Idea from MMadmer's UsableCpuCount in a fork of the Shadow of the Colossus PC port.
2026-10-31 04:51:14 -04:00
shenjack e4eef8ae0c Offer the keep-running-out-of-focus switch in the Game tab
The game pauses whenever its window loses focus, as it does on the Switch when Home is pressed, and ASSUME_FOCUS was the
only way to change that: a known key with no row in the settings table. The runtime reads it at each focus change rather
than once at start, so the row takes effect the next time the window is left, and the build guide says what an unfocused
window now does.
2026-10-31 16:50:32 +08:00
shenjack 7508469b6e Take a compiler cache in front of clang from SCCACHE
A lift rewrites every generated chunk with the same contents, so a local sccache hits all of their objects and the
compile step costs seconds instead of minutes. build_game.py writes it into the ninja file ahead of cc when the
environment names one; the build guide describes the setting.
2026-10-31 16:49:40 +08:00
shenjack 1f87069e50 Detach the game window from the input method
A Chinese IME switched on over the game attached to the window, then showed its candidate window and took the keys it
composes although there is nothing to type into here; Shift, which a player may have bound as the walk key, is one of
the keys such an IME takes. The software keyboard's edit box is a window of its own and keeps the IME it needs, so only
the game's window is detached.
2026-10-31 16:49:16 +08:00
Valiantsin DzerakhandClaude Opus 5.5 c944f9f945 Split the issue form into crash, graphics, other bug and feature request
Each form applies its own labels, asks whether the reporter can keep playing,
and requires a check of the pinned and existing issues. Questions go to
Discussions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:44:37 +03:00
tuwaryandClaude Opus 5.5 bca7e8aea5 Take alpha to coverage out of the dynamic state on AMD's RDNA 2-and-older driver
AMD's Windows driver branch for RDNA 2 and older (vk_caps.amd_sc, driverInfo
"(AMD proprietary shader compiler)") applies alpha to coverage even while
vkCmdSetAlphaToCoverageEnableEXT sets it to false. TotK never turns alpha to
coverage on (a TOTK_DUMP frame showed it off in all 1170 pipelines), yet on an
RX 590 with driver 24.9.1 the character draws kept their depth and lost their
colour in the G-buffer, so Link and the NPCs were black silhouettes, and other
surfaces were dithered into checkerboards (issue #67, RX 470/570/580).

On that driver branch vk_pipeline.c ensure_init now leaves alpha to coverage out
of the dynamic state, so it goes into the pipeline key as on GPUs without EDS3.
TOTK_VK_DYN_A2C=0/1 (ini vk_dyn_a2c) forces either behaviour. NVIDIA and RDNA 3/4
are unchanged. Tested on the RX 590 with 1.4.3: characters, the checkerboards and
the black blocks all render correctly.

Fixes #67.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcYzeu7eb2txzmu4brvcgh
2026-10-31 16:19:45 +08:00
Lucas-blip-png 76563b34a9 Offer the 32:9 sizes 3840x1080 and 5120x1440 as game resolutions with their HUD layout
tools/ultrawide_layout.py computes every edit from the canvas aspect, so 32:9 needs no new layout data: the installer now builds romfs-patches-3840x1080 and -5120x1440 beside the four 21:9 sizes, and the menu lists both. Before, a 32:9 output_res got the wide camera with the 16:9 HUD stretched over it (issue #181).
2026-10-31 04:16:55 -04:00
applezraandClaude Opus 5.5 bbc6212d16 Document how to connect a Switch 2 Pro Controller with motion
SDL does not open the Switch 2 Pro Controller on Windows by itself, so it needs a bridge program. Tested with the
game: hbashton's DS4Windows fork emulating a DualSense gives buttons, rumble, gyro, accelerometer and calibrated
sticks, read directly by the game. Bridges that output a ViGEm DualShock 4 (ProCon2Windows, connectyourpro) give rumble
but frozen motion values, so motion controls do nothing with them. docs/controls.md gets a short section with the log
lines that show a working setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:08:17 -07:00
Lucas-blip-png 48d9f0f719 Keep long menu descriptions whole in the description pane
The pane copied each description into 512 bytes. The Russian button layout description is 840 bytes of UTF-8, so it stopped mid-sentence, and the English one (507 bytes) lost the note appended after it. The pane now holds UI_DESC_BYTES (2048), and probe_menu_text checks every English and Russian menu text leaves room for a note.
2026-10-31 03:40:09 -04:00
Wellington Dias c626cda8ea Find the translator and spirv-val by OS in xlate_smoke.py, so the smoke test runs on Linux. 2026-10-31 02:05:35 -03:00
Wellington Dias e45eb07c6f Name each save backup so it sorts after every existing one, so a second commit in the same millisecond no longer gets a name the pruning deletes at once. 2026-10-31 02:05:21 -03:00
Wellington Dias 01712150a4 Read the overlay's GPU load and VRAM from the DRM card on the game GPU's PCI bus, so a machine with an AMD iGPU and an NVIDIA GPU no longer shows the idle iGPU's numbers. 2026-10-31 02:03:34 -03:00
Wellington Dias aaa9610aaf Refuse to replace a read-only file in plat_file_write_atomic on Linux, as Windows does, so a read-only totk.ini stays as the player left it. 2026-10-31 02:02:27 -03:00
Wellington Dias a4c99a47b0 Remove environment variables with plat_setenv(name, NULL) and set them through the C runtime on Windows, so getenv sees every change and --probe-cfg passes on Linux. 2026-10-31 02:02:27 -03:00
Wellington Dias efe1b5db54 Check that the built hactool starts with an empty home folder, so a malformed key in the builder's own keys file no longer fails the build. 2026-10-30 21:32:12 -03:00
Wellington Dias ff72b2e241 Use the profile folder next to the game when it exists, so a copied install no longer looks for the profile at the path it was built with. 2026-10-30 21:01:18 -03:00
Wellington Dias 6b4b228fdd Name no operating system in the oe log lines, so Linux logs stop saying Windows.
The application environment and performance request lines and the services probe's locale line said Windows on every
host; a Steam Deck tester's log printed them on SteamOS.
2026-10-30 16:32:49 -03:00
Wellington Dias 7285688165 Give the interface its proportions at 16:10 and 21:9, and limit game resolutions to those shapes and 16:9.
The game lays its interface out in a 1920x1080 space and draws it over the whole output, so at 1280x800 every menu and
the HUD were 11 % taller than drawn, and at 21:9 34 % wider. tools/aspect_layouts.py rewrites the layout archive for
each shape: every layout's root pane scaled back, full-screen backgrounds kept full, the HUD moved to the screen's
edges, and the minimap, place name and dragon's tears map icon animations adjusted. It follows the behaviour of
Fruithapje21's TOTK-Ultrawide (GPL-3.0, read for behaviour only) and checks every original value before it writes.
make_romfs_patches.py writes the results to romfs-patches/16x10 and 21x9 at install and build time, and romfs.c serves
the one of the output's shape. resolution.c accepts 16:9, 16:10 and 21:9; another shape draws 16:9 with bars. The
menu lists the game resolutions by shape. Checked on 1.2.1 and 1.4.3 at 1280x800 and 3440x1440: HUD, minimap,
inventory, map, system tab and loading screen.
2026-10-30 16:23:58 -03:00
Wellington Dias b7baf43332 Keep the Linux setup's temp files and Mesa's shader cache with the game, and stop the setup with one message on a glibc older than 2.34.
A Steam Deck tester had 1.2 GB free in /home, where the setup put its 1.3 GB of generated C and objects
(~/.cache/totk-setup) and RADV its shader cache (up to 1 GB, about 35 MB a minute at the title screen), while the game
goes to the SD card. The setup's temp is now .setup-temp in the install folder (--temp changes it; the empty-folder
check leaves it out), and totk points MESA_SHADER_CACHE_DIR at mesa-cache beside itself unless it is set. A scan of
every ELF file in the release folder showed the bundled LLVM and the cryptography wheel need glibc 2.34, not the 2.31
the docs gave: make_release_linux.py now checks that floor (check_glibc), the setup's check stops below it
(check.glibc_old) instead of failing on a compiler that cannot start, and the docs give both floors. The Linux notes
also record the testers' results, that the Deck has the host-import extension too, and its software ASTC decoding.
2026-10-30 14:56:54 -03:00
Wellington Dias 48d1a6d3a0 Build the release from either game version's generated C, so a checkout with only 1.4.3 can make it.
make_release.py and make_release_linux.py built the runtime library and the --prove build from generated/v121 alone.
The library reads only what both versions' layout.h share (MOD_COUNT, MOD_MAIN_BASE), so both scripts now take
generated/v121, else generated/v143, pass its --version, and name extract.py and gen.py when neither exists.
2026-10-30 14:56:25 -03:00
Wellington Dias 021413740d Keep the game in focus by default, so switching windows during a load no longer stops it for good.
The game sets focus handling mode 2: on a Switch the system then suspends the whole process while it is out of focus.
Here only its main thread stopped, in PopNotificationMessage, while the loader threads, audio and clocks ran on, and a
tester on Hyprland with 1.4.3 saw a load never finish after the window lost and regained the focus; TOTK_ASSUME_FOCUS=1
avoided it. That is now the default (assume_focus in totk.ini, as MacNativeGames always reports in focus); 0 restores
the pause. Each focus change is logged, so a report shows whether the window saw it.
2026-10-30 14:56:13 -03:00
Wellington Dias ba4eae2123 Make BC 3D textures with block views on GPUs whose driver cannot lay them out as render targets, so agl's BC4 volumes exist on Vega.
RADV on GFX9 returns a 0-byte image for a mipmapped block-compressed 3D image with COLOR_ATTACHMENT usage and no
STORAGE (the usage asks for a thin layout the GFX9 address library has no swizzle mode for), and VMA then fails the
allocation: "image creation failed (-3): nvn fmt 0x49 32x32x32" on a Ryzen 5 4600G. create_tex checks the size the
device wants for such an image and, when it is 0, adds STORAGE usage, which keeps RADV on a thick layout; the replay
never sees that usage. If the device still cannot lay it out, the image is made without block views. bind_pool_memory
refuses a 0-byte image. Forcing the storage path on an RX 7800 XT renders the same frames.
2026-10-30 14:56:03 -03:00
Wellington Dias 7766716e16 Fill a window one pixel wider than the picture, and let the probes use the shader census of either game version.
gfx_fit_rect rounded the picture up to an even size, so at 1167x656 a 16:9 picture was 1166 wide and the last column
stayed black, which failed --probe-vkexec; it now takes the window's parity, which keeps the bars equal at any size.
The present check of --probe-vkexec expects the picture's rectangle and black bars, as --probe-window does, so it
passes on a 16:10 screen too. --probe-shader, the draw part of --probe-vkexec and the pipeline self-test looked only
for build/v121/shaders; probe_census_dir takes the running version's census first, then the other one's. The Linux
notes say what the probes need over SSH.
2026-10-30 14:55:55 -03:00
Wellington Dias 08df794360 Move the aspect words of the patch page to +80..+91 and list the words in use, so other patch sets can take +56..+79.
The shadow cascade range of a pending change loads its four distances from +64..+79 on 1.4.3, where the aspect and the
canvas factors were; with both on, the camera would have read a cascade distance as its aspect. recomp/patches.py now
lists the words each set uses.
2026-10-30 14:35:23 -03:00
Wellington Dias 89b5126da0 Declare the clobbers mbedtls's x86-64 multiply asm lacks, so a clang-built hactool verifies signatures.
hactool's bundled mbedtls 2.6.1 adds into memory and changes the flags in its MULADDC asm without saying so. gcc builds
happen to work; clang, the Linux release's compiler, keeps values in registers across it at -O1 and above, so every RSA
check failed (NCA header, NPDM, ACID) and the AppImage's setup stopped at the update merge. Found by installing from the
mounted AppImage; a bisection of the mbedtls objects pointed at bignum.o, and the memory and cc clobbers (as later
mbedtls releases have them) fix it. With the patch the full install from the AppImage completes and the game runs.
2026-10-30 14:28:35 -03:00
Wellington Dias aa030e4e28 Give 1.4.3 the aspect patch, so it draws 16:10 and 21:9 like 1.2.1.
The 1.4.3 sites, found by their code: the camera parameter reader's DefaultAspect default (main+0x1aa0ac4, the default
at main+0x2d06910), the two camera-viewport updates (0x1ff04f4, 0x253fbb0, where the division becomes the page load
because x9 is free after the dirty-flag store) and two of the 2D canvas-size loads (0x2136fa8, 0x213641c). Three of
1.2.1's five canvas loads have no match yet; switching them off on 1.2.1 left the gameplay HUD and the system menu
unchanged at 21:9. At 16:10 and 21:9 the 1.4.3 title and gameplay show a round emblem, an unstretched world and the
HUD at the screen's edges.
2026-10-29 23:05:30 -03:00
Wellington Dias 119884c52e Bias gather coordinates on non-NVIDIA GPUs, so shadows lose the dashed lines along edges.
AMD and Intel round a gather coordinate that sits between two texels the other way from Maxwell, so shadow-map
filtering picked the neighbouring texel and drew dark dashes along wall and floor edges that moved with the camera
(docs/maxwell.md 2.10; BotW had the same on AMD). vk_caps.c passes the device's subTexelPrecisionBits to the translator
for every non-NVIDIA GPU, the job carries it as gather_bias, and totk_xlate answers QueryHostGatherBiasPrecision with
it, which turns on Ryujinx's own coordinate bias (TexturePass.InsertCoordGatherBias). The answer joins the translator
identity, so those shaders get their own cache keys. TOTK_GATHER_BIAS=0 turns it off.
2026-10-29 22:32:40 -03:00
Wellington Dias f3ab1b0e76 Give the camera and interface the output's shape on 1.2.1, so 16:10 and 21:9 are not stretched.
The aspect patch set (recomp/patches-v121.txt) makes the world camera's DefaultAspect parameter default, the two
camera-viewport updates that divide a 16:9 rectangle, and the five loads of the 2D canvas size read the patch page.
resolution.c writes the output's aspect and the canvas factors there (wider than 16:9 the canvas grows in x, narrower
in y) and switches the set on when the game resolution is not 16:9. The sites are those of Fruithapje21's
TOTK-Ultrawide 1.2.1 patch (GPL-3.0, read for behaviour); a heap scan of the live projections confirmed the four world
cameras take the new aspect. The interface keeps its 16:9 layout in the middle of the canvas: the per-layout edits
that move it to the screen's edges are not done.

A version without the set (1.4.3 for now) draws the largest 16:9 size inside the wanted one, letterboxed. The Steam
Deck profile now draws the screen's own 1280x800, and the menu offers 16:10 and 21:9 game resolutions.
2026-10-29 22:20:59 -03:00
Wellington Dias 2f78585b4c Add a Steam Deck profile and keep the picture's proportions on screens of another shape.
The present fits the game's picture into the largest centred rectangle of its own aspect ratio and clears the rest to
black (gfx_fit_rect, through both the blit and FSR), so a 16:9 game on the Deck's 16:10 screen is no longer stretched;
picture_fit = stretch keeps the old fill. probe-window checks the bars as well as the picture.

device_profile = auto|desktop|deck: on a Steam Deck (the firmware's Valve Jupiter or Galileo board, or SteamDeck=1) the
keys totk.ini does not set default to fullscreen, a 1280x800 window, a 1280x720 game resolution, the game's own scene
scale, vsync and a 40 fps limit; the environment and totk.ini still win. These are starting points until measured on
the device (docs/tester-guide.md, Steam Deck).

Both stick clicks (L3 + R3) step the performance overlay as F10 does, for a controller with no keyboard. The menu offers
1280x720 and 1600x900 game resolutions and 40, 45 and 90 fps limits.
2026-10-29 18:14:30 -03:00
Wellington Dias 29a14d69fe Build the setup for Linux as an AppImage.
tools/make_release_linux.py assembles the Linux release folder and packs it with the static type-2 AppImage runtime:
python-build-standalone 3.14 with tkinter, LLVM 23.1.4 (clang, lld, llvm-ar) and the part of the Steam Runtime 3 sniper
SDK sysroot a game build reads, so the game, hostcheck, hactool and the translator need no newer glibc than 2.31 and run
on SteamOS. The game it builds needs only glibc, the system's Vulkan loader and the libSDL3.so.0 beside it; Opus, libvpx,
zstd and libstdc++ are linked in. --prove builds generated/v121 with the folder alone and runs the probes.

The installer runs on Linux: release and dev layouts, temp under the XDG cache (the AppImage mount is read-only), cancel
through the CLI's process group, language from LANG, xdg-open and a PNG icon, texts that name the right executable, and a
warning on kernels older than 6.7. hostcheck reads x86-64-v3 from CPUID, since clang's __builtin_cpu_supports reads it
from libgcc, which before GCC 12 has no level bits. build_game.py takes --sysroot, build_hactool.py --cc and
build_xlate.py --llvm/--sysroot; linux.c carries the Linux 6.7 write-watch definitions for older headers.

Also: shader_census.py and package_build.py use the Linux executable names, and probe-shader writes its shader path with
forward slashes.
2026-10-29 18:00:26 -03:00
Wellington Dias 5a84b19476 Give CPU-visible pools shadow storage on Linux, so the game renders and plays.
On Linux, write-protecting memory the GPU imported costs milliseconds per submit and the import's pinning clears the
write watch, so each CPU-visible pool gets a host-visible copy instead: the pages the CPU wrote since the last sync go in
right before each submit (skipping bytes the GPU still has to write back), and the replay's GPU writes are copied back
into guest memory at their fence (vk_gpu_written). In 1.2.1 play it costs about 0.2 ms a frame. TOTK_SHADOW=1 turns it
on under Windows too.

Also: build_game.py fetches the VMA header the Windows release pins when the system has none; plat_localtime and
plat_gmtime reject the times Windows rejects; the SDL text entry honours TOTK_SWKBD_TEST=cancel; probe-vkmem follows
the replay's protocol for GPU writes and syncs.
2026-10-29 17:29:31 -03:00
Wellington Dias 1d51343ac1 Build and run the runtime natively on Linux.
Shared runtime code now goes through runtime/platform/platform.h, implemented by platform/win32.c and a new
platform/linux.c: futex locks, a userfaultfd and PAGEMAP_SCAN write watch, signal-based async preemption, our own
fiber switch, and a SIGSEGV handler for the thaw and crash hooks. The window, input, menu and software keyboard get
SDL3 back ends beside the Win32 ones, with the swapchain (gfx_present.c) and input merging (input.c) shared. SHA-256
replaces BCrypt, ELF builds get their own override sections, and the build tools gain Linux branches.

On Linux the selftest and the gfx-abi, astc, nvn and window probes pass. probe-vkmem still fails: imported guest
memory cannot be write-watched on Linux, which the shadow buffers in vk_memory.c will address.
2026-10-29 16:56:56 -03:00
153 changed files with 11929 additions and 4964 deletions

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
name: Crash
description: The game closes, freezes or shows a FATAL message.
labels: [bug, crash]
body:
- type: markdown
attributes:
value: |
Check the pinned issues at the top of the issue list first: they cover the known crashes and their workarounds.
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this crash.
required: true
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
validations:
required: true
- type: input
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
validations:
required: true
- type: dropdown
id: when
attributes:
label: When does it crash?
options:
- At startup, before the Nintendo logo
- Between the Nintendo logo and the title screen
- While loading a save
- In gameplay
- In a cutscene
validations:
required: true
- type: dropdown
id: blocker
attributes:
label: Can you keep playing?
options:
- "Yes, it does not happen again after a restart, or I can avoid it"
- "No, it happens every time at the same point"
validations:
required: true
- type: textarea
id: steps
attributes:
label: What you did before the crash
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe. Copy it before you start the game again, because the next start overwrites it.
validations:
required: true
+59
View File
@@ -0,0 +1,59 @@
name: Graphics glitch
description: Artifacts, missing or broken textures, flicker, wrong colours or shadows.
labels: [bug, graphics]
body:
- type: markdown
attributes:
value: |
Check the pinned issues at the top of the issue list first: they cover the known glitches on each GPU.
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this glitch.
required: true
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
validations:
required: true
- type: input
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: Screenshot or video
description: Drag the file into this field. Say where in the game it was taken.
validations:
required: true
- type: textarea
id: details
attributes:
label: What looks wrong and when
description: Does it show all the time, in one place, or after some time in play? Which settings did you change from the defaults?
validations:
required: true
- type: dropdown
id: blocker
attributes:
label: Can you keep playing?
options:
- "Yes"
- "No, I cannot see enough to play"
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe.
+65
View File
@@ -0,0 +1,65 @@
name: Other bug
description: Sound, controls, performance, the settings menu, the installer, or game logic.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Check the pinned issues at the top of the issue list first.
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this bug.
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Audio
- Controls and input
- Performance and stutter
- Settings menu and overlay
- Installer
- Game logic and physics
- Something else
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
validations:
required: true
- type: input
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
validations:
required: true
- type: dropdown
id: blocker
attributes:
label: Can you keep playing?
options:
- "Yes"
- "No, it stops the game or a quest"
validations:
required: true
- type: textarea
id: steps
attributes:
label: What happened and how to reproduce it
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe.
+17
View File
@@ -0,0 +1,17 @@
name: Feature request
description: A setting, option or support for something the port does not do yet.
labels: [enhancement]
body:
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) and the Planned section of the README for this request.
required: true
- type: textarea
id: request
attributes:
label: What you want and why
validations:
required: true
-32
View File
@@ -1,32 +0,0 @@
name: Bug report
description: Something breaks, crashes or looks wrong.
body:
- type: markdown
attributes:
value: |
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
validations:
required: true
- type: input
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
validations:
required: true
- type: textarea
id: steps
attributes:
label: What happened and how to reproduce it
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe. After a crash, copy it before you start the game again.
+4
View File
@@ -1 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question or help with setup
url: https://github.com/valentderah/totk-pc-recomp/discussions
about: Installing, updating and configuring the port. Ask in Discussions.
+3
View File
@@ -42,6 +42,9 @@ __pycache__/
*.o
*.lib
*.a
*.so
*.so.*
*.AppImage
# .NET build output of the shader translator (tools/build_xlate.py)
/tools/xlate/bin/
+1 -1
View File
@@ -19,6 +19,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- Game updates 1.2.1 and 1.4.3, with saves that carry over between them
- 60 fps by default; up to 144 fps or the monitor's refresh rate (experimental)
- Custom resolutions (1080p, 2k...), supersampling and AMD FSR 1 upscaling
- 16:10 (Steam Deck, handhelds), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
- In-game settings menu (F1) and performance overlay (F10)
- Keyboard, mouse and gamepad with remapping, rumble and gyro
- Xbox button prompts: a layout that keeps the Switch button positions and shows the Xbox labels
@@ -28,7 +29,6 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- Linux and the Steam Deck
- Mods made for emulators: unpack a mod into a `mods` folder
- Ultrawide on 1.2.1 (1.4.3 has it for 2560x1080, 3440x1440, 3840x1600 and 5120x2160; other shapes get the wide camera with a 16:9 HUD)
- VRR
## Installing
+24 -1
View File
@@ -64,6 +64,8 @@ These tools run on the developer's or player's machine during the build. They ar
| Component | Source | License | Where in the release |
|---|---|---|---|
| Tcl/Tk 9.0.4, rebuilt with Xft for the Linux installer | https://github.com/tcltk/tcl and https://github.com/tcltk/tk, tag `core-9-0-4`; archives pinned by SHA-256 in `tools/make_release_linux.py` | Tcl/Tk licence (BSD-style) | `python/lib/`; licence texts in `licenses/tcl-license.terms` and `licenses/tk-license.terms` |
| libXft 2 and libXrender 1, the sniper SDK sysroot's shared libraries, bundled beside the Xft Tk of the Linux installer; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | HPND (MIT-style) | `python/lib/`; `licenses/libXft-copyright.txt`, `licenses/libXrender-copyright.txt` |
| CPython 3.14.9 embeddable package, with `tkinter` and Tcl/Tk 9.0.4 taken from the python.org 3.14.9 installer's component MSI (unpacked, never run); includes `vcruntime140*.dll` (Microsoft, redistributable with Python), OpenSSL, libffi, SQLite, zlib, libtommath | https://www.python.org/downloads/release/python-3149/ | PSF-2.0 (Tcl/Tk: Tcl/Tk licence, BSD-style) | `python/` |
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
| llvm-mingw 20261020 (LLVM/Clang/LLD 23, mingw-w64, libc++, libunwind, compiler-rt), x86_64 subset | https://github.com/mstorsjo/llvm-mingw/releases/tag/20261020 | Apache-2.0 with LLVM exception; mingw-w64 ZPL-2.1/BSD/public domain; libc++ and libunwind Apache-2.0 with LLVM exception and MIT/UIUC; winpthreads MIT | `llvm/` |
@@ -74,6 +76,27 @@ These tools run on the developer's or player's machine during the build. They ar
| Vulkan-Headers and Vulkan-Loader 1.4.363.0 (MSYS2 packages; headers and `libvulkan-1.dll.a` only, no loader DLL) | https://github.com/KhronosGroup | Apache-2.0 OR MIT | `runtime/deps/` |
| Vulkan Memory Allocator 3.4.0 (MSYS2 package), header | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator | MIT | `runtime/deps/` |
| Ninja 1.13.2 (`ninja-win.zip`) | https://github.com/ninja-build/ninja/releases/tag/v1.13.2 | Apache-2.0 | `bin/ninja.exe` |
| hactool (SciresM @ `1d64a83450e025622f3468c28fc4164dad2c5ef6` plus `tools/hactool/portable.patch`, from MacNativeGames' `build_hactool()`: `MNG_TITLE_KEYS` and relaxed BKTR layout checks; our `tools/hactool/unicode.patch`: an embedded manifest that makes UTF-8 the process code page), built by `tools/build_hactool.py` with MSYS2 UCRT64 gcc; imports only KERNEL32 and the UCRT | https://github.com/SciresM/hactool | ISC | `bin/hactool.exe` |
| hactool (SciresM @ `1d64a83450e025622f3468c28fc4164dad2c5ef6` plus `tools/hactool/portable.patch`, from MacNativeGames' `build_hactool()`: `MNG_TITLE_KEYS` and relaxed BKTR layout checks; our `tools/hactool/unicode.patch`: an embedded manifest that makes UTF-8 the process code page; our `tools/hactool/mbedtls-asm-clobbers.patch`: the clobbers the bundled mbedtls's x86-64 multiply asm lacks), built by `tools/build_hactool.py` with MSYS2 UCRT64 gcc; imports only KERNEL32 and the UCRT | https://github.com/SciresM/hactool | ISC | `bin/hactool.exe` |
| `totk_xlate.exe` (Ryujinx shader translator, NativeAOT build of `tools/xlate/`, see the Ryujinx row above) | built by `tools/build_xlate.py` | MIT | `bin/totk_xlate.exe` |
| MacNativeGames shader variant list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/assets/shader_list.tsv`), Copyright (c) 2026 MacNativeGames contributors | MIT | `data/shader_list.tsv` |
## Distributed in the Linux release (AppImage)
`tools/make_release_linux.py` assembles these into the folder of `docs/installer.md` ("Linux release") and packs it as
`TotK-Setup-x86_64.AppImage`. Versions and sha256 of every download are pinned in that script; licence texts are in the
folder's `licenses/` and in each component's own folder.
| Component | Source | License | Where in the release |
|---|---|---|---|
| CPython 3.14.10, python-build-standalone 20261026 `install_only` build, with `tkinter` and the libraries it bundles (OpenSSL, libffi, SQLite, zlib, Tcl/Tk and others; their licences ship in the folder) | https://github.com/astral-sh/python-build-standalone/releases/tag/20261026 | PSF-2.0; bundled libraries under their own licences | `python/` |
| numpy 2.5.4 (with the libraries its manylinux wheel bundles), lz4 4.4.5, cryptography 50.0.2, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | as in the installer release row above | `python/lib/python3.14/site-packages/` |
| LLVM 23.1.4 official Linux X64 release: `clang`, `lld`, `llvm-ar`, `llvm-ranlib`, `llvm-objcopy`, `llvm-strip` and the clang resource headers; `llvm-LICENSE.txt` from the `llvmorg-23.1.4` source tag | https://github.com/llvm/llvm-project/releases/tag/llvmorg-23.1.4 | Apache-2.0 with LLVM exception | `llvm/` |
| Steam Runtime 3 "sniper" SDK sysroot 3.0.20261021.266025 (Debian 11 based), only the headers, libraries and objects a game build reads: glibc 2.31 (headers, `crt*.o`, `libc_nonshared.a`, link libraries), GCC 10 `libstdc++.a` and `libgcc`, SDL 3.4.18, libopus, libvpx 1.9 (static), the Vulkan loader's link library; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | glibc LGPL-2.1+; libstdc++ and libgcc GPL-3.0 with the GCC Runtime Library Exception; SDL zlib; libopus BSD-3-Clause; libvpx BSD-3-Clause with the WebM patent grant (`libvpx-PATENTS.txt` from the v1.17.0 source tag); Vulkan loader Apache-2.0 | `llvm/sysroot/`, `bin/libSDL3.so.0` (the sysroot's, copied next to the game by the setup), `licenses/` |
| Vulkan-Headers 1.4.363 (source tag) | https://github.com/KhronosGroup/Vulkan-Headers/tree/v1.4.363 | Apache-2.0 OR MIT | `runtime/deps/include/` |
| Vulkan Memory Allocator 3.4.0, header (the licence text is its header comment) | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/tree/v3.4.0 | MIT | `runtime/deps/include/vma/` |
| Zstandard 1.5.7, built from the release tarball into a static `libzstd.a` with the folder's clang and sysroot | https://github.com/facebook/zstd/releases/tag/v1.5.7 | BSD-3-Clause | `runtime/deps/` |
| Ninja 1.13.2 (`ninja-linux.zip`) | https://github.com/ninja-build/ninja/releases/tag/v1.13.2 | Apache-2.0 | `bin/ninja` |
| hactool (as in the installer release row above), built by `tools/build_hactool.py --cc` with the folder's clang and sysroot; includes its bundled mbedtls | https://github.com/SciresM/hactool | ISC; mbedtls Apache-2.0 | `bin/hactool` |
| `totk_xlate` (NativeAOT linux-x64 build of `tools/xlate/`, linked with the folder's clang, lld and sysroot) | built by `tools/build_xlate.py --llvm --sysroot` | MIT | `bin/totk_xlate` |
| AppImage type-2 runtime (static; includes libfuse and squashfuse), the AppImage's first part; appimagetool packs the folder and is not distributed | https://github.com/AppImage/type2-runtime | MIT; libfuse LGPL-2.1; squashfuse BSD-2-Clause | the AppImage header |
| MacNativeGames shader variant list | as in the installer release row above | MIT | `data/shader_list.tsv` |
+1 -1
View File
@@ -1 +1 @@
0.1.9
0.2.8
+5 -4
View File
@@ -94,11 +94,12 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
(MacNativeGames' sites); `resolution.c` sets them from `output_res` and `render_scale` and grows the texture heap
and the application pool to match. When the window is larger than the output, `vk_fsr.c` runs AMD FSR 1 (EASU,
RCAS) on the present. An `output_res` that is not 16:9 (more than 1% off, `res_is_16x9`) also switches on the
`aspect` patch set (1.4.3 only), which hands the output's ratio to the camera, and `romfs.c` `overlay_init` loads the
`aspect` patch set, which hands the output's ratio to the camera, and `romfs.c` `overlay_init` loads the
`romfs-patches-<w>x<h>` overlay for it when it exists (`tools/make_romfs_patches.py --aspect`, laid out by
`tools/ultrawide_layout.py`; the installer builds four sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160). Other
shapes, such as 16:10 or 32:9, get the wide camera and the 16:9 HUD stretched over it. 1.2.1 has no camera patch: a
wide size there keeps the 16:9 view. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
`tools/ultrawide_layout.py`; the installer builds ten sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160, 32:9
3840x1080, 5120x1440 and 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600). A wider layout moves the HUD to the
screen's corners, a narrower one to its top and bottom edges. Other sizes get the camera of their shape and the 16:9
HUD stretched over it. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
when the window's shape differs from the output's: `fit` letterboxes, where a 16:9 game in a wider window used to be
stretched.
- Occlusion queries never block the CPU; a result that is not ready counts as visible.
+63 -6
View File
@@ -37,20 +37,23 @@ PATH:
### hactool
`tools/build_hactool.py` fetches SciresM/hactool at `1d64a83` into `toolchain\src\hactool`, applies the two patches in
`tools/build_hactool.py` fetches SciresM/hactool at `1d64a83` into `toolchain\src\hactool`, applies the three patches in
`tools/hactool/` and builds a static `toolchain\hactool\hactool.exe` with MSYS2 UCRT64 gcc (a few seconds):
- `portable.patch`: the parts of MacNativeGames' `build_hactool()` that run on Windows, the `MNG_TITLE_KEYS` variable and
the two BKTR fixes in `nca.c` for the subsection table padding and the subsection layout of newer cumulative updates;
- `unicode.patch`: a manifest with `<activeCodePage>UTF-8</activeCodePage>`, so paths under a non-ASCII user profile
reach it intact.
reach it intact;
- `mbedtls-asm-clobbers.patch`: the `memory` and `cc` clobbers the bundled mbedtls 2.6.1's x86-64 multiply asm lacks.
Without them a clang build (the Linux release) fails every RSA signature check and the update merge stops.
hactool reads `-k <prod.keys>`; MSYS2 shells set `HOME`, which hides its default key location, so pass `-k` there.
## Build
From the repository root, in PowerShell. Every step that depends on the game version takes it as `<ver>`: `v143` for
1.4.3 or `v121` for 1.2.1. The tools default to `v121` when `--version` is left out. Saves load on either version.
1.4.3, `v121` for 1.2.1 or `v111` for 1.1.1. The tools default to `v121` when `--version` is left out. Saves load on any
of them.
1. Python environments (once). `.venv` runs the lifter and the build; `.venv-py314` runs the extraction scripts, which
need the stdlib `compression.zstd` of Python 3.14:
@@ -78,7 +81,8 @@ From the repository root, in PowerShell. Every step that depends on the game ver
.venv\Scripts\python tools\build_xlate.py
```
5. Lift the modules to C, write the RomFS half of the frame-rate patches, then compile and link `build\<ver>\totk.exe`:
5. Lift the modules to C, write the RomFS overlay (the frame-rate fix), then compile and link
`build\<ver>\totk.exe`:
```powershell
.venv\Scripts\python recomp\gen.py --exefs build\extracted\<ver>\exefs --out generated\<ver> --patches recomp\patches-<ver>.txt
@@ -87,6 +91,14 @@ From the repository root, in PowerShell. Every step that depends on the game ver
```
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
writes it into the ninja file ahead of `cc`, and every lift rewrites the generated chunks with the same contents, so
a second build of the same version hits all of their objects:
```powershell
$env:SCCACHE = "path\to\sccache.exe"
.venv\Scripts\python tools\build_game.py --version <ver>
```
`--patches` builds in the frame-rate, resolution and other patch sets, switchable at run time. The link copies
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
For Xbox button prompts, run `make_romfs_patches.py` once more with `--xbox-prompts`: it writes
@@ -104,7 +116,8 @@ From the repository root, in PowerShell. Every step that depends on the game ver
The AMD driver keeps its pipeline cache per exe path and holds a limited amount: preparing another build folder can
evict this one's. `totk.exe --warm-shaders` rebuilds it from the shader cache.
7. Run. Keep the window in the foreground; an unfocused window stalls the game.
7. Run. The game keeps running when its window loses the focus (`assume_focus = 1` in `totk.ini`, the default; the
Game tab switches it). With 0 it pauses, as it does on the Switch when Home is pressed.
```powershell
build\<ver>\totk.exe
@@ -120,6 +133,48 @@ your machines without MSYS2, Python or .NET. `romfs.bin` stays out unless you pa
`romfs\` next to the exe or point `TOTK_ROMFS` at its folder. Run `totk.exe --prepare-shaders` once in the copied
folder. The folder holds code and data derived from your game: for your own machines only.
## Linux (in progress)
The runtime builds and runs on x86-64 Linux with a window, input and menu on SDL3 (`*_sdl.c`) in place of the Win32
files (`*_win.c`); `runtime/platform/linux.c` implements `platform.h`. Status: the selftest and the probes pass; 1.2.1
plays at 60 fps on RADV (RX 7800 XT) and NVIDIA (RTX 3060); 1.4.3 builds, probes and plays on RADV (RX 6600, and the
Vega 7 of a Ryzen 5 4600G at about 34 fps). Long play sessions and the Steam Deck itself are not verified yet.
Requirements:
- Linux 6.7 or later: the write watch uses userfaultfd write protection with `PAGEMAP_SCAN`. SteamOS 3.8 qualifies.
- A Vulkan 1.3 driver with `VK_EXT_external_memory_host` (RADV, NVIDIA).
- Packages on Arch:
```sh
pacman -S --needed clang lld ninja make sdl3 opus libvpx zstd vulkan-headers vulkan-icd-loader \
vulkan-validation-layers shaderc spirv-tools dotnet-sdk uv git
```
Vulkan Memory Allocator is a single header (AUR `vulkan-memory-allocator`). Without it, `build_game.py` fetches the
version the Windows release pins (3.4.0) into `toolchain/vma/` on first use and checks its hash.
The steps are the Windows ones with POSIX paths: `.venv/bin/python` in place of `.venv\Scripts\python`, and `totk` in
place of `totk.exe`. `build_game.py` takes clang, lld and Ninja from PATH (or the ones in `<dir>/bin` with
`--toolchain <dir>`) and links against the system libraries, with `libstdc++` static and an `$ORIGIN` rpath;
`build_hactool.py` builds `hactool` with make. `tools/prepare_shaders.py` reads the shader list from
`refs/MacNativeGames` at `origin/HEAD`, which must be the pinned commit with its history (the list format changed later).
The probes open a window, so from an SSH session they need the desktop session's display, or they stop at `gpu 0
skipped: no graphics+compute+present queue family`: on Wayland `XDG_RUNTIME_DIR=/run/user/$(id -u)
WAYLAND_DISPLAY=wayland-1 SDL_VIDEODRIVER=wayland` (the socket name is in `$XDG_RUNTIME_DIR`), on X11 `DISPLAY=:0`.
Memory: on Windows the GPU imports guest memory and `GetWriteWatch` tracks the CPU's writes to it. On Linux,
re-protecting imported memory invalidates the driver's userptr buffer on every submit (about 1.3 ms per 16 MiB on
amdgpu), and importing pins the pages, which drops their write protection. Each CPU-visible pool therefore gets a
host-visible copy (shadow storage, `runtime/vk_memory.c`): the pages the CPU wrote go in before each submit, and the GPU's
writes come back at its fence. In play that costs about 0.2 ms a frame and doubles the memory of those pools (about
1.1 GB). `TOTK_SHADOW=1` turns it on under Windows too. The Steam Deck's RADV has `VK_EXT_external_memory_host` as
well: the shadow copies are chosen for the measured cost above, not for a missing extension.
The Deck's GPU (Van Gogh) has no native ASTC, so its textures go through the compute decoder (`runtime/vk_astc.c`,
`TOTK_ASTC`) as on desktop AMD GPUs without it; frame times measured there include it.
## Checks
Before a commit:
@@ -142,7 +197,9 @@ change passes these, all run with `.venv`:
| `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes |
Runtime probes, each a mode of `totk.exe` (the list is at the top of `runtime/main.c`): `--probe-kernel`,
`--probe-vkexec`, `--probe-nvn-tex` and `--probe-shader` cover most runtime changes.
`--probe-vkexec`, `--probe-nvn-tex` and `--probe-shader` cover most runtime changes. `--probe-shader` and the draw
part of `--probe-vkexec` translate shaders of the census below: they look for `build/<ver>/shaders/jobs.json` of the
exe's own version first, then of the other one.
Shaders: `.venv-py314\Scripts\python tools\shader_census.py --version <ver> --translate` extracts every Maxwell shader
to `build\<ver>\shaders\` and runs it through the translator and `spirv-val`; failure groups go to `failures.txt`.
+7 -1
View File
@@ -49,7 +49,7 @@ Without the folder (an install made by an older setup, or one where the setup co
| Key | Gamepad | Action |
|---|---|---|
| F1 or Esc | Minus and Plus together | open or close the settings menu; the game gets no input while it is open |
| F10 | | performance overlay: off, frame rate, full panel |
| F10 | L3 + R3 (both stick clicks) | performance overlay: off, frame rate, full panel |
| F11 | | borderless fullscreen on or off, saved to `totk.ini` |
The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-pad or the left stick move, Enter or A selects, Q and E or L and R switch tabs, Esc or B goes back (Switch button names, as in the table above).
@@ -86,6 +86,12 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
## Nintendo Switch 2 controllers
Windows does not present the Switch 2 Pro Controller as a gamepad SDL can open (the log says `input: no gamepad`), so it needs a bridge program that wakes it and creates a virtual gamepad. The DS4Windows fork by hbashton ([github.com/hbashton/DS4Windows](https://github.com/hbashton/DS4Windows), tested with release VIIPERRC4.5.1, a pre-release) with the output set to an emulated DualSense gives buttons, rumble, gyro and accelerometer, and reads the sticks through the controller's calibration (they rest at 0 to 2%). The game reads its virtual DualSense directly; the log then shows `input: gamepad "DualSense Wireless Controller" (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Its release notes also list Joy-Con 2 support, which is untested here.
Bridges that output a ViGEm DualShock 4 (ProCon2Windows, connectyourpro as tested) give buttons and rumble but no motion: their virtual DualShock 4 reports a gyro and an accelerometer that never change, so motion aiming and Ultrahand rotation do nothing. Run one bridge at a time; Steam Input can also claim the controller, so close Steam if the bridge does not find it.
## Scripted input
`TOTK_INPUT_SCRIPT=<file>` replays presses. It is ORed with real input and still applies under `TOTK_INPUT=none`. One event per line, `#` starts a comment:
+41 -3
View File
@@ -34,7 +34,7 @@ The player runs a newer installer on the same folder. It reads `totk-install.jso
`profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the shaders. A newer
release that changes nothing game-derived skips the rebuild.
Given another update file than the recorded one (1.2.1, then 1.4.3), the installer extracts and builds again from it and
Given an update file other than the recorded one (1.2.1, then 1.4.3), the installer extracts and builds again from it and
keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on load, and 1.2.1 loads the saves 1.4.3 wrote.
## Setup steps
@@ -109,9 +109,47 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
finishes.
## Linux release
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `TotK-Setup-x86_64.AppImage` (the static
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
Desktop Mode. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
`libSDL3.so.0` beside it. The folder inside the AppImage:
```
AppRun starts python/bin/python3 -m installer.gui (no bytecode written into the read-only mount)
totk-setup.desktop, totk-setup.png
installer/, recomp/, tools/, data/, licenses/, THIRD_PARTY_NOTICES.md, totk.ini.example, VERSION as on Windows
python/ python-build-standalone 3.14 (with tkinter) + the installer's packages; a .pth puts the folder,
tools/ and recomp/ on sys.path
llvm/ LLVM 23 (clang, lld, llvm-ar) and llvm/sysroot: the files of the Steam Runtime 3 "sniper" SDK the
game's compile and link read (about 130 files)
runtime/ libtotkrt.a, headers, the install-side sources, totk.png; deps/ with the Vulkan headers, VMA and a
static zstd
bin/ totk_xlate, hactool, ninja, libSDL3.so.0
```
- What we compile is built against the sniper sysroot (glibc 2.31): the game, hostcheck, hactool and the translator
need no newer glibc. The game links Opus, libvpx, zstd and libstdc++ statically and needs only glibc, the system's
Vulkan loader and the SDL3 beside it. The setup itself needs glibc 2.34 (Ubuntu 22.04, Debian 12, Fedora 35,
SteamOS 3): the official LLVM binaries and the `cryptography` wheel are built against it. `make_release_linux.py`
checks every ELF file in the folder against that floor (`check_glibc`), and the setup's check step stops with one
message on an older system.
- The setup's temp folder is `.setup-temp` inside the install folder (`--temp` changes it): the AppImage is mounted
read-only, and a Steam Deck's `/home` is often too full for the generated C while the game goes to the SD card.
- The game keeps Mesa's shader cache in `mesa-cache` beside `totk` unless `MESA_SHADER_CACHE_DIR` is set, for the
same reason: RADV fills its default folder in `~/.cache` up to 1 GB.
- The window follows `LANG` for English or Russian, cancels through SIGTERM to the CLI's process group (the CLI handles
it as Ctrl+Break on Windows) and opens the finished folder with `xdg-open`.
- Requirements beyond Windows': Linux 6.7 or later (the write watch, `docs/building.md`) and glibc 2.34.
- Building the release needs one game version extracted and lifted (`tools/extract.py`, `recomp/gen.py`, either
version): the runtime library includes the generated `layout.h`, whose shared part (`MOD_COUNT`, `MOD_MAIN_BASE`)
is the same for both. `tools/make_release.py` takes `generated/v121`, else `generated/v143`.
## Constraints
- The installer steps contain no Win32 calls: Linux follows with its own compiler and the same steps.
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
@@ -127,4 +165,4 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
## Later
- Re-warm the driver cache on start after a move, GPU or driver change.
- NSZ and XCZ input. Linux release.
- NSZ and XCZ input.
+2 -1
View File
@@ -66,7 +66,7 @@ Source shorthands:
| 2.7 | **Swizzles** per texture and per view (`TextureBuilderSetSwizzle`, `TextureViewSetSwizzle`); BotW layout captures rebuild a texture on the same storage with (1, 1, 1, R) or alpha 1. Default per-format mapping R8 → (R, 0, 0, 1). | `VkComponentMapping` on the view. | NXN: font atlas white boxes, HUD dark boxes without it. Ours: mapped (`vk_exec_draw.c` `get_view`). | NXN |
| 2.8 | **Sample dimension comes from the instruction**: a non-array sample of an array texture reads layer 0; 1,512 BotW shaders sample a cube as a cube array. | View type must match the declared type. | NXN: 2D array bound as 2D gave one pale colour (washed-out scene); cubes created as 6-layer arrays with cube / cube-array views. Ours: views per resource type (`get_view`). | NXN |
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | yuzu: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). | — |
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); TotK on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `TOTK_GATHER_BIAS=0` turns it off. | — |
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. yuzu offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `TOTK_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
@@ -141,6 +141,7 @@ Source shorthands:
| TotK | AO broken above 1008p mods | rescale thresholds | YZ-2306 PR 10675 |
| TotK | "detective mode" Link on AMD | AMD EDS3 colour-blend driver regression | YZ-2306 PR 10946 |
| BotW | shadow artifacts at object edges (AMD) | gather rounding | yuzu commit 780240e (2.10) |
| TotK | dashed dark lines along wall and floor edges, changing with the camera angle (AMD, RADV) | gather rounding | gather bias (2.10) |
| BotW | black textures on GCN | HTILE (depth compression) | `AMD_DEBUG=nohyperz` wiki (1.9) |
| BotW | 3D scene black | deferred texture handles | NXN |
| BotW | albedo scattered squares | duplicate colour attachments | NXN |
+22 -1
View File
@@ -38,7 +38,7 @@ build\<ver>\totk.exe
`TOTK_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
do one run with it and one without (`set TOTK_VK_VALIDATION=`).
Keep the window in the foreground: an unfocused window stalls the game. Shaders the preparation did not cover compile on first use, so
The game keeps running behind other windows (`assume_focus = 0` in `totk.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
the first minutes of play can stutter.
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
@@ -63,6 +63,27 @@ The line `vk: using gpu N: <name>` says which one ran.
5. For a bug you can trigger again: a recording of your input (`set TOTK_INPUT_RECORD=input.txt`, see
[controls.md](controls.md), "Scripted input") and the save you started from.
## Steam Deck
Build or install on Linux ([installer.md](installer.md), "Linux release"), in Desktop Mode. The game then runs in either
mode; for Game Mode, add `totk` from the install folder to Steam as a non-Steam game.
The game recognises the Deck (LCD and OLED) and uses its device profile for every setting `totk.ini` does not set:
fullscreen, the screen's own 1280x800 (16:10: the camera and interface take that shape), the game's own 3D scale,
vsync and a 40 fps limit. These are starting points, not measurements. What helps most:
1. The performance overlay (F10, or both stick clicks L3 + R3) in full mode: the frame rate, mean and longest frame
time over a minute in Lookout Landing, in the Depths and in the Sky, with the settings named.
2. The same with `fps_limit` 30, 45 and 60 and `render_scale` 50, 67 and game, so the defaults can be picked from
measurements. Steam's own performance overlay (the ... button) adds power draw and temperatures.
3. LCD or OLED, SteamOS version (`uname -r` in Desktop Mode's terminal), and the game update.
4. Anything that looks stretched, squeezed or cut off, with a screenshot. At 16:10 the menus keep their 16:9
proportions in the middle of the taller screen, the HUD sits at the top and bottom edges, and full-screen
backgrounds reach them; pre-rendered cutscenes play stretched.
`device_profile = desktop` in `totk.ini` turns the profile off; `deck` turns it on elsewhere, to try the Deck's
settings on a PC.
## Switches for the developer's questions
Only set these when asked; each forces a fallback the build picks by itself on GPUs that need it.
+10 -4
View File
@@ -28,6 +28,11 @@ class Parser(argparse.ArgumentParser):
sys.exit(2)
# The temp folder inside the install folder when the layout names none (the Linux release); the check step does not
# count it when it requires an empty install folder
TEMP_IN_DEST = '.setup-temp'
def temp_folder(base, dest):
"""One temp folder per install folder, so two installs never share generated files."""
tag = hashlib.sha256(str(dest).lower().encode()).hexdigest()[:8]
@@ -41,13 +46,13 @@ def cancel(signum, frame):
def main(argv=None):
ap = Parser(prog='python -m installer', description=__doc__.splitlines()[0])
ap.add_argument('--base', type=Path, help='base game, XCI or NSP')
ap.add_argument('--update', type=Path, help='1.2.1 or 1.4.3 update NSP')
ap.add_argument('--update', type=Path, help='the update NSP: 1.1.1, 1.2.1 or 1.4.3')
ap.add_argument('--keys', type=Path, help='prod.keys')
ap.add_argument('--lang', help='game language, a Switch language code (en-US, en-GB, ja, de, ru, ...)')
ap.add_argument('--dest', type=Path, required=True, help='install folder')
ap.add_argument('--root', type=Path, help='release folder or repository (default: the folder above installer/)')
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release, build/install-temp in a '
'repository)')
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release on Windows, .setup-temp in '
'the install folder on Linux, build/install-temp in a repository)')
ap.add_argument('--verify', action='store_true', help='hash every kept file, also those this release made')
args = ap.parse_args(argv)
@@ -58,7 +63,8 @@ def main(argv=None):
signal.signal(getattr(signal, name), cancel)
layout = layouts.find(args.root)
dest = args.dest.resolve()
temp = temp_folder((args.temp or layout.temp).resolve(), dest)
base = args.temp or layout.temp
temp = temp_folder(base.resolve(), dest) if base else dest / TEMP_IN_DEST
log = temp / 'logs' / time.strftime('install-%Y%m%d-%H%M%S.log')
rep = Reporter(log_path=log)
t0 = time.monotonic()
+5 -1
View File
@@ -15,6 +15,7 @@ Text is never the payload: ids name a step ("step.<id>"), a phase ("phase.<step>
and installer/text/en.json holds their English text with {param} placeholders. The log renders events with it.
"""
import json
import os
import sys
import threading
import time
@@ -28,11 +29,14 @@ def load_catalog(path=CATALOG):
return json.loads(path.read_text(encoding='utf-8'))
GAME_EXE = 'totk.exe' if os.name == 'nt' else 'totk' # {game_exe} in the catalogs
def render(catalog, id, params=None):
"""The English text of id, or the id itself when the catalog lacks it."""
text = catalog.get(id, id)
try:
return text.format(**(params or {}))
return text.format(**{'game_exe': GAME_EXE, **(params or {})})
except (KeyError, IndexError, ValueError):
return f'{text} {params}'
+16 -5
View File
@@ -10,6 +10,7 @@ this file only draws. The window text is in installer/text/<en|ru>.json under "u
import argparse
import json
import os
import subprocess
import sys
import tkinter as tk
from pathlib import Path
@@ -58,11 +59,17 @@ class App:
root.title(self.cat('ui.title'))
icon = layout.runtime / 'totk.ico'
png = layout.runtime / 'totk.png' # Tk loads no .ico off Windows; the Linux release converts it
if os.name == 'nt' and icon.is_file():
root.iconbitmap(default=str(icon))
elif png.is_file():
self.icon = tk.PhotoImage(file=str(png))
root.iconphoto(True, self.icon)
root.protocol('WM_DELETE_WINDOW', self.on_close)
self.ui_font = 'Segoe UI' if os.name == 'nt' else 'DejaVu Sans'
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
style = ttk.Style(root)
style.configure('Title.TLabel', font=('Segoe UI', 15, 'bold'))
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
style.configure('Err.TLabel', foreground='#c01c28')
style.configure('Version.TLabel', foreground='#808080')
@@ -214,7 +221,7 @@ class App:
self.cancel_button.pack(side='right')
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
self.log_toggle.pack(side='left')
self.run_header = ttk.Label(f, font=('Segoe UI', 11, 'bold'))
self.run_header = ttk.Label(f, font=(self.ui_font, 11, 'bold'))
self.run_header.pack(anchor='w')
self.step_rows = {}
self.steps_box = ttk.Frame(f)
@@ -228,7 +235,7 @@ class App:
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
self.warn_label.pack(anchor='w')
self.log_frame = ttk.Frame(f)
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=('Consolas', 9))
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
scroll = ttk.Scrollbar(self.log_frame, command=self.log.yview)
self.log.configure(yscrollcommand=scroll.set)
scroll.pack(side='right', fill='y')
@@ -373,7 +380,7 @@ class App:
glyph.configure(text=GLYPH[state], foreground=COLOR[state])
name.configure(text=self.cat('step.' + s) + (f' ({self.cat("ui.state_" + state)})'
if state in (SKIPPED, FAILED) else ''),
font=('Segoe UI', 10, 'bold') if state == RUNNING else ('Segoe UI', 10))
font=(self.ui_font, 10, 'bold') if state == RUNNING else (self.ui_font, 10))
mode = m.mode or 'install'
self.run_header.configure(text=self.cat('ui.running_' + mode if mode in ('install', 'resume', 'update')
else 'ui.running_install'))
@@ -478,7 +485,11 @@ class App:
def open_folder(self):
dest = self.model.dest
if dest and Path(dest).is_dir():
os.startfile(dest)
if os.name == 'nt':
os.startfile(dest)
else:
subprocess.Popen(['xdg-open', dest], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True)
# ---- self test ---------------------------------------------------------------------------------------------------
+19 -4
View File
@@ -11,6 +11,7 @@ import ctypes
import json
import os
import queue
import signal
import subprocess
import sys
import threading
@@ -75,7 +76,10 @@ def flat(value):
def windows_ui_language():
"""'ru' when the Windows display language is Russian, else 'en'."""
"""'ru' when the display language (Windows; elsewhere LC_ALL, LC_MESSAGES or LANG) is Russian, else 'en'."""
if os.name != 'nt':
loc = os.environ.get('LC_ALL') or os.environ.get('LC_MESSAGES') or os.environ.get('LANG') or ''
return 'ru' if loc.lower().startswith('ru') else 'en'
try:
langid = ctypes.windll.kernel32.GetUserDefaultUILanguage()
except (AttributeError, OSError):
@@ -316,7 +320,14 @@ else:
def send_break(proc, python=None):
"""Ctrl+Break to the process group of proc (started with CREATE_NEW_PROCESS_GROUP). The window itself may have no
console (pythonw), so a short helper process attaches to the child's console and sends it."""
console (pythonw), so a short helper process attaches to the child's console and sends it. Elsewhere SIGTERM to
proc's session (Process starts it in one of its own), which installer/cli.py handles as Ctrl+Break."""
if os.name != 'nt':
try:
os.killpg(proc.pid, signal.SIGTERM)
return True
except OSError:
return False
helper = [str(python or sys.executable), '-c', _BREAK, str(proc.pid)]
flags = CREATE_NO_WINDOW if os.name == 'nt' else 0
r = subprocess.run(helper, creationflags=flags, capture_output=True)
@@ -332,10 +343,14 @@ class Process:
self.command = command
self.helper_python = helper_python
self.lines = queue.Queue()
flags = (CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) if creationflags is None else creationflags
env = dict(os.environ, PYTHONIOENCODING='utf-8', PYTHONUTF8='1', PYTHONUNBUFFERED='1')
if os.name == 'nt':
flags = (CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) if creationflags is None else creationflags
group = {'creationflags': flags}
else:
group = {'start_new_session': True}
self.proc = subprocess.Popen(command, cwd=cwd, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, creationflags=flags)
stderr=subprocess.STDOUT, **group)
self.cancelled_at = None
self.reader = threading.Thread(target=self.read, daemon=True)
self.reader.start()
+20 -2
View File
@@ -1,6 +1,7 @@
// Host check of the installer's first step, compiled with the bundled clang at install time: does the CPU run
// -march=x86-64-v3 code, and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
// The Vulkan loader is opened at run time, so a machine without one gets "loader": false instead of a failed start.
#include <cpuid.h>
#include <stdio.h>
#include <string.h>
#include <vulkan/vulkan.h>
@@ -24,9 +25,26 @@ static void json_string(const char *s) {
putchar('"');
}
// The x86-64-v3 level from CPUID: AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT and MOVBE, with the OS saving the YMM state.
// Not __builtin_cpu_supports("x86-64-v3"): clang reads that from the C runtime's CPU model, and libgcc before GCC 12
// (the Linux release's sysroot has GCC 10) has no level bits, so it answers false on every CPU.
static int x86_64_v3(void) {
unsigned a, b, c, d;
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
const unsigned fma = 1u << 12, movbe = 1u << 22, osxsave = 1u << 27, avx = 1u << 28, f16c = 1u << 29;
if ((c & (fma | movbe | osxsave | avx | f16c)) != (fma | movbe | osxsave | avx | f16c)) return 0;
unsigned lo, hi;
__asm__("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
if ((lo & 6) != 6) return 0; // XMM and YMM state enabled by the OS
if (!__get_cpuid_count(7, 0, &a, &b, &c, &d)) return 0;
const unsigned bmi1 = 1u << 3, avx2 = 1u << 5, bmi2 = 1u << 8;
if ((b & (bmi1 | avx2 | bmi2)) != (bmi1 | avx2 | bmi2)) return 0;
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
return (c & (1u << 5)) != 0; // LZCNT (ABM)
}
int main(void) {
__builtin_cpu_init();
printf("{\"x86_64_v3\": %s, ", __builtin_cpu_supports("x86-64-v3") ? "true" : "false");
printf("{\"x86_64_v3\": %s, ", x86_64_v3() ? "true" : "false");
void *lib = open_lib();
PFN_vkGetInstanceProcAddr gipa = lib ? (PFN_vkGetInstanceProcAddr)lib_sym(lib, "vkGetInstanceProcAddr") : NULL;
if (!gipa) {
+48 -10
View File
@@ -15,21 +15,28 @@ Release (docs/installer.md "Release layout"), root = the unpacked folder:
VERSION the release version, one line
temp/ the default temp folder (created)
Linux release (the AppImage's folder, docs/installer.md "Linux release"): python/bin/python3 (python-build-standalone,
with tkinter), llvm/ with clang, lld and llvm-ar plus llvm/sysroot (the glibc headers and libraries the game links
against), runtime/deps/ with include/ and the static Opus, libvpx and zstd, bin/ without .exe and with libSDL3.so.0.
Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed/py/python.exe (the embeddable
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
runtime/ as the sources with build/install-rtlib/libtotkrt.a (tools/build_game.py --make-runtime-lib --out
build/install-rtlib); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626.
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
"""
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
EXE = '.exe' if os.name == 'nt' else ''
WIN = os.name == 'nt'
EXE = '.exe' if WIN else ''
UCRT = Path(r'C:\msys64\ucrt64\bin')
SUPPORTED = ('1.2.1', '1.4.3') # updates the installer builds; the check identifies the player's by hash
SUPPORTED = ('1.2.1', '1.4.3', '1.1.1') # updates the installer builds; the check identifies the player's by hash
def version_tag(version):
@@ -45,8 +52,8 @@ class Layout:
python: Path # runs the tools
tools: Path
recomp: Path
llvm: Path # llvm-mingw tree
deps: Path # include/ and lib/ for the link
llvm: Path # llvm-mingw tree; on Linux the tree with bin/clang (a release's, or the system's /usr)
deps: Path | None # include/ and lib/ for the link; None: the system's (Linux dev mode)
runtime: Path # runtime headers and install-side sources
runtime_lib: Path # libtotkrt.a
ninja: Path
@@ -56,6 +63,7 @@ class Layout:
shader_list: Path | None = None # None: dev, read from refs/MacNativeGames through git
ini_example: Path | None = None
temp: Path | None = None
sysroot: Path | None = None # Linux release: what the game links against, instead of the host's
@property
def clang(self):
@@ -75,15 +83,21 @@ class Layout:
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
self.recomp / 'gen.py',
self.clang,
self.deps / 'include', self.deps / 'lib', self.runtime, self.runtime_lib, self.ninja, self.hactool,
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, self.runtime_lib, self.ninja, self.hactool,
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
if self.shader_list:
need.append(self.shader_list)
if self.sysroot:
need.append(self.sysroot)
return [str(p) for p in need if not p.exists()]
def release_python(root):
return root / 'python' / ('python.exe' if WIN else 'bin/python3')
def is_release(root):
return (root / 'python' / ('python' + EXE)).is_file()
return release_python(root).is_file()
def release_layout(root):
@@ -91,17 +105,23 @@ def release_layout(root):
return Layout(
root=root, dev=False,
release=version.read_text(encoding='utf-8').strip() if version.is_file() else 'unknown',
python=root / 'python' / ('python' + EXE),
python=release_python(root),
tools=root / 'tools', recomp=root / 'recomp',
llvm=root / 'llvm', deps=root / 'runtime' / 'deps', runtime=root / 'runtime',
runtime_lib=root / 'runtime' / 'libtotkrt.a',
ninja=root / 'bin' / ('ninja' + EXE), hactool=root / 'bin' / ('hactool' + EXE),
xlate=root / 'bin' / ('totk_xlate' + EXE),
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll'),
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example', temp=root / 'temp')
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example',
# Linux: None, the install folder's .setup-temp (installer/cli.py): the AppImage is mounted read-only, and on
# a Steam Deck /home is often too full for the generated C while the install folder is on the SD card
temp=root / 'temp' if WIN else None,
sysroot=None if WIN else root / 'llvm' / 'sysroot')
def dev_layout(root):
if not WIN:
return dev_layout_posix(root)
commit = subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
text=True).stdout.strip()
embed = root / 'build' / 'py314-embed' / 'py' / ('python' + EXE)
@@ -120,6 +140,24 @@ def dev_layout(root):
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
def dev_commit(root):
return subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
text=True).stdout.strip()
def dev_layout_posix(root):
clang, ninja = shutil.which('clang'), shutil.which('ninja')
semver = (root / 'VERSION').read_text(encoding='utf-8').strip()
return Layout(
root=root, dev=True, release=f'{semver}-dev+{dev_commit(root) or "unknown"}',
python=Path(sys.executable), tools=root / 'tools', recomp=root / 'recomp',
llvm=Path(clang).parent.parent if clang else root / 'toolchain' / 'clang-missing', deps=None,
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
ninja=Path(ninja) if ninja else root / 'toolchain' / 'ninja-missing',
hactool=root / 'toolchain' / 'hactool' / 'hactool', xlate=root / 'build' / 'xlate' / 'totk_xlate',
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
def find(root=None):
"""The layout of root (default: the folder above this package)."""
root = Path(root or Path(__file__).resolve().parent.parent).resolve()
+62 -18
View File
@@ -16,6 +16,7 @@ import json
import os
import re
import shutil
import signal
import subprocess
import sys
import threading
@@ -95,10 +96,36 @@ def kill_tree(proc):
return
if os.name == 'nt':
subprocess.run(['taskkill', '/T', '/F', '/PID', str(proc.pid)], capture_output=True)
else:
return
try: # run_tool starts each tool in a session of its own: the group holds ninja, clang and the game
os.killpg(proc.pid, signal.SIGKILL)
except OSError:
proc.kill()
def linux_glibc():
"""(major, minor) of the C library this Linux process runs on, None elsewhere or when it is not glibc."""
try:
name = os.confstr('CS_GNU_LIBC_VERSION') if sys.platform.startswith('linux') else None
except (ValueError, OSError):
return None
m = re.match(r'glibc (\d+)\.(\d+)', name or '')
return (int(m.group(1)), int(m.group(2))) if m else None
# The bundled clang, lld and the cryptography wheel are built against glibc 2.34 (tools/make_release_linux.py
# GLIBC_FLOOR); on an older one they cannot start
GLIBC_NEEDED = (2, 34)
def linux_kernel():
"""(major, minor) of the running Linux kernel, None elsewhere."""
if not sys.platform.startswith('linux'):
return None
m = re.match(r'(\d+)\.(\d+)', os.uname().release)
return (int(m.group(1)), int(m.group(2))) if m else None
def run_tool(ctx, step, script, args, parse=None, cwd=None):
"""Run a tool script with the bundled Python. Its output goes to <temp>/logs/<step>-<script>.log and, line by line,
to parse(line). Returns (exit code, log path)."""
@@ -110,7 +137,8 @@ def run_tool(ctx, step, script, args, parse=None, cwd=None):
env = dict(os.environ, PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8')
with open(log, 'w', encoding='utf-8', newline='\n') as out:
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd or ctx.layout.root,
env=env, text=True, encoding='utf-8', errors='replace', bufsize=1)
env=env, text=True, encoding='utf-8', errors='replace', bufsize=1,
start_new_session=os.name != 'nt')
try:
for line in proc.stdout:
out.write(line)
@@ -140,9 +168,9 @@ def raise_if_damaged(error, name, path):
def game_version(ctx):
"""The update this install is built from: what the check identified, else the recorded one (an install made before
1.4.3 support records none: 1.2.1)."""
return ctx.state.get('version', SUPPORTED[0])
"""The update this install is built from: what the check identified, else the recorded one. An install made before
1.4.3 support records no version, and that install is 1.2.1: name it here rather than leaning on SUPPORTED's order."""
return ctx.state.get('version', '1.2.1')
def expected_update(ctx):
@@ -202,7 +230,8 @@ class Check(Step):
raise InstallError('check.release_incomplete', paths=missing[:5])
if dest.exists() and not dest.is_dir():
raise InstallError('check.dest_not_folder', path=str(dest))
if ctx.mode == 'install' and dest.is_dir() and any(dest.iterdir()):
own = (ctx.temp, *ctx.temp.parents) # a temp folder inside the install folder (installer/cli.py) is ours
if ctx.mode == 'install' and dest.is_dir() and any(p not in own for p in dest.iterdir()):
raise InstallError('check.dest_not_empty', path=str(dest))
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
if not lang:
@@ -230,9 +259,17 @@ class Check(Step):
def host(self, ctx):
layout, work = ctx.layout, ctx.temp / 'check'
glibc = linux_glibc()
if glibc and glibc < GLIBC_NEEDED and not layout.dev:
raise InstallError('check.glibc_old', version='.'.join(map(str, glibc)),
need='.'.join(map(str, GLIBC_NEEDED)))
work.mkdir(parents=True, exist_ok=True)
exe = work / ('hostcheck' + EXE)
command = [str(layout.clang), '-O1', str(HOSTCHECK), '-I', str(layout.deps / 'include'), '-o', str(exe)]
command = [str(layout.clang), '-O1', str(HOSTCHECK), '-o', str(exe)]
if layout.deps:
command += ['-I', str(layout.deps / 'include')]
if layout.sysroot:
command += [f'--sysroot={layout.sysroot}', '-fuse-ld=lld']
if os.name != 'nt':
command.append('-ldl')
r = subprocess.run(command, capture_output=True, text=True, errors='replace')
@@ -255,6 +292,11 @@ class Check(Step):
if not any(tuple(d['api'][:2]) >= (1, 3) for d in devices):
raise InstallError('check.vulkan_too_old',
devices=', '.join(f'{d["name"]} ({d["api"][0]}.{d["api"][1]})' for d in devices))
kernel = linux_kernel()
if kernel and kernel < (6, 7):
# Without userfaultfd's asynchronous write protection nothing tracks the CPU's writes: the game copies whole
# pools to the GPU at each submit and its texture checks see every page as changed (docs/building.md, Linux)
ctx.reporter.warning('warn.kernel_old', version='.'.join(map(str, kernel)))
ctx.host = host
def inputs(self, ctx):
@@ -533,8 +575,8 @@ class Compile(Step):
layout = ctx.layout
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py')]
return fingerprint('compile-1', ctx.keys['lift'], layout.runtime, layout.runtime_lib, layout.deps, *tools,
clang, *[d.name for d in layout.dlls])
return fingerprint('compile-1', ctx.keys['lift'], layout.runtime, layout.runtime_lib, layout.deps or 'system',
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
def current(self, ctx, record):
exe = ctx.dest / ('totk' + EXE)
@@ -559,7 +601,8 @@ class Compile(Step):
rep.progress('compile', 'compile', force=True)
code, log = run_tool(ctx, 'compile', layout.tools / 'build_game.py', [
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm, '--deps', layout.deps,
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm,
*(['--deps', layout.deps] if layout.deps else []), *(['--sysroot', layout.sysroot] if layout.sysroot else []),
'--runtime', layout.runtime, '--runtime-lib', layout.runtime_lib, '--ninja', layout.ninja,
'--version', version_tag(game_version(ctx)), '--expected', layout.expected(game_version(ctx)),
'--exefs', dest / 'exefs', '--romfs', dest / 'romfs',
@@ -582,7 +625,10 @@ class Compile(Step):
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
WIDE_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160') # runtime/ui/menu.cpp row_output_res
# The game resolutions of another shape than 16:9 that runtime/ui/menu.cpp row_output_res lists: 21:9 and 32:9 monitors,
# and 16:10 screens (the Steam Deck's 1280x800, handhelds and laptops)
ASPECT_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160', '3840x1080', '5120x1440',
'1280x800', '1680x1050', '1920x1200', '2560x1600')
class Prepare(Step):
@@ -622,15 +668,13 @@ class Prepare(Step):
dest / 'romfs-patches'])
if code:
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
# The same overlay with the UI laid out for the 21:9 game resolutions the settings menu offers (runtime/romfs.c
# overlay_init picks the one that matches). Only 1.4.3 has the camera aspect patch (recomp/patches-v143.txt) the
# layouts go with, and ultrawide_layout.py is checked against 1.4.3's layout archive: 1.2.1 builds none.
for size in WIDE_SIZES:
# The same overlay with the UI laid out for each game resolution of another shape the settings menu offers
# (runtime/romfs.c overlay_init picks the one that matches; the camera's side is the patch set aspect,
# recomp/patches-v<ver>.txt).
for size in ASPECT_SIZES:
remove(dest / f'romfs-patches-{size}')
if version_tag(game_version(ctx)) != 'v143':
continue
code, log = run_tool(ctx, 'prepare', layout.tools / 'make_romfs_patches.py',
['--version', 'v143', '--romfs', dest / 'romfs', '--out',
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
dest / f'romfs-patches-{size}', '--aspect', size])
if code:
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
+11 -9
View File
@@ -2,7 +2,7 @@
"step.check": "Checking the computer and the game files",
"step.extract": "Extracting the game",
"step.lift": "Translating the game code to C",
"step.compile": "Compiling totk.exe",
"step.compile": "Compiling {game_exe}",
"step.prepare": "Preparing the shaders",
"step.cleanup": "Cleaning up",
@@ -21,7 +21,7 @@
"phase.lift.discover": "Finding the functions",
"phase.lift.emit": "Writing C code",
"phase.compile.compile": "Compiling",
"phase.compile.link": "Linking totk.exe",
"phase.compile.link": "Linking {game_exe}",
"phase.prepare.romfs_patches": "Writing the 60 fps game data patches",
"phase.prepare.seed": "Reading the shaders from the game data",
"phase.prepare.translate": "Translating the shaders",
@@ -42,12 +42,13 @@
"check.lang_missing": "Choose the game language.",
"check.lang_unknown": "Unknown language '{lang}'. Known: {known}.",
"check.toolchain_broken": "The bundled compiler does not work: {detail}",
"check.glibc_old": "This system's C library is glibc {version}; the setup's compiler needs {need} or later (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 and newer have it). Updating the system fixes this.",
"check.host_probe_failed": "The computer check did not run (exit code {code}).",
"check.cpu_no_avx2": "This processor lacks AVX2 (x86-64-v3), which the game needs. Intel Haswell (2013) or AMD Excavator / Zen and newer have it.",
"check.vulkan_missing": "No Vulkan driver is installed. Install the latest driver for your graphics card.",
"check.vulkan_no_device": "Vulkan finds no graphics card. Install the latest driver for your graphics card.",
"check.vulkan_too_old": "The game needs a graphics card with Vulkan 1.3; found: {devices}. Update the graphics driver.",
"check.inputs_needed": "The game files are needed: choose the base game, the 1.2.1 or 1.4.3 update and prod.keys.",
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.1 or 1.2.1 or 1.4.3 update and prod.keys.",
"check.input_missing": "File not found ({input}): {path}",
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
"check.keys_no_header_key": "The keys file has no header_key. Dump prod.keys from your console again.",
@@ -59,7 +60,7 @@
"check.ticket_missing": "The {input} file lacks the ticket its content needs.",
"check.base_not_totk": "The base game file is not The Legend of Zelda: Tears of the Kingdom ({detail}).",
"check.update_not_nsp": "The update must be an NSP file.",
"check.update_is_base": "The update file holds a base game. Choose the 1.2.1 or 1.4.3 update NSP.",
"check.update_is_base": "The update file holds a base game. Choose the 1.1.1 or 1.2.1 or 1.4.3 update NSP.",
"check.update_not_totk": "The update file is not a Tears of the Kingdom update ({detail}).",
"check.exefs_failed": "The game code of the {input} file failed its checks; see {log}.",
"check.input_damaged": "The {input} file is damaged or incomplete: {path} ({detail}). Copy that file again from its source, or dump it again, then run the setup again.",
@@ -71,7 +72,7 @@
"extract.romfs_merge_failed": "Applying the update to the game data failed: {detail}. Details: {log}",
"extract.romfs_mismatch": "The extracted game data does not match version {version} (size {size}, checksum {sha256}...).",
"lift.failed": "Translating the game code failed (exit code {code}). Details: {log}",
"compile.failed": "Compiling totk.exe failed (exit code {code}). Details: {log}",
"compile.failed": "Compiling {game_exe} failed (exit code {code}). Details: {log}",
"compile.exe_in_use": "{path} is in use. Close the game and run the installer again.",
"prepare.shader_list_missing": "The shader list is missing.",
"prepare.romfs_patches_failed": "Writing the game data patches failed (exit code {code}). Details: {log}",
@@ -83,6 +84,7 @@
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
"warn.kernel_old": "Linux {version} lacks the write tracking the game relies on (6.7 or later); the game may run very slowly. Updating the system fixes this.",
"ui.title": "TotK Setup",
"ui.version": "version {version}",
@@ -90,13 +92,13 @@
"ui.notice": "The folder this setup builds contains the game. It is for your own use only; do not share or upload it.",
"ui.keys_private": "The keys file is read in place. Its contents are never shown, copied or stored.",
"ui.base": "Base game (NSP or XCI)",
"ui.update": "Update 1.2.1 or 1.4.3 (NSP)",
"ui.update": "Update 1.1.1 or 1.2.1 or 1.4.3 (NSP)",
"ui.keys": "prod.keys",
"ui.game_language": "Game language",
"ui.dest": "Install folder",
"ui.browse": "Browse...",
"ui.pick_game": "Choose the base game file",
"ui.pick_update": "Choose the 1.2.1 or 1.4.3 update file",
"ui.pick_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file",
"ui.pick_keys": "Choose prod.keys",
"ui.pick_dest": "Choose the install folder",
"ui.type_game": "Game files (NSP, XCI)",
@@ -114,7 +116,7 @@
"ui.update_mode": "This folder holds an earlier installation. The setup continues or updates it; the game files are asked for only if they are needed again.",
"ui.need_dest": "Choose the install folder.",
"ui.need_base": "Choose the base game file.",
"ui.need_update": "Choose the 1.2.1 or 1.4.3 update file.",
"ui.need_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file.",
"ui.need_keys": "Choose the prod.keys file.",
"ui.need_lang": "Choose the game language.",
"ui.missing_base": "The base game file does not exist.",
@@ -136,7 +138,7 @@
"ui.result_ok": "The game is ready",
"ui.result_failed": "The setup failed",
"ui.result_cancelled": "The setup was cancelled",
"ui.done_text": "Start totk.exe from {dest}. The setup program and the game files you chose are no longer needed.",
"ui.done_text": "Start {game_exe} from {dest}. The setup program and the game files you chose are no longer needed.",
"ui.cancelled_text": "The finished steps are kept. Start the setup again with the same folder to continue.",
"ui.log_file": "Log file: {path}",
"ui.process_died": "The setup stopped unexpectedly (exit code {code}).",
+11 -9
View File
@@ -2,7 +2,7 @@
"step.check": "Проверка компьютера и игровых файлов",
"step.extract": "Распаковка игры",
"step.lift": "Перевод кода игры в C",
"step.compile": "Сборка totk.exe",
"step.compile": "Сборка {game_exe}",
"step.prepare": "Подготовка шейдеров",
"step.cleanup": "Уборка",
@@ -21,7 +21,7 @@
"phase.lift.discover": "Поиск функций",
"phase.lift.emit": "Запись кода на C",
"phase.compile.compile": "Компиляция",
"phase.compile.link": "Компоновка totk.exe",
"phase.compile.link": "Компоновка {game_exe}",
"phase.prepare.romfs_patches": "Запись патчей данных игры для 60 кадров/с",
"phase.prepare.seed": "Чтение шейдеров из данных игры",
"phase.prepare.translate": "Трансляция шейдеров",
@@ -42,12 +42,13 @@
"check.lang_missing": "Выберите язык игры.",
"check.lang_unknown": "Неизвестный язык «{lang}». Доступные: {known}.",
"check.toolchain_broken": "Встроенный компилятор не работает: {detail}",
"check.glibc_old": "Библиотека C этой системы — glibc {version}; компилятору установщика нужна {need} или новее (она есть в Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 и новее). Обновление системы это исправит.",
"check.host_probe_failed": "Проверка компьютера не выполнилась (код выхода {code}).",
"check.cpu_no_avx2": "Этому процессору не хватает AVX2 (x86-64-v3), который нужен игре. AVX2 есть у Intel Haswell (2013) и новее, у AMD Excavator и Zen и новее.",
"check.vulkan_missing": "Драйвер Vulkan не установлен. Установите свежий драйвер видеокарты.",
"check.vulkan_no_device": "Vulkan не находит видеокарту. Установите свежий драйвер видеокарты.",
"check.vulkan_too_old": "Игре нужна видеокарта с Vulkan 1.3; найдено: {devices}. Обновите драйвер видеокарты.",
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.2.1 или 1.4.3 и prod.keys.",
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.1 или 1.2.1 или 1.4.3 и prod.keys.",
"check.input_missing": "Файл не найден ({input}): {path}",
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
"check.keys_no_header_key": "В файле ключей нет header_key. Снова получите prod.keys со своей консоли.",
@@ -59,7 +60,7 @@
"check.ticket_missing": "В файле «{input}» нет тикета, который нужен его содержимому.",
"check.base_not_totk": "Файл базовой игры — не The Legend of Zelda: Tears of the Kingdom ({detail}).",
"check.update_not_nsp": "Обновление должно быть файлом NSP.",
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.2.1 или 1.4.3.",
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.1 или 1.2.1 или 1.4.3.",
"check.update_not_totk": "Файл обновления — не обновление Tears of the Kingdom ({detail}).",
"check.exefs_failed": "Код игры из файла «{input}» не прошёл проверку; подробности в {log}.",
"check.input_damaged": "Файл «{input}» повреждён или скопирован не полностью: {path} ({detail}). Скопируйте этот файл заново из источника или сделайте дамп ещё раз, затем снова запустите установку.",
@@ -71,7 +72,7 @@
"extract.romfs_merge_failed": "Не удалось применить обновление к данным игры: {detail}. Подробности: {log}",
"extract.romfs_mismatch": "Распакованные данные игры не соответствуют версии {version} (размер {size}, контрольная сумма {sha256}...).",
"lift.failed": "Не удалось перевести код игры (код выхода {code}). Подробности: {log}",
"compile.failed": "Не удалось собрать totk.exe (код выхода {code}). Подробности: {log}",
"compile.failed": "Не удалось собрать {game_exe} (код выхода {code}). Подробности: {log}",
"compile.exe_in_use": "Файл {path} занят. Закройте игру и запустите установку снова.",
"prepare.shader_list_missing": "Нет списка шейдеров.",
"prepare.romfs_patches_failed": "Не удалось записать патчи данных игры (код выхода {code}). Подробности: {log}",
@@ -83,6 +84,7 @@
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, которые установка не извлекает; часть текста может не отображаться.",
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
"warn.kernel_old": "В Linux {version} нет отслеживания записи, на которое опирается игра (нужна версия 6.7 или новее); игра может работать очень медленно. Обновление системы это исправит.",
"ui.title": "Установка TotK",
"ui.version": "версия {version}",
@@ -90,13 +92,13 @@
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
"ui.base": "Базовая игра (NSP или XCI)",
"ui.update": "Обновление 1.2.1 или 1.4.3 (NSP)",
"ui.update": "Обновление 1.1.1 или 1.2.1 или 1.4.3 (NSP)",
"ui.keys": "prod.keys",
"ui.game_language": "Язык игры",
"ui.dest": "Папка установки",
"ui.browse": "Обзор...",
"ui.pick_game": "Выберите файл базовой игры",
"ui.pick_update": "Выберите файл обновления 1.2.1 или 1.4.3",
"ui.pick_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3",
"ui.pick_keys": "Выберите prod.keys",
"ui.pick_dest": "Выберите папку установки",
"ui.type_game": "Файлы игры (NSP, XCI)",
@@ -114,7 +116,7 @@
"ui.update_mode": "В этой папке уже есть установка. Программа продолжит или обновит её; файлы игры понадобятся, только если их снова придётся читать.",
"ui.need_dest": "Выберите папку установки.",
"ui.need_base": "Выберите файл базовой игры.",
"ui.need_update": "Выберите файл обновления 1.2.1 или 1.4.3.",
"ui.need_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3.",
"ui.need_keys": "Выберите файл prod.keys.",
"ui.need_lang": "Выберите язык игры.",
"ui.missing_base": "Файл базовой игры не найден.",
@@ -136,7 +138,7 @@
"ui.result_ok": "Игра готова",
"ui.result_failed": "Установка не удалась",
"ui.result_cancelled": "Установка отменена",
"ui.done_text": "Запустите totk.exe из папки {dest}. Программа установки и выбранные игровые файлы больше не нужны.",
"ui.done_text": "Запустите {game_exe} из папки {dest}. Программа установки и выбранные игровые файлы больше не нужны.",
"ui.cancelled_text": "Завершённые шаги сохранены. Чтобы продолжить, снова запустите установку с той же папкой.",
"ui.log_file": "Файл журнала: {path}",
"ui.process_died": "Установка неожиданно остановилась (код выхода {code}).",
+957
View File
@@ -0,0 +1,957 @@
# Code patches for TotK 1.1.1 main (recomp/patches.py). The sites come from MacNativeGames
# games/totk/runtime/game_patches.c (MIT, Copyright (c) 2026 MacNativeGames contributors) at upstream ebe621d, through
# its 1.2.1 addresses in game_version.h (2db612b); each one was located again in 1.1.1 by instruction-window matching
# and its original words checked against this build's main. The patch page holds two floats the runtime writes at every
# present (frame_rate.c): +0 the 30 Hz ratio (seconds * 30), +4 the frame step in seconds.
set fps60 TOTK_FPS60 1
set realtime TOTK_REALTIME 1
set cloth TOTK_CLOTH_REALTIME 1
set bounce TOTK_HIGHFPS_FIX 1
set output TOTK_PATCH_OUTPUT 0
set scene TOTK_PATCH_SCENE 0
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
site fps60 main 90e978 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
site fps60 main 90e97c 1a89b10a 1a9fa52a # clamp: csinc w10, w9, wzr, ge (desired = max(desired, 1))
site fps60 main 90e9a0 b9401a8a d503201f # lower trigger: nop the stored-candidate load
site fps60 main 90ea30 6b08015f 7100055f # raise path: cmp w10, #1
site fps60 main 90ea40 1a88c1ac 1a9fc5ac # raise path: csinc w12, w13, wzr, gt (next lower candidate floors at 1)
site fps60 main 90ea94 b9400288 52800028 # lower path: mov w8, #1 (minimum interval 1)
site fps60 main 90eab0 2a0a03f5 1a9fa555 # lower path: csinc w21, w10, wzr, ge (result never below 1)
# Real-time step: the VFR step (main+0x7ede6c, 1.0.0 0x7a83e4) and the global ratio after the controller call
# (main+0x8f2394, 1.0.0 0x8f68b8) read ratio and seconds from the patch page instead of interval * 0.5 and interval / 60.
site realtime main 833e4c 1e220020 @page:x9 # scvtf s0, w1 -> adrp x9, page
site realtime main 833e50 52a84e08 bd400120 # mov w8, #60.0 -> ldr s0, [x9] (ratio)
site realtime main 833e54 1e270101 bd400521 # fmov s1, w8 -> ldr s1, [x9, #4] (seconds)
site realtime main 833e58 1e2c1002 d503201f # fmov s2, #0.5 -> nop
site realtime main 833e64 1e211801 d503201f # fdiv s1, s0, s1 -> nop
site realtime main 833e68 1e220800 d503201f # fmul s0, s0, s2 -> nop
site realtime main 90e5d0 bd425e60 @page:x9 # ldr s0, [x19, #0x25c] -> adrp x9, page
site realtime main 90e5d4 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (global ratio)
site realtime main 90e5e4 5e21d800 d503201f # scvtf s0, s0 -> nop
site realtime main 90e5e8 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (1.1.1
# main+0x8ecb40, 1.2.1 main+0x89822c) scales s2 (seconds) and s3 (ratio) by the measured ratio. Upstream's
# veneer bodies, inlined at the site.
site cloth main 9efc20 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
site cloth main 9efc1c bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
site cloth main 9f0bcc 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
site cloth main 9f0ba8 bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
site cloth main a05a04 1e210801 f81f0fe9 @page:x9 bd400521 1e210801 f84107e9 # fmul s1, s0, s1
site cloth main a05a08 bd0137e0 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f081f bd0157ff 3dc007ff f94003e9 910083ff # str s0, [sp, #0x134]
site cloth main 9f044c 97fbf1bd @page:x8 bd400104 1e240842 1e240863 97fbf1bd # bl 0x8ecb40
# Trampolines (upstream 27c3b43): the slow-motion check compares time object +0x28 (30 / (ratio * time scale)) with
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
site bounce main bee7e0 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
# through main+0xcc4160) from page +32; the renderer itself goes to page +40.
site output main eb8cfc d280f008 @page:x8 # mov x8, #0x780 -> adrp x8, page
site output main eb8d00 f2c08708 f9401908 # movk x8, #0x438, lsl #32 -> ldr x8, [x8, #48]
site output main cea5c8 aa1403e0 @page:x21 # mov x0, x20 -> adrp x21, page
site output main cea5cc 94000081 f90016b3 # bl 0xcc4160 -> str x19, [x21, #40]
site output main cea5d0 f9400015 f94012b5 # ldr x21, [x0] -> ldr x21, [x21, #32]
# Scene render size (upstream patch_render_res): the renderer's +0xcc4 / +0xcc8, which size every scene render target,
# take two floats from page +16 instead of the graphics parameters' docked 1600x900.
site scene main cea5e0 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
site scene main cea5e8 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
site scene main cea5ec 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v111/exefs --patches recomp/patches-v111.txt: loops that only
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
# spin waits found by tools/spin_waits.py on the 1.1.x modules
# main: 869 spin waits
spin main ac600 ac620 ac630 # 13 instructions; loads
spin main ac680 ac6a8 ac6b8 # 15 instructions; loads
spin main ac6f0 ac718 ac728 # 15 instructions; loads
spin main e8544 e854c # 3 instructions; loads
spin main 2457f0 245804 # 6 instructions; loads
spin main 644bbc 644bd0 # 6 instructions; loads
spin main 6456ac 6456c0 # 6 instructions; loads
spin main 6459d4 6459e8 # 6 instructions; loads
spin main 645d58 645d6c # 6 instructions; loads
spin main 6460c4 6460d8 # 6 instructions; loads
spin main 64649c 6464b0 # 6 instructions; loads
spin main 646740 646754 # 6 instructions; loads
spin main 6467d4 6467e8 # 6 instructions; loads
spin main 646e6c 646e80 # 6 instructions; loads
spin main 647080 647094 # 6 instructions; loads
spin main 6484f4 648510 # 8 instructions; loads
spin main 6485a4 6485c0 # 8 instructions; loads
spin main 649fb0 649fc4 # 6 instructions; loads
spin main 64a064 64a078 # 6 instructions; loads
spin main 64a3d0 64a3e4 # 6 instructions; loads
spin main 64afac 64afc8 # 8 instructions; loads
spin main 64b404 64b418 # 6 instructions; loads
spin main 64b980 64b994 # 6 instructions; loads
spin main 64c68c 64c6a0 # 6 instructions; loads
spin main 64c978 64c98c # 6 instructions; loads
spin main 64ca28 64ca3c # 6 instructions; loads
spin main 64d378 64d38c # 6 instructions; loads
spin main 64d8d8 64d8ec # 6 instructions; loads
spin main 64db90 64dba4 # 6 instructions; loads
spin main 64de18 64de2c # 6 instructions; loads
spin main 64de40 64de54 # 6 instructions; loads
spin main 64de68 64de7c # 6 instructions; loads
spin main 64e6c8 64e6dc # 6 instructions; loads
spin main 64e6f0 64e704 # 6 instructions; loads
spin main 64e8ac 64e8c0 # 6 instructions; loads
spin main 64e9a4 64e9b8 # 6 instructions; loads
spin main 64fb50 64fb64 # 6 instructions; loads
spin main 652eac 652ec0 # 6 instructions; loads
spin main 652ed4 652ee8 # 6 instructions; loads
spin main 652efc 652f10 # 6 instructions; loads
spin main 654590 6545a4 # 6 instructions; loads
spin main 654c5c 654c70 # 6 instructions; loads
spin main 654cfc 654d10 # 6 instructions; loads
spin main 655124 655138 # 6 instructions; loads
spin main 655c14 655c28 # 6 instructions; loads
spin main 656f80 656f94 # 6 instructions; loads
spin main 658e60 658e74 # 6 instructions; loads
spin main 658ebc 658ed0 # 6 instructions; loads
spin main 659b28 659b3c # 6 instructions; loads
spin main 659b50 659b64 # 6 instructions; loads
spin main 65c9e0 65c9f4 # 6 instructions; loads
spin main 65ca08 65ca1c # 6 instructions; loads
spin main 65ca30 65ca44 # 6 instructions; loads
spin main 65ca58 65ca6c # 6 instructions; loads
spin main 65ca80 65ca94 # 6 instructions; loads
spin main 65caa8 65cabc # 6 instructions; loads
spin main 65cad0 65cae4 # 6 instructions; loads
spin main 65caf8 65cb0c # 6 instructions; loads
spin main 65cb20 65cb34 # 6 instructions; loads
spin main 65cb48 65cb5c # 6 instructions; loads
spin main 65cb70 65cb84 # 6 instructions; loads
spin main 65cb98 65cbac # 6 instructions; loads
spin main 65cbc0 65cbd4 # 6 instructions; loads
spin main 65d4b0 65d4c4 # 6 instructions; loads
spin main 65d4d8 65d4ec # 6 instructions; loads
spin main 65d500 65d514 # 6 instructions; loads
spin main 65d528 65d53c # 6 instructions; loads
spin main 65da54 65da68 # 6 instructions; loads
spin main 65de00 65de14 # 6 instructions; loads
spin main 65e200 65e214 # 6 instructions; loads
spin main 65e6ec 65e700 # 6 instructions; loads
spin main 65ecbc 65ecd0 # 6 instructions; loads
spin main 65ece4 65ecf8 # 6 instructions; loads
spin main 663588 66359c # 6 instructions; loads
spin main 663b0c 663b20 # 6 instructions; loads
spin main 663da0 663db4 # 6 instructions; loads
spin main 663dec 663e00 # 6 instructions; loads
spin main 6645a4 6645b8 # 6 instructions; loads
spin main 6645cc 6645e0 # 6 instructions; loads
spin main 666530 666544 # 6 instructions; loads
spin main 666570 666584 # 6 instructions; loads
spin main 6671d4 6671e8 # 6 instructions; loads
spin main 6671fc 667210 # 6 instructions; loads
spin main 6676c0 6676d4 # 6 instructions; loads
spin main 667cfc 667d10 # 6 instructions; loads
spin main 66f34c 66f360 # 6 instructions; loads
spin main 6708f8 67090c # 6 instructions; loads
spin main 670920 670934 # 6 instructions; loads
spin main 670948 67095c # 6 instructions; loads
spin main 673f34 673f48 # 6 instructions; loads
spin main 67556c 675580 # 6 instructions; loads
spin main 676954 676968 # 6 instructions; loads
spin main 676994 6769a8 # 6 instructions; loads
spin main 676e6c 676e80 # 6 instructions; loads
spin main 677148 67715c # 6 instructions; loads
spin main 6771c8 6771dc # 6 instructions; loads
spin main 677484 677498 # 6 instructions; loads
spin main 677df0 677e04 # 6 instructions; loads
spin main 6792a8 6792bc # 6 instructions; loads
spin main 67a984 67a998 # 6 instructions; loads
spin main 698ef8 698f0c # 6 instructions; loads
spin main 6b6d8c 6b6da0 # 6 instructions; loads
spin main 6b922c 6b9240 # 6 instructions; loads
spin main 6c5408 6c541c # 6 instructions; loads
spin main 6c5614 6c5628 # 6 instructions; loads
spin main 6ccce0 6cccf4 # 6 instructions; loads
spin main 6ccd98 6ccdac # 6 instructions; loads
spin main 6d3c9c 6d3cb0 # 6 instructions; loads
spin main 6d3ff8 6d400c # 6 instructions; loads
spin main 6d61cc 6d61e0 # 6 instructions; loads
spin main 6d6210 6d6224 # 6 instructions; loads
spin main 6d6254 6d6268 # 6 instructions; loads
spin main 6d6298 6d62ac # 6 instructions; loads
spin main 6d75c4 6d75d8 # 6 instructions; loads
spin main 6d79dc 6d79f0 # 6 instructions; loads
spin main 6d9398 6d93ac # 6 instructions; loads
spin main 6dd8d0 6dd8e4 # 6 instructions; loads
spin main 6de150 6de164 # 6 instructions; loads
spin main 6e073c 6e0750 # 6 instructions; loads
spin main 6e0a94 6e0aa8 # 6 instructions; loads
spin main 6e4eb8 6e4ecc # 6 instructions; loads
spin main 6eb590 6eb5a4 # 6 instructions; loads
spin main 6eccac 6eccc0 # 6 instructions; loads
spin main 6ed9b4 6ed9c8 # 6 instructions; loads
spin main 6ee69c 6ee6b0 # 6 instructions; loads
spin main 6f94c0 6f94d4 # 6 instructions; loads
spin main 6f9738 6f974c # 6 instructions; loads
spin main 6fa324 6fa338 # 6 instructions; loads
spin main 70d844 70d858 # 6 instructions; loads
spin main 70dc74 70dc88 # 6 instructions; loads
spin main 710a70 710a84 # 6 instructions; loads
spin main 711848 71185c # 6 instructions; loads
spin main 7125a4 7125b8 # 6 instructions; loads
spin main 716474 716488 # 6 instructions; loads
spin main 719a30 719a44 # 6 instructions; loads
spin main 72cd78 72cd8c # 6 instructions; loads
spin main 72f66c 72f680 # 6 instructions; loads
spin main 737684 737698 # 6 instructions; loads
spin main 73e0dc 73e0f0 # 6 instructions; loads
spin main 73ed14 73ed28 # 6 instructions; loads
spin main 783520 783534 # 6 instructions; loads
spin main 797924 797938 # 6 instructions; loads
spin main 79794c 797960 # 6 instructions; loads
spin main 797c08 797c1c # 6 instructions; loads
spin main 797c64 797c78 # 6 instructions; loads
spin main 798ff4 799008 # 6 instructions; loads
spin main 79cbec 79cc00 # 6 instructions; loads
spin main 7ae134 7ae148 # 6 instructions; loads
spin main 7ae388 7ae39c # 6 instructions; loads
spin main 7c292c 7c2940 # 6 instructions; loads
spin main 7d618c 7d61a0 # 6 instructions; loads
spin main 7d904c 7d9060 # 6 instructions; loads
spin main 7e11c0 7e11d4 # 6 instructions; loads
spin main 7e1c9c 7e1cb0 # 6 instructions; loads
spin main 7e24a0 7e24b4 # 6 instructions; loads
spin main 7ed89c 7ed8b0 # 6 instructions; loads
spin main 7eef28 7eef3c # 6 instructions; loads
spin main 7f515c 7f5170 # 6 instructions; loads
spin main 801468 80147c # 6 instructions; loads
spin main 801490 8014a4 # 6 instructions; loads
spin main 8014b8 8014cc # 6 instructions; loads
spin main 80a0a4 80a0b8 # 6 instructions; loads
spin main 811500 811514 # 6 instructions; loads
spin main 816c40 816c54 # 6 instructions; loads
spin main 819450 819464 # 6 instructions; loads
spin main 85df6c 85e068 # 4 instructions; loads
spin main 863220 863234 # 6 instructions; loads
spin main 86352c 863540 # 6 instructions; loads
spin main 863a80 863a94 # 6 instructions; loads
spin main 863b94 863ba8 # 6 instructions; loads
spin main 86543c 865450 # 6 instructions; loads
spin main 865464 865478 # 6 instructions; loads
spin main 87e564 87e578 # 6 instructions; loads
spin main 87e58c 87e5a0 # 6 instructions; loads
spin main 880d44 880d58 # 6 instructions; loads
spin main 8810f4 881108 # 6 instructions; loads
spin main 8814c8 8814dc # 6 instructions; loads
spin main 88189c 8818b0 # 6 instructions; loads
spin main 887ce8 887cfc # 6 instructions; loads
spin main 887f50 887f64 # 6 instructions; loads
spin main 8884ec 888500 # 6 instructions; loads
spin main 888618 88862c # 6 instructions; loads
spin main 888748 88875c # 6 instructions; loads
spin main 8888e0 8888f4 # 6 instructions; loads
spin main 89183c 891850 # 6 instructions; loads
spin main 892ccc 892cec # 9 instructions; loads
spin main 89540c 89541c # 5 instructions; loads
spin main 895668 895694 # 12 instructions; loads
spin main 8956a8 8956b8 # 5 instructions; loads
spin main 8da7dc 8da7e4 # 3 instructions; loads
spin main 8da7ec 8da7f0 # 2 instructions; loads
spin main 8da858 8da860 # 3 instructions; loads
spin main 8fb190 8fb1a4 # 6 instructions; loads
spin main 9045bc 9045d0 # 6 instructions; loads
spin main 9045e4 9045f8 # 6 instructions; loads
spin main 90460c 904620 # 6 instructions; loads
spin main 904634 904648 # 6 instructions; loads
spin main 905b20 905b34 # 6 instructions; loads
spin main 9062f0 906304 # 6 instructions; loads
spin main 91019c 9101b0 # 6 instructions; loads
spin main 910720 910734 # 6 instructions; loads
spin main 9363cc 9363e0 # 6 instructions; loads
spin main 93655c 936570 # 6 instructions; loads
spin main 93819c 9381b0 # 6 instructions; loads
spin main 940060 94007c # 8 instructions; loads
spin main 94012c 940148 # 8 instructions; loads
spin main 97864c 978660 # 6 instructions; loads
spin main 99aa8c 99aabc # 13 instructions; loads
spin main 99aad0 99aae0 # 5 instructions; loads
spin main 9a5fd4 9a6004 # 13 instructions; loads
spin main 9a6018 9a6028 # 5 instructions; loads
spin main 9b2700 9b2710 # 5 instructions; loads
spin main 9f395c 9f3970 # 6 instructions; loads
spin main 9f39e0 9f39f4 # 6 instructions; loads
spin main 9f3b2c 9f3b40 # 6 instructions; loads
spin main 9f3bf8 9f3c0c # 6 instructions; loads
spin main 9f6f5c 9f6f70 # 6 instructions; loads
spin main a11908 a1191c # 6 instructions; loads
spin main a1257c a12590 # 6 instructions; loads
spin main a14d6c a14d80 # 6 instructions; loads
spin main a1b0a4 a1b0b8 # 6 instructions; loads
spin main a1b0ec a1b100 # 6 instructions; loads
spin main a1c3dc a1c3f0 # 6 instructions; loads
spin main a2b268 a2b27c # 6 instructions; loads
spin main a2cab8 a2cacc # 6 instructions; loads
spin main a2f228 a2f23c # 6 instructions; loads
spin main a8e920 a8e934 # 6 instructions; loads
spin main aa2c20 aa2c34 # 6 instructions; loads
spin main aa5108 aa511c # 6 instructions; loads
spin main aa9f94 aa9fa8 # 6 instructions; loads
spin main aaa674 aaa688 # 6 instructions; loads
spin main aad564 aad578 # 6 instructions; loads
spin main ab245c ab24b4 # 7 instructions; loads
spin main ac8874 ac88fc # 35 instructions; loads
spin main af4040 af4054 # 6 instructions; loads
spin main af4068 af407c # 6 instructions; loads
spin main af4090 af40a4 # 6 instructions; loads
spin main af40cc af40e0 # 6 instructions; loads
spin main af40f4 af4108 # 6 instructions; loads
spin main af411c af4130 # 6 instructions; loads
spin main b47a74 b47a88 # 6 instructions; loads
spin main b4e080 b4e094 # 6 instructions; loads
spin main b529f4 b52a08 # 6 instructions; loads
spin main b52a7c b52a90 # 6 instructions; loads
spin main b52cfc b52d10 # 6 instructions; loads
spin main b52dd0 b52de4 # 6 instructions; loads
spin main b53458 b5346c # 6 instructions; loads
spin main b535a8 b535bc # 6 instructions; loads
spin main b54320 b54334 # 6 instructions; loads
spin main b545fc b54610 # 6 instructions; loads
spin main b5662c b56640 # 6 instructions; loads
spin main b56654 b56668 # 6 instructions; loads
spin main b71800 b71814 # 6 instructions; loads
spin main b71828 b7183c # 6 instructions; loads
spin main b7b82c b7b840 # 6 instructions; loads
spin main b80970 b80984 # 6 instructions; loads
spin main b86fe8 b86ffc # 6 instructions; loads
spin main b8702c b87040 # 6 instructions; loads
spin main b87070 b87084 # 6 instructions; loads
spin main b8775c b87770 # 6 instructions; loads
spin main b97d6c b97d80 # 6 instructions; loads
spin main b97ed4 b97ee8 # 6 instructions; loads
spin main b9c628 b9c63c # 6 instructions; loads
spin main b9c6c8 b9c6dc # 6 instructions; loads
spin main b9c6f0 b9c704 # 6 instructions; loads
spin main b9c7b8 b9c7cc # 6 instructions; loads
spin main ba1db4 ba1dc8 # 6 instructions; loads
spin main ba72e8 ba72fc # 6 instructions; loads
spin main bb1070 bb1084 # 6 instructions; loads
spin main bb1098 bb10ac # 6 instructions; loads
spin main bb6c38 bb6c4c # 6 instructions; loads
spin main bb7fa0 bb7fb4 # 6 instructions; loads
spin main bd27c8 bd27d8 # 5 instructions; loads
spin main bdd678 bdd68c # 6 instructions; loads
spin main bf2ccc bf2ce0 # 6 instructions; loads
spin main bf2e24 bf2e38 # 6 instructions; loads
spin main bf32a0 bf32b4 # 6 instructions; loads
spin main bf3730 bf3744 # 6 instructions; loads
spin main c0a554 c0a568 # 6 instructions; loads
spin main c0a6b4 c0a6c8 # 6 instructions; loads
spin main c0a850 c0a864 # 6 instructions; loads
spin main c0d758 c0d76c # 6 instructions; loads
spin main c0d8b0 c0d8c4 # 6 instructions; loads
spin main c0dd3c c0dd50 # 6 instructions; loads
spin main c15ffc c16010 # 6 instructions; loads
spin main c1665c c16670 # 6 instructions; loads
spin main c2315c c23170 # 6 instructions; loads
spin main c2374c c23760 # 6 instructions; loads
spin main c23998 c239ac # 6 instructions; loads
spin main c2d128 c2d13c # 6 instructions; loads
spin main c3a9cc c3a9e0 # 6 instructions; loads
spin main c5ab4c c5ab60 # 6 instructions; loads
spin main c70388 c7039c # 6 instructions; loads
spin main c703b0 c703c4 # 6 instructions; loads
spin main c94f70 c94f84 # 6 instructions; loads
spin main c95a00 c95a14 # 6 instructions; loads
spin main c96764 c96778 # 6 instructions; loads
spin main c96948 c9695c # 6 instructions; loads
spin main c96ce4 c96cf8 # 6 instructions; loads
spin main c96d54 c96d68 # 6 instructions; loads
spin main c97400 c97414 # 6 instructions; loads
spin main c97ca0 c97cb4 # 6 instructions; loads
spin main c99ba0 c99bb4 # 6 instructions; loads
spin main c99c90 c99ca4 # 6 instructions; loads
spin main c9abac c9abc0 # 6 instructions; loads
spin main c9ac8c c9aca0 # 6 instructions; loads
spin main c9b2e0 c9b2f4 # 6 instructions; loads
spin main c9d9f4 c9da08 # 6 instructions; loads
spin main c9db3c c9db50 # 6 instructions; loads
spin main c9df14 c9df30 # 8 instructions; loads
spin main ccaaa8 ccaabc # 6 instructions; loads
spin main cd38a4 cd38b8 # 6 instructions; loads
spin main cda7c4 cda7d8 # 6 instructions; loads
spin main cdb388 cdb39c # 6 instructions; loads
spin main cdb3cc cdb3e0 # 6 instructions; loads
spin main cdbab4 cdbac8 # 6 instructions; loads
spin main cdbaf8 cdbb0c # 6 instructions; loads
spin main cdbce4 cdbcf8 # 6 instructions; loads
spin main cdce0c cdce20 # 6 instructions; loads
spin main cecedc cecef0 # 6 instructions; loads
spin main d0fbb4 d0fbc8 # 6 instructions; loads
spin main d1acd0 d1ace4 # 6 instructions; loads
spin main d1dc54 d1dc60 d1dcac d1dcc4 # 29 instructions; loads
spin main d2273c d22750 # 6 instructions; loads
spin main d231bc d231d0 # 6 instructions; loads
spin main d23398 d233ac # 6 instructions; loads
spin main d2e810 d2e824 # 6 instructions; loads
spin main d3d8dc d3d8f0 # 6 instructions; loads
spin main d4c3a8 d4c3bc # 6 instructions; loads
spin main d4ffc4 d4ffd8 # 6 instructions; loads
spin main d50a78 d50a8c # 6 instructions; loads
spin main d50b04 d50b18 # 6 instructions; loads
spin main d50bfc d50c10 # 6 instructions; loads
spin main d50e60 d50e74 # 6 instructions; loads
spin main d50e88 d50e9c # 6 instructions; loads
spin main d50ebc d50ed0 # 6 instructions; loads
spin main d510b8 d510cc # 6 instructions; loads
spin main d51afc d51b18 # 8 instructions; loads
spin main d52478 d5248c # 6 instructions; loads
spin main d527f4 d52808 # 6 instructions; loads
spin main d5283c d52850 # 6 instructions; loads
spin main d52864 d52878 # 6 instructions; loads
spin main d5288c d528a0 # 6 instructions; loads
spin main d528f8 d5290c # 6 instructions; loads
spin main d52920 d52934 # 6 instructions; loads
spin main d529a8 d529bc # 6 instructions; loads
spin main d52ab0 d52ac4 # 6 instructions; loads
spin main d91344 d91358 # 6 instructions; loads
spin main d91970 d91984 # 6 instructions; loads
spin main d91cc0 d91cd4 # 6 instructions; loads
spin main d91dc0 d91dd4 # 6 instructions; loads
spin main d92280 d92294 # 6 instructions; loads
spin main d923cc d923e0 # 6 instructions; loads
spin main d93600 d93614 # 6 instructions; loads
spin main d94100 d94114 # 6 instructions; loads
spin main d9412c d94140 # 6 instructions; loads
spin main d99098 d990ac # 6 instructions; loads
spin main d9c260 d9c298 # 8 instructions; loads
spin main d9c2c8 d9c300 # 8 instructions; loads
spin main d9fe34 d9fe48 # 6 instructions; loads
spin main da1eb4 da1ec8 # 6 instructions; loads
spin main da1edc da1ef0 # 6 instructions; loads
spin main da7010 da7024 # 6 instructions; loads
spin main da70f8 da710c # 6 instructions; loads
spin main dade1c dade30 # 6 instructions; loads
spin main db58dc db58f0 # 6 instructions; loads
spin main dbb0e4 dbb0f8 # 6 instructions; loads
spin main dbca30 dbca4c # 8 instructions; loads
spin main dbfc20 dbfc34 # 6 instructions; loads
spin main dbff00 dbff14 # 6 instructions; loads
spin main dc78fc dc7910 # 6 instructions; loads
spin main dcaa88 dcaa9c # 6 instructions; loads
spin main dcb2ac dcb2c0 # 6 instructions; loads
spin main dcb9c8 dcb9dc # 6 instructions; loads
spin main dd4a28 dd4a3c # 6 instructions; loads
spin main dd6414 dd6428 # 6 instructions; loads
spin main dd7498 dd74ac # 6 instructions; loads
spin main dd78b4 dd78c8 # 6 instructions; loads
spin main ddf338 ddf34c # 6 instructions; loads
spin main de7324 de7338 # 6 instructions; loads
spin main deb218 deb22c # 6 instructions; loads
spin main df249c df24b0 # 6 instructions; loads
spin main df2930 df2944 # 6 instructions; loads
spin main dfab0c dfab20 # 6 instructions; loads
spin main e1a248 e1a25c # 6 instructions; loads
spin main e27b10 e27b24 # 6 instructions; loads
spin main e28428 e2843c # 6 instructions; loads
spin main e38cac e38cc0 # 6 instructions; loads
spin main e38f90 e38fa4 # 6 instructions; loads
spin main e39274 e39288 # 6 instructions; loads
spin main e39558 e3956c # 6 instructions; loads
spin main e40b1c e40b30 # 6 instructions; loads
spin main e41478 e4148c # 6 instructions; loads
spin main e450bc e450d0 # 6 instructions; loads
spin main e45400 e45414 # 6 instructions; loads
spin main e51aa0 e51ab4 # 6 instructions; loads
spin main e51c08 e51c1c # 6 instructions; loads
spin main e51cf4 e51d08 # 6 instructions; loads
spin main e8e42c e8e440 # 6 instructions; loads
spin main e90628 e9063c # 6 instructions; loads
spin main e930d8 e930ec # 6 instructions; loads
spin main e94c64 e94c78 # 6 instructions; loads
spin main e9557c e95590 # 6 instructions; loads
spin main e966e4 e966f8 # 6 instructions; loads
spin main e9711c e97130 # 6 instructions; loads
spin main e97164 e97178 # 6 instructions; loads
spin main e9c134 e9c148 # 6 instructions; loads
spin main ea4c28 ea4c3c # 6 instructions; loads
spin main ea4c78 ea4c8c # 6 instructions; loads
spin main ea4cc4 ea4cd8 # 6 instructions; loads
spin main ea4d10 ea4d24 # 6 instructions; loads
spin main eb07cc eb07d4 # 3 instructions; loads
spin main eb07dc eb07e0 # 2 instructions; loads
spin main eb0808 eb0810 # 3 instructions; loads
spin main eb1548 eb155c # 6 instructions; loads
spin main eb4240 eb4254 # 6 instructions; loads
spin main eb5aa8 eb5abc # 6 instructions; loads
spin main eb637c eb6390 # 6 instructions; loads
spin main eb7954 eb7968 # 6 instructions; loads
spin main ebf63c ebf650 # 6 instructions; loads
spin main ebfb5c ebfb70 # 6 instructions; loads
spin main ebfb98 ebfbac # 6 instructions; loads
spin main ebfd50 ebfd64 # 6 instructions; loads
spin main ebffb4 ebffc8 # 6 instructions; loads
spin main eca534 eca548 # 6 instructions; loads
spin main eca6f8 eca70c # 6 instructions; loads
spin main ecb088 ecb09c # 6 instructions; loads
spin main ed26e8 ed26fc # 6 instructions; loads
spin main ed293c ed2950 # 6 instructions; loads
spin main ed2ab0 ed2ac4 # 6 instructions; loads
spin main ed2c24 ed2c38 # 6 instructions; loads
spin main ed2ed0 ed2ee4 # 6 instructions; loads
spin main ed3348 ed335c # 6 instructions; loads
spin main ed351c ed3530 # 6 instructions; loads
spin main ed3cf4 ed3d08 # 6 instructions; loads
spin main ed42e0 ed42f4 # 6 instructions; loads
spin main ed430c ed4320 # 6 instructions; loads
spin main ed5294 ed52a8 # 6 instructions; loads
spin main ed5658 ed566c # 6 instructions; loads
spin main ed5680 ed5694 # 6 instructions; loads
spin main ed79e8 ed79fc # 6 instructions; loads
spin main ed7a28 ed7a3c # 6 instructions; loads
spin main ed7a50 ed7a64 # 6 instructions; loads
spin main ed7a78 ed7a8c # 6 instructions; loads
spin main ed80ac ed80c0 # 6 instructions; loads
spin main ed80d4 ed80e8 # 6 instructions; loads
spin main ed934c ed9360 # 6 instructions; loads
spin main ed94a0 ed94b4 # 6 instructions; loads
spin main ed9a50 ed9a64 # 6 instructions; loads
spin main ede7fc ede810 # 6 instructions; loads
spin main ee0ce4 ee0cf8 # 6 instructions; loads
spin main eebd28 eebd3c # 6 instructions; loads
spin main ef9ed4 ef9ee8 # 6 instructions; loads
spin main efa7a4 efa7b8 # 6 instructions; loads
spin main efa7d4 efa7e8 # 6 instructions; loads
spin main efad94 efada8 # 6 instructions; loads
spin main efadc4 efadd8 # 6 instructions; loads
spin main efdac8 efdadc # 6 instructions; loads
spin main f01fa4 f01fb8 # 6 instructions; loads
spin main f022e4 f022f8 # 6 instructions; loads
spin main f02394 f023a8 # 6 instructions; loads
spin main f02434 f02448 # 6 instructions; loads
spin main f024e4 f024f8 # 6 instructions; loads
spin main f3c87c f3c890 # 6 instructions; loads
spin main f3d54c f3d560 # 6 instructions; loads
spin main f3dba8 f3dbbc # 6 instructions; loads
spin main f3dbd4 f3dbe8 # 6 instructions; loads
spin main f547a4 f547b8 # 6 instructions; loads
spin main f54d60 f54d74 # 6 instructions; loads
spin main f57494 f574a8 # 6 instructions; loads
spin main f66ef8 f66f0c # 6 instructions; loads
spin main f66f20 f66f34 # 6 instructions; loads
spin main f67170 f67184 # 6 instructions; loads
spin main f671fc f67210 # 6 instructions; loads
spin main f67224 f67238 # 6 instructions; loads
spin main f67254 f67268 # 6 instructions; loads
spin main f684f0 f68504 # 6 instructions; loads
spin main f6a1ec f6a200 # 6 instructions; loads
spin main f6a3f8 f6a40c # 6 instructions; loads
spin main f6afc4 f6afd8 # 6 instructions; loads
spin main f712a4 f712b8 # 6 instructions; loads
spin main f71400 f71414 # 6 instructions; loads
spin main f7155c f71570 # 6 instructions; loads
spin main f716b8 f716cc # 6 instructions; loads
spin main f729dc f729f0 # 6 instructions; loads
spin main f73084 f73098 # 6 instructions; loads
spin main f73148 f7315c # 6 instructions; loads
spin main f76574 f76588 # 6 instructions; loads
spin main f7c8ec f7c900 # 6 instructions; loads
spin main f83bd0 f83be4 # 6 instructions; loads
spin main f870f0 f87104 # 6 instructions; loads
spin main f94eac f94ec0 # 6 instructions; loads
spin main fa6580 fa6594 # 6 instructions; loads
spin main fa65a8 fa65bc # 6 instructions; loads
spin main fabb50 fabb64 # 6 instructions; loads
spin main fabe74 fabe88 # 6 instructions; loads
spin main fac8a0 fac8b4 # 6 instructions; loads
spin main fac8c4 fac8d8 # 6 instructions; loads
spin main face58 face6c # 6 instructions; loads
spin main fc0a28 fc0a3c # 6 instructions; loads
spin main fc0c68 fc0c7c # 6 instructions; loads
spin main fc0e8c fc0ea0 # 6 instructions; loads
spin main fc1054 fc1068 # 6 instructions; loads
spin main fc1178 fc118c # 6 instructions; loads
spin main fcb18c fcb1a8 # 8 instructions; loads
spin main fccfe8 fccffc # 6 instructions; loads
spin main fcd554 fcd640 # 4 instructions; loads
spin main fcdc5c fcdce4 # 4 instructions; loads
spin main fd2d30 fd2d44 # 6 instructions; loads
spin main fd6250 fd6264 # 6 instructions; loads
spin main fdf818 fdf82c # 6 instructions; loads
spin main fdfb30 fdfb44 # 6 instructions; loads
spin main fe0fa8 fe0fbc # 6 instructions; loads
spin main fe1ad8 fe1aec # 6 instructions; loads
spin main fea3a0 fea3b4 # 6 instructions; loads
spin main fead08 fead1c # 6 instructions; loads
spin main feb168 feb17c # 6 instructions; loads
spin main feb1d0 feb1e4 # 6 instructions; loads
spin main ff2af8 ff2b0c # 6 instructions; loads
spin main ff5680 ff5694 # 6 instructions; loads
spin main ff58e0 ff58f4 # 6 instructions; loads
spin main ff9660 ff9674 # 6 instructions; loads
spin main ffacec ffad00 # 6 instructions; loads
spin main 1001500 1001514 # 6 instructions; loads
spin main 1002c28 1002c3c # 6 instructions; loads
spin main 1010b3c 1010b50 # 6 instructions; loads
spin main 1010ef4 1010f08 # 6 instructions; loads
spin main 1011244 1011258 # 6 instructions; loads
spin main 1011544 1011558 # 6 instructions; loads
spin main 1011a9c 1011ab0 # 6 instructions; loads
spin main 1011e34 1011e48 # 6 instructions; loads
spin main 10121b4 10121c8 # 6 instructions; loads
spin main 1012228 101223c # 6 instructions; loads
spin main 1013150 1013164 # 6 instructions; loads
spin main 101349c 10134b0 # 6 instructions; loads
spin main 1013920 1013934 # 6 instructions; loads
spin main 1013b6c 1013b80 # 6 instructions; loads
spin main 1014950 1014964 # 6 instructions; loads
spin main 10149e8 10149fc # 6 instructions; loads
spin main 1015bac 1015bc0 # 6 instructions; loads
spin main 10160c0 10160d4 # 6 instructions; loads
spin main 10166f8 101670c # 6 instructions; loads
spin main 10167b4 10167c8 # 6 instructions; loads
spin main 1016870 1016884 # 6 instructions; loads
spin main 101692c 1016940 # 6 instructions; loads
spin main 10170f0 1017104 # 6 instructions; loads
spin main 10191dc 10191f0 # 6 instructions; loads
spin main 101948c 10194a0 # 6 instructions; loads
spin main 1019778 101978c # 6 instructions; loads
spin main 1019a2c 1019a40 # 6 instructions; loads
spin main 1019cf0 1019d04 # 6 instructions; loads
spin main 1019ff4 101a008 # 6 instructions; loads
spin main 101a054 101a068 # 6 instructions; loads
spin main 101b594 101b5b0 # 8 instructions; loads
spin main 101c7b4 101c7c8 # 6 instructions; loads
spin main 101c7fc 101c810 # 6 instructions; loads
spin main 101c844 101c858 # 6 instructions; loads
spin main 101c88c 101c8a0 # 6 instructions; loads
spin main 101c8d4 101c8e8 # 6 instructions; loads
spin main 1023190 10231a4 # 6 instructions; loads
spin main 10461ac 10461c0 # 6 instructions; loads
spin main 1047278 104728c # 6 instructions; loads
spin main 1050a8c 1050aa0 # 6 instructions; loads
spin main 1055074 10550bc # 3 instructions; loads
spin main 1056bd4 1056be8 # 6 instructions; loads
spin main 106037c 1060390 # 6 instructions; loads
spin main 1066bf4 1066c08 # 6 instructions; loads
spin main 106bc38 106bc4c # 6 instructions; loads
spin main 106be80 106be94 # 6 instructions; loads
spin main 1070220 1070234 # 6 instructions; loads
spin main 1071264 1071278 # 6 instructions; loads
spin main 1071788 107179c # 6 instructions; loads
spin main 1073910 1073924 # 6 instructions; loads
spin main 10815e0 10815f4 # 6 instructions; loads
spin main 108cb28 108cb3c # 6 instructions; loads
spin main 1095678 109568c # 6 instructions; loads
spin main 10956c0 10956d4 # 6 instructions; loads
spin main 109f134 109f148 # 6 instructions; loads
spin main 10bc820 10bc834 # 6 instructions; loads
spin main 10be4c8 10be4dc # 6 instructions; loads
spin main 10be4f0 10be504 # 6 instructions; loads
spin main 10c2d4c 10c2d60 # 6 instructions; loads
spin main 10c3748 10c375c # 6 instructions; loads
spin main 10d5ef8 10d5f0c # 6 instructions; loads
spin main 10da208 10da21c # 6 instructions; loads
spin main 10e3edc 10e3ef0 # 6 instructions; loads
spin main 10e5ee4 10e5ef8 # 6 instructions; loads
spin main 10ec860 10ec874 # 6 instructions; loads
spin main 10ec8dc 10ec8f0 # 6 instructions; loads
spin main 10f1db4 10f1dc8 # 6 instructions; loads
spin main 10ff3b4 10ff3c8 # 6 instructions; loads
spin main 10ff788 10ff79c # 6 instructions; loads
spin main 10ffef0 10fff04 # 6 instructions; loads
spin main 1100500 1100514 # 6 instructions; loads
spin main 1103e80 1103e94 # 6 instructions; loads
spin main 110ca34 110cb68 # 4 instructions; loads
spin main 110cab8 110cb4c # 4 instructions; loads
spin main 1117420 1117434 # 6 instructions; loads
spin main 1117448 111745c # 6 instructions; loads
spin main 111bf5c 111bf70 # 6 instructions; loads
spin main 111c1bc 111c1d0 # 6 instructions; loads
spin main 111c918 111c92c # 6 instructions; loads
spin main 112aef0 112af04 # 6 instructions; loads
spin main 1143e28 1143e3c # 6 instructions; loads
spin main 1145110 1145124 # 6 instructions; loads
spin main 1145138 114514c # 6 instructions; loads
spin main 1149244 1149258 # 6 instructions; loads
spin main 114b868 114b87c # 6 instructions; loads
spin main 114f850 114f864 # 6 instructions; loads
spin main 114fcdc 114fcf0 # 6 instructions; loads
spin main 1150cbc 1150cd0 # 6 instructions; loads
spin main 115aa38 115aa4c # 6 instructions; loads
spin main 117d7c8 117d7dc # 6 instructions; loads
spin main 118a000 118a014 # 6 instructions; loads
spin main 118a3d8 118a3ec # 6 instructions; loads
spin main 118f65c 118f670 # 6 instructions; loads
spin main 1193d30 1193d44 # 6 instructions; loads
spin main 1198130 1198144 # 6 instructions; loads
spin main 1198158 119816c # 6 instructions; loads
spin main 1198e48 1198e5c # 6 instructions; loads
spin main 119c414 119c428 # 6 instructions; loads
spin main 11a0778 11a078c # 6 instructions; loads
spin main 11a0fcc 11a0fe0 # 6 instructions; loads
spin main 11a5884 11a5898 # 6 instructions; loads
spin main 11bbd80 11bbd94 # 6 instructions; loads
spin main 11bd154 11bd168 # 6 instructions; loads
spin main 11bd17c 11bd190 # 6 instructions; loads
spin main 11e5fc8 11e5fdc # 6 instructions; loads
spin main 11e5ff0 11e6004 # 6 instructions; loads
spin main 11ea5b8 11ea5cc # 6 instructions; loads
spin main 11eccf8 11ecd0c # 6 instructions; loads
spin main 11f50d8 11f50ec # 6 instructions; loads
spin main 11f5624 11f5638 # 6 instructions; loads
spin main 12010e8 12010fc # 6 instructions; loads
spin main 120ecc0 120ecd4 # 6 instructions; loads
spin main 12171d8 12171ec # 6 instructions; loads
spin main 1217cd8 1217cec # 6 instructions; loads
spin main 1219124 1219138 # 6 instructions; loads
spin main 1231ca8 1231cbc # 6 instructions; loads
spin main 1231e58 1231e6c # 6 instructions; loads
spin main 1234784 1234798 # 6 instructions; loads
spin main 1234d14 1234d28 # 6 instructions; loads
spin main 1236974 1236988 # 6 instructions; loads
spin main 1236b1c 1236b30 # 6 instructions; loads
spin main 12479cc 12479e0 # 6 instructions; loads
spin main 1261af8 1261b0c # 6 instructions; loads
spin main 1261f20 1261f34 # 6 instructions; loads
spin main 1262200 1262214 # 6 instructions; loads
spin main 1268448 126845c # 6 instructions; loads
spin main 1283a80 1283a94 # 6 instructions; loads
spin main 128ba28 128ba3c # 6 instructions; loads
spin main 128ba90 128baa4 # 6 instructions; loads
spin main 128bc40 128bc54 # 6 instructions; loads
spin main 128bc6c 128bc80 # 6 instructions; loads
spin main 128bc98 128bcac # 6 instructions; loads
spin main 128bcc4 128bcd8 # 6 instructions; loads
spin main 128c468 128c47c # 6 instructions; loads
spin main 128c490 128c4a4 # 6 instructions; loads
spin main 12924ec 1292500 # 6 instructions; loads
spin main 1292e4c 1292e60 # 6 instructions; loads
spin main 1292eb4 1292ec8 # 6 instructions; loads
spin main 129458c 12945a0 # 6 instructions; loads
spin main 12966e0 12966f4 # 6 instructions; loads
spin main 129e62c 129e640 # 6 instructions; loads
spin main 129f954 129f968 # 6 instructions; loads
spin main 12a61b0 12a61c4 # 6 instructions; loads
spin main 12ab4cc 12ab4e0 # 6 instructions; loads
spin main 12ab4f4 12ab508 # 6 instructions; loads
spin main 12c1228 12c123c # 6 instructions; loads
spin main 12c1250 12c1264 # 6 instructions; loads
spin main 12ca068 12ca07c # 6 instructions; loads
spin main 12d040c 12d0420 # 6 instructions; loads
spin main 12e3820 12e3834 # 6 instructions; loads
spin main 12e8e80 12e8e94 # 6 instructions; loads
spin main 12e9640 12e9654 # 6 instructions; loads
spin main 1309bc8 1309bdc # 6 instructions; loads
spin main 1314ed8 1314eec # 6 instructions; loads
spin main 131553c 1315550 # 6 instructions; loads
spin main 1322ccc 1322dac # 11 instructions; loads
spin main 13277f4 1327808 # 6 instructions; loads
spin main 1327ab8 1327acc # 6 instructions; loads
spin main 132a1e0 132a1f4 # 6 instructions; loads
spin main 132b0a8 132b0bc # 6 instructions; loads
spin main 132b87c 132b890 # 6 instructions; loads
spin main 1332fc0 1332fd4 # 6 instructions; loads
spin main 1363450 1363464 # 6 instructions; loads
spin main 13feca8 13fecbc # 6 instructions; loads
spin main 13fee40 13fee54 # 6 instructions; loads
spin main 157c12c 157c140 # 6 instructions; loads
spin main 157dfe0 157dff4 # 6 instructions; loads
spin main 1585a20 1585a34 # 6 instructions; loads
spin main 1585ee0 1585ef4 # 6 instructions; loads
spin main 158c4c8 158c4dc # 6 instructions; loads
spin main 1599a14 1599a28 # 6 instructions; loads
spin main 159aed0 159aee4 # 6 instructions; loads
spin main 159aef8 159af0c # 6 instructions; loads
spin main 15a2a20 15a2a34 # 6 instructions; loads
spin main 15a3228 15a323c # 6 instructions; loads
spin main 15a34e8 15a34fc # 6 instructions; loads
spin main 15a3510 15a3524 # 6 instructions; loads
spin main 15a3f44 15a3f58 # 6 instructions; loads
spin main 15ae678 15ae68c # 6 instructions; loads
spin main 15b8d78 15b8d8c # 6 instructions; loads
spin main 15b9218 15b922c # 6 instructions; loads
spin main 15b9320 15b9334 # 6 instructions; loads
spin main 15b9638 15b964c # 6 instructions; loads
spin main 15c6c9c 15c6cb0 # 6 instructions; loads
spin main 15d5380 15d5394 # 6 instructions; loads
spin main 15e8a0c 15e8a20 # 6 instructions; loads
spin main 15e9ad4 15e9ae8 # 6 instructions; loads
spin main 15e9b7c 15e9b90 # 6 instructions; loads
spin main 15ef1c4 15ef1d8 # 6 instructions; loads
spin main 15ef304 15ef318 # 6 instructions; loads
spin main 15ef32c 15ef340 # 6 instructions; loads
spin main 1607238 160724c # 6 instructions; loads
spin main 160779c 16077b0 # 6 instructions; loads
spin main 16173e0 16173f4 # 6 instructions; loads
spin main 1623748 162375c # 6 instructions; loads
spin main 1627bdc 1627bf0 # 6 instructions; loads
spin main 1627c2c 1627c40 # 6 instructions; loads
spin main 1628eb8 1628ecc # 6 instructions; loads
spin main 16298a8 16298bc # 6 instructions; loads
spin main 16340f8 163410c # 6 instructions; loads
spin main 1636748 163675c # 6 instructions; loads
spin main 1636ed4 1636ee8 # 6 instructions; loads
spin main 163b7f0 163b804 # 6 instructions; loads
spin main 1648354 1648368 # 6 instructions; loads
spin main 1651478 165148c # 6 instructions; loads
spin main 1653730 1653744 # 6 instructions; loads
spin main 1656c5c 1656c70 # 6 instructions; loads
spin main 16596b0 16596b8 # 3 instructions; loads
spin main 16596c0 16596c4 # 2 instructions; loads
spin main 16596f8 1659700 # 3 instructions; loads
spin main 165cb68 165cb7c # 6 instructions; loads
spin main 1677368 167737c # 6 instructions; loads
spin main 16dc224 16dc238 # 6 instructions; loads
spin main 173d61c 173d630 # 6 instructions; loads
spin main 1747354 1747368 # 6 instructions; loads
spin main 1747918 174792c # 6 instructions; loads
spin main 17a0a20 17a0a34 # 6 instructions; loads
spin main 1932774 1932788 # 6 instructions; loads
spin main 1933ea8 1933ebc # 6 instructions; loads
spin main 1962e88 1962e90 # 3 instructions; loads
spin main 1962e98 1962e9c # 2 instructions; loads
spin main 1962f20 1962f28 # 3 instructions; loads
spin main 196300c 1963014 # 3 instructions; loads
spin main 196301c 1963020 # 2 instructions; loads
spin main 19630c8 19630d0 # 3 instructions; loads
spin main 1975850 1975858 # 3 instructions; loads
spin main 1975860 1975864 # 2 instructions; loads
spin main 1975888 1975890 # 3 instructions; loads
spin main 19758c0 19758c8 # 3 instructions; loads
spin main 19758d0 19758d4 # 2 instructions; loads
spin main 19758fc 1975904 # 3 instructions; loads
spin main 19e5048 19e505c # 6 instructions; loads
spin main 1a8b8b4 1a8b8c4 # 5 instructions; loads
spin main 1b65f9c 1b65fb0 # 6 instructions; loads
spin main 1b66058 1b6606c # 6 instructions; loads
spin main 1b66114 1b66128 # 6 instructions; loads
spin main 1b66714 1b66728 # 6 instructions; loads
spin main 1b667d0 1b667e4 # 6 instructions; loads
spin main 1b6688c 1b668a0 # 6 instructions; loads
spin main 1dff370 1dff37c # 4 instructions; loads
spin main 21e6508 21e6530 # 4 instructions; loads
spin main 222a3e0 222a3fc # 8 instructions; loads
spin main 22322d0 22322d8 # 3 instructions; loads
spin main 2232358 2232360 # 3 instructions; loads
spin main 22323e8 2232410 # 3 instructions; loads
spin main 223931c 223932c # 5 instructions; loads
spin main 225dc10 225dc18 # 3 instructions; loads
spin main 225dc20 225dc24 # 2 instructions; loads
spin main 225dcac 225dcb4 # 3 instructions; loads
spin main 225dd38 225dd40 # 3 instructions; loads
spin main 225dd48 225dd4c # 2 instructions; loads
spin main 225dd64 225dd6c # 3 instructions; loads
spin main 22b72dc 22b72f0 # 6 instructions; loads
spin main 22b7440 22b7454 # 6 instructions; loads
spin main 22c60ac 22c60d0 # 4 instructions; loads
spin main 22c619c 22c61a8 # 4 instructions; loads
spin main 233ce40 233ce54 # 6 instructions; loads
spin main 240ccb0 240ccb8 # 3 instructions; loads
spin main 240ccc0 240ccc4 # 2 instructions; loads
spin main 240cd40 240cd48 # 3 instructions; loads
spin main 240d57c 240d584 # 3 instructions; loads
spin main 240d58c 240d590 # 2 instructions; loads
spin main 240d5fc 240d604 # 3 instructions; loads
spin main 240da9c 240daa4 # 3 instructions; loads
spin main 240daac 240dab0 # 2 instructions; loads
spin main 240db3c 240db44 # 3 instructions; loads
spin main 240dc80 240dc88 # 3 instructions; loads
spin main 240dc90 240dc94 # 2 instructions; loads
spin main 240dce8 240dcf0 # 3 instructions; loads
spin main 240dd34 240dd3c # 3 instructions; loads
spin main 240dd44 240dd48 # 2 instructions; loads
spin main 240ddfc 240de04 # 3 instructions; loads
spin main 240de0c 240de10 # 2 instructions; loads
spin main 240de50 240de58 # 3 instructions; loads
spin main 240de80 240de88 # 3 instructions; loads
spin main 240df74 240df7c # 3 instructions; loads
spin main 240df84 240df88 # 2 instructions; loads
spin main 240e068 240e070 # 3 instructions; loads
spin main 240e0fc 240e104 # 3 instructions; loads
spin main 240e10c 240e110 # 2 instructions; loads
spin main 240e1c4 240e1cc # 3 instructions; loads
spin main 240e1e4 240e1e8 # 2 instructions; loads
spin main 240e288 240e290 # 3 instructions; loads
spin main 240e2b8 240e2c0 # 3 instructions; loads
spin main 240e300 240e308 # 3 instructions; loads
spin main 240e310 240e314 # 2 instructions; loads
spin main 240e338 240e340 # 3 instructions; loads
spin main 240e384 240e38c # 3 instructions; loads
spin main 240e394 240e398 # 2 instructions; loads
spin main 240e3c0 240e3c8 # 3 instructions; loads
spin main 240e91c 240e924 # 3 instructions; loads
spin main 240e92c 240e930 # 2 instructions; loads
spin main 240e954 240e95c # 3 instructions; loads
spin main 240f088 240f090 # 3 instructions; loads
spin main 240f098 240f09c # 2 instructions; loads
spin main 240f0c0 240f0c8 # 3 instructions; loads
spin main 2410984 241098c # 3 instructions; loads
spin main 2410994 2410998 # 2 instructions; loads
spin main 2410a18 2410a20 # 3 instructions; loads
spin main 2410a40 2410a48 # 3 instructions; loads
spin main 2410a50 2410a54 # 2 instructions; loads
spin main 2410ad4 2410adc # 3 instructions; loads
spin main 2410e70 2410e78 # 3 instructions; loads
spin main 2410e80 2410e84 # 2 instructions; loads
spin main 2410ed4 2410edc # 3 instructions; loads
spin main 2410f1c 2410f24 # 3 instructions; loads
spin main 2410f2c 2410f30 # 2 instructions; loads
spin main 2410fcc 2410fd4 # 3 instructions; loads
spin main 241102c 2411034 # 3 instructions; loads
spin main 241103c 2411040 # 2 instructions; loads
spin main 24110c4 24110cc # 3 instructions; loads
spin main 2413d28 2413d30 # 3 instructions; loads
spin main 2413d38 2413d3c # 2 instructions; loads
spin main 2413dc8 2413dd0 # 3 instructions; loads
spin main 2413e04 2413e0c # 3 instructions; loads
spin main 2413e14 2413e18 # 2 instructions; loads
spin main 2413e84 2413e8c # 3 instructions; loads
spin main 2416358 2416360 # 3 instructions; loads
spin main 2416368 241636c # 2 instructions; loads
spin main 2416388 2416390 # 3 instructions; loads
spin main 2462470 2462484 # 6 instructions; loads
spin main 2462498 24624ac # 6 instructions; loads
spin main 24651f4 2465208 # 6 instructions; loads
spin main 2465688 246569c # 6 instructions; loads
spin main 2474d20 2474d34 # 6 instructions; loads
spin main 2475154 2475168 # 6 instructions; loads
spin main 2479134 2479148 # 6 instructions; loads
spin main 2479b7c 2479b90 # 6 instructions; loads
spin main 247a378 247a38c # 6 instructions; loads
spin main 2493050 2493064 # 6 instructions; loads
spin main 2886500 2886514 # 6 instructions; loads
spin main 28865bc 28865d0 # 6 instructions; loads
spin main 2886678 288668c # 6 instructions; loads
spin main 2887484 2887498 # 6 instructions; loads
spin main 2887540 2887554 # 6 instructions; loads
spin main 28875fc 2887610 # 6 instructions; loads
spin main 28876b8 28876cc # 6 instructions; loads
spin main 2887774 2887788 # 6 instructions; loads
spin main 2887830 2887844 # 6 instructions; loads
spin main 28878ec 2887900 # 6 instructions; loads
spin main 28879a8 28879bc # 6 instructions; loads
spin main 2887e70 2887e84 # 6 instructions; loads
spin main 2887f2c 2887f40 # 6 instructions; loads
spin main 2a3e540 2a3e548 # 3 instructions; loads
spin main 2a3e550 2a3e554 # 2 instructions; loads
spin main 2a3e58c 2a3e594 # 3 instructions; loads
# subsdk0: 8 spin waits
spin subsdk0 2090 20a4 # 6 instructions; loads
spin subsdk0 278b4 278c8 # 6 instructions; loads
spin subsdk0 bcf28 bcfd0 bcfd8 # 14 instructions; loads
spin subsdk0 bd0d0 bd178 bd180 # 14 instructions; loads
spin subsdk0 c03a0 c03b4 # 6 instructions; loads
spin subsdk0 1bbefc 1bbf08 # 4 instructions; loads
spin subsdk0 1bd838 1bd84c # 6 instructions; loads
spin subsdk0 1c0c68 1c0c74 # 4 instructions; loads
# sdk: 5 spin waits
spin sdk f5218 f5350 # 14 instructions; loads
spin sdk 23a590 23a5d8 # 19 instructions; loads
spin sdk 23a658 23a6a0 # 19 instructions; loads
spin sdk 23a7fc 23a918 # 19 instructions; loads
spin sdk 3d3250 3d3270 # 9 instructions; loads
+17
View File
@@ -32,6 +32,12 @@ site realtime main 8f2398 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (g
site realtime main 8f23a8 5e21d800 d503201f # scvtf s0, s0 -> nop
site realtime main 8f23ac 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Equipment-switch cooldown (issue #18, found by JieML1100 on 1.4.3): the player update (main+0x852d24) steps the
# countdown at player state +0x109c through the timer step (main+0xec98cc) only while it is above FLT_EPSILON, but the
# equipment gate (main+0xcae6f8) blocks weapon, shield and bow changes while it is above 0. A measured ratio can leave
# a remainder in (0, FLT_EPSILON] that is never stepped again; stepping while above 0 takes it below 0.
site realtime main 852d28 1e270101 1e2703e1 # fmov s1, w8 (FLT_EPSILON) -> fmov s1, wzr
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (main+0x89822c)
@@ -64,6 +70,17 @@ site scene main cc3f68 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
site scene main cc3f70 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
site scene main cc3f74 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches.c, the 1.2.1 sites of aspect_install;
# runtime/resolution.c res_init writes the output's width / height to page +56 and switches the set on). The same four
# sites as patches-v143.txt, with the same original words: two camera sites (update and setup) load the camera's copy of
# DefaultAspect 1.777778, two viewport routines divide width by height. The replacement loads the ratio from the page
# instead (Hor+). x10 is kept on the stack, so no register liveness is assumed.
set aspect TOTK_ASPECT_PATCH 0
site aspect main cb4b10 bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera update)
site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera setup)
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
spin main ac600 ac620 ac630 # 13 instructions; loads
+16
View File
@@ -32,6 +32,13 @@ site realtime main 2daa70 1e201820 d503201f # fdiv s0, s1, s0 -> nop
site realtime main 2daa74 1e2c1001 d503201f # fmov s1, #0.5 -> nop
site realtime main 2daa78 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Equipment-switch cooldown (issue #18, found by JieML1100): the player update (main+0x2b0368) steps the countdown at
# player state +0x109c through the timer step (main+0x1b0834, value += ratio * rate) only while it is above FLT_EPSILON,
# but the equipment gate (main+0x31cc7c) blocks weapon, shield and bow changes while it is above 0. Whole 30 Hz steps
# reach exactly 0; a measured ratio leaves a rounding remainder, and one in (0, FLT_EPSILON] is never stepped again and
# keeps equipment locked until a load. Stepping while above 0 takes the remainder below 0 like any other last step.
site realtime main 2b036c 0f016681 0f000401 # movi v1.2s, #0x34, lsl #24 (FLT_EPSILON) -> movi v1.2s, #0
# Hair and cloth: the actor animation update builds a second clock {seconds = factor / 30, ratio = factor} per
# rendered frame. 1.4.3 inlines it in two actor paths, where the factor was just stored to the ratio slot: the
# replacement stores measured ratio * factor there and computes measured seconds * factor. Two other callers share the
@@ -85,6 +92,15 @@ site aspect main b43460 bd406ac0 f81f0fea @page:x10 bd403940 f84107ea # ldr s0
site aspect main 1ff04f4 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
site aspect main 253fbb0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
# Field of view (runtime/resolution.c res_fov_live writes TOTK_FOV / 100 to page +60 and switches the set). The same two
# camera routines as the aspect set load the camera's vertical view angle (radians) a few words before the ratio and
# hand it to sead's perspective projection (+0xa0, then sin / cos / tan of half of it at +0xa4..+0xac). The replacement
# loads the angle as before and multiplies it by the factor on the page; s8 holds the factor because the next
# instruction (fmul s8, s0, s1) overwrites it. Zooms the game makes itself (the bow, the scope) scale with it.
set fov TOTK_FOV_PATCH 0
site fov main 628c3c bd413e60 f81f0fea @page:x10 bd413e60 bd403d48 1e280800 f84107ea # ldr s0, [x19, #0x13c] (camera update)
site fov main b4342c bd4066c0 f81f0fea @page:x10 bd4066c0 bd403d48 1e280800 f84107ea # ldr s0, [x22, #0x64] (camera setup)
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
+5 -1
View File
@@ -11,7 +11,11 @@ Lifted code cannot be byte-patched when the game runs, so a patch is applied whe
<address> is a module offset (hex), <original> the instruction word the module has there (checked; a mismatch stops
the generator), each <replacement> an instruction word (hex) or `@page:x<n>`, which is `adrp x<n>, <patch page>`
encoded for the site's address. The patch page is the first 4 KiB page after main's image (layout.py leaves the rest
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents.
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents. Words in use, by byte offset:
+0..15 the frame-rate step (runtime/frame_rate.c), +16..55 the output, swapchain and scene sizes
(runtime/resolution.c res_init), +56..59 the camera's aspect ratio (runtime/resolution.c res_init, set aspect), +60..63
the field-of-view factor (runtime/resolution.c res_fov_live, set fov). A new
set takes words outside these.
The replacement words are lifted at the site's address in place of the original instruction, and the generated C reads
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Replacement
+3 -7
View File
@@ -10,8 +10,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include "platform/platform.h"
#include "host.h"
#include "tex.h"
#include "vk.h"
@@ -112,11 +112,7 @@ static void to_general(VkCommandBuffer cb, VkImage img) {
vkCmdPipelineBarrier2(cb, &di);
}
static double now_ms(void) {
LARGE_INTEGER t, f;
QueryPerformanceCounter(&t), QueryPerformanceFrequency(&f);
return (double)t.QuadPart * 1e3 / (double)f.QuadPart;
}
static double now_ms(void) { return (double)plat_now_ns() / 1e6; }
// Decodes regions (w0 x h0 at level 0, w1 x h1 at level 1) of blk on the GPU; out_rb: the decoder's texels, out_img:
// the image's two levels read back. 0 on a Vulkan failure.
@@ -359,7 +355,7 @@ static void timing(VkCtx *c, uint32_t bw, uint32_t bh) {
}
int probe_astc(void) {
_putenv_s("TOTK_ASTC_BC", "1"); // also builds the BC3 encoder; the RGBA8 cases pass bc 0 and do not use it
plat_setenv("TOTK_ASTC_BC", "1"); // also builds the BC3 encoder; the RGBA8 cases pass bc 0 and do not use it
if (!vk_init()) { printf("probe-astc: FAIL vk_init\n"); return 1; }
VkCtx *c = vk_ctx();
printf("probe-astc: GPU %s, textureCompressionASTC_LDR %d, mode %d\n", c->props.deviceName, c->has_astc_ldr,
+23 -35
View File
@@ -5,7 +5,6 @@
// service validates every section against the header, keeps server-side voice state, advances it in real time (one
// frame of sample_count / sample_rate seconds per render tick, signalling the renderer's event) and writes the output
// status the client expects. The mixer behind it is audio_mixer.c.
#include <windows.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@@ -14,10 +13,11 @@
#include "audren.h"
#include "host.h"
#include "ipc.h"
#include "platform/platform.h"
static volatile long g_live;
static volatile int32_t g_live;
long audren_renderers_live(void) { return InterlockedCompareExchange(&g_live, 0, 0); }
long audren_renderers_live(void) { return plat_cas32(&g_live, 0, 0); }
static int revision_number(uint32_t rev) { return (int)(rev >> 24) - '0'; }
@@ -44,7 +44,7 @@ static AudioRenderer *renderer_new(const AudrenParameter *p) {
r->effects = calloc((size_t)(p->effect_count ? p->effect_count : 1), sizeof *r->effects);
r->channels = calloc((size_t)p->voice_count * 6, sizeof *r->channels);
need(r->voices && r->pools && r->mixes && r->sinks && r->effects && r->channels, "renderer tables");
InitializeSRWLock(&r->lock);
plat_mutex_init(&r->lock);
r->event = kern_event_new();
r->state = 1; // stopped
return r;
@@ -195,7 +195,7 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
memcpy(&h, input, sizeof h);
need(h.total <= in_size, "update total size");
Reader in = {input + sizeof h, input + h.total};
AcquireSRWLockExclusive(&r->lock);
plat_mutex_lock(&r->lock);
r->revision = h.revision;
const int vc = r->param.voice_count, ec = r->param.effect_count, sc = r->param.sink_count,
mc = r->param.submix_count + 1;
@@ -313,7 +313,7 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
w += 0x10; // performance: nothing written
w += AUDREN_BEHAVIOR_OUT; // behavior: no errors
memcpy(w, &r->elapsed_frames, 8);
ReleaseSRWLockExclusive(&r->lock);
plat_mutex_unlock(&r->lock);
}
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
@@ -349,36 +349,27 @@ static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds)
}
}
static void wait_ns(HANDLE timer, uint64_t ns) {
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)((ns + 99) / 100)}; // relative, 100 ns units
if (!SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE)) rt_fatal("audren: SetWaitableTimer failed (error %lu)", GetLastError());
WaitForSingleObject(timer, INFINITE);
}
static DWORD WINAPI render_thread(void *p) {
static void render_thread(void *p) {
AudioRenderer *r = p;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
HANDLE timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
if (!timer) timer = CreateWaitableTimerExW(NULL, NULL, 0, TIMER_ALL_ACCESS);
if (!timer) rt_fatal("audren: CreateWaitableTimerExW failed (error %lu)", GetLastError());
plat_thread_priority(PLAT_PRIO_HIGHEST);
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
const double frame = (double)r->param.sample_count / r->param.sample_rate;
uint64_t next = kern_now_ns();
while (InterlockedCompareExchange(&r->running, 0, 0)) {
while (plat_cas32(&r->running, 0, 0)) {
uint64_t now = kern_now_ns();
int64_t pace = audio_mixer_pace_ns(r, now);
if (pace > 0) {
wait_ns(timer, (uint64_t)pace);
plat_wait_ns((uint64_t)pace);
continue;
}
if (pace == 0) {
next = now + frame_ns; // where the timer goes on if the device stops pacing
} else {
if (next > now) wait_ns(timer, next - now);
if (next > now) plat_wait_ns(next - now);
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
next += frame_ns;
}
AcquireSRWLockExclusive(&r->lock);
plat_mutex_lock(&r->lock);
if (r->state == 0) {
voices_prepare(r);
audio_mixer_render(r, frame);
@@ -392,11 +383,9 @@ static DWORD WINAPI render_thread(void *p) {
(unsigned long long)r->flushed);
}
}
ReleaseSRWLockExclusive(&r->lock);
plat_mutex_unlock(&r->lock);
kern_event_signal(r->event);
}
CloseHandle(timer);
return 0;
}
// ---- IAudioRenderer
@@ -428,16 +417,16 @@ static void renderer_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *r
return;
}
case 5: // Start
AcquireSRWLockExclusive(&r->lock);
plat_mutex_lock(&r->lock);
r->state = 0;
audio_mixer_set_running(r, 1);
ReleaseSRWLockExclusive(&r->lock);
plat_mutex_unlock(&r->lock);
return;
case 6: // Stop
AcquireSRWLockExclusive(&r->lock);
plat_mutex_lock(&r->lock);
r->state = 1;
audio_mixer_set_running(r, 0);
ReleaseSRWLockExclusive(&r->lock);
plat_mutex_unlock(&r->lock);
return;
case 7: rs->copy[rs->ncopy++] = kern_event_readable(r->event); return; // QuerySystemEvent
case 8: r->time_limit = IPC_ARG(rq, uint32_t, 0); return; // SetRenderingTimeLimit
@@ -448,12 +437,11 @@ static void renderer_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *r
static void renderer_close(void *p) {
AudioRenderer *r = p;
InterlockedExchange(&r->running, 0);
plat_xchg32(&r->running, 0);
int away = kern_core_away();
WaitForSingleObject(r->thread, INFINITE);
plat_thread_join(r->thread);
kern_core_back(&away);
CloseHandle(r->thread);
InterlockedDecrement(&g_live);
plat_dec32(&g_live);
audio_mixer_close(r);
kern_handle_close(r->event);
rt_log("audren: renderer closed after %llu frames", (unsigned long long)r->elapsed_frames);
@@ -472,9 +460,9 @@ void audren_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs
AudioRenderer *r = renderer_new(&p);
r->running = 1;
audio_mixer_open(r);
r->thread = CreateThread(NULL, 0, render_thread, r, 0, NULL);
if (!r->thread) rt_fatal("audren: render thread CreateThread failed (error %lu)", GetLastError());
InterlockedIncrement(&g_live);
r->thread = plat_thread_new(render_thread, r, "audren");
if (!r->thread) rt_fatal("audren: cannot start the render thread");
plat_inc32(&g_live);
rt_log("audren: renderer open %d Hz x%d, mix buffers %d, voices %d, effects %d, submixes %d, sinks %d, "
"splitters %d/%d, REV%d, work buffer %#llx+%#llx",
p.sample_rate, p.sample_count, p.mix_buffer_count, p.voice_count, p.effect_count, p.submix_count,
+4 -4
View File
@@ -3,10 +3,10 @@
// audren:u, the audio renderer service: RequestUpdate wire formats, the server-side
// renderer state, and the mixer interface (audio_mixer.c: the real mixer with SDL3 output).
#pragma once
#include <windows.h>
#include <stdint.h>
#include "ipc.h"
#include "platform/platform.h"
typedef struct {
int32_t sample_rate, sample_count, mix_buffer_count, submix_count, voice_count, sink_count, effect_count,
@@ -150,9 +150,9 @@ enum { SV_STOPPED, SV_STARTED, SV_PAUSED, SV_STOPPING };
typedef struct AudioRenderer {
AudrenParameter param;
SRWLOCK lock;
HANDLE thread;
volatile long running;
PlatMutex lock;
PlatThread *thread;
volatile int32_t running;
uint32_t state, revision, time_limit, event; // state 0 started, 1 stopped; event: writable handle
int logged;
uint64_t behavior_flags, elapsed_frames;
+34 -36
View File
@@ -3,13 +3,13 @@
// da35b83 (MIT, Copyright (c) 2026 Peiikai).
// The boot driver: the guest main thread runs the game's own nn::init::Start
// with three host callbacks, the second of which runs DT_INIT of main, subsdk0 and sdk; Start then calls nnMain.
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call:
// SetFocusHandlingMode called from main+0xe7a954 (1.4.3: +0xbdb48c) in nnMain, every module's constructor flag
// reads 2
// nvnBootstrapLoader called from main+0xe7eb2c (1.4.3: +0xbe0360) in the graphics bring-up; logs the run
// statistics, exits 0
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call, at the return address of
// each in main (1.1.1, 1.2.1, 1.4.3):
// SetFocusHandlingMode main+0xeb8c14 (1.2.1: +0xe7a954, 1.4.3: +0xbdb48c) in nnMain, every module's constructor
// flag reads 2
// nvnBootstrapLoader main+0xebcdec (1.2.1: +0xe7eb2c, 1.4.3: +0xbe0360) in the graphics bring-up; logs the
// run statistics, exits 0
// With --seconds S the run continues past that call; rt_frame_presented ends it once the world clock passed S.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -18,23 +18,26 @@
#include "fs.h"
#include "gfx.h"
#include "nvn_api.h"
#include "platform/platform.h"
#include "vk.h"
#include "platform/platform.h"
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
// main: return addresses of nnMain's SetFocusHandlingMode call (1.2.1 from this port's analysis of main; 1.4.3 from
// MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : 0xe7eb2cull)
// main: return addresses of nnMain's SetFocusHandlingMode call (1.1.1 and 1.2.1 from this port's analysis of main;
// 1.4.3 from MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : RT_V111 ? 0xeb8c14ull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : RT_V111 ? 0xebcdecull : 0xe7eb2cull)
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
// The init_array sizes (1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check expects
// the same.
// The init_array sizes (1.1.1 and 1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check
// expects the same. The three tables carry the same rows, so cb_module_inits walks them by the size of one.
typedef struct { const char *name; uint64_t entries; } InitCount;
static const InitCount inits_111[] = {{"main", 10668}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_121[] = {{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_143[] = {{"main", 10717}, {"subsdk0", 45}, {"sdk", 83}};
#define expected_inits (RT_V143 ? inits_143 : inits_121)
#define expected_inits (RT_V143 ? inits_143 : RT_V111 ? inits_111 : inits_121)
static volatile LONG inits_done, checkpoint_seen;
static volatile int32_t inits_done, checkpoint_seen;
uint64_t rt_guest_start_ns;
static const RtLoaded *module_named(const char *name) {
@@ -71,7 +74,7 @@ static void cb_loader_ready(Cpu *c) {
// DT_INIT registers the module with musl (__nnmusl_init_dso) and runs its init_array; the constructor flag reads 2
// afterwards (MacNativeGames loader.c:458-462).
static void cb_module_inits(Cpu *c) {
if (InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
if (plat_cas32(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
for (unsigned k = 0; k < sizeof inits_121 / sizeof *inits_121; k++) {
const RtLoaded *m = module_named(expected_inits[k].name);
uint64_t entries = m->init_array_size / 8;
@@ -85,7 +88,7 @@ static void cb_module_inits(Cpu *c) {
rt_fatal("boot: %s has %llu init_array entries, this game version has %llu", m->name, (unsigned long long)entries,
(unsigned long long)expected_inits[k].entries);
}
InterlockedExchange(&inits_done, 1);
plat_xchg32(&inits_done, 1);
}
static void cb_module_finis(Cpu *c) {
@@ -109,12 +112,12 @@ void rt_boot_event(const char *what, Cpu *c, const char *detail) {
if (ret != main_mod->base + CHECKPOINT_RET)
rt_fatal("boot: checkpoint %s reached from %s, expected main+%#llx", detail, at,
(unsigned long long)CHECKPOINT_RET);
if (!InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: checkpoint %s before DT_INIT", detail);
if (!plat_cas32(&inits_done, 0, 0)) rt_fatal("boot: checkpoint %s before DT_INIT", detail);
for (int i = 0; i < ldr_count(); i++)
if (ctor_state(ldr_module(i)) != CTOR_DONE)
rt_fatal("boot: checkpoint %s: %s constructor flag reads %u", detail, ldr_module(i)->name,
ctor_state(ldr_module(i)));
InterlockedExchange(&checkpoint_seen, 1);
plat_xchg32(&checkpoint_seen, 1);
rt_logf("boot: checkpoint %s from main+%#llx\n", detail, (unsigned long long)(ret - main_mod->base));
return;
}
@@ -122,7 +125,7 @@ void rt_boot_event(const char *what, Cpu *c, const char *detail) {
if (ret != main_mod->base + NVN_RET)
rt_fatal("boot: first NVN call %s(\"%s\") from %s, expected main+%#llx", "nvnBootstrapLoader", detail, at,
(unsigned long long)NVN_RET);
if (!InterlockedCompareExchange(&checkpoint_seen, 0, 0))
if (!plat_cas32(&checkpoint_seen, 0, 0))
rt_fatal("boot: first NVN call before the SetFocusHandlingMode checkpoint");
rt_logf("boot: first NVN call nvnBootstrapLoader(\"%s\") from main+%#llx\n", detail,
(unsigned long long)(ret - main_mod->base));
@@ -131,7 +134,7 @@ void rt_boot_event(const char *what, Cpu *c, const char *detail) {
stats_line();
fflush(stdout);
fflush(stderr);
_exit(0); // other guest threads are still running; nothing of theirs needs an orderly end
plat_exit(0); // other guest threads are still running; nothing of theirs needs an orderly end
}
rt_fatal("boot: unknown boot event %s (%s) from %s", what, detail ? detail : "", at);
}
@@ -241,7 +244,7 @@ static void series_shot(const tex_t *t, uint64_t n) {
step = (unsigned long long)(ws * 1e6), next = lo;
else if (!e || sscanf(e, "%llu-%llu:%llu", &lo, &hi, &step) != 3 || !step) hi = 0;
dir = cfg_get("TOTK_SHOTS_DIR") ? cfg_get("TOTK_SHOTS_DIR") : ".";
if (hi) CreateDirectoryA(dir, NULL);
if (hi) plat_file_mkdir_p(dir);
parsed = 1;
}
if (world) { // the first present at or after each step of the world clock
@@ -275,7 +278,7 @@ static void series_shot(const tex_t *t, uint64_t n) {
for (int c = 0; c < 4; c++) o[c] = (uint8_t)(sum[bgra && c != 3 ? 2 - c : c] / k);
o[3] = 255;
}
char path[MAX_PATH];
char path[PLAT_PATH_MAX];
snprintf(path, sizeof path, "%s/f%05llu.png", dir, (unsigned long long)n);
rt_write_png(path, out, ow, oh);
free(out);
@@ -304,7 +307,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
vk_pipeline_save(); // no-op without the Vulkan back end
prof_write();
fflush(stderr);
_exit(0);
plat_exit(0);
}
// ---- steps 9-14
@@ -321,22 +324,22 @@ static void guest_main(Cpu *c) {
rt_fatal("boot: nn::init::Start returned");
}
static DWORD WINAPI prewarm_main(void *arg) {
static void prewarm_main(void *arg) {
(void)arg;
vk_pipeline_prewarm();
const char *prep = cfg_get("TOTK_TEST_PREPARE_SHADERS"); // Task 2.3 test hook; Task 2.6 builds the preparation
if (prep && !strcmp(prep, "1")) {
int cpus = (int)GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
int cpus = (int)plat_cpu_usable();
gfx_shader_prepare_recorded(cpus, cpus / 2 > 16 ? 16 : cpus / 2 < 1 ? 1 : cpus / 2);
}
return 0;
return;
}
void rt_boot(void) {
check_npdm();
if (!rt_config()->shot) vk_pipeline_async_enable(); // a --shot image must not depend on build timing
// Shaders and pipelines recorded in earlier runs are made ready while the modules load; the guest waits for them.
HANDLE warm = CreateThread(NULL, 0, prewarm_main, NULL, 0, NULL);
PlatThread *warm = plat_thread_new(prewarm_main, NULL, "prewarm");
ldr_load(rt_config()->exefs);
rt_patches_init();
frame_step_init();
@@ -345,17 +348,12 @@ void rt_boot(void) {
cb_addr[1] = hostfn_register("<rtld module_inits>", cb_module_inits);
cb_addr[2] = hostfn_register("<rtld module_finis>", cb_module_finis);
hostfn_seal();
ULONGLONG held = GetTickCount64();
if (warm) WaitForSingleObject(warm, INFINITE), CloseHandle(warm);
uint64_t held = plat_now_ms();
if (warm) plat_thread_join(warm);
else vk_pipeline_prewarm();
held = GetTickCount64() - held;
FILETIME created, x0, x1, x2, now;
GetProcessTimes(GetCurrentProcess(), &created, &x0, &x1, &x2);
GetSystemTimeAsFileTime(&now);
ULARGE_INTEGER c = {.LowPart = created.dwLowDateTime, .HighPart = created.dwHighDateTime},
n = {.LowPart = now.dwLowDateTime, .HighPart = now.dwHighDateTime};
held = plat_now_ms() - held;
rt_log("boot: guest starts %llu ms after process start (%llu ms of it waiting for the prewarm)",
(unsigned long long)((n.QuadPart - c.QuadPart) / 10000), (unsigned long long)held);
(unsigned long long)(plat_process_uptime_ns() / 1000000), (unsigned long long)held);
rt_guest_start_ns = kern_now_ns();
kern_run_on_guest_main(guest_main);
rt_fatal("boot: the guest main thread ended");
+96 -20
View File
@@ -35,9 +35,9 @@ static const char *const known[] = {
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA", "COLOR_PRESET", "COLOR_SATURATION", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DEVICE_PROFILE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
"EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "FLIGHT_SPIKE_MS",
"FLIGHT_THUMBS", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
"FLIGHT_THUMBS", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
"GPU_INDEX", "GPU_NAME", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
@@ -48,9 +48,9 @@ static const char *const known[] = {
"REPLAY_CHECK", "RESOLVE_CHECK", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
"WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
"WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
};
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
@@ -59,7 +59,10 @@ static const struct { const char *key, *section; } sections[] = {
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"COLOR_PRESET", "display"},
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
{"COLOR_GAMMA", "display"},
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
{"VOLUME", "audio"},
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
@@ -77,9 +80,9 @@ static const struct { const char *key, *section; } sections[] = {
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
};
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
@@ -94,13 +97,19 @@ static const CfgMeta metas[] = {
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
{"ASPECT", "fit", "fit|stretch", LIVE},
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
{"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE},
{"COLOR_GAMMA", "1", "0.8..1.2", LIVE},
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
// Game
{"FPS_LIMIT", "60", "30|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
{"HIGHFPS_FIX", "1", "0|1", RESTART},
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
// Sound, Controls, Menu
{"VOLUME", "100", "0..100", LIVE},
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
@@ -125,10 +134,10 @@ static const CfgMeta metas[] = {
{"SPLIT_US", "1000", "0..20000", RESTART},
// Vulkan
{"VK_VALIDATION", "0", "0|1", RESTART}, {"VK_DYN_VERTEX", "", "0|1", RESTART},
{"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"INPUT_RECORD", "", NULL, RESTART},
};
@@ -166,11 +175,11 @@ static void add(char *key, char *value, int line) {
for (int i = 0; i < n_entries; i++)
if (!strcmp(entries[i].key, key)) { // a repeated key: the later line wins
free(entries[i].value);
entries[i].value = _strdup(value);
entries[i].value = strdup(value);
return;
}
if (n_entries == CFG_MAX_KEYS) return;
entries[n_entries++] = (CfgEntry){_strdup(key), _strdup(value), 0, 0};
entries[n_entries++] = (CfgEntry){strdup(key), strdup(value), 0, 0};
}
int cfg_load(const char *path) {
@@ -199,15 +208,75 @@ int cfg_load(const char *path) {
return 1;
}
const char *cfg_get(const char *name) {
// ---- device profiles: defaults for a known machine, below the environment and totk.ini and above the table, so any
// key the player sets wins and resetting a key in the menu returns to the profile's value. TOTK_DEVICE_PROFILE = auto
// (default: the Steam Deck when the firmware says so), desktop (none) or deck.
//
// The Steam Deck (LCD Jupiter, OLED Galileo; 1280x800, 16:10, 4 Zen 2 cores, RDNA 2 with 8 CUs sharing 16 GB): the game
// at the screen's 1280x800 with its own 5/6 scene (the aspect patch gives the camera its 16:10, resolution.c, and the
// romfs-patches-1280x800 overlay the HUD's);
// fullscreen and vsync, as gamescope composes Game Mode; 40 fps, which divides 40 Hz (and 120 Hz on the OLED).
// Starting points to replace with measured ones (docs/tester-guide.md, Steam Deck).
static const struct { const char *key, *value; } DECK[] = {
{"FULLSCREEN", "1"}, {"WINDOW_SIZE", "1280x800"}, {"OUTPUT_RES", "1280x800"}, {"RENDER_SCALE", "game"},
{"VK_PRESENT", "fifo"}, {"FPS_LIMIT", "40"},
};
static int profile = -1; // 0 none, 1 Steam Deck
static int read_line(const char *path, char *out, size_t n) {
FILE *f = fopen(path, "r");
if (!f) return 0;
int ok = fgets(out, (int)n, f) != NULL;
fclose(f);
out[ok ? strcspn(out, "\r\n") : 0] = 0;
return ok;
}
// The Steam Deck: Valve's board, product Jupiter or Galileo (/sys/class/dmi/id); SteamDeck=1, which Steam sets on it.
static int on_steam_deck(void) {
const char *env = getenv("SteamDeck");
if (env && !strcmp(env, "1")) return 1;
char vendor[64], product[64];
return read_line("/sys/class/dmi/id/board_vendor", vendor, sizeof vendor) && !strcmp(vendor, "Valve") &&
read_line("/sys/class/dmi/id/product_name", product, sizeof product) &&
(!strcmp(product, "Jupiter") || !strcmp(product, "Galileo"));
}
static const char *raw_get(const char *name);
static int profile_of(void) {
int p = __atomic_load_n(&profile, __ATOMIC_ACQUIRE);
if (p >= 0) return p;
const char *set = raw_get(PREFIX "DEVICE_PROFILE");
if (set && !strcmp(set, "deck")) p = 1;
else if (set && *set && strcmp(set, "auto")) p = 0;
else p = on_steam_deck();
__atomic_store_n(&profile, p, __ATOMIC_RELEASE);
return p;
}
const char *cfg_device_profile(void) { return profile_of() ? "deck" : "desktop"; }
static const char *raw_get(const char *name) {
const char *e = getenv(name);
if (e || strncmp(name, PREFIX, PREFIX_LEN)) return e;
int n = __atomic_load_n(&n_entries, __ATOMIC_ACQUIRE);
for (int i = 0; i < n; i++)
if (!_stricmp(entries[i].key, name + PREFIX_LEN)) return __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
if (!strcasecmp(entries[i].key, name + PREFIX_LEN)) return __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
return NULL;
}
const char *cfg_get(const char *name) {
const char *v = raw_get(name);
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
return NULL;
}
// Whether name's value comes from the player (the environment or totk.ini), not a profile or the table.
int cfg_is_set(const char *name) { return raw_get(name) != NULL; }
unsigned cfg_version(void) { return __atomic_load_n(&version, __ATOMIC_ACQUIRE); }
int cfg_from_env(const char *name) { return getenv(name) != NULL; }
@@ -222,12 +291,12 @@ void cfg_set(const char *name, const char *value) {
while (i < n && strcmp(entries[i].key, key)) i++;
if (i < n) {
if (!entries[i].value || strcmp(entries[i].value, value)) {
__atomic_store_n(&entries[i].value, _strdup(value), __ATOMIC_RELEASE); // the old string stays for readers
__atomic_store_n(&entries[i].value, strdup(value), __ATOMIC_RELEASE); // the old string stays for readers
entries[i].dirty = 1;
}
entries[i].removed = 0; // a value set after cfg_reset replaces the line instead of deleting it
} else if (n < CFG_MAX_KEYS) {
entries[n] = (CfgEntry){_strdup(key), _strdup(value), 1, 0};
entries[n] = (CfgEntry){strdup(key), strdup(value), 1, 0};
__atomic_store_n(&n_entries, n + 1, __ATOMIC_RELEASE);
}
__atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
@@ -247,7 +316,7 @@ int cfg_reset(const char *const *keys, int n) {
while (i < n_entries && strcmp(entries[i].key, name)) i++;
if (i == n_entries) { // no value in memory ("key =" in the file): the line still has to go
if (i == CFG_MAX_KEYS) continue;
entries[i] = (CfgEntry){_strdup(name), NULL, 0, 1};
entries[i] = (CfgEntry){strdup(name), NULL, 0, 1};
__atomic_store_n(&n_entries, i + 1, __ATOMIC_RELEASE);
} else if (entries[i].value) {
__atomic_store_n(&entries[i].value, NULL, __ATOMIC_RELEASE); // the old string stays for readers
@@ -543,7 +612,7 @@ static int find_ini(void) {
if (ini_path[0]) return 1;
char path[1024];
if (!plat_exe_path(path, sizeof path)) return 0;
char *cut = strrchr(path, '\\');
char *cut = plat_last_sep(path);
if (!cut) cut = strrchr(path, '/');
if (!cut || cut + 10 >= path + sizeof path) return 0;
strcpy(cut + 1, "totk.ini");
@@ -557,7 +626,7 @@ int cfg_save(void) {
return 0;
}
void cfg_init(void) {
static void load_ini(void) {
if (!find_ini()) return;
if (!cfg_load(ini_path)) {
rt_log("cfg: no settings file at %s", ini_path);
@@ -570,3 +639,10 @@ void cfg_init(void) {
if (getenv(var)) rt_log("cfg: %s is set in the environment and overrides totk.ini", var);
}
}
void cfg_init(void) {
load_ini();
if (profile_of())
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
"turns it off)");
}
+5 -1
View File
@@ -2,7 +2,7 @@
#pragma once
// getenv for a setting: the environment variable when set, else the totk.ini key (the name without "TOTK_", any
// case), else NULL. Same string semantics as getenv; the pointer stays valid for the process (a value cfg_set replaces
// case), else the device profile's default (cfg.c: the Steam Deck), else NULL. Same string semantics as getenv; the pointer stays valid for the process (a value cfg_set replaces
// is kept). A name without the TOTK_ prefix goes to getenv unchanged. Safe from any thread once cfg_init has returned.
const char *cfg_get(const char *name);
@@ -21,6 +21,10 @@ void cfg_set(const char *name, const char *value);
unsigned cfg_version(void);
// 1 when the environment sets name: cfg_set cannot change what cfg_get returns (the menu greys the setting out).
int cfg_from_env(const char *name);
// 1 when the player set name (the environment or totk.ini), 0 when cfg_get gives a device profile's default or NULL.
int cfg_is_set(const char *name);
// The device profile in use, "deck" or "desktop" (TOTK_DEVICE_PROFILE, else what the machine is).
const char *cfg_device_profile(void);
// Writes the keys cfg_set changed since the last save into totk.ini next to totk.exe (cfg_save_to: the file at path).
// Only those lines change: the last line of a changed key gets the new value, a missing key goes under its section
// (after the commented example line of the key when the section has one; the section is added at the end of the file
+24 -24
View File
@@ -17,7 +17,7 @@ static int fails;
int probe_cfg(void) {
char path[1024];
if (!plat_exe_path(path, sizeof path)) return 1;
char *cut = strrchr(path, '\\');
char *cut = plat_last_sep(path);
if (!cut) return 1;
strcpy(cut + 1, "cfg-probe.ini");
static const char ini[] =
@@ -35,8 +35,8 @@ int probe_cfg(void) {
"no_such_setting = 1\r\n"
"no equals sign\r\n"
"last = no newline at the end";
_putenv_s("TOTK_LOCALE", ""); // an empty variable is how _putenv_s spells "remove" on Windows
_putenv_s("TOTK_FPS60", "");
plat_setenv("TOTK_LOCALE", NULL);
plat_setenv("TOTK_FPS60", NULL);
CHECK(plat_file_write(path, ini, sizeof ini - 1), "write the probe ini");
CHECK(cfg_load(path) == 1, "load the probe ini");
CHECK(EQ(cfg_get("TOTK_FPS60"), "0"), "value read, whitespace trimmed, lower-case key");
@@ -48,9 +48,9 @@ int probe_cfg(void) {
CHECK(cfg_get("TOTK_CLOTH_REALTIME") == NULL, "absent key is NULL");
CHECK(cfg_get("TOTK_comment") == NULL, "comment is not a key");
CHECK(EQ(cfg_get("TOTK_NO_SUCH_SETTING"), "1"), "unknown key is still readable");
_putenv_s("TOTK_FPS60", "1");
plat_setenv("TOTK_FPS60", "1");
CHECK(EQ(cfg_get("TOTK_FPS60"), "1"), "environment overrides the file");
_putenv_s("TOTK_FPS60", "");
plat_setenv("TOTK_FPS60", NULL);
CHECK(EQ(cfg_get("TOTK_FPS60"), "0"), "file value returns when the variable is removed");
plat_file_delete(path);
CHECK(cfg_load(path) == 0, "missing file is not an error");
@@ -90,9 +90,9 @@ int probe_cfg(void) {
"\r\n"
"[audio]\r\n"
"volume = 40\r\n";
_putenv_s("TOTK_MOTION", "");
_putenv_s("TOTK_VOLUME", "");
_putenv_s("TOTK_RUMBLE_STRENGTH", "");
plat_setenv("TOTK_MOTION", NULL);
plat_setenv("TOTK_VOLUME", NULL);
plat_setenv("TOTK_RUMBLE_STRENGTH", NULL);
CHECK(plat_file_write(path, before, sizeof before - 1), "write the save ini");
CHECK(cfg_load(path) == 1, "load the save ini");
unsigned v0 = cfg_version();
@@ -117,11 +117,11 @@ int probe_cfg(void) {
EQ(cfg_get("TOTK_RUMBLE_STRENGTH"), "80") && EQ(cfg_get("TOTK_VOLUME"), "40") &&
EQ(cfg_get("TOTK_ROMFS_MODS"), "C:\\mods"), "the saved file reads back");
const char *held = cfg_get("TOTK_VOLUME");
_putenv_s("TOTK_VOLUME", "70");
plat_setenv("TOTK_VOLUME", "70");
cfg_set("TOTK_VOLUME", "90");
CHECK(cfg_from_env("TOTK_VOLUME") && EQ(cfg_get("TOTK_VOLUME"), "70"), "environment wins over cfg_set");
CHECK(EQ(held, "40"), "a pointer cfg_get handed out stays valid after cfg_set");
_putenv_s("TOTK_VOLUME", "");
plat_setenv("TOTK_VOLUME", NULL);
CHECK(!cfg_from_env("TOTK_VOLUME") && EQ(cfg_get("TOTK_VOLUME"), "90"), "cfg_set value returns without the variable");
plat_file_delete(path);
CHECK(cfg_save_to(path) == 1, "cfg_save_to creates a missing file");
@@ -147,7 +147,7 @@ int probe_cfg(void) {
// cfg_reset: the key's active lines leave totk.ini, the commented example stays
static const char rbase[] = "[audio]\n# volume = 100\n[input]\nmotion = 0\n";
static const char *const vol[] = {"VOLUME"};
_putenv_s("TOTK_VOLUME", "");
plat_setenv("TOTK_VOLUME", NULL);
CHECK(plat_file_write(path, rbase, sizeof rbase - 1), "write the reset ini");
CHECK(cfg_load(path) == 1, "load the reset ini");
cfg_set("TOTK_VOLUME", "40");
@@ -186,9 +186,9 @@ int probe_cfg(void) {
// a key the environment sets is skipped and keeps its file line
CHECK(cfg_load(path) == 1 && EQ(cfg_get("TOTK_VOLUME"), "60"), "reset: reload");
_putenv_s("TOTK_VOLUME", "70");
plat_setenv("TOTK_VOLUME", "70");
CHECK(cfg_reset(vol, 1) == 0, "reset: an environment-set key is skipped");
_putenv_s("TOTK_VOLUME", "");
plat_setenv("TOTK_VOLUME", NULL);
CHECK(EQ(cfg_get("TOTK_VOLUME"), "60"), "reset: the skipped key keeps its value");
CHECK(cfg_save_to(path) == 1, "reset: save after a skip");
got = plat_file_read(path, &got_n);
@@ -200,7 +200,7 @@ int probe_cfg(void) {
cfg_set("TOTK_VOLUME", "20");
char bad[1100];
snprintf(bad, sizeof bad, "%s", path);
strcpy(strrchr(bad, '\\') + 1, "no-such-dir\\cfg-probe.ini");
strcpy(plat_last_sep(bad) + 1, "no-such-dir/cfg-probe.ini");
CHECK(cfg_save_to(bad) == 0 && cfg_last_save_ok() == 0 && EQ(cfg_get("TOTK_VOLUME"), "20"),
"save status: an unwritable path fails, the value stays");
cfg_set("TOTK_VOLUME", "21");
@@ -352,24 +352,24 @@ int probe_cfg(void) {
CHECK(!strcmp(line, "none"), "edit: a stick direction without keys is none");
// defaults live in the CfgMeta table: cfg_value and the typed accessors fall back to it
cfg_load("cfg-probe-missing.ini");
_putenv_s("TOTK_FPS60", "");
_putenv_s("TOTK_MOUSE_SENS", "");
_putenv_s("TOTK_FREE_CORES", "");
plat_setenv("TOTK_FPS60", NULL);
plat_setenv("TOTK_MOUSE_SENS", NULL);
plat_setenv("TOTK_FREE_CORES", NULL);
CHECK(cfg_bool("TOTK_FPS60") == 1, "default: unset FPS60 is on");
CHECK(EQ(cfg_value("TOTK_VK_PRESENT"), "mailbox") && cfg_int("TOTK_SPLIT_DRAWS") == 64, "default: text and integer");
_putenv_s("TOTK_MOUSE_SENS", "2.5");
plat_setenv("TOTK_MOUSE_SENS", "2.5");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 2.5, "double: the environment value");
_putenv_s("TOTK_MOUSE_SENS", "fast");
plat_setenv("TOTK_MOUSE_SENS", "fast");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 1.0, "double: a value that does not parse gives the default");
_putenv_s("TOTK_MOUSE_SENS", "");
plat_setenv("TOTK_MOUSE_SENS", NULL);
cfg_set("TOTK_MOUSE_SENS", "3");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 3.0 && cfg_int("TOTK_MOUSE_SENS") == 3, "a cfg_set value beats the default");
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores unset: off");
_putenv_s("TOTK_FREE_CORES", "0");
plat_setenv("TOTK_FREE_CORES", "0");
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
_putenv_s("TOTK_FREE_CORES", "1");
plat_setenv("TOTK_FREE_CORES", "1");
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
_putenv_s("TOTK_FREE_CORES", "");
plat_setenv("TOTK_FREE_CORES", NULL);
cfg_load("cfg-probe-missing.ini");
int listed = 0;
for (int i = 0; cfg_meta_at(i); i++) {
@@ -378,7 +378,7 @@ int probe_cfg(void) {
listed++;
char name[96];
snprintf(name, sizeof name, "TOTK_%s", m->key);
_putenv_s(name, "");
plat_setenv(name, "");
CHECK(EQ(cfg_value(name), m->def), "cfg_value returns the table default");
int ok = 0;
if (!m->values) ok = 1;
+71
View File
@@ -0,0 +1,71 @@
// Generated by tools/build_shaders.py from runtime/shaders/color_grade.comp; do not edit.
// source sha256 c93d6ac75667bdee5442d7e94fbf0bd6e7fe7250caa98974424aead10f554106
#pragma once
#include <stdint.h>
static const uint32_t color_grade_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x00000073, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00060010, 0x00000004,
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000000d, 0x0000000b, 0x0000001c,
0x00030047, 0x00000015, 0x00000002, 0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000,
0x00050048, 0x00000015, 0x00000001, 0x00000023, 0x00000008, 0x00050048, 0x00000015, 0x00000002,
0x00000023, 0x00000010, 0x00040047, 0x00000029, 0x00000021, 0x00000000, 0x00040047, 0x00000029,
0x00000022, 0x00000000, 0x00030047, 0x0000005d, 0x00000019, 0x00040047, 0x0000005d, 0x00000021,
0x00000001, 0x00040047, 0x0000005d, 0x00000022, 0x00000000, 0x00040047, 0x0000006a, 0x0000000b,
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
0x00000020, 0x00000001, 0x00040017, 0x00000007, 0x00000006, 0x00000002, 0x00040015, 0x0000000a,
0x00000020, 0x00000000, 0x00040017, 0x0000000b, 0x0000000a, 0x00000003, 0x00040020, 0x0000000c,
0x00000001, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000001, 0x00040017, 0x0000000e,
0x0000000a, 0x00000002, 0x00030016, 0x00000013, 0x00000020, 0x00040017, 0x00000014, 0x00000013,
0x00000004, 0x0005001e, 0x00000015, 0x00000007, 0x00000007, 0x00000014, 0x00040020, 0x00000016,
0x00000009, 0x00000015, 0x0004003b, 0x00000016, 0x00000017, 0x00000009, 0x0004002b, 0x00000006,
0x00000018, 0x00000001, 0x00040020, 0x00000019, 0x00000009, 0x00000007, 0x00020014, 0x0000001c,
0x00040017, 0x0000001d, 0x0000001c, 0x00000002, 0x00040017, 0x00000023, 0x00000013, 0x00000003,
0x00090019, 0x00000026, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
0x00000000, 0x0003001b, 0x00000027, 0x00000026, 0x00040020, 0x00000028, 0x00000000, 0x00000027,
0x0004003b, 0x00000028, 0x00000029, 0x00000000, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000,
0x0004002b, 0x00000006, 0x00000033, 0x00000002, 0x0004002b, 0x0000000a, 0x00000034, 0x00000000,
0x00040020, 0x00000035, 0x00000009, 0x00000013, 0x0004002b, 0x00000013, 0x0000003c, 0x3f000000,
0x0004002b, 0x0000000a, 0x0000003f, 0x00000001, 0x0004002b, 0x00000013, 0x00000046, 0x3e59b3d0,
0x0004002b, 0x00000013, 0x00000047, 0x3f371759, 0x0004002b, 0x00000013, 0x00000048, 0x3d93dd98,
0x0006002c, 0x00000023, 0x00000049, 0x00000046, 0x00000047, 0x00000048, 0x0004002b, 0x0000000a,
0x0000004d, 0x00000002, 0x0004002b, 0x00000013, 0x00000053, 0x00000000, 0x0006002c, 0x00000023,
0x00000054, 0x00000053, 0x00000053, 0x00000053, 0x0004002b, 0x0000000a, 0x00000056, 0x00000003,
0x00090019, 0x0000005b, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
0x00000002, 0x00040020, 0x0000005c, 0x00000000, 0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d,
0x00000000, 0x0004002b, 0x00000013, 0x00000061, 0x3f800000, 0x0004002b, 0x0000000a, 0x00000069,
0x00000008, 0x0006002c, 0x0000000b, 0x0000006a, 0x00000069, 0x00000069, 0x0000003f, 0x0006002c,
0x00000023, 0x00000071, 0x0000003c, 0x0000003c, 0x0000003c, 0x0006002c, 0x00000023, 0x00000072,
0x00000061, 0x00000061, 0x00000061, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
0x000200f8, 0x00000005, 0x000300f7, 0x0000006b, 0x00000000, 0x000300fb, 0x00000034, 0x0000006c,
0x000200f8, 0x0000006c, 0x0004003d, 0x0000000b, 0x0000000f, 0x0000000d, 0x0007004f, 0x0000000e,
0x00000010, 0x0000000f, 0x0000000f, 0x00000000, 0x00000001, 0x0004007c, 0x00000007, 0x00000011,
0x00000010, 0x00050041, 0x00000019, 0x0000001a, 0x00000017, 0x00000018, 0x0004003d, 0x00000007,
0x0000001b, 0x0000001a, 0x000500af, 0x0000001d, 0x0000001e, 0x00000011, 0x0000001b, 0x0004009a,
0x0000001c, 0x0000001f, 0x0000001e, 0x000300f7, 0x00000021, 0x00000000, 0x000400fa, 0x0000001f,
0x00000020, 0x00000021, 0x000200f8, 0x00000020, 0x000200f9, 0x0000006b, 0x000200f8, 0x00000021,
0x0004003d, 0x00000027, 0x0000002a, 0x00000029, 0x00050041, 0x00000019, 0x0000002d, 0x00000017,
0x0000002c, 0x0004003d, 0x00000007, 0x0000002e, 0x0000002d, 0x00050080, 0x00000007, 0x0000002f,
0x00000011, 0x0000002e, 0x00040064, 0x00000026, 0x00000030, 0x0000002a, 0x0007005f, 0x00000014,
0x00000031, 0x00000030, 0x0000002f, 0x00000002, 0x0000002c, 0x0008004f, 0x00000023, 0x00000032,
0x00000031, 0x00000031, 0x00000000, 0x00000001, 0x00000002, 0x00060041, 0x00000035, 0x00000036,
0x00000017, 0x00000033, 0x00000034, 0x0004003d, 0x00000013, 0x00000037, 0x00000036, 0x00060050,
0x00000023, 0x00000039, 0x00000037, 0x00000037, 0x00000037, 0x00050081, 0x00000023, 0x0000003a,
0x00000032, 0x00000039, 0x00050083, 0x00000023, 0x0000003e, 0x0000003a, 0x00000071, 0x00060041,
0x00000035, 0x00000040, 0x00000017, 0x00000033, 0x0000003f, 0x0004003d, 0x00000013, 0x00000041,
0x00000040, 0x0005008e, 0x00000023, 0x00000042, 0x0000003e, 0x00000041, 0x00050081, 0x00000023,
0x00000044, 0x00000042, 0x00000071, 0x00050094, 0x00000013, 0x0000004a, 0x00000044, 0x00000049,
0x00060050, 0x00000023, 0x0000004b, 0x0000004a, 0x0000004a, 0x0000004a, 0x00060041, 0x00000035,
0x0000004e, 0x00000017, 0x00000033, 0x0000004d, 0x0004003d, 0x00000013, 0x0000004f, 0x0000004e,
0x00060050, 0x00000023, 0x00000050, 0x0000004f, 0x0000004f, 0x0000004f, 0x0008000c, 0x00000023,
0x00000051, 0x00000001, 0x0000002e, 0x0000004b, 0x00000044, 0x00000050, 0x0007000c, 0x00000023,
0x00000055, 0x00000001, 0x00000028, 0x00000051, 0x00000054, 0x00060041, 0x00000035, 0x00000057,
0x00000017, 0x00000033, 0x00000056, 0x0004003d, 0x00000013, 0x00000058, 0x00000057, 0x00060050,
0x00000023, 0x00000059, 0x00000058, 0x00000058, 0x00000058, 0x0007000c, 0x00000023, 0x0000005a,
0x00000001, 0x0000001a, 0x00000055, 0x00000059, 0x0004003d, 0x0000005b, 0x0000005e, 0x0000005d,
0x0008000c, 0x00000023, 0x00000064, 0x00000001, 0x0000002b, 0x0000005a, 0x00000054, 0x00000072,
0x00050051, 0x00000013, 0x00000065, 0x00000064, 0x00000000, 0x00050051, 0x00000013, 0x00000066,
0x00000064, 0x00000001, 0x00050051, 0x00000013, 0x00000067, 0x00000064, 0x00000002, 0x00070050,
0x00000014, 0x00000068, 0x00000065, 0x00000066, 0x00000067, 0x00000061, 0x00040063, 0x0000005e,
0x00000011, 0x00000068, 0x000200f9, 0x0000006b, 0x000200f8, 0x0000006b, 0x000100fd, 0x00010038,
};
+2 -17
View File
@@ -19,11 +19,6 @@
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#define TABLE_SPAN_LIMIT (1ull << 30) // guest bytes covered by the table, and its size in bytes
#define HOST_CAP 8192 // host function registry, power of two
@@ -98,21 +93,11 @@ void rt_register_host(uint64_t addr, GuestFn fn) {
}
static void *table_alloc(uint64_t bytes) {
#ifdef _WIN32
return VirtualAlloc(NULL, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); // zero pages, backed on first write
#else
void *p = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
return p == MAP_FAILED ? NULL : p;
#endif
return plat_mem_alloc(bytes); // zero pages, backed on first write
}
static void table_seal(void *p, uint64_t bytes) {
#ifdef _WIN32
DWORD old;
if (!VirtualProtect(p, bytes, PAGE_READONLY, &old)) rt_fatal("dispatch: VirtualProtect failed (%lu)", GetLastError());
#else
if (mprotect(p, bytes, PROT_READ)) rt_fatal("dispatch: mprotect failed");
#endif
if (!plat_mem_protect((uint64_t)(uintptr_t)p, bytes, PLAT_PROT_READ)) rt_fatal("dispatch: sealing the table failed");
}
void rt_dispatch_init(const RtModule *mods, int count) {
File renamed without changes.
+30 -33
View File
@@ -1,5 +1,5 @@
// TOTK_FLIGHT=1 flight recorder: a ring of the last TOTK_FLIGHT_FRAMES (600) presents, one record each (interval,
// render-thread blocked time per perf kind, event counts) and a 320x180 thumbnail each (gfx_window.c, blitted on the
// render-thread blocked time per perf kind, event counts) and a 320x180 thumbnail each (gfx_present.c, blitted on the
// GPU by the present thread). A dump goes to <dir>/<YYYYMMDD-HHMMSS>-f<frame>/: frames.tsv and f<frame>.png per
// thumbnail, written by a background thread. Triggers: F8 while the game window has focus, a frame interval over
// TOTK_FLIGHT_SPIKE_MS (100; 0 off; at most one dump per 10 s), and TOTK_FLIGHT_AT=<frame>[,<frame>...]. A dump holds
@@ -7,7 +7,6 @@
//
// Records come from perf_present (prof_win.c) on the render thread; the counts are differences of the TOTK_TRACE=perf
// running totals and of vk_exec_frame. Off, the only cost is the flight_on branch in perf_present and vk_backend.c.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -16,8 +15,10 @@
#include "flight.h"
#include "gfx.h"
#include "host.h"
#include "input.h"
#include "vk.h"
#include "vk_exec.h"
#include "platform/platform.h"
#define FLIGHT_POST 30 // presents recorded after the trigger before the dump starts
#define FLIGHT_SPIKE_GAP_NS 10000000000ull
@@ -41,7 +42,7 @@ typedef struct {
int n; // records, oldest first
uint64_t trig_frame, trig_interval_us;
const char *why;
char dir[MAX_PATH];
char dir[PLAT_PATH_MAX];
Rec r[];
} DumpJob;
@@ -54,7 +55,7 @@ static uint64_t dump_at, trig_frame, trig_interval_us, last_spike_ns;
static const char *trig_why;
static uint64_t at_frames[16];
static int n_at;
static char base_dir[MAX_PATH];
static char base_dir[PLAT_PATH_MAX];
static uint64_t prev_cnt[PERF_CNT_N], prev_xlated;
static VkExecFrame prev_exec;
static volatile uint64_t cost_ns, cost_max_ns; // render-thread time in flight_frame, for the dump log line
@@ -79,15 +80,14 @@ void flight_init(void) {
p = *end == ',' ? end + 1 : end;
}
const char *d = cfg_get("TOTK_FLIGHT_DIR");
char tmp[MAX_PATH];
char tmp[PLAT_PATH_MAX];
if (d && *d) snprintf(tmp, sizeof tmp, "%s", d);
else { // <exe dir>/../flight: build/flight for build/v121*/totk.exe
DWORD k = GetModuleFileNameA(NULL, tmp, sizeof tmp);
char *slash = k ? strrchr(tmp, '\\') : NULL;
if (slash) snprintf(slash, sizeof tmp - (size_t)(slash - tmp), "\\..\\flight");
char *slash = plat_exe_path(tmp, sizeof tmp) ? plat_last_sep(tmp) : NULL;
if (slash) snprintf(slash, sizeof tmp - (size_t)(slash - tmp), "/../flight");
else snprintf(tmp, sizeof tmp, "flight");
}
if (!GetFullPathNameA(tmp, sizeof base_dir, base_dir, NULL)) snprintf(base_dir, sizeof base_dir, "%s", tmp);
if (!plat_full_path(tmp, base_dir, sizeof base_dir)) snprintf(base_dir, sizeof base_dir, "%s", tmp);
thumbs_on = env_u("TOTK_FLIGHT_THUMBS", 1) != 0;
if (thumbs_on) vk_thumbs_enable(ring_n);
flight_on = 1;
@@ -225,15 +225,15 @@ static int write_png(const char *path, const uint8_t *rgba, PngBufs *pb) {
}
// ---------------------------------------------------------------- dump thread
static DWORD WINAPI dump_main(void *arg) {
static void dump_main(void *arg) {
DumpJob *j = arg;
uint64_t t_start = GetTickCount64();
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
CreateDirectoryA(base_dir, NULL);
if (!CreateDirectoryA(j->dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
rt_log("flight: cannot create %s (error %lu)", j->dir, GetLastError());
uint64_t t_start = plat_now_ms();
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
plat_file_mkdir_p(j->dir);
if (!plat_dir_exists(j->dir)) {
rt_log("flight: cannot create %s", j->dir);
free(j);
return 0;
return;
}
uint8_t *have = calloc((size_t)j->n, 1);
int n_thumbs = 0;
@@ -250,23 +250,23 @@ static DWORD WINAPI dump_main(void *arg) {
// Oldest first: each slot is copied out long before the presents that go on meanwhile reach it again. The newest
// few complete only when their present fence has been waited for, so they get a second try.
for (int pass = 0; pass < 2 && pb.raw && pb.z && pb.head && px; pass++) {
if (pass) Sleep(200);
if (pass) plat_sleep_ms(200);
for (int i = pass ? (j->n > 8 ? j->n - 8 : 0) : 0; i < j->n; i++) {
if (have[i] || !vk_thumbs_read(j->r[i].frame, px)) continue;
char path[MAX_PATH + 32];
snprintf(path, sizeof path, "%s\\f%06llu.png", j->dir, (unsigned long long)j->r[i].frame);
char path[PLAT_PATH_MAX + 32];
snprintf(path, sizeof path, "%s/f%06llu.png", j->dir, (unsigned long long)j->r[i].frame);
if (write_png(path, px, &pb)) have[i] = 1, n_thumbs++;
}
}
free(px), free(pb.raw), free(pb.z), free(pb.head);
}
char path[MAX_PATH + 32];
snprintf(path, sizeof path, "%s\\frames.tsv", j->dir);
char path[PLAT_PATH_MAX + 32];
snprintf(path, sizeof path, "%s/frames.tsv", j->dir);
FILE *f = fopen(path, "w");
if (!f) {
rt_log("flight: cannot write %s", path);
free(have), free(j);
return 0;
return;
}
fprintf(f, "# totk flight dump: %s at frame %llu, interval %.1f ms\n", j->why, (unsigned long long)j->trig_frame,
j->trig_interval_us / 1000.0);
@@ -285,11 +285,11 @@ static DWORD WINAPI dump_main(void *arg) {
rt_log("flight: dump %s: %s at frame %llu (%.1f ms), frames %llu-%llu, %d thumbnails, written in %llu ms; "
"recorder %.1f us per frame (max %.0f us)", j->dir,
j->why, (unsigned long long)j->trig_frame, j->trig_interval_us / 1000.0, (unsigned long long)j->r[0].frame,
(unsigned long long)j->r[j->n - 1].frame, n_thumbs, (unsigned long long)(GetTickCount64() - t_start),
(unsigned long long)j->r[j->n - 1].frame, n_thumbs, (unsigned long long)(plat_now_ms() - t_start),
recorded ? cost_ns / 1e3 / (double)recorded : 0.0, cost_max_ns / 1e3);
free(have);
free(j);
return 0;
return;
}
static void start_dump(void) {
@@ -301,13 +301,11 @@ static void start_dump(void) {
j->trig_interval_us = trig_interval_us;
j->why = trig_why;
for (int i = 0; i < n; i++) j->r[i] = ring[(recorded - (uint64_t)n + (uint64_t)i) % (uint64_t)ring_n];
SYSTEMTIME st;
GetLocalTime(&st);
snprintf(j->dir, sizeof j->dir, "%s\\%04u%02u%02u-%02u%02u%02u-f%llu", base_dir, st.wYear, st.wMonth, st.wDay, st.wHour,
st.wMinute, st.wSecond, (unsigned long long)trig_frame);
HANDLE h = CreateThread(NULL, 0, dump_main, j, 0, NULL);
if (h) CloseHandle(h);
else rt_log("flight: cannot start the dump thread (error %lu)", GetLastError()), free(j);
struct tm st;
plat_local_time(&st);
snprintf(j->dir, sizeof j->dir, "%s/%04d%02d%02d-%02d%02d%02d-f%llu", base_dir, st.tm_year + 1900, st.tm_mon + 1,
st.tm_mday, st.tm_hour, st.tm_min, st.tm_sec, (unsigned long long)trig_frame);
if (!plat_thread_spawn(dump_main, j, "flight dump", 0)) rt_log("flight: cannot start the dump thread"), free(j);
}
static void trigger(uint64_t frame, uint64_t interval_ns, const char *why) {
@@ -351,8 +349,7 @@ void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const u
recorded++;
static int key_was;
HWND w = gfx_window_handle();
int key = w && w == GetForegroundWindow() && (GetAsyncKeyState(VK_F8) & 0x8000);
int key = input_flight_key();
if (key && !key_was) trigger(frame, interval_ns, "F8");
key_was = key;
for (int i = 0; i < n_at; i++)
+1 -1
View File
@@ -14,7 +14,7 @@ void flight_init(void); // prof_start calls it
void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const uint64_t blocked_ns[],
const volatile int64_t counts[]);
// gfx_window.c: the present thread blits every presented frame into a host-visible ring of n thumbnails.
// gfx_present.c: the present thread blits every presented frame into a host-visible ring of n thumbnails.
void vk_thumbs_enable(int n); // before the first present; n slots of FLIGHT_THUMB_W x FLIGHT_THUMB_H
void vk_thumbs_frame(uint64_t frame); // the present thread names the frame of the vk_present_image call that follows
// Copies frame's thumbnail as RGBA when the ring still holds it complete; 0 when it was never written, is still in
+8 -4
View File
@@ -24,7 +24,7 @@
//
// The world clock (frame_step_world_us) sums the steps from the first present that finds the player actor away from
// the origin, where the title screen keeps it: the end of the first save load. Recorded and scripted input
// (input_win.c) runs on it, so a replay presses at the same game time at any frame rate and after a first load of any
// (input.c) runs on it, so a replay presses at the same game time at any frame rate and after a first load of any
// length; a later load (fast travel) still shifts what follows by its length.
//
// TOTK_TRACE=perf adds a line per 120 presents: fps, the game seconds stepped against wall seconds, and with
@@ -161,10 +161,14 @@ static uint64_t read_ptr(uint64_t a) {
uint64_t p = 0;
return read_guest(a, &p, 8) ? p : 0;
}
// 1.2.1: [main+0x4728538] (1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the resident actors; the one
// named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h TOTK_GAME_V121/V143).
// 1.2.1: [main+0x4728538] (1.1.1: [main+0x4630a68], 1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the
// resident actors; the one named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h
// TOTK_GAME_V121/V143). The 1.1.1 slot was measured from its own dereference chain: 181 scene accesses in that build
// read it, and one of them reproduces this walk (the 14-component check, component 13, the 0x70 stride and the name
// compare) instruction for instruction.
static int player_pos(float xyz[3]) {
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : 0x4728538)), scene = module ? read_ptr(module + 0x1e8) : 0;
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : RT_V111 ? 0x4630a68 : 0x4728538)),
scene = module ? read_ptr(module + 0x1e8) : 0;
int32_t count = 0;
if (!scene || !read_guest(scene + 0x50, &count, 4) || count < 14 || count > 256) return 0;
uint64_t comps = read_ptr(scene + 0x58), resident = comps ? read_ptr(comps + 13 * 8) : 0;
-2
View File
@@ -44,8 +44,6 @@ const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_a
void fs_trace(const char *what, const char *path); // TOTK_TRACE=fs: one line per open
// ---- fs_host.c
wchar_t *fs_wide(const char *utf8); // UTF-16 copy for the W APIs; free() it. Fatal on
// invalid UTF-8
void fs_host_set_profile(const char *dir); // profile directory; before the first storage mount only
// (default rt_config()->profile)
int fs_save_busy(void); // a save is being written: a save file open for writing,
+532 -495
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -16,7 +16,7 @@ const OvNvState *ov_nv_state(void); // ov_gfx.c
#define RT_GPU_VA_BASE 0x8000000000ull // 2^39, never mapped on the host
#define RT_GPU_VA_SIZE (64ull << 30)
// ---- window, vsync, focus (gfx_window.c, vk_device.c). Host threads only; never run guest code.
// ---- window, vsync, focus (gfx_present.c and the window back end, gfx_wsys.h; vk_device.c). Host threads only; never run guest code.
void gfx_window_start(void); // window thread, Vulkan device, swapchain; idempotent
void gfx_vsync_bind(uint64_t system_event); // nn::vi vsync event; signalled at 60 Hz
uint64_t gfx_vsync_ticks(void); // ticks since start
@@ -50,7 +50,7 @@ const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8
uint64_t gfx_shader_async_done(void); // translations finished by the background worker
void gfx_shader_counts(uint64_t *keys, uint64_t *translated); // distinct keys in memory, translations done (TOTK_TRACE=perf)
// Translator answers for this device (vk_caps.c): shaderFloat64, storage offset rounding, subgroup size per vs/fs/cs.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3]);
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits);
const uint8_t *gfx_shader_key(const GfxShader *s); // the 32-byte cache key (stable while the translator is)
const uint8_t *gfx_shader_input_id(const GfxShader *s); // the 32-byte input id (stable across translators)
// By input id from memory or disk; xlate 1 also translates it from its stored inputs. NULL if absent.
+6 -7
View File
@@ -5,9 +5,9 @@
// Needs no game files.
#include <stddef.h>
#include <stdio.h>
#include <windows.h>
#include "gfx.h"
#include "platform/platform.h"
static int failures, checks;
#define CHECK(cond, ...) do { checks++; if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
@@ -65,8 +65,8 @@ static void check_write_watch(void) {
// read) thaws it and holds it thawed; SetHeapSize below decommits it.
mem_written_pages(heap, 0x200000, pages, 8);
CHECK(mem_freeze(heap) == 1 && mem_frozen(heap), "freeze of an idle committed 2 MiB region");
MEMORY_BASIC_INFORMATION mbi;
CHECK(VirtualQuery((void *)(uintptr_t)heap, &mbi, sizeof mbi) && mbi.Protect == PAGE_READONLY, "frozen region is not read-only");
uint64_t end;
CHECK(plat_mem_state(heap, heap + 1, &end) == PLAT_PROT_READ, "frozen region is not read-only");
p[0x3000] = 7;
CHECK(!mem_frozen(heap) && p[0x3000] == 7, "a write into a frozen region did not thaw it");
n = mem_written_pages(heap, 0x200000, pages, 8);
@@ -88,12 +88,11 @@ static void check_write_watch(void) {
// A region with reserved holes freezes its committed pages only, and a write thaws them without committing a hole.
uint64_t ra = a & ~0x1fffffull;
CHECK(mem_freeze(ra) == 1 && mem_frozen(ra), "freeze of a 2 MiB region with reserved holes");
CHECK(VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) && mbi.Protect == PAGE_READONLY, "its committed pages are not read-only");
CHECK(plat_mem_state(a, a + 1, &end) == PLAT_PROT_READ, "its committed pages are not read-only");
*(volatile uint8_t *)(uintptr_t)(a + 0x2000) = 9;
CHECK(!mem_frozen(ra) && *(volatile uint8_t *)(uintptr_t)(a + 0x2000) == 9, "a write did not thaw the region with holes");
CHECK(VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) && mbi.Protect == PAGE_READWRITE && mbi.RegionSize == 0x4000,
"the thawed pages are not read-write");
CHECK(VirtualQuery((void *)(uintptr_t)(a + 0x4000), &mbi, sizeof mbi) && mbi.State == MEM_RESERVE, "a hole got committed");
CHECK(plat_mem_state(a, a + 0x10000, &end) == PLAT_PROT_RW && end == a + 0x4000, "the thawed pages are not read-write");
CHECK(plat_mem_state(a + 0x4000, a + 0x4001, &end) == PLAT_MEM_RESERVED, "a hole got committed");
printf("write-watch: region with reserved holes frozen and thawed by a write\n");
CHECK(mem_unmap_physical(a, 0x4000) == 0, "UnmapPhysicalMemory");
CHECK(mem_written_pages(RT_KERNEL_BASE, 0x1000, pages, 8) == UINT64_MAX, "kernel region is not write-watched");
+164 -458
View File
@@ -1,34 +1,31 @@
// Win32 window, Vulkan surface and swapchain, present blit, 60 Hz vsync thread, focus.
// Vulkan surface and swapchain, present blit, 60 Hz vsync thread, focus: the window-system-free half of the window
// (the other half: gfx_window_win.c with Win32, gfx_window_sdl.c with SDL, through gfx_wsys.h).
// Swapchain set-up ported from LAS Recomp, runtime/nvn/vulkan/device.c:254-330 (https://github.com/peiikai/LASRecomp @
// da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai), SDL replaced by Win32. Focus and vsync
// behaviour from MacNativeGames, games/totk/runtime/frontend.m:88-97, 175-176, 350-352 (https://github.com/ly05010419/
// MacNativeGames @ b75be8cfa704df3701e86130c8ced268e79d6b11, MIT, Copyright (c) 2026 MacNativeGames contributors).
// da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai). Focus and vsync behaviour from
// MacNativeGames, games/totk/runtime/frontend.m:88-97, 175-176, 350-352 (https://github.com/ly05010419/MacNativeGames @
// b75be8cfa704df3701e86130c8ced268e79d6b11, MIT, Copyright (c) 2026 MacNativeGames contributors).
//
// Threads: the window thread owns the HWND and its message loop and never runs guest code. The vsync thread signals the
// Threads: the window thread (the back end's) owns the window and never runs guest code. The vsync thread signals the
// bound system events. Presents come from any host thread (the back end's present worker) and take g_pl; every queue
// submission takes the recursive queue lock (vk_queue_lock). The swapchain is rebuilt lazily: WM_SIZE only marks it
// dirty, the next present (or gfx_window_resize) rebuilds it.
// submission takes the recursive queue lock (vk_queue_lock). The swapchain is rebuilt lazily: a resize only marks it
// dirty (gfx_window_mark_dirty), the next present (or gfx_window_resize) rebuilds it.
//
// The in-game menu (ui/menu.h) draws over the game image in the present's command buffer, after the blit; its keyboard
// and mouse come from the window procedure (ui/menu_win.cpp). The window's size and fullscreen state are the settings
// TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, applied at start and whenever a cfg_set changes them (window_settings_live).
//
// The process is per-monitor DPI aware (totk.manifest): every size here is physical pixels, TOTK_WINDOW_SIZE included,
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
// The in-game menu (ui/menu.h) draws over the game image in the present's command buffer, after the blit. The window's
// size and fullscreen state are the settings TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, which the back end applies at start
// and whenever a cfg_set changes them (window_settings_live -> gfx_window_apply_settings).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include "cfg.h"
#include "flight.h"
#include "fs.h"
#include "gfx.h"
#include "input.h"
#include "gfx_wsys.h"
#include "nvn_api.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#define FRAMES 2
#define MAX_IMAGES 16
@@ -36,26 +33,64 @@
#define VSYNC_HZ 60
// ---- queue lock
static CRITICAL_SECTION g_ql;
__attribute__((constructor)) static void window_init_locks(void);
void vk_queue_lock(void) { EnterCriticalSection(&g_ql); }
void vk_queue_unlock(void) { LeaveCriticalSection(&g_ql); }
static PlatRMutex g_ql;
void vk_queue_lock(void) { plat_rmutex_lock(&g_ql); }
void vk_queue_unlock(void) { plat_rmutex_unlock(&g_ql); }
// ---- window state
static HWND g_hwnd;
static HANDLE g_wnd_ready;
// ---- window state shared with the back end
static VkSurfaceKHR g_surface;
static volatile LONG g_dirty; // WM_SIZE seen; swapchain stale
static volatile LONG g_focused = 1;
static volatile LONG g_focus_msgs;
static CRITICAL_SECTION g_start_lock; // gfx_window_start / vsync start
static int g_started, g_wnd_failed;
static int g_background; // TOTK_BACKGROUND_WINDOW: the window stays behind the user's windows
static int g_fs_pending; // window thread: TOTK_FULLSCREEN waits for the first activation
static volatile LONG g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
static volatile int32_t g_dirty; // the window was resized; swapchain stale
static volatile int32_t g_focused = 1;
static volatile int32_t g_focus_msgs;
static PlatRMutex g_start_lock; // gfx_window_start / vsync start
static int g_started;
void gfx_window_mark_dirty(void) { plat_xchg32(&g_dirty, 1); }
// The window gained or lost the focus: the game hears it as nn::oe's focus state and a notification message.
void gfx_window_focus_changed(int in) {
plat_xchg32(&g_focused, in);
oe_set_focus(in); // pinned to 1 by TOTK_ASSUME_FOCUS inside
oe_push_message(OE_MSG_FOCUS_STATE_CHANGED);
plat_inc32(&g_focus_msgs);
rt_log("gfx: window %s the focus", in ? "gained" : "lost");
}
// The window was closed: the process ends with it, as in MNG, once a save Commit still running in the background is
// done.
void gfx_window_closed(void) {
rt_log("gfx: window closed; exiting");
fs_save_flush();
vk_pipeline_save();
prof_write(); // plat_exit runs no atexit handlers
plat_exit(0);
}
// TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=<w>x<h>: client area of the window (the
// swapchain follows the window; the game image is scaled to it).
int gfx_cfg_fullscreen(void) { return cfg_bool("TOTK_FULLSCREEN"); }
void gfx_cfg_window_size(int *w, int *h) {
int x = 0, y = 0;
if (sscanf(cfg_value("TOTK_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320)
sscanf(cfg_meta("WINDOW_SIZE")->def, "%dx%d", &x, &y); // unparsable or out of range: the default
*w = x;
*h = y;
}
// The refresh rate of the window's monitor (TOTK_FPS_LIMIT=display), measured by the back end's window thread when the
// window opens, lands on another monitor or the display mode changes; readers take the stored value.
static volatile int32_t g_rate_mhz; // millihertz, 0 unknown
void gfx_window_set_refresh(double hz) {
int32_t mhz = (int32_t)(hz * 1000 + 0.5);
if (plat_xchg32(&g_rate_mhz, mhz) != mhz) rt_log("gfx: monitor refresh rate %.3f Hz", hz);
}
double gfx_window_refresh_hz(void) { return plat_cas32(&g_rate_mhz, 0, 0) / 1000.0; }
// ---- swapchain and present state (g_pl)
static CRITICAL_SECTION g_pl;
static PlatRMutex g_pl;
static VkSwapchainKHR g_sc;
static VkFormat g_sc_fmt;
static VkExtent2D g_sc_ext;
@@ -93,207 +128,82 @@ static VkBuffer g_th_buf;
static VmaAllocation g_th_alloc;
static uint8_t *g_th_ptr;
static void window_init_locks(void) {
InitializeCriticalSection(&g_ql);
InitializeCriticalSection(&g_pl);
InitializeCriticalSection(&g_start_lock);
}
// ---- vsync
static SRWLOCK g_vs_lock = SRWLOCK_INIT;
static PlatMutex g_vs_lock = PLAT_MUTEX_INIT;
static uint64_t g_vs_ev[16];
static int g_vs_nev;
static volatile long *g_vs_ctr[4];
static volatile int32_t *g_vs_ctr[4];
static int g_vs_nctr;
static volatile LONGLONG g_ticks;
static volatile LONGLONG g_vs_start_ns;
static volatile int64_t g_ticks;
static volatile int64_t g_vs_start_ns;
static int g_vs_started;
static DWORD WINAPI vsync_main(void *arg) {
static void vsync_main(void *arg) {
(void)arg;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
HANDLE t = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
if (!t) { rt_log("gfx: no high-resolution timer; vsync falls back to the system timer tick"); t = CreateWaitableTimerW(NULL, TRUE, NULL); }
plat_thread_priority(PLAT_PRIO_HIGHEST);
uint64_t n = 0;
for (;;) {
uint64_t start = (uint64_t)g_vs_start_ns;
uint64_t due = start + (n + 1) * VSYNC_NS / VSYNC_HZ;
uint64_t now = kern_now_ns();
if (now > due + 100000000ull) { // the process stalled: restart the schedule, no catch-up burst
g_vs_start_ns = (LONGLONG)(now - (n + 1) * VSYNC_NS / VSYNC_HZ);
g_vs_start_ns = (int64_t)(now - (n + 1) * VSYNC_NS / VSYNC_HZ);
} else if (due > now) {
LARGE_INTEGER d;
d.QuadPart = -(LONGLONG)((due - now) / 100);
if (d.QuadPart == 0) d.QuadPart = -1;
SetWaitableTimer(t, &d, 0, NULL, NULL, FALSE);
WaitForSingleObject(t, INFINITE);
plat_wait_ns(due - now);
}
n++;
InterlockedExchange64(&g_ticks, (LONGLONG)n);
plat_xchg64(&g_ticks, (int64_t)n);
uint64_t ev[16];
volatile long *ctr[4];
volatile int32_t *ctr[4];
int ne, nc;
AcquireSRWLockShared(&g_vs_lock);
plat_mutex_lock_shared(&g_vs_lock);
ne = g_vs_nev;
nc = g_vs_nctr;
memcpy(ev, g_vs_ev, sizeof ev);
memcpy(ctr, g_vs_ctr, sizeof ctr);
ReleaseSRWLockShared(&g_vs_lock);
for (int i = 0; i < nc; i++) InterlockedIncrement(ctr[i]);
plat_mutex_unlock_shared(&g_vs_lock);
for (int i = 0; i < nc; i++) plat_inc32(ctr[i]);
for (int i = 0; i < ne; i++) kern_signal_system_event(ev[i]);
}
return 0;
return;
}
static void vsync_ensure(void) {
EnterCriticalSection(&g_start_lock);
plat_rmutex_lock(&g_start_lock);
if (!g_vs_started) {
g_vs_started = 1;
g_vs_start_ns = (LONGLONG)kern_now_ns();
HANDLE h = CreateThread(NULL, 0, vsync_main, NULL, 0, NULL);
if (!h) rt_fatal("gfx: cannot start the vsync thread (error %lu)", GetLastError());
CloseHandle(h);
g_vs_start_ns = (int64_t)kern_now_ns();
if (!plat_thread_spawn(vsync_main, NULL, "vsync", 0)) rt_fatal("gfx: cannot start the vsync thread");
}
LeaveCriticalSection(&g_start_lock);
plat_rmutex_unlock(&g_start_lock);
}
void gfx_vsync_bind(uint64_t system_event) {
AcquireSRWLockExclusive(&g_vs_lock);
plat_mutex_lock(&g_vs_lock);
int have = 0;
for (int i = 0; i < g_vs_nev; i++) have |= g_vs_ev[i] == system_event;
if (!have) {
if (g_vs_nev == 16) rt_fatal("gfx: more than 16 vsync events");
g_vs_ev[g_vs_nev++] = system_event;
}
ReleaseSRWLockExclusive(&g_vs_lock);
plat_mutex_unlock(&g_vs_lock);
vsync_ensure();
}
void gfx_vsync_bind_counter(volatile long *counter) {
AcquireSRWLockExclusive(&g_vs_lock);
void gfx_vsync_bind_counter(volatile int32_t *counter) {
plat_mutex_lock(&g_vs_lock);
if (g_vs_nctr == 4) rt_fatal("gfx: more than 4 vsync counters");
g_vs_ctr[g_vs_nctr++] = counter;
ReleaseSRWLockExclusive(&g_vs_lock);
plat_mutex_unlock(&g_vs_lock);
vsync_ensure();
}
uint64_t gfx_vsync_ticks(void) { return (uint64_t)InterlockedCompareExchange64(&g_ticks, 0, 0); }
uint64_t gfx_vsync_ticks(void) { return (uint64_t)plat_cas64(&g_ticks, 0, 0); }
uint64_t gfx_vsync_time_ns(uint64_t tick) { return (uint64_t)g_vs_start_ns + tick * VSYNC_NS / VSYNC_HZ; }
// ---- focus
int gfx_window_focused(void) { return g_focused; }
unsigned gfx_window_focus_messages(void) { return (unsigned)g_focus_msgs; }
HWND gfx_window_handle(void) { return g_hwnd; }
// ---- window thread
#define WND_STYLE WS_OVERLAPPEDWINDOW
#define WM_APP_SETTINGS (WM_APP + 1)
#define WM_APP_FULLSCREEN (WM_APP + 2) // the fullscreen setting, held back until the window is first activated
static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window behind fullscreen (window thread)
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
unsigned gfx_window_dpi(void) { return (unsigned)InterlockedCompareExchange(&g_dpi, 0, 0); }
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
// at another display scale.
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
RECT r = {0, 0, cw, ch};
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
*ow = r.right - r.left;
*oh = r.bottom - r.top;
}
// TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=<w>x<h>: client area of the window (the
// swapchain follows the window; the game image is scaled to it).
static int cfg_fullscreen(void) {
return cfg_bool("TOTK_FULLSCREEN");
}
static void cfg_window_size(int *w, int *h) {
int x = 0, y = 0;
if (sscanf(cfg_value("TOTK_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320)
sscanf(cfg_meta("WINDOW_SIZE")->def, "%dx%d", &x, &y); // unparsable or out of range: the default
*w = x;
*h = y;
}
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
static void keep_awake(int on) {
static int cur = -1;
if (on == cur) return;
cur = on;
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
}
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
// user's windows and takes no focus.
static void toggle_fullscreen(HWND h) {
LONG style = GetWindowLongW(h, GWL_STYLE);
if (style & WS_OVERLAPPEDWINDOW) {
MONITORINFO mi = {sizeof mi};
if (!GetWindowPlacement(h, &g_saved_place) || !GetMonitorInfoW(MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY), &mi))
return;
SetWindowLongW(h, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
SetWindowPos(h, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top,
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | (g_background ? SWP_NOZORDER | SWP_NOACTIVATE : 0));
} else {
SetWindowLongW(h, GWL_STYLE, (style & ~WS_POPUP) | WND_STYLE);
if (g_background) g_saved_place.showCmd = SW_SHOWNOACTIVATE;
SetWindowPlacement(h, &g_saved_place);
SetWindowPos(h, NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED |
(g_background ? SWP_NOACTIVATE : 0));
}
}
// The window size and fullscreen state last taken from the settings (window thread). apply_settings acts on a change of
// either value only, so a window resized by hand keeps its size when another setting changes.
static char g_applied_size[32];
static int g_applied_fs;
static void note_applied(void) {
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
g_applied_fs = cfg_fullscreen();
}
// Window thread: the window takes the size and fullscreen state of the settings when they changed. In fullscreen the
// size goes into the placement restored when fullscreen ends.
static void apply_settings(HWND h) {
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
note_applied();
if (size_changed) {
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(h, cw, ch, &ow, &oh);
if (is_fullscreen(h)) {
RECT *n = &g_saved_place.rcNormalPosition;
n->right = n->left + ow;
n->bottom = n->top + oh;
} else {
RECT have;
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
}
if (fs_changed && fs != is_fullscreen(h)) {
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
}
}
void gfx_window_apply_settings(void) {
if (g_hwnd) PostMessageW(g_hwnd, WM_APP_SETTINGS, 0, 0);
}
// Present path (g_pl held): a settings change (cfg_version) reaches the window thread, which applies a changed size or
// fullscreen; a changed TOTK_VK_PRESENT rebuilds the swapchain at this present.
@@ -304,209 +214,16 @@ static void window_settings_live(void) {
unsigned v = cfg_version();
if (started && v == seen) return;
seen = v;
res_fov_live();
const char *e = cfg_value("TOTK_VK_PRESENT");
if (!started || strncmp(e, present, sizeof present - 1)) {
snprintf(present, sizeof present, "%s", e);
if (started) InterlockedExchange(&g_dirty, 1);
if (started) plat_xchg32(&g_dirty, 1);
}
if (started) gfx_window_apply_settings();
started = 1;
}
void gfx_window_request_close(void) {
if (g_hwnd) PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
}
// ---- refresh rate of the window's monitor (TOTK_FPS_LIMIT=display). The window thread measures it when the window
// opens, lands on another monitor or the display mode changes; readers take the stored value. QueryDisplayConfig gives
// the exact rational rate (59.94, 143.998); EnumDisplaySettings' whole hertz is the fallback.
static HMONITOR g_rate_mon;
static volatile LONG g_rate_mhz; // millihertz, 0 unknown
static double monitor_rate_hz(HMONITOR mon) {
MONITORINFOEXW mi = {.cbSize = sizeof mi};
if (!GetMonitorInfoW(mon, (MONITORINFO *)&mi)) return 0;
double hz = 0;
UINT32 np = 0, nm = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) == ERROR_SUCCESS && np && nm) {
DISPLAYCONFIG_PATH_INFO *paths = calloc(np, sizeof *paths);
DISPLAYCONFIG_MODE_INFO *modes = calloc(nm, sizeof *modes);
if (paths && modes && QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL) == ERROR_SUCCESS)
for (UINT32 i = 0; i < np && !hz; i++) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof src,
.adapterId = paths[i].sourceInfo.adapterId,
.id = paths[i].sourceInfo.id}};
DISPLAYCONFIG_RATIONAL r = paths[i].targetInfo.refreshRate;
if (DisplayConfigGetDeviceInfo(&src.header) == ERROR_SUCCESS && !wcscmp(src.viewGdiDeviceName, mi.szDevice) &&
r.Denominator)
hz = (double)r.Numerator / r.Denominator;
}
free(paths);
free(modes);
}
DEVMODEW dm = {.dmSize = sizeof dm};
if (!hz && EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
hz = dm.dmDisplayFrequency; // 0 and 1 mean the hardware default
return hz;
}
// Window thread. force: the display mode changed, measure even on the same monitor.
static void refresh_rate_update(HWND h, int force) {
HMONITOR mon = MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY);
if (!force && mon == g_rate_mon) return;
g_rate_mon = mon;
double hz = monitor_rate_hz(mon);
LONG mhz = (LONG)(hz * 1000 + 0.5);
if (InterlockedExchange(&g_rate_mhz, mhz) != mhz) rt_log("gfx: monitor refresh rate %.3f Hz", hz);
}
double gfx_window_refresh_hz(void) { return InterlockedCompareExchange(&g_rate_mhz, 0, 0) / 1000.0; }
int gfx_window_monitor_size(int *w, int *h) {
MONITORINFO mi = {sizeof mi};
if (!g_hwnd || !GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) return 0;
*w = mi.rcMonitor.right - mi.rcMonitor.left;
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
return 1;
}
static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
LRESULT input_res;
if (input_window_msg(h, m, w, l, &input_res)) return input_res;
if (menu_window_msg(h, m, w, l, &input_res)) return input_res;
switch (m) {
case WM_KEYDOWN:
if (w == VK_F11 && !(l & (1 << 30))) { // bit 30: auto-repeat
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
cfg_set("TOTK_FULLSCREEN", is_fullscreen(h) ? "1" : "0"); // the menu shows it; the next start keeps it
cfg_save();
note_applied(); // what cfg_get reads now (the environment may still override it)
}
break;
case WM_APP_SETTINGS:
apply_settings(h);
return 0;
case WM_APP_FULLSCREEN:
if (cfg_fullscreen() && !is_fullscreen(h)) {
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
}
return 0;
case WM_SIZE:
keep_awake(w != SIZE_MINIMIZED);
if (w != SIZE_MINIMIZED) InterlockedExchange(&g_dirty, 1);
refresh_rate_update(h, 0);
return 0;
case WM_EXITSIZEMOVE: // dragged onto another monitor
refresh_rate_update(h, 0);
break;
case WM_DISPLAYCHANGE:
refresh_rate_update(h, 1);
break;
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
if (!is_fullscreen(h) && !IsZoomed(h)) {
RECT c, r;
if (GetClientRect(h, &c)) {
r = (RECT){0, 0, c.right, c.bottom};
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
return TRUE;
}
}
}
break; // DefWindowProc: Windows scales the size with the DPI
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
const RECT *s = (const RECT *)l;
InterlockedExchange(&g_dpi, LOWORD(w));
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
MONITORINFO mi = {sizeof mi};
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
} else {
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
}
return 0;
}
case WM_ACTIVATEAPP: {
int in = w != 0;
InterlockedExchange(&g_focused, in);
oe_set_focus(in); // pinned to 1 by TOTK_ASSUME_FOCUS inside
oe_push_message(OE_MSG_FOCUS_STATE_CHANGED);
InterlockedIncrement(&g_focus_msgs);
if (in && g_fs_pending) {
g_fs_pending = 0;
PostMessageW(h, WM_APP_FULLSCREEN, 0, 0);
}
return 0;
}
case WM_CLOSE:
// The process ends with the window, as in MNG, once a save Commit still running in the background is done.
rt_log("gfx: window closed; exiting");
fs_save_flush();
vk_pipeline_save();
prof_write(); // ExitProcess runs no atexit handlers
ExitProcess(0);
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcW(h, m, w, l);
}
static DWORD WINAPI window_main(void *arg) {
(void)arg;
HINSTANCE inst = GetModuleHandleW(NULL);
// Icon resource 1 of totk.rc (runtime/totk.ico): the title bar, the taskbar button and Alt+Tab.
WNDCLASSEXW wc = {.cbSize = sizeof wc, .lpfnWndProc = wnd_proc, .hInstance = inst,
.hIcon = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON), LR_SHARED),
.hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
RegisterClassExW(&wc);
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(NULL, cw, ch, &ow, &oh);
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
NULL, inst, NULL);
if (!g_hwnd) {
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
g_wnd_failed = 1;
} else {
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
InterlockedExchange(&g_dpi, (LONG)GetDpiForWindow(g_hwnd));
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!background) {
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
FlashWindowEx(&fw);
g_fs_pending = cfg_fullscreen();
}
note_applied();
refresh_rate_update(g_hwnd, 1);
keep_awake(1);
}
SetEvent(g_wnd_ready);
if (g_wnd_failed) return 1;
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return 0;
}
// ---- swapchain
static const char *g_present_got = ""; // the TOTK_VK_PRESENT value of the mode the swapchain got; "" before one
@@ -546,7 +263,7 @@ static void flip_locked(void) {
VkResult r = vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi);
vk_queue_unlock();
vk_device_fault_report(r);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) InterlockedExchange(&g_dirty, 1);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1);
}
static void free_capture(void) {
@@ -564,10 +281,10 @@ static int create_swapchain(void) {
VkCtx *c = vk_ctx();
VkSurfaceCapabilitiesKHR caps;
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(c->phys, g_surface, &caps) != VK_SUCCESS) return 0;
RECT rc = {0};
GetClientRect(g_hwnd, &rc);
int cw = 0, ch = 0;
wsys_client_size(&cw, &ch);
VkExtent2D ext = caps.currentExtent.width != UINT32_MAX ? caps.currentExtent
: (VkExtent2D){(uint32_t)rc.right, (uint32_t)rc.bottom};
: (VkExtent2D){(uint32_t)cw, (uint32_t)ch};
if (!ext.width || !ext.height) return 0;
uint32_t nf = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(c->phys, g_surface, &nf, NULL);
@@ -624,7 +341,7 @@ static int create_swapchain(void) {
}
free_capture();
vk_queue_unlock();
InterlockedExchange(&g_dirty, 0);
plat_xchg32(&g_dirty, 0);
return 1;
}
@@ -654,15 +371,15 @@ static int ensure_capture_buffer(void) {
}
void vk_thumbs_enable(int n) {
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
if (!g_th_n && n > 0 && (g_th_tag = calloc((size_t)n, sizeof *g_th_tag))) g_th_n = n;
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
void vk_thumbs_frame(uint64_t frame) {
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
g_th_frame = frame;
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
static void free_thumbs(void) {
@@ -745,7 +462,7 @@ static void record_thumb(VkCommandBuffer cb, VkImage dst, unsigned fence_slot) {
int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
int ok = 0;
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
if (g_th_n && g_th_ptr && frame) {
unsigned ring = (unsigned)(frame % (uint64_t)g_th_n);
if (g_th_tag[ring] == frame) {
@@ -755,7 +472,7 @@ int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
ok = g_th_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_th_fmt == VK_FORMAT_B8G8R8A8_SRGB ? 2 : 1;
}
}
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
if (ok == 2)
for (size_t i = 0; i < TH_BYTES; i += 4) {
uint8_t t = rgba[i];
@@ -765,17 +482,17 @@ int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
}
void vk_present_capture(int on) {
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
g_capture = on;
if (!on) g_cap_valid = 0;
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
int vk_present_captured(const uint8_t **px, int *w, int *h, VkFormat *fmt) {
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
int ok = g_cap_valid;
if (ok) { *px = g_cap_ptr; *w = g_cap_w; *h = g_cap_h; *fmt = g_sc_fmt; }
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
return ok;
}
@@ -796,37 +513,30 @@ static void save_capture(const char *path) {
rt_log("gfx: menu test shot %s (%dx%d)", path, g_cap_w, g_cap_h);
}
// Where the game's picture (cw x ch) goes on the sw x sh swapchain: TOTK_ASPECT=fit (default) keeps its shape and centres
// it, with black bars at the sides (a 16:9 picture in a 21:9 window) or above and below; stretch fills the swapchain.
// Bars under a pixel are not drawn. Returns 1 when the rectangle leaves bars to clear.
static int fit_rect(int cw, int ch, int sw, int sh, int out[4]) {
out[0] = 0, out[1] = 0, out[2] = sw, out[3] = sh;
if (cw <= 0 || ch <= 0 || strcmp(cfg_value("TOTK_ASPECT"), "fit")) return 0;
long long a = (long long)cw * sh, b = (long long)ch * sw;
if (a < b) { // narrower than the window: bars at the sides
int fw = (int)((a + ch / 2) / ch);
if (fw >= sw - 1) return 0;
out[0] = (sw - fw) / 2, out[2] = fw;
} else if (a > b) { // wider: bars above and below
int fh = (int)(((long long)ch * sw + cw / 2) / cw);
if (fh >= sh - 1) return 0;
out[1] = (sh - fh) / 2, out[3] = fh;
} else {
return 0;
}
return 1;
// The rectangle {x, y, w, h} of a dw x dh swapchain image the cw x ch picture goes into: the whole image with
// TOTK_ASPECT=stretch, else (fit, the default) the largest one of the picture's own aspect ratio, centred (a 16:9 picture
// on the Steam Deck's 1280x800 screen gets bars of 40 rows above and below; a 16:9 picture in a 21:9 window, bars at the
// sides).
void gfx_fit_rect(int cw, int ch, int dw, int dh, int out[4]) {
out[0] = out[1] = 0, out[2] = dw, out[3] = dh;
if (cw <= 0 || ch <= 0 || !strcmp(cfg_value("TOTK_ASPECT"), "stretch")) return;
if ((int64_t)dw * ch > (int64_t)dh * cw) out[2] = (int)(((int64_t)dh * cw + ch / 2) / ch); // window wider: pillarbox
else out[3] = (int)(((int64_t)dw * ch + cw / 2) / cw); // taller: letterbox
// A side of the window's parity keeps the two bars equal; a picture one pixel short of the window fills it
out[2] += (dw - out[2]) & 1, out[3] += (dh - out[3]) & 1;
out[0] = (dw - out[2]) / 2, out[1] = (dh - out[3]) / 2;
}
static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) {
int general = layout == VK_IMAGE_LAYOUT_GENERAL;
gfx_window_start();
VkCtx *c = vk_ctx();
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
flip_locked();
if (!g_sc || InterlockedCompareExchange(&g_dirty, 0, 0)) {
if (!IsIconic(g_hwnd) && !create_swapchain() && !g_sc) goto out;
if (!g_sc || plat_cas32(&g_dirty, 0, 0)) {
if (!wsys_minimized() && !create_swapchain() && !g_sc) goto out;
}
if (IsIconic(g_hwnd) || !g_sc) goto out;
if (wsys_minimized() || !g_sc) goto out;
unsigned slot = g_frame % FRAMES;
if (vkWaitForFences(c->device, 1, &g_fence[slot], VK_TRUE, 5000000000ull) != VK_SUCCESS) {
@@ -836,13 +546,13 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
uint32_t idx = 0;
VkResult r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx);
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
InterlockedExchange(&g_dirty, 1);
plat_xchg32(&g_dirty, 1);
if (!create_swapchain()) goto out;
r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx);
}
vk_device_fault_report(r);
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame
if (r == VK_SUBOPTIMAL_KHR) InterlockedExchange(&g_dirty, 1);
if (r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1);
vkResetFences(c->device, 1, &g_fence[slot]);
if (g_th_pending[slot]) { // that fence has signalled: the thumbnail its submission wrote is complete
g_th_tag[g_th_pending[slot] % (uint64_t)g_th_n] = g_th_pending[slot];
@@ -876,30 +586,37 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
const int rect[4] = {cx, cy, cw, ch};
int to[4];
if (fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to)) {
// black bars: clear the whole image, then the picture's writes below wait for the clear
VkClearColorValue black = {{0, 0, 0, 1}};
int to[4]; // where the picture goes in the swapchain image: {x, y, w, h}
gfx_fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to);
if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) { // black bars beside or above and below it
VkClearColorValue black = {.float32 = {0, 0, 0, 1}};
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all);
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT);
}
// FSR (TOTK_UPSCALER) when the picture's place on the swapchain is larger than the crop; else the bilinear blit
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, to);
// Colour adjustment (TOTK_COLOR_*, vk_color.c): an adjusted copy of the crop takes the game image's place below
VkImage graded = general ? vk_color_record(cb, slot, src, fmt, rect) : VK_NULL_HANDLE;
const int grect[4] = {0, 0, cw, ch};
VkImage psrc = graded ? graded : src;
VkFormat pfmt = graded ? VK_FORMAT_R16G16B16A16_SFLOAT : fmt;
const int *prect = graded ? grect : rect;
// FSR (TOTK_UPSCALER) when the picture's rectangle is larger than the crop; else the bilinear blit
int fsr = general && vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to);
g_scaler = fsr ? 2 : cw != to[2] || ch != to[3];
if (!fsr) {
if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
VkImageBlit region = {
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{cx, cy, 0}, {cx + cw, cy + ch, 1}},
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.srcOffsets = {{prect[0], prect[1], 0}, {prect[0] + cw, prect[1] + ch, 1}},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}};
vkCmdBlitImage(cb, src, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
vkCmdBlitImage(cb, psrc, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region, VK_FILTER_LINEAR);
if (general) // later submissions that write the source wait for the blit's read
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0);
}
// last: the image's layout, and the stage and access of its last write, as each step leaves it
@@ -974,7 +691,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
if (menu_shot) save_capture(menu_shot);
}
out:
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
void vk_present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4]) {
@@ -986,25 +703,23 @@ void vk_present_blit(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int
}
void vk_present_flip(void) {
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
flip_locked();
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
void gfx_window_resize(int cw, int chh) {
gfx_window_start();
int ow, oh;
outer_size(g_hwnd, cw, chh, &ow, &oh);
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
EnterCriticalSection(&g_pl);
wsys_resize(cw, chh);
plat_rmutex_lock(&g_pl);
create_swapchain();
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
// Called by vk_shutdown before the device goes away; the window and its thread stay.
void gfx_window_release_vk(void) {
VkCtx *c = vk_ctx();
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
if (c && g_started) {
vk_queue_lock();
vkDeviceWaitIdle(c->device);
@@ -1012,6 +727,7 @@ void gfx_window_release_vk(void) {
g_flip_on = 0; // its blit is done (device idle); the swapchain goes away unpresented
menu_vk_release();
vk_fsr_release();
vk_color_release();
free_capture();
free_thumbs();
for (int i = 0; i < FRAMES; i++) {
@@ -1029,28 +745,18 @@ void gfx_window_release_vk(void) {
g_sc = VK_NULL_HANDLE; g_surface = VK_NULL_HANDLE; g_pool = VK_NULL_HANDLE; g_sc_n = 0;
g_started = 0; // the window and its thread stay; the next gfx_window_start makes a new surface
}
LeaveCriticalSection(&g_pl);
plat_rmutex_unlock(&g_pl);
}
void gfx_window_start(void) {
EnterCriticalSection(&g_start_lock);
plat_rmutex_lock(&g_start_lock);
if (!g_started) {
if (!vk_init()) rt_fatal("gfx: no usable Vulkan device (see the vk: lines above)");
VkCtx *c = vk_ctx();
if (!g_hwnd) {
g_wnd_failed = 0;
g_wnd_ready = CreateEventW(NULL, TRUE, FALSE, NULL);
HANDLE t = CreateThread(NULL, 0, window_main, NULL, 0, NULL);
if (!t) rt_fatal("gfx: cannot start the window thread (error %lu)", GetLastError());
CloseHandle(t);
WaitForSingleObject(g_wnd_ready, INFINITE);
CloseHandle(g_wnd_ready);
if (g_wnd_failed) rt_fatal("gfx: window creation failed");
}
VkWin32SurfaceCreateInfoKHR si = {.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
.hinstance = GetModuleHandleW(NULL), .hwnd = g_hwnd};
VkResult vr = vkCreateWin32SurfaceKHR(c->instance, &si, NULL, &g_surface);
if (vr != VK_SUCCESS) rt_fatal("gfx: vkCreateWin32SurfaceKHR failed (%d)", (int)vr);
static int opened;
if (!opened && !(opened = wsys_open())) rt_fatal("gfx: window creation failed");
g_surface = wsys_surface(c->instance);
if (!g_surface) rt_fatal("gfx: no Vulkan surface for the window");
VkCommandPoolCreateInfo pci = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = c->gfx_family};
@@ -1066,14 +772,14 @@ void gfx_window_start(void) {
}
g_frame = 0;
g_started = 1;
EnterCriticalSection(&g_pl);
plat_rmutex_lock(&g_pl);
create_swapchain();
LeaveCriticalSection(&g_pl);
RECT rc = {0};
GetClientRect(g_hwnd, &rc);
rt_log("gfx: window %ldx%ld at DPI %u, swapchain %ux%u format %d", rc.right, rc.bottom, gfx_window_dpi(),
g_sc_ext.width, g_sc_ext.height, (int)g_sc_fmt);
plat_rmutex_unlock(&g_pl);
int cw = 0, ch = 0;
wsys_client_size(&cw, &ch);
rt_log("gfx: window %dx%d at DPI %u, swapchain %ux%u format %d", cw, ch, gfx_window_dpi(), g_sc_ext.width,
g_sc_ext.height, (int)g_sc_fmt);
}
LeaveCriticalSection(&g_start_lock);
plat_rmutex_unlock(&g_start_lock);
vsync_ensure();
}
+212 -220
View File
@@ -38,7 +38,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include "cfg.h"
#include "gfx.h"
@@ -89,10 +88,10 @@ struct GfxShader {
uint32_t local[3];
};
static SRWLOCK cache_lock = SRWLOCK_INIT; // memory cache
static PlatMutex cache_lock = PLAT_MUTEX_INIT; // memory cache
static GfxShader *buckets[BUCKETS];
static UseEnt *use_buckets[BUCKETS]; // under cache_lock
static volatile LONG64 n_requests, n_hits, n_failures, n_keys, n_xlated; // n_keys: memory cache entries
static volatile int64_t n_requests, n_hits, n_failures, n_keys, n_xlated; // n_keys: memory cache entries
typedef struct { // one translator child
PlatChild *child; // NULL: not started
@@ -102,26 +101,26 @@ typedef struct { // one translator chi
} Svc;
static struct {
SRWLOCK lock;
CONDITION_VARIABLE freed;
PlatMutex lock;
PlatCond freed;
int limit; // children in use at most (0: svc_runtime_limit)
Svc s[MAX_SVC];
} S = {SRWLOCK_INIT, CONDITION_VARIABLE_INIT};
} S = {PLAT_MUTEX_INIT, PLAT_COND_INIT};
static struct { // translations in flight: cache key, or code hash while the
SRWLOCK lock; // use mask is unknown; one translation per id at a time
CONDITION_VARIABLE done;
PlatMutex lock; // use mask is unknown; one translation per id at a time
PlatCond done;
uint8_t id[2 * MAX_SVC][32];
int n;
} F = {SRWLOCK_INIT, CONDITION_VARIABLE_INIT};
} F = {PLAT_MUTEX_INIT, PLAT_COND_INIT};
#define PATH_CAP 1024 // bytes of a UTF-8 path
static char cache_dir[PATH_CAP];
static char xlate_exe[PATH_CAP];
static uint8_t xlate_id[32]; // translator identity (see the keys at the top)
static int host_f64 = 1, host_set; // gfx_shader_set_host
static uint32_t host_sbuf, host_sg[3] = {32, 32, 32};
static INIT_ONCE paths_once = INIT_ONCE_STATIC_INIT;
static uint32_t host_sbuf, host_sg[3] = {32, 32, 32}, host_gather;
static PlatOnce paths_once = PLAT_ONCE_INIT;
// ---- small helpers
@@ -138,7 +137,7 @@ static INIT_ONCE paths_once = INIT_ONCE_STATIC_INIT;
enum { PACK_SPV = 1, PACK_RES = 2, PACK_USE = 3, PACK_IN = 4 };
typedef struct { uint8_t name[32]; uint32_t kind, size; uint64_t offset; } PackEnt;
_Static_assert(sizeof(PackEnt) == 48, "shader-cache.pack entry layout");
static HANDLE pack_file = INVALID_HANDLE_VALUE;
static PlatFile *pack_file;
static PackEnt *pack_ix;
static uint32_t pack_n;
@@ -148,44 +147,38 @@ static int pack_cmp(const PackEnt *a, uint32_t kind, const uint8_t name[32]) {
}
static int pack_read_at(uint64_t offset, void *out, uint32_t n) {
OVERLAPPED o = {0};
o.Offset = (DWORD)offset, o.OffsetHigh = (DWORD)(offset >> 32);
DWORD got = 0;
return ReadFile(pack_file, out, n, &got, &o) && got == n;
return plat_file_pread(pack_file, out, n, offset) == (int64_t)n;
}
// Opens <cache>/shader-cache.pack and reads its index (init_paths). Nothing happens when there is none.
static void pack_open(void) {
char p[PATH_CAP + 32];
wchar_t w[PATH_CAP + 32];
snprintf(p, sizeof p, "%s\\shader-cache.pack", cache_dir);
if (!MultiByteToWideChar(CP_UTF8, 0, p, -1, w, PATH_CAP + 32)) return;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return;
LARGE_INTEGER size;
snprintf(p, sizeof p, "%s/shader-cache.pack", cache_dir);
PlatFile *h = plat_file_open(p);
if (!h) return;
PlatFileId id;
uint8_t head[16];
uint32_t n = 0;
const char *why = NULL;
PackEnt *ix = NULL;
pack_file = h;
if (!GetFileSizeEx(h, &size) || !pack_read_at(0, head, sizeof head) || memcmp(head, "TKSPACK1", 8)) why = "header";
if (!plat_file_id(h, &id) || !pack_read_at(0, head, sizeof head) || memcmp(head, "TKSPACK1", 8)) why = "header";
if (!why) {
memcpy(&n, head + 8, 4);
if (n > (1u << 24) || (uint64_t)n * sizeof *ix + sizeof head > (uint64_t)size.QuadPart ||
if (n > (1u << 24) || (uint64_t)n * sizeof *ix + sizeof head > id.size ||
!(ix = malloc((size_t)n * sizeof *ix + 1)))
why = "index size";
else if (!pack_read_at(sizeof head, ix, n * (uint32_t)sizeof *ix)) why = "index read";
}
for (uint32_t i = 0; !why && i < n; i++) {
if (ix[i].offset + ix[i].size > (uint64_t)size.QuadPart) why = "entry out of bounds";
if (ix[i].offset + ix[i].size > id.size) why = "entry out of bounds";
else if (i && pack_cmp(&ix[i - 1], ix[i].kind, ix[i].name) >= 0) why = "index order";
}
if (why) {
rt_log("gfx_shader: %s is not a usable cache pack (%s); reading the loose files", p, why);
free(ix);
CloseHandle(h);
pack_file = INVALID_HANDLE_VALUE;
plat_file_close(h);
pack_file = NULL;
return;
}
pack_ix = ix, pack_n = n;
@@ -264,7 +257,7 @@ static void xlate_identity(void) {
} else {
char p[PATH_CAP + 8];
strcpy(p, xlate_exe);
char *dot = strrchr(p, '.'), *sep = strrchr(p, '\\');
char *dot = strrchr(p, '.'), *sep = plat_last_sep(p);
if (dot && dot > sep) *dot = 0;
strcat(p, ".id");
uint64_t size = 0;
@@ -286,16 +279,16 @@ static void xlate_identity(void) {
// next to totk.exe, so every build dir has its own (as the pipeline cache, vk_pipeline.c cache_path_init).
static void use_preload_start(void);
static BOOL CALLBACK init_paths(PINIT_ONCE once, PVOID param, PVOID *ctx) {
(void)once, (void)param, (void)ctx;
static void init_paths(void *param) {
(void)param;
char exe[PATH_CAP], root[PATH_CAP] = "", exe_dir[PATH_CAP] = "";
if (plat_exe_path(exe, sizeof exe)) {
char *last = strrchr(exe, '\\');
char *last = plat_last_sep(exe);
if (last) snprintf(exe_dir, sizeof exe_dir, "%.*s", (int)(last - exe), exe);
for (char *cut = strrchr(exe, '\\'); cut; cut = strrchr(exe, '\\')) {
for (char *cut = plat_last_sep(exe); cut; cut = plat_last_sep(exe)) {
*cut = 0;
char probe[PATH_CAP + 32];
snprintf(probe, sizeof probe, "%s\\build\\xlate\\totk_xlate.exe", exe);
snprintf(probe, sizeof probe, "%s/build/xlate/totk_xlate" PLAT_EXE_SUFFIX, exe);
if (file_exists(probe)) {
strcpy(root, exe);
break;
@@ -304,31 +297,33 @@ static BOOL CALLBACK init_paths(PINIT_ONCE once, PVOID param, PVOID *ctx) {
}
const char *xlate_cfg = cfg_get("TOTK_XLATE"), *cache_cfg = cfg_get("TOTK_SHADER_CACHE");
char beside[PATH_CAP + 32]; // a packaged folder (tools/package_build.py) keeps the translator next to totk.exe
if (exe_dir[0]) snprintf(beside, sizeof beside, "%s\\totk_xlate.exe", exe_dir);
if (exe_dir[0]) snprintf(beside, sizeof beside, "%s/totk_xlate" PLAT_EXE_SUFFIX, exe_dir);
if (xlate_cfg && *xlate_cfg && strlen(xlate_cfg) < sizeof xlate_exe) strcpy(xlate_exe, xlate_cfg);
else if (exe_dir[0] && file_exists(beside)) strcpy(xlate_exe, beside);
else if (root[0]) snprintf(xlate_exe, sizeof xlate_exe, "%s\\build\\xlate\\totk_xlate.exe", root);
else if (root[0]) snprintf(xlate_exe, sizeof xlate_exe, "%s/build/xlate/totk_xlate" PLAT_EXE_SUFFIX, root);
if (cache_cfg && *cache_cfg && strlen(cache_cfg) < sizeof cache_dir) strcpy(cache_dir, cache_cfg);
else if (exe_dir[0]) snprintf(cache_dir, sizeof cache_dir, "%s\\shader-cache", exe_dir);
else if (exe_dir[0]) snprintf(cache_dir, sizeof cache_dir, "%s/shader-cache", exe_dir);
xlate_identity();
if (cache_dir[0]) pack_open();
use_preload_start();
return TRUE;
return;
}
static void paths(void) { InitOnceExecuteOnce(&paths_once, init_paths, NULL, NULL); }
static void paths(void) { plat_once(&paths_once, init_paths, NULL); }
// Host answers (vk_caps.c, at device creation, before any translation). Answers other than the defaults (Float64,
// exact storage offsets, 32 lanes) are mixed into the translator identity, so they get their own cache keys.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3]) {
// exact storage offsets, 32 lanes, no gather bias) are mixed into the translator identity, so they get their own cache
// keys.
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits) {
paths();
if (host_set++) return;
host_f64 = float64, host_sbuf = sbuf_align;
host_f64 = float64, host_sbuf = sbuf_align, host_gather = gather_bits;
memcpy(host_sg, subgroup, sizeof host_sg);
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32) return;
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32 && !gather_bits) return;
uint8_t buf[32 + 96];
int n = snprintf((char *)buf + 32, 96, "TKHO f64=%d sbuf=%u sg=%u,%u,%u", float64, sbuf_align, subgroup[0], subgroup[1],
subgroup[2]);
if (gather_bits) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " gather=%u", gather_bits);
memcpy(buf, xlate_id, 32);
rt_sha256(buf, 32 + (size_t)n, xlate_id);
rt_log("gfx_shader: host options %s: own shader cache keys", (char *)buf + 37);
@@ -361,7 +356,7 @@ static int child_start(Svc *v) {
// One line out, one line in (no newline in the reply). 0 when the pipe broke.
static int child_exchange_raw(Svc *v, const char *line, char *reply, size_t reply_size) {
if (!v->child && !child_start(v)) return 0;
InterlockedIncrement64(&n_requests);
plat_inc64(&n_requests);
size_t n = strlen(line);
if (!plat_child_write(v->child, line, n)) return 0;
if (!plat_child_write(v->child, "\n", 1)) return 0;
@@ -392,11 +387,11 @@ static int child_exchange(Svc *v, const char *line, char *reply, size_t reply_si
// Children allowed at run time: TOTK_XLATE_PROCS, default 2.
static int svc_runtime_limit(void) {
static volatile LONG n;
static volatile int32_t n;
if (!n) {
const char *e = cfg_get("TOTK_XLATE_PROCS");
int v = e && *e ? atoi(e) : 2;
InterlockedExchange(&n, v < 1 ? 1 : v > MAX_SVC ? MAX_SVC : v);
plat_xchg32(&n, v < 1 ? 1 : v > MAX_SVC ? MAX_SVC : v);
}
return (int)n;
}
@@ -409,7 +404,7 @@ static int svc_used(void) { // caller holds S.loc
// A child for one request: an idle running one, else a new slot while fewer than S.limit are in use, else waits.
static Svc *svc_get(void) {
AcquireSRWLockExclusive(&S.lock);
plat_mutex_lock(&S.lock);
if (!S.limit) S.limit = svc_runtime_limit();
Svc *v;
for (;;) {
@@ -422,42 +417,42 @@ static Svc *svc_get(void) {
}
if (!v && empty && svc_used() < S.limit) v = empty;
if (v) break;
SleepConditionVariableSRW(&S.freed, &S.lock, INFINITE, 0);
plat_cond_wait(&S.freed, &S.lock);
}
v->busy = 1;
ReleaseSRWLockExclusive(&S.lock);
plat_mutex_unlock(&S.lock);
return v;
}
// Returns a child; one over the limit (svc_set_limit lowered it) is stopped.
static void svc_put(Svc *v) {
AcquireSRWLockExclusive(&S.lock);
plat_mutex_lock(&S.lock);
int stop = v->child && svc_used() > S.limit;
if (!stop) v->busy = 0;
ReleaseSRWLockExclusive(&S.lock);
plat_mutex_unlock(&S.lock);
if (stop) {
child_stop(v);
AcquireSRWLockExclusive(&S.lock);
plat_mutex_lock(&S.lock);
v->busy = 0;
ReleaseSRWLockExclusive(&S.lock);
plat_mutex_unlock(&S.lock);
}
WakeAllConditionVariable(&S.freed);
plat_cond_broadcast(&S.freed);
}
// Sets the number of children; idle ones over it are stopped now, busy ones when they are returned.
static void svc_set_limit(int n) {
Svc *stop[MAX_SVC];
int k = 0;
AcquireSRWLockExclusive(&S.lock);
plat_mutex_lock(&S.lock);
S.limit = n < 1 ? 1 : n > MAX_SVC ? MAX_SVC : n;
for (int i = 0, used = svc_used(); i < MAX_SVC && used > S.limit; i++)
if (!S.s[i].busy && S.s[i].child) S.s[i].busy = 1, stop[k++] = &S.s[i], used--;
ReleaseSRWLockExclusive(&S.lock);
plat_mutex_unlock(&S.lock);
for (int i = 0; i < k; i++) child_stop(stop[i]);
AcquireSRWLockExclusive(&S.lock);
plat_mutex_lock(&S.lock);
for (int i = 0; i < k; i++) stop[i]->busy = 0;
ReleaseSRWLockExclusive(&S.lock);
WakeAllConditionVariable(&S.freed);
plat_mutex_unlock(&S.lock);
plat_cond_broadcast(&S.freed);
}
// One request on a child; a dead child is restarted once. 0 when the translator is unavailable. The caller has given
@@ -473,35 +468,35 @@ static int svc_request(const char *line, char *reply, size_t reply_size) {
// One translation per id at a time (F); F.id full also waits.
static void flight_enter(const uint8_t id[32]) {
AcquireSRWLockExclusive(&F.lock);
plat_mutex_lock(&F.lock);
for (;;) {
int busy = F.n == 2 * MAX_SVC;
for (int i = 0; i < F.n && !busy; i++) busy = !memcmp(F.id[i], id, 32);
if (!busy) break;
SleepConditionVariableSRW(&F.done, &F.lock, INFINITE, 0);
plat_cond_wait(&F.done, &F.lock);
}
memcpy(F.id[F.n++], id, 32);
ReleaseSRWLockExclusive(&F.lock);
plat_mutex_unlock(&F.lock);
}
static void flight_leave(const uint8_t id[32]) {
AcquireSRWLockExclusive(&F.lock);
plat_mutex_lock(&F.lock);
for (int i = 0; i < F.n; i++)
if (!memcmp(F.id[i], id, 32)) {
memcpy(F.id[i], F.id[--F.n], 32);
break;
}
ReleaseSRWLockExclusive(&F.lock);
WakeAllConditionVariable(&F.done);
plat_mutex_unlock(&F.lock);
plat_cond_broadcast(&F.done);
}
// ---- cache
static GfxShader *mem_find(const uint8_t key[32]) {
AcquireSRWLockShared(&cache_lock);
plat_mutex_lock_shared(&cache_lock);
GfxShader *s = buckets[(key[0] | key[1] << 8) % BUCKETS];
while (s && memcmp(s->key, key, 32)) s = s->next;
ReleaseSRWLockShared(&cache_lock);
plat_mutex_unlock_shared(&cache_lock);
return s;
}
@@ -514,7 +509,7 @@ static void reflect(GfxShader *s);
// Inserts s, or frees it and returns the entry another thread inserted first.
static GfxShader *mem_insert(GfxShader *s) {
AcquireSRWLockExclusive(&cache_lock);
plat_mutex_lock(&cache_lock);
GfxShader **head = &buckets[(s->key[0] | s->key[1] << 8) % BUCKETS], *r = *head;
while (r && memcmp(r->key, s->key, 32)) r = r->next;
if (r) {
@@ -525,7 +520,7 @@ static GfxShader *mem_insert(GfxShader *s) {
*head = s;
n_keys++;
}
ReleaseSRWLockExclusive(&cache_lock);
plat_mutex_unlock(&cache_lock);
return s;
}
@@ -565,20 +560,20 @@ static int spirv_complete(const uint32_t *w, uint64_t n) {
// Reads <key>.spv and <key>.res.json from the pack (packed 1) or the loose files; NULL if either is missing or
// malformed. iid: the input id, NULL when the caller sets it.
static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int packed) {
static volatile LONG n_bad;
static volatile int32_t n_bad;
char hex[65], p[PATH_CAP + 80];
key_hex(key, hex);
uint64_t spv_size = 0, res_size = 0;
snprintf(p, PATH_CAP + 80, "%s\\%s.res.json", cache_dir, hex);
snprintf(p, PATH_CAP + 80, "%s/%s.res.json", cache_dir, hex);
uint8_t *res = packed ? pack_read(PACK_RES, key, &res_size) : read_file(p, &res_size);
if (!res) return NULL;
if (!reflection_valid((const char *)res, res_size)) {
LONG n = InterlockedIncrement(&n_bad);
if (n <= 3 || !(n & (n - 1))) rt_log("gfx_shader: %s.res.json is malformed, translating again (%ld so far)", hex, n);
int32_t n = plat_inc32(&n_bad);
if (n <= 3 || !(n & (n - 1))) rt_log("gfx_shader: %s.res.json is malformed, translating again (%d so far)", hex, n);
free(res);
return NULL;
}
snprintf(p, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
snprintf(p, PATH_CAP + 80, "%s/%s.spv", cache_dir, hex);
uint8_t *spv = packed ? pack_read(PACK_SPV, key, &spv_size) : read_file(p, &spv_size);
uint32_t magic = 0;
if (spv && spv_size >= 20) memcpy(&magic, spv, 4);
@@ -814,7 +809,7 @@ static void use_name(const uint8_t h[32], uint8_t name[32]) {
static void use_path_of(const uint8_t name[32], char *p) {
char hex[65];
key_hex(name, hex);
snprintf(p, PATH_CAP + 80, "%s\\%s.use", cache_dir, hex);
snprintf(p, PATH_CAP + 80, "%s/%s.use", cache_dir, hex);
}
static void use_path(const uint8_t h[32], char *p) {
@@ -830,7 +825,7 @@ static void use_path(const uint8_t h[32], char *p) {
#define USE_FILE_BUCKETS 65536
typedef struct UseFile { struct UseFile *next; uint8_t name[32]; UseMask m; } UseFile;
static UseFile *use_files[USE_FILE_BUCKETS];
static volatile LONG use_files_ready;
static volatile int32_t use_files_ready;
static UseFile **use_file_slot(const uint8_t name[32]) {
UseFile **e = &use_files[(name[0] | name[1] << 8) % USE_FILE_BUCKETS];
@@ -848,10 +843,40 @@ static int hex_byte(const char *c) {
return v;
}
static DWORD WINAPI use_preload(void *arg) {
typedef struct { int n, bad; } UsePreload;
// One <64 hex digits>.use file of the cache directory into the table.
static int use_preload_file(const char *name, void *ctx) {
UsePreload *st = ctx;
char p[PATH_CAP + 80];
UseFile e = {0};
int ok = strlen(name) == 68;
for (int i = 0; ok && i < 32; i++) {
int v = hex_byte(name + 2 * i);
ok = v >= 0, e.name[i] = (uint8_t)v;
}
uint64_t len = 0;
uint32_t *u = NULL;
if (ok) {
snprintf(p, sizeof p, "%s/%s", cache_dir, name);
u = (uint32_t *)read_file(p, &len);
ok = u && len == 20 && u[0] == USE_MAGIC && u[1] == USE_VERSION;
}
UseFile **slot = ok ? use_file_slot(e.name) : NULL, *x;
if (slot && !*slot && (x = malloc(sizeof *x))) {
e.m.slots = u[2], e.m.attrs = u[3], e.m.fouts = u[4];
*x = e, *slot = x, st->n++;
} else if (!ok) {
st->bad++;
}
free(u);
return 1;
}
static void use_preload(void *arg) {
(void)arg;
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
uint64_t t0 = plat_now_ns();
char pattern[PATH_CAP + 16], p[PATH_CAP + 80];
int n = 0, bad = 0;
for (uint32_t i = 0; i < pack_n; i++) { // the packed masks; the loose files below add the masks found since
uint32_t u[5];
@@ -868,45 +893,18 @@ static DWORD WINAPI use_preload(void *arg) {
*slot = x, n++;
}
}
snprintf(pattern, sizeof pattern, "%s\\*.use", cache_dir);
WIN32_FIND_DATAA fd;
HANDLE f = FindFirstFileExA(pattern, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, FIND_FIRST_EX_LARGE_FETCH);
if (f != INVALID_HANDLE_VALUE) {
do {
UseFile e = {0};
int ok = strlen(fd.cFileName) == 68;
for (int i = 0; ok && i < 32; i++) {
int v = hex_byte(fd.cFileName + 2 * i);
ok = v >= 0, e.name[i] = (uint8_t)v;
}
uint64_t len = 0;
uint32_t *u = NULL;
if (ok) {
snprintf(p, sizeof p, "%s\\%s", cache_dir, fd.cFileName);
u = (uint32_t *)read_file(p, &len);
ok = u && len == 20 && u[0] == USE_MAGIC && u[1] == USE_VERSION;
}
UseFile **slot = ok ? use_file_slot(e.name) : NULL, *x;
if (slot && !*slot && (x = malloc(sizeof *x))) {
e.m.slots = u[2], e.m.attrs = u[3], e.m.fouts = u[4];
*x = e, *slot = x, n++;
} else if (!ok) {
bad++;
}
free(u);
} while (FindNextFileA(f, &fd));
FindClose(f);
}
InterlockedExchange(&use_files_ready, 1);
UsePreload st = {0};
plat_file_list(cache_dir, ".use", use_preload_file, &st);
n += st.n, bad += st.bad;
plat_xchg32(&use_files_ready, 1);
rt_log("gfx_shader: %d use masks read in %.0f ms on a background thread (%d unreadable)", n,
(plat_now_ns() - t0) / 1e6, bad);
return 0;
return;
}
static void use_preload_start(void) {
if (!cache_dir[0]) return;
HANDLE t = CreateThread(NULL, 0, use_preload, NULL, 0, NULL);
if (t) SetThreadPriority(t, THREAD_PRIORITY_BELOW_NORMAL), CloseHandle(t);
plat_thread_spawn(use_preload, NULL, "use preload", 0);
}
static UseEnt **use_slot(const uint8_t h[32]) { // caller holds cache_lock
@@ -925,15 +923,15 @@ static void use_put(const uint8_t h[32], int known, const UseMask *m) {
// 1 with *m when the use mask of h is known (memory, then <hash>.use; a disk miss is remembered).
static int use_find(const uint8_t h[32], UseMask *m) {
AcquireSRWLockShared(&cache_lock);
plat_mutex_lock_shared(&cache_lock);
UseEnt *e = *use_slot(h);
int seen = e != NULL, known = e && e->known;
if (known) *m = e->m;
ReleaseSRWLockShared(&cache_lock);
plat_mutex_unlock_shared(&cache_lock);
if (seen) return known;
uint8_t name[32];
use_name(h, name);
if (InterlockedCompareExchange(&use_files_ready, 0, 0)) { // complete: a name not in it has no file
if (plat_cas32(&use_files_ready, 0, 0)) { // complete: a name not in it has no file
UseFile *u = *use_file_slot(name);
if ((known = u != NULL)) *m = u->m;
} else {
@@ -946,9 +944,9 @@ static int use_find(const uint8_t h[32], UseMask *m) {
if (known) m->slots = f[2], m->attrs = f[3], m->fouts = f[4];
free(f);
}
AcquireSRWLockExclusive(&cache_lock);
plat_mutex_lock(&cache_lock);
use_put(h, known, m);
ReleaseSRWLockExclusive(&cache_lock);
plat_mutex_unlock(&cache_lock);
return known;
}
@@ -957,9 +955,9 @@ static void use_store(const uint8_t h[32], const UseMask *m) {
use_path(h, p);
uint32_t f[5] = {USE_MAGIC, USE_VERSION, m->slots, m->attrs, m->fouts};
plat_file_write_atomic(p, f, sizeof f);
AcquireSRWLockExclusive(&cache_lock);
plat_mutex_lock(&cache_lock);
use_put(h, 1, m);
ReleaseSRWLockExclusive(&cache_lock);
plat_mutex_unlock(&cache_lock);
}
static void put_u32(uint8_t **p, uint32_t v) { memcpy(*p, &v, 4), *p += 4; }
@@ -1089,6 +1087,7 @@ static size_t build_request(char *o0, const char *hex, const char *stage, const
if (!host_f64) o += sprintf(o, ",\"softfloat64\":true");
if (host_sbuf) o += sprintf(o, ",\"sbuf_align\":%u", host_sbuf);
if (host_sg[si] != 32) o += sprintf(o, ",\"subgroup\":%u", host_sg[si]);
if (host_gather) o += sprintf(o, ",\"gather_bias\":%u", host_gather);
o += sprintf(o, "},\"out\":\"");
json_esc(&o, cache_dir);
o += sprintf(o, "\"}");
@@ -1114,7 +1113,7 @@ uint64_t gfx_shader_cache_hits(void) { return (uint64_t)n_hits; } // gets an
// Drops the memory cache (the probe uses it to prove the disk cache). Pointers handed out before are invalid.
void gfx_shader_drop_memory(void) {
AcquireSRWLockExclusive(&cache_lock);
plat_mutex_lock(&cache_lock);
for (int i = 0; i < BUCKETS; i++) {
for (GfxShader *s = buckets[i], *n; s; s = n) {
n = s->next;
@@ -1126,16 +1125,16 @@ void gfx_shader_drop_memory(void) {
use_buckets[i] = NULL;
}
// the preloaded .use table misses the masks this run wrote, which only use_buckets held: back to the disk
InterlockedExchange(&use_files_ready, 0);
plat_xchg32(&use_files_ready, 0);
for (int i = 0; i < USE_FILE_BUCKETS; i++) {
for (UseFile *e = use_files[i], *n; e; e = n) n = e->next, free(e);
use_files[i] = NULL;
}
ReleaseSRWLockExclusive(&cache_lock);
plat_mutex_unlock(&cache_lock);
}
static const GfxShader *fail(const char *why, const char *hex) {
InterlockedIncrement64(&n_failures);
plat_inc64(&n_failures);
rt_log("gfx_shader: translation failed (%s): %s", hex ? hex : "-", why);
return NULL;
}
@@ -1181,10 +1180,10 @@ static const GfxShader *get_async_one(Req r, int stage, const uint8_t *control,
static const GfxShader *get_one(const Req *r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a) {
GfxShader *s = mem_find(r->key);
if (s) return InterlockedIncrement64(&n_hits), s;
if (s) return plat_inc64(&n_hits), s;
paths();
if (!cache_dir[0]) return fail("no cache directory", NULL);
if (r->final && (s = disk_load(r->key, r->iid))) return InterlockedIncrement64(&n_hits), mem_insert(s);
if (r->final && (s = disk_load(r->key, r->iid))) return plat_inc64(&n_hits), mem_insert(s);
return translate(r, stage, control, code, a);
}
@@ -1214,7 +1213,7 @@ typedef struct {
static void in_path(const uint8_t iid[32], char *p) {
char hex[65];
key_hex(iid, hex);
snprintf(p, PATH_CAP + 80, "%s\\in\\%s.in", cache_dir, hex);
snprintf(p, PATH_CAP + 80, "%s/in/%s.in", cache_dir, hex);
}
// Stores the inputs of a translation unless they are stored already; the file appears whole (written, then renamed).
@@ -1233,7 +1232,7 @@ static void in_store(const uint8_t iid[32], int stage, const uint8_t *control, c
memcpy(o, control, CONTROL_SIZE), o += CONTROL_SIZE;
memcpy(o, code, r->length), o += r->length;
if (r->c1_len) memcpy(o, r->c1, r->c1_len);
snprintf(tmp, PATH_CAP + 120, "%s\\in", cache_dir);
snprintf(tmp, PATH_CAP + 120, "%s/in", cache_dir);
mkdir_p(tmp);
plat_file_write_atomic(p, buf, n);
free(buf);
@@ -1272,16 +1271,16 @@ static const GfxShader *translate_one(const Req *r, int stage, const uint8_t *co
// Another thread may have translated it while this one waited in flight_enter.
const uint8_t *look = r->final ? key : NULL, *look_iid = r->iid;
if (!r->final && use_find(r->code_hash, &m)) build_key(r->code_hash, a, &m, mk, miid), look = mk, look_iid = miid;
if (look && (s = mem_find(look))) return InterlockedIncrement64(&n_hits), s;
if (look && (s = disk_load(look, look_iid))) return InterlockedIncrement64(&n_hits), mem_insert(s);
if (look && (s = mem_find(look))) return plat_inc64(&n_hits), s;
if (look && (s = disk_load(look, look_iid))) return plat_inc64(&n_hits), mem_insert(s);
uint64_t skip = stage == GFX_STAGE_CS ? 0 : CODE_SKIP;
char hex[65], code_path[PATH_CAP + 80], spv_path[PATH_CAP + 80], res_path[PATH_CAP + 80];
key_hex(key, hex);
mkdir_p(cache_dir);
snprintf(code_path, PATH_CAP + 80, "%s\\%s.code", cache_dir, hex);
snprintf(spv_path, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
snprintf(res_path, PATH_CAP + 80, "%s\\%s.res.json", cache_dir, hex);
snprintf(code_path, PATH_CAP + 80, "%s/%s.code", cache_dir, hex);
snprintf(spv_path, PATH_CAP + 80, "%s/%s.spv", cache_dir, hex);
snprintf(res_path, PATH_CAP + 80, "%s/%s.res.json", cache_dir, hex);
if (!write_file(code_path, code + skip, (size_t)(r->length - skip))) return fail("cannot write the code file", hex);
size_t cap = 2 * PATH_CAP * 6 + 2 * r->c1_len + 8192;
char *req = malloc(cap), reply[LINE_MAX_LEN];
@@ -1302,22 +1301,22 @@ static const GfxShader *translate_one(const Req *r, int stage, const uint8_t *co
build_key(r->code_hash, a, &m, mk, miid);
char mhex[65], to[PATH_CAP + 80];
key_hex(mk, mhex);
snprintf(to, PATH_CAP + 80, "%s\\%s.spv", cache_dir, mhex);
snprintf(to, PATH_CAP + 80, "%s/%s.spv", cache_dir, mhex);
int moved = plat_file_replace(spv_path, to);
snprintf(to, PATH_CAP + 80, "%s\\%s.res.json", cache_dir, mhex);
snprintf(to, PATH_CAP + 80, "%s/%s.res.json", cache_dir, mhex);
moved = moved && plat_file_replace(res_path, to);
if (moved) {
use_store(r->code_hash, &m);
} else { // keep the mask for this run only
plat_file_delete(spv_path), plat_file_delete(res_path), rt_log("gfx_shader: cannot rename %s", hex);
AcquireSRWLockExclusive(&cache_lock);
plat_mutex_lock(&cache_lock);
use_put(r->code_hash, 1, &m);
ReleaseSRWLockExclusive(&cache_lock);
plat_mutex_unlock(&cache_lock);
}
memcpy(s->key, mk, 32), memcpy(s->iid, miid, 32);
}
in_store(s->iid, stage, control, code, r, a);
InterlockedIncrement64(&n_xlated);
plat_inc64(&n_xlated);
return mem_insert(s);
}
@@ -1353,14 +1352,14 @@ typedef struct XWait {
} XWait;
static struct {
SRWLOCK lock;
CONDITION_VARIABLE work, done; // done: a job finished (gfx_shader_await)
PlatMutex lock;
PlatCond work, done; // done: a job finished (gfx_shader_await)
XJob *head, *tail;
XWait *waits[256];
int started; // 1 running, -1 no thread could be created
} X = {SRWLOCK_INIT, CONDITION_VARIABLE_INIT, CONDITION_VARIABLE_INIT};
} X = {PLAT_MUTEX_INIT, PLAT_COND_INIT, PLAT_COND_INIT};
static volatile LONG64 n_async;
static volatile int64_t n_async;
// Caller holds X.lock.
static XWait **wait_slot(const uint8_t key[32]) {
@@ -1369,25 +1368,25 @@ static XWait **wait_slot(const uint8_t key[32]) {
return w;
}
static DWORD WINAPI xlate_worker(void *arg) {
static void xlate_worker(void *arg) {
(void)arg;
for (;;) {
AcquireSRWLockExclusive(&X.lock);
while (!X.head) SleepConditionVariableSRW(&X.work, &X.lock, INFINITE, 0);
plat_mutex_lock(&X.lock);
while (!X.head) plat_cond_wait(&X.work, &X.lock);
XJob *j = X.head;
if (!(X.head = j->next)) X.tail = NULL;
ReleaseSRWLockExclusive(&X.lock);
plat_mutex_unlock(&X.lock);
const GfxShader *s = translate(&j->r, j->stage, j->control, j->code, &j->a);
AcquireSRWLockExclusive(&X.lock);
plat_mutex_lock(&X.lock);
XWait **w = wait_slot(j->r.key), *e = *w;
if (e && s) *w = e->next, free(e);
else if (e) e->failed = 1;
ReleaseSRWLockExclusive(&X.lock);
WakeAllConditionVariable(&X.done);
if (s) InterlockedIncrement64(&n_async);
plat_mutex_unlock(&X.lock);
plat_cond_broadcast(&X.done);
if (s) plat_inc64(&n_async);
free(j->control), free(j->code), free(j);
}
return 0;
return;
}
const GfxShader *gfx_shader_get_async(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
const GfxShaderAnswers *a, int *pending) {
@@ -1402,25 +1401,24 @@ const GfxShader *gfx_shader_get_async(int stage, const uint8_t *control, const u
static const GfxShader *get_async_one(Req r, int stage, const uint8_t *control, const uint8_t *code,
const GfxShaderAnswers *a, int *pending) {
GfxShader *s = mem_find(r.key);
if (s) return InterlockedIncrement64(&n_hits), s;
AcquireSRWLockShared(&X.lock);
if (s) return plat_inc64(&n_hits), s;
plat_mutex_lock_shared(&X.lock);
XWait *w = *wait_slot(r.key);
int failed = w && w->failed;
ReleaseSRWLockShared(&X.lock);
plat_mutex_unlock_shared(&X.lock);
if (w) return *pending = !failed, NULL;
paths();
if (!cache_dir[0]) return fail("no cache directory", NULL);
if (r.final && (s = disk_load(r.key, r.iid))) return InterlockedIncrement64(&n_hits), mem_insert(s);
if (r.final && (s = disk_load(r.key, r.iid))) return plat_inc64(&n_hits), mem_insert(s);
XJob *j = calloc(1, sizeof *j);
XWait *e = calloc(1, sizeof *e);
uint8_t *ctl = malloc(CONTROL_SIZE), *cd = malloc(r.length);
AcquireSRWLockExclusive(&X.lock);
plat_mutex_lock(&X.lock);
if (!X.started) {
int n = 0;
for (int i = 0; i < svc_runtime_limit(); i++) {
HANDLE t = CreateThread(NULL, 0, xlate_worker, NULL, 0, NULL);
if (t) CloseHandle(t), n++;
if (plat_thread_spawn(xlate_worker, NULL, "xlate", 0)) n++;
}
X.started = n ? 1 : -1;
if (n) rt_log("gfx_shader: graphics shaders are translated by %d background workers (TOTK_XLATE_PROCS)", n);
@@ -1438,9 +1436,9 @@ static const GfxShader *get_async_one(Req r, int stage, const uint8_t *control,
if (X.tail) X.tail->next = j;
else X.head = j;
X.tail = j;
WakeConditionVariable(&X.work);
plat_cond_signal(&X.work);
}
ReleaseSRWLockExclusive(&X.lock);
plat_mutex_unlock(&X.lock);
if (queued) return *pending = 1, NULL;
free(j), free(e), free(ctl), free(cd);
if (raced) return *pending = 1, NULL;
@@ -1454,7 +1452,7 @@ const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8
Req r;
if (!pending || !req_init(&r, stage, control, code, code_size, NULL, a)) return s;
uint64_t p0 = perf_begin(); // the wait counts as the render thread's translation time
AcquireSRWLockExclusive(&X.lock);
plat_mutex_lock(&X.lock);
for (XJob *j = X.head, *prev = NULL; j; prev = j, j = j->next)
if (!memcmp(j->r.key, r.key, 32)) { // queued: to the front, the worker takes it next
if (prev) {
@@ -1465,8 +1463,8 @@ const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8
break;
}
XWait *w;
while ((w = *wait_slot(r.key)) && !w->failed) SleepConditionVariableSRW(&X.done, &X.lock, INFINITE, 0);
ReleaseSRWLockExclusive(&X.lock);
while ((w = *wait_slot(r.key)) && !w->failed) plat_cond_wait(&X.done, &X.lock);
plat_mutex_unlock(&X.lock);
perf_end(PERF_XLATE, p0);
if (w) return NULL; // failed: logged by the worker
s = gfx_shader_get_async(stage, control, code, code_size, a, &pending);
@@ -1525,28 +1523,28 @@ typedef struct {
const GfxShader **out;
uint8_t (*other)[32]; // other stored inputs: translated when not cached, not loaded
size_t n_other;
volatile LONG64 next, done;
volatile LONG missing, other_cached;
volatile int64_t next, done;
volatile int32_t missing, other_cached;
} Warm;
static DWORD WINAPI warm_worker(void *arg) {
static void warm_worker(void *arg) {
Warm *w = arg;
for (;;) {
size_t i = (size_t)InterlockedIncrement64(&w->next) - 1;
if (i >= w->n_named + w->n_other) return 0;
size_t i = (size_t)plat_inc64(&w->next) - 1;
if (i >= w->n_named + w->n_other) return;
if (i < w->n_named) {
if (!(w->out[i] = gfx_shader_load_input(w->named[i], 1))) InterlockedIncrement(&w->missing);
if (!(w->out[i] = gfx_shader_load_input(w->named[i], 1))) plat_inc32(&w->missing);
} else {
const uint8_t *iid = w->other[i - w->n_named];
uint8_t key[32];
char hex[65], p[PATH_CAP + 80];
key_of_input(iid, key);
key_hex(key, hex);
snprintf(p, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
if (pack_find(PACK_SPV, key) || file_exists(p)) InterlockedIncrement(&w->other_cached);
snprintf(p, PATH_CAP + 80, "%s/%s.spv", cache_dir, hex);
if (pack_find(PACK_SPV, key) || file_exists(p)) plat_inc32(&w->other_cached);
else gfx_shader_load_input(iid, 1);
}
InterlockedIncrement64(&w->done);
plat_inc64(&w->done);
}
}
@@ -1579,7 +1577,7 @@ static int in_collect(const char *file, void *ctx) {
// The stored inputs not in named (sorted): ids parsed from the in/ file names.
static size_t other_inputs(const uint8_t (*named)[32], size_t n_named, uint8_t (**out)[32]) {
char dir[PATH_CAP + 80];
snprintf(dir, sizeof dir, "%s\\in", cache_dir);
snprintf(dir, sizeof dir, "%s/in", cache_dir);
InList l = {.named = named, .n_named = n_named};
char name[68];
for (uint32_t i = 0; i < pack_n; i++) { // the packed inputs, under the name their loose file would have
@@ -1594,7 +1592,7 @@ static size_t other_inputs(const uint8_t (*named)[32], size_t n_named, uint8_t (
void gfx_shader_prewarm(const uint8_t (*iids)[32], size_t n, const GfxShader **out, int threads, int procs) {
paths();
double t0 = (double)GetTickCount64();
double t0 = (double)plat_now_ms();
memset(out, 0, n * sizeof *out);
if (!cache_dir[0]) return;
uint64_t xl0 = (uint64_t)n_xlated, fail0 = (uint64_t)n_failures;
@@ -1607,23 +1605,20 @@ void gfx_shader_prewarm(const uint8_t (*iids)[32], size_t n, const GfxShader **o
threads = threads < 1 ? 1 : threads > 64 ? 64 : threads;
if ((size_t)threads > total) threads = (int)(total ? total : 1);
svc_set_limit(procs);
HANDLE t[64];
PlatThread *t[64];
int k = 0;
for (int i = 0; i < threads; i++)
if ((t[k] = CreateThread(NULL, 0, warm_worker, &w, 0, NULL))) k++;
if ((t[k] = plat_thread_new(warm_worker, &w, "shader prewarm"))) k++;
if (!k) warm_worker(&w);
for (int m = 0; k && m < k; m += MAXIMUM_WAIT_OBJECTS) {
int c = k - m < MAXIMUM_WAIT_OBJECTS ? k - m : MAXIMUM_WAIT_OBJECTS;
while (WaitForMultipleObjects((DWORD)c, t + m, TRUE, 2000) == WAIT_TIMEOUT)
for (int m = 0; m < k; m++)
while (!plat_thread_join_ns(t[m], 2000000000u))
rt_log("gfx_shader: prewarm: %lld of %zu shaders, %llu translated, %.0f ms", (long long)w.done, total,
(unsigned long long)((uint64_t)n_xlated - xl0), (double)GetTickCount64() - t0);
}
for (int i = 0; i < k; i++) CloseHandle(t[i]);
(unsigned long long)((uint64_t)n_xlated - xl0), (double)plat_now_ms() - t0);
svc_set_limit(svc_runtime_limit());
rt_log("gfx_shader: prewarm: %zu shaders named by pipeline keys (%ld not available), %zu other stored inputs (%ld "
"cached); %llu translated, %llu failed, with up to %d translators and %d threads, in %.0f ms", w.n_named,
(long)w.missing, w.n_other, (long)w.other_cached, (unsigned long long)((uint64_t)n_xlated - xl0),
(unsigned long long)((uint64_t)n_failures - fail0), procs, k, (double)GetTickCount64() - t0);
(unsigned long long)((uint64_t)n_failures - fail0), procs, k, (double)plat_now_ms() - t0);
free(w.other);
}
const uint32_t *gfx_shader_spirv(const GfxShader *s, uint64_t *words) {
@@ -1680,13 +1675,13 @@ typedef struct { uint64_t *v; uint32_t cap, n; } Set64; // open addressing, 0
typedef struct { uint8_t *p; size_t n, cap; } Bytes;
static struct {
SRWLOCK lock;
volatile LONG on;
PlatMutex lock;
volatile int32_t on;
char ans_path[PATH_CAP], prg_path[PATH_CAP];
Set64 pairs, progs; // input ids, code hashes seen (in the files or this run)
Bytes ans, prg; // pending
uint64_t pairs_new; // added this run
} RC = {SRWLOCK_INIT};
} RC = {PLAT_MUTEX_INIT};
static int set_add(Set64 *s, uint64_t h) { // 1 if new
h = h ? h : 1;
@@ -1786,7 +1781,7 @@ static void truncate_to(const char *path, size_t size) {
void gfx_shader_record_at(const char *keys_path) {
if (!keys_path || !keys_path[0] || RC.on) return;
AcquireSRWLockExclusive(&RC.lock);
plat_mutex_lock(&RC.lock);
const char *slash = strrchr(keys_path, '\\'), *fs = strrchr(keys_path, '/');
if (fs > slash) slash = fs;
int dir = slash ? (int)(slash + 1 - keys_path) : 0;
@@ -1806,8 +1801,8 @@ void gfx_shader_record_at(const char *keys_path) {
free(b);
rt_log("gfx_shader: recording answers met in play to %s (%u records, %u programs recorded before)", RC.ans_path,
RC.pairs.n, RC.progs.n);
InterlockedExchange(&RC.on, 1);
ReleaseSRWLockExclusive(&RC.lock);
plat_xchg32(&RC.on, 1);
plat_mutex_unlock(&RC.lock);
}
// A get resolved (r, a) to s: queue the record when its input id is new, and its program when new. Cost: a set
@@ -1816,7 +1811,7 @@ static void ans_note(const Req *r, int stage, const uint8_t *control, const uint
const GfxShader *s) {
if (!RC.on) return;
uint32_t st = (uint32_t)stage;
AcquireSRWLockExclusive(&RC.lock);
plat_mutex_lock(&RC.lock);
if (set_add(&RC.pairs, hash8(s->iid))) {
uint8_t rec[ANS_REC] = {0};
memcpy(rec, &st, 4), memcpy(rec + 4, r->code_hash, 32), memcpy(rec + 36, s->iid, 32);
@@ -1830,7 +1825,7 @@ static void ans_note(const Req *r, int stage, const uint8_t *control, const uint
bytes_put(&RC.ans, rec, sizeof rec);
RC.pairs_new++;
}
ReleaseSRWLockExclusive(&RC.lock);
plat_mutex_unlock(&RC.lock);
}
static int rec_append(const char *path, const char *magic, uint32_t version, uint32_t rec_size, const Bytes *b) {
@@ -1847,23 +1842,23 @@ static int rec_append(const char *path, const char *magic, uint32_t version, uin
return !fclose(f) && ok;
}
static SRWLOCK rec_io = SRWLOCK_INIT; // the record files: appends in order, no read during an append
static PlatMutex rec_io = PLAT_MUTEX_INIT; // the record files: appends in order, no read during an append
void gfx_shader_record_flush(int force) {
static volatile LONG64 last;
LONG64 now = (LONG64)GetTickCount64();
static volatile int64_t last;
int64_t now = (int64_t)plat_now_ms();
if (!RC.on || (!force && now - last < 2000)) return;
last = now;
// The files are written outside RC.lock, which the render thread takes to record a new answer.
AcquireSRWLockExclusive(&rec_io);
AcquireSRWLockExclusive(&RC.lock);
plat_mutex_lock(&rec_io);
plat_mutex_lock(&RC.lock);
Bytes ans = RC.ans, prg = RC.prg;
RC.ans = (Bytes){0}, RC.prg = (Bytes){0};
uint64_t total = RC.pairs.n, added = RC.pairs_new;
ReleaseSRWLockExclusive(&RC.lock);
plat_mutex_unlock(&RC.lock);
if (prg.n && !rec_append(RC.prg_path, "TOTKSPRG", PRG_VERSION, 0, &prg)) rt_log("gfx_shader: cannot write %s", RC.prg_path);
if (ans.n && !rec_append(RC.ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &ans)) rt_log("gfx_shader: cannot write %s", RC.ans_path);
ReleaseSRWLockExclusive(&rec_io);
plat_mutex_unlock(&rec_io);
if (ans.n) rt_log("gfx_shader: answers: %zu new records appended (%llu new this run, %llu recorded)", ans.n / ANS_REC,
(unsigned long long)added, (unsigned long long)total);
free(ans.p), free(prg.p);
@@ -1880,7 +1875,7 @@ typedef struct {
const uint8_t *ans;
size_t n_ans;
uint8_t (*iid)[32]; // per answers record: the resolved input id, zero on failure
volatile LONG64 next, done, missing, failed;
volatile int64_t next, done, missing, failed;
} Prep;
static int prg_cmp(const void *x, const void *y) { return memcmp(((const PrgIdx *)x)->rec + 12, ((const PrgIdx *)y)->rec + 12, 32); }
@@ -1890,17 +1885,17 @@ static void prg_collect(const uint8_t *rec, void *ctx) {
l->idx[l->n++].rec = rec;
}
static DWORD WINAPI prep_worker(void *arg) {
static void prep_worker(void *arg) {
Prep *p = arg;
for (;;) {
size_t i = (size_t)InterlockedIncrement64(&p->next) - 1;
if (i >= p->n_ans) return 0;
size_t i = (size_t)plat_inc64(&p->next) - 1;
if (i >= p->n_ans) return;
const uint8_t *r = p->ans + i * ANS_REC;
uint8_t key_rec[12 + 32];
memcpy(key_rec + 12, r + 4, 32);
PrgIdx k = {key_rec}, *e = bsearch(&k, p->progs.idx, p->progs.n, sizeof k, prg_cmp);
if (!e) {
InterlockedIncrement64(&p->missing), InterlockedIncrement64(&p->done);
plat_inc64(&p->missing), plat_inc64(&p->done);
continue;
}
uint32_t st, len;
@@ -1909,9 +1904,9 @@ static DWORD WINAPI prep_worker(void *arg) {
GfxShaderAnswers a;
unpack_answers(r + 68, &a);
const GfxShader *s = gfx_shader_get((int)st, control, code, len, c1, &a);
if (!s) InterlockedIncrement64(&p->failed);
if (!s) plat_inc64(&p->failed);
else if (p->iid) memcpy(p->iid[i], s->iid, 32);
InterlockedIncrement64(&p->done);
plat_inc64(&p->done);
}
}
@@ -1921,7 +1916,7 @@ static DWORD WINAPI prep_worker(void *arg) {
static long long prepare_files(const char *ans_path, const char *prg_path, int threads, int procs, const char *what,
uint8_t **ans_out, uint8_t (**iid_out)[32]) {
paths();
double t0 = (double)GetTickCount64();
double t0 = (double)plat_now_ms();
uint8_t *ab = NULL, *pb = NULL;
size_t an = 0, pn = 0;
if (!rec_read(ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &ab, &an) ||
@@ -1938,22 +1933,19 @@ static long long prepare_files(const char *ans_path, const char *prg_path, int t
uint64_t xl0 = (uint64_t)n_xlated;
threads = threads < 1 ? 1 : threads > 64 ? 64 : threads;
svc_set_limit(procs);
HANDLE t[64];
PlatThread *t[64];
int k = 0;
for (int i = 0; i < threads; i++)
if ((t[k] = CreateThread(NULL, 0, prep_worker, &p, 0, NULL))) k++;
if ((t[k] = plat_thread_new(prep_worker, &p, "shader prepare"))) k++;
if (!k) prep_worker(&p);
for (int m = 0; k && m < k; m += MAXIMUM_WAIT_OBJECTS) {
int c = k - m < MAXIMUM_WAIT_OBJECTS ? k - m : MAXIMUM_WAIT_OBJECTS;
while (WaitForMultipleObjects((DWORD)c, t + m, TRUE, 5000) == WAIT_TIMEOUT)
for (int m = 0; m < k; m++)
while (!plat_thread_join_ns(t[m], 5000000000u))
rt_log("gfx_shader: prepare: %lld of %zu pairs, %llu translated, %.0f ms", (long long)p.done, p.n_ans,
(unsigned long long)((uint64_t)n_xlated - xl0), (double)GetTickCount64() - t0);
}
for (int i = 0; i < k; i++) CloseHandle(t[i]);
(unsigned long long)((uint64_t)n_xlated - xl0), (double)plat_now_ms() - t0);
svc_set_limit(svc_runtime_limit());
rt_log("gfx_shader: prepare: %zu %s of %zu programs: %llu translated, %lld failed, %lld without a program, the rest "
"cached; %.0f ms", p.n_ans, what, p.progs.n, (unsigned long long)((uint64_t)n_xlated - xl0), (long long)p.failed,
(long long)p.missing, (double)GetTickCount64() - t0);
(long long)p.missing, (double)plat_now_ms() - t0);
free(p.progs.idx), free(pb);
if (ans_out) *ans_out = ab;
else free(ab);
@@ -1977,10 +1969,10 @@ void gfx_shader_prepare_recorded(int threads, int procs) {
int gfx_shader_prepare_seed(const char *dir, int threads, int procs) {
char ans[PATH_CAP + 32], prg[PATH_CAP + 32], out[PATH_CAP + 32];
snprintf(ans, sizeof ans, "%s\\seed-answers.bin", dir);
snprintf(prg, sizeof prg, "%s\\seed-programs.bin", dir);
snprintf(out, sizeof out, "%s\\shader-prepared.bin", dir);
double t0 = (double)GetTickCount64();
snprintf(ans, sizeof ans, "%s/seed-answers.bin", dir);
snprintf(prg, sizeof prg, "%s/seed-programs.bin", dir);
snprintf(out, sizeof out, "%s/shader-prepared.bin", dir);
double t0 = (double)plat_now_ms();
uint64_t fail0 = (uint64_t)n_failures;
uint8_t *ab = NULL, (*iid)[32] = NULL;
long long n = prepare_files(ans, prg, threads, procs, "seed variants", &ab, &iid);
@@ -2009,7 +2001,7 @@ int gfx_shader_prepare_seed(const char *dir, int threads, int procs) {
snprintf(msg, sizeof msg, "gfx_shader: prepared %zu shader variants (vertex %zu, fragment %zu, compute %zu; title %zu, "
"warm-up %zu, other %zu) from %lld seed records, %llu translations failed, in %.0f ms; %s %s", u,
stages[GFX_STAGE_VS], stages[GFX_STAGE_FS], stages[GFX_STAGE_CS], by[0], by[1], by[2], n,
(unsigned long long)((uint64_t)n_failures - fail0), (double)GetTickCount64() - t0,
(unsigned long long)((uint64_t)n_failures - fail0), (double)plat_now_ms() - t0,
ok ? "wrote" : "cannot write", out);
rt_log("%s", msg);
printf("%s\n", msg);
@@ -2027,9 +2019,9 @@ size_t gfx_shader_warm_list(GfxWarmEnt **out) {
RC.ans_path);
uint8_t *ab = NULL, *pb = NULL;
size_t an = 0, pn = 0;
AcquireSRWLockExclusive(&rec_io); // no append meanwhile
plat_mutex_lock(&rec_io); // no append meanwhile
rec_read(RC.ans_path, "TOTKSANS", ANS_VERSION, ANS_REC, &ab, &an);
ReleaseSRWLockExclusive(&rec_io);
plat_mutex_unlock(&rec_io);
rec_read(prp, "TOTKSPRP", PRP_VERSION, PRP_REC, &pb, &pn);
size_t na = an / ANS_REC, np = pn / PRP_REC, n = 0;
GfxWarmEnt *e = calloc(na + np + 1, sizeof *e);
+46 -37
View File
@@ -1,14 +1,15 @@
// --probe-shader: the shader service client (gfx_shader.c) against 32 census shaders. Needs build/xlate/totk_xlate.exe and
// build/v121/shaders/ (tools/shader_census.py); uses a throwaway cache directory.
// a census, build/<version>/shaders/ (tools/shader_census.py, probe_census_dir); uses a throwaway cache directory.
// 1. Translate 32 shaders (mixed stages) through gfx_shader_get; each SPIR-V starts with the magic 0x07230203.
// 2. Drop the memory cache and ask again: 32 hits from disk, 0 service requests. A third pass returns the same pointers.
// 3. Send a malformed line straight to the service: it answers with an error line and still serves a valid request.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include "gfx.h"
#include "host.h"
#include "platform/platform.h"
int gfx_shader_raw_request(const char *line, char *reply, size_t reply_size);
uint64_t gfx_shader_requests(void);
@@ -48,16 +49,23 @@ static const char *str_field(const char *from, const char *to, const char *name,
static int hexval(char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; }
// The repository root: the nearest parent of totk.exe holding build/v121/shaders/jobs.json.
static int find_root(char *root, size_t n) {
char exe[MAX_PATH * 2];
DWORD len = GetModuleFileNameA(NULL, exe, sizeof exe);
if (!len || len >= sizeof exe) return 0;
for (char *cut = strrchr(exe, '\\'); cut; cut = strrchr(exe, '\\')) {
*cut = 0;
char p[MAX_PATH * 2 + 40];
snprintf(p, sizeof p, "%s\\build\\v121\\shaders\\jobs.json", exe);
if (GetFileAttributesA(p) != INVALID_FILE_ATTRIBUTES) return snprintf(root, n, "%s", exe) < (int)n;
// The shader census the probes translate (tools/shader_census.py --version <ver>): build/<ver>/shaders in the nearest
// parent of totk.exe that has one with a jobs.json, the running game version's first, then the others' (any census
// serves a probe). 1 with that folder in dir.
int probe_census_dir(char *dir, size_t n) {
const char *const versions[3] = {RT_V111 ? "v111" : RT_V143 ? "v143" : "v121",
RT_V111 ? "v121" : RT_V143 ? "v121" : "v143",
RT_V111 ? "v143" : RT_V143 ? "v111" : "v111"};
for (int v = 0; v < 3; v++) {
char exe[PLAT_PATH_MAX * 2];
int len = plat_exe_path(exe, sizeof exe);
if (!len || len >= sizeof exe) return 0;
for (char *cut = plat_last_sep(exe); cut; cut = plat_last_sep(exe)) {
*cut = 0;
char p[PLAT_PATH_MAX * 2 + 40];
snprintf(p, sizeof p, "%s/build/%s/shaders/jobs.json", exe, versions[v]);
if (plat_file_exists(p)) return snprintf(dir, n, "%s/build/%s/shaders", exe, versions[v]) < (int)n;
}
}
return 0;
}
@@ -95,15 +103,15 @@ static int pick_entries(const char *jobs, Entry *out) {
return got;
}
static char root[MAX_PATH * 2];
static char root[PLAT_PATH_MAX * 2]; // the census folder (probe_census_dir)
// Builds the program the game would hand NVN for a census entry and translates it.
static const GfxShader *translate(const Entry *e, int *err) {
char path[MAX_PATH * 2 + 64];
char path[PLAT_PATH_MAX * 2 + 64];
uint8_t c1[8192], control[0x880] = {0};
if (e->c1_len > sizeof c1) return *err = 1, NULL;
for (size_t b = 0; b < e->c1_len; b++) c1[b] = (uint8_t)(hexval(e->c1[b * 2]) << 4 | hexval(e->c1[b * 2 + 1]));
snprintf(path, sizeof path, "%s\\build\\v121\\shaders\\code\\%s.code", root, e->key);
snprintf(path, sizeof path, "%s/code/%s.code", root, e->key);
size_t body_size;
char *body = slurp(path, &body_size);
if (!body) return *err = 1, NULL;
@@ -120,28 +128,27 @@ static const GfxShader *translate(const Entry *e, int *err) {
return s;
}
static int remove_entry(const char *name, int kind, uint64_t size, void *dir) {
(void)size;
char f[PLAT_PATH_MAX * 2];
snprintf(f, sizeof f, "%s/%s", (const char *)dir, name);
if (kind == PLAT_FS_FILE) plat_fs_unlink(f);
return 1;
}
static void remove_dir(const char *dir) {
char pat[MAX_PATH * 2];
snprintf(pat, sizeof pat, "%s\\*", dir);
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pat, &fd);
if (h != INVALID_HANDLE_VALUE) {
do {
char f[MAX_PATH * 2];
snprintf(f, sizeof f, "%s\\%s", dir, fd.cFileName);
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) DeleteFileA(f);
} while (FindNextFileA(h, &fd));
FindClose(h);
}
RemoveDirectoryA(dir);
plat_fs_list(dir, remove_entry, (void *)dir);
plat_fs_rmdir(dir);
}
#define FAIL_IF(cond, ...) do { if (cond) { printf("probe-shader: FAILED: " __VA_ARGS__); printf("\n"); remove_dir(dir); return 1; } } while (0)
int probe_shader(void) {
char path[MAX_PATH * 2 + 64];
CHECK(find_root(root, sizeof root), "build/v121/shaders/jobs.json not found (run tools/shader_census.py)");
snprintf(path, sizeof path, "%s\\build\\v121\\shaders\\jobs.json", root);
char path[PLAT_PATH_MAX * 2 + 64];
CHECK(probe_census_dir(root, sizeof root),
"no build/v%d/shaders/jobs.json above the exe (run tools/shader_census.py --version v%d)", rt_game_version,
rt_game_version);
snprintf(path, sizeof path, "%s/jobs.json", root);
size_t jobs_size;
char *jobs = slurp(path, &jobs_size);
CHECK(jobs, "cannot read %s", path);
@@ -149,9 +156,11 @@ int probe_shader(void) {
int count = pick_entries(jobs, entries);
CHECK(count == WANT, "only %d census shaders usable", count);
char dir[MAX_PATH];
snprintf(dir, sizeof dir, "%s\\totk-shader-probe-%lu", getenv("TEMP") ? getenv("TEMP") : ".", GetCurrentProcessId());
CHECK(_putenv_s("TOTK_SHADER_CACHE", dir) == 0, "cannot set TOTK_SHADER_CACHE");
char dir[PLAT_PATH_MAX];
char tmp[PLAT_PATH_MAX];
if (!plat_temp_dir(tmp, sizeof tmp)) strcpy(tmp, ".");
snprintf(dir, sizeof dir, "%s/totk-shader-probe-%u", tmp, plat_process_id());
CHECK(plat_setenv("TOTK_SHADER_CACHE", dir), "cannot set TOTK_SHADER_CACHE");
// pass 1: translate through the service
const GfxShader *first[WANT];
@@ -198,16 +207,16 @@ int probe_shader(void) {
FAIL_IF(strncmp(reply, "fail ", 5), "malformed line answered with \"%s\"", reply);
FAIL_IF(!gfx_shader_raw_request("{\"job\":{\"stage\":\"ps\"},\"out\":\"x\"}", reply, sizeof reply) || strncmp(reply, "fail ", 5),
"job without key not rejected");
char dir_json[MAX_PATH * 2], line[MAX_PATH * 4 + 1024], *o = dir_json;
char dir_json[PLAT_PATH_MAX * 2], line[PLAT_PATH_MAX * 4 + 1024], *o = dir_json;
for (const char *s = dir; *s; s++) { if (*s == '\\') *o++ = '\\'; *o++ = *s; }
*o = 0;
char esc_root[MAX_PATH * 2];
char esc_root[PLAT_PATH_MAX * 2];
o = esc_root;
for (const char *s = root; *s; s++) { if (*s == '\\') *o++ = '\\'; *o++ = *s; }
*o = 0;
const Entry *e = &entries[0];
snprintf(line, sizeof line,
"{\"job\":{\"key\":\"%.64sa\",\"stage\":\"%s\",\"debug\":false,\"c1\":\"%.*s\",\"code\":\"%s\\\\build\\\\v121\\\\shaders\\\\code\\\\%.64s.code\"},\"out\":\"%s\"}",
"{\"job\":{\"key\":\"%.64sa\",\"stage\":\"%s\",\"debug\":false,\"c1\":\"%.*s\",\"code\":\"%s/code/%.64s.code\"},\"out\":\"%s\"}",
e->key, e->stage == GFX_STAGE_VS ? "vs" : e->stage == GFX_STAGE_FS ? "ps" : "cs", (int)(e->c1_len * 2), e->c1,
esc_root, e->key, dir_json);
FAIL_IF(!gfx_shader_raw_request(line, reply, sizeof reply), "service did not answer a valid request after the malformed one");
+38 -34
View File
@@ -1,14 +1,16 @@
// --probe-window: presents 300 frames of a cycling clear colour through vk_present_image, reads the swapchain image back
// and compares every pixel, measures the vsync rate, flips the focus state with synthetic WM_ACTIVATEAPP messages and
// resizes the window. Run with TOTK_VK_VALIDATION=1 to count validation errors.
// and compares every pixel, measures the vsync rate, flips the focus state and drives the cursor release with synthetic
// window events (the window back end's wsys_probe_*, gfx_wsys.h) and resizes the window. Run with TOTK_VK_VALIDATION=1
// to count validation errors.
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include "gfx.h"
#include "gfx_wsys.h"
#include "input.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#define SRC_W 640
#define SRC_H 360
@@ -110,20 +112,26 @@ static int present_and_check(Source *s, int first, int i, int *w_out, int *h_out
else if (fmt == VK_FORMAT_R8G8B8A8_UNORM) { want[0] = rgb[0]; want[1] = rgb[1]; want[2] = rgb[2]; }
else { printf("probe-window: swapchain format %d is not UNORM RGBA/BGRA\n", (int)fmt); return 3; }
want[3] = 255;
uint32_t wv;
uint32_t wv, black = 0xff000000u; // opaque black in both byte orders
memcpy(&wv, want, 4);
int r[4]; // the picture's rectangle; bars of black around it when the window's shape differs
gfx_fit_rect(SRC_W, SRC_H, w, h, r);
const uint32_t *p = (const uint32_t *)px;
for (long k = 0, n = (long)w * h; k < n; k++)
if (p[k] != wv) {
printf("probe-window: frame %d pixel %ld is %08x, expected %08x\n", i, k, p[k], wv);
for (long k = 0, n = (long)w * h; k < n; k++) {
int x = (int)(k % w), y = (int)(k / w);
uint32_t e = x >= r[0] && x < r[0] + r[2] && y >= r[1] && y < r[1] + r[3] ? wv : black;
if (p[k] != e) {
printf("probe-window: frame %d pixel %ld (%d, %d) is %08x, expected %08x\n", i, k, x, y, p[k], e);
return 4;
}
}
*w_out = w;
*h_out = h;
return 0;
}
int probe_window(void) {
if (!menu_font_check()) return fail("the menu's font check takes a damaged font or refuses a good one");
gfx_window_start();
VkCtx *c = vk_ctx();
if (!c) return fail("no Vulkan device");
@@ -136,11 +144,11 @@ int probe_window(void) {
if (present_and_check(&s, i == 0, i, &w, &h)) return fail("presented pixels differ");
printf("probe-window: %d presents, readback %dx%d exact\n", FRAMES, w, h);
// Focus: synthetic WM_ACTIVATEAPP off and on, delivered synchronously on the window thread.
// Focus: synthetic focus loss and gain, delivered synchronously on the window thread.
unsigned m0 = gfx_window_focus_messages();
SendMessageW(gfx_window_handle(), WM_ACTIVATEAPP, FALSE, 0);
wsys_probe_focus(0);
int off = gfx_window_focused();
SendMessageW(gfx_window_handle(), WM_ACTIVATEAPP, TRUE, 0);
wsys_probe_focus(1);
int on = gfx_window_focused();
unsigned m1 = gfx_window_focus_messages();
if (off != 0 || on != 1 || m1 - m0 != 2) {
@@ -149,19 +157,17 @@ int probe_window(void) {
}
printf("probe-window: focus off/on pushed 2 messages (15)\n");
// Cursor release: Esc releases, a client click recaptures, a title bar activation releases. The probe window may not be
// the foreground window, so this checks the release flag, not the clip state.
HWND hw = gfx_window_handle();
SendMessageW(hw, WM_KEYDOWN, VK_ESCAPE, 0);
// Cursor release: Esc releases, a client click recaptures, a title bar activation releases (where the window system
// reports one). The probe window may not be the foreground window, so this checks the release flag, not the capture.
wsys_probe_escape();
int esc = input_cursor_released();
SendMessageW(hw, WM_LBUTTONDOWN, MK_LBUTTON, 0);
wsys_probe_click(0);
int click = input_cursor_released();
SendMessageW(hw, WM_MOUSEACTIVATE, (WPARAM)hw, MAKELPARAM(HTCLIENT, WM_LBUTTONDOWN));
wsys_probe_click(1);
int client = input_cursor_released();
SendMessageW(hw, WM_MOUSEACTIVATE, (WPARAM)hw, MAKELPARAM(HTCAPTION, WM_LBUTTONDOWN));
int caption = input_cursor_released();
SendMessageW(hw, WM_ACTIVATEAPP, FALSE, 0);
SendMessageW(hw, WM_ACTIVATEAPP, TRUE, 0);
int caption = wsys_probe_click(2) ? input_cursor_released() : 1;
wsys_probe_focus(0);
wsys_probe_focus(1);
int back = input_cursor_released();
if (menu_is_open()) menu_toggle(); // Esc opened the menu: the presents below compare the bare game image
if (!esc || click || client || !caption || back) {
@@ -172,33 +178,31 @@ int probe_window(void) {
// Resize: the client area changes, the swapchain follows, presenting still reads back exact.
gfx_window_resize(1280, 720);
RECT rc;
GetClientRect(gfx_window_handle(), &rc);
int cw = 0, ch = 0;
wsys_client_size(&cw, &ch);
for (int i = 0; i < 10; i++)
if (present_and_check(&s, 0, FRAMES + i, &w, &h)) return fail("presents after resize");
if (w != rc.right || h != rc.bottom) {
printf("probe-window: readback %dx%d, client area %ldx%ld\n", w, h, (long)rc.right, (long)rc.bottom);
if (w != cw || h != ch) {
printf("probe-window: readback %dx%d, client area %dx%d\n", w, h, cw, ch);
return fail("swapchain size after resize");
}
printf("probe-window: resized to %dx%d, presents still exact\n", w, h);
vk_present_capture(0);
// Vsync: count ticks between two tick edges about 5 s apart.
static volatile long ticks;
static volatile int32_t ticks;
gfx_vsync_bind_counter(&ticks);
LARGE_INTEGER f, t0, t1;
QueryPerformanceFrequency(&f);
long a = ticks;
while (ticks == a) Sleep(1);
QueryPerformanceCounter(&t0);
while (ticks == a) plat_sleep_ms(1);
uint64_t t0 = plat_now_ns();
long n0 = ticks;
Sleep(5000);
plat_sleep_ms(5000);
long b = ticks;
while (ticks == b) Sleep(1);
QueryPerformanceCounter(&t1);
while (ticks == b) plat_sleep_ms(1);
uint64_t t1 = plat_now_ns();
long n1 = ticks;
double rate = (double)(n1 - n0) * (double)f.QuadPart / (double)(t1.QuadPart - t0.QuadPart);
printf("probe-window: vsync %ld ticks in %.3f s = %.3f Hz\n", n1 - n0, (double)(t1.QuadPart - t0.QuadPart) / (double)f.QuadPart, rate);
double rate = (double)(n1 - n0) * 1e9 / (double)(t1 - t0);
printf("probe-window: vsync %ld ticks in %.3f s = %.3f Hz\n", n1 - n0, (double)(t1 - t0) / 1e9, rate);
if (rate < 60.0 * 0.995 || rate > 60.0 * 1.005) return fail("vsync rate");
source_destroy(&s);
+338
View File
@@ -0,0 +1,338 @@
// The SDL3 back end of the window (gfx_wsys.h), for hosts without Win32: the window on a thread of its own that also
// runs SDL's event loop, F11 fullscreen (borderless, the desktop's mode), the settings TOTK_WINDOW_SIZE and
// TOTK_FULLSCREEN, the display's refresh rate, focus, the Vulkan surface. The swapchain and the present are
// gfx_present.c's. Keyboard and mouse events go to input_sdl.c and to the menu (ui/menu_sdl.cpp).
//
// SDL wants its video calls on the thread that initialised video, so this thread does them all: the others hand it
// work through run_on_window (a user event and a wait) or post a user event. Video is initialised before the window
// exists: the Vulkan instance asks for SDL's surface extensions first (gfx_window_vk_extensions), the window comes with
// gfx_window_start (wsys_open).
//
// TOTK_WINDOW_SIZE is in pixels. On a display that scales windows (Wayland, HiDPI) the window asks for its pixels in
// the display's units (SDL_GetWindowDisplayScale), and SDL reports mouse positions in those units: input_sdl.c and the
// menu scale them back (gfx_window_sdl_density).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "cfg.h"
#include "gfx.h"
#include "gfx_wsys.h"
#include "input.h"
#include "input_sdl.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
static SDL_Window *g_win;
static uint32_t g_ev_base; // SDL_RegisterEvents: the user events below
enum { EV_RUN, EV_SETTINGS, EV_CLOSE, EV_COUNT };
static PlatEvent g_video_ready; // manual-reset: SDL video is up (or failed)
static int g_video_ok;
static PlatOnce g_thread_once = PLAT_ONCE_INIT;
static uint32_t g_window_tid; // the window thread, which runs the video calls
static volatile int32_t g_density_milli = 1000; // pixels per window unit, x1000
// ---- work on the window thread
typedef struct {
void (*fn)(void *arg);
void *arg;
PlatEvent done;
} RunReq;
static void run_on_window(void (*fn)(void *arg), void *arg) {
if (plat_thread_id() == g_window_tid) {
fn(arg);
return;
}
RunReq r = {fn, arg};
plat_event_init(&r.done, 1, 0);
SDL_Event e = {.user = {.type = g_ev_base + EV_RUN, .data1 = &r}};
if (!SDL_PushEvent(&e)) rt_fatal("gfx: cannot reach the window thread (%s)", SDL_GetError());
plat_event_wait(&r.done);
}
static void post(int ev) {
SDL_Event e = {.user = {.type = g_ev_base + (uint32_t)ev}};
SDL_PushEvent(&e);
}
double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, __ATOMIC_RELAXED) / 1000.0; }
SDL_Window *gfx_window_sdl(void) { return g_win; }
// ---- settings: size and fullscreen
static char g_applied_size[32];
static int g_applied_fs;
static void note_applied(void) {
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
g_applied_fs = gfx_cfg_fullscreen();
}
static int is_fullscreen(void) { return (SDL_GetWindowFlags(g_win) & SDL_WINDOW_FULLSCREEN) != 0; }
// The display scale as a DPI, 96 at 100% (the menu's text size floor); 96 before the window exists.
unsigned gfx_window_dpi(void) {
float scale = g_win ? SDL_GetWindowDisplayScale(g_win) : 0;
return scale > 0 ? (unsigned)(scale * 96 + 0.5f) : 96;
}
// The window's size in its units for w x h pixels.
static void set_pixel_size(int w, int h) {
float scale = SDL_GetWindowDisplayScale(g_win);
if (!(scale > 0)) scale = 1;
SDL_SetWindowSize(g_win, (int)(w / scale + 0.5f), (int)(h / scale + 0.5f));
}
static void set_fullscreen(int on) {
input_cursor_suspend(1);
SDL_SetWindowFullscreen(g_win, on); // the desktop's mode: borderless over the display
SDL_SyncWindow(g_win);
input_cursor_suspend(0);
}
// Window thread: the window takes the size and fullscreen state of the settings when they changed. A window resized by
// hand keeps its size when another setting changes.
static void apply_settings(void) {
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = gfx_cfg_fullscreen();
int fs_changed = fs != g_applied_fs;
note_applied();
if (size_changed) {
int w, h;
gfx_cfg_window_size(&w, &h);
if (is_fullscreen()) SDL_SetWindowFullscreen(g_win, false);
set_pixel_size(w, h);
if (is_fullscreen() || (fs && !fs_changed)) SDL_SetWindowFullscreen(g_win, true);
}
if (fs_changed && fs != is_fullscreen()) set_fullscreen(fs);
}
void gfx_window_apply_settings(void) {
if (g_win) post(EV_SETTINGS);
}
void gfx_window_request_close(void) {
if (g_win) post(EV_CLOSE);
}
// ---- the display's refresh rate and size
static void refresh_rate_update(void) {
const SDL_DisplayMode *m = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(g_win));
double hz = 0;
if (m && m->refresh_rate_denominator) hz = (double)m->refresh_rate_numerator / m->refresh_rate_denominator;
else if (m) hz = m->refresh_rate;
gfx_window_set_refresh(hz);
}
int gfx_window_monitor_size(int *w, int *h) {
if (!g_win) return 0;
const SDL_DisplayMode *m = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(g_win));
if (!m) return 0;
*w = (int)(m->w * m->pixel_density + 0.5f);
*h = (int)(m->h * m->pixel_density + 0.5f);
return 1;
}
static void density_update(void) {
float d = SDL_GetWindowPixelDensity(g_win);
__atomic_store_n(&g_density_milli, (int32_t)((d > 0 ? d : 1) * 1000 + 0.5f), __ATOMIC_RELAXED);
}
// ---- the window thread
static void handle(const SDL_Event *e) {
if (e->type == g_ev_base + EV_RUN) {
RunReq *r = e->user.data1;
r->fn(r->arg);
plat_event_set(&r->done);
return;
}
if (e->type == g_ev_base + EV_SETTINGS) {
if (g_win) apply_settings();
return;
}
if (e->type == g_ev_base + EV_CLOSE) gfx_window_closed();
if (!g_win) return;
input_sdl_event(e);
menu_sdl_event(e);
switch (e->type) {
case SDL_EVENT_KEY_DOWN:
if (e->key.scancode == SDL_SCANCODE_F11 && !e->key.repeat) {
set_fullscreen(!is_fullscreen());
cfg_set("TOTK_FULLSCREEN", is_fullscreen() ? "1" : "0"); // the menu shows it; the next start keeps it
cfg_save();
note_applied();
}
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_RESTORED:
density_update();
gfx_window_mark_dirty();
break;
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED:
density_update();
refresh_rate_update();
gfx_window_mark_dirty();
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
case SDL_EVENT_WINDOW_FOCUS_LOST:
gfx_window_focus_changed(e->type == SDL_EVENT_WINDOW_FOCUS_GAINED);
break;
case SDL_EVENT_QUIT:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
gfx_window_closed();
break;
}
}
static void window_main(void *arg) {
(void)arg;
g_window_tid = plat_thread_id();
SDL_SetHint(SDL_HINT_APP_ID, "totk-recomp"); // Wayland's app id: the desktop file and the taskbar match it
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "0");
g_video_ok = SDL_Init(SDL_INIT_VIDEO);
if (!g_video_ok) rt_log("gfx: SDL video init failed (%s)", SDL_GetError());
// SDL's Vulkan queries need its Vulkan loader, which a window would load at creation: the instance and the device
// are made before the window.
else if (!SDL_Vulkan_LoadLibrary(NULL)) {
rt_log("gfx: SDL cannot load Vulkan (%s)", SDL_GetError());
g_video_ok = 0;
} else {
g_ev_base = SDL_RegisterEvents(EV_COUNT);
rt_log("gfx: SDL %d.%d.%d, video driver %s", SDL_VERSIONNUM_MAJOR(SDL_GetVersion()),
SDL_VERSIONNUM_MINOR(SDL_GetVersion()), SDL_VERSIONNUM_MICRO(SDL_GetVersion()), SDL_GetCurrentVideoDriver());
}
plat_event_set(&g_video_ready);
if (!g_video_ok) return;
for (;;) {
SDL_Event e;
if (SDL_WaitEventTimeout(&e, 100)) handle(&e);
else input_sdl_idle(); // a cursor change requested from another thread meanwhile
}
}
static void video_start(void *arg) {
(void)arg;
plat_event_init(&g_video_ready, 1, 0);
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
plat_event_wait(&g_video_ready);
}
static int video_up(void) {
plat_once(&g_thread_once, video_start, NULL);
return g_video_ok;
}
int gfx_window_vk_extensions(const char **out, int max) {
if (!video_up()) return 0;
Uint32 n = 0;
const char *const *ext = SDL_Vulkan_GetInstanceExtensions(&n);
if (!ext) {
rt_log("gfx: SDL_Vulkan_GetInstanceExtensions failed (%s)", SDL_GetError());
return 0;
}
int k = 0;
for (Uint32 i = 0; i < n && k < max; i++) out[k++] = ext[i];
return k;
}
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
return video_up() && SDL_Vulkan_GetPresentationSupport(inst, pd, family);
}
// ---- gfx_wsys.h
static int open_ok;
static void open_window(void *arg) {
(void)arg;
int w, h;
gfx_cfg_window_size(&w, &h);
g_win = SDL_CreateWindow("TotK Recomp", w, h, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
if (!g_win) {
rt_log("gfx: SDL_CreateWindow failed (%s)", SDL_GetError());
return;
}
set_pixel_size(w, h); // w x h were window units; the setting is in pixels
density_update();
note_applied();
refresh_rate_update();
if (cfg_get("TOTK_BACKGROUND_WINDOW")) rt_log("gfx: TOTK_BACKGROUND_WINDOW has no effect with SDL windows");
if (gfx_cfg_fullscreen()) SDL_SetWindowFullscreen(g_win, true);
SDL_SyncWindow(g_win);
input_sdl_window(g_win);
open_ok = 1;
}
int wsys_open(void) {
if (!video_up()) return 0;
if (!g_win) run_on_window(open_window, NULL);
return open_ok;
}
typedef struct { VkInstance inst; VkSurfaceKHR surface; } SurfaceReq;
static void make_surface(void *arg) {
SurfaceReq *r = arg;
if (!SDL_Vulkan_CreateSurface(g_win, r->inst, NULL, &r->surface)) {
rt_log("gfx: SDL_Vulkan_CreateSurface failed (%s)", SDL_GetError());
r->surface = VK_NULL_HANDLE;
}
}
VkSurfaceKHR wsys_surface(VkInstance inst) {
SurfaceReq r = {inst, VK_NULL_HANDLE};
run_on_window(make_surface, &r);
return r.surface;
}
void wsys_client_size(int *w, int *h) {
*w = *h = 0;
if (g_win) SDL_GetWindowSizeInPixels(g_win, w, h);
}
int wsys_minimized(void) { return g_win && (SDL_GetWindowFlags(g_win) & (SDL_WINDOW_MINIMIZED | SDL_WINDOW_HIDDEN)); }
static void resize_window(void *arg) {
const int *wh = arg;
set_pixel_size(wh[0], wh[1]);
SDL_SyncWindow(g_win);
}
void wsys_resize(int w, int h) {
int wh[2] = {w, h};
run_on_window(resize_window, wh);
}
// ---- --probe-window: synthetic events through the event handler, on the window thread
static void probe_event(void *arg) { handle(arg); }
void wsys_probe_focus(int in) {
SDL_Event e = {.window = {.type = in ? SDL_EVENT_WINDOW_FOCUS_GAINED : SDL_EVENT_WINDOW_FOCUS_LOST,
.windowID = SDL_GetWindowID(g_win)}};
run_on_window(probe_event, &e);
}
void wsys_probe_escape(void) {
SDL_Event e = {.key = {.type = SDL_EVENT_KEY_DOWN, .scancode = SDL_SCANCODE_ESCAPE, .key = SDLK_ESCAPE, .down = true,
.windowID = SDL_GetWindowID(g_win)}};
run_on_window(probe_event, &e);
e.key.type = SDL_EVENT_KEY_UP, e.key.down = false;
run_on_window(probe_event, &e);
}
// SDL reports no activation by a title bar click: only the client click exists.
int wsys_probe_click(int where) {
if (where == 2) return 0;
SDL_Event e = {.button = {.type = SDL_EVENT_MOUSE_BUTTON_DOWN, .button = SDL_BUTTON_LEFT, .down = true,
.windowID = SDL_GetWindowID(g_win)}};
run_on_window(probe_event, &e);
e.button.type = SDL_EVENT_MOUSE_BUTTON_UP, e.button.down = false;
run_on_window(probe_event, &e);
return 1;
}
+377
View File
@@ -0,0 +1,377 @@
// The Win32 back end of the window (gfx_wsys.h): the window and its message loop on a thread of their own, F11
// fullscreen, the settings TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, the monitor's refresh rate, focus, the Vulkan surface.
// The swapchain and the present are gfx_present.c's. The in-game menu's keyboard and mouse come from the window
// procedure (ui/menu_win.cpp), and so do the game's (input_win.c).
//
// The process is per-monitor DPI aware (totk.manifest): every size here is physical pixels, TOTK_WINDOW_SIZE included,
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <imm.h>
#include "cfg.h"
#include "flight.h"
#include "fs.h"
#include "gfx.h"
#include "gfx_wsys.h"
#include "input.h"
#include "input_win.h"
#include "nvn_api.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#include <vulkan/vulkan_win32.h>
// ---- window state
static HWND g_hwnd;
static PlatEvent g_wnd_ready; // manual-reset: the window thread made the window or gave up
static int g_wnd_failed;
static int g_background; // TOTK_BACKGROUND_WINDOW: the window stays behind the user's windows
static int g_fs_pending; // window thread: TOTK_FULLSCREEN waits for the first activation
static volatile int32_t g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
HWND gfx_window_handle(void) { return g_hwnd; }
int gfx_window_vk_extensions(const char **out, int max) {
if (max < 2) return 0;
out[0] = VK_KHR_SURFACE_EXTENSION_NAME;
out[1] = VK_KHR_WIN32_SURFACE_EXTENSION_NAME;
return 2;
}
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
(void)inst;
return vkGetPhysicalDeviceWin32PresentationSupportKHR(pd, family) != VK_FALSE;
}
// ---- window thread
#define WND_STYLE WS_OVERLAPPEDWINDOW
#define WM_APP_SETTINGS (WM_APP + 1)
#define WM_APP_FULLSCREEN (WM_APP + 2) // the fullscreen setting, held back until the window is first activated
static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window behind fullscreen (window thread)
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
#define cfg_fullscreen gfx_cfg_fullscreen
#define cfg_window_size gfx_cfg_window_size
unsigned gfx_window_dpi(void) { return (unsigned)plat_cas32(&g_dpi, 0, 0); }
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
// at another display scale.
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
RECT r = {0, 0, cw, ch};
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
*ow = r.right - r.left;
*oh = r.bottom - r.top;
}
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
static void keep_awake(int on) {
static int cur = -1;
if (on == cur) return;
cur = on;
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
}
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
// user's windows and takes no focus.
static void toggle_fullscreen(HWND h) {
LONG style = GetWindowLongW(h, GWL_STYLE);
if (style & WS_OVERLAPPEDWINDOW) {
MONITORINFO mi = {sizeof mi};
if (!GetWindowPlacement(h, &g_saved_place) || !GetMonitorInfoW(MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY), &mi))
return;
SetWindowLongW(h, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
SetWindowPos(h, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top,
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | (g_background ? SWP_NOZORDER | SWP_NOACTIVATE : 0));
} else {
SetWindowLongW(h, GWL_STYLE, (style & ~WS_POPUP) | WND_STYLE);
if (g_background) g_saved_place.showCmd = SW_SHOWNOACTIVATE;
SetWindowPlacement(h, &g_saved_place);
SetWindowPos(h, NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED |
(g_background ? SWP_NOACTIVATE : 0));
}
}
// The window size and fullscreen state last taken from the settings (window thread). apply_settings acts on a change of
// either value only, so a window resized by hand keeps its size when another setting changes.
static char g_applied_size[32];
static int g_applied_fs;
static void note_applied(void) {
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
g_applied_fs = cfg_fullscreen();
}
// Window thread: the window takes the size and fullscreen state of the settings when they changed. In fullscreen the
// size goes into the placement restored when fullscreen ends.
static void apply_settings(HWND h) {
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
note_applied();
if (size_changed) {
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(h, cw, ch, &ow, &oh);
if (is_fullscreen(h)) {
RECT *n = &g_saved_place.rcNormalPosition;
n->right = n->left + ow;
n->bottom = n->top + oh;
} else {
RECT have;
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
}
if (fs_changed && fs != is_fullscreen(h)) {
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
}
}
void gfx_window_apply_settings(void) {
if (g_hwnd) PostMessageW(g_hwnd, WM_APP_SETTINGS, 0, 0);
}
void gfx_window_request_close(void) {
if (g_hwnd) PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
}
// ---- refresh rate of the window's monitor (TOTK_FPS_LIMIT=display), measured when the window opens, lands on another
// monitor or the display mode changes (gfx_window_set_refresh keeps it). QueryDisplayConfig gives the exact rational
// rate (59.94, 143.998); EnumDisplaySettings' whole hertz is the fallback.
static HMONITOR g_rate_mon;
static double monitor_rate_hz(HMONITOR mon) {
MONITORINFOEXW mi = {.cbSize = sizeof mi};
if (!GetMonitorInfoW(mon, (MONITORINFO *)&mi)) return 0;
double hz = 0;
UINT32 np = 0, nm = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) == ERROR_SUCCESS && np && nm) {
DISPLAYCONFIG_PATH_INFO *paths = calloc(np, sizeof *paths);
DISPLAYCONFIG_MODE_INFO *modes = calloc(nm, sizeof *modes);
if (paths && modes && QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL) == ERROR_SUCCESS)
for (UINT32 i = 0; i < np && !hz; i++) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof src,
.adapterId = paths[i].sourceInfo.adapterId,
.id = paths[i].sourceInfo.id}};
DISPLAYCONFIG_RATIONAL r = paths[i].targetInfo.refreshRate;
if (DisplayConfigGetDeviceInfo(&src.header) == ERROR_SUCCESS && !wcscmp(src.viewGdiDeviceName, mi.szDevice) &&
r.Denominator)
hz = (double)r.Numerator / r.Denominator;
}
free(paths);
free(modes);
}
DEVMODEW dm = {.dmSize = sizeof dm};
if (!hz && EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
hz = dm.dmDisplayFrequency; // 0 and 1 mean the hardware default
return hz;
}
// Window thread. force: the display mode changed, measure even on the same monitor.
static void refresh_rate_update(HWND h, int force) {
HMONITOR mon = MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY);
if (!force && mon == g_rate_mon) return;
g_rate_mon = mon;
gfx_window_set_refresh(monitor_rate_hz(mon));
}
int gfx_window_monitor_size(int *w, int *h) {
MONITORINFO mi = {sizeof mi};
if (!g_hwnd || !GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) return 0;
*w = mi.rcMonitor.right - mi.rcMonitor.left;
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
return 1;
}
static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
LRESULT input_res;
if (input_window_msg(h, m, w, l, &input_res)) return input_res;
if (menu_window_msg(h, m, w, l, &input_res)) return input_res;
switch (m) {
case WM_KEYDOWN:
if (w == VK_F11 && !(l & (1 << 30))) { // bit 30: auto-repeat
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
cfg_set("TOTK_FULLSCREEN", is_fullscreen(h) ? "1" : "0"); // the menu shows it; the next start keeps it
cfg_save();
note_applied(); // what cfg_get reads now (the environment may still override it)
}
break;
case WM_APP_SETTINGS:
apply_settings(h);
return 0;
case WM_APP_FULLSCREEN:
if (cfg_fullscreen() && !is_fullscreen(h)) {
input_cursor_suspend(1);
toggle_fullscreen(h);
input_cursor_suspend(0);
}
return 0;
case WM_SIZE:
keep_awake(w != SIZE_MINIMIZED);
if (w != SIZE_MINIMIZED) gfx_window_mark_dirty();
refresh_rate_update(h, 0);
return 0;
case WM_EXITSIZEMOVE: // dragged onto another monitor
refresh_rate_update(h, 0);
break;
case WM_DISPLAYCHANGE:
refresh_rate_update(h, 1);
break;
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
if (!is_fullscreen(h) && !IsZoomed(h)) {
RECT c, r;
if (GetClientRect(h, &c)) {
r = (RECT){0, 0, c.right, c.bottom};
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
return TRUE;
}
}
}
break; // DefWindowProc: Windows scales the size with the DPI
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
const RECT *s = (const RECT *)l;
plat_xchg32(&g_dpi, LOWORD(w));
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
MONITORINFO mi = {sizeof mi};
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
} else {
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
}
return 0;
}
case WM_ACTIVATEAPP: {
int in = w != 0;
gfx_window_focus_changed(in);
if (in && g_fs_pending) {
g_fs_pending = 0;
PostMessageW(h, WM_APP_FULLSCREEN, 0, 0);
}
return 0;
}
case WM_CLOSE:
gfx_window_closed();
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcW(h, m, w, l);
}
static void window_main(void *arg) {
(void)arg;
HINSTANCE inst = GetModuleHandleW(NULL);
// Icon resource 1 of totk.rc (runtime/totk.ico): the title bar, the taskbar button and Alt+Tab.
WNDCLASSEXW wc = {.cbSize = sizeof wc, .lpfnWndProc = wnd_proc, .hInstance = inst,
.hIcon = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON), LR_SHARED),
.hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
RegisterClassExW(&wc);
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(NULL, cw, ch, &ow, &oh);
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
NULL, inst, NULL);
if (!g_hwnd) {
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
g_wnd_failed = 1;
} else {
// An IME switched on over the game attaches to this window and then shows its candidate window and eats the keys
// it composes, although there is nothing here to type into (a Chinese IME takes Shift, the B button by default).
// Detaching the game's window leaves the software keyboard alone: its edit box sits in a window of its own
// (swkbd_win.c) and gets the IME it needs.
ImmAssociateContext(g_hwnd, NULL);
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
plat_xchg32(&g_dpi, (int32_t)GetDpiForWindow(g_hwnd));
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!background) {
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
FlashWindowEx(&fw);
g_fs_pending = cfg_fullscreen();
}
note_applied();
refresh_rate_update(g_hwnd, 1);
keep_awake(1);
}
plat_event_set(&g_wnd_ready);
if (g_wnd_failed) return;
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return;
}
// ---- gfx_wsys.h
// Makes the window on its thread and waits until it is there.
int wsys_open(void) {
g_wnd_failed = 0;
plat_event_init(&g_wnd_ready, 1, 0);
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
plat_event_wait(&g_wnd_ready);
return !g_wnd_failed;
}
VkSurfaceKHR wsys_surface(VkInstance inst) {
VkWin32SurfaceCreateInfoKHR si = {.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
.hinstance = GetModuleHandleW(NULL), .hwnd = g_hwnd};
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkResult r = vkCreateWin32SurfaceKHR(inst, &si, NULL, &surface);
if (r != VK_SUCCESS) rt_log("gfx: vkCreateWin32SurfaceKHR failed (%d)", (int)r);
return r == VK_SUCCESS ? surface : VK_NULL_HANDLE;
}
void wsys_client_size(int *w, int *h) {
RECT rc = {0};
GetClientRect(g_hwnd, &rc);
*w = rc.right, *h = rc.bottom;
}
int wsys_minimized(void) { return IsIconic(g_hwnd) != 0; }
void wsys_resize(int cw, int ch) {
int ow, oh;
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
void wsys_probe_focus(int in) { SendMessageW(g_hwnd, WM_ACTIVATEAPP, in ? TRUE : FALSE, 0); }
void wsys_probe_escape(void) { SendMessageW(g_hwnd, WM_KEYDOWN, VK_ESCAPE, 0); }
int wsys_probe_click(int where) {
if (where == 0) SendMessageW(g_hwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
else SendMessageW(g_hwnd, WM_MOUSEACTIVATE, (WPARAM)g_hwnd, MAKELPARAM(where == 1 ? HTCLIENT : HTCAPTION, WM_LBUTTONDOWN));
return 1;
}
+25
View File
@@ -0,0 +1,25 @@
// Between gfx_present.c (swapchain, present, vsync) and the window system's back end (gfx_window_win.c with Win32,
// gfx_window_sdl.c with SDL). Host threads only.
#pragma once
#include "vk.h"
// ---- what the back end provides
int wsys_open(void); // makes the window (and its thread); 0 on failure. Once.
VkSurfaceKHR wsys_surface(VkInstance inst); // a new Vulkan surface of the window; VK_NULL_HANDLE on failure
void wsys_client_size(int *w, int *h); // the drawable area in pixels
int wsys_minimized(void); // nothing to present into
void wsys_resize(int w, int h); // gfx_window_resize: the drawable area becomes w x h
// --probe-window (gfx_window_probe.c): synthetic window events, handled on the window thread before these return.
void wsys_probe_focus(int in); // the window gains (1) or loses (0) the focus
void wsys_probe_escape(void); // Esc pressed
// A left click: 0 in the client area, 1 activating the window from its client area, 2 activating it from the title bar.
// 0 when the window system has no such event (then the caller skips that check).
int wsys_probe_click(int where);
// ---- what gfx_present.c provides the back end
void gfx_window_mark_dirty(void); // the window changed size: the next present rebuilds the swapchain
void gfx_window_focus_changed(int in); // the window gained (1) or lost (0) the focus
void gfx_window_closed(void); // the player closed the window: saves, then ends the process
void gfx_window_set_refresh(double hz); // the refresh rate of the window's monitor, 0 unknown
int gfx_cfg_fullscreen(void); // TOTK_FULLSCREEN
void gfx_cfg_window_size(int *w, int *h); // TOTK_WINDOW_SIZE, the default when unparsable or out of range
+17 -6
View File
@@ -91,11 +91,19 @@ static inline void ret_d(Cpu *c, double d) { uint64_t b; memcpy(&b, &d, 8); c->v
typedef struct { const char *name; GuestFn fn; } RtHostEntry;
#define RT_CAT2(a, b) a##b
#define RT_CAT(a, b) RT_CAT2(a, b)
// File-scope declarations; fn must be declared before. The loader binds them by symbol name.
// File-scope declarations; fn must be declared before. The loader binds them by symbol name. The entries of every
// object file meet in one section per kind, which hostfn.c walks: on PE/COFF ".ovr$m", which the linker sorts between
// hostfn.c's ".ovr$a" and ".ovr$z" markers; on ELF "totk_ovr", which the linker brackets with __start_totk_ovr and
// __stop_totk_ovr.
#ifdef _WIN32
#define RT_HOST_SECTION(kind) section("." #kind "$m")
#else
#define RT_HOST_SECTION(kind) section("totk_" #kind)
#endif
#define HOST_OVERRIDE(sym, fn) /* replaces sym in every GOT/GLOB_DAT/ABS64 of every module */ \
__attribute__((used, section(".ovr$m"))) static const RtHostEntry RT_CAT(rt_ovr_, __COUNTER__) = {sym, fn}
__attribute__((used, RT_HOST_SECTION(ovr))) static const RtHostEntry RT_CAT(rt_ovr_, __COUNTER__) = {sym, fn}
#define HOST_OBSERVER(sym, fn) /* JUMP_SLOT only; fn calls rt_original(c) */ \
__attribute__((used, section(".obs$m"))) static const RtHostEntry RT_CAT(rt_obs_, __COUNTER__) = {sym, fn}
__attribute__((used, RT_HOST_SECTION(obs))) static const RtHostEntry RT_CAT(rt_obs_, __COUNTER__) = {sym, fn}
uint64_t hostfn_register(const char *name, GuestFn fn); // synthetic address; before hostfn_seal only. The same
// (name, fn) pair returns the same address
GuestFn hostfn_override(const char *name); // NULL if none
@@ -176,8 +184,9 @@ typedef struct { const char *file, *sha256; } RtFileHash;
extern const RtFileHash rt_exefs_sha256[]; // tools/data/expected-<version>.json "exefs_sha256"
extern const int rt_exefs_sha256_count;
extern const char rt_default_exefs[], rt_default_romfs[], rt_default_profile[];
extern const int rt_game_version; // the game build: 121 (1.2.1) or 143 (1.4.3)
extern const int rt_game_version; // the game build: 111 (1.1.1), 121 (1.2.1) or 143 (1.4.3)
extern const char rt_port_version[]; // VERSION at the build's root: 0.1.0, 0.1.0+973c626 in a release
#define RT_V111 (rt_game_version == 111) // 1.1.1: its own boot constants and data slots
#define RT_V143 (rt_game_version >= 143)
// ---- kernel (kernel.c)
@@ -245,6 +254,7 @@ void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene
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
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)
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
// page committed; logs the sets
@@ -284,13 +294,14 @@ void rt_boot(void); // boot.c
// modules, registers its host functions, then seals (hostfn_seal, or implicitly at the first rt_call). Return the
// process exit code.
int probe_cfg(void); // cfg_probe.c
int probe_dpi(void); // dpi_probe.c
int probe_dpi(void); // dpi_probe_win.c (Windows only)
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
int probe_gfx_abi(void); // gfx_abi_probe.c
int probe_vkdevice(void); // vk_device.c
int probe_nvn(void), probe_nvn_tex(void); // nvn_probe.c, nvn_tex_probe.c
int probe_window(void); // gfx_window.c
int probe_window(void); // gfx_window_probe.c
int probe_shader(void); // gfx_shader.c
int probe_census_dir(char *dir, size_t n); // gfx_shader_probe.c: build/<version>/shaders for the probes
int probe_vkmem(void), probe_vkexec(void), probe_astc(void); // tex_probe.c, vk_exec_probe.c, astc_probe.c
int probe_movie(void); // movie_probe.c: host VP9 movie::Decoder on one RomFS movie
int vk_pipeline_prepare_shaders(void); // vk_pipeline.c: --prepare-shaders (tools/prepare_shaders.py)
+11
View File
@@ -14,11 +14,22 @@
#define MAX_HOSTFN 4096 // dispatch.c holds HOST_CAP / 2 host functions
#define MAX_ADDED 64
#ifdef _WIN32
// PE/COFF: the linker sorts .ovr$a < .ovr$m (entries, host.h) < .ovr$z by the text after '$'.
__attribute__((section(".ovr$a"), used)) static const RtHostEntry ovr_start[1];
__attribute__((section(".ovr$z"), used)) static const RtHostEntry ovr_stop[1];
__attribute__((section(".obs$a"), used)) static const RtHostEntry obs_start[1];
__attribute__((section(".obs$z"), used)) static const RtHostEntry obs_stop[1];
#else
// ELF: the linker defines __start_<section> and __stop_<section> around a section named like a C identifier. Weak: a
// build without observers has no such section, and both bounds are then NULL.
extern const RtHostEntry __start_totk_ovr[] __attribute__((weak)), __stop_totk_ovr[] __attribute__((weak));
extern const RtHostEntry __start_totk_obs[] __attribute__((weak)), __stop_totk_obs[] __attribute__((weak));
#define ovr_start __start_totk_ovr
#define ovr_stop __stop_totk_ovr
#define obs_start __start_totk_obs
#define obs_stop __stop_totk_obs
#endif
typedef struct {
const char *name;
+685
View File
@@ -0,0 +1,685 @@
// Host input for nn::hid (runtime/ov_input.c): an SDL3 gamepad (no SDL window needed), the keyboard and the mouse,
// merged into Switch npad button bits and stick axes. Button order follows libnx HidNpadButton (ISC). The key and pad
// bindings are the tables in input_map.c (totk.ini overrides them). The keyboard and the mouse come from the window
// system's back end (input_wsys.h: input_win.c with Win32, input_sdl.c with SDL), which also owns the cursor capture.
// Keys and their map: docs/controls.md.
// The gamepad also rumbles (input_rumble) and, when it has a gyro and an accelerometer, feeds the six-axis sensor from a
// thread of its own (input_motion_start). Every SDL call on the gamepad is made under `lock`.
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "cfg.h"
#include "host.h"
#include "input.h"
#include "input_map.h"
#include "input_wsys.h"
#include "nvn_api.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
static PlatMutex lock = PLAT_MUTEX_INIT;
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
static SDL_Gamepad *pad;
static uint64_t next_scan;
static int pad_rumble; // the open gamepad has rumble motors
static volatile int32_t pad_labels[4]; // ... and the labels of its south, east, west, north buttons (PAD_LABEL_*), any thread
static int pad_east_a; // ... and labels its east face button A (Nintendo layout under TOTK_PAD_LAYOUT=auto)
static int pad_layout = -1; // PAD_LAYOUT_* in effect for the open gamepad, -1 to pick again; live (cfg_version)
static unsigned pad_layout_seen;
static volatile int32_t pad_layout_shown = -1; // pad_layout for the menu (any thread), -1 without a gamepad
static SDL_JoystickID sensor_pad; // the gamepad whose sensors the motion thread set up, 0 none
static int sensor_ok; // ... and it has both sensors, enabled
static int motion_thread; // 0 not started, 1 running, -1 off (TOTK_MOTION=0, no SDL)
// SDL buttons of the gamepad inputs (input_map.h PAD_*), by position; PAD_ZL and PAD_ZR are the triggers (axes).
static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
[PAD_SOUTH] = SDL_GAMEPAD_BUTTON_SOUTH, [PAD_EAST] = SDL_GAMEPAD_BUTTON_EAST, [PAD_WEST] = SDL_GAMEPAD_BUTTON_WEST,
[PAD_NORTH] = SDL_GAMEPAD_BUTTON_NORTH, [PAD_LSTICK] = SDL_GAMEPAD_BUTTON_LEFT_STICK,
[PAD_RSTICK] = SDL_GAMEPAD_BUTTON_RIGHT_STICK, [PAD_L] = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
[PAD_LPADDLE1] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, [PAD_RPADDLE1] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
[PAD_LPADDLE2] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, [PAD_RPADDLE2] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
};
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
// <frame> and its hold are 60 Hz ticks of game time since the first present (frame_step_ticks: presents at 60 fps), so
// the title menus get the presses at the same game time at any frame limit. A line "@<ms> ..." is timed instead: <ms>
// and its hold are milliseconds of the world clock (frame_step_world_us, game time since the loaded save's first
// frame), so it lands at the same game time at any frame rate and load length. A timed event shorter than a frame is
// applied once. In a script with timed lines (a recording) the untimed ones belong before the world clock: they stop
// when it starts, as the load ends at another tick in each run.
// at, hold: ticks, or microseconds of the world clock when timed.
typedef struct { uint64_t at, hold; int bit, stick, timed; int32_t x, y; int logged; char text[48]; } ScriptEv;
static const struct { const char *name; int bit; } script_buttons[] = {
{"A", B_A}, {"B", B_B}, {"X", B_X}, {"Y", B_Y}, {"LCLICK", B_STICK_L}, {"RCLICK", B_STICK_R}, {"L", B_L}, {"R", B_R},
{"ZL", B_ZL}, {"ZR", B_ZR}, {"PLUS", B_PLUS}, {"MINUS", B_MINUS}, {"LEFT", B_LEFT}, {"UP", B_UP}, {"RIGHT", B_RIGHT}, {"DOWN", B_DOWN},
};
static ScriptEv *script_ev;
static int script_n, script_state; // 0 not loaded, 1 loaded
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
static void script_load(void) {
script_state = 1;
const char *path = cfg_get("TOTK_INPUT_SCRIPT");
if (!path || !*path) return;
FILE *f = fopen(path, "r");
if (!f) { rt_log("input: cannot open TOTK_INPUT_SCRIPT %s", path); return; }
char line[256];
int cap = 0, ln = 0;
while (fgets(line, sizeof line, f)) {
ln++;
char *h = strchr(line, '#');
if (h) *h = 0;
char name[32];
double at;
int off = 0, timed = line[0] == '@';
if (sscanf(line + timed, "%lf %31s%n", &at, name, &off) < 2 || at < 0) continue;
off += timed;
double unit = timed ? 1000.0 : 1.0; // ms to us, or presents
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
long a, b;
double c;
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
} else {
for (size_t i = 0; i < sizeof script_buttons / sizeof *script_buttons; i++)
if (!strcmp(name, script_buttons[i].name)) e.bit = script_buttons[i].bit;
if (e.bit < 0) { rt_log("input: script line %d: unknown button %s", ln, name); continue; }
if (sscanf(line + off, "%lf", &c) == 1 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
snprintf(e.text, sizeof e.text, "%s", name);
}
if (script_n == cap) {
cap = cap ? cap * 2 : 32;
script_ev = realloc(script_ev, (size_t)cap * sizeof *script_ev);
if (!script_ev) { script_n = 0; fclose(f); return; }
}
script_ev[script_n++] = e;
script_mixed |= timed;
}
fclose(f);
rt_log("input: script %s, %d events", path, script_n);
}
static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
if (!script_state) script_load();
if (!script_n) return;
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), tick = frame_step_ticks();
int64_t w = frame_step_world_us();
for (int i = 0; i < script_n; i++) {
ScriptEv *e = &script_ev[i];
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
uint64_t now = e->timed ? (uint64_t)w : tick;
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
if (!e->logged) {
e->logged = 1;
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
}
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
else *b |= 1ull << e->bit;
}
}
// TOTK_INPUT_RECORD=<file>: writes the merged input as TOTK_INPUT_SCRIPT lines, one per button press and per stick
// segment, so a played session replays as a script. A stick segment ends when an axis moves by more than REC_STICK_STEP.
// An event that begins on the world clock is written timed ("@<ms>", hold in ms), one before it in ticks
// (frame_step_ticks). When the world clock starts, held buttons and sticks are cut there and go on as timed events.
enum { REC_STICK_STEP = 2048 };
static FILE *rec;
static int rec_state; // 0 not opened, 1 recording, -1 off
static uint64_t rec_down[16]; // tick + 1 at which each button went down, 0 while up
static int64_t rec_down_w[16]; // the world clock then, -1 before it started
static uint64_t rec_stick_from[2]; // tick + 1 at which the current non-zero stick segment began
static int64_t rec_stick_w[2];
static int32_t rec_stick[2][2];
static int64_t rec_last_w = -1; // the world clock at the previous apply
// "<tick> <what> <hold>" or "@<ms> <what> <hold ms>": from tick from and world clock from_w to now.
static void rec_line(uint64_t from, int64_t from_w, uint64_t fr, int64_t w, const char *what) {
if (from_w >= 0 && w >= from_w)
fprintf(rec, "@%.1f %s %.1f\n", from_w / 1000.0, what, (w > from_w ? w - from_w : 1000) / 1000.0);
else
fprintf(rec, "%llu %s %llu\n", (unsigned long long)from, what, (unsigned long long)(fr > from ? fr - from : 1));
}
static void rec_close_stick(int s, uint64_t fr, int64_t w) {
if (!rec_stick_from[s]) return;
char what[48];
snprintf(what, sizeof what, "%s %d %d", s ? "RSTICK" : "LSTICK", rec_stick[s][0], rec_stick[s][1]);
rec_line(rec_stick_from[s] - 1, rec_stick_w[s], fr, w, what);
rec_stick_from[s] = 0;
}
static void rec_flush_all(void) {
if (rec_state != 1) return;
uint64_t fr = frame_step_ticks() + 1;
int64_t w = frame_step_world_us();
for (int i = 0; i < 16; i++)
if (rec_down[i]) { rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name); rec_down[i] = 0; }
rec_close_stick(0, fr, w);
rec_close_stick(1, fr, w);
fflush(rec);
}
static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
if (!rec_state) {
const char *path = cfg_get("TOTK_INPUT_RECORD"); // no default: not recording when unset
rec = path && *path ? fopen(path, "w") : NULL;
rec_state = rec ? 1 : -1;
if (path && *path && !rec) rt_log("input: cannot open TOTK_INPUT_RECORD %s", path);
if (rec) { fprintf(rec, "# Recorded input (TOTK_INPUT_RECORD); frame = 60 Hz ticks of game time, @ms = world clock.\n"); atexit(rec_flush_all);
rt_log("input: recording to %s", path); }
}
if (rec_state != 1) return;
uint64_t fr = frame_step_ticks();
int64_t w = frame_step_world_us();
int wrote = 0;
if (w >= 0 && rec_last_w < 0) { // the world clock starts: what is held goes on as timed events from @0
for (int i = 0; i < 16; i++)
if (rec_down[i]) {
rec_line(rec_down[i] - 1, -1, fr, -1, script_buttons[i].name);
rec_down[i] = fr + 1, rec_down_w[i] = w;
wrote = 1;
}
for (int s = 0; s < 2; s++)
if (rec_stick_from[s]) {
rec_close_stick(s, fr, -1);
rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
wrote = 1;
}
}
rec_last_w = w;
for (int i = 0; i < 16; i++) {
int bit = script_buttons[i].bit, down = (b >> bit & 1) != 0;
if (down && !rec_down[i]) rec_down[i] = fr + 1, rec_down_w[i] = w;
else if (!down && rec_down[i]) {
rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name);
rec_down[i] = 0;
wrote = 1;
}
}
const int32_t *st[2] = {sl, sr};
for (int s = 0; s < 2; s++) {
int moved = abs(st[s][0] - rec_stick[s][0]) > REC_STICK_STEP || abs(st[s][1] - rec_stick[s][1]) > REC_STICK_STEP ||
((st[s][0] || st[s][1]) != (rec_stick[s][0] || rec_stick[s][1]));
if (!moved) continue;
if (rec_stick_from[s]) { rec_close_stick(s, fr, w); wrote = 1; }
rec_stick[s][0] = st[s][0]; rec_stick[s][1] = st[s][1];
if (st[s][0] || st[s][1]) rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
}
if (wrote) fflush(rec);
}
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
static unsigned mouse_sens_seen;
static float mouse_sens_get(void) {
unsigned ver = cfg_version();
if (mouse_sens >= 0 && ver == mouse_sens_seen) return mouse_sens;
mouse_sens_seen = ver;
double v = cfg_double("TOTK_MOUSE_SENS");
mouse_sens = (float)(v < 0 ? 0 : v > 10 ? 10 : v);
return mouse_sens;
}
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
static void mouse_camera(int32_t out[2]) {
static uint64_t frame = ~0ull, t0;
static int32_t held[2];
uint64_t now = plat_now_ns();
int32_t dx, dy;
if (!ikb_captured()) {
ikb_mouse_take(&dx, &dy);
held[0] = held[1] = 0;
frame = ~0ull;
} else {
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
double age = frame == ~0ull ? 0 : (double)(now - t0) / 1e9;
if (fr != frame) {
double dt = age < 0.004 ? 0.004 : age > 0.1 ? 0.1 : age;
ikb_mouse_take(&dx, &dy);
input_mouse_stick(dx, dy, dt, mouse_sens_get(), held);
frame = fr;
t0 = now;
} else if (age > 0.1) {
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
}
}
out[0] = held[0];
out[1] = held[1];
}
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
static unsigned stick_dead_seen;
static float stick_dead_get(void) {
unsigned ver = cfg_version();
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
stick_dead_seen = ver;
double v = cfg_double("TOTK_STICK_DEADZONE");
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
return stick_dead;
}
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) {
input_pad_stick(SDL_GetGamepadAxis(pad, ax), -(int32_t)SDL_GetGamepadAxis(pad, ay), dead, out);
}
// Open the first gamepad when none is open or the open one went away; logs once per change.
static void scan_pads(void) {
if (pad && SDL_GamepadConnected(pad)) return;
if (pad) {
SDL_CloseGamepad(pad); pad = NULL; sensor_pad = 0; sensor_ok = 0;
plat_xchg32(&pad_layout_shown, -1);
for (int i = 0; i < 4; i++) plat_xchg32(&pad_labels[i], PAD_LABEL_NONE);
rt_log("input: gamepad disconnected");
}
int n = 0;
SDL_JoystickID *ids = SDL_GetGamepads(&n);
for (int i = 0; ids && i < n && !pad; i++) pad = SDL_OpenGamepad(ids[i]);
SDL_free(ids);
if (!pad) return;
pad_rumble = SDL_GetBooleanProperty(SDL_GetGamepadProperties(pad), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false);
pad_east_a = SDL_GetGamepadButtonLabel(pad, SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_A;
for (int i = 0; i < 4; i++) {
SDL_GamepadButtonLabel lb = SDL_GetGamepadButtonLabel(pad, pad_sdl[PAD_SOUTH + i]);
plat_xchg32(&pad_labels[i], lb == SDL_GAMEPAD_BUTTON_LABEL_A ? PAD_LABEL_A
: lb == SDL_GAMEPAD_BUTTON_LABEL_B ? PAD_LABEL_B
: lb == SDL_GAMEPAD_BUTTON_LABEL_X ? PAD_LABEL_X
: lb == SDL_GAMEPAD_BUTTON_LABEL_Y ? PAD_LABEL_Y
: lb == SDL_GAMEPAD_BUTTON_LABEL_CROSS ? PAD_LABEL_CROSS
: lb == SDL_GAMEPAD_BUTTON_LABEL_CIRCLE ? PAD_LABEL_CIRCLE
: lb == SDL_GAMEPAD_BUTTON_LABEL_SQUARE ? PAD_LABEL_SQUARE
: lb == SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE ? PAD_LABEL_TRIANGLE
: PAD_LABEL_NONE);
}
pad_layout = -1;
int motion = SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL);
rt_log("input: gamepad \"%s\" (%s, %s, east button %s)", SDL_GetGamepadName(pad), pad_rumble ? "rumble" : "no rumble",
motion ? "gyro and accelerometer" : "no motion sensors", pad_east_a ? "labelled A" : "not labelled A");
}
// Under lock: the layout of the open gamepad, picked again when a setting or the gamepad changed; logs each change.
static int pad_layout_get(void) {
unsigned ver = cfg_version();
if (pad_layout >= 0 && ver == pad_layout_seen) return pad_layout;
pad_layout_seen = ver;
int was = pad_layout;
const char *set = cfg_value("TOTK_PAD_LAYOUT");
pad_layout = input_pad_layout_pick(set, pad_east_a);
if (pad_layout != was)
rt_log("input: gamepad layout %s (pad_layout = %s)", pad_layout == PAD_LAYOUT_XBOX ? "xbox" : "nintendo", set);
plat_xchg32(&pad_layout_shown, pad_layout);
return pad_layout;
}
// The TOTK_BIND_* values last applied, one per line; the tables are built again when one changes (the menu's remapping rows write
// them while the game runs). Under lock.
static unsigned binds_seen = ~0u;
static char binds_applied[4096];
static void binds_refresh(void) {
unsigned v = cfg_version();
if (v == binds_seen) return;
binds_seen = v;
char now[sizeof binds_applied];
size_t len = 0;
for (int a = 0; a < ACTION_COUNT; a++) {
char var[32];
snprintf(var, sizeof var, "TOTK_BIND_%s", input_action_name(a));
const char *s = cfg_get(var);
len += (size_t)snprintf(now + len, len < sizeof now ? sizeof now - len : 0, "%s\n", s ? s : "");
}
if (!strcmp(now, binds_applied)) return;
memcpy(binds_applied, now, sizeof now);
input_map_configure();
}
// TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> (test runs): an SDL virtual gamepad whose sticks rest at these raw SDL axis values
// (-32768..32767, down is +y), attached before the first scan so it is the gamepad the game reads: drift and full
// deflection go through the same mapping and dead zone as a real pad.
static void test_pad_attach(void) {
static SDL_Joystick *joy; // kept open: a virtual joystick's axes are set through an open handle
const char *e = cfg_get("TOTK_TEST_PAD");
int v[4];
if (!e || !*e) return;
if (sscanf(e, "%d,%d,%d,%d", &v[0], &v[1], &v[2], &v[3]) != 4) {
rt_log("input: TOTK_TEST_PAD=%s is not <lx>,<ly>,<rx>,<ry>; no test gamepad", e);
return;
}
SDL_VirtualJoystickDesc d;
SDL_INIT_INTERFACE(&d);
d.type = SDL_JOYSTICK_TYPE_GAMEPAD;
d.naxes = SDL_GAMEPAD_AXIS_COUNT;
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
d.name = "TotK test gamepad";
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
joy = id ? SDL_OpenJoystick(id) : NULL;
if (!joy) {
rt_log("input: test gamepad not attached (%s)", SDL_GetError());
return;
}
static const SDL_GamepadAxis axes[4] = {SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX,
SDL_GAMEPAD_AXIS_RIGHTY};
for (int i = 0; i < 4; i++)
SDL_SetJoystickVirtualAxis(joy, axes[i], (Sint16)(v[i] < -32768 ? -32768 : v[i] > 32767 ? 32767 : v[i]));
rt_log("input: test gamepad attached, sticks at %d,%d %d,%d", v[0], v[1], v[2], v[3]);
}
static void start(void) {
binds_refresh();
const char *e = cfg_get("TOTK_INPUT");
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
else state = 1;
if (state == 1) test_pad_attach();
if (state == 1) {
scan_pads();
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
}
if (state == -1) state = 2; // keyboard only
}
static void stick_bits(uint64_t *b, int32_t x, int32_t y, int first) {
if (x < -STICK_DIR) *b |= 1ull << (first + 0);
if (y > STICK_DIR) *b |= 1ull << (first + 1);
if (x > STICK_DIR) *b |= 1ull << (first + 2);
if (y < -STICK_DIR) *b |= 1ull << (first + 3);
}
static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
int64_t a = (int64_t)*x * *x + (int64_t)*y * *y, b = (int64_t)kx * kx + (int64_t)ky * ky;
if (b > a) { *x = kx; *y = ky; }
}
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
static int pad_physical(uint8_t pd[PAD_COUNT]) {
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
SDL_UpdateGamepads();
if (!pad || !SDL_GamepadConnected(pad)) return 0;
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
for (int p = PAD_ZR + 1; p < PAD_COUNT; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]); // false without paddles
return 1;
}
// Under lock, state 1: the open gamepad's buttons (Switch bits after TOTK_PAD_LAYOUT and the TOTK_BIND_* lines) and
// sticks; 0 without a connected gamepad.
static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
if (!pad_physical(pd)) return 0;
*b |= input_map_pad_buttons(pd, pad_layout_get());
float dead = stick_dead_get();
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
return 1;
}
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
plat_mutex_lock(&lock);
if (!state) start();
if (state > 0) {
binds_refresh();
if (state == 1) pad_read(&b, sl, sr);
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.
static int was_captured;
static uint8_t mouse_ignore[KEY_COUNT];
int cap = ikb_captured();
for (int k = 1; k < KEY_COUNT; k++) {
down[k] = ikb_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;
}
was_captured = cap;
b |= input_map_buttons(down);
int32_t k[2];
input_map_stick(down, 0, k);
pick(&sl[0], &sl[1], k[0], k[1]);
input_map_stick(down, 1, k);
pick(&sr[0], &sr[1], k[0], k[1]);
mouse_camera(k);
pick(&sr[0], &sr[1], k[0], k[1]);
}
}
script_apply(&b, sl, sr);
rec_apply(b, sl, sr);
// Minus and Plus held together (gamepad or keys) open or close the menu and never reach the game
static int combo_held;
int32_t st[4] = {sl[0], sl[1], sr[0], sr[1]};
int toggle = input_menu_gate(&combo_held, menu_is_open(), &b, st);
static int overlay_held; // L3+R3 step the performance overlay (F10)
int overlay = input_overlay_gate(&overlay_held, &b);
sl[0] = st[0], sl[1] = st[1], sr[0] = st[2], sr[1] = st[3];
plat_mutex_unlock(&lock);
if (toggle) menu_toggle();
if (overlay) menu_perf_cycle();
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
}
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
static volatile int32_t g_block;
static volatile int32_t g_menu; // the in-game menu, the same way (input_menu)
int input_cursor_allowed(void) {
const char *in = cfg_get("TOTK_INPUT");
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none"));
}
void input_block(int on) {
plat_xchg32(&g_block, on ? 1 : 2);
ikb_cursor_update();
}
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
// the release. The cursor is released while it is open, for the menu's mouse.
void input_menu(int open) {
plat_xchg32(&g_menu, open ? 1 : 2);
if (!open) ikb_menu_closed(); // the capture before Esc comes back
ikb_cursor_update();
}
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held.
static int blocked(volatile int32_t *flag, uint64_t buttons) {
int32_t k = *flag;
return k && !(k == 2 && !buttons && plat_cas32(flag, 0, 2) == 2);
}
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
poll_merged(buttons, lx, ly, rx, ry);
int text = blocked(&g_block, *buttons), menu = blocked(&g_menu, *buttons);
if (!text && !menu) return;
*buttons = 0; *lx = *ly = *rx = *ry = 0;
}
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
poll_merged(buttons, lx, ly, rx, ry);
}
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
plat_mutex_lock(&lock);
if (!state) start();
if (state > 0) binds_refresh();
int ok = state == 1 && pad_read(&b, sl, sr);
plat_mutex_unlock(&lock);
*buttons = b;
*lx = sl[0];
*ly = sl[1];
return ok;
}
uint16_t rumble_motor(float a, float curve, float strength) {
if (!(a > 0) || !(strength > 0)) return 0;
float v = powf(a > 1 ? 1 : a, curve) * strength * 65535.0f + 0.5f;
return v >= 65535.0f ? 65535 : (uint16_t)v;
}
int input_pad_layout(void) {
return (int)plat_cas32(&pad_layout_shown, 0, 0);
}
int input_pad_label(int pad_id) {
if (pad_id < PAD_SOUTH || pad_id > PAD_NORTH) return PAD_LABEL_NONE;
return (int)plat_cas32(&pad_labels[pad_id - PAD_SOUTH], 0, 0);
}
void input_map_snapshot(InputMap *m) {
plat_mutex_lock(&lock);
if (!state) start();
if (state > 0) binds_refresh();
*m = input_map;
plat_mutex_unlock(&lock);
}
int input_capture(int reset, int *id) {
static uint8_t prev_k[KEY_COUNT], prev_p[PAD_COUNT];
static int prev_esc;
uint8_t k[KEY_COUNT] = {0}, p[PAD_COUNT] = {0};
int fg = ikb_foreground();
for (int i = 1; i < KEY_COUNT; i++) k[i] = fg && ikb_down(i);
int esc = fg && ikb_escape();
plat_mutex_lock(&lock);
if (!state) start();
if (state == 1) pad_physical(p);
plat_mutex_unlock(&lock);
int got = INPUT_CAPTURE_NONE;
if (!reset) {
if (esc && !prev_esc) got = INPUT_CAPTURE_CANCEL;
for (int i = 1; i < KEY_COUNT && !got; i++)
if (k[i] && !prev_k[i]) got = INPUT_CAPTURE_KEY, *id = i;
for (int i = 1; i < PAD_COUNT && !got; i++)
if (p[i] && !prev_p[i]) got = INPUT_CAPTURE_PAD, *id = i;
}
memcpy(prev_k, k, sizeof k);
memcpy(prev_p, p, sizeof p);
prev_esc = esc;
return got;
}
int input_rumble_triggers_enabled(void) {
return cfg_bool("TOTK_RUMBLE_TRIGGERS");
}
int input_rumble(uint16_t low, uint16_t high, uint32_t ms) {
int ok = 0;
static int triggers = -1;
static unsigned seen;
if (triggers < 0 || seen != cfg_version()) seen = cfg_version(), triggers = input_rumble_triggers_enabled();
plat_mutex_lock(&lock);
if (state == 1 && pad && pad_rumble) {
ok = SDL_RumbleGamepad(pad, low, high, ms);
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
}
plat_mutex_unlock(&lock);
return ok;
}
// SDL gamepad sensor axes (SDL_sensor.h, a gamepad held in front of you): x to the right, y up out of the face (lying
// flat face up, +y points at the ceiling), z toward the player (out of the bottom edge). The accelerometer reads the
// push against gravity in m/s^2 (lying flat: y = +9.81); the gyro reads rad/s, counter-clockwise positive (right hand).
// Switch six-axis axes (nn::hid, as MacNativeGames' GCMotion feed and its Pro Controller HID path use them): x to the
// right, y toward the top edge (away from the player), z out of the face; acceleration as gravity reads (lying flat:
// z = -1 G). So Switch x = SDL x, Switch y = -SDL z, Switch z = SDL y: a rotation (right-handed to right-handed), so the
// gyro keeps its sense, while the acceleration is also negated (push -> gravity) and scaled to G.
void input_motion_axes(const float a[3], const float g[3], double acc[3], double gyro[3]) {
acc[0] = -a[0] / SDL_STANDARD_GRAVITY;
acc[1] = a[2] / SDL_STANDARD_GRAVITY;
acc[2] = -a[1] / SDL_STANDARD_GRAVITY;
gyro[0] = g[0];
gyro[1] = -g[2];
gyro[2] = g[1];
}
// Under lock: (re)enable the sensors when the open gamepad changed; 1 if it has both and they are on.
static int sensors_ready(void) {
SDL_JoystickID id = SDL_GetGamepadID(pad);
if (id == sensor_pad) return sensor_ok;
sensor_pad = id;
// Nothing drains the SDL event queue: no event per sample (the data still updates, SDL_SendJoystickSensor)
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
sensor_ok =SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) &&
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_GYRO, true) &&
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true);
if (sensor_ok) rt_log("input: motion sensors on (gyro %.0f Hz, accelerometer %.0f Hz)",
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_GYRO),
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_ACCEL));
else rt_log("input: \"%s\" has no gyro and accelerometer: the six-axis sensor reads a controller at rest",
SDL_GetGamepadName(pad));
return sensor_ok;
}
int input_motion_enabled(void) {
static int on = -1;
static unsigned seen;
unsigned v = cfg_version();
if (on < 0 || v != seen) {
seen = v;
on = cfg_bool("TOTK_MOTION");
}
return on;
}
// 250 Hz: the gamepads update (reports read, rumble expiry) and the newest gyro and accelerometer sample goes into the
// fusion. Without a gamepad with sensors, or with motion off (TOTK_MOTION=0, live), nothing is fed and the six-axis
// sensor reads at rest.
static void motion_main(void *arg) {
(void)arg;
for (;;) {
plat_wait_ns(4000000);
float a[3], g[3];
int have = 0;
plat_mutex_lock(&lock);
if (pad && SDL_GamepadConnected(pad)) {
SDL_UpdateGamepads();
have = sensors_ready() && SDL_GetGamepadSensorData(pad, SDL_SENSOR_ACCEL, a, 3) &&
SDL_GetGamepadSensorData(pad, SDL_SENSOR_GYRO, g, 3);
}
plat_mutex_unlock(&lock);
if (!have || !input_motion_enabled()) continue;
double acc[3], gyro[3];
input_motion_axes(a, g, acc, gyro);
motion_feed(motion_global(), acc, gyro, kern_now_ns());
}
}
void input_motion_start(void) {
plat_mutex_lock(&lock);
if (!state) start();
if (!motion_thread) {
if (!input_motion_enabled()) rt_log("input: motion off (TOTK_MOTION=0); it can be turned on while the game runs");
if (state != 1) motion_thread = -1;
else {
if (!plat_thread_spawn(motion_main, NULL, "motion", 0)) rt_fatal("input: cannot start the motion thread");
motion_thread = 1;
}
}
plat_mutex_unlock(&lock);
}
+5 -11
View File
@@ -1,9 +1,6 @@
// Host input sampling (runtime/input_win.c): SDL3 gamepad and the keyboard as one Switch npad.
// Host input sampling (runtime/input.c, with input_win.c or input_sdl.c): SDL3 gamepad and the keyboard as one Switch npad.
#pragma once
#include <stdint.h>
#ifdef _WIN32
#include <windows.h>
#endif
// Bits of libnx HidNpadButton (A=0 B=1 X=2 Y=3 StickL=4 StickR=5 L=6 R=7 ZL=8 ZR=9 Plus=10 Minus=11 Left=12 Up=13 Right=14
// Down=15) and the stick directions (StickLLeft..Down 16-19, StickRLeft..Down 20-23, derived from the sticks).
@@ -20,6 +17,7 @@ void input_menu(int open);
// Minus+Plus combo and the neutral pad while the menu is open (input_map.c, window-system free): *held is the caller's memory
// of the combo, sticks are lx, ly, rx, ry. Returns 1 in the poll where the combo goes down (toggle the menu once).
int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[4]);
int input_overlay_gate(int *held, uint64_t *buttons); // L3+R3: 1 in the poll both go down
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly);
// The gamepad layout in effect (PAD_LAYOUT_*, input_map.h; TOTK_PAD_LAYOUT, live); -1 without a gamepad. Any thread.
int input_pad_layout(void);
@@ -38,18 +36,14 @@ void input_map_snapshot(struct InputMap *m);
int input_capture(int reset, int *id);
// The character on a punctuation key (KEY_MINUS..KEY_GRAVE) in the keyboard layout in use, 0 for another key.
uint32_t input_key_char(int key);
// F8, the flight recorder's key (no KEY_ id), held while the game window is in front.
int input_flight_key(void);
// Mouse camera and cursor (input_win.c, Win32 window thread). The window procedure calls input_window_msg first for raw
// mouse input, cursor hiding and confinement; it returns 1 with *res set when the message is handled. input_cursor_suspend(1)
// releases the cursor around an F11 window change, (0) captures it again once the client area has its new size.
#ifdef _WIN32
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
int input_cursor_released(void); // Esc or a non-client activation released the cursor (probe)
// The menu's Win32 adapter (ui/menu_win.cpp), called next: F1 / Esc open or close the menu; while it is open, keys,
// mouse and focus go to it. Returns 1 with *res set when the message is fully handled.
int menu_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
#endif
void input_cursor_suspend(int on);
int input_cursor_released(void); // Esc or a non-client activation released the cursor (probe)
// Rumble: SDL_RumbleGamepad on the open gamepad, low (heavy, left) and high (light, right) motor 0..65535 for ms
// milliseconds. 1 if a gamepad with rumble took it. Callable from any thread.
+23 -13
View File
@@ -67,13 +67,13 @@ void input_map_reset(void) { input_map = defaults; }
static int key_id(const char *name) {
for (int k = KEY_A; k < KEY_COUNT; k++)
if (key_names[k] && !_stricmp(key_names[k], name)) return k;
if (key_names[k] && !strcasecmp(key_names[k], name)) return k;
return KEY_NONE;
}
static int pad_id(const char *name) {
for (int p = PAD_SOUTH; p < PAD_COUNT; p++)
if (!_stricmp(pad_names[p], name)) return p;
if (!strcasecmp(pad_names[p], name)) return p;
return PAD_NONE;
}
@@ -120,25 +120,25 @@ static void pads_put(InputMap *m, int layout, int action, const uint8_t *list, i
int input_map_bind(const char *button, const char *list) {
int action = -1;
for (int a = 0; a < ACTION_COUNT; a++)
if (!_stricmp(action_names[a], button)) action = a;
if (!strcasecmp(action_names[a], button)) action = a;
if (action < 0) { rt_log("input: bind: unknown button '%s'", button); return 0; }
uint8_t keys[INPUT_BINDING_MAX], pads[INPUT_PAD_BINDING_MAX];
int nk = 0, np = 0, none = 0, key_def = 0, pad_def = 0, bad = 0;
char buf[256];
snprintf(buf, sizeof buf, "%s", list);
for (char *save = NULL, *tok = strtok_s(buf, ",", &save); tok; tok = strtok_s(NULL, ",", &save)) {
for (char *save = NULL, *tok = strtok_r(buf, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
tok = trim(tok);
if (!_stricmp(tok, "none")) { none = 1; continue; }
if (!strcasecmp(tok, "none")) { none = 1; continue; }
int ok = 0;
if (!_strnicmp(tok, "pad:", 4) && action < B_SL_LEFT) {
if (!strncasecmp(tok, "pad:", 4) && action < B_SL_LEFT) {
const char *name = trim(tok + 4);
int p = pad_id(name);
if (!_stricmp(name, "default")) pad_def = ok = 1;
if (!strcasecmp(name, "default")) pad_def = ok = 1;
else if (p && np < INPUT_PAD_BINDING_MAX) pads[np++] = (uint8_t)p, ok = 1;
} else if (!_strnicmp(tok, "key:", 4)) {
} else if (!strncasecmp(tok, "key:", 4)) {
const char *name = trim(tok + 4);
int k = key_id(name);
if (!_stricmp(name, "default")) key_def = ok = 1;
if (!strcasecmp(name, "default")) key_def = ok = 1;
else if (k && nk < INPUT_BINDING_MAX) keys[nk++] = (uint8_t)k, ok = 1;
}
if (!ok) {
@@ -283,8 +283,8 @@ void input_map_line(const InputMap *m, int action, char *out, size_t n) {
}
int input_pad_layout_pick(const char *setting, int east_labelled_a) {
if (setting && (!_stricmp(setting, "nintendo") || !_stricmp(setting, "xbox_prompts"))) return PAD_LAYOUT_NINTENDO;
if (setting && !_stricmp(setting, "xbox")) return PAD_LAYOUT_XBOX;
if (setting && (!strcasecmp(setting, "nintendo") || !strcasecmp(setting, "xbox_prompts"))) return PAD_LAYOUT_NINTENDO;
if (setting && !strcasecmp(setting, "xbox")) return PAD_LAYOUT_XBOX;
return east_labelled_a ? PAD_LAYOUT_NINTENDO : PAD_LAYOUT_XBOX;
}
@@ -338,8 +338,7 @@ void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]) {
// down (the caller keeps it). Every poll of the latch reports neither to the game, so releasing one button first does not
// leak the other; Minus alone before Plus reaches the game unchanged. With the menu open the game sees a neutral pad.
// Returns 1 in the poll where both go down: the caller toggles the menu once.
int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[4]) {
const uint64_t combo = 1ull << B_MINUS | 1ull << B_PLUS;
static int chord(int *held, uint64_t *buttons, uint64_t combo) {
uint64_t raw = *buttons & combo;
int edge = 0;
if (*held) {
@@ -349,9 +348,20 @@ int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[
edge = 1;
}
if (*held) *buttons &= ~combo;
return edge;
}
int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[4]) {
int edge = chord(held, buttons, 1ull << B_MINUS | 1ull << B_PLUS);
if (menu_open) {
*buttons = 0;
sticks[0] = sticks[1] = sticks[2] = sticks[3] = 0;
}
return edge;
}
// Both stick clicks held together step the performance overlay as F10 does (a gamepad has no F10: the Steam Deck in
// Game Mode). The same latch as the menu's above; the game sees neither click while it holds.
int input_overlay_gate(int *held, uint64_t *buttons) {
return chord(held, buttons, 1ull << B_STICK_L | 1ull << B_STICK_R);
}
+224
View File
@@ -0,0 +1,224 @@
// The SDL3 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): SDL scancodes to
// the key ids of input_map.h, the mouse camera's relative motion and the cursor capture (SDL's relative mouse mode).
// Key and button state comes from the window's events (window thread) and is read by any thread. Keys and their map:
// docs/controls.md.
//
// The capture follows the Win32 back end's rules (input_win.c): captured while the window has the focus, the mouse
// camera is on and no text entry or menu is open; Esc releases it until a click in the window (and closing the menu
// opened by that Esc gives it back); losing the focus releases it until the window has the focus again.
#include <string.h>
#include <SDL3/SDL.h>
#include "host.h"
#include "input.h"
#include "input_map.h"
#include "input_sdl.h"
#include "input_wsys.h"
#include "platform/platform.h"
static SDL_Window *win;
static volatile uint8_t key_held[KEY_COUNT];
static volatile int32_t esc_held, f8_held, focused;
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
static volatile int32_t captured; // relative mouse mode is on
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
static volatile int32_t cur_rel, cur_suspend, cur_pending;
static uint32_t wake_event; // SDL_RegisterEvents: wakes the window thread for a cursor change
// SDL scancode of a key id, 0 for an id without a key (the mouse buttons).
static SDL_Scancode key_sc(int key) {
if (key >= KEY_A && key <= KEY_Z) return (SDL_Scancode)(SDL_SCANCODE_A + (key - KEY_A));
if (key >= KEY_1 && key <= KEY_9) return (SDL_Scancode)(SDL_SCANCODE_1 + (key - KEY_1));
if (key >= KEY_NUM1 && key <= KEY_NUM9) return (SDL_Scancode)(SDL_SCANCODE_KP_1 + (key - KEY_NUM1));
switch (key) {
case KEY_0: return SDL_SCANCODE_0;
case KEY_NUM0: return SDL_SCANCODE_KP_0;
case KEY_SPACE: return SDL_SCANCODE_SPACE;
case KEY_TAB: return SDL_SCANCODE_TAB;
case KEY_LSHIFT: return SDL_SCANCODE_LSHIFT;
case KEY_LCTRL: return SDL_SCANCODE_LCTRL;
case KEY_UP: return SDL_SCANCODE_UP;
case KEY_DOWN: return SDL_SCANCODE_DOWN;
case KEY_LEFT: return SDL_SCANCODE_LEFT;
case KEY_RIGHT: return SDL_SCANCODE_RIGHT;
case KEY_ENTER: return SDL_SCANCODE_RETURN;
case KEY_BACKSPACE: return SDL_SCANCODE_BACKSPACE;
case KEY_RSHIFT: return SDL_SCANCODE_RSHIFT;
case KEY_RCTRL: return SDL_SCANCODE_RCTRL;
case KEY_LALT: return SDL_SCANCODE_LALT;
case KEY_CAPSLOCK: return SDL_SCANCODE_CAPSLOCK;
case KEY_INSERT: return SDL_SCANCODE_INSERT;
case KEY_DELETE: return SDL_SCANCODE_DELETE;
case KEY_HOME: return SDL_SCANCODE_HOME;
case KEY_END: return SDL_SCANCODE_END;
case KEY_PAGEUP: return SDL_SCANCODE_PAGEUP;
case KEY_PAGEDOWN: return SDL_SCANCODE_PAGEDOWN;
case KEY_F2: return SDL_SCANCODE_F2;
case KEY_F3: return SDL_SCANCODE_F3;
case KEY_F4: return SDL_SCANCODE_F4;
case KEY_F5: return SDL_SCANCODE_F5;
case KEY_F6: return SDL_SCANCODE_F6;
case KEY_F7: return SDL_SCANCODE_F7;
case KEY_F9: return SDL_SCANCODE_F9;
case KEY_F12: return SDL_SCANCODE_F12;
case KEY_NUM_ADD: return SDL_SCANCODE_KP_PLUS;
case KEY_NUM_SUB: return SDL_SCANCODE_KP_MINUS;
case KEY_NUM_MUL: return SDL_SCANCODE_KP_MULTIPLY;
case KEY_NUM_DIV: return SDL_SCANCODE_KP_DIVIDE;
case KEY_NUM_DECIMAL: return SDL_SCANCODE_KP_PERIOD;
case KEY_MINUS: return SDL_SCANCODE_MINUS;
case KEY_EQUALS: return SDL_SCANCODE_EQUALS;
case KEY_LBRACKET: return SDL_SCANCODE_LEFTBRACKET;
case KEY_RBRACKET: return SDL_SCANCODE_RIGHTBRACKET;
case KEY_SEMICOLON: return SDL_SCANCODE_SEMICOLON;
case KEY_APOSTROPHE: return SDL_SCANCODE_APOSTROPHE;
case KEY_COMMA: return SDL_SCANCODE_COMMA;
case KEY_PERIOD: return SDL_SCANCODE_PERIOD;
case KEY_SLASH: return SDL_SCANCODE_SLASH;
case KEY_BACKSLASH: return SDL_SCANCODE_BACKSLASH;
case KEY_GRAVE: return SDL_SCANCODE_GRAVE;
}
return SDL_SCANCODE_UNKNOWN;
}
// The key id of a scancode (the inverse of key_sc, built on first use), 0 for none.
static int key_of_sc(SDL_Scancode sc) {
static uint8_t map[SDL_SCANCODE_COUNT];
static int built;
if (!built) {
for (int k = 1; k < KEY_COUNT; k++) {
SDL_Scancode s = key_sc(k);
if (s != SDL_SCANCODE_UNKNOWN) map[s] = (uint8_t)k;
}
built = 1;
}
return sc > 0 && sc < SDL_SCANCODE_COUNT ? map[sc] : 0;
}
static int mouse_key(uint8_t button) {
switch (button) {
case SDL_BUTTON_LEFT: return KEY_MOUSE_L;
case SDL_BUTTON_RIGHT: return KEY_MOUSE_R;
case SDL_BUTTON_MIDDLE: return KEY_MOUSE_M;
case SDL_BUTTON_X1: return KEY_MOUSE_X1;
case SDL_BUTTON_X2: return KEY_MOUSE_X2;
}
return 0;
}
uint32_t input_key_char(int key) {
if (key < KEY_MINUS || key > KEY_GRAVE) return 0;
SDL_Keycode k = SDL_GetKeyFromScancode(key_sc(key), SDL_KMOD_NONE, false);
return k & SDLK_SCANCODE_MASK ? 0 : (uint32_t)k; // a keycode with the mask bit names no character
}
int input_flight_key(void) { return focused && f8_held; }
// ---- the cursor (window thread)
static void cursor_apply(void) {
__atomic_store_n(&cur_pending, 0, __ATOMIC_RELAXED);
if (!win) return;
int want = focused && !(cur_rel & CUR_REL) && !cur_suspend && input_cursor_allowed() &&
!(SDL_GetWindowFlags(win) & SDL_WINDOW_MINIMIZED);
if (want == captured) return;
if (!SDL_SetWindowRelativeMouseMode(win, want)) {
rt_log("input: relative mouse mode %s failed (%s)", want ? "on" : "off", SDL_GetError());
return;
}
plat_xchg32(&mouse_dx, 0);
plat_xchg32(&mouse_dy, 0);
rem_x = rem_y = 0;
plat_xchg32(&captured, want);
}
// Any thread: the window thread applies the change at its next event.
void ikb_cursor_update(void) {
plat_xchg32(&cur_pending, 1);
if (wake_event) {
SDL_Event e = {.user = {.type = wake_event}};
SDL_PushEvent(&e);
}
}
void input_cursor_suspend(int on) {
plat_xchg32(&cur_suspend, on != 0);
ikb_cursor_update();
}
int input_cursor_released(void) { return (cur_rel & CUR_REL) != 0; }
void ikb_menu_closed(void) {
int32_t o = cur_rel;
if (o & CUR_ESC) plat_cas32(&cur_rel, 0, o);
}
void input_sdl_window(SDL_Window *w) {
win = w;
wake_event = SDL_RegisterEvents(1);
plat_xchg32(&focused, (SDL_GetWindowFlags(w) & SDL_WINDOW_INPUT_FOCUS) != 0);
cursor_apply();
}
void input_sdl_idle(void) {
if (cur_pending) cursor_apply();
}
void input_sdl_event(const SDL_Event *e) {
switch (e->type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: {
int down = e->type == SDL_EVENT_KEY_DOWN;
if (e->key.scancode == SDL_SCANCODE_ESCAPE) {
plat_xchg32(&esc_held, down);
// Esc is unbound in the game layout: it releases the cursor and opens the menu (ui/menu_sdl.cpp)
if (down && !e->key.repeat && !(cur_rel & CUR_REL)) plat_xchg32(&cur_rel, CUR_REL | CUR_ESC);
}
if (e->key.scancode == SDL_SCANCODE_F8) plat_xchg32(&f8_held, down);
int k = key_of_sc(e->key.scancode);
if (k) key_held[k] = (uint8_t)down;
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP: {
int down = e->type == SDL_EVENT_MOUSE_BUTTON_DOWN;
int k = mouse_key(e->button.button);
if (k) key_held[k] = (uint8_t)down;
if (down && (cur_rel & CUR_REL)) plat_xchg32(&cur_rel, 0); // a click in the window captures again
break;
}
case SDL_EVENT_MOUSE_MOTION:
if (captured) {
rem_x += e->motion.xrel;
rem_y += e->motion.yrel;
int32_t dx = (int32_t)rem_x, dy = (int32_t)rem_y;
rem_x -= (float)dx, rem_y -= (float)dy;
if (dx) plat_add32(&mouse_dx, dx);
if (dy) plat_add32(&mouse_dy, dy);
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
plat_xchg32(&focused, 1);
break;
case SDL_EVENT_WINDOW_FOCUS_LOST:
plat_xchg32(&focused, 0);
plat_xchg32(&cur_rel, 0); // coming back (Alt+Tab) captures again
memset((void *)key_held, 0, sizeof key_held); // the key-ups go elsewhere
plat_xchg32(&esc_held, 0);
plat_xchg32(&f8_held, 0);
break;
}
cursor_apply();
}
// ---- input_wsys.h
int ikb_foreground(void) { return focused != 0; }
int ikb_down(int key) { return key > 0 && key < KEY_COUNT && key_held[key]; }
int ikb_escape(void) { return esc_held != 0; }
int ikb_captured(void) { return captured != 0; }
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*dx = plat_xchg32(&mouse_dx, 0);
*dy = plat_xchg32(&mouse_dy, 0);
}
+25
View File
@@ -0,0 +1,25 @@
// The SDL3 window's input seams (Linux and other hosts without Win32): gfx_window_sdl.c owns the window and its event
// loop, input_sdl.c the keyboard, the mouse and the cursor, ui/menu_sdl.cpp the menu's events. Window thread, except
// where noted.
#pragma once
#include <SDL3/SDL.h>
#ifdef __cplusplus
extern "C" {
#endif
// gfx_window_sdl.c
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
// input_sdl.c
void input_sdl_window(SDL_Window *w); // the game window was created
void input_sdl_event(const SDL_Event *e); // every event, first
void input_sdl_idle(void); // no event for a while: a cursor change asked for from another thread
// ui/menu_sdl.cpp
void menu_sdl_event(const SDL_Event *e); // every event, after input_sdl_event
#ifdef __cplusplus
}
#endif
+26 -665
View File
@@ -1,225 +1,22 @@
// Host input for nn::hid (runtime/ov_input.c): an SDL3 gamepad (no SDL window, no SDL video), the keyboard and the mouse,
// merged into Switch npad button bits and stick axes. Button order follows libnx HidNpadButton (ISC). The key and pad bindings
// are the tables in input_map.c (totk.ini overrides them); this file translates Win32 virtual keys to its key ids, reads the mouse (raw input, WM_INPUT)
// and owns the cursor capture. Keys and their map: docs/controls.md.
// The gamepad also rumbles (input_rumble) and, when it has a gyro and an accelerometer, feeds the six-axis sensor from a
// thread of its own (input_motion_start). Every SDL call on the gamepad is made under `lock`.
// The Win32 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): Win32 virtual
// keys to the key ids of input_map.h, the mouse camera's raw motion (raw input, WM_INPUT) and the cursor capture.
// Keys and their map: docs/controls.md.
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "cfg.h"
#include "host.h"
#include "input.h"
#include "input_win.h"
#include "input_map.h"
#include "input_wsys.h"
#include "nvn_api.h"
#include "ui/menu.h"
#include "vk.h"
enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
static SRWLOCK lock = SRWLOCK_INIT;
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
static SDL_Gamepad *pad;
static uint64_t next_scan;
static int pad_rumble; // the open gamepad has rumble motors
static volatile LONG pad_labels[4]; // ... and the labels of its south, east, west, north buttons (PAD_LABEL_*), any thread
static int pad_east_a; // ... and labels its east face button A (Nintendo layout under TOTK_PAD_LAYOUT=auto)
static int pad_layout = -1; // PAD_LAYOUT_* in effect for the open gamepad, -1 to pick again; live (cfg_version)
static unsigned pad_layout_seen;
static volatile LONG pad_layout_shown = -1; // pad_layout for the menu (any thread), -1 without a gamepad
static SDL_JoystickID sensor_pad; // the gamepad whose sensors the motion thread set up, 0 none
static int sensor_ok; // ... and it has both sensors, enabled
static int motion_thread; // 0 not started, 1 running, -1 off (TOTK_MOTION=0, no SDL)
// SDL buttons of the gamepad inputs (input_map.h PAD_*), by position; PAD_ZL and PAD_ZR are the triggers (axes).
static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
[PAD_SOUTH] = SDL_GAMEPAD_BUTTON_SOUTH, [PAD_EAST] = SDL_GAMEPAD_BUTTON_EAST, [PAD_WEST] = SDL_GAMEPAD_BUTTON_WEST,
[PAD_NORTH] = SDL_GAMEPAD_BUTTON_NORTH, [PAD_LSTICK] = SDL_GAMEPAD_BUTTON_LEFT_STICK,
[PAD_RSTICK] = SDL_GAMEPAD_BUTTON_RIGHT_STICK, [PAD_L] = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
[PAD_LPADDLE1] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, [PAD_RPADDLE1] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
[PAD_LPADDLE2] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, [PAD_RPADDLE2] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
};
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
// <frame> and its hold are 60 Hz ticks of game time since the first present (frame_step_ticks: presents at 60 fps), so
// the title menus get the presses at the same game time at any frame limit. A line "@<ms> ..." is timed instead: <ms>
// and its hold are milliseconds of the world clock (frame_step_world_us, game time since the loaded save's first
// frame), so it lands at the same game time at any frame rate and load length. A timed event shorter than a frame is
// applied once. In a script with timed lines (a recording) the untimed ones belong before the world clock: they stop
// when it starts, as the load ends at another tick in each run.
// at, hold: ticks, or microseconds of the world clock when timed.
typedef struct { uint64_t at, hold; int bit, stick, timed; int32_t x, y; int logged; char text[48]; } ScriptEv;
static const struct { const char *name; int bit; } script_buttons[] = {
{"A", B_A}, {"B", B_B}, {"X", B_X}, {"Y", B_Y}, {"LCLICK", B_STICK_L}, {"RCLICK", B_STICK_R}, {"L", B_L}, {"R", B_R},
{"ZL", B_ZL}, {"ZR", B_ZR}, {"PLUS", B_PLUS}, {"MINUS", B_MINUS}, {"LEFT", B_LEFT}, {"UP", B_UP}, {"RIGHT", B_RIGHT}, {"DOWN", B_DOWN},
};
static ScriptEv *script_ev;
static int script_n, script_state; // 0 not loaded, 1 loaded
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
static void script_load(void) {
script_state = 1;
const char *path = cfg_get("TOTK_INPUT_SCRIPT");
if (!path || !*path) return;
FILE *f = fopen(path, "r");
if (!f) { rt_log("input: cannot open TOTK_INPUT_SCRIPT %s", path); return; }
char line[256];
int cap = 0, ln = 0;
while (fgets(line, sizeof line, f)) {
ln++;
char *h = strchr(line, '#');
if (h) *h = 0;
char name[32];
double at;
int off = 0, timed = line[0] == '@';
if (sscanf(line + timed, "%lf %31s%n", &at, name, &off) < 2 || at < 0) continue;
off += timed;
double unit = timed ? 1000.0 : 1.0; // ms to us, or presents
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
long a, b;
double c;
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
} else {
for (size_t i = 0; i < sizeof script_buttons / sizeof *script_buttons; i++)
if (!strcmp(name, script_buttons[i].name)) e.bit = script_buttons[i].bit;
if (e.bit < 0) { rt_log("input: script line %d: unknown button %s", ln, name); continue; }
if (sscanf(line + off, "%lf", &c) == 1 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
snprintf(e.text, sizeof e.text, "%s", name);
}
if (script_n == cap) {
cap = cap ? cap * 2 : 32;
script_ev = realloc(script_ev, (size_t)cap * sizeof *script_ev);
if (!script_ev) { script_n = 0; fclose(f); return; }
}
script_ev[script_n++] = e;
script_mixed |= timed;
}
fclose(f);
rt_log("input: script %s, %d events", path, script_n);
}
static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
if (!script_state) script_load();
if (!script_n) return;
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), tick = frame_step_ticks();
int64_t w = frame_step_world_us();
for (int i = 0; i < script_n; i++) {
ScriptEv *e = &script_ev[i];
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
uint64_t now = e->timed ? (uint64_t)w : tick;
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
if (!e->logged) {
e->logged = 1;
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
}
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
else *b |= 1ull << e->bit;
}
}
// TOTK_INPUT_RECORD=<file>: writes the merged input as TOTK_INPUT_SCRIPT lines, one per button press and per stick
// segment, so a played session replays as a script. A stick segment ends when an axis moves by more than REC_STICK_STEP.
// An event that begins on the world clock is written timed ("@<ms>", hold in ms), one before it in ticks
// (frame_step_ticks). When the world clock starts, held buttons and sticks are cut there and go on as timed events.
enum { REC_STICK_STEP = 2048 };
static FILE *rec;
static int rec_state; // 0 not opened, 1 recording, -1 off
static uint64_t rec_down[16]; // tick + 1 at which each button went down, 0 while up
static int64_t rec_down_w[16]; // the world clock then, -1 before it started
static uint64_t rec_stick_from[2]; // tick + 1 at which the current non-zero stick segment began
static int64_t rec_stick_w[2];
static int32_t rec_stick[2][2];
static int64_t rec_last_w = -1; // the world clock at the previous apply
// "<tick> <what> <hold>" or "@<ms> <what> <hold ms>": from tick from and world clock from_w to now.
static void rec_line(uint64_t from, int64_t from_w, uint64_t fr, int64_t w, const char *what) {
if (from_w >= 0 && w >= from_w)
fprintf(rec, "@%.1f %s %.1f\n", from_w / 1000.0, what, (w > from_w ? w - from_w : 1000) / 1000.0);
else
fprintf(rec, "%llu %s %llu\n", (unsigned long long)from, what, (unsigned long long)(fr > from ? fr - from : 1));
}
static void rec_close_stick(int s, uint64_t fr, int64_t w) {
if (!rec_stick_from[s]) return;
char what[48];
snprintf(what, sizeof what, "%s %d %d", s ? "RSTICK" : "LSTICK", rec_stick[s][0], rec_stick[s][1]);
rec_line(rec_stick_from[s] - 1, rec_stick_w[s], fr, w, what);
rec_stick_from[s] = 0;
}
static void rec_flush_all(void) {
if (rec_state != 1) return;
uint64_t fr = frame_step_ticks() + 1;
int64_t w = frame_step_world_us();
for (int i = 0; i < 16; i++)
if (rec_down[i]) { rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name); rec_down[i] = 0; }
rec_close_stick(0, fr, w);
rec_close_stick(1, fr, w);
fflush(rec);
}
static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
if (!rec_state) {
const char *path = cfg_get("TOTK_INPUT_RECORD"); // no default: not recording when unset
rec = path && *path ? fopen(path, "w") : NULL;
rec_state = rec ? 1 : -1;
if (path && *path && !rec) rt_log("input: cannot open TOTK_INPUT_RECORD %s", path);
if (rec) { fprintf(rec, "# Recorded input (TOTK_INPUT_RECORD); frame = 60 Hz ticks of game time, @ms = world clock.\n"); atexit(rec_flush_all);
rt_log("input: recording to %s", path); }
}
if (rec_state != 1) return;
uint64_t fr = frame_step_ticks();
int64_t w = frame_step_world_us();
int wrote = 0;
if (w >= 0 && rec_last_w < 0) { // the world clock starts: what is held goes on as timed events from @0
for (int i = 0; i < 16; i++)
if (rec_down[i]) {
rec_line(rec_down[i] - 1, -1, fr, -1, script_buttons[i].name);
rec_down[i] = fr + 1, rec_down_w[i] = w;
wrote = 1;
}
for (int s = 0; s < 2; s++)
if (rec_stick_from[s]) {
rec_close_stick(s, fr, -1);
rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
wrote = 1;
}
}
rec_last_w = w;
for (int i = 0; i < 16; i++) {
int bit = script_buttons[i].bit, down = (b >> bit & 1) != 0;
if (down && !rec_down[i]) rec_down[i] = fr + 1, rec_down_w[i] = w;
else if (!down && rec_down[i]) {
rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name);
rec_down[i] = 0;
wrote = 1;
}
}
const int32_t *st[2] = {sl, sr};
for (int s = 0; s < 2; s++) {
int moved = abs(st[s][0] - rec_stick[s][0]) > REC_STICK_STEP || abs(st[s][1] - rec_stick[s][1]) > REC_STICK_STEP ||
((st[s][0] || st[s][1]) != (rec_stick[s][0] || rec_stick[s][1]));
if (!moved) continue;
if (rec_stick_from[s]) { rec_close_stick(s, fr, w); wrote = 1; }
rec_stick[s][0] = st[s][0]; rec_stick[s][1] = st[s][1];
if (st[s][0] || st[s][1]) rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
}
if (wrote) fflush(rec);
}
// Win32 virtual key of a key id (the one place that knows them), 0 for an id without a key.
static int key_vk(int key) {
if (key >= KEY_A && key <= KEY_Z) return 'A' + (key - KEY_A);
@@ -302,27 +99,12 @@ static LONG rel_clear(LONG o) { (void)o; return 0; }
static LONG rel_menu_closed(LONG o) { return o & CUR_ESC ? 0 : o; }
static volatile LONG cur_suspend; // F11 or a modal size or move loop under way
static SRWLOCK cur_lock = SRWLOCK_INIT; // serialises cursor_update (window thread, input_block callers)
static volatile LONG g_block; // text entry state, see input_block
static volatile LONG g_menu; // in-game menu state, see input_menu
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
static unsigned mouse_sens_seen;
static float mouse_sens_get(void) {
unsigned ver = cfg_version();
if (mouse_sens >= 0 && ver == mouse_sens_seen) return mouse_sens;
mouse_sens_seen = ver;
double v = cfg_double("TOTK_MOUSE_SENS");
mouse_sens = (float)(v < 0 ? 0 : v > 10 ? 10 : v);
return mouse_sens;
}
// Hide the cursor and confine it to the client area while the window is focused, not suspended, not behind a text entry
// or the menu, and the mouse camera is on; release it otherwise. Safe from any thread.
static void cursor_update(HWND h) {
const char *in = cfg_get("TOTK_INPUT");
AcquireSRWLockExclusive(&cur_lock);
int want = h && cur_active && !(cur_rel & CUR_REL) && !cur_suspend && g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 &&
!(in && !strcmp(in, "none")) && GetForegroundWindow() == h && !IsIconic(h);
int want = h && cur_active && !(cur_rel & CUR_REL) && !cur_suspend && input_cursor_allowed() &&
GetForegroundWindow() == h && !IsIconic(h);
RECT r;
POINT tl = {0, 0};
if (want && GetClientRect(h, &r) && ClientToScreen(h, &tl)) {
@@ -344,7 +126,7 @@ void input_cursor_suspend(int on) {
cursor_update(gfx_window_handle());
}
// Window procedure hook (gfx_window.c calls it first). Returns 1 with *res set when the message is fully handled.
// Window procedure hook (gfx_window_win.c calls it first). Returns 1 with *res set when the message is fully handled.
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
switch (m) {
case WM_CREATE: {
@@ -401,452 +183,31 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
static int key_down(int vk) { return (GetAsyncKeyState(vk) & 0x8000) != 0; }
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
static void mouse_camera(int32_t out[2]) {
static uint64_t frame = ~0ull;
static LARGE_INTEGER t0;
static int32_t held[2];
LARGE_INTEGER now, f;
QueryPerformanceCounter(&now);
QueryPerformanceFrequency(&f);
if (!cur_captured) {
InterlockedExchange(&mouse_dx, 0);
InterlockedExchange(&mouse_dy, 0);
held[0] = held[1] = 0;
frame = ~0ull;
} else {
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
double age = frame == ~0ull ? 0 : (double)(now.QuadPart - t0.QuadPart) / (double)f.QuadPart;
if (fr != frame) {
double dt = age < 0.004 ? 0.004 : age > 0.1 ? 0.1 : age;
input_mouse_stick(InterlockedExchange(&mouse_dx, 0), InterlockedExchange(&mouse_dy, 0), dt, mouse_sens_get(), held);
frame = fr;
t0 = now;
} else if (age > 0.1) {
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
}
}
out[0] = held[0];
out[1] = held[1];
}
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
static unsigned stick_dead_seen;
static float stick_dead_get(void) {
unsigned ver = cfg_version();
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
stick_dead_seen = ver;
double v = cfg_double("TOTK_STICK_DEADZONE");
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
return stick_dead;
}
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) {
input_pad_stick(SDL_GetGamepadAxis(pad, ax), -(int32_t)SDL_GetGamepadAxis(pad, ay), dead, out);
}
// Open the first gamepad when none is open or the open one went away; logs once per change.
static void scan_pads(void) {
if (pad && SDL_GamepadConnected(pad)) return;
if (pad) {
SDL_CloseGamepad(pad); pad = NULL; sensor_pad = 0; sensor_ok = 0;
InterlockedExchange(&pad_layout_shown, -1);
for (int i = 0; i < 4; i++) InterlockedExchange(&pad_labels[i], PAD_LABEL_NONE);
rt_log("input: gamepad disconnected");
}
int n = 0;
SDL_JoystickID *ids = SDL_GetGamepads(&n);
for (int i = 0; ids && i < n && !pad; i++) pad = SDL_OpenGamepad(ids[i]);
SDL_free(ids);
if (!pad) return;
pad_rumble = SDL_GetBooleanProperty(SDL_GetGamepadProperties(pad), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false);
pad_east_a = SDL_GetGamepadButtonLabel(pad, SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_A;
for (int i = 0; i < 4; i++) {
SDL_GamepadButtonLabel lb = SDL_GetGamepadButtonLabel(pad, pad_sdl[PAD_SOUTH + i]);
InterlockedExchange(&pad_labels[i], lb == SDL_GAMEPAD_BUTTON_LABEL_A ? PAD_LABEL_A
: lb == SDL_GAMEPAD_BUTTON_LABEL_B ? PAD_LABEL_B
: lb == SDL_GAMEPAD_BUTTON_LABEL_X ? PAD_LABEL_X
: lb == SDL_GAMEPAD_BUTTON_LABEL_Y ? PAD_LABEL_Y
: lb == SDL_GAMEPAD_BUTTON_LABEL_CROSS ? PAD_LABEL_CROSS
: lb == SDL_GAMEPAD_BUTTON_LABEL_CIRCLE ? PAD_LABEL_CIRCLE
: lb == SDL_GAMEPAD_BUTTON_LABEL_SQUARE ? PAD_LABEL_SQUARE
: lb == SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE ? PAD_LABEL_TRIANGLE
: PAD_LABEL_NONE);
}
pad_layout = -1;
int motion = SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL);
rt_log("input: gamepad \"%s\" (%s, %s, east button %s)", SDL_GetGamepadName(pad), pad_rumble ? "rumble" : "no rumble",
motion ? "gyro and accelerometer" : "no motion sensors", pad_east_a ? "labelled A" : "not labelled A");
}
// Under lock: the layout of the open gamepad, picked again when a setting or the gamepad changed; logs each change.
static int pad_layout_get(void) {
unsigned ver = cfg_version();
if (pad_layout >= 0 && ver == pad_layout_seen) return pad_layout;
pad_layout_seen = ver;
int was = pad_layout;
const char *set = cfg_value("TOTK_PAD_LAYOUT");
pad_layout = input_pad_layout_pick(set, pad_east_a);
if (pad_layout != was)
rt_log("input: gamepad layout %s (pad_layout = %s)", pad_layout == PAD_LAYOUT_XBOX ? "xbox" : "nintendo", set);
InterlockedExchange(&pad_layout_shown, pad_layout);
return pad_layout;
}
// The TOTK_BIND_* values last applied, one per line; the tables are built again when one changes (the menu's remapping rows write
// them while the game runs). Under lock.
static unsigned binds_seen = ~0u;
static char binds_applied[4096];
static void binds_refresh(void) {
unsigned v = cfg_version();
if (v == binds_seen) return;
binds_seen = v;
char now[sizeof binds_applied];
size_t len = 0;
for (int a = 0; a < ACTION_COUNT; a++) {
char var[32];
snprintf(var, sizeof var, "TOTK_BIND_%s", input_action_name(a));
const char *s = cfg_get(var);
len += (size_t)snprintf(now + len, len < sizeof now ? sizeof now - len : 0, "%s\n", s ? s : "");
}
if (!strcmp(now, binds_applied)) return;
memcpy(binds_applied, now, sizeof now);
input_map_configure();
}
// TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> (test runs): an SDL virtual gamepad whose sticks rest at these raw SDL axis values
// (-32768..32767, down is +y), attached before the first scan so it is the gamepad the game reads: drift and full
// deflection go through the same mapping and dead zone as a real pad.
static void test_pad_attach(void) {
static SDL_Joystick *joy; // kept open: a virtual joystick's axes are set through an open handle
const char *e = cfg_get("TOTK_TEST_PAD");
int v[4];
if (!e || !*e) return;
if (sscanf(e, "%d,%d,%d,%d", &v[0], &v[1], &v[2], &v[3]) != 4) {
rt_log("input: TOTK_TEST_PAD=%s is not <lx>,<ly>,<rx>,<ry>; no test gamepad", e);
return;
}
SDL_VirtualJoystickDesc d;
SDL_INIT_INTERFACE(&d);
d.type = SDL_JOYSTICK_TYPE_GAMEPAD;
d.naxes = SDL_GAMEPAD_AXIS_COUNT;
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
d.name = "TotK test gamepad";
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
joy = id ? SDL_OpenJoystick(id) : NULL;
if (!joy) {
rt_log("input: test gamepad not attached (%s)", SDL_GetError());
return;
}
static const SDL_GamepadAxis axes[4] = {SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX,
SDL_GAMEPAD_AXIS_RIGHTY};
for (int i = 0; i < 4; i++)
SDL_SetJoystickVirtualAxis(joy, axes[i], (Sint16)(v[i] < -32768 ? -32768 : v[i] > 32767 ? 32767 : v[i]));
rt_log("input: test gamepad attached, sticks at %d,%d %d,%d", v[0], v[1], v[2], v[3]);
}
static void start(void) {
binds_refresh();
const char *e = cfg_get("TOTK_INPUT");
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
else state = 1;
if (state == 1) test_pad_attach();
if (state == 1) {
scan_pads();
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
}
if (state == -1) state = 2; // keyboard only
}
static void stick_bits(uint64_t *b, int32_t x, int32_t y, int first) {
if (x < -STICK_DIR) *b |= 1ull << (first + 0);
if (y > STICK_DIR) *b |= 1ull << (first + 1);
if (x > STICK_DIR) *b |= 1ull << (first + 2);
if (y < -STICK_DIR) *b |= 1ull << (first + 3);
}
static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
int64_t a = (int64_t)*x * *x + (int64_t)*y * *y, b = (int64_t)kx * kx + (int64_t)ky * ky;
if (b > a) { *x = kx; *y = ky; }
}
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
static int pad_physical(uint8_t pd[PAD_COUNT]) {
if ((!pad || !SDL_GamepadConnected(pad)) && GetTickCount64() >= next_scan) { next_scan = GetTickCount64() + RESCAN_MS; scan_pads(); }
SDL_UpdateGamepads();
if (!pad || !SDL_GamepadConnected(pad)) return 0;
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
for (int p = PAD_ZR + 1; p < PAD_COUNT; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]); // false without paddles
return 1;
}
// Under lock, state 1: the open gamepad's buttons (Switch bits after TOTK_PAD_LAYOUT and the TOTK_BIND_* lines) and
// sticks; 0 without a connected gamepad.
static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
if (!pad_physical(pd)) return 0;
*b |= input_map_pad_buttons(pd, pad_layout_get());
float dead = stick_dead_get();
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
return 1;
}
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
AcquireSRWLockExclusive(&lock);
if (!state) start();
if (state > 0) {
binds_refresh();
if (state == 1) pad_read(&b, sl, sr);
HWND w = gfx_window_handle();
if (w && w == GetForegroundWindow()) {
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.
static int was_captured;
static uint8_t mouse_ignore[KEY_COUNT];
int cap = cur_captured != 0;
for (int k = 1; k < KEY_COUNT; k++) {
int vk = key_vk(k);
down[k] = vk && key_down(vk);
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;
}
was_captured = cap;
b |= input_map_buttons(down);
int32_t k[2];
input_map_stick(down, 0, k);
pick(&sl[0], &sl[1], k[0], k[1]);
input_map_stick(down, 1, k);
pick(&sr[0], &sr[1], k[0], k[1]);
mouse_camera(k);
pick(&sr[0], &sr[1], k[0], k[1]);
}
}
script_apply(&b, sl, sr);
rec_apply(b, sl, sr);
// Minus and Plus held together (gamepad or keys) open or close the menu and never reach the game
static int combo_held;
int32_t st[4] = {sl[0], sl[1], sr[0], sr[1]};
int toggle = input_menu_gate(&combo_held, menu_is_open(), &b, st);
sl[0] = st[0], sl[1] = st[1], sr[0] = st[2], sr[1] = st[3];
ReleaseSRWLockExclusive(&lock);
if (toggle) menu_toggle();
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
}
// Text entry (swkbd_win.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
// g_block (above): 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
void input_block(int on) {
InterlockedExchange(&g_block, on ? 1 : 2);
cursor_update(gfx_window_handle());
}
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
// the release. The cursor is released while it is open, for the menu's mouse.
void input_menu(int open) {
InterlockedExchange(&g_menu, open ? 1 : 2);
if (!open) cur_rel_update(rel_menu_closed); // the capture before Esc comes back
cursor_update(gfx_window_handle());
}
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held.
static int blocked(volatile LONG *flag, uint64_t buttons) {
LONG k = *flag;
return k && !(k == 2 && !buttons && InterlockedCompareExchange(flag, 0, 2) == 2);
}
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
poll_merged(buttons, lx, ly, rx, ry);
int text = blocked(&g_block, *buttons), menu = blocked(&g_menu, *buttons);
if (!text && !menu) return;
*buttons = 0; *lx = *ly = *rx = *ry = 0;
}
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
poll_merged(buttons, lx, ly, rx, ry);
}
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
AcquireSRWLockExclusive(&lock);
if (!state) start();
if (state > 0) binds_refresh();
int ok = state == 1 && pad_read(&b, sl, sr);
ReleaseSRWLockExclusive(&lock);
*buttons = b;
*lx = sl[0];
*ly = sl[1];
return ok;
}
uint16_t rumble_motor(float a, float curve, float strength) {
if (!(a > 0) || !(strength > 0)) return 0;
float v = powf(a > 1 ? 1 : a, curve) * strength * 65535.0f + 0.5f;
return v >= 65535.0f ? 65535 : (uint16_t)v;
}
int input_pad_layout(void) {
return (int)InterlockedCompareExchange(&pad_layout_shown, 0, 0);
}
int input_pad_label(int pad_id) {
if (pad_id < PAD_SOUTH || pad_id > PAD_NORTH) return PAD_LABEL_NONE;
return (int)InterlockedCompareExchange(&pad_labels[pad_id - PAD_SOUTH], 0, 0);
}
void input_map_snapshot(InputMap *m) {
AcquireSRWLockExclusive(&lock);
if (!state) start();
if (state > 0) binds_refresh();
*m = input_map;
ReleaseSRWLockExclusive(&lock);
}
int input_capture(int reset, int *id) {
static uint8_t prev_k[KEY_COUNT], prev_p[PAD_COUNT];
static int prev_esc;
uint8_t k[KEY_COUNT] = {0}, p[PAD_COUNT] = {0};
int input_flight_key(void) {
HWND w = gfx_window_handle();
int fg = w && w == GetForegroundWindow();
for (int i = 1; i < KEY_COUNT; i++) k[i] = fg && key_down(key_vk(i));
int esc = fg && key_down(VK_ESCAPE);
AcquireSRWLockExclusive(&lock);
if (!state) start();
if (state == 1) pad_physical(p);
ReleaseSRWLockExclusive(&lock);
int got = INPUT_CAPTURE_NONE;
if (!reset) {
if (esc && !prev_esc) got = INPUT_CAPTURE_CANCEL;
for (int i = 1; i < KEY_COUNT && !got; i++)
if (k[i] && !prev_k[i]) got = INPUT_CAPTURE_KEY, *id = i;
for (int i = 1; i < PAD_COUNT && !got; i++)
if (p[i] && !prev_p[i]) got = INPUT_CAPTURE_PAD, *id = i;
}
memcpy(prev_k, k, sizeof k);
memcpy(prev_p, p, sizeof p);
prev_esc = esc;
return got;
return w && w == GetForegroundWindow() && key_down(VK_F8);
}
int input_rumble_triggers_enabled(void) {
return cfg_bool("TOTK_RUMBLE_TRIGGERS");
// ---- input_wsys.h
int ikb_foreground(void) {
HWND w = gfx_window_handle();
return w && w == GetForegroundWindow();
}
int input_rumble(uint16_t low, uint16_t high, uint32_t ms) {
int ok = 0;
static int triggers = -1;
static unsigned seen;
if (triggers < 0 || seen != cfg_version()) seen = cfg_version(), triggers = input_rumble_triggers_enabled();
AcquireSRWLockExclusive(&lock);
if (state == 1 && pad && pad_rumble) {
ok = SDL_RumbleGamepad(pad, low, high, ms);
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
}
ReleaseSRWLockExclusive(&lock);
return ok;
int ikb_down(int key) {
int vk = key_vk(key);
return vk && key_down(vk);
}
// SDL gamepad sensor axes (SDL_sensor.h, a gamepad held in front of you): x to the right, y up out of the face (lying
// flat face up, +y points at the ceiling), z toward the player (out of the bottom edge). The accelerometer reads the
// push against gravity in m/s^2 (lying flat: y = +9.81); the gyro reads rad/s, counter-clockwise positive (right hand).
// Switch six-axis axes (nn::hid, as MacNativeGames' GCMotion feed and its Pro Controller HID path use them): x to the
// right, y toward the top edge (away from the player), z out of the face; acceleration as gravity reads (lying flat:
// z = -1 G). So Switch x = SDL x, Switch y = -SDL z, Switch z = SDL y: a rotation (right-handed to right-handed), so the
// gyro keeps its sense, while the acceleration is also negated (push -> gravity) and scaled to G.
void input_motion_axes(const float a[3], const float g[3], double acc[3], double gyro[3]) {
acc[0] = -a[0] / SDL_STANDARD_GRAVITY;
acc[1] = a[2] / SDL_STANDARD_GRAVITY;
acc[2] = -a[1] / SDL_STANDARD_GRAVITY;
gyro[0] = g[0];
gyro[1] = -g[2];
gyro[2] = g[1];
int ikb_escape(void) { return key_down(VK_ESCAPE); }
int ikb_captured(void) { return cur_captured != 0; }
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*dx = InterlockedExchange(&mouse_dx, 0);
*dy = InterlockedExchange(&mouse_dy, 0);
}
// Under lock: (re)enable the sensors when the open gamepad changed; 1 if it has both and they are on.
static int sensors_ready(void) {
SDL_JoystickID id = SDL_GetGamepadID(pad);
if (id == sensor_pad) return sensor_ok;
sensor_pad = id;
// Nothing drains the SDL event queue: no event per sample (the data still updates, SDL_SendJoystickSensor)
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
sensor_ok =SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) &&
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_GYRO, true) &&
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true);
if (sensor_ok) rt_log("input: motion sensors on (gyro %.0f Hz, accelerometer %.0f Hz)",
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_GYRO),
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_ACCEL));
else rt_log("input: \"%s\" has no gyro and accelerometer: the six-axis sensor reads a controller at rest",
SDL_GetGamepadName(pad));
return sensor_ok;
}
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
int input_motion_enabled(void) {
static int on = -1;
static unsigned seen;
unsigned v = cfg_version();
if (on < 0 || v != seen) {
seen = v;
on = cfg_bool("TOTK_MOTION");
}
return on;
}
// 250 Hz: the gamepads update (reports read, rumble expiry) and the newest gyro and accelerometer sample goes into the
// fusion. Without a gamepad with sensors, or with motion off (TOTK_MOTION=0, live), nothing is fed and the six-axis
// sensor reads at rest.
static DWORD WINAPI motion_main(void *arg) {
(void)arg;
HANDLE timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
for (;;) {
if (timer) {
LARGE_INTEGER due = {.QuadPart = -40000}; // 4 ms
SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE);
WaitForSingleObject(timer, INFINITE);
} else Sleep(4);
float a[3], g[3];
int have = 0;
AcquireSRWLockExclusive(&lock);
if (pad && SDL_GamepadConnected(pad)) {
SDL_UpdateGamepads();
have = sensors_ready() && SDL_GetGamepadSensorData(pad, SDL_SENSOR_ACCEL, a, 3) &&
SDL_GetGamepadSensorData(pad, SDL_SENSOR_GYRO, g, 3);
}
ReleaseSRWLockExclusive(&lock);
if (!have || !input_motion_enabled()) continue;
double acc[3], gyro[3];
input_motion_axes(a, g, acc, gyro);
motion_feed(motion_global(), acc, gyro, kern_now_ns());
}
return 0;
}
void input_motion_start(void) {
AcquireSRWLockExclusive(&lock);
if (!state) start();
if (!motion_thread) {
if (!input_motion_enabled()) rt_log("input: motion off (TOTK_MOTION=0); it can be turned on while the game runs");
if (state != 1) motion_thread = -1;
else {
HANDLE h = CreateThread(NULL, 0, motion_main, NULL, 0, NULL);
if (!h) rt_fatal("input: motion thread CreateThread failed (error %lu)", GetLastError());
CloseHandle(h);
motion_thread = 1;
}
}
ReleaseSRWLockExclusive(&lock);
}
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }
+9
View File
@@ -0,0 +1,9 @@
// The Win32 window's input seams: gfx_window_win.c's window procedure calls these first. Windows only.
#pragma once
#include <windows.h>
HWND gfx_window_handle(void); // gfx_window_win.c; NULL before gfx_window_start
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
// The menu's Win32 adapter (ui/menu_win.cpp), called next: F1 / Esc open or close the menu; while it is open, keys,
// mouse and focus go to it. Returns 1 with *res set when the message is fully handled.
int menu_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
+16
View File
@@ -0,0 +1,16 @@
// Between input.c (gamepad, merging, scripts) and the keyboard and mouse of the window system's back end (input_win.c
// with Win32, input_sdl.c with SDL). Any thread.
#pragma once
#include <stdint.h>
// ---- what the back end provides
int ikb_foreground(void); // the game window is in front: keys count
int ikb_down(int key); // key id (input_map.h KEY_*) held, mouse buttons included
int ikb_escape(void); // Esc held (no key id: it belongs to the menu)
int ikb_captured(void); // the cursor is captured for the mouse camera
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
// ---- what input.c provides the back end
int input_cursor_allowed(void); // no text entry or menu open, the mouse camera on, input not disabled
+17 -43
View File
@@ -4,7 +4,6 @@
// and the service table. The SDK client code runs unchanged: requests are read from the calling thread's message
// buffer (TLR, or the user buffer of 0x22) and answered in place. Unknown ports, services and commands stop with their
// name and id instead of returning a fake success. Also --probe-ipc.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -12,6 +11,7 @@
#include "audren.h"
#include "host.h"
#include "ipc.h"
#include "platform/platform.h"
#define CMIF_IN_MAGIC 0x49434653u // "SFCI"
#define CMIF_OUT_MAGIC 0x4f434653u // "SFCO"
@@ -27,24 +27,24 @@ static const struct { const char *name; IpcHandler handler; } services[] = {
{"audren:u", audren_manager_handler},
};
static volatile long g_live;
static volatile long long g_released, g_requests;
static volatile int32_t g_live;
static volatile int64_t g_released, g_requests;
static _Thread_local const Cpu *t_req; // guest context of the request being served
long ipc_sessions_live(void) { return InterlockedCompareExchange(&g_live, 0, 0); }
uint64_t ipc_sessions_released(void) { return (uint64_t)InterlockedCompareExchange64(&g_released, 0, 0); }
long ipc_sessions_live(void) { return plat_cas32(&g_live, 0, 0); }
uint64_t ipc_sessions_released(void) { return (uint64_t)plat_cas64(&g_released, 0, 0); }
// ---- sessions
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)) {
IpcSession *s = calloc(1, sizeof *s);
if (!s) rt_fatal("ipc: session allocation");
*s = (IpcSession){name, handler, state, close, 1};
InterlockedIncrement(&g_live);
plat_inc32(&g_live);
return kern_handle_new(KERN_SESSION, s);
}
static uint32_t session_clone(IpcSession *s) {
InterlockedIncrement(&s->refs);
plat_inc32(&s->refs);
return kern_handle_new(KERN_SESSION, s);
}
@@ -55,21 +55,16 @@ void ipc_handle_closed(int kind, void *obj) {
}
if (kind != KERN_SESSION) rt_fatal("ipc_handle_closed: handle kind %d", kind);
IpcSession *s = obj;
if (InterlockedDecrement(&s->refs)) return;
if (plat_dec32(&s->refs)) return;
if (rt_trace & RT_TRACE_IPC) rt_log("ipc: %s closed", s->name);
if (s->close) s->close(s->state);
free(s);
InterlockedDecrement(&g_live);
InterlockedIncrement64(&g_released);
plat_dec32(&g_live);
plat_inc64(&g_released);
}
// ---- helpers for services
static int readable(uint64_t a, uint64_t n) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
return a + n <= (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
}
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
void ipc_backtrace(void) {
const Cpu *c = t_req;
@@ -213,7 +208,7 @@ static void dispatch(IpcSession *s, uint32_t *m) {
unsigned ndata;
parse(m, &rq, &words, &ndata);
if (rq.type == T_CLOSE || rq.type == T_TIPC_CLOSE) return;
InterlockedIncrement64(&g_requests);
plat_inc64(&g_requests);
IpcResponse rs;
memset(&rs, 0, sizeof rs);
int tipc = rq.type >= T_TIPC_BASE;
@@ -655,33 +650,12 @@ static void child_main(Cpu *c) {
// Re-run this executable with the same arguments and PROBE_CHILD_ENV set; it must stop with the named fatal.
static void probe_unknown_service(void) {
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
HANDLE rd, wr;
if (!CreatePipe(&rd, &wr, &sa, 0)) rt_fatal("probe-ipc: CreatePipe failed (error %lu)", GetLastError());
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdInput = GetStdHandle(STD_INPUT_HANDLE),
.hStdOutput = wr, .hStdError = wr};
PROCESS_INFORMATION pi;
wchar_t *cmdline = _wcsdup(GetCommandLineW());
SetEnvironmentVariableA(PROBE_CHILD_ENV, "1");
BOOL ok = CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
SetEnvironmentVariableA(PROBE_CHILD_ENV, NULL);
free(cmdline);
CloseHandle(wr);
if (!ok) rt_fatal("probe-ipc: CreateProcessW failed (error %lu)", GetLastError());
static char out[65536];
DWORD n, len = 0;
while (len < sizeof out - 1 && ReadFile(rd, out + len, (DWORD)(sizeof out - 1 - len), &n, NULL) && n) len += n;
out[len] = 0;
CloseHandle(rd);
WaitForSingleObject(pi.hProcess, 30000);
DWORD code = 0;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
int code = plat_run_self(PROBE_CHILD_ENV, "1", out, sizeof out, 30000);
if (code < 0) rt_fatal("probe-ipc: cannot start the child");
const char *want = "FATAL: sm: service \"" PROBE_UNKNOWN_SERVICE "\" is not implemented";
const char *line = strstr(out, want);
printf("child GetServiceHandle(%s): exit code %lu, %s\n", PROBE_UNKNOWN_SERVICE, (unsigned long)code,
printf("child GetServiceHandle(%s): exit code %d, %s\n", PROBE_UNKNOWN_SERVICE, code,
line ? "named fatal seen" : "named fatal MISSING");
if (line) printf(" child: %.*s\n", (int)strcspn(line, "\r\n"), line);
CHECK(code == 70 && line, "unknown service fatal (child output: %s)", out);
@@ -738,7 +712,7 @@ static void probe_sdk_client(Cpu *c) {
unsigned trace = rt_trace;
rt_trace |= RT_TRACE_IPC; // log the requests the SDK sends during set-up
sdk_prepare(c);
uint64_t requests0 = (uint64_t)InterlockedCompareExchange64(&g_requests, 0, 0);
uint64_t requests0 = (uint64_t)plat_cas64(&g_requests, 0, 0);
uint64_t released0 = ipc_sessions_released();
int32_t *param = mem_kernel_alloc(0x100, 64);
rt_call(c, stub(SYM_INIT_PARAM), 1, (uint64_t)(uintptr_t)param);
@@ -783,7 +757,7 @@ static void probe_sdk_client(Cpu *c) {
"SDK render loop");
rt_call(c, stub(SYM_STOP), 2, handle[0], handle[1]);
rt_call(c, stub(SYM_CLOSE), 2, handle[0], handle[1]);
uint64_t requests = (uint64_t)InterlockedCompareExchange64(&g_requests, 0, 0) - requests0;
uint64_t requests = (uint64_t)plat_cas64(&g_requests, 0, 0) - requests0;
printf("sdk: CloseAudioRenderer: renderers running %ld, sessions released +%llu; %llu requests served for the "
"lifted client (sm: kept open by the SDK: %ld session)\n", audren_renderers_live(),
(unsigned long long)(ipc_sessions_released() - released0), (unsigned long long)requests,
+1 -1
View File
@@ -46,7 +46,7 @@ struct IpcSession {
IpcHandler handler;
void *state;
void (*close)(void *state);
long refs;
int32_t refs;
};
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)); // handle
+198 -148
View File
@@ -8,8 +8,9 @@
// Cpu. The SDK's nn::os runs as lifted code; only the kernel under it is the host. Every wait hangs on one global lock
// G with one condition variable per thread; wake order is the kernel's: lower priority value first, then first come.
// Semantics follow the public Horizon kernel (KConditionVariable, KAddressArbiter, KSynchronization).
#include <windows.h>
#ifdef _WIN32
#define __USE_MINGW_SETJMP_NON_SEH // setjmp(b) = _setjmp(b, NULL): longjmp restores registers, no SEH unwinding
#endif
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
@@ -63,7 +64,7 @@ struct RtThread {
int prio, core;
uint64_t mask;
int started, exited, closed, cancel, is_main;
CONDITION_VARIABLE cv;
PlatCond cv;
// current wait
int wkind, woke;
uint64_t waddr, wkey;
@@ -78,13 +79,12 @@ struct RtThread {
uint8_t *tlr, *tls;
jmp_buf exit_jmp; // the thread start; ExitThread longjmps to it
// fibers
void *base_fiber; // the host thread as a host fiber, after the first switch
PlatFiber *base_fiber; // the host thread as a host fiber, after the first switch
Fiber *fiber; // current guest fiber, NULL on the thread's own context
Fiber *leaving; // fiber just switched away from, released by whoever resumes next
int exit_pending; // ExitThread on a fiber: unwind once back on the thread's own context
HANDLE timer; // high-resolution waitable timer for SleepThread
// TOTK_TRACE=sched accounting (holding G)
DWORD tid; // host thread id
uint32_t tid; // host thread id
int st; // S_* state since st_since
uint64_t st_since, st_ns[S_N], nblk, npre, pre_since, cpu_last;
uint64_t wobj; // what the current wait is on: owner handle, key, address or first handle
@@ -94,7 +94,7 @@ struct RtThread {
uint64_t sp_n, sp_ho, sp_site; // spin wait passes and core handoffs, the last loop head (the thread writes them)
RtThread *target; // as a core waiter: the owner core_kick asked to give its core up for it (holding G)
// TOTK_ASYNC_PREEMPT (holding G)
HANDLE hthr; // the host thread, for SuspendThread / Get/SetThreadContext; open while it may own a core
PlatIntr *hthr; // the host thread, for plat_intr_stop / resume; open while it may own a core
uint64_t ap_rip; // the interrupted host rip kern_ap_stub returns to
uint64_t ap_last; // the last redirect attempt on this owner
int ap_pending; // redirected to kern_ap_stub, which has not reached kern_ap_yield yet
@@ -110,7 +110,7 @@ struct RtThread {
// owner that released
};
static SRWLOCK G = SRWLOCK_INIT;
static PlatMutex G = PLAT_MUTEX_INIT;
static struct { int type; void *obj; } g_h[MAX_H];
static _Thread_local RtThread *tcur;
_Static_assert(offsetof(RtThread, preempt) == 0, "host.h kern_preempt_pending reads the first int");
@@ -119,8 +119,8 @@ static RtThread *g_all, *g_wait, *g_main;
static uint64_t g_next_id = 0x51, g_wseq, g_created;
static uint64_t g_main_stack;
static void lock(void) { AcquireSRWLockExclusive(&G); }
static void unlock(void) { ReleaseSRWLockExclusive(&G); }
static void lock(void) { plat_mutex_lock(&G); }
static void unlock(void) { plat_mutex_unlock(&G); }
uint64_t kern_now_ns(void) { return plat_now_ns(); }
@@ -239,7 +239,7 @@ static void wake(RtThread *t, uint32_t res) {
t->waker = tcur ? tcur->handle : 0;
if (rt_trace & RT_TRACE_SCHED) t->wake_at = kern_now_ns();
if (g_crit_on) crit_add(CRIT_WAKE, t->handle, t->waker, 0, 0, t->wake_at);
WakeConditionVariable(&t->cv);
plat_cond_signal(&t->cv);
}
static void wake_sync_waiters(void) {
@@ -268,7 +268,7 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
uint64_t deadline = forever ? 0 : now + (uint64_t)ns;
while (!t->woke) {
if (forever) {
SleepConditionVariableSRW(&t->cv, &G, INFINITE, 0);
plat_cond_wait(&t->cv, &G);
continue;
}
now = kern_now_ns();
@@ -278,7 +278,7 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
return R_TIMEOUT;
}
uint64_t ms = (deadline - now + 999999) / 1000000;
SleepConditionVariableSRW(&t->cv, &G, ms > 0xFFFFFFF0u ? 0xFFFFFFF0u : (DWORD)ms, 0);
plat_cond_wait_ms(&t->cv, &G, ms > 0xFFFFFFF0u ? 0xFFFFFFF0u : (uint32_t)ms);
}
if (t->wake_at) {
uint64_t d = kern_now_ns() - t->wake_at;
@@ -306,6 +306,9 @@ static void ap_init(void);
static int cores_enabled(void) {
if (g_cores_on < 0) {
g_cores_on = !cfg_bool("TOTK_FREE_CORES"); // "0" keeps the gate, like unset
if (!g_cores_on)
rt_log("kernel: virtual core gate off (TOTK_FREE_CORES): guest threads run without the limit of one per "
"core that the game's per-core data needs; for experiments only, the game can fail");
diag_init();
ap_init();
}
@@ -320,7 +323,7 @@ static int core_allowed(const RtThread *t, int c) { return (int)((t->mask >> c)
// Here a better waiter only marks the owner, which gives the core up at its next SVC or indirect call; an owner in a
// stretch of game code with neither keeps it (TOTK_CORE_DIAG: 0.5-1.1 ms of thread-time per frame, waits to 16 ms and
// more). With it on, three parts, all of which MacNativeGames found necessary:
// - The waiter interrupts the owner (SuspendThread, GetThreadContext) when it has waited AP_FIRST_NS, then every
// - The waiter interrupts the owner (plat_intr_stop) when it has waited AP_FIRST_NS, then every
// millisecond: if the owner's host rip is in the lifted modules' code (g_gen_lo..g_gen_hi) and its host depth is 0
// (not in a host call or SVC, nor in game code a host call made), it holds no host lock, so its rip is moved to
// kern_ap_stub, which saves every volatile register, gives the core up like a checkpoint (kern_ap_yield) and jumps
@@ -375,8 +378,8 @@ static void ap_init(void) {
}
rt_log("kernel: asynchronous preemption of virtual cores %s, priority inheritance and preemption disable %s; lifted "
"code exe+%#llx-exe+%#llx", g_ap_int ? "on" : "off", g_ap ? "on (TOTK_ASYNC_PREEMPT=0 turns all off)" : "off",
(unsigned long long)(g_gen_lo ? g_gen_lo - (uintptr_t)GetModuleHandleW(NULL) : 0),
(unsigned long long)(g_gen_hi ? g_gen_hi - (uintptr_t)GetModuleHandleW(NULL) : 0));
(unsigned long long)(g_gen_lo ? g_gen_lo - plat_exe_base() : 0),
(unsigned long long)(g_gen_hi ? g_gen_hi - plat_exe_base() : 0));
}
// Horizon's priority inheritance: o counts at the priority of the best thread blocked on a guest mutex it owns (the
@@ -403,6 +406,7 @@ static int ap_disabled(const RtThread *t) { return g_ap > 0 && ap_tlr(t)[0]; }
// in kern_ap_yield and jumps back to kern_ap_rip(), whose value it first stores in the return slot it reserved at
// entry, so a stack walk sees the interrupted function as its caller. No ret: the jump keeps call/return pairs
// balanced. The waiter never writes the owner's stack.
#ifdef _WIN32
__asm__(".text\n"
".globl kern_ap_stub\n"
".def kern_ap_stub; .scl 2; .type 32; .endef\n"
@@ -488,11 +492,94 @@ __asm__(".text\n"
" leaq 8(%rsp), %rsp\n"
" jmpq *-8(%rsp)\n"
".seh_endproc\n");
#else
// The System V version: rsi and rdi are volatile there too, and the interrupted code may keep data in the 128 bytes
// below its stack pointer (the red zone), so the return slot and the saves go below that. Without a red zone in the
// way the slot is at entry - 136; the exit restores rsp to the entry value and jumps through the slot.
__asm__(".text\n"
".globl kern_ap_stub\n"
".type kern_ap_stub, @function\n"
".p2align 4\n"
"kern_ap_stub:\n"
" .cfi_startproc\n"
" leaq -136(%rsp), %rsp\n" // not sub: the flags are still the interrupted code's until pushfq
" pushfq\n"
" pushq %rax\n"
" pushq %rcx\n"
" pushq %rdx\n"
" pushq %rsi\n"
" pushq %rdi\n"
" pushq %r8\n"
" pushq %r9\n"
" pushq %r10\n"
" pushq %r11\n"
" pushq %rbp\n"
" movq %rsp, %rbp\n"
" andq $-32, %rsp\n"
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
" vmovdqa %ymm0, 64(%rsp)\n"
" vmovdqa %ymm1, 96(%rsp)\n"
" vmovdqa %ymm2, 128(%rsp)\n"
" vmovdqa %ymm3, 160(%rsp)\n"
" vmovdqa %ymm4, 192(%rsp)\n"
" vmovdqa %ymm5, 224(%rsp)\n"
" vmovdqa %ymm6, 256(%rsp)\n"
" vmovdqa %ymm7, 288(%rsp)\n"
" vmovdqa %ymm8, 320(%rsp)\n"
" vmovdqa %ymm9, 352(%rsp)\n"
" vmovdqa %ymm10, 384(%rsp)\n"
" vmovdqa %ymm11, 416(%rsp)\n"
" vmovdqa %ymm12, 448(%rsp)\n"
" vmovdqa %ymm13, 480(%rsp)\n"
" vmovdqa %ymm14, 512(%rsp)\n"
" vmovdqa %ymm15, 544(%rsp)\n"
" stmxcsr 32(%rsp)\n"
" movl $0x1f80, 36(%rsp)\n"
" ldmxcsr 36(%rsp)\n"
" cld\n"
" vzeroupper\n"
" call kern_ap_rip\n"
" movq %rax, 88(%rbp)\n" // the return slot: rbp + 10 pushes + pushfq
" call kern_ap_yield\n"
" ldmxcsr 32(%rsp)\n"
" vmovdqa 64(%rsp), %ymm0\n"
" vmovdqa 96(%rsp), %ymm1\n"
" vmovdqa 128(%rsp), %ymm2\n"
" vmovdqa 160(%rsp), %ymm3\n"
" vmovdqa 192(%rsp), %ymm4\n"
" vmovdqa 224(%rsp), %ymm5\n"
" vmovdqa 256(%rsp), %ymm6\n"
" vmovdqa 288(%rsp), %ymm7\n"
" vmovdqa 320(%rsp), %ymm8\n"
" vmovdqa 352(%rsp), %ymm9\n"
" vmovdqa 384(%rsp), %ymm10\n"
" vmovdqa 416(%rsp), %ymm11\n"
" vmovdqa 448(%rsp), %ymm12\n"
" vmovdqa 480(%rsp), %ymm13\n"
" vmovdqa 512(%rsp), %ymm14\n"
" vmovdqa 544(%rsp), %ymm15\n"
" movq %rbp, %rsp\n"
" popq %rbp\n"
" popq %r11\n"
" popq %r10\n"
" popq %r9\n"
" popq %r8\n"
" popq %rdi\n"
" popq %rsi\n"
" popq %rdx\n"
" popq %rcx\n"
" popq %rax\n"
" popfq\n"
" leaq 136(%rsp), %rsp\n"
" jmpq *-136(%rsp)\n"
" .cfi_endproc\n"
".size kern_ap_stub, .-kern_ap_stub\n");
#endif
enum { AP_NONE, AP_RETRY, AP_SENT };
// The waiter w interrupts its target owner if that runs top-level game code (holding G; the owner is suspended only
// between SuspendThread and ResumeThread, during which nothing here takes a lock).
// The waiter w interrupts its target owner if that runs top-level game code (holding G; the owner is stopped only
// between plat_intr_stop and plat_intr_resume, during which nothing here takes a lock).
static int ap_try(RtThread *w) {
RtThread *o = w->target;
if (!o || o->cur_core < 0 || !o->preempt) return AP_NONE;
@@ -510,17 +597,14 @@ static int ap_try(RtThread *w) {
uint64_t now = kern_now_ns();
if (o->ap_last && now - o->ap_last < AP_GAP_NS) return AP_RETRY;
o->ap_last = now;
if (SuspendThread(o->hthr) == (DWORD)-1) {
uint64_t rip, rsp;
if (!plat_intr_stop(o->hthr, &rip, &rsp)) {
g_ap_fail++;
return AP_RETRY;
}
CONTEXT ctx;
memset(&ctx, 0, sizeof ctx);
ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; // set back whole, so the kernel restores every GPR
int r = AP_RETRY;
if (!GetThreadContext(o->hthr, &ctx))
g_ap_fail++;
else if (ctx.Rip < g_gen_lo || ctx.Rip > g_gen_hi)
uint64_t to = 0;
if (rip < g_gen_lo || rip > g_gen_hi)
g_ap_pc++;
else if (__atomic_load_n(&o->host_depth, __ATOMIC_RELAXED) > 0)
g_ap_host++;
@@ -528,23 +612,21 @@ static int ap_try(RtThread *w) {
tl[1] = 1;
g_ap_deferred++;
} else {
o->ap_rip = ctx.Rip;
o->ap_rip = rip;
o->ap_pending = 1;
ctx.Rip = (DWORD64)(uintptr_t)kern_ap_stub;
if (SetThreadContext(o->hthr, &ctx)) {
g_ap_sent++;
r = AP_SENT;
} else {
o->ap_pending = 0;
g_ap_fail++;
}
to = (uint64_t)(uintptr_t)kern_ap_stub;
}
if (!plat_intr_resume(o->hthr, to)) {
o->ap_pending = 0;
g_ap_fail++;
} else if (to) {
g_ap_sent++;
r = AP_SENT;
}
ResumeThread(o->hthr);
if (r == AP_SENT && g_ap_log < AP_LOG_N) {
g_ap_log++;
rt_log("kernel: interrupt %#x p%d at exe+%#llx rsp %#llx for %#x p%d", o->handle, o->prio,
(unsigned long long)(o->ap_rip - (uintptr_t)GetModuleHandleW(NULL)), (unsigned long long)ctx.Rsp,
w->handle, w->prio);
(unsigned long long)(o->ap_rip - plat_exe_base()), (unsigned long long)rsp, w->handle, w->prio);
}
return r;
}
@@ -665,30 +747,18 @@ static void dg_done(RtThread *t) {
// its guest lr. Holding G; the owner is resumed before anything that could take a lock it holds.
static void dg_sample(RtThread *t) {
RtThread *o = t->target;
if (!o || o->cur_core < 0 || !o->tid || g_dg_samples >= DG_SAMPLES) return;
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, o->tid);
if (!h) return;
CONTEXT ctx;
memset(&ctx, 0, sizeof ctx);
ctx.ContextFlags = CONTEXT_CONTROL;
int depth = 0, ok = 0;
if (SuspendThread(h) != (DWORD)-1) {
ok = GetThreadContext(h, &ctx);
depth = __atomic_load_n(&o->host_depth, __ATOMIC_RELAXED);
ResumeThread(h);
}
CloseHandle(h);
if (!ok) return;
if (!o || o->cur_core < 0 || !o->hthr || g_dg_samples >= DG_SAMPLES) return;
uint64_t rip, rsp;
if (!plat_intr_stop(o->hthr, &rip, &rsp)) return;
int depth = __atomic_load_n(&o->host_depth, __ATOMIC_RELAXED);
plat_intr_resume(o->hthr, 0);
g_dg_samples++;
HMODULE m = NULL;
wchar_t mw[MAX_PATH] = L"?";
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCWSTR)(uintptr_t)ctx.Rip, &m) && m)
GetModuleFileNameW(m, mw, MAX_PATH);
const wchar_t *mn = wcsrchr(mw, L'\\') ? wcsrchr(mw, L'\\') + 1 : mw;
char mn[256] = "?";
uintptr_t m = 0;
plat_module_of((uintptr_t)rip, mn, sizeof mn, &m);
char lr[128];
rt_log("core-diag: sample owner %#x p%d depth %d rip %ls+%#llx, last svc %#x, guest lr %s (waiter %#x p%d, %s, "
"%.2f ms)", o->handle, o->prio, depth, mn, (unsigned long long)(ctx.Rip - (uintptr_t)m), o->dg_svc,
rt_log("core-diag: sample owner %#x p%d depth %d rip %s+%#llx, last svc %#x, guest lr %s (waiter %#x p%d, %s, "
"%.2f ms)", o->handle, o->prio, depth, mn, (unsigned long long)(rip - m), o->dg_svc,
o->cur ? ldr_describe(o->cur->x[30], lr, sizeof lr) : "-", t->handle, t->prio,
t->dg_cls == DG_HOST ? "host" : "game", (kern_now_ns() - t->dg_since) / 1e6);
}
@@ -788,7 +858,7 @@ static void core_grant(int c) {
core_count();
if (g_diag > 0) b->dg_rel = g_dg_rel;
if (rt_trace & RT_TRACE_SCHED) b->grant_at = kern_now_ns();
WakeConditionVariable(&b->cv);
plat_cond_signal(&b->cv);
}
static void core_put(RtThread *t) {
@@ -897,17 +967,18 @@ static void core_take(RtThread *t) {
if (g_ap_int && t->target && t->cur_core < 0) { // most waits end within microseconds: spin before interrupting
uint64_t end = kern_now_ns() + AP_FIRST_NS;
unlock();
while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) YieldProcessor();
while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) plat_pause();
lock();
if (t->cur_core >= 0) g_ap_spin_ok++;
}
int sampled = 0;
while (t->cur_core < 0) {
DWORD ms = INFINITE;
uint32_t ms = 0; // 0: until signalled
if (g_ap_int && ap_try(t) != AP_NONE) ms = 1; // the owner keeps its core: again in a millisecond
int sample = g_diag > 0 && !sampled && (t->dg_cls == DG_GAME || t->dg_cls == DG_HOST);
if (sample && ms == INFINITE) ms = 2;
SleepConditionVariableSRW(&t->cv, &G, ms, 0);
if (sample && !ms) ms = 2;
if (ms) plat_cond_wait_ms(&t->cv, &G, ms);
else plat_cond_wait(&t->cv, &G);
if (sample && t->cur_core < 0 && kern_now_ns() - t->dg_since >= 2000000) {
sampled = 1;
dg_sample(t);
@@ -943,7 +1014,7 @@ int kern_core_away(void) {
core_put(t);
acct(t, S_HOST);
if (rt_trace & RT_TRACE_SCHED) // the caller as an exe RVA (totk.map)
t->wobj = (uintptr_t)__builtin_return_address(0) - (uintptr_t)GetModuleHandleW(NULL);
t->wobj = (uintptr_t)__builtin_return_address(0) - plat_exe_base();
unlock();
return 1;
}
@@ -975,7 +1046,7 @@ static _Thread_local uint64_t sw_head, sw_t0, sw_last;
void rt_spin_gate(Cpu *c, uint64_t head) {
(void)c;
YieldProcessor();
plat_pause();
RtThread *t = tcur;
if (!t) return;
if (g_spin_on < 0) {
@@ -996,7 +1067,7 @@ void rt_spin_gate(Cpu *c, uint64_t head) {
return;
t->sp_ho++;
int away = kern_core_away();
SwitchToThread();
plat_yield();
kern_core_back(&away);
sw_last = kern_now_ns();
}
@@ -1194,15 +1265,11 @@ void kern_sched_report(uint64_t frame) {
r->sl_n = t->sl_n, r->sl_req = t->sl_req, r->sl_act = t->sl_act;
r->sp_n = t->sp_n, r->sp_ho = t->sp_ho, r->sp_site = t->sp_site;
r->cpu = 0;
HANDLE th = t->tid ? OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, t->tid) : NULL;
FILETIME a, b, k, u;
if (th && GetThreadTimes(th, &a, &b, &k, &u)) {
uint64_t c = (((uint64_t)k.dwHighDateTime << 32 | k.dwLowDateTime) +
((uint64_t)u.dwHighDateTime << 32 | u.dwLowDateTime)) * 100;
uint64_t c = t->tid ? plat_thread_cpu_ns(t->tid) : 0;
if (c) {
r->cpu = t->cpu_last ? c - t->cpu_last : 0;
t->cpu_last = c;
}
if (th) CloseHandle(th);
}
for (int k = 0; k < 2; k++) {
const WaitRec *src = k ? g_mw : g_rw;
@@ -1311,10 +1378,10 @@ static void lock_acquire_for(RtThread *t, uint64_t addr, uint32_t tag) {
// ---- threads
static RtThread *thr_new(int prio, int core, uint64_t mask) {
RtThread *t = calloc(1, sizeof *t);
Cpu *c = _aligned_malloc(sizeof(Cpu), 64);
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
if (!t || !c) rt_fatal("kernel: thread allocation");
memset(c, 0, sizeof *c);
InitializeConditionVariable(&t->cv);
plat_cond_init(&t->cv);
t->prio = prio;
t->core = core;
t->mask = mask;
@@ -1347,22 +1414,20 @@ static void thr_free(RtThread *t) { // holding G; t has exited and its handle
}
mem_kernel_free(t->tlr, TLR_SIZE + THREAD_OBJ_SIZE);
mem_kernel_free(t->tls, TLS_SIZE);
_aligned_free(t->cpu);
plat_aligned_free(t->cpu);
free(t);
}
// The end of every guest thread, on its own host context after the unwind: give the core up, signal the thread
// object, free it if its handle is already closed. t is not touched after the unlock.
static void thread_finish(RtThread *t) {
if (t->timer) CloseHandle(t->timer);
t->timer = NULL;
if (t->base_fiber) ConvertFiberToThread();
if (t->base_fiber) plat_fiber_unself();
t->base_fiber = NULL;
tcur = NULL;
lock();
core_put(t);
acct(t, S_OFF);
if (t->hthr) CloseHandle(t->hthr); // no waiter interrupts a thread without a core
if (t->hthr) plat_intr_free(t->hthr); // no waiter interrupts a thread without a core
t->hthr = NULL;
t->exited = 1;
wake_sync_waiters();
@@ -1375,23 +1440,22 @@ static void thread_finish(RtThread *t) {
// is offset, and the landing code then reads its locals through the wrong base.)
static __attribute__((noreturn)) void exit_now(RtThread *t) { longjmp(t->exit_jmp, 1); }
// The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT only).
static HANDLE ap_open_self(void) {
if (!cores_enabled() || !g_ap_int) return NULL;
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION,
FALSE, GetCurrentThreadId());
if (!h) rt_log("kernel: OpenThread for asynchronous preemption failed (error %lu)", GetLastError());
// The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT) and dg_sample (TOTK_CORE_DIAG).
static PlatIntr *ap_open_self(void) {
if (!cores_enabled() || (!g_ap_int && g_diag <= 0)) return NULL;
PlatIntr *h = plat_intr_self();
if (!h) rt_log("kernel: no interruption handle for this thread: it is not preempted asynchronously");
return h;
}
static DWORD WINAPI thread_main(void *p) {
static void thread_main(void *p) {
RtThread *t = p;
tcur = t;
t->tid = GetCurrentThreadId();
HANDLE self = ap_open_self();
t->tid = plat_thread_id();
PlatIntr *self = ap_open_self();
lock();
t->hthr = self;
while (!t->started) SleepConditionVariableSRW(&t->cv, &G, INFINITE, 0);
while (!t->started) plat_cond_wait(&t->cv, &G);
core_take(t);
unlock();
Cpu *c = t->cpu;
@@ -1403,7 +1467,7 @@ static DWORD WINAPI thread_main(void *p) {
ldr_describe(t->entry, d, sizeof d));
}
thread_finish(tcur);
return 0;
return;
}
uint64_t kern_threads_created(void) {
@@ -1424,17 +1488,16 @@ uint32_t kern_main_thread(Cpu **cpu) {
if (g_main) rt_fatal("kern_main_thread: the main thread already runs on another host thread");
const RtGuestLayout *L = mem_layout();
uint64_t stack = L->stack.base + L->stack.size - MAIN_STACK;
if (!VirtualAlloc((void *)(uintptr_t)stack, MAIN_STACK, MEM_COMMIT, PAGE_READWRITE))
rt_fatal("kernel: commit the main thread stack at %#llx failed (error %lu)", (unsigned long long)stack,
GetLastError());
if (!plat_mem_commit(stack, MAIN_STACK))
rt_fatal("kernel: commit the main thread stack at %#llx failed", (unsigned long long)stack);
RtThread *t = thr_new(MAIN_PRIO, MAIN_CORE, 7);
t->started = 1;
t->is_main = 1;
t->stack_top = stack + MAIN_STACK;
t->cpu->sp = t->stack_top;
t->tid = GetCurrentThreadId();
t->tid = plat_thread_id();
perf_main_thread();
HANDLE self = ap_open_self();
PlatIntr *self = ap_open_self();
lock();
t->hthr = self;
thr_link(t);
@@ -1453,44 +1516,33 @@ void kern_main_stack(uint64_t *base, uint64_t *size) {
*size = b ? MAIN_STACK : 0;
}
static DWORD WINAPI guest_main(void *p) {
static void guest_main(void *p) {
void (*fn)(Cpu *) = (void (*)(Cpu *))p;
Cpu *c;
kern_main_thread(&c);
if (!setjmp(tcur->exit_jmp)) fn(c);
thread_finish(tcur);
return 0;
return;
}
void kern_run_on_guest_main(void (*fn)(Cpu *c)) {
HANDLE h = CreateThread(NULL, HOST_STACK, guest_main, (void *)fn, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
if (!h) rt_fatal("kernel: CreateThread for the guest main thread failed (error %lu)", GetLastError());
WaitForSingleObject(h, INFINITE);
CloseHandle(h);
PlatThread *h = plat_thread_new_ex(guest_main, (void *)fn, "guest main", HOST_STACK);
if (!h) rt_fatal("kernel: cannot start the guest main thread");
plat_thread_join(h);
}
// The high-resolution waitable timer ends a wait 0.3-0.5 ms late at best: the SDK's 20 us polling sleeps (main thread
// and job workers, about 1.5 per frame each) took 460-515 us. Sleeps up to SLEEP_SPIN_NS spin on the performance
// counter instead; the virtual core is already given up. Linux: clock_nanosleep wakes within about 50 us (timer slack),
// so a Linux host spins only below that, or not at all.
#define SLEEP_SPIN_NS 1000000
// The OS timer ends a wait late by up to PLAT_WAIT_LATE_NS (Windows' high-resolution waitable timer: 0.3-0.5 ms; the
// SDK's 20 us polling sleeps, main thread and job workers, about 1.5 per frame each, took 460-515 us). Sleeps up to
// twice that spin on the clock instead (1 ms on Windows, 120 us on Linux); the virtual core is already given up.
#define SLEEP_SPIN_NS (2 * PLAT_WAIT_LATE_NS)
static void sleep_ns(RtThread *t, int64_t ns) {
(void)t;
if (ns <= SLEEP_SPIN_NS) {
uint64_t end = kern_now_ns() + (uint64_t)ns;
while (kern_now_ns() < end) YieldProcessor();
while (kern_now_ns() < end) plat_pause();
return;
}
if (!t->timer) {
t->timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
if (!t->timer) t->timer = CreateWaitableTimerExW(NULL, NULL, 0, TIMER_ALL_ACCESS);
if (!t->timer) rt_fatal("kernel: CreateWaitableTimerExW failed (error %lu)", GetLastError());
}
LARGE_INTEGER due;
int64_t units = ns / 100 + (ns % 100 != 0); // 100 ns units, rounded up
due.QuadPart = -(units > 0 ? units : 1);
if (!SetWaitableTimer(t->timer, &due, 0, NULL, NULL, FALSE))
rt_fatal("kernel: SetWaitableTimer failed (error %lu)", GetLastError());
WaitForSingleObject(t->timer, INFINITE);
plat_wait_ns((uint64_t)ns);
}
// ---- synchronization objects
@@ -1579,9 +1631,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
lock();
thr_link(t);
unlock();
HANDLE h = CreateThread(NULL, HOST_STACK, thread_main, t, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
if (!h) rt_fatal("kernel: CreateThread failed (error %lu)", GetLastError());
CloseHandle(h);
if (!plat_thread_spawn(thread_main, t, "guest", HOST_STACK)) rt_fatal("kernel: cannot start a guest thread");
if (rt_trace & RT_TRACE_SVC) {
char d[128];
rt_log("CreateThread %#x entry %s stack %#llx prio %d core %d", t->handle,
@@ -1596,7 +1646,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
RtThread *t = h_get((uint32_t)x[0], O_THREAD);
if (t) {
t->started = 1;
WakeConditionVariable(&t->cv);
plat_cond_signal(&t->cv);
}
unlock();
x[0] = t ? R_OK : R_BAD_HANDLE;
@@ -1609,7 +1659,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
t->leaving = t->fiber;
t->fiber = NULL;
t->cur = t->cpu;
SwitchToFiber(t->base_fiber);
plat_fiber_switch(t->base_fiber);
rt_fatal("kernel: ExitThread on a fiber came back");
}
exit_now(t);
@@ -1646,7 +1696,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
core_take(t);
}
unlock();
if (!give) SwitchToThread();
if (!give) plat_yield();
return 1;
}
case 0x0C: { // GetThreadPriority(w1 handle -> w1)
@@ -1999,14 +2049,14 @@ void kern_event_signal(uint32_t hw) {
// guest thread; the writable handle is kept, so any thread (host audio tick included) can signal it later.
static struct { uint64_t ev; uint32_t wh; } g_se[512];
static int g_nse;
static SRWLOCK g_se_lock = SRWLOCK_INIT;
static PlatMutex g_se_lock = PLAT_MUTEX_INIT;
static uint32_t se_find(uint64_t ev) {
AcquireSRWLockShared(&g_se_lock);
plat_mutex_lock_shared(&g_se_lock);
uint32_t wh = 0;
for (int i = 0; i < g_nse && !wh; i++)
if (g_se[i].ev == ev) wh = g_se[i].wh;
ReleaseSRWLockShared(&g_se_lock);
plat_mutex_unlock_shared(&g_se_lock);
return wh;
}
@@ -2022,11 +2072,11 @@ void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
uint32_t r = (uint32_t)rt_call(c, create, 3, ev, (uint64_t)(autoclear ? 1 : 0), (uint64_t)1);
if (r) rt_fatal("kern_bind_system_event: CreateSystemEvent(%#llx) failed %#x", (unsigned long long)ev, r);
uint32_t wh = (uint32_t)rt_call(c, getw, 1, ev);
AcquireSRWLockExclusive(&g_se_lock);
plat_mutex_lock(&g_se_lock);
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
g_se[g_nse].ev = ev;
g_se[g_nse++].wh = wh;
ReleaseSRWLockExclusive(&g_se_lock);
plat_mutex_unlock(&g_se_lock);
}
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread.
@@ -2048,19 +2098,19 @@ enum { F_NEW, F_STARTED, F_DONE };
struct Fiber {
uint64_t addr, func, arg, stack, size;
Cpu *cpu;
void *host;
PlatFiber *host;
int state;
volatile int running;
};
static Fiber *g_fibers[FIBER_MAX];
static SRWLOCK g_fiber_lock = SRWLOCK_INIT;
static PlatMutex g_fiber_lock = PLAT_MUTEX_INIT;
static Fiber *fiber_find(uint64_t addr) {
Fiber *f = NULL;
AcquireSRWLockShared(&g_fiber_lock);
plat_mutex_lock_shared(&g_fiber_lock);
for (int i = 0; i < FIBER_MAX && !f; i++)
if (g_fibers[i] && g_fibers[i]->addr == addr) f = g_fibers[i];
ReleaseSRWLockShared(&g_fiber_lock);
plat_mutex_unlock_shared(&g_fiber_lock);
return f;
}
@@ -2069,7 +2119,7 @@ static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to);
// After a host fiber switch: release the context left behind, and finish an ExitThread issued on a fiber.
static void fiber_resumed(void) {
RtThread *t = tcur;
rt_fpcr_sync(t->cur); // the host fiber resumes with its own MXCSR (FIBER_FLAG_FLOAT_SWITCH)
rt_fpcr_sync(t->cur); // the host fiber resumes with its own MXCSR (platform.h fibers switch it)
if (t->leaving) {
__atomic_store_n(&t->leaving->running, 0, __ATOMIC_RELEASE);
t->leaving = NULL;
@@ -2077,7 +2127,7 @@ static void fiber_resumed(void) {
if (t->exit_pending && !t->fiber) exit_now(t);
}
static void WINAPI fiber_main(void *p) {
static void fiber_main(void *p) {
Fiber *f = p;
fiber_resumed();
Cpu *c = f->cpu;
@@ -2094,8 +2144,8 @@ static void WINAPI fiber_main(void *p) {
static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to) {
if (!t->base_fiber) {
t->base_fiber = ConvertThreadToFiberEx(NULL, FIBER_FLAG_FLOAT_SWITCH);
if (!t->base_fiber) rt_fatal("kernel: ConvertThreadToFiberEx failed (error %lu)", GetLastError());
t->base_fiber = plat_fiber_self();
if (!t->base_fiber) rt_fatal("kernel: cannot make the thread a fiber");
}
Cpu *dst = to ? to->cpu : t->cpu;
if (to) {
@@ -2118,7 +2168,7 @@ static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to) {
t->fiber = to;
t->cur = dst;
int depth = t->host_depth;
SwitchToFiber(to ? to->host : t->base_fiber);
plat_fiber_switch(to ? to->host : t->base_fiber);
t->host_depth = depth;
fiber_resumed();
}
@@ -2130,17 +2180,17 @@ void kern_fiber_initialize(Cpu *c) {
if (f && f->running) rt_fatal("InitializeFiber(%#llx): the fiber is running", (unsigned long long)addr);
if (!f) {
f = calloc(1, sizeof *f);
Cpu *fc = _aligned_malloc(sizeof(Cpu), 64);
Cpu *fc = plat_aligned_alloc(sizeof(Cpu), 64);
if (!f || !fc) rt_fatal("kernel: fiber allocation");
f->cpu = fc;
AcquireSRWLockExclusive(&g_fiber_lock);
plat_mutex_lock(&g_fiber_lock);
int i = 0;
while (i < FIBER_MAX && g_fibers[i]) i++;
if (i == FIBER_MAX) rt_fatal("kernel: more than %d fibers", FIBER_MAX);
g_fibers[i] = f;
ReleaseSRWLockExclusive(&g_fiber_lock);
plat_mutex_unlock(&g_fiber_lock);
} else if (f->host) {
DeleteFiber(f->host);
plat_fiber_free(f->host);
}
memset(f->cpu, 0, sizeof *f->cpu);
f->cpu->excl_addr = ~0ull;
@@ -2151,25 +2201,25 @@ void kern_fiber_initialize(Cpu *c) {
f->size = c->x[4];
f->state = F_NEW;
f->running = 0;
f->host = CreateFiberEx(0x10000, HOST_STACK, FIBER_FLAG_FLOAT_SWITCH, fiber_main, f);
if (!f->host) rt_fatal("kernel: CreateFiberEx failed (error %lu)", GetLastError());
f->host = plat_fiber_new(HOST_STACK, fiber_main, f);
if (!f->host) rt_fatal("kernel: cannot create a host fiber");
}
// FinalizeFiber(x0 FiberType*)
void kern_fiber_finalize(Cpu *c) {
uint64_t addr = c->x[0];
AcquireSRWLockExclusive(&g_fiber_lock);
plat_mutex_lock(&g_fiber_lock);
Fiber *f = NULL;
for (int i = 0; i < FIBER_MAX && !f; i++)
if (g_fibers[i] && g_fibers[i]->addr == addr) {
f = g_fibers[i];
g_fibers[i] = NULL;
}
ReleaseSRWLockExclusive(&g_fiber_lock);
plat_mutex_unlock(&g_fiber_lock);
if (!f) rt_fatal("FinalizeFiber(%#llx): not an initialized fiber", (unsigned long long)addr);
if (f->running) rt_fatal("FinalizeFiber(%#llx): the fiber is running", (unsigned long long)addr);
DeleteFiber(f->host);
_aligned_free(f->cpu);
plat_fiber_free(f->host);
plat_aligned_free(f->cpu);
free(f);
}
+15 -16
View File
@@ -3,7 +3,6 @@
// PLT stub for nn::os::GetSystemTick, through the lifted sdk function, to rt_ticks.
//
// Every probe thread is created with SVC 0x08 and drives the kernel through rt_svc, the entry the lifted code uses.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -111,10 +110,10 @@ static void user_unlock(Cpu *c, volatile uint32_t *a, uint32_t tag) {
// ---- lock handoff order
typedef struct { volatile uint32_t *lock, *key; uint32_t tag, r; int idx, held; } LockJob;
static volatile uint32_t g_lockw __attribute__((aligned(8))), g_keyw __attribute__((aligned(8)));
static volatile long g_order_n;
static volatile int32_t g_order_n;
static int g_order[8];
static void record(int idx) { g_order[InterlockedIncrement(&g_order_n) - 1] = idx; }
static void record(int idx) { g_order[plat_inc32(&g_order_n) - 1] = idx; }
static void lock_waiter(Cpu *c) {
LockJob *j = arg_p(c, 0);
@@ -388,18 +387,18 @@ static void test_events(Cpu *c) {
}
// ---- virtual cores: count the threads running guest code at once, and that no two of them share a core number
static volatile long g_running, g_run_max, g_core_use[3], g_core_clash;
static volatile int32_t g_running, g_run_max, g_core_use[3], g_core_clash;
static void core_worker(Cpu *c) {
for (int i = 0; i < CORE_RUNS; i++) {
uint32_t core = sv(c, 0x10, 0, 0, 0, 0);
if (core > 2 || InterlockedIncrement(&g_core_use[core > 2 ? 0 : core]) != 1) InterlockedIncrement(&g_core_clash);
long n = InterlockedIncrement(&g_running), m;
while (n > (m = g_run_max) && InterlockedCompareExchange(&g_run_max, n, m) != m) {}
if (core > 2 || plat_inc32(&g_core_use[core > 2 ? 0 : core]) != 1) plat_inc32(&g_core_clash);
long n = plat_inc32(&g_running), m;
while (n > (m = g_run_max) && plat_cas32(&g_run_max, n, m) != m) {}
uint64_t until = kern_now_ns() + 20000;
while (kern_now_ns() < until) {}
InterlockedDecrement(&g_running);
InterlockedDecrement(&g_core_use[core > 2 ? 0 : core]);
plat_dec32(&g_running);
plat_dec32(&g_core_use[core > 2 ? 0 : core]);
sv(c, 0x0B, i % 8 == 7 ? 50000 : 0, 0, 0, 0);
}
exit_thread(c);
@@ -416,18 +415,18 @@ static void test_cores(Cpu *c) {
for (int i = 0; i < CORE_WORKERS; i++) join(c, h[i]);
int now, held_max;
kern_core_stats(&now, &held_max);
DWORD cpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
uint32_t cpus = plat_cpu_usable();
int gate = !cfg_bool("TOTK_FREE_CORES"); // the kernel's own switch
int ok = g_run_max <= 3 && held_max <= 3 && !g_core_clash && (cpus < 4 || g_run_max == 3);
printf("virtual cores: %d threads x %d runs on %lu host CPUs: at most %ld running guest code at once, at most %d "
"core holders (kernel count), %ld core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
printf("virtual cores: %d threads x %d runs on %u host CPUs: at most %d running guest code at once, at most %d "
"core holders (kernel count), %d core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
held_max, g_core_clash, !gate ? "not checked (TOTK_FREE_CORES is set)" : ok ? "ok" : "FAIL");
CHECK(ok || !gate, "virtual cores");
}
// ---- fibers: two fibers switch to each other; each checks its callee-saved registers, sp and tpidr survive
typedef struct { uint64_t self, other; int id; } FiberJob;
static volatile long g_switches, g_runs[3];
static volatile int32_t g_switches, g_runs[3];
static volatile int g_last_fiber;
static long g_fiber_bad, g_fiber_turns;
@@ -487,7 +486,7 @@ static void test_fibers(Cpu *c) {
rt_call(c, a_fib_fin, 1, fb);
int ok = g_switches == FIBER_SWITCHES && g_runs[1] == FIBER_SWITCHES / 2 && g_runs[2] == FIBER_SWITCHES / 2 &&
!g_fiber_turns && !g_fiber_bad && base_ok;
printf("fibers: %ld switches (fiber 1 %ld, fiber 2 %ld, %ld out of turn) in %.1f ms (%.3f us each), %ld register "
printf("fibers: %d switches (fiber 1 %d, fiber 2 %d, %ld out of turn) in %.1f ms (%.3f us each), %ld register "
"mismatches, thread context %s: %s\n", g_switches, g_runs[1], g_runs[2], g_fiber_turns, ms,
ms * 1000.0 / (double)(g_switches ? g_switches : 1), g_fiber_bad, base_ok ? "preserved" : "CHANGED",
ok ? "ok" : "FAIL");
@@ -593,14 +592,14 @@ static void test_lifted_ticks(Cpu *c) {
// svc 0x07 ExitProcess: the path up to the final ExitProcess (logged, pipeline cache saved, logs flushed) runs with the
// test hook in its place, which must see exit code 0; the SVC then returns to the probe.
static volatile long exit_calls;
static volatile int32_t exit_calls;
static volatile unsigned exit_code = 99;
static void exit_hook(unsigned code) { exit_code = code, exit_calls++; }
static void test_exit_process(Cpu *c) {
svc_test_exit_hook = exit_hook;
sv(c, 0x07, 0, 0, 0, 0);
svc_test_exit_hook = NULL;
CHECK(exit_calls == 1 && exit_code == 0, "ExitProcess reached the exit %ld times with code %u", exit_calls, exit_code);
CHECK(exit_calls == 1 && exit_code == 0, "ExitProcess reached the exit %d times with code %u", exit_calls, exit_code);
printf("ExitProcess: exit path reached once with code %u\n", exit_code);
}
+26 -35
View File
@@ -6,13 +6,13 @@
//
// The lifted C was generated from the unrelocated words of .text and .rodata and embeds absolute guest addresses, so
// each module must sit at its layout base and relocations may only write the data segment.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "host.h"
#include "layout.h"
#include "platform/platform.h"
// hostfn.c, for the loader only.
uint64_t hostfn_register_observer(const char *name, GuestFn fn, uint64_t original);
@@ -55,7 +55,7 @@ static Mod mods[MOD_COUNT];
static int nmods;
static uint64_t identity_refs, stubs;
static int *ovr_used, *obs_used; // per linked table entry, plus probe overrides counted separately
static SRWLOCK plt_lock = SRWLOCK_INIT;
static PlatMutex plt_lock = PLAT_MUTEX_INIT;
int ldr_count(void) { return nmods; }
const RtLoaded *ldr_module(int i) { return i >= 0 && i < nmods ? &mods[i].pub : NULL; }
@@ -64,21 +64,9 @@ static uint32_t rd32(uint64_t a) { return LD32(a); }
// ---- files
static uint8_t *read_file(const char *path, size_t *len) {
HANDLE h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return NULL;
LARGE_INTEGER size;
uint8_t *p = NULL;
if (GetFileSizeEx(h, &size) && size.QuadPart < (1ll << 31)) {
p = malloc(size.QuadPart ? (size_t)size.QuadPart : 1);
DWORD got = 0;
if (p && ReadFile(h, p, (DWORD)size.QuadPart, &got, NULL) && got == (DWORD)size.QuadPart) {
*len = (size_t)size.QuadPart;
} else {
free(p);
p = NULL;
}
}
CloseHandle(h);
uint64_t size = 0;
uint8_t *p = plat_file_read(path, &size);
if (p) *len = (size_t)size;
return p;
}
@@ -293,9 +281,8 @@ static void load_module(Mod *m, const char *dir, const RtLayoutEntry *lay) {
if (size != lay->size)
rt_fatal("loader: %s image is %#llx bytes, layout.h says %#llx: generated C and game files differ", lay->name,
(unsigned long long)size, (unsigned long long)lay->size);
if (!VirtualAlloc((void *)(uintptr_t)lay->base, size, MEM_COMMIT, PAGE_READWRITE))
rt_fatal("loader: cannot commit %s at %#llx (error %lu)", lay->name, (unsigned long long)lay->base,
GetLastError());
if (!plat_mem_commit(lay->base, size))
rt_fatal("loader: cannot commit %s at %#llx", lay->name, (unsigned long long)lay->base);
for (int i = 0; i < 3; i++) {
if ((uint64_t)seg[i].mem_off + seg[i].size > size) rt_fatal("loader: %s segment %d exceeds the image", lay->name, i);
nso_segment(raw, &seg[i], (uint8_t *)(uintptr_t)(lay->base + seg[i].mem_off), lay->name, i);
@@ -450,9 +437,8 @@ static void verify_identity(void) {
// .text and .rodata read-only: guest code never executes from guest memory, and nothing may write them.
static void seal(Mod *m) {
DWORD old;
if (!VirtualProtect((void *)(uintptr_t)m->pub.base, m->pub.data_off, PAGE_READONLY, &old))
rt_fatal("loader: cannot seal %s (error %lu)", m->pub.name, GetLastError());
if (!plat_mem_protect(m->pub.base, m->pub.data_off, PLAT_PROT_READ))
rt_fatal("loader: cannot seal %s", m->pub.name);
}
void ldr_load(const char *exefs_dir) {
@@ -547,9 +533,9 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
for (int i = 0; i < nmods; i++)
if (!strcmp(mods[i].pub.name, module)) m = &mods[i];
if (!m) return 0;
AcquireSRWLockExclusive(&plt_lock);
plat_mutex_lock(&plt_lock);
if (!m->plt_slot) find_plt(m);
ReleaseSRWLockExclusive(&plt_lock);
plat_mutex_unlock(&plt_lock);
for (uint64_t j = 0; j < m->njmprel; j++) {
uint32_t si = (uint32_t)(m->jmprel[j].info >> 32);
if ((uint32_t)m->jmprel[j].info != R_JUMP_SLOT || strcmp(sym_name(m, si), name)) continue;
@@ -566,8 +552,8 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
}
// ---- --probe-loader
// Expected values, measured on the 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
// MacNativeGames' port notes give for that version.
// Expected values, measured on the 1.1.1, 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
// MacNativeGames' port notes give for that version. 1.1.1 shares subsdk0 and sdk with 1.2.1 down to every count.
typedef struct {
const char *name;
uint64_t kinds[K_COUNT], total, sym_import, sym_own;
@@ -578,22 +564,27 @@ static const Expect expect_121[] = {
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_111[] = {
{"main", {508604, 1412, 32, 639}, 510687, 2083, 0, 673, {0, 16, 645}, 12, 0x30, 10668},
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_143[] = {
{"main", {499078, 1393, 28, 650}, 501149, 2071, 0, 680, {0, 16, 656}, 8, 0x10b4f10, 10717},
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 6557, 9092, 663, {9, 0, 654}, 0, 0x20, 45},
{"sdk", {15013, 10135, 1079, 7832}, 34059, 10, 19036, 8, {8, 0, 0}, 0, 0x400, 83},
};
#define expect (RT_V143 ? expect_143 : expect_121)
#define expect (RT_V143 ? expect_143 : RT_V111 ? expect_111 : expect_121)
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
#define PROBE_SYMBOL "_ZN2nn2os13GetSystemTickEv"
#define PROBE_MAGIC 0x5eed0123456789abull
static volatile long probe_hits;
static volatile int32_t probe_hits;
static void probe_override(Cpu *c) {
InterlockedIncrement(&probe_hits);
plat_inc32(&probe_hits);
c->x[0] = PROBE_MAGIC;
}
@@ -614,9 +605,9 @@ static void check_segments(const char *dir) {
free(buf);
}
free(raw);
MEMORY_BASIC_INFORMATION mbi;
int ro = VirtualQuery((void *)(uintptr_t)m->pub.base, &mbi, sizeof mbi) && mbi.Protect == PAGE_READONLY &&
mbi.RegionSize >= m->pub.data_off;
uint64_t ro_end;
int ro = plat_mem_state(m->pub.base, m->pub.base + m->pub.data_off, &ro_end) == PLAT_PROT_READ &&
ro_end == m->pub.base + m->pub.data_off;
printf(" %-8s text %#x+%#x rodata %#x+%#x: bytes %s the NSO, %s\n", m->pub.name, seg[0].mem_off, seg[0].size,
seg[1].mem_off, seg[1].size, same ? "equal" : "DIFFER FROM", ro ? "read-only" : "NOT READ-ONLY");
CHECK(same, "%s .text/.rodata changed after relocation", m->pub.name);
@@ -650,7 +641,7 @@ static void check_got(void) {
CHECK(stub, "no PLT stub in main for %s", PROBE_SYMBOL);
hostfn_seal();
Cpu *c = _aligned_malloc(sizeof(Cpu), 64);
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
memset(c, 0, sizeof *c);
uint8_t *stack = mem_kernel_alloc(0x10000, 16);
c->sp = (uint64_t)(uintptr_t)stack + 0x10000;
@@ -666,7 +657,7 @@ static void check_got(void) {
CHECK(r2 == PROBE_MAGIC && h2 == 2, "the lifted PLT stub did not reach the host function");
CHECK(c->x[30] == 0 && c->pc == 0 && c->x[19] == 0x1919, "rt_call did not restore x30/pc or lost x19");
mem_kernel_free(stack, 0x10000);
_aligned_free(c);
plat_aligned_free(c);
}
int probe_loader(void) {
+60 -102
View File
@@ -18,11 +18,7 @@
// Also here: the runtime services the generated code calls besides rt_dispatch and rt_svc (rt_brk, rt_udf,
// rt_ticks, the function profiler hook), logging, SHA-256, and the crash reporter (a vectored exception handler that
// names the lifted function, the guest registers and the guest frame-pointer chain).
#include <windows.h>
#include <bcrypt.h>
#include <limits.h>
#include <psapi.h>
#include <timeapi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -30,6 +26,7 @@
#include "cfg.h"
#include "host.h"
#include "platform/platform.h"
#include "sha256.h"
#define MAX_MODULES 16
@@ -46,7 +43,7 @@ unsigned rt_trace;
enum { LOG_EARLY, LOG_FILE, LOG_OFF };
static int g_log_state;
static PlatAppend *g_log_file;
static char g_log_path[MAX_PATH * 3];
static char g_log_path[PLAT_PATH_MAX * 3];
static char g_log_early[32768];
static size_t g_log_early_n;
static uint64_t g_log_bytes;
@@ -100,23 +97,23 @@ const char *rt_log_path(void) {
// Opens the log file for a game run (game = 1) and writes the buffered lines to it; anything else drops them. Runs on
// the main thread before the boot starts other threads.
static void log_open(int game) {
char dir[MAX_PATH * 3], from[MAX_PATH * 3 + 16], to[MAX_PATH * 3 + 16];
char dir[PLAT_PATH_MAX * 3], from[PLAT_PATH_MAX * 3 + 16], to[PLAT_PATH_MAX * 3 + 16];
char *cut = NULL;
if (game && !plat_stderr_path(dir, sizeof dir) && plat_exe_path(dir, sizeof dir)) {
cut = strrchr(dir, '\\');
cut = plat_last_sep(dir);
char *f = strrchr(dir, '/');
if (f > cut) cut = f;
}
if (cut && (size_t)(cut - dir) + sizeof "\\logs" <= sizeof dir) {
strcpy(cut, "\\logs");
if (cut && (size_t)(cut - dir) + sizeof "/logs" <= sizeof dir) {
strcpy(cut, "/logs");
plat_file_mkdir_p(dir);
for (int i = LOG_KEEP - 1; i > 0; i--) { // totk.3.log -> totk.4.log, ..., totk.log -> totk.1.log
if (i > 1) snprintf(from, sizeof from, "%s\\totk.%d.log", dir, i - 1);
else snprintf(from, sizeof from, "%s\\totk.log", dir);
snprintf(to, sizeof to, "%s\\totk.%d.log", dir, i);
if (i > 1) snprintf(from, sizeof from, "%s/totk.%d.log", dir, i - 1);
else snprintf(from, sizeof from, "%s/totk.log", dir);
snprintf(to, sizeof to, "%s/totk.%d.log", dir, i);
if (plat_file_exists(from)) plat_file_replace(from, to);
}
snprintf(g_log_path, sizeof g_log_path, "%s\\totk.log", dir);
snprintf(g_log_path, sizeof g_log_path, "%s/totk.log", dir);
g_log_file = plat_append_open(g_log_path);
}
if (!g_log_file) {
@@ -148,29 +145,7 @@ static void parse_trace(void) {
}
}
void rt_sha256(const void *p, size_t n, uint8_t out[32]) {
static BCRYPT_ALG_HANDLE alg;
BCRYPT_ALG_HANDLE a = __atomic_load_n(&alg, __ATOMIC_ACQUIRE);
if (!a) {
if (BCryptOpenAlgorithmProvider(&a, BCRYPT_SHA256_ALGORITHM, NULL, 0)) rt_fatal("SHA-256 provider unavailable");
BCRYPT_ALG_HANDLE expected = NULL;
if (!__atomic_compare_exchange_n(&alg, &expected, a, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
BCryptCloseAlgorithmProvider(a, 0);
a = expected;
}
}
BCRYPT_HASH_HANDLE h;
if (BCryptCreateHash(a, &h, NULL, 0, NULL, 0, 0)) rt_fatal("SHA-256: BCryptCreateHash failed");
const uint8_t *s = p;
while (n) {
ULONG k = n > 0x40000000 ? 0x40000000 : (ULONG)n;
if (BCryptHashData(h, (PUCHAR)s, k, 0)) rt_fatal("SHA-256: BCryptHashData failed");
s += k;
n -= k;
}
if (BCryptFinishHash(h, out, 32, 0)) rt_fatal("SHA-256: BCryptFinishHash failed");
BCryptDestroyHash(h);
}
void rt_sha256(const void *p, size_t n, uint8_t out[32]) { sha256(p, n, out); }
// ---- configuration
static RtConfig cfg;
@@ -194,17 +169,9 @@ void rt_udf(Cpu *c, uint64_t pc) {
// CNTPCT_EL0: the Switch counter runs at 19.2 MHz. The scale is set before main, so no thread reads it half written.
static PlatScale tick_scale;
__attribute__((constructor)) static void tick_scale_init(void) {
LARGE_INTEGER f;
QueryPerformanceFrequency(&f);
tick_scale = plat_scale(19200000u, (uint64_t)f.QuadPart);
}
__attribute__((constructor)) static void tick_scale_init(void) { tick_scale = plat_scale(19200000u, plat_counter_hz()); }
uint64_t rt_ticks(void) {
LARGE_INTEGER t;
QueryPerformanceCounter(&t);
return plat_scaled(tick_scale, (uint64_t)t.QuadPart);
}
uint64_t rt_ticks(void) { return plat_scaled(tick_scale, plat_counter()); }
// ---- crash reporter
typedef struct { uintptr_t host; uint64_t guest; } FnRev;
@@ -216,7 +183,8 @@ static int by_host(const void *a, const void *b) {
return x < y ? -1 : x > y;
}
// The lifted function that contains a host code address, by the nearest function start below it.
// The lifted function that contains a host code address, by the nearest function start below it; 0 outside the lifted
// code.
static uint64_t lifted_function(uintptr_t rip) {
if (!fn_rev) {
static RtModule mods[MAX_MODULES];
@@ -230,6 +198,9 @@ static uint64_t lifted_function(uintptr_t rip) {
fn_rev[fn_rev_n++] = (FnRev){(uintptr_t)mods[i].fns[k], mods[i].addrs[k]};
qsort(fn_rev, fn_rev_n, sizeof *fn_rev, by_host);
}
// The lifted code is one range with the host code outside it (kernel.c ap_init checks that); an address above the
// last function start is taken for host code, which costs only the last function's body.
if (!fn_rev_n || rip > fn_rev[fn_rev_n - 1].host) return 0;
uint64_t lo = 0, hi = fn_rev_n;
while (lo < hi) {
uint64_t mid = (lo + hi) / 2;
@@ -239,36 +210,26 @@ static uint64_t lifted_function(uintptr_t rip) {
return lo ? fn_rev[lo - 1].guest : 0;
}
static int readable(uint64_t a, uint64_t n) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
return a + n <= (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
}
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
static LONG CALLBACK crash_handler(EXCEPTION_POINTERS *ep) {
static volatile LONG busy;
DWORD code = ep->ExceptionRecord->ExceptionCode;
if (code != EXCEPTION_ACCESS_VIOLATION && code != EXCEPTION_ILLEGAL_INSTRUCTION &&
code != EXCEPTION_INT_DIVIDE_BY_ZERO && code != EXCEPTION_PRIV_INSTRUCTION && code != EXCEPTION_STACK_OVERFLOW &&
code != EXCEPTION_IN_PAGE_ERROR)
return EXCEPTION_CONTINUE_SEARCH;
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
HMODULE self = GetModuleHandleW(NULL);
MODULEINFO mi;
if (!GetModuleInformation(GetCurrentProcess(), self, &mi, sizeof mi) || rip - (uintptr_t)mi.lpBaseOfDll >= mi.SizeOfImage)
return EXCEPTION_CONTINUE_SEARCH; // not our code: leave it to the handler that expects it
if (InterlockedExchange(&busy, 1)) ExitProcess(71);
static void crash_handler(const PlatCrash *x) {
static volatile int32_t busy;
if (plat_xchg32(&busy, 1)) plat_exit(71);
char d[160];
rt_logf("FATAL: exception %#lx at host %p (totk.exe+%#llx)", code, (void *)rip,
(unsigned long long)(rip - (uintptr_t)mi.lpBaseOfDll));
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2)
rt_logf(", %s address %#llx",
ep->ExceptionRecord->ExceptionInformation[0] == 1 ? "writing" : ep->ExceptionRecord->ExceptionInformation[0] == 8 ? "executing" : "reading",
(unsigned long long)ep->ExceptionRecord->ExceptionInformation[1]);
uintptr_t rip = x->rip;
rt_logf("FATAL: %s (%#x) at host %p (totk+%#llx)", x->what, x->code, (void *)rip, (unsigned long long)x->offset);
if (x->access >= 0)
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
(unsigned long long)x->addr);
rt_log_raw("\n", 1);
uint64_t f = lifted_function(rip);
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
for (int i = 0; i < x->nframes; i++) { // abort and invalid parameter: where the host code called it
uint64_t g = lifted_function(x->frames[i]);
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - (rip - x->offset)));
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
rt_log_raw("\n", 1);
}
Cpu *c = kern_cpu();
if (c) {
for (int i = 0; i < 31; i++)
@@ -286,19 +247,14 @@ static LONG CALLBACK crash_handler(EXCEPTION_POINTERS *ep) {
rt_logf(" not on a guest thread\n");
}
fflush(stderr);
ExitProcess(71);
plat_exit(71);
}
// ---- self-test: layout, dispatch table, host-function registry, without game files
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
static double now_s(void) {
LARGE_INTEGER t, f;
QueryPerformanceCounter(&t);
QueryPerformanceFrequency(&f);
return (double)t.QuadPart / (double)f.QuadPart;
}
static double now_s(void) { return (double)plat_now_ns() / 1e9; }
static volatile int host_probe_hits;
static void host_probe(Cpu *c) { host_probe_hits++; c->x[0] = 42; }
@@ -325,9 +281,8 @@ static void check_layout(void) {
CHECK(lay[i].base > lay[i - 1].base, "%s base is not above %s", lay[i].name, lay[i - 1].name);
CHECK(lay[i - 1].base + lay[i - 1].size <= lay[i].base, "%s overlaps %s", lay[i - 1].name, lay[i].name);
}
MEMORY_BASIC_INFORMATION mbi;
int ours = VirtualQuery((void *)(uintptr_t)lay[i].base, &mbi, sizeof mbi) && mbi.State == MEM_RESERVE &&
(uint64_t)(uintptr_t)mbi.AllocationBase <= lay[i].base;
uint64_t end;
int ours = plat_mem_state(lay[i].base, lay[i].base + 1, &end) == PLAT_MEM_RESERVED;
CHECK(ours, "%s is not inside the reserved module window", lay[i].name);
}
const RtGuestLayout *g = mem_layout();
@@ -447,14 +402,14 @@ static int selftest(void) {
(double)s.table_bytes / (1 << 20), dt * 1e3);
check_modules(mods, n);
CHECK(s.functions > 0, "no functions linked");
Cpu *c = _aligned_malloc(sizeof(Cpu), 64);
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
memset(c, 0, sizeof *c);
uint64_t r = rt_call(c, probe, 1, (uint64_t)7);
CHECK(rt_dispatch_lookup(probe) && r == 42 && host_probe_hits == 1 && c->x[30] == 0,
"a registered host function does not dispatch through rt_call");
CHECK(!rt_dispatch_lookup(probe + 16), "an unregistered host-function address dispatches");
CHECK(rt_dispatch_lookup(RT_RET_HOST), "the rt_call return sentinel is not registered");
_aligned_free(c);
plat_aligned_free(c);
bench(mods, n);
void *k1 = mem_kernel_alloc(0x200, 16), *k2 = mem_kernel_alloc(0x2000, 0x1000);
@@ -464,9 +419,9 @@ static int selftest(void) {
CHECK(mem_kernel_alloc(0x200, 16) == k1, "a freed kernel-region block is not reused");
check_memory();
PROCESS_MEMORY_COUNTERS pmc = {.cb = sizeof pmc};
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc))
printf("process: peak working set %.1f MiB\n", (double)pmc.PeakWorkingSetSize / (1 << 20));
float used, total, proc;
plat_mem_info(&used, &total, &proc);
if (proc >= 0) printf("process: working set %.0f MiB\n", proc);
printf("selftest: %s (%d failures)\n", failures ? "FAILED" : "passed", failures);
return failures ? 1 : 0;
}
@@ -480,9 +435,9 @@ static int usage(const char *argv0) {
}
// Paths for a copied folder (tools/package_build.py). Per path: TOTK_EXEFS, TOTK_ROMFS, TOTK_PROFILE_DIR, else the
// folder next to totk.exe (exefs, romfs, profile) if it exists, else the path baked in at build time. A relative
// value is relative to totk.exe's folder.
static char pp_exefs[MAX_PATH * 2], pp_romfs[MAX_PATH * 2], pp_profile[MAX_PATH * 2];
// folder next to totk.exe (exefs with main.npdm, romfs with romfs.bin, any profile folder) if it is there, else the
// path baked in at build time. A relative value is relative to totk.exe's folder.
static char pp_exefs[PLAT_PATH_MAX * 2], pp_romfs[PLAT_PATH_MAX * 2], pp_profile[PLAT_PATH_MAX * 2];
static const char *portable_path(const char *dir, const char *var, const char *sub, const char *must, char *out, size_t cap,
const char *fallback) {
@@ -490,36 +445,37 @@ static const char *portable_path(const char *dir, const char *var, const char *s
if (e && *e) {
int abs_path = e[0] == '\\' || e[0] == '/' || e[1] == ':';
if (abs_path) snprintf(out, cap, "%s", e);
else snprintf(out, cap, "%s\\%s", dir, e);
else snprintf(out, cap, "%s/%s", dir, e);
return out;
}
char probe[MAX_PATH * 3];
snprintf(out, cap, "%s\\%s", dir, sub);
snprintf(probe, sizeof probe, "%s\\%s", out, must);
return GetFileAttributesA(probe) != INVALID_FILE_ATTRIBUTES ? out : fallback;
char probe[PLAT_PATH_MAX * 3];
snprintf(out, cap, "%s/%s", dir, sub);
if (!must) return plat_dir_exists(out) ? out : fallback; // the folder itself is the sign
snprintf(probe, sizeof probe, "%s/%s", out, must);
return plat_file_exists(probe) ? out : fallback;
}
static void portable_paths(RtConfig *c) {
char exe[MAX_PATH * 2];
DWORD n = GetModuleFileNameA(NULL, exe, sizeof exe);
char *cut = n && n < sizeof exe ? strrchr(exe, '\\') : NULL;
char exe[PLAT_PATH_MAX * 2];
int n = plat_exe_path(exe, sizeof exe);
char *cut = n && n < sizeof exe ? plat_last_sep(exe) : NULL;
if (!cut) return;
*cut = 0;
c->exefs = portable_path(exe, "TOTK_EXEFS", "exefs", "main.npdm", pp_exefs, sizeof pp_exefs, c->exefs);
c->romfs = portable_path(exe, "TOTK_ROMFS", "romfs", "romfs.bin", pp_romfs, sizeof pp_romfs, c->romfs);
c->profile = portable_path(exe, "TOTK_PROFILE_DIR", "profile", ".", pp_profile, sizeof pp_profile, c->profile);
c->profile = portable_path(exe, "TOTK_PROFILE_DIR", "profile", NULL, pp_profile, sizeof pp_profile, c->profile);
}
int main(int argc, char **argv) {
mem_init(); // first: claim the guest address space before anything else can land there; reads totk.ini
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
AddVectoredExceptionHandler(0, crash_handler);
plat_crash_hook(crash_handler);
parse_trace();
prof_start();
// Timed guest waits (kernel.c block_inner) sleep in whole milliseconds; at the default 15.6 ms tick a 1 ms wait
// costs a full tick. TOTK_TIMER_1MS=0 keeps the default for comparison.
if (cfg_bool("TOTK_TIMER_1MS")) timeBeginPeriod(1);
if (cfg_bool("TOTK_TIMER_1MS")) plat_timer_1ms();
cfg = (RtConfig){.exefs = rt_default_exefs, .romfs = rt_default_romfs, .profile = rt_default_profile,
.gpu = cfg_get("TOTK_GPU")};
portable_paths(&cfg); // command-line paths below still win
@@ -560,7 +516,9 @@ int main(int argc, char **argv) {
if (!strcmp(mode, "--selftest")) return selftest();
if (!strcmp(mode, "--probe-loader")) return probe_loader();
if (!strcmp(mode, "--probe-cfg")) return probe_cfg();
if (!strcmp(mode, "--probe-dpi")) return probe_dpi();
#ifdef _WIN32
if (!strcmp(mode, "--probe-dpi")) return probe_dpi(); // the Win32 DPI awareness of totk.manifest
#endif
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
if (!strcmp(mode, "--probe-services")) return probe_services();
+90 -139
View File
@@ -6,9 +6,8 @@
//
// Every region is a plain reservation; pages are committed and decommitted at 4 KiB granularity. Decommitted pages
// come back zeroed, so no stale-page bookkeeping is needed. The stack and heap block and the alias region are
// reserved with MEM_WRITE_WATCH: mem_written_pages reports the pages the CPU wrote, which
// reserved write-watched (platform.h plat_mem_reserve): mem_written_pages reports the pages the CPU wrote, which
// the Vulkan back end uploads again.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -16,6 +15,7 @@
#include "cfg.h"
#include "host.h"
#include "layout.h"
#include "platform/platform.h"
#define PAGE 0x1000ull
#define GiB (1ull << 30)
@@ -48,7 +48,7 @@ enum { ATTR_PERMISSION_LOCKED = 16 };
static RtGuestLayout L;
static uint64_t mods_lo, mods_hi; // fixed module window
static SRWLOCK lock = SRWLOCK_INIT;
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
typedef struct { uint64_t dst, src, size; } StackMap;
@@ -58,14 +58,7 @@ static uint8_t *module_perm[MOD_COUNT]; // per data page of each module: perm
static uint64_t align_up(uint64_t v, uint64_t a) { return (v + a - 1) & ~(a - 1); }
static void *reserve_at(uint64_t at, uint64_t size, DWORD extra) {
void *p = VirtualAlloc((void *)(uintptr_t)at, size, MEM_RESERVE | extra, PAGE_NOACCESS);
if (p && p != (void *)(uintptr_t)at) {
VirtualFree(p, 0, MEM_RELEASE);
p = NULL;
}
return p;
}
static int reserve_at(uint64_t at, uint64_t size, int watch) { return plat_mem_reserve(at, size, watch); }
// ---- frozen regions
// A 2 MiB region of the watched reservations that nothing wrote for a while can be made read-only (mem_freeze), so the
@@ -79,9 +72,9 @@ static void *reserve_at(uint64_t at, uint64_t size, DWORD extra) {
#define FRZ_HOLD_MS 5000u
static volatile uint8_t frz[RT_ADDRESS_LIMIT >> FRZ_SHIFT];
static uint32_t thaw_ms[RT_ADDRESS_LIMIT >> FRZ_SHIFT]; // frz_lock: GetTickCount of the last thaw request, 0 never
static volatile LONG nfrz;
static SRWLOCK frz_lock = SRWLOCK_INIT;
static volatile LONG64 frz_count[4]; // freezes; thaws by a write fault, by mem_thaw, by a mapping change
static volatile int32_t nfrz;
static PlatMutex frz_lock = PLAT_MUTEX_INIT;
static volatile int64_t frz_count[4]; // freezes; thaws by a write fault, by mem_thaw, by a mapping change
static int watched(uint64_t addr, uint64_t size) {
const RtGuestLayout *g = &L;
@@ -91,27 +84,18 @@ static int watched(uint64_t addr, uint64_t size) {
// frz_lock held exclusively. why: 1 a write fault, 2 mem_thaw (holds the region thawed), 3 a mapping change here.
static void thaw_locked(uint64_t r, int why) {
if (why == 2) thaw_ms[r] = (uint32_t)GetTickCount64() | 1;
if (why == 2) thaw_ms[r] = (uint32_t)plat_now_ms() | 1;
if (!frz[r]) return;
DWORD old;
uint64_t a = r << FRZ_SHIFT, end = a + FRZ_SIZE;
if (!VirtualProtect((void *)(uintptr_t)a, FRZ_SIZE, PAGE_READWRITE, &old)) {
// Not all committed (freeze_locked left reserved holes): the read-only runs only. Every committed page of a
// frozen region was read-write at the freeze, and mapping changes thaw before they run, so these are all ours.
MEMORY_BASIC_INFORMATION mbi;
while (a < end && VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi)) {
uint64_t b = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
if (b > end) b = end;
if (mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READONLY &&
!VirtualProtect((void *)(uintptr_t)a, b - a, PAGE_READWRITE, &old))
rt_fatal("memory: thawing %#llx+%#llx failed (error %lu)", (unsigned long long)a,
(unsigned long long)(b - a), GetLastError());
a = b;
}
}
// The read-only runs only: freeze_locked left reserved holes (heap pages given back) as they were. Every committed
// page of a frozen region was read-write at the freeze, and mapping changes thaw before they run, so these are all
// ours.
uint64_t a = r << FRZ_SHIFT, end = a + FRZ_SIZE, b;
for (; a < end; a = b)
if (plat_mem_state(a, end, &b) == PLAT_PROT_READ && !plat_mem_protect(a, b - a, PLAT_PROT_RW))
rt_fatal("memory: thawing %#llx+%#llx failed", (unsigned long long)a, (unsigned long long)(b - a));
__atomic_store_n(&frz[r], 0, __ATOMIC_RELEASE);
InterlockedDecrement(&nfrz);
InterlockedIncrement64(&frz_count[why]);
plat_dec32(&nfrz);
plat_inc64(&frz_count[why]);
}
// frz_lock held exclusively.
@@ -122,9 +106,9 @@ static void thaw_range_locked(uint64_t a, uint64_t n, int why) {
}
void mem_thaw(uint64_t a, uint64_t n) {
AcquireSRWLockExclusive(&frz_lock); // also when nothing is frozen: the hold keeps a read's buffer writable
plat_mutex_lock(&frz_lock); // also when nothing is frozen: the hold keeps a read's buffer writable
thaw_range_locked(a, n, 2);
ReleaseSRWLockExclusive(&frz_lock);
plat_mutex_unlock(&frz_lock);
}
// frz_lock held exclusively. Makes the committed pages of region r read-only when every one of them is read-write;
@@ -132,21 +116,16 @@ void mem_thaw(uint64_t a, uint64_t n) {
static int freeze_locked(uint64_t r) {
uint64_t a = r << FRZ_SHIFT, end = a + FRZ_SIZE, run[16][2];
int n = 0;
MEMORY_BASIC_INFORMATION mbi;
for (uint64_t x = a; x < end; ) {
if (!VirtualQuery((void *)(uintptr_t)x, &mbi, sizeof mbi)) return 0;
uint64_t y = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
if (y > end) y = end;
if (mbi.State == MEM_COMMIT) {
if (mbi.Protect != PAGE_READWRITE || n == 16) return 0;
for (uint64_t x = a, y; x < end; x = y) {
int st = plat_mem_state(x, end, &y);
if (st >= PLAT_PROT_NONE) {
if (st != PLAT_PROT_RW || n == 16) return 0;
run[n][0] = x, run[n][1] = y, n++;
} else if (mbi.State != MEM_RESERVE) return 0;
x = y;
} else if (st != PLAT_MEM_RESERVED) return 0;
}
DWORD old;
for (int i = 0; i < n; i++)
if (!VirtualProtect((void *)(uintptr_t)run[i][0], run[i][1] - run[i][0], PAGE_READONLY, &old)) {
while (i--) VirtualProtect((void *)(uintptr_t)run[i][0], run[i][1] - run[i][0], PAGE_READWRITE, &old);
if (!plat_mem_protect(run[i][0], run[i][1] - run[i][0], PLAT_PROT_READ)) {
while (i--) plat_mem_protect(run[i][0], run[i][1] - run[i][0], PLAT_PROT_RW);
return 0;
}
return n > 0;
@@ -156,16 +135,16 @@ int mem_freeze(uint64_t a) {
if ((a & (FRZ_SIZE - 1)) || !watched(a, FRZ_SIZE)) return 0;
uint64_t r = a >> FRZ_SHIFT;
int ok = 0;
AcquireSRWLockExclusive(&frz_lock);
plat_mutex_lock(&frz_lock);
if (frz[r]) ok = 1;
else if (thaw_ms[r] && (int32_t)((uint32_t)GetTickCount64() - thaw_ms[r]) < (int32_t)FRZ_HOLD_MS) ok = 0; // |1 may be ahead
else if (thaw_ms[r] && (int32_t)((uint32_t)plat_now_ms() - thaw_ms[r]) < (int32_t)FRZ_HOLD_MS) ok = 0; // |1 may be ahead
else if (freeze_locked(r)) {
__atomic_store_n(&frz[r], 1, __ATOMIC_RELEASE);
InterlockedIncrement(&nfrz);
InterlockedIncrement64(&frz_count[0]);
plat_inc32(&nfrz);
plat_inc64(&frz_count[0]);
ok = 1;
}
ReleaseSRWLockExclusive(&frz_lock);
plat_mutex_unlock(&frz_lock);
return ok;
}
@@ -173,83 +152,60 @@ int mem_frozen(uint64_t a) { return a < RT_ADDRESS_LIMIT && __atomic_load_n(&frz
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws) {
*frozen = (uint64_t)__atomic_load_n(&nfrz, __ATOMIC_RELAXED);
*freezes = (uint64_t)InterlockedExchange64(&frz_count[0], 0);
*fault_thaws = (uint64_t)InterlockedExchange64(&frz_count[1], 0);
*other_thaws = (uint64_t)InterlockedExchange64(&frz_count[2], 0) + (uint64_t)InterlockedExchange64(&frz_count[3], 0);
*freezes = (uint64_t)plat_xchg64(&frz_count[0], 0);
*fault_thaws = (uint64_t)plat_xchg64(&frz_count[1], 0);
*other_thaws = (uint64_t)plat_xchg64(&frz_count[2], 0) + (uint64_t)plat_xchg64(&frz_count[3], 0);
}
// The first write into a frozen region thaws it and retries. A fault in a region another thread thawed meanwhile
// retries too; any other fault goes on to the crash handler.
static LONG CALLBACK thaw_handler(EXCEPTION_POINTERS *ep) {
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
if (er->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || er->NumberParameters < 2 || er->ExceptionInformation[0] != 1)
return EXCEPTION_CONTINUE_SEARCH;
uint64_t a = er->ExceptionInformation[1];
if (a >= RT_ADDRESS_LIMIT || !watched(a, 1)) return EXCEPTION_CONTINUE_SEARCH;
AcquireSRWLockExclusive(&frz_lock);
// The platform's write-fault hook (plat_mem_fault_hook): 1 retries the write.
static int thaw_fault(uint64_t a) {
if (a >= RT_ADDRESS_LIMIT || !watched(a, 1)) return 0;
plat_mutex_lock(&frz_lock);
int was = frz[a >> FRZ_SHIFT];
thaw_locked(a >> FRZ_SHIFT, 1);
ReleaseSRWLockExclusive(&frz_lock);
if (was) return EXCEPTION_CONTINUE_EXECUTION;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) && mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE)
return EXCEPTION_CONTINUE_EXECUTION;
return EXCEPTION_CONTINUE_SEARCH;
plat_mutex_unlock(&frz_lock);
if (was) return 1;
uint64_t end;
return plat_mem_state(a & ~(PAGE - 1), (a & ~(PAGE - 1)) + PAGE, &end) == PLAT_PROT_RW;
}
// Mapping changes thaw the regions they touch and run under frz_lock, so no freeze sees them half done.
static void commit(uint64_t a, uint64_t n, const char *what) {
AcquireSRWLockExclusive(&frz_lock);
plat_mutex_lock(&frz_lock);
thaw_range_locked(a, n, 3);
int ok = !n || VirtualAlloc((void *)(uintptr_t)a, n, MEM_COMMIT, PAGE_READWRITE);
ReleaseSRWLockExclusive(&frz_lock);
if (!ok)
rt_fatal("memory: commit %s %#llx+%#llx failed (error %lu)", what, (unsigned long long)a,
(unsigned long long)n, GetLastError());
int ok = !n || plat_mem_commit(a, n);
plat_mutex_unlock(&frz_lock);
if (!ok) rt_fatal("memory: commit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
}
static void decommit(uint64_t a, uint64_t n, const char *what) {
AcquireSRWLockExclusive(&frz_lock);
plat_mutex_lock(&frz_lock);
thaw_range_locked(a, n, 3);
int ok = !n || VirtualFree((void *)(uintptr_t)a, n, MEM_DECOMMIT);
ReleaseSRWLockExclusive(&frz_lock);
if (!ok)
rt_fatal("memory: decommit %s %#llx+%#llx failed (error %lu)", what, (unsigned long long)a,
(unsigned long long)n, GetLastError());
int ok = !n || plat_mem_decommit(a, n);
plat_mutex_unlock(&frz_lock);
if (!ok) rt_fatal("memory: decommit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
}
static void protect(uint64_t a, uint64_t n, DWORD prot, const char *what) {
DWORD old;
AcquireSRWLockExclusive(&frz_lock);
static void protect(uint64_t a, uint64_t n, int prot, const char *what) {
plat_mutex_lock(&frz_lock);
thaw_range_locked(a, n, 3);
int ok = !n || VirtualProtect((void *)(uintptr_t)a, n, prot, &old);
ReleaseSRWLockExclusive(&frz_lock);
if (!ok)
rt_fatal("memory: protect %s %#llx+%#llx failed (error %lu)", what, (unsigned long long)a,
(unsigned long long)n, GetLastError());
int ok = !n || plat_mem_protect(a, n, prot);
plat_mutex_unlock(&frz_lock);
if (!ok) rt_fatal("memory: protect %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
}
// The process is re-run when a region is taken: Windows places DLLs, heaps and thread stacks at random addresses,
// and a fresh process gets a fresh layout. The parent waits and exits with the child's code.
// and a fresh process gets a fresh layout.
static void reexec_or_fail(const char *what) {
const char *e = cfg_get("TOTK_REEXEC");
int n = e ? atoi(e) : 0;
if (n >= REEXEC_LIMIT) rt_fatal("memory: %s is not free after %d attempts", what, n);
char buf[16];
snprintf(buf, sizeof buf, "%d", n + 1);
SetEnvironmentVariableA("TOTK_REEXEC", buf);
fflush(stdout);
fflush(stderr);
STARTUPINFOW si = {.cb = sizeof si};
PROCESS_INFORMATION pi;
wchar_t *cmd = _wcsdup(GetCommandLineW()); // CreateProcessW may write its command line
if (!cmd || !CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
rt_fatal("memory: %s is not free and re-running the process failed (error %lu)", what, GetLastError());
CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 70;
GetExitCodeProcess(pi.hProcess, &code);
ExitProcess(code);
plat_setenv("TOTK_REEXEC", buf);
plat_reexec();
}
void mem_init(void) {
@@ -266,14 +222,14 @@ 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, MEM_WRITE_WATCH)) low = at;
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)
if (reserve_at(at, ALIAS_SIZE, MEM_WRITE_WATCH)) alias = at;
if (reserve_at(at, ALIAS_SIZE, 1)) alias = at;
if (!alias) reexec_or_fail("a 64 GiB alias region below 2^39");
AddVectoredExceptionHandler(1, thaw_handler); // before the crash handler
alias_pages = VirtualAlloc(NULL, ALIAS_PAGES, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
plat_mem_fault_hook(thaw_fault); // before the crash handler
alias_pages = calloc(ALIAS_PAGES, 1);
if (!alias_pages) rt_fatal("memory: cannot allocate the alias page map");
// totk.ini is read here, once the address space is claimed and before the first setting: the pool size below
// depends on TOTK_APP_POOL and on the resolution settings (resolution.c decides them once, on this first call).
@@ -293,20 +249,15 @@ void mem_init(void) {
const RtGuestLayout *mem_layout(void) { return &L; }
// Adapted from LAS Recomp runtime/platform/windows.c:40-63 @ da35b83 (MIT, Copyright (c) 2026 Peiikai): GetWriteWatch
// with the reset flag. A range inside one watched reservation (stack and heap block, or alias region) only.
static uint64_t written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max, DWORD flags) {
// A range inside one watched reservation (stack and heap block, or alias region) only.
static uint64_t written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max, int reset) {
if (!max || !size) return 0;
if (!watched(addr, size)) return UINT64_MAX;
ULONG_PTR n = (ULONG_PTR)max;
DWORD gran;
_Static_assert(sizeof(PVOID) == sizeof(uint64_t), "page addresses are written straight into a uint64_t array");
if (GetWriteWatch(flags, (void *)(uintptr_t)addr, (SIZE_T)size, (PVOID *)pages, &n, &gran)) return UINT64_MAX;
return n;
return plat_mem_written(addr, size, pages, max, reset);
}
uint64_t mem_written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max) {
return written_pages(addr, size, pages, max, WRITE_WATCH_FLAG_RESET);
return written_pages(addr, size, pages, max, 1);
}
uint64_t mem_written_peek(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max) {
@@ -324,15 +275,15 @@ 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) {
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
if (size > HEAP_CAP || size % 0x200000 || size + image_bytes() > L.pool) {
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
return R_INVALID_STATE;
}
if (size > heap_size) commit(L.heap.base + heap_size, size - heap_size, "heap");
if (size < heap_size) decommit(L.heap.base + size, heap_size - size, "heap");
heap_size = size;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
if (addr) *addr = L.heap.base;
return R_OK;
}
@@ -379,16 +330,16 @@ uint32_t mem_set_permission(uint64_t a, uint64_t n, uint32_t perm) {
int i = module_data(a, n);
if (i < 0) return R_INVALID_STATE;
const RtLoaded *m = ldr_module(i);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
uint8_t *t = perm_table(i) + (a - m->base - m->data_off) / PAGE;
for (uint64_t p = 0; p < n / PAGE; p++)
if (t[p] & ATTR_PERMISSION_LOCKED) {
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
return R_INVALID_STATE;
}
memset(t, (int)perm, n / PAGE);
protect(a, n, perm == PERM_RW ? PAGE_READWRITE : perm == PERM_R ? PAGE_READONLY : PAGE_NOACCESS, "module data");
ReleaseSRWLockExclusive(&lock);
protect(a, n, perm == PERM_RW ? PLAT_PROT_RW : perm == PERM_R ? PLAT_PROT_READ : PLAT_PROT_NONE, "module data");
plat_mutex_unlock(&lock);
rt_log("%s data permission +%#llx size %#llx perm %u", m->name, (unsigned long long)(a - m->base),
(unsigned long long)n, perm);
return R_OK;
@@ -402,10 +353,10 @@ uint32_t mem_set_attribute(uint64_t a, uint64_t n, uint32_t mask, uint32_t at) {
int i = module_data(a, n);
if (i < 0) return R_INVALID_STATE;
const RtLoaded *m = ldr_module(i);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
uint8_t *t = perm_table(i) + (a - m->base - m->data_off) / PAGE;
for (uint64_t p = 0; p < n / PAGE; p++) t[p] |= ATTR_PERMISSION_LOCKED;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
return R_OK;
}
@@ -424,7 +375,7 @@ static uint32_t stack_move(int unmap, uint64_t dst, uint64_t src, uint64_t n) {
int in_heap = inside(src, n, (RtRange){L.heap.base, heap_size});
int in_alias = inside(src, n, L.alias);
if (!in_heap && !in_alias) return R_INVALID_REGION;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
uint32_t r = R_OK;
if (in_alias && !alias_all_mapped(src, n)) {
r = R_INVALID_REGION;
@@ -438,7 +389,7 @@ static uint32_t stack_move(int unmap, uint64_t dst, uint64_t src, uint64_t n) {
r = R_INVALID_STATE;
goto out;
}
protect(src, n, PAGE_READWRITE, "moved stack source");
protect(src, n, PLAT_PROT_RW, "moved stack source");
memcpy((void *)(uintptr_t)src, (const void *)(uintptr_t)dst, n);
decommit(dst, n, "moved stack");
stack_maps[found] = stack_maps[--nstack_maps];
@@ -455,11 +406,11 @@ static uint32_t stack_move(int unmap, uint64_t dst, uint64_t src, uint64_t n) {
}
commit(dst, n, "moved stack");
memcpy((void *)(uintptr_t)dst, (const void *)(uintptr_t)src, n);
protect(src, n, PAGE_NOACCESS, "moved stack source");
protect(src, n, PLAT_PROT_NONE, "moved stack source");
stack_maps[nstack_maps++] = (StackMap){dst, src, n};
}
out:
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
return r;
}
@@ -486,7 +437,7 @@ static uint32_t alias_map(int unmap, uint64_t a, uint64_t n) {
if (!n || ((a | n) & (PAGE - 1))) return R_INVALID_ADDRESS;
if (!inside(a, n, L.alias)) return R_INVALID_REGION;
uint64_t first = (a - L.alias.base) / PAGE, count = n / PAGE;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
uint32_t r = R_OK;
if (unmap) {
for (unsigned i = 0; i < nstack_maps; i++)
@@ -509,7 +460,7 @@ static uint32_t alias_map(int unmap, uint64_t a, uint64_t n) {
alias_bytes += fresh * PAGE;
}
out:
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
if (r) rt_log("%sMapPhysicalMemory %#llx+%#llx failed %#x (alias %#llx used, heap %#llx)", unmap ? "Un" : "",
(unsigned long long)a, (unsigned long long)n, r, (unsigned long long)alias_bytes,
(unsigned long long)heap_size);
@@ -559,7 +510,7 @@ static uint64_t run_start(const uint8_t *p, uint64_t i, uint8_t v) { // first
uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
RtMemoryInfo r = {.base = a & ~(PAGE - 1), .size = PAGE};
uint64_t low = L.aslr.base;
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
int mi;
if (a >= RT_ADDRESS_LIMIT) {
r.base = RT_ADDRESS_LIMIT;
@@ -625,7 +576,7 @@ uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
r.base = lo;
r.size = hi - lo;
}
ReleaseSRWLockShared(&lock);
plat_mutex_unlock_shared(&lock);
if (out) *out = r;
return R_OK;
}
@@ -640,9 +591,9 @@ int mem_info(uint32_t id, uint64_t *out) {
case 5: v = L.heap.size; break;
case 6: case 21: v = L.pool; break;
case 7: case 22:
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
v = image_bytes() + heap_size + alias_bytes;
ReleaseSRWLockShared(&lock);
plat_mutex_unlock_shared(&lock);
break;
case 12: v = L.aslr.base; break;
case 13: v = L.aslr.size; break;
@@ -664,7 +615,7 @@ int mem_info(uint32_t id, uint64_t *out) {
typedef struct KFree { struct KFree *next; } KFree;
static KFree *kfree_list[KCLASSES];
static uint64_t kbump, kcommitted;
static SRWLOCK klock = SRWLOCK_INIT;
static PlatMutex klock = PLAT_MUTEX_INIT;
static int kclass(uint64_t size) {
int s = KMIN_SHIFT;
@@ -679,7 +630,7 @@ void *mem_kernel_alloc(uint64_t size, uint64_t align) {
int k = kclass(want ? want : 1);
if (k >= KCLASSES) rt_fatal("mem_kernel_alloc: %#llx bytes", (unsigned long long)size);
uint64_t bytes = 1ull << (k + KMIN_SHIFT), al = bytes < KCOMMIT ? bytes : KCOMMIT;
AcquireSRWLockExclusive(&klock);
plat_mutex_lock(&klock);
KFree *f = kfree_list[k];
void *p;
if (f) {
@@ -698,7 +649,7 @@ void *mem_kernel_alloc(uint64_t size, uint64_t align) {
kbump = a + bytes;
p = (void *)(uintptr_t)a;
}
ReleaseSRWLockExclusive(&klock);
plat_mutex_unlock(&klock);
memset(p, 0, size ? size : 1);
return p;
}
@@ -710,9 +661,9 @@ void mem_kernel_free(void *p, uint64_t size) {
int k = kclass(size ? size : 1);
// A block allocated with a larger alignment sits in a larger class; freeing it into the smaller class only
// wastes the rest.
AcquireSRWLockExclusive(&klock);
plat_mutex_lock(&klock);
KFree *f = p;
f->next = kfree_list[k];
kfree_list[k] = f;
ReleaseSRWLockExclusive(&klock);
plat_mutex_unlock(&klock);
}
+19 -17
View File
@@ -10,19 +10,19 @@
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. A restart (StartSixAxisSensor) drops
// the samples before it: reads return nothing until the next sample, which starts a fresh frame and angle; sampling
// numbers go on. The feeding thread writes, guest threads read, both under the instance's SRW lock.
#include <windows.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "host.h"
#include "input.h"
#include "platform/platform.h"
enum { MOTION_RING = 64 };
#define STALE_NS 200000000ull // no sample for 200 ms: no motion
struct MotionFusion {
SRWLOCK lock;
PlatMutex lock;
MotionState ring[MOTION_RING];
uint64_t produced, last_ns;
uint64_t first; // produced at the last restart: older samples are not read
@@ -34,20 +34,22 @@ struct MotionFusion {
MotionFusion *motion_new(void) {
MotionFusion *m = calloc(1, sizeof *m);
if (!m) rt_fatal("motion: out of memory");
InitializeSRWLock(&m->lock);
plat_mutex_init(&m->lock);
m->restart = 1;
return m;
}
static MotionFusion *global;
static void global_init(void *arg) {
(void)arg;
global = motion_new();
}
MotionFusion *motion_global(void) {
static MotionFusion *g;
static INIT_ONCE once = INIT_ONCE_STATIC_INIT;
BOOL pending;
if (InitOnceBeginInitialize(&once, 0, &pending, NULL) && pending) {
g = motion_new();
InitOnceComplete(&once, 0, NULL);
}
return g;
static PlatOnce once = PLAT_ONCE_INIT;
plat_once(&once, global_init, NULL);
return global;
}
static double norm3(const double v[3]) { return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); }
@@ -106,7 +108,7 @@ static void integrate(double R[3][3], const double gyro[3], const double acc[3],
}
void motion_feed(MotionFusion *m, const double acc[3], const double gyro[3], uint64_t now) {
AcquireSRWLockExclusive(&m->lock);
plat_mutex_lock(&m->lock);
double dt = m->last_ns && now > m->last_ns ? (double)(now - m->last_ns) / 1e9 : 0.004;
if (dt > 0.05) dt = 0.004; // a gap (no controller, sensors were off): no integration across it
m->last_ns = now;
@@ -126,27 +128,27 @@ void motion_feed(MotionFusion *m, const double acc[3], const double gyro[3], uin
for (int j = 0; j < 3; j++) s->dir[i * 3 + j] = (float)m->R[j][i]; // direction i = controller axis i in the reference frame
}
m->produced++;
ReleaseSRWLockExclusive(&m->lock);
plat_mutex_unlock(&m->lock);
}
int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
if (max <= 0) return 0;
int n = 0;
AcquireSRWLockShared(&m->lock);
plat_mutex_lock_shared(&m->lock);
if (m->produced > m->first && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
uint64_t have = m->produced - m->first < MOTION_RING ? m->produced - m->first : MOTION_RING;
n = (int)(have < (uint64_t)max ? have : (uint64_t)max);
for (int i = 0; i < n; i++) out[i] = m->ring[(m->produced - 1 - (uint64_t)i) % MOTION_RING];
}
ReleaseSRWLockShared(&m->lock);
plat_mutex_unlock_shared(&m->lock);
return n;
}
void motion_restart(MotionFusion *m) {
AcquireSRWLockExclusive(&m->lock);
plat_mutex_lock(&m->lock);
m->restart = 1;
m->first = m->produced;
m->last_ns = 0; // the first sample after the restart integrates nothing across the stop
memset(m->angle, 0, sizeof m->angle);
ReleaseSRWLockExclusive(&m->lock);
plat_mutex_unlock(&m->lock);
}
+16 -19
View File
@@ -7,7 +7,6 @@
// 2. paced at 60 presents per second (fake clock): at most one frame per present, one frame per 2 presents, no drops;
// 3. paced at 15 presents per second: late frames are dropped, the frame handed over is the one due.
// Without the RomFS (no game data) it prints why and exits 0. TOTK_PROBE_MOVIE_SHOT=<file.ppm> saves the middle frame.
#include <windows.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@@ -17,6 +16,7 @@
#include "fs.h"
#include "host.h"
#include "movie.h"
#include "platform/platform.h"
#define CHECK(cond, ...) \
do { \
@@ -125,7 +125,7 @@ static void *md_slot(PMd *m, const char *k, int create) {
return &m->e[m->n++];
}
#define MDE(p) ((__typeof__(&((PMd *)0)->e[0]))(p))
static void s_signal(Cpu *c) { InterlockedIncrement((volatile LONG *)arg_p(c, 0)); }
static void s_signal(Cpu *c) { plat_inc32((volatile int32_t *)arg_p(c, 0)); }
static void s_set_range(Cpu *c) { PBuf *b = arg_p(c, 0); b->off = (int)arg_x(c, 1); b->size = (int)arg_x(c, 2); }
static void s_set_data(Cpu *c) { PBuf *b = arg_p(c, 0); b->base = arg_x(c, 1); b->cap = arg_x(c, 2); }
static void s_base(Cpu *c) { ret_x(c, ((PBuf *)arg_p(c, 0))->base); }
@@ -160,9 +160,9 @@ static void s_set_string(Cpu *c) {
snprintf(MDE(e)->s, sizeof MDE(e)->s, "%s", (const char *)arg_p(c, 2));
}
}
static volatile LONG g_d0, g_d2;
static void s_orig_d0(Cpu *c) { (void)c; InterlockedIncrement(&g_d0); } // the library's deleting destructor
static void s_orig_d2(Cpu *c) { (void)c; InterlockedIncrement(&g_d2); }
static volatile int32_t g_d0, g_d2;
static void s_orig_d0(Cpu *c) { (void)c; plat_inc32(&g_d0); } // the library's deleting destructor
static void s_orig_d2(Cpu *c) { (void)c; plat_inc32(&g_d2); }
static void s_vtable(Cpu *c) { (void)c; rt_fatal("probe-movie: the decoder vtable was called"); }
// ---- driving the entries as the game does
@@ -203,7 +203,7 @@ static void save_ppm(const char *path, const uint8_t *nv12, int w, int h) {
static int wait_quiet(uint64_t obj) {
for (int i = 0; i < 5000; i++) {
if (movie_quiet(obj)) return 1;
Sleep(1);
plat_sleep_ms(1);
}
return 0;
}
@@ -269,9 +269,9 @@ static int paced(uint64_t obj, const uint8_t *data, const Track *t, int limit, u
int probe_movie(void) {
const char *dir = rt_config()->romfs;
char image[MAX_PATH];
char image[PLAT_PATH_MAX];
snprintf(image, sizeof image, "%s/romfs.bin", dir);
if (GetFileAttributesA(image) == INVALID_FILE_ATTRIBUTES) {
if (!plat_file_exists(image)) {
printf("probe-movie: skipped: no RomFS image at %s (game data absent; extract it with tools/extract.py)\n", image);
return 0;
}
@@ -334,12 +334,12 @@ int probe_movie(void) {
movie_symbols[i]);
hostfn_seal();
movie_set_guest(&g);
C = _aligned_malloc(sizeof(Cpu), 64);
C = plat_aligned_alloc(sizeof(Cpu), 64);
CHECK(C, "Cpu allocation");
memset(C, 0, sizeof *C);
uint64_t obj_mem[2] = {0}, obj = U(obj_mem);
volatile LONG ev[4] = {0};
volatile int32_t ev[4] = {0};
uint64_t events[4] = {U(&ev[0]), U(&ev[1]), U(&ev[2]), U(&ev[3])};
PMd cfg = {0};
C->x[0] = U(&cfg);
@@ -377,9 +377,7 @@ int probe_movie(void) {
int sent = 0, eos_sent = 0, got = 0, eos = 0, formats = 0;
int64_t prev = INT64_MIN;
double luma_lo = 255, luma_hi = 0;
LARGE_INTEGER f0, f1, qf;
QueryPerformanceFrequency(&qf);
QueryPerformanceCounter(&f0);
uint64_t f0 = plat_now_ns();
for (int idle = 0; !eos;) {
if (feed(obj, data, &t, &sent, t.n, &eos_sent)) return 1;
movie_tick(C);
@@ -419,11 +417,10 @@ int probe_movie(void) {
if (progress) idle = 0;
else {
CHECK(++idle < 20000, "no output for 20 s (%d of %d frames out, %d in)", got, t.n, sent);
Sleep(1);
plat_sleep_ms(1);
}
}
QueryPerformanceCounter(&f1);
double wall = (double)(f1.QuadPart - f0.QuadPart) / (double)qf.QuadPart;
double wall = (double)(plat_now_ns() - f0) / 1e9;
MovieStats st;
CHECK(movie_stats(obj, &st), "stats");
CHECK(got == t.n, "%d frames out of %d in", got, t.n);
@@ -431,10 +428,10 @@ int probe_movie(void) {
(unsigned long long)st.frames, (unsigned long long)st.errors);
CHECK(formats, "GetOutputBuffer's format does not describe the NV12 frame");
CHECK(ev[MOVIE_EV_INPUT] > 0 && ev[MOVIE_EV_OUTPUT] > 0 && ev[MOVIE_EV_FORMAT] == 1 && ev[MOVIE_EV_ERROR] == 0,
"events: input %ld output %ld format %ld error %ld", ev[0], ev[1], ev[2], ev[3]);
"events: input %d output %d format %d error %d", ev[0], ev[1], ev[2], ev[3]);
CHECK(luma_hi - luma_lo > 1 || luma_hi > 4, "decoded pictures look blank (centre luma %.1f-%.1f)", luma_lo, luma_hi);
double avg = st.decode_ns / 1e6 / (double)st.frames;
printf("probe-movie: unpaced: %d frames, %dx%d NV12, PTS in order, events input %ld output %ld format %ld; centre "
printf("probe-movie: unpaced: %d frames, %dx%d NV12, PTS in order, events input %d output %d format %d; centre "
"luma %.1f-%.1f\n", got, t.w, t.h, ev[0], ev[1], ev[2], luma_lo, luma_hi);
printf("probe-movie: decode %.2f ms per frame (max %.2f ms) on the worker; %.1f frames/s through the API\n", avg,
st.max_ns / 1e6, got / wall);
@@ -475,7 +472,7 @@ int probe_movie(void) {
free(frame);
free(data);
free(t.f);
_aligned_free(C);
plat_aligned_free(C);
printf("probe-movie: passed\n");
return 0;
}
+37 -26
View File
@@ -12,7 +12,6 @@
// recording into host memory; the handle is that copy (a flat OP_BEGIN ... OP_END sequence) and lives until the game
// adds the same control memory again. CallCommands and CopyCommands inline the called recording, so copies hold no
// OP_CALL or OP_JUMP.
#include <windows.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
@@ -22,6 +21,7 @@
#include "host.h"
#include "nvn_api.h"
#include "nvn_hmap.h"
#include "platform/platform.h"
#define CMD_MAGIC MAGIC('C', 'M', 'D', 'B')
#define COPY_MAGIC 0x59504f43444d43ull // "CMDCOPY": prefix of a live recording copy
@@ -39,23 +39,23 @@ static cmd_t *cmd_of(const Cpu *c) { return arg_p(c, 0); }
// without any command memory, or ends without any control memory. It runs on the calling guest thread through
// rt_call; whatever memory it adds is accounted. Set{Command,Control}MemoryCallbackEnabled(false) suppress it.
enum { CB_NO_COMMAND = 1, CB_NO_CONTROL = 2 };
static SRWLOCK cb_lock = SRWLOCK_INIT;
static PlatMutex cb_lock = PLAT_MUTEX_INIT;
static nvn_hmap cb_off = {.klen = 8}; // command buffer -> disabled callback kinds | 0x100
static unsigned cb_disabled(const cmd_t *o) {
AcquireSRWLockShared(&cb_lock);
plat_mutex_lock_shared(&cb_lock);
uintptr_t v = (uintptr_t)nvn_hm_get(&cb_off, &o);
ReleaseSRWLockShared(&cb_lock);
plat_mutex_unlock_shared(&cb_lock);
return (unsigned)v & 0xff;
}
static void cb_set_disabled(const cmd_t *o, unsigned kind, int disabled) {
AcquireSRWLockExclusive(&cb_lock);
plat_mutex_lock(&cb_lock);
unsigned v = (unsigned)(uintptr_t)nvn_hm_get(&cb_off, &o) & 0xff;
v = disabled ? v | kind : v & ~kind;
if (v) nvn_hm_put(&cb_off, &o, (void *)(uintptr_t)(v | 0x100));
else nvn_hm_del(&cb_off, &o);
ReleaseSRWLockExclusive(&cb_lock);
plat_mutex_unlock(&cb_lock);
}
static void memory_event(Cpu *c, cmd_t *o, int event, uint64_t min) {
@@ -82,7 +82,12 @@ static void cmd_reserve(cmd_t *o, size_t need) {
if (!cap) cap = 64 << 10;
while (used + need + OP_RESERVE > cap) cap *= 2;
uint8_t *b = realloc(o->seg, cap);
if (!b) rt_fatal("nvn: command recording buffer of %zu bytes", cap);
// A block this size fails only without commit (the fatal report states it) or when seg is no block of ours: a
// command buffer object the game overwrote or copied shows a wrong magic or more used than its old size.
if (!b)
rt_fatal("nvn: command recording buffer of %zu bytes (command buffer %p, magic %#x, buffer %p of %zu bytes, %zu "
"used, %zu asked)", cap, (void *)o, o->magic, (void *)o->seg, o->seg ? (size_t)(o->end - o->seg) : 0,
used, need);
o->seg = b;
o->cur = b + used;
o->end = b + cap;
@@ -132,7 +137,7 @@ static void copy_view(view_t *dst, uint8_t *has, const NVNtextureView *v) {
// Recordings refer to textures by pointer, but the game may finalize or reuse an NVNtexture after recording and before
// the submit (NVN itself captured only GPU state). Records therefore point at an immutable host copy per texture
// initialization (tex_t.serial). The copies are kept for the whole run (MacNativeGames sweeps them for long sessions).
static SRWLOCK pin_lock = SRWLOCK_INIT;
static PlatMutex pin_lock = PLAT_MUTEX_INIT;
static nvn_hmap pins = {.klen = 8}; // serial -> tex_t copy
static const tex_t *tex_pin(const NVNtexture *nt) {
@@ -140,11 +145,11 @@ static const tex_t *tex_pin(const NVNtexture *nt) {
if (!t) return NULL;
uint64_t serial = t->serial;
if (!serial) rt_fatal("nvn: a recording references the uninitialized texture object %p", (const void *)t);
AcquireSRWLockShared(&pin_lock);
plat_mutex_lock_shared(&pin_lock);
const tex_t *r = nvn_hm_get(&pins, &serial);
ReleaseSRWLockShared(&pin_lock);
plat_mutex_unlock_shared(&pin_lock);
if (r) return r;
AcquireSRWLockExclusive(&pin_lock);
plat_mutex_lock(&pin_lock);
r = nvn_hm_get(&pins, &serial);
if (!r) {
tex_t *copy = malloc(sizeof *copy);
@@ -153,7 +158,7 @@ static const tex_t *tex_pin(const NVNtexture *nt) {
nvn_hm_put(&pins, &serial, copy);
r = copy;
}
ReleaseSRWLockExclusive(&pin_lock);
plat_mutex_unlock(&pin_lock);
return r;
}
@@ -166,7 +171,7 @@ typedef struct { uint64_t magic, bytes, made_at, used_at, deferred; } copy_prefi
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
static SRWLOCK copy_lock = SRWLOCK_INIT; // guards the two maps, the counters and every prefix
static PlatMutex copy_lock = PLAT_MUTEX_INIT; // guards the two maps, the counters and every prefix
static nvn_hmap copy_pages = {.klen = 8}; // page -> copy_bucket
static nvn_hmap copy_live = {.klen = 8}; // handle -> copy_prefix of a live copy
static uint64_t ncopies, copy_bytes, copies_made, nsubmits;
@@ -200,27 +205,27 @@ static copy_prefix *copy_find(uint64_t handle) { // copy_lock held
}
int nvn_handle_live(uint64_t handle) {
AcquireSRWLockShared(&copy_lock);
plat_mutex_lock_shared(&copy_lock);
int live = copy_find(handle) != NULL;
ReleaseSRWLockShared(&copy_lock);
plat_mutex_unlock_shared(&copy_lock);
return live;
}
uint64_t nvn_handle_bytes(uint64_t handle) {
AcquireSRWLockShared(&copy_lock);
plat_mutex_lock_shared(&copy_lock);
const copy_prefix *pf = copy_find(handle);
uint64_t n = pf ? pf->bytes : 0;
ReleaseSRWLockShared(&copy_lock);
plat_mutex_unlock_shared(&copy_lock);
return n;
}
// Marks a live copy used by submission now; its size from OP_BEGIN to OP_END inclusive.
static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
AcquireSRWLockShared(&copy_lock);
plat_mutex_lock_shared(&copy_lock);
copy_prefix *pf = copy_find(handle);
uint64_t bytes = 0;
if (pf) __atomic_store_n(&pf->used_at, now, __ATOMIC_RELAXED), bytes = pf->bytes;
ReleaseSRWLockShared(&copy_lock);
plat_mutex_unlock_shared(&copy_lock);
if (!pf) rt_fatal("nvn: %s: command handle %#llx is not a live recording (released or never recorded)", what,
(unsigned long long)handle);
return bytes;
@@ -253,7 +258,7 @@ static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) {
copy_prefix *pf = (copy_prefix *)copy - 1;
uint64_t handle = (uintptr_t)copy;
uintptr_t page = lo >> COPY_PAGE_SHIFT;
AcquireSRWLockExclusive(&copy_lock);
plat_mutex_lock(&copy_lock);
pf->made_at = nsubmits;
copy_bucket *b = nvn_hm_get(&copy_pages, &page);
if (!b) {
@@ -273,14 +278,14 @@ static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) {
if (++copies_made % 20000 == 0)
NVN_LOG("recording copies: %llu made, %llu live, %.1f MiB, submit %llu", (unsigned long long)copies_made,
(unsigned long long)ncopies, copy_bytes / 1048576.0, (unsigned long long)nsubmits);
ReleaseSRWLockExclusive(&copy_lock);
plat_mutex_unlock(&copy_lock);
}
// Adding control memory again retires every recording copy made in it (the driver's handle lived there).
static void release_copies(uintptr_t lo, uintptr_t hi) {
if (hi <= lo) return;
uint64_t replayed = gfx_replayed_submits();
AcquireSRWLockExclusive(&copy_lock);
plat_mutex_lock(&copy_lock);
if (nzombies) zombie_sweep(replayed);
for (uintptr_t pg = lo >> COPY_PAGE_SHIFT; pg <= (hi - 1) >> COPY_PAGE_SHIFT; pg++) {
copy_bucket *b = nvn_hm_get(&copy_pages, &pg);
@@ -304,7 +309,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
free(b);
}
}
ReleaseSRWLockExclusive(&copy_lock);
plat_mutex_unlock(&copy_lock);
}
// ---------- CommandBuffer: object and memory
@@ -312,6 +317,12 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
// at ~0 / 0 so NVN 55 inline code never finds room and calls the API (nvn_obj.h cmd_t)
NVN_API(nvnCommandBufferInitialize) {
cmd_t *o = cmd_of(c);
// Initialize over an object that was never finalized drops its recording buffer (seg lives in host memory). Logged
// only: the old bytes may be a copy whose buffer another object owns, so freeing it here is not safe.
static uint32_t reinits;
if (o->magic == CMD_MAGIC && o->seg && __atomic_fetch_add(&reinits, 1, __ATOMIC_RELAXED) < 8)
rt_log("nvn: Initialize over the command buffer %p, which was not finalized; its %zu-byte recording buffer %p "
"is dropped", (void *)o, (size_t)(o->end - o->seg), (void *)o->seg);
memset(o, 0, sizeof *o);
o->magic = CMD_MAGIC;
o->dev = arg_p(c, 1);
@@ -910,12 +921,12 @@ void nvn_submit_prepare(const uint64_t *handles, int n) {
void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
uint64_t now = __atomic_add_fetch(&nsubmits, 1, __ATOMIC_RELAXED);
uint64_t replayed = gfx_replayed_submits();
AcquireSRWLockExclusive(&copy_lock);
plat_mutex_lock(&copy_lock);
if (nzombies) zombie_sweep(replayed);
for (int i = 0; i < n; i++) {
copy_prefix *pf = copy_find(handles[i]);
if (!pf) {
ReleaseSRWLockExclusive(&copy_lock);
plat_mutex_unlock(&copy_lock);
rt_fatal("nvn: QueueSubmitCommands: command handle %#llx is not a live recording (released or never recorded)",
(unsigned long long)handles[i]);
}
@@ -934,7 +945,7 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
pf->used_at = now;
}
}
ReleaseSRWLockExclusive(&copy_lock);
plat_mutex_unlock(&copy_lock);
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;
+16 -16
View File
@@ -6,7 +6,6 @@
//
// Objects are stored in place in the NVN object memory the game provides (nvn_obj.h); every argument is read from the
// AAPCS64 registers (host.h arg_x / arg_p) and every result returned with ret_x.
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -16,6 +15,7 @@
#include "host.h"
#include "nvn_api.h"
#include "nvn_pool_index.h"
#include "platform/platform.h"
#define DEV_MAGIC MAGIC('D', 'E', 'V', 0)
#define QUEUE_MAGIC MAGIC('Q', 'U', 'E', 'U')
@@ -72,7 +72,7 @@ __attribute__((weak)) int gfx_replay_async(void) { return 0; }
__attribute__((weak)) uint64_t gfx_replayed_submits(void) { return nvn_submit_count(); }
// ---------- pools: GPU window (section 3.1) and the address index
static SRWLOCK pool_lock = SRWLOCK_INIT; // guards gva and index
static PlatMutex pool_lock = PLAT_MUTEX_INIT; // guards gva and index
typedef struct { uint64_t lo, hi; } Span;
enum { GVA_MAX = 1024 };
static Span gva[GVA_MAX]; // ranges handed out in the GPU window, sorted by lo
@@ -81,7 +81,7 @@ static pidx pool_index;
// First fit in [RT_GPU_VA_BASE, +RT_GPU_VA_SIZE); size is a multiple of 64 KiB, so every range stays 64 KiB aligned.
static uint64_t gva_alloc(uint64_t size) {
AcquireSRWLockExclusive(&pool_lock);
plat_mutex_lock(&pool_lock);
uint64_t at = RT_GPU_VA_BASE, r = 0;
int i = 0;
for (; i < ngva && gva[i].lo - at < size; i++) at = gva[i].hi;
@@ -91,42 +91,42 @@ static uint64_t gva_alloc(uint64_t size) {
ngva++;
r = at;
}
ReleaseSRWLockExclusive(&pool_lock);
plat_mutex_unlock(&pool_lock);
return r;
}
static void gva_free(uint64_t lo) {
AcquireSRWLockExclusive(&pool_lock);
plat_mutex_lock(&pool_lock);
for (int i = 0; i < ngva; i++)
if (gva[i].lo == lo) {
memmove(gva + i, gva + i + 1, (size_t)(ngva - i - 1) * sizeof *gva);
ngva--;
break;
}
ReleaseSRWLockExclusive(&pool_lock);
plat_mutex_unlock(&pool_lock);
}
static void index_add(const pool_t *o) {
uintptr_t lo = (uintptr_t)o->mem;
pidx_ent e = {.lo = lo, .hi = lo + o->size, .base = lo, .gpu = lo, .key = o};
AcquireSRWLockExclusive(&pool_lock);
plat_mutex_lock(&pool_lock);
pidx_remove(&pool_index, o, NULL); // a pool object initialized again without Finalize
int ok = pidx_insert(&pool_index, &e);
ReleaseSRWLockExclusive(&pool_lock);
plat_mutex_unlock(&pool_lock);
if (!ok) rt_fatal("nvn: out of memory for the pool index");
}
static void index_del(const pool_t *o) {
AcquireSRWLockExclusive(&pool_lock);
plat_mutex_lock(&pool_lock);
pidx_remove(&pool_index, o, NULL);
ReleaseSRWLockExclusive(&pool_lock);
plat_mutex_unlock(&pool_lock);
}
const pool_t *nvn_pool_find(uint64_t addr, uint64_t len) {
AcquireSRWLockShared(&pool_lock);
plat_mutex_lock_shared(&pool_lock);
const pidx_ent *e = pidx_find(&pool_index, (uintptr_t)addr, len);
const pool_t *p = e ? e->key : NULL;
ReleaseSRWLockShared(&pool_lock);
plat_mutex_unlock_shared(&pool_lock);
return p;
}
@@ -599,13 +599,13 @@ NVN_API(nvnQueueSubmitCommands) {
uint64_t f;
if (gfx_replay_async()) {
// Prepare and queue under one lock: submit numbers follow queue order (gfx_replayed_submits).
static SRWLOCK submit_lock = SRWLOCK_INIT;
static PlatMutex submit_lock = PLAT_MUTEX_INIT;
uint64_t local[32], *hs = n <= 32 ? local : malloc(sizeof *hs * (size_t)n);
if (!hs) rt_fatal("nvn: out of memory (submit of %d handles)", n);
AcquireSRWLockExclusive(&submit_lock);
plat_mutex_lock(&submit_lock);
nvn_submit_prepare_async(h, hs, n);
f = gfx_backend()->submit(hs, n);
ReleaseSRWLockExclusive(&submit_lock);
plat_mutex_unlock(&submit_lock);
if (hs != local) free(hs);
} else {
nvn_submit_prepare(h, n);
@@ -631,7 +631,7 @@ static void obs_release_read(Cpu *c) {
uint64_t e = __atomic_load_n(&lod_entered, __ATOMIC_SEQ_CST);
if (!e) return;
NVN_BLOCKING();
for (int i = 0; i < 200 && __atomic_load_n(&lod_issued, __ATOMIC_SEQ_CST) < e; i++) Sleep(1);
for (int i = 0; i < 200 && __atomic_load_n(&lod_issued, __ATOMIC_SEQ_CST) < e; i++) plat_sleep_ms(1);
uint64_t f = __atomic_load_n(&lod_fence[e % LOD_RING], __ATOMIC_RELAXED);
if (gfx_backend()->fence_done() < f) gfx_backend()->fence_wait(f, 200000000ull);
}
+10 -10
View File
@@ -7,7 +7,6 @@
// Objects are stored in place in the NVN object memory the game provides (nvn_obj.h); every argument is read from the
// AAPCS64 registers (host.h arg_x / arg_s / arg_p) and every result returned with ret_x. Command-buffer procs are in
// nvn_api_cmd.c, the TOTK extension procs in nvn_ext.c.
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -17,6 +16,7 @@
#include "nvn_api.h"
#include "nvn_hmap.h"
#include "nvn_tex.h"
#include "platform/platform.h"
// ---------- Descriptor pools
// NVNboolean nvnTexturePoolInitialize(NVNtexturePool *tp, const NVNmemoryPool *p, ptrdiff_t offset, int n)
@@ -143,7 +143,7 @@ NVN_API(nvnTextureBuilderGetStorageAlignment) {
// registering the descriptor again, as an NVN descriptor refers to memory and stays valid on hardware. The table maps
// storage address -> a copy of the live texture so the back end can redirect a registration whose serial was finalized.
static nvn_hmap live = {.klen = 8};
static SRWLOCK live_lock = SRWLOCK_INIT;
static PlatMutex live_lock = PLAT_MUTEX_INIT;
static void live_put(const tex_t *o) {
if (!o->mem) return;
@@ -151,30 +151,30 @@ static void live_put(const tex_t *o) {
tex_t *copy = malloc(sizeof *copy);
if (!copy) rt_fatal("nvn: out of memory for the live texture table");
*copy = *o;
AcquireSRWLockExclusive(&live_lock);
plat_mutex_lock(&live_lock);
tex_t *old = nvn_hm_put(&live, &k, copy);
ReleaseSRWLockExclusive(&live_lock);
plat_mutex_unlock(&live_lock);
free(old);
}
static void live_del(const tex_t *o) {
if (!o->mem) return;
uint64_t k = (uintptr_t)o->mem;
AcquireSRWLockExclusive(&live_lock);
plat_mutex_lock(&live_lock);
tex_t *cur = nvn_hm_get(&live, &k);
if (cur && cur->serial == o->serial) nvn_hm_del(&live, &k);
else cur = NULL;
ReleaseSRWLockExclusive(&live_lock);
plat_mutex_unlock(&live_lock);
free(cur);
}
int nvn_tex_live_at(const void *mem, tex_t *out) {
if (!mem) return 0;
uint64_t k = (uintptr_t)mem;
AcquireSRWLockShared(&live_lock);
plat_mutex_lock_shared(&live_lock);
const tex_t *cur = nvn_hm_get(&live, &k);
if (cur) *out = *cur;
ReleaseSRWLockShared(&live_lock);
plat_mutex_unlock_shared(&live_lock);
return cur != NULL;
}
@@ -657,8 +657,8 @@ static void sleep_until(uint64_t deadline) {
uint64_t now = kern_now_ns();
if (now >= deadline) return;
uint64_t d = deadline - now;
if (d >= 2000000) Sleep((DWORD)(d / 1000000 - 1));
else SwitchToThread();
if (d >= 2000000) plat_sleep_ms((uint32_t)(d / 1000000 - 1));
else plat_yield();
}
}
+8 -31
View File
@@ -12,7 +12,6 @@
// The 40 layout goldens themselves are run by --probe-nvn-tex against nvn_tex.c; here four of them go through the procs.
// Ported in part from MacNativeGames games/totk/tests/texture_auto_layout/probe.c @ b75be8cf (MIT, Copyright (c) 2026
// jhaizhou; origin d2r-mobile): the golden metadata, which holds no game bytes.
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -21,6 +20,7 @@
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
#include "platform/platform.h"
#define CHILD_ENV "TOTK_PROBE_NVN_CHILD"
@@ -835,33 +835,10 @@ static void probe_proc_table(void) {
}
// ---- named fatal stubs, each in a child process
static DWORD run_child(const char *mode, char *out, size_t cap) {
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
HANDLE rd, wr;
if (!CreatePipe(&rd, &wr, &sa, 0)) rt_fatal("probe-nvn: CreatePipe (error %lu)", GetLastError());
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdOutput = wr, .hStdError = wr};
PROCESS_INFORMATION pi;
wchar_t *cmdline = _wcsdup(GetCommandLineW());
SetEnvironmentVariableA(CHILD_ENV, mode);
BOOL ok = CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
SetEnvironmentVariableA(CHILD_ENV, NULL);
free(cmdline);
CloseHandle(wr);
if (!ok) rt_fatal("probe-nvn: CreateProcess for the %s child (error %lu)", mode, GetLastError());
size_t n = 0;
DWORD got;
while (n + 1 < cap && ReadFile(rd, out + n, (DWORD)(cap - 1 - n), &got, NULL) && got) n += got;
out[n] = 0;
char sink[512];
while (ReadFile(rd, sink, sizeof sink, &got, NULL) && got) {}
CloseHandle(rd);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return code;
static uint32_t run_child(const char *mode, char *out, size_t cap) {
int code = plat_run_self(CHILD_ENV, mode, out, cap, 0);
if (code < 0) rt_fatal("probe-nvn: cannot start the %s child", mode);
return (uint32_t)code;
}
static int run_as_child(const char *name) {
@@ -877,13 +854,13 @@ static int run_as_child(const char *name) {
static void probe_children(void) {
static char out[16384];
for (int i = 0; i < NFATAL; i++) {
DWORD code = run_child(fatal_procs[i], out, sizeof out);
uint32_t code = run_child(fatal_procs[i], out, sizeof out);
char want[160];
snprintf(want, sizeof want, "NVN %s is not implemented", fatal_procs[i]);
CHECK(code == 70 && strstr(out, "FATAL: ") && strstr(out, want), "child %s: exit %lu, output: %s", fatal_procs[i], code, out);
CHECK(code == 70 && strstr(out, "FATAL: ") && strstr(out, want), "child %s: exit %u, output: %s", fatal_procs[i], code, out);
const char *line = strstr(out, "FATAL: ");
int len = line ? (int)strcspn(line, "\r\n") : 0;
printf("fatal stub %-30s child exit %lu: %.*s\n", fatal_procs[i], code, len, line ? line : "(no FATAL line)");
printf("fatal stub %-30s child exit %u: %.*s\n", fatal_procs[i], code, len, line ? line : "(no FATAL line)");
}
}
+4 -4
View File
@@ -5,10 +5,10 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include "gfx.h"
#include "host.h"
#include "platform/platform.h"
// The strong nvnp_nvnDeviceGetProcAddress below replaces the stand-in generated for the same row.
#define nvnp_nvnDeviceGetProcAddress nvnp_stub_nvnDeviceGetProcAddress
@@ -53,10 +53,10 @@ uint64_t nvn_proc_address(const char *name) {
const Proc *p = bsearch(&(Proc){name, 0}, procs, (size_t)nprocs, sizeof *procs, by_name);
if (p) return p->addr;
// Unknown names get NULL, as on hardware; each distinct one is logged once.
static SRWLOCK lock = SRWLOCK_INIT;
static PlatMutex lock = PLAT_MUTEX_INIT;
static char seen[32][96];
static int nseen;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
int fresh = 1;
for (int i = 0; i < nseen; i++)
if (!strncmp(seen[i], name, sizeof seen[i] - 1)) fresh = 0;
@@ -66,7 +66,7 @@ uint64_t nvn_proc_address(const char *name) {
} else {
fresh = 0;
}
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
if (fresh) rt_log("nvn: no such proc \"%s\", returning NULL", name);
return 0;
}
+12 -12
View File
@@ -5,24 +5,24 @@
// nv::* record the game's allocator callbacks and graphics work memory (guest addresses) for the NVN layer
// nn::vi::* display, layer and native window identities; the vsync event is bound here
// nvnBootstrapLoader the end of --boot (rt_boot_event "nvn"); otherwise the synthetic address of an NVN proc (nvn_procs.c)
#include <windows.h>
#include <stdint.h>
#include "gfx.h"
#include "host.h"
#include "ov.h"
#include "platform/platform.h"
#define VI_R_INVALID 0xf601u // MacNativeGames' result for a call on an unknown display or layer
static OvNvState nv;
static SRWLOCK lock = SRWLOCK_INIT;
static PlatMutex lock = PLAT_MUTEX_INIT;
const OvNvState *ov_nv_state(void) { return &nv; }
static void set_allocator(Cpu *c, OvNvAllocator *a) {
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
*a = (OvNvAllocator){arg_x(c, 0), arg_x(c, 1), arg_x(c, 2), arg_x(c, 3)};
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// void nv::SetGraphicsAllocator(AllocateFunction, FreeFunction, ReallocateFunction, void *user)
@@ -35,11 +35,11 @@ static void ov_initialize_graphics(Cpu *c) {
uint64_t mem = arg_x(c, 0), size = arg_x(c, 1);
if (!mem || !size) rt_fatal("nv::InitializeGraphics: no graphics work memory (%#llx, %#llx)", (unsigned long long)mem,
(unsigned long long)size);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
nv.memory = mem;
nv.memory_size = size;
nv.initialized = 1;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
rt_log("nv: graphics work memory %#llx+%#llx recorded", (unsigned long long)mem,
(unsigned long long)size);
}
@@ -51,20 +51,20 @@ static void *display, *layer, *window;
// void nn::vi::Initialize()
static void ov_vi_initialize(Cpu *c) {
(void)c;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
if (!display) {
display = mem_kernel_alloc(64, 16);
layer = mem_kernel_alloc(64, 16);
window = mem_kernel_alloc(64, 16);
}
vi_ready = 1;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
static int vi_up(void) {
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
int r = vi_ready;
ReleaseSRWLockShared(&lock);
plat_mutex_unlock_shared(&lock);
return r;
}
@@ -105,8 +105,8 @@ static void ov_vsync_event(Cpu *c) {
static void ov_nvn_bootstrap(Cpu *c) {
const char *name = arg_p(c, 0);
if (!name) name = "(null)";
static LONG first = 1;
if (rt_config()->boot && InterlockedExchange(&first, 0)) rt_boot_event("nvn", c, name); // ends --boot; returns
static int32_t first = 1;
if (rt_config()->boot && plat_xchg32(&first, 0)) rt_boot_event("nvn", c, name); // ends --boot; returns
// when --seconds goes on
ret_x(c, nvn_proc_address(name));
}
+48 -48
View File
@@ -6,7 +6,6 @@
// which is the controller the game reads in that mode. Mouse and keyboard report no input. The controller has two
// vibration devices (left, right) played on the gamepad's motors (TOTK_RUMBLE=0: none) and a six-axis sensor fed by
// the gamepad's gyro and accelerometer (TOTK_MOTION=0, live: reads at rest). State layouts follow libnx HID.
#include <windows.h>
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
@@ -16,6 +15,7 @@
#include "host.h"
#include "input.h"
#include "ov.h"
#include "platform/platform.h"
typedef struct { int64_t sampling; uint64_t buttons; int32_t lx, ly, rx, ry; uint32_t attributes, pad; } NpadState;
typedef struct { int64_t sampling; int32_t x, y, dx, dy, wx, wy; uint32_t buttons, attributes; } MouseState;
@@ -26,7 +26,7 @@ _Static_assert(sizeof(ControllerSupportResult) == 8, "controller support result
enum { STYLE_FULL_KEY = 1, STYLE_HANDHELD = 2, ATTR_CONNECTED = 1 };
static SRWLOCK lock = SRWLOCK_INIT;
static PlatMutex lock = PLAT_MUTEX_INIT;
static uint32_t styles = STYLE_FULL_KEY;
static int enabled, allowed = 1, mouse_ready, keyboard_ready;
static int64_t sample;
@@ -43,9 +43,9 @@ static const uint32_t *need_id(Cpu *c, int i, const char *fn) {
// void InitializeNpad()
static void ov_initialize_npad(Cpu *c) {
(void)c;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
enabled = 1;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
rt_log("hid: controllers initialized (one connected %s controller)",
ov_handheld() ? "handheld" : "full-key");
}
@@ -57,24 +57,24 @@ static void ov_supported_ids(Cpu *c) {
if (n > 10 || (!ids && n)) rt_fatal("nn::hid::SetSupportedNpadIdType: invalid id list (%llu ids)", (unsigned long long)n);
int ok = 0;
for (uint64_t i = 0; i < n; i++) ok |= ids[i] == pad_id();
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
allowed = ok;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// void SetSupportedNpadStyleSet(NpadStyleSet styles)
static void ov_supported_styles(Cpu *c) {
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
styles = (uint32_t)arg_x(c, 0);
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// NpadStyleSet GetNpadStyleSet(const NpadIdType &id)
static void ov_style_set(Cpu *c) {
uint32_t id = *need_id(c, 0, "GetNpadStyleSet");
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
uint32_t v = enabled && allowed && id == pad_id() ? styles & pad_style() : 0;
ReleaseSRWLockShared(&lock);
plat_mutex_unlock_shared(&lock);
ret_x(c, v);
}
@@ -86,10 +86,10 @@ static void npad_states(Cpu *c, uint32_t want) {
uint32_t id = *need_id(c, 2, "GetNpadStates");
if (max <= 0) { ret_x(c, 0); return; }
if (!out) rt_fatal("nn::hid::GetNpadStates: null output");
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
NpadState s = {.sampling = ++sample};
int live = want && enabled && allowed && id == pad_id() && want == pad_style() && (styles & want);
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
if (live) {
s.attributes = ATTR_CONNECTED;
input_poll(&s.buttons, &s.lx, &s.ly, &s.rx, &s.ry);
@@ -111,9 +111,9 @@ static void ov_hold_type(Cpu *c) {
static void ov_disconnect(Cpu *c) {
uint32_t id = *need_id(c, 0, "DisconnectNpad");
if (id != pad_id()) return;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
allowed = 0;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// Result ShowControllerSupport(ControllerSupportResultInfo *out, const ControllerSupportArg &arg): the one controller
@@ -126,16 +126,16 @@ static void ov_controller_support(Cpu *c) {
static void ov_initialize_mouse(Cpu *c) {
(void)c;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
mouse_ready = 1;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
static void ov_initialize_keyboard(Cpu *c) {
(void)c;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
keyboard_ready = 1;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// int GetMouseStates(MouseState *out, int count): a mouse that is not connected
@@ -144,10 +144,10 @@ static void ov_mouse_states(Cpu *c) {
int max = (int)arg_x(c, 1);
if (max <= 0) { ret_x(c, 0); return; }
if (!out) rt_fatal("nn::hid::GetMouseStates: null output");
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
if (!mouse_ready) rt_fatal("nn::hid::GetMouseStates before InitializeMouse");
*out = (MouseState){.sampling = ++sample};
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
ret_x(c, 1);
}
@@ -155,10 +155,10 @@ static void ov_mouse_states(Cpu *c) {
static void ov_keyboard_state(Cpu *c) {
KeyboardState *out = arg_p(c, 0);
if (!out) rt_fatal("nn::hid::GetKeyboardState: null output");
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
if (!keyboard_ready) rt_fatal("nn::hid::GetKeyboardState before InitializeKeyboard");
*out = (KeyboardState){.sampling = ++sample};
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// ---- vibration and six-axis sensor (MacNativeGames hid_feedback.c)
@@ -182,8 +182,8 @@ static int env_zero(const char *name) {
return e && !strcmp(e, "0");
}
static void once(volatile long *flag, const char *fmt, uint32_t v) {
if (!InterlockedExchange(flag, 1)) rt_log(fmt, v);
static void once(volatile int32_t *flag, const char *fmt, uint32_t v) {
if (!plat_xchg32(flag, 1)) rt_log(fmt, v);
}
// TOTK_RUMBLE_STRENGTH (percent, 0..300, default 130; 0 = off) and TOTK_RUMBLE_CURVE (exponent on the amplitude, 0.2..1.5,
@@ -191,7 +191,7 @@ static void once(volatile long *flag, const char *fmt, uint32_t v) {
// curve (0.1 -> 0.25) and the strength scales the result (saturating at 65535; 0.66 and up reach full drive at 130 %).
// Both are live settings (the menu): read again whenever cfg_version changes.
static float rumble_strength = 1.3f, rumble_curve = 0.6f;
static volatile long rumble_cfg_logged;
static volatile int32_t rumble_cfg_logged;
static unsigned rumble_cfg_seen = ~0u;
static float cfg_float(const char *name, float lo, float hi) {
double v = cfg_double(name);
@@ -204,7 +204,7 @@ static void rumble_cfg(void) {
rumble_strength = cfg_float("TOTK_RUMBLE_STRENGTH", 0, 300) / 100;
rumble_curve = cfg_float("TOTK_RUMBLE_CURVE", 0.2f, 1.5f);
int trig = input_rumble_triggers_enabled();
if (!InterlockedExchange(&rumble_cfg_logged, 1))
if (!plat_xchg32(&rumble_cfg_logged, 1))
rt_log("hid: rumble strength %.0f%%, curve %.2f, triggers %s", rumble_strength * 100, rumble_curve, trig ? "on" : "off");
}
static uint16_t motor(float a) { return rumble_motor(a, rumble_curve, rumble_strength); }
@@ -227,7 +227,7 @@ static void ov_vibration_handles(Cpu *c) {
uint32_t id = handle_id(c, "GetVibrationDeviceHandles");
if (!out || max <= 0 || __builtin_popcount(style) != 1)
rt_fatal("nn::hid::GetVibrationDeviceHandles: invalid arguments (count %d, styles %#x)", max, style);
static volatile long asked;
static volatile int32_t asked;
rumble_cfg();
// strength 0 keeps the devices (silent), so the menu can turn rumble up again
if (env_zero("TOTK_RUMBLE")) { once(&asked, "hid: no vibration devices (TOTK_RUMBLE=%u)", 0); ret_x(c, 0); return; }
@@ -252,7 +252,7 @@ static void ov_six_axis_handles(Cpu *c) {
int max = (int)arg_x(c, 1);
uint32_t id = handle_id(c, "GetSixAxisSensorHandles");
if (!out || max <= 0) rt_fatal("nn::hid::GetSixAxisSensorHandles: invalid arguments (count %d)", max);
static volatile long asked;
static volatile int32_t asked;
uint32_t base = 0, n = 1;
if (style & 1) base = 0x20003;
else if (style & 2) base = 0x20004;
@@ -279,12 +279,12 @@ static const uint32_t *need_handle(const void *p, const char *fn) {
static int vibration_side(const uint32_t *h, const char *fn) {
uint32_t v = *need_handle(h, fn), dev = v >> 16 & 0xff;
if ((v & 0xff) == pad_style_index() && (v >> 8 & 0xff) == pad_id() && dev < 2) return (int)dev;
static volatile long warned;
static volatile int32_t warned;
once(&warned, "hid: vibration call for handle %#x, not a device of the connected controller: ignored", v);
return -1;
}
static SRWLOCK vib_lock = SRWLOCK_INIT;
static PlatMutex vib_lock = PLAT_MUTEX_INIT;
static VibrationValue vib_last[2] = {{0, 160, 0, 320}, {0, 160, 0, 320}}; // vib_lock
static uint64_t vib_at[2], sent_at; // vib_lock
static uint16_t sent_low, sent_high; // vib_lock
@@ -323,24 +323,24 @@ static void play(int side, const VibrationValue *v) {
if (side < 0) return;
VibrationValue c = *v;
if (!isfinite(c.amp_low) || !isfinite(c.amp_high) || !isfinite(c.freq_low) || !isfinite(c.freq_high)) c = vib_neutral;
static volatile long first, first_on;
if (!InterlockedExchange(&first, 1))
static volatile int32_t first, first_on;
if (!plat_xchg32(&first, 1))
rt_log("hid: first vibration value from the game: %s %.2f@%.0f Hz %.2f@%.0f Hz", side ? "right" : "left",
(double)c.amp_low, (double)c.freq_low, (double)c.amp_high, (double)c.freq_high);
if ((c.amp_low > 0.01f || c.amp_high > 0.01f) && !InterlockedExchange(&first_on, 1))
if ((c.amp_low > 0.01f || c.amp_high > 0.01f) && !plat_xchg32(&first_on, 1))
rt_log("hid: first non-zero vibration from the game: %s %.2f@%.0f Hz %.2f@%.0f Hz", side ? "right" : "left",
(double)c.amp_low, (double)c.freq_low, (double)c.amp_high, (double)c.freq_high);
uint64_t now = kern_now_ns();
AcquireSRWLockExclusive(&vib_lock);
plat_mutex_lock(&vib_lock);
vib_last[side] = c;
vib_at[side] = now;
rumble_update(now);
ReleaseSRWLockExclusive(&vib_lock);
plat_mutex_unlock(&vib_lock);
}
// Result detail::InitializeVibrationDevice(const VibrationDeviceHandle &)
static void ov_vib_init(Cpu *c) {
static volatile long logged;
static volatile int32_t logged;
const uint32_t *h = arg_p(c, 0);
if (vibration_side(h, "InitializeVibrationDevice") >= 0)
once(&logged, "hid: the game initialized its vibration devices (handle %#x)", *h);
@@ -369,9 +369,9 @@ static void ov_vib_actual(Cpu *c) {
if (!out) rt_fatal("nn::hid::detail::GetActualVibrationValue: null output");
int side = vibration_side(arg_p(c, 1), "GetActualVibrationValue");
VibrationValue v = vib_neutral;
AcquireSRWLockShared(&vib_lock);
plat_mutex_lock_shared(&vib_lock);
if (side >= 0) v = vib_last[side];
ReleaseSRWLockShared(&vib_lock);
plat_mutex_unlock_shared(&vib_lock);
*out = v;
ret_x(c, 0);
}
@@ -397,28 +397,28 @@ static void ov_vib_mounted(Cpu *c) {
static void ov_vib_permitted(Cpu *c) {
uint8_t *out = arg_p(c, 0);
if (!out) rt_fatal("nn::hid::detail::IsVibrationPermitted: null output");
AcquireSRWLockShared(&vib_lock);
plat_mutex_lock_shared(&vib_lock);
*out = (uint8_t)vib_allowed;
ReleaseSRWLockShared(&vib_lock);
plat_mutex_unlock_shared(&vib_lock);
ret_x(c, 0);
}
// Result detail::PermitVibration(bool): off silences the gamepad at once
static void ov_vib_permit(Cpu *c) {
int on = (arg_x(c, 0) & 0xff) != 0;
AcquireSRWLockExclusive(&vib_lock);
plat_mutex_lock(&vib_lock);
vib_allowed = on;
rumble_update(kern_now_ns());
ReleaseSRWLockExclusive(&vib_lock);
plat_mutex_unlock(&vib_lock);
ret_x(c, 0);
}
static volatile long six_running; // StartSixAxisSensor .. StopSixAxisSensor
static volatile int32_t six_running; // StartSixAxisSensor .. StopSixAxisSensor
static int six_axis_ours(const uint32_t *h, const char *fn) {
uint32_t v = *need_handle(h, fn);
if ((v & 0xff) == pad_style_index() && (v >> 8 & 0xff) == pad_id() && (v >> 16 & 0xff) == SIX_AXIS_DEVICE) return 1;
static volatile long warned;
static volatile int32_t warned;
once(&warned, "hid: six-axis call for handle %#x, not the connected controller's sensor: reads at rest", v);
return 0;
}
@@ -427,18 +427,18 @@ static int six_axis_ours(const uint32_t *h, const char *fn) {
static void ov_six_start(Cpu *c) {
const uint32_t *h = arg_p(c, 0);
if (six_axis_ours(h, "StartSixAxisSensor")) {
static volatile long logged;
static volatile int32_t logged;
once(&logged, "hid: the game started the six-axis sensor (handle %#x)", *h);
motion_restart(motion_global());
input_motion_start();
InterlockedExchange(&six_running, 1);
plat_xchg32(&six_running, 1);
}
ret_x(c, 0);
}
// Result detail::StopSixAxisSensor(const SixAxisSensorHandle &)
static void ov_six_stop(Cpu *c) {
if (six_axis_ours(arg_p(c, 0), "StopSixAxisSensor")) InterlockedExchange(&six_running, 0);
if (six_axis_ours(arg_p(c, 0), "StopSixAxisSensor")) plat_xchg32(&six_running, 0);
ret_x(c, 0);
}
@@ -455,7 +455,7 @@ static int six_fill(MotionState *s, int max, const uint32_t *h, const char *fn)
for (int i = 0; i < max; i++) s[i] = rest;
} else
for (int i = n; i < max; i++) s[i] = s[n - 1];
static volatile long first_rest, first_live;
static volatile int32_t first_rest, first_live;
if (!n && six_running) once(&first_rest, "hid: six-axis read with no motion samples: at rest (%u states)", (uint32_t)max);
if (n) once(&first_live, "hid: first six-axis read with live motion (%u samples)", (uint32_t)n);
return max;
+18 -18
View File
@@ -2,7 +2,6 @@
// contributors).
// The hardware Opus decoder (nn::codec), decoded with libopus on the host. The software keyboard (nn::swkbd) is
// in swkbd_win.c.
#include <windows.h>
#include <stdint.h>
#include <limits.h>
#include <stdlib.h>
@@ -10,6 +9,7 @@
#include <opus/opus.h>
#include "host.h"
#include "platform/platform.h"
#define CODEC_R_INVALID 0x1fcd1u // MacNativeGames' "invalid argument" result for nn::codec
#define OPUS_WORK_SIZE 0x9000u // what the game is told to reserve: libopus stereo decoder state (about 26 KiB) plus 4 KiB
@@ -22,11 +22,11 @@ typedef struct Decoder {
uint64_t guest;
OpusDecoder *dec;
int rate, channels;
SRWLOCK use;
PlatMutex use;
struct Decoder *next;
} Decoder;
static Decoder *decoders;
static SRWLOCK lock = SRWLOCK_INIT;
static PlatMutex lock = PLAT_MUTEX_INIT;
static int valid_format(int rate, int channels) {
return (rate == 48000 || rate == 24000 || rate == 16000 || rate == 12000 || rate == 8000) &&
@@ -35,16 +35,16 @@ static int valid_format(int rate, int channels) {
// Returns the decoder with its use lock held.
static Decoder *acquire(uint64_t guest, const char *fn) {
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
Decoder *d = decoders;
while (d && d->guest != guest) d = d->next;
if (d) AcquireSRWLockExclusive(&d->use);
ReleaseSRWLockShared(&lock);
if (d) plat_mutex_lock(&d->use);
plat_mutex_unlock_shared(&lock);
if (!d) rt_fatal("nn::codec::%s: unknown hardware Opus decoder %#llx", fn, (unsigned long long)guest);
return d;
}
static void release(Decoder *d) { ReleaseSRWLockExclusive(&d->use); }
static void release(Decoder *d) { plat_mutex_unlock(&d->use); }
static OpusDecoder *opus_new(int rate, int channels) {
int err = 0;
@@ -66,29 +66,29 @@ static void ov_initialize(Cpu *c) {
int rate = (int)arg_x(c, 1), channels = (int)arg_x(c, 2);
if (!guest || !valid_format(rate, channels) || size < OPUS_WORK_SIZE) { ret_x(c, CODEC_R_INVALID); return; }
OpusDecoder *fresh = opus_new(rate, channels);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
Decoder *d = decoders;
while (d && d->guest != guest) d = d->next;
if (d) {
AcquireSRWLockExclusive(&d->use); // waits for a decode in flight
plat_mutex_lock(&d->use); // waits for a decode in flight
OpusDecoder *old = d->dec;
d->dec = fresh;
d->rate = rate;
d->channels = channels;
ReleaseSRWLockExclusive(&d->use);
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&d->use);
plat_mutex_unlock(&lock);
opus_decoder_destroy(old);
} else {
d = calloc(1, sizeof *d);
if (!d) rt_fatal("nn::codec: decoder allocation");
InitializeSRWLock(&d->use);
plat_mutex_init(&d->use);
d->guest = guest;
d->dec = fresh;
d->rate = rate;
d->channels = channels;
d->next = decoders;
decoders = d;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
ret_x(c, 0);
}
@@ -96,14 +96,14 @@ static void ov_initialize(Cpu *c) {
// void FinalizeHardwareOpusDecoder(HardwareOpusDecoderType *d)
static void ov_finalize(Cpu *c) {
uint64_t guest = arg_x(c, 0);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
Decoder **p = &decoders;
while (*p && (*p)->guest != guest) p = &(*p)->next;
if (!*p) rt_fatal("nn::codec::FinalizeHardwareOpusDecoder: unknown decoder %#llx", (unsigned long long)guest);
Decoder *d = *p;
AcquireSRWLockExclusive(&d->use); // a decode in flight finishes first; the registry lock keeps new ones out
plat_mutex_lock(&d->use); // a decode in flight finishes first; the registry lock keeps new ones out
*p = d->next;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
opus_decoder_destroy(d->dec);
free(d);
}
@@ -138,7 +138,7 @@ static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t
// {payload size, final range} and the Opus packet. A header range of 0 is not a recorded value (some encoders write 0
// into every packet), so it is not checked. A range mismatch is logged once and the samples are kept.
static void ov_decode(Cpu *c) {
static LONG warned;
static int32_t warned;
uint64_t guest = arg_x(c, 0), out_size = arg_x(c, 4), in_size = arg_x(c, 6);
size_t *consumed = arg_p(c, 1);
int *samples = arg_p(c, 2);
@@ -159,7 +159,7 @@ static void ov_decode(Cpu *c) {
ret_x(c, CODEC_R_INVALID);
return;
}
if (n && range && final != range && !InterlockedExchange(&warned, 1))
if (n && range && final != range && !plat_xchg32(&warned, 1))
rt_log("nn::codec: Opus final range %#x != stream %#x (logged once)", (unsigned)final, (unsigned)range);
*consumed = 8 + n;
*samples = got;
+81 -82
View File
@@ -23,7 +23,6 @@
// periods late is dropped if the next one is due too, so the picture keeps time with the movie's sound.
// TOTK_MOVIE_PACE=0 hands every decoded frame over at the next present; TOTK_MOVIE_HOST=0 leaves every decoder to the
// library (which aborts at the first movie).
#include <windows.h>
#include <emmintrin.h>
#include <stdint.h>
#include <stdlib.h>
@@ -35,6 +34,7 @@
#include "cfg.h"
#include "host.h"
#include "movie.h"
#include "platform/platform.h"
enum { IN_BUFS = 4, IN_CAP = 2 << 20, OUT_BUFS = 8, MAX_MOVIES = 16, VPX_THREADS = 4 };
enum { ST_OK = 0, ST_ERROR = 0x1000, ST_NULL = 0x100b, ST_NO_OUTPUT = 0x100e, ST_NO_INPUT = 0x1011,
@@ -74,19 +74,19 @@ typedef struct {
uint64_t t0; // host time of the first frame handed over (pacing)
int64_t pts0; // its presentation time (us)
MovieStats st;
SRWLOCK mu;
CONDITION_VARIABLE work, idle;
HANDLE thread;
PlatMutex mu;
PlatCond work, idle;
PlatThread *thread;
int busy, quit;
uint64_t gen; // bumped by Flush, Stop, Reset, destruction: a decode in flight is dropped
} Movie;
static MovieGuest G;
static volatile LONG g_ready;
static SRWLOCK g_init = SRWLOCK_INIT;
static volatile int32_t g_ready;
static PlatMutex g_init = PLAT_MUTEX_INIT;
static Movie *g_movies[MAX_MOVIES];
static volatile LONG g_count; // live host decoders: movie_tick returns at once when 0
static SRWLOCK g_lock = SRWLOCK_INIT; // lock order: g_lock, then Movie.mu
static volatile int32_t g_count; // live host decoders: movie_tick returns at once when 0
static PlatMutex g_lock = PLAT_MUTEX_INIT; // lock order: g_lock, then Movie.mu
static uint64_t (*g_clock)(void) = kern_now_ns;
static int g_pace = -1;
@@ -117,7 +117,7 @@ static uint64_t need(const char *name) {
// The guest functions, resolved the first time any entry runs (the modules are loaded and relocated by then).
static void guest_init(void) {
if (g_ready) return;
AcquireSRWLockExclusive(&g_init);
plat_mutex_lock(&g_init);
if (!g_ready) {
G.signal_event = need("_ZN2nn2os11SignalEventEPNS0_9EventTypeE");
G.buf_set_range = need("_ZN5movie6Buffer8SetRangeEii");
@@ -131,16 +131,16 @@ static void guest_init(void) {
G.md_set_string = need("_ZN5movie9MediaData9SetStringEPKcS2_");
G.vptr = need("_ZTVN5movie7DecoderE") + 0x10;
for (int i = 0; i < MOVIE_ENTRIES; i++) G.orig[i] = need(movie_symbols[i]);
InterlockedExchange(&g_ready, 1);
plat_xchg32(&g_ready, 1);
}
ReleaseSRWLockExclusive(&g_init);
plat_mutex_unlock(&g_init);
}
void movie_set_guest(const MovieGuest *g) {
AcquireSRWLockExclusive(&g_init);
plat_mutex_lock(&g_init);
G = *g;
InterlockedExchange(&g_ready, 1);
ReleaseSRWLockExclusive(&g_init);
plat_xchg32(&g_ready, 1);
plat_mutex_unlock(&g_init);
}
#define U(x) ((uint64_t)(uintptr_t)(x))
@@ -161,10 +161,10 @@ static void pass(Cpu *c, int which) {
static Movie *find(uint64_t guest) {
Movie *m = NULL;
AcquireSRWLockShared(&g_lock);
plat_mutex_lock_shared(&g_lock);
for (int i = 0; i < MAX_MOVIES && !m; i++)
if (g_movies[i] && g_movies[i]->guest == guest) m = g_movies[i];
ReleaseSRWLockShared(&g_lock);
plat_mutex_unlock_shared(&g_lock);
return m;
}
@@ -236,12 +236,12 @@ static int vpx_open(Movie *m) {
}
// The worker: takes inputs in order, decodes them outside the lock into a reserved output slot, publishes the frame.
static DWORD WINAPI worker(void *arg) {
static void worker(void *arg) {
Movie *m = arg;
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
for (;;) {
while (!m->quit && !(m->started && m->vpx_live && m->npending && can_take(m, &m->pending[0])))
SleepConditionVariableSRW(&m->work, &m->mu, INFINITE, 0);
plat_cond_wait(&m->work, &m->mu);
if (m->quit) break;
Pending p = m->pending[0];
memmove(m->pending, m->pending + 1, (size_t)--m->npending * sizeof *m->pending);
@@ -256,7 +256,7 @@ static DWORD WINAPI worker(void *arg) {
vpx_codec_err_t e = VPX_CODEC_OK;
const char *bad = NULL;
m->busy = 1;
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
if (p.size > 0) {
uint64_t t0 = kern_now_ns();
e = vpx_codec_decode(&m->vpx, m->in[p.index] + p.offset, (unsigned)p.size, NULL, 0);
@@ -271,9 +271,9 @@ static DWORD WINAPI worker(void *arg) {
}
dt = kern_now_ns() - t0;
}
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
m->busy = 0;
WakeAllConditionVariable(&m->idle);
plat_cond_broadcast(&m->idle);
if (m->gen != gen) continue; // flushed, stopped or reset meanwhile: the queues were reset after this
m->in_free[m->nin_free++] = p.index;
m->owed |= 1u << MOVIE_EV_INPUT;
@@ -314,8 +314,8 @@ static DWORD WINAPI worker(void *arg) {
m->st.max_ns / 1e6, (unsigned long long)m->st.errors);
}
}
ReleaseSRWLockExclusive(&m->mu);
return 0;
plat_mutex_unlock(&m->mu);
return;
}
// With m->mu held: invalidate a decode in flight and wait for the worker to leave it. Returns whether the calling guest
@@ -325,12 +325,12 @@ static int quiesce(Movie *m) {
m->gen++;
while (m->busy) {
if (!away) away = kern_core_away();
SleepConditionVariableSRW(&m->idle, &m->mu, INFINITE, 0);
plat_cond_wait(&m->idle, &m->mu);
}
return away;
}
static void unlock_back(Movie *m, int away) {
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
kern_core_back(&away);
}
@@ -383,7 +383,7 @@ static void release_due(Movie *m, uint64_t now) {
m->out_held[m->decoded[0].index] = 0;
m->st.dropped++;
memmove(m->decoded, m->decoded + 1, (size_t)--m->ndecoded * sizeof *m->decoded);
WakeConditionVariable(&m->work);
plat_cond_signal(&m->work);
}
Ready d = m->decoded[0];
if (!(d.flags & MOVIE_FLAG_EOS)) {
@@ -402,34 +402,34 @@ static void release_due(Movie *m, uint64_t now) {
void movie_tick(Cpu *c) {
if (!g_count) return;
Signals s = {0};
AcquireSRWLockShared(&g_lock);
plat_mutex_lock_shared(&g_lock);
for (int i = 0; i < MAX_MOVIES; i++) {
Movie *m = g_movies[i];
if (!m) continue;
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
if (m->started) release_due(m, g_clock());
collect(m, &s);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
}
ReleaseSRWLockShared(&g_lock);
plat_mutex_unlock_shared(&g_lock);
fire(c, &s);
}
int movie_stats(uint64_t obj, MovieStats *out) {
Movie *m = find(obj);
if (!m) return 0;
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
*out = m->st;
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
return 1;
}
int movie_quiet(uint64_t obj) {
Movie *m = find(obj);
if (!m) return 1;
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
int q = !m->busy && !(m->started && m->vpx_live && m->npending && can_take(m, &m->pending[0]));
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
return q;
}
@@ -449,19 +449,19 @@ static void ov_ctor(Cpu *c) {
if (!m) rt_fatal("movie: decoder allocation");
m->guest = obj;
m->format = format;
InitializeSRWLock(&m->mu);
InitializeConditionVariable(&m->work);
InitializeConditionVariable(&m->idle);
m->thread = CreateThread(NULL, 0, worker, m, 0, NULL);
if (!m->thread) rt_fatal("movie: worker thread (error %lu)", GetLastError());
AcquireSRWLockExclusive(&g_lock);
plat_mutex_init(&m->mu);
plat_cond_init(&m->work);
plat_cond_init(&m->idle);
m->thread = plat_thread_new(worker, m, "movie");
if (!m->thread) rt_fatal("movie: cannot start the worker thread");
plat_mutex_lock(&g_lock);
int i = 0;
while (i < MAX_MOVIES && g_movies[i] && g_movies[i]->guest != obj) i++;
if (i == MAX_MOVIES) rt_fatal("movie: more than %d live decoders", MAX_MOVIES);
if (g_movies[i]) rt_fatal("movie: decoder object %#llx constructed twice", (unsigned long long)obj);
g_movies[i] = m;
InterlockedIncrement(&g_count);
ReleaseSRWLockExclusive(&g_lock);
plat_inc32(&g_count);
plat_mutex_unlock(&g_lock);
ST64(obj, G.vptr);
ST64(obj + 8, U(m));
rt_log("movie: VP9 decoder %#llx on the host (libvpx %s), output format %d", (unsigned long long)obj,
@@ -471,24 +471,23 @@ static void ov_ctor(Cpu *c) {
static void destroy(Cpu *c, uint64_t obj) {
(void)c;
Movie *m = NULL;
AcquireSRWLockExclusive(&g_lock);
plat_mutex_lock(&g_lock);
for (int i = 0; i < MAX_MOVIES; i++)
if (g_movies[i] && g_movies[i]->guest == obj) {
m = g_movies[i];
g_movies[i] = NULL;
InterlockedDecrement(&g_count);
plat_dec32(&g_count);
}
ReleaseSRWLockExclusive(&g_lock);
plat_mutex_unlock(&g_lock);
if (!m) return;
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
m->quit = 1;
m->gen++;
WakeAllConditionVariable(&m->work);
ReleaseSRWLockExclusive(&m->mu);
plat_cond_broadcast(&m->work);
plat_mutex_unlock(&m->mu);
int away = kern_core_away();
WaitForSingleObject(m->thread, INFINITE);
plat_thread_join(m->thread);
kern_core_back(&away);
CloseHandle(m->thread);
rt_log("movie: decoder %#llx destroyed after %llu frames (%llu dropped to keep time)", (unsigned long long)obj,
(unsigned long long)m->st.frames, (unsigned long long)m->st.dropped);
release_codec(m);
@@ -531,7 +530,7 @@ static void ov_configure(Cpu *c) {
ret_x(c, ST_UNSUPPORTED);
return;
}
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int away = quiesce(m);
release_codec(m);
m->w = w;
@@ -554,9 +553,9 @@ static void ov_configure(Cpu *c) {
static void ov_register_events(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_REGISTER_EVENTS); return; }
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
m->events = arg_x(c, 1);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
ret_x(c, ST_OK);
}
@@ -583,9 +582,9 @@ static void ov_get_output_format(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT_FORMAT); return; }
if (!md) { ret_x(c, ST_NULL); return; }
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
int ok = m->configured, w = m->w, h = m->h;
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
if (!ok) { ret_x(c, ST_ERROR); return; }
set_format(c, w, h, md);
ret_x(c, ST_OK);
@@ -595,16 +594,16 @@ static void ov_start(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_START); return; }
Signals s = {0};
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int st = ST_NOT_CONFIGURED;
if (m->configured) {
m->started = 1;
m->owed |= 1u << MOVIE_EV_INPUT;
WakeConditionVariable(&m->work);
plat_cond_signal(&m->work);
st = ST_OK;
}
collect(m, &s);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
@@ -612,7 +611,7 @@ static void ov_start(Cpu *c) {
static void ov_stop(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_STOP); return; }
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int away = quiesce(m);
rt_log("movie: stop after %llu frames (%llu dropped to keep time; decode %.2f ms per frame, max %.2f)",
(unsigned long long)m->st.frames, (unsigned long long)m->st.dropped,
@@ -625,7 +624,7 @@ static void ov_stop(Cpu *c) {
static void ov_reset(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_RESET); return; }
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int away = quiesce(m);
release_codec(m);
m->last_error = 0;
@@ -637,7 +636,7 @@ static void ov_reset(Cpu *c) {
static void ov_flush(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_FLUSH); return; }
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int away = quiesce(m);
if (m->configured) {
vpx_codec_destroy(&m->vpx);
@@ -656,7 +655,7 @@ static void ov_acquire_input(Cpu *c) {
if (!m) { pass(c, MOVIE_ACQUIRE_INPUT); return; }
if (!index || !remaining) { ret_x(c, ST_NULL); return; }
Signals s = {0};
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
*index = -1;
*remaining = 0;
int st = ST_NO_INPUT;
@@ -667,7 +666,7 @@ static void ov_acquire_input(Cpu *c) {
st = ST_OK;
}
collect(m, &s);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
@@ -679,9 +678,9 @@ static void ov_get_input(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_INPUT); return; }
if (!buf) { ret_x(c, ST_NULL); return; }
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
uint8_t *p = index >= 0 && index < IN_BUFS ? m->in[index] : NULL;
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
buf_set_range(c, buf, 0, p ? IN_CAP : 0);
buf_set_data(c, buf, U(p), p ? IN_CAP : 0);
ret_x(c, p ? ST_OK : ST_ERROR);
@@ -699,7 +698,7 @@ static void ov_send_input(Cpu *c) {
return;
}
Signals s = {0};
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int st = ST_OK;
if (!m->started || m->npending == IN_BUFS) st = ST_ERROR;
else if (m->eos_in) { // after end of stream: hand the buffer back
@@ -707,10 +706,10 @@ static void ov_send_input(Cpu *c) {
m->owed |= 1u << MOVIE_EV_INPUT;
} else {
m->pending[m->npending++] = (Pending){index, offset, size, time, flags};
WakeConditionVariable(&m->work);
plat_cond_signal(&m->work);
}
collect(m, &s);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
@@ -724,7 +723,7 @@ static void ov_acquire_output(Cpu *c) {
if (!m) { pass(c, MOVIE_ACQUIRE_OUTPUT); return; }
if (!index || !time || !flags || !remaining) { ret_x(c, ST_NULL); return; }
Signals s = {0};
AcquireSRWLockExclusive(&m->mu); // paced frames become ready only at presents (movie_tick)
plat_mutex_lock(&m->mu); // paced frames become ready only at presents (movie_tick)
*index = -1;
*time = 0;
*flags = 0;
@@ -740,7 +739,7 @@ static void ov_acquire_output(Cpu *c) {
st = ST_OK;
}
collect(m, &s);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
@@ -750,7 +749,7 @@ static int get_output(Cpu *c, Movie *m, int index, uint64_t buf) {
uint8_t *dst = (uint8_t *)(uintptr_t)rt_call(c, G.buf_base, 1, buf);
uint64_t cap = rt_call(c, G.buf_capacity, 1, buf);
int st = ST_ERROR, bytes = 0;
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
if (index >= 0 && index < OUT_BUFS && m->out_held[index]) {
bytes = m->out_bytes[index];
if (!bytes) st = ST_OK;
@@ -762,7 +761,7 @@ static int get_output(Cpu *c, Movie *m, int index, uint64_t buf) {
bytes = 0;
}
}
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
if (st == ST_OK && bytes) buf_set_range(c, buf, 0, bytes);
return st;
}
@@ -782,10 +781,10 @@ static void ov_get_output_size(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT_SIZE); return; }
if (!size || !count) { ret_x(c, ST_NULL); return; }
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
*count = 1;
*size = m->configured ? frame_bytes(m) : 0;
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
ret_x(c, ST_OK);
}
@@ -795,7 +794,7 @@ static void ov_release_output(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_RELEASE_OUTPUT); return; }
Signals s = {0};
AcquireSRWLockExclusive(&m->mu);
plat_mutex_lock(&m->mu);
int st = ST_ERROR;
if (index >= 0 && index < OUT_BUFS && m->out_held[index]) {
int queued = 0;
@@ -803,12 +802,12 @@ static void ov_release_output(Cpu *c) {
for (int i = 0; i < m->ndecoded; i++) queued |= m->decoded[i].index == index;
if (!queued) {
m->out_held[index] = 0;
WakeConditionVariable(&m->work);
plat_cond_signal(&m->work);
st = ST_OK;
}
}
collect(m, &s);
ReleaseSRWLockExclusive(&m->mu);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
@@ -816,16 +815,16 @@ static void ov_release_output(Cpu *c) {
static void ov_get_last_error(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_LAST_ERROR); return; }
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
int e = m->last_error;
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
ret_x(c, (uint32_t)e);
}
static int output_format(Cpu *c, Movie *m, int index, uint64_t md) {
AcquireSRWLockShared(&m->mu);
plat_mutex_lock_shared(&m->mu);
int ok = index >= 0 && index < OUT_BUFS && m->out_held[index], w = m->w, h = m->h;
ReleaseSRWLockShared(&m->mu);
plat_mutex_unlock_shared(&m->mu);
if (!ok) return ST_ERROR;
set_format(c, w, h, md);
return ST_OK;
+39 -61
View File
@@ -1,4 +1,4 @@
// --probe-services: every override of ov_system.c, ov_input.c, ov_media.c, swkbd_win.c and
// --probe-services: every override of ov_system.c, ov_input.c, ov_media.c, swkbd.c and
// ov_gfx.c called the way guest code calls it: by its synthetic address through rt_call / rt_dispatch, with AAPCS64
// arguments, on the guest main thread. Checks that each one matches a relocation of the loaded modules, then the
// values: language, operation mode, notification messages, account, time round trips, controllers, codec state,
@@ -6,7 +6,6 @@
// (answers from TOTK_SWKBD_ANSWER, then the window pressed by TOTK_SWKBD_TEST) and the shared fonts when
// tools/extract_fonts.py has written them. The named fatal stubs, and the account UID of a second run, are checked in
// child processes (this executable again, TOTK_PROBE_SERVICES_CHILD=<case>).
#include <windows.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@@ -21,6 +20,7 @@
#include "kernel.h"
#include "ov.h"
#include "platform/platform.h"
#include "utf16.h"
#include "ui_text.h"
#define CHILD_ENV "TOTK_PROBE_SERVICES_CHILD"
@@ -241,11 +241,11 @@ static char *lang(void) {
return s;
}
static void set_env(const char *name, const char *value) { _putenv_s(name, value ? value : ""); }
static void set_env(const char *name, const char *value) { plat_setenv(name, value); } // NULL removes it
static void probe_oe(void) {
char *keep_lang = cfg_get("TOTK_LANG") ? _strdup(cfg_get("TOTK_LANG")) : NULL;
char *keep_locale = cfg_get("TOTK_LOCALE") ? _strdup(cfg_get("TOTK_LOCALE")) : NULL;
char *keep_lang = cfg_get("TOTK_LANG") ? strdup(cfg_get("TOTK_LANG")) : NULL;
char *keep_locale = cfg_get("TOTK_LOCALE") ? strdup(cfg_get("TOTK_LOCALE")) : NULL;
static const struct { const char *lang, *locale, *want; } cases[] = {
{NULL, "en-US", "en-US"}, {NULL, "be-BY", "en-US"}, {NULL, "xx", "en-US"}, {NULL, "ja-JP", "ja"},
{NULL, "fr-CA", "fr-CA"}, {NULL, "es-MX", "es-419"}, {NULL, "es_ES", "es"}, {NULL, "zh-Hant-TW", "zh-Hant"},
@@ -277,7 +277,7 @@ static void probe_oe(void) {
free(keep_locale);
checks++;
failures += bad > 0;
printf("oe: language %s by default (en-US locale), %s for this Windows user locale; %d locale/TOTK_LANG cases %s\n",
printf("oe: language %s by default (en-US locale), %s for this user's locale; %d locale/TOTK_LANG cases %s\n",
def, sys, (int)(sizeof cases / sizeof *cases), bad ? "FAILED" : "ok");
CALL("oe.Initialize", 0);
@@ -304,7 +304,7 @@ static void probe_oe(void) {
CALL("oe.GetExpectedVolumeBalance", U(&l), U(&r));
CHECK(l == 0.25f && r == 0.75f, "volume balance %g/%g", (double)l, (double)r);
int64_t t1 = (int64_t)CALL("oe.GetProgramTotalActiveTime", 0);
Sleep(2);
plat_sleep_ms(2);
int64_t t2 = (int64_t)CALL("oe.GetProgramTotalActiveTime", 0);
CHECK(t1 > 0 && t2 > t1, "program total active time %lld, %lld", (long long)t1, (long long)t2);
CALL("oe.SetUserInactivityDetectionTimeExtended", 1);
@@ -312,11 +312,11 @@ static void probe_oe(void) {
CALL("oe.FinishStartupLogo", 0);
}
static DWORD WINAPI late_push(void *p) {
static void late_push(void *p) {
(void)p;
Sleep(50);
plat_sleep_ms(50);
oe_push_message(0x21);
return 0;
return;
}
static void probe_messages(void) {
@@ -326,12 +326,11 @@ static void probe_messages(void) {
oe_push_message(0x20);
CHECK((uint8_t)CALL("oe.TryPopNotificationMessage", U(&m)) == 1 && m == 0x1f, "TryPop after a push: %#x", m);
CHECK((uint32_t)CALL("oe.PopNotificationMessage", 0) == 0x20, "Pop after a push");
HANDLE h = CreateThread(NULL, 0, late_push, NULL, 0, NULL);
PlatThread *h = plat_thread_new(late_push, NULL, "late push");
uint64_t t0 = kern_now_ns();
uint32_t late = (uint32_t)CALL("oe.PopNotificationMessage", 0);
uint64_t waited = kern_now_ns() - t0;
WaitForSingleObject(h, INFINITE);
CloseHandle(h);
plat_thread_join(h);
CHECK(late == 0x21 && waited >= 40000000, "blocking Pop got %#x after %llu ms", late,
(unsigned long long)(waited / 1000000));
printf("oe: notification messages pushed then popped (TryPop, Pop, Pop blocked %llu ms until a host push): %s\n",
@@ -485,8 +484,8 @@ static void probe_feedback(uint32_t pid, uint32_t style) {
"six-axis handle %#x", sh[0]);
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(dual), 2, U(&zero), 4) == 2 && dual[0] == 5 &&
dual[1] == 0x10005, "Joy-Con pair vibration handles %#x %#x (SDK: 0x5 0x10005)", dual[0], dual[1]);
char *keep_r = cfg_get("TOTK_RUMBLE") ? _strdup(cfg_get("TOTK_RUMBLE")) : NULL;
char *keep_m = cfg_get("TOTK_MOTION") ? _strdup(cfg_get("TOTK_MOTION")) : NULL;
char *keep_r = cfg_get("TOTK_RUMBLE") ? strdup(cfg_get("TOTK_RUMBLE")) : NULL;
char *keep_m = cfg_get("TOTK_MOTION") ? strdup(cfg_get("TOTK_MOTION")) : NULL;
set_env("TOTK_RUMBLE", "0");
set_env("TOTK_MOTION", "0");
uint32_t none[2] = {0xdead, 0xdead}, six_off[2] = {0};
@@ -644,10 +643,10 @@ static void probe_media_gfx(void) {
"vsync event %s\n", display, layer, window, vsync ? "bound (auto-clear)" : "FAILED");
}
// ---- software keyboard (swkbd_win.c): ShowKeyboardArg built by the SDK's own setters, as the horse naming does
// ---- software keyboard (swkbd.c): ShowKeyboardArg built by the SDK's own setters, as the horse naming does
enum { KB_TYPE = 0x00, KB_DISABLED = 0x1c, KB_HEADER = 0x24, KB_GUIDE = 0x1a8, KB_MAX_LEN = 0x3ac, KB_MIN_LEN = 0x3b0,
KB_UTF8 = 0x3bd, KB_INIT_LEN = 0x3c4, STRING_SIZE = 0x7d4, SWKBD_CANCELED = 0x29f };
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0) // SDK 20.5 grew KeyboardConfig (swkbd_win.c)
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0) // SDK 20.5 grew KeyboardConfig (swkbd.c)
#define ARG_WORK_SIZE (ARG_WORK + 8)
#define ARG_SIZE (RT_V143 ? 0x548 : 0x500)
@@ -660,15 +659,13 @@ static uint64_t sdk(const char *name) {
// NUL-terminated UTF-16 copy of utf8 in guest memory
static uint16_t *u16(const char *utf8) {
uint16_t *g = guest_alloc(0x200);
wchar_t w[128];
int n = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, w, 128);
memcpy(g, w, (size_t)(n > 0 ? n : 1) * 2);
utf8_to_utf16(utf8, g, 128);
return g;
}
static int u16_is(const uint16_t *a, const char *utf8) {
wchar_t b[128];
if (!MultiByteToWideChar(CP_UTF8, 0, utf8, -1, b, 128)) return 0;
uint16_t b[128];
utf8_to_utf16(utf8, b, 128);
for (size_t i = 0;; i++) {
if (a[i] != b[i]) return 0;
if (!a[i]) return 1;
@@ -697,7 +694,7 @@ static void probe_swkbd(void) {
str[0] = U(out), str[1] = STRING_SIZE;
uint64_t w = U(work), ws = 0x1000;
// the SDK setters write where swkbd_win.c reads
// the SDK setters write where swkbd.c reads
rt_call(gc, make, 2, U(arg), 2);
rt_call(gc, header, 2, U(arg), U(u16("Hdr")));
rt_call(gc, guide, 2, U(arg), U(u16("Gd")));
@@ -706,7 +703,7 @@ static void probe_swkbd(void) {
int32_t init_len;
memcpy(&init_len, arg + KB_INIT_LEN, 4);
CHECK(u16_is((uint16_t *)(arg + KB_HEADER), "Hdr") && u16_is((uint16_t *)(arg + KB_GUIDE), "Gd") && init_len == 5,
"ShowKeyboardArg layout: header, guide or initial text not where swkbd_win.c reads them");
"ShowKeyboardArg layout: header, guide or initial text not where swkbd.c reads them");
const char *def = ui_text(UI_SWKBD_DEFAULT_NAME);
const KbCase cases[] = {
@@ -791,7 +788,7 @@ static void probe_menu_gate(void) {
// ---- shared fonts: the extracted files when present (the missing case is a fatal child)
static void probe_fonts(void) {
char dir[MAX_PATH], path[MAX_PATH + 32];
char dir[PLAT_PATH_MAX], path[PLAT_PATH_MAX + 32];
snprintf(path, sizeof path, "%s/0-standard.bfttf", ov_shared_font_dir(dir, sizeof dir));
FILE *f = fopen(path, "rb");
if (!f) {
@@ -800,8 +797,8 @@ static void probe_fonts(void) {
}
uint8_t head[12];
size_t got = fread(head, 1, sizeof head, f);
_fseeki64(f, 0, SEEK_END);
int64_t len = _ftelli64(f);
plat_fseek64(f, 0, SEEK_END);
int64_t len = plat_ftell64(f);
fclose(f);
uint64_t size = CALL("pl.GetSharedFontSize", 0), addr = CALL("pl.GetSharedFontAddress", 0);
CHECK(got == sizeof head && size && size + 8 == (uint64_t)len, "shared font 0: size %llu, file %lld bytes",
@@ -821,6 +818,10 @@ static void probe_menu_text(void) {
for (int id = UI_MENU_FIRST; id <= UI_MENU_LAST_ID; id++) {
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id), "menu text id %d lacks en or ru", id);
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id), "menu text id %d is empty", id);
// the description pane keeps the whole text and a note after it (option_rows.cpp desc_set)
CHECK(strlen(ui_text_in("en", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES, "menu text id %d is too long for the "
"description pane", id);
}
CHECK(!ui_text_defined("en", UI_TEXT_COUNT) && !*ui_text_in("ru", UI_TEXT_COUNT), "an id past the catalog gives text");
// other languages fall back to English for the menu, and keep their own keyboard texts
@@ -865,37 +866,14 @@ static void probe_main(Cpu *c) {
// ---- child processes
// Runs this executable again with the same command line and TOTK_PROBE_SERVICES_CHILD=<mode>; its stdout and stderr
// into out. Returns the exit code.
static DWORD run_child(const char *mode, const char *env, char *out, size_t cap) {
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
HANDLE rd, wr;
if (!CreatePipe(&rd, &wr, &sa, 0)) rt_fatal("probe-services: CreatePipe (error %lu)", GetLastError());
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdOutput = wr, .hStdError = wr};
PROCESS_INFORMATION pi;
wchar_t *cmd = _wcsdup(GetCommandLineW());
SetEnvironmentVariableA(CHILD_ENV, mode);
static uint32_t run_child(const char *mode, const char *env, char *out, size_t cap) {
char name[64] = "";
const char *eq = env ? strchr(env, '=') : NULL;
if (eq) snprintf(name, sizeof name, "%.*s", (int)(eq - env), env), SetEnvironmentVariableA(name, eq + 1);
BOOL ok = CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
SetEnvironmentVariableA(CHILD_ENV, NULL);
if (eq) SetEnvironmentVariableA(name, NULL);
free(cmd);
CloseHandle(wr);
if (!ok) rt_fatal("probe-services: CreateProcess for the %s child (error %lu)", mode, GetLastError());
size_t n = 0;
DWORD got;
while (n + 1 < cap && ReadFile(rd, out + n, (DWORD)(cap - 1 - n), &got, NULL) && got) n += got;
out[n] = 0;
char sink[512];
while (ReadFile(rd, sink, sizeof sink, &got, NULL) && got) {}
CloseHandle(rd);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return code;
if (eq) snprintf(name, sizeof name, "%.*s", (int)(eq - env), env), plat_setenv(name, eq + 1);
int code = plat_run_self(CHILD_ENV, mode, out, cap, 0);
if (eq) plat_setenv(name, NULL);
if (code < 0) rt_fatal("probe-services: cannot start the %s child", mode);
return (uint32_t)code;
}
static const char *child_mode;
@@ -936,12 +914,12 @@ static int run_as_child(const char *mode) {
static void probe_children(void) {
static char out[16384];
DWORD code = run_child("uid", NULL, out, sizeof out);
uint32_t code = run_child("uid", NULL, out, sizeof out);
unsigned long long hi = 0, lo = 0;
const char *p = strstr(out, "child uid ");
int parsed = p && sscanf(p + 10, "%16llx%16llx", &hi, &lo) == 2;
int same = parsed && code == 0 && hi == probe_uid[1] && lo == probe_uid[0];
CHECK(same, "the second run's account UID differs (exit %lu): %s", code, out);
CHECK(same, "the second run's account UID differs (exit %u): %s", code, out);
printf("account: UID %016llx%016llx in %s/account.uid, the same in a second run: %s\n",
(unsigned long long)probe_uid[1], (unsigned long long)probe_uid[0], rt_config()->profile, same ? "yes" : "NO");
@@ -951,7 +929,7 @@ static void probe_children(void) {
p = strstr(out, "child nvn: ");
parsed = p && sscanf(p, "child nvn: known %llx expected %llx unknown %llx", &known, &expected, &unknown) == 3;
int nvn_ok = parsed && code == 0 && known && known == expected && unknown == 0;
CHECK(nvn_ok, "child nvn: exit %lu, output: %s", code, out);
CHECK(nvn_ok, "child nvn: exit %u, output: %s", code, out);
if (nvn_ok) ov("nvnBootstrapLoader")->calls++;
printf("nvnBootstrapLoader: nvnDeviceGetProcAddress -> %#llx (proc table %#llx), unknown name -> %#llx\n", known,
expected, unknown);
@@ -959,11 +937,11 @@ static void probe_children(void) {
for (int i = 0; i < NFATAL; i++) {
code = run_child(fatals[i].child, fatals[i].env, out, sizeof out);
int ok = code == 70 && strstr(out, fatals[i].fatal);
CHECK(ok, "child %s: exit %lu, output: %s", fatals[i].child, code, out);
CHECK(ok, "child %s: exit %u, output: %s", fatals[i].child, code, out);
if (ok) ov(fatals[i].key)->calls++;
const char *line = strstr(out, "FATAL: ");
int len = line ? (int)strcspn(line, "\r\n") : 0;
printf("fatal stub %-20s child exit %lu: %.*s\n", fatals[i].key, code, len, line ? line : "(no FATAL line)");
printf("fatal stub %-20s child exit %u: %.*s\n", fatals[i].key, code, len, line ? line : "(no FATAL line)");
}
}
+103 -129
View File
@@ -9,12 +9,10 @@
// TOTK_LOCALE=<BCP 47 tag> else the language for this locale ("pt-BR"), else the Windows user locale;
// a language the game does not ship becomes en-US
// TOTK_OPERATION_MODE=docked|handheld operation and performance mode reported to the game (default docked)
// TOTK_ASSUME_FOCUS=1 GetCurrentFocusState always reports in focus, whatever the window says; the
// window opens without taking focus (gfx_window.c), for scripted runs
// TOTK_ASSUME_FOCUS=1|0 1 (default, the Game tab's row): GetCurrentFocusState always reports in focus,
// whatever the window says; 0: out of focus while the window is, as on a Switch
// TOTK_SHARED_FONTS=<dir> nn::pl shared fonts (tools/extract_fonts.py output), default
// <romfs>/../../firmware/shared-fonts
#include <windows.h>
#include <bcrypt.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
@@ -25,6 +23,7 @@
#include "fs.h"
#include "host.h"
#include "ov.h"
#include "platform/platform.h"
#define NFP_R_DEVICE_NOT_FOUND 0x8073u // nfp: no NFC reader on the host
#define ACCOUNT_R_INVALID 0x7c67u // MacNativeGames' result for an unknown user handle or uid
@@ -32,7 +31,7 @@
#define TIME_R_SOURCE_MISMATCH 0x18074u
static void need(int ok, const char *what) {
if (!ok) rt_fatal("%s (error %lu)", what, GetLastError());
if (!ok) rt_fatal("%s", what);
}
static const char *caller(Cpu *c, char *buf, int n) { return ldr_describe(c->x[30] - 4, buf, n); }
@@ -40,14 +39,14 @@ static const char *caller(Cpu *c, char *buf, int n) { return ldr_describe(c->x[3
// ==== nn::oe (services.c)
// TOTK_OPERATION_MODE, read once: 1 handheld (OperationMode 0, PerformanceMode Normal 0), 0 docked (1, Boost 1).
int ov_handheld(void) {
static volatile LONG v = -1;
LONG h = v;
static volatile int32_t v = -1;
int32_t h = v;
if (h < 0) {
const char *e = cfg_value("TOTK_OPERATION_MODE");
if (!strcmp(e, "docked")) h = 0;
else if (!strcmp(e, "handheld")) h = 1;
else rt_fatal("TOTK_OPERATION_MODE must be docked or handheld, not '%s'", e);
if (InterlockedCompareExchange(&v, h, -1) == -1)
if (plat_cas32(&v, h, -1) == -1)
rt_log("oe: operation mode reported to the game: %s", h ? "handheld (performance normal)"
: "docked (performance boost)");
}
@@ -103,14 +102,9 @@ const char *ov_language(void) {
rt_fatal("TOTK_LANG '%s' is not a Switch language code", code);
}
const char *tag = cfg_get("TOTK_LOCALE");
char sys[LOCALE_NAME_MAX_LENGTH] = "en-US";
char sys[85] = "en-US";
if (!tag || !*tag) {
wchar_t w[LOCALE_NAME_MAX_LENGTH];
if (GetUserDefaultLocaleName(w, LOCALE_NAME_MAX_LENGTH)) {
size_t i = 0;
for (; w[i] && w[i] < 0x80 && i + 1 < sizeof sys; i++) sys[i] = (char)w[i];
sys[i] = 0;
}
if (*plat_user_locale()) snprintf(sys, sizeof sys, "%s", plat_user_locale());
tag = sys;
}
return switch_language(tag);
@@ -124,13 +118,13 @@ static void ov_desired_language(Cpu *c) {
ret_x(c, v);
}
static void ov_oe_initialize(Cpu *c) { (void)c; rt_log("oe: application environment: local Windows process"); }
static void ov_oe_initialize(Cpu *c) { (void)c; rt_log("oe: application environment: a local host process"); }
// void SetPerformanceConfiguration(PerformanceMode mode, int config)
static void ov_performance_config(Cpu *c) {
int64_t mode = (int32_t)arg_x(c, 0);
if (mode < 0 || mode > 1) rt_fatal("nn::oe::SetPerformanceConfiguration: invalid mode %lld", (long long)mode);
rt_log("oe: performance request mode %lld configuration %#x: Windows schedules the CPU", (long long)mode,
rt_log("oe: performance request mode %lld configuration %#x: the host OS schedules the CPU", (long long)mode,
(uint32_t)arg_x(c, 1));
}
@@ -144,11 +138,11 @@ static void ov_operation_mode(Cpu *c) { ret_x(c, ov_handheld() ? 0 : 1); }
static void ov_performance_mode(Cpu *c) { ret_x(c, ov_handheld() ? 0 : 1); }
// void SetFocusHandlingMode(FocusHandlingMode mode): the nnMain checkpoint of --boot
static volatile LONG focus_mode;
static volatile int32_t focus_mode;
static void ov_focus_mode(Cpu *c) {
int64_t mode = (int32_t)arg_x(c, 0);
if (mode < 0 || mode > 3) rt_fatal("nn::oe::SetFocusHandlingMode: unknown mode %lld", (long long)mode);
InterlockedExchange(&focus_mode, (LONG)mode);
plat_xchg32(&focus_mode, (int32_t)mode);
if (rt_config()->boot) {
rt_boot_event("checkpoint", c, "SetFocusHandlingMode");
} else {
@@ -157,31 +151,30 @@ static void ov_focus_mode(Cpu *c) {
}
}
// FocusState GetCurrentFocusState(): 1 in focus, 2 out of focus (nn::oe::FocusState). In focus until the window
// reports otherwise through oe_set_focus; TOTK_ASSUME_FOCUS=1 (unattended runs) pins it to 1. The caller of
// oe_set_focus also pushes OE_MSG_FOCUS_STATE_CHANGED.
static volatile LONG focus_state = 1;
static int assume_focus(void) {
static volatile LONG v = -1;
LONG a = v;
if (a < 0) {
const char *e = cfg_get("TOTK_ASSUME_FOCUS");
a = e && atoi(e) ? 1 : 0;
InterlockedExchange(&v, a);
}
return (int)a;
// FocusState GetCurrentFocusState(): 1 in focus, 2 out of focus (nn::oe::FocusState). The game sets focus handling
// mode 2: on a Switch the system then suspends the whole process while it is out of focus and sends Resume when it
// returns. Here only the game's main thread stops (it waits in PopNotificationMessage for the next message) while its
// loader threads, audio and clocks run on, and a load interrupted that way was seen to never finish (Linux, 1.4.3).
// So TOTK_ASSUME_FOCUS=1, the default, keeps it at 1, as MacNativeGames does; with 0 it follows the window through
// oe_set_focus. The caller of oe_set_focus also pushes OE_MSG_FOCUS_STATE_CHANGED.
//
// The setting is read on every focus change rather than once at start: a focus change is rare, the lookup is a short
// walk of the loaded ini entries, and the Game tab's row then takes effect the next time the window is left, without
// a restart.
static volatile int32_t focus_state = 1;
void oe_set_focus(int in_focus) {
plat_xchg32(&focus_state, in_focus || atoi(cfg_value("TOTK_ASSUME_FOCUS")) ? 1 : 2);
}
void oe_set_focus(int in_focus) { InterlockedExchange(&focus_state, in_focus || assume_focus() ? 1 : 2); }
static void ov_focus_state(Cpu *c) { ret_x(c, (uint64_t)focus_state); }
static volatile LONG notify_resume, notify_operation, notify_performance;
static void ov_resume_enabled(Cpu *c) { InterlockedExchange(&notify_resume, (LONG)(arg_x(c, 0) & 0xff)); }
static void ov_operation_enabled(Cpu *c) { InterlockedExchange(&notify_operation, (LONG)(arg_x(c, 0) & 0xff)); }
static void ov_performance_enabled(Cpu *c) { InterlockedExchange(&notify_performance, (LONG)(arg_x(c, 0) & 0xff)); }
static volatile int32_t notify_resume, notify_operation, notify_performance;
static void ov_resume_enabled(Cpu *c) { plat_xchg32(&notify_resume, (int32_t)(arg_x(c, 0) & 0xff)); }
static void ov_operation_enabled(Cpu *c) { plat_xchg32(&notify_operation, (int32_t)(arg_x(c, 0) & 0xff)); }
static void ov_performance_enabled(Cpu *c) { plat_xchg32(&notify_performance, (int32_t)(arg_x(c, 0) & 0xff)); }
// ==== nn::oe, applet, am, prepo, nfp, err (applet_host.c)
static SRWLOCK lock = SRWLOCK_INIT;
static CONDITION_VARIABLE message_cv = CONDITION_VARIABLE_INIT;
static PlatMutex lock = PLAT_MUTEX_INIT;
static PlatCond message_cv = PLAT_COND_INIT;
static uint32_t messages[32];
static unsigned head, tail;
static float balance[2] = {1.0f, 1.0f};
@@ -189,21 +182,21 @@ static uint64_t start_ticks;
__attribute__((constructor)) static void started(void) { start_ticks = rt_ticks(); }
void oe_push_message(uint32_t message) {
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
if (tail - head < 32) {
messages[tail++ % 32] = message;
WakeAllConditionVariable(&message_cv);
plat_cond_broadcast(&message_cv);
}
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// void GetExpectedVolumeBalance(float *left, float *right)
static void ov_get_balance(Cpu *c) {
float *l = arg_p(c, 0), *r = arg_p(c, 1);
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
if (l) *l = balance[0];
if (r) *r = balance[1];
ReleaseSRWLockShared(&lock);
plat_mutex_unlock_shared(&lock);
}
// void SetExpectedVolumeBalance(float left, float right)
@@ -211,10 +204,10 @@ static void ov_set_balance(Cpu *c) {
float l = arg_s(c, 0), r = arg_s(c, 1);
if (!(l >= 0 && l <= 1 && r >= 0 && r <= 1))
rt_fatal("nn::oe::SetExpectedVolumeBalance: balance %g/%g out of range", (double)l, (double)r);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
balance[0] = l;
balance[1] = r;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
}
// TimeSpan GetProgramTotalActiveTime(): nanoseconds since the process started (19.2 MHz ticks * 625 / 12)
@@ -238,23 +231,23 @@ static void ov_launch_parameter(Cpu *c) {
// bool TryPopNotificationMessage(Message *out)
static void ov_try_pop(Cpu *c) {
uint32_t *out = arg_p(c, 0);
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
int ok = head != tail;
if (ok) {
uint32_t m = messages[head++ % 32];
if (out) *out = m;
}
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
ret_x(c, (uint64_t)ok);
}
// Message PopNotificationMessage(): blocks like a Horizon wait, so the thread gives its virtual core up meanwhile
static void ov_pop(Cpu *c) {
int away = kern_core_away();
AcquireSRWLockExclusive(&lock);
while (head == tail) SleepConditionVariableSRW(&message_cv, &lock, INFINITE, 0);
plat_mutex_lock(&lock);
while (head == tail) plat_cond_wait(&message_cv, &lock);
uint32_t m = messages[head++ % 32];
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
kern_core_back(&away);
ret_x(c, m);
}
@@ -265,18 +258,18 @@ static void ov_previous_program(Cpu *c) { ret_x(c, (uint64_t)(int64_t)-1); }
static void ov_aruid(Cpu *c) { ret_x(c, 1); }
// nn::prepo: play reports stay local (no Nintendo servers)
static volatile LONG64 reports;
static volatile int64_t reports;
static void ov_report_save(Cpu *c) { // Result PlayReport::Save() and Save(const account::Uid &)
LONG64 n = InterlockedIncrement64(&reports);
int64_t n = plat_inc64(&reports);
if (n % 100 == 1) rt_log("prepo: play report kept local (not sent), %lld so far", (long long)n);
ret_x(c, 0);
}
static void ov_result_ok(Cpu *c) { ret_x(c, 0); } // RequestImmediateTransmission, AddInternetConnectionStatus
// nn::nfp: no NFC reader
static volatile LONG nfp_state;
static void ov_nfp_initialize(Cpu *c) { InterlockedExchange(&nfp_state, 1); ret_x(c, 0); }
static void ov_nfp_finalize(Cpu *c) { InterlockedExchange(&nfp_state, 0); ret_x(c, 0); }
static volatile int32_t nfp_state;
static void ov_nfp_initialize(Cpu *c) { plat_xchg32(&nfp_state, 1); ret_x(c, 0); }
static void ov_nfp_finalize(Cpu *c) { plat_xchg32(&nfp_state, 0); ret_x(c, 0); }
static void ov_nfp_state(Cpu *c) { ret_x(c, (uint64_t)nfp_state); }
// Result ListDevices(DeviceHandle *out, int *count, int max)
static void ov_nfp_list(Cpu *c) {
@@ -347,26 +340,13 @@ static void obs_memory_pages(Cpu *c) {
// ==== nn::account (account.c): one offline local user whose Uid survives restarts in <profile>/account.uid
typedef struct { uint64_t lo, hi; } Uid;
typedef struct { Uid uid; uint64_t token; } UserHandle;
static SRWLOCK account_lock = SRWLOCK_INIT;
static PlatMutex account_lock = PLAT_MUTEX_INIT;
static Uid local;
static uint64_t tokens[64], next_token = 1;
static void make_dirs(const char *dir) {
char p[1100];
need(snprintf(p, sizeof p, "%s", dir) < (int)sizeof p, "account: profile path too long");
for (char *s = p + 1; ; s++) {
char ch = *s;
if (ch && ch != '/' && ch != '\\') continue;
if (s[-1] != ':' && s[-1] != '/' && s[-1] != '\\') {
*s = 0;
wchar_t *w = fs_wide(p);
if (!CreateDirectoryW(w, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
rt_fatal("account: cannot create %s (error %lu)", p, GetLastError());
free(w);
*s = ch;
}
if (!ch) break;
}
plat_file_mkdir_p(dir);
if (!plat_dir_exists(dir)) rt_fatal("account: cannot create %s", dir);
}
// Holding account_lock.
@@ -376,33 +356,29 @@ static void load_uid(void) {
char path[1100];
make_dirs(dir);
need(snprintf(path, sizeof path, "%s/account.uid", dir) < (int)sizeof path, "account: path too long");
wchar_t *w = fs_wide(path);
HANDLE f = CreateFileW(w, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
free(w);
if (f == INVALID_HANDLE_VALUE) rt_fatal("account: cannot open %s (error %lu)", path, GetLastError());
LARGE_INTEGER size;
DWORD n = 0;
need(GetFileSizeEx(f, &size), "account: size of the local account identity");
if (!size.QuadPart) {
do need(!BCryptGenRandom(NULL, (PUCHAR)&local, sizeof local, BCRYPT_USE_SYSTEM_PREFERRED_RNG), "account: random uid");
int64_t size = plat_file_size(path);
if (size <= 0) {
do need(plat_random(&local, sizeof local), "account: random uid");
while (!local.lo && !local.hi);
need(WriteFile(f, &local, sizeof local, &n, NULL) && n == sizeof local && FlushFileBuffers(f),
"account: persist the local account identity");
need(plat_file_write_atomic(path, &local, sizeof local), "account: persist the local account identity");
rt_log("account: new local account in %s", path);
} else if (size.QuadPart != sizeof local || !ReadFile(f, &local, sizeof local, &n, NULL) || n != sizeof local ||
(!local.lo && !local.hi)) {
rt_fatal("account: %s is not a local account identity (16 bytes)", path);
} else {
uint64_t got = 0;
uint8_t *b = plat_file_read(path, &got);
if (b && got == sizeof local) memcpy(&local, b, sizeof local);
free(b);
if (got != sizeof local || (!local.lo && !local.hi))
rt_fatal("account: %s is not a local account identity (16 bytes)", path);
}
CloseHandle(f);
}
// host.h: the save directory of fs_host.c is named after this Uid.
void account_uid(uint64_t out[2]) {
AcquireSRWLockExclusive(&account_lock);
plat_mutex_lock(&account_lock);
load_uid();
out[0] = local.lo;
out[1] = local.hi;
ReleaseSRWLockExclusive(&account_lock);
plat_mutex_unlock(&account_lock);
}
static int same_uid(const Uid *u) { return u && u->lo == local.lo && u->hi == local.hi; }
@@ -426,7 +402,7 @@ static void ov_account_initialize(Cpu *c) {
static void ov_open_user(Cpu *c) {
UserHandle *out = arg_p(c, 0);
if (!out) rt_fatal("nn::account::TryOpenPreselectedUser: null handle");
AcquireSRWLockExclusive(&account_lock);
plat_mutex_lock(&account_lock);
load_uid();
int ok = 0;
for (int i = 0; i < 64 && !ok; i++)
@@ -435,27 +411,27 @@ static void ov_open_user(Cpu *c) {
*out = (UserHandle){local, tokens[i]};
ok = 1;
}
ReleaseSRWLockExclusive(&account_lock);
plat_mutex_unlock(&account_lock);
ret_x(c, (uint64_t)ok);
}
// void CloseUser(const UserHandle &)
static void ov_close_user(Cpu *c) {
const UserHandle *h = arg_p(c, 0);
AcquireSRWLockExclusive(&account_lock);
plat_mutex_lock(&account_lock);
if (!valid_handle(h)) rt_fatal("nn::account::CloseUser: not an open user handle");
for (int i = 0; i < 64; i++)
if (tokens[i] == h->token) tokens[i] = 0;
ReleaseSRWLockExclusive(&account_lock);
plat_mutex_unlock(&account_lock);
}
// Result GetUserId(Uid *out, const UserHandle &)
static void ov_user_id(Cpu *c) {
Uid *out = arg_p(c, 0);
AcquireSRWLockShared(&account_lock);
plat_mutex_lock_shared(&account_lock);
int ok = out && valid_handle(arg_p(c, 1));
if (ok) *out = local;
ReleaseSRWLockShared(&account_lock);
plat_mutex_unlock_shared(&account_lock);
ret_x(c, ok ? 0 : ACCOUNT_R_INVALID);
}
@@ -464,20 +440,20 @@ static void ov_user_existence(Cpu *c) {
uint8_t *out = arg_p(c, 0);
const Uid *u = arg_p(c, 1);
if (!out || !u) { ret_x(c, ACCOUNT_R_INVALID); return; }
AcquireSRWLockExclusive(&account_lock);
plat_mutex_lock(&account_lock);
load_uid();
*out = (uint8_t)same_uid(u);
ReleaseSRWLockExclusive(&account_lock);
plat_mutex_unlock(&account_lock);
ret_x(c, 0);
}
// Result GetNickname(Nickname *out, const Uid &): 33 bytes, NUL-terminated UTF-8
static void ov_nickname(Cpu *c) {
char *out = arg_p(c, 0);
AcquireSRWLockExclusive(&account_lock);
plat_mutex_lock(&account_lock);
load_uid();
int ok = out && same_uid(arg_p(c, 1));
ReleaseSRWLockExclusive(&account_lock);
plat_mutex_unlock(&account_lock);
if (ok) {
memset(out, 0, 33);
memcpy(out, "Local", 5);
@@ -506,19 +482,19 @@ _Static_assert(sizeof(SteadyClockTimePoint) == 0x18 && sizeof(CalendarTime) == 8
_Static_assert(sizeof(ClockSnapshot) == 0xd0, "nn::time::ClockSnapshot");
static uint8_t source_id[16];
static volatile LONG time_ready;
static INIT_ONCE source_once = INIT_ONCE_STATIC_INIT;
static volatile int32_t time_ready;
static PlatOnce source_once = PLAT_ONCE_INIT;
static BOOL CALLBACK make_source(PINIT_ONCE once, PVOID param, PVOID *ctx) {
(void)once, (void)param, (void)ctx;
need(!BCryptGenRandom(NULL, source_id, sizeof source_id, BCRYPT_USE_SYSTEM_PREFERRED_RNG), "time: source id");
_tzset();
return TRUE;
static void make_source(void *param) {
(void)param;
need(plat_random(source_id, sizeof source_id), "time: source id");
plat_tz_west(); // reads the zone once, before the first localtime
return;
}
static SteadyClockTimePoint steady_now(void) {
InitOnceExecuteOnce(&source_once, make_source, NULL, NULL);
SteadyClockTimePoint p = {(int64_t)(GetTickCount64() / 1000), {0}};
plat_once(&source_once, make_source, NULL);
SteadyClockTimePoint p = {(int64_t)(plat_now_ms() / 1000), {0}};
memcpy(p.source, source_id, 16);
return p;
}
@@ -566,12 +542,11 @@ static int utc_calendar(int64_t t, CalendarTime *c, CalendarInfo *info) {
}
// Local calendar fields of a POSIX time in the Windows time zone. The zone name is "UTC" or the UTC offset ("+03",
// "-0330"), as tzdata names zones without an abbreviation; Windows has no abbreviations. 0 outside localtime_s' range.
// "-0330"), as tzdata names zones without an abbreviation; Windows has no abbreviations. 0 outside localtime's range.
static int local_calendar(int64_t t, CalendarTime *c, CalendarInfo *info) {
InitOnceExecuteOnce(&source_once, make_source, NULL, NULL);
time_t tt = (time_t)t;
plat_once(&source_once, make_source, NULL);
struct tm tm;
if (localtime_s(&tm, &tt)) return 0;
if (!plat_localtime(t, &tm)) return 0;
int64_t as_utc = days_from_civil(tm.tm_year + 1900, (unsigned)tm.tm_mon + 1, (unsigned)tm.tm_mday) * 86400 +
tm.tm_hour * 3600 + tm.tm_min * 60 + tm.tm_sec;
int32_t offset = (int32_t)(as_utc - t);
@@ -593,19 +568,18 @@ static int local_calendar(int64_t t, CalendarTime *c, CalendarInfo *info) {
// IANA name of the standard offset: Etc/GMT+5 is UTC-5 (POSIX sign); "UTC" for a zone off whole hours.
static void location_name(char out[0x24]) {
long west = 0;
long west = plat_tz_west();
memset(out, 0, 0x24);
_get_timezone(&west);
if (!west || west % 3600) snprintf(out, 0x24, "UTC");
else snprintf(out, 0x24, "Etc/GMT%+ld", west / 3600);
}
static void ov_time_initialize(Cpu *c) {
InitOnceExecuteOnce(&source_once, make_source, NULL, NULL);
InterlockedExchange(&time_ready, 1);
plat_once(&source_once, make_source, NULL);
plat_xchg32(&time_ready, 1);
ret_x(c, 0);
}
static void ov_time_finalize(Cpu *c) { InterlockedExchange(&time_ready, 0); ret_x(c, 0); }
static void ov_time_finalize(Cpu *c) { plat_xchg32(&time_ready, 0); ret_x(c, 0); }
static void ov_time_ready(Cpu *c) { ret_x(c, (uint64_t)time_ready); }
// Result StandardUserSystemClock / StandardNetworkSystemClock::GetCurrentTime(PosixTime *out)
@@ -711,18 +685,18 @@ static void ov_user_difference(Cpu *c) {
static const char *const font_files[SHARED_FONT_TYPES] = {
"0-standard.bfttf", "1-chinese-simplified.bfttf", "2-chinese-simplified-extension.bfttf",
"3-chinese-traditional.bfttf", "4-korean.bfttf", "5-nintendo-extension.bfttf"};
static struct { SRWLOCK lock; const uint8_t *file[SHARED_FONT_TYPES]; uint64_t size[SHARED_FONT_TYPES]; } fonts;
static struct { PlatMutex lock; const uint8_t *file[SHARED_FONT_TYPES]; uint64_t size[SHARED_FONT_TYPES]; } fonts;
const char *ov_shared_font_dir(char *buf, size_t n) {
const char *e = cfg_get("TOTK_SHARED_FONTS");
if (e && *e) snprintf(buf, n, "%s", e);
else {
char exe[MAX_PATH], probe[MAX_PATH + 40]; // shared-fonts next to totk.exe (tools/package_build.py), else the repo's
DWORD len = GetModuleFileNameA(NULL, exe, sizeof exe);
char *cut = len && len < sizeof exe ? strrchr(exe, '\\') : NULL;
char exe[PLAT_PATH_MAX], probe[PLAT_PATH_MAX + 40]; // shared-fonts next to totk.exe (tools/package_build.py), else the repo's
int len = plat_exe_path(exe, sizeof exe);
char *cut = len && len < sizeof exe ? plat_last_sep(exe) : NULL;
if (cut) *cut = 0;
snprintf(probe, sizeof probe, "%s\\shared-fonts\\0-standard.bfttf", exe);
if (cut && GetFileAttributesA(probe) != INVALID_FILE_ATTRIBUTES) snprintf(buf, n, "%s\\shared-fonts", exe);
snprintf(probe, sizeof probe, "%s/shared-fonts/0-standard.bfttf", exe);
if (cut && plat_file_exists(probe)) snprintf(buf, n, "%s/shared-fonts", exe);
else snprintf(buf, n, "%s/../../firmware/shared-fonts", rt_config()->romfs);
}
return buf;
@@ -749,18 +723,18 @@ static void ov_font_state(Cpu *c) { font_type(c, "GetSharedFontLoadState"); ret_
// The font of type t, read and checked on first use; a named fatal when it is missing or damaged.
static const uint8_t *font_data(Cpu *c, const char *fn, int64_t t) {
AcquireSRWLockExclusive(&fonts.lock);
plat_mutex_lock(&fonts.lock);
if (!fonts.file[t]) {
char dir[MAX_PATH], path[MAX_PATH + 64], lr[96];
char dir[PLAT_PATH_MAX], path[PLAT_PATH_MAX + 64], lr[96];
snprintf(path, sizeof path, "%s/%s", ov_shared_font_dir(dir, sizeof dir), font_files[t]);
FILE *f = fopen(path, "rb");
if (!f)
rt_fatal("nn::pl::%s(%lld) from %s: no shared font %s (needed for Chinese and Korean text); run "
"tools/extract_fonts.py --firmware <firmware NCA dir> or set TOTK_SHARED_FONTS", fn, (long long)t,
caller(c, lr, sizeof lr), path);
_fseeki64(f, 0, SEEK_END);
int64_t len = _ftelli64(f);
_fseeki64(f, 0, SEEK_SET);
plat_fseek64(f, 0, SEEK_END);
int64_t len = plat_ftell64(f);
plat_fseek64(f, 0, SEEK_SET);
if (len < 12 || len > (64 << 20)) rt_fatal("nn::pl: shared font %s: size %lld", path, (long long)len);
uint8_t *p = mem_kernel_alloc((uint64_t)len, 16);
size_t got = fread(p, 1, (size_t)len, f);
@@ -773,7 +747,7 @@ static const uint8_t *font_data(Cpu *c, const char *fn, int64_t t) {
(unsigned long long)size, path);
}
const uint8_t *p = fonts.file[t];
ReleaseSRWLockExclusive(&fonts.lock);
plat_mutex_unlock(&fonts.lock);
return p;
}
static void ov_font_address(Cpu *c) {
+6 -6
View File
@@ -11,7 +11,6 @@
// - Inputs of at least AWAY_BYTES give the virtual core up while they decode (other guest threads run on it).
// TOTK_ZSTD_NATIVE=0 keeps the guest decoder (the patch set switch); TOTK_ZSTD_CHECK=1 decodes each call both ways,
// keeps the guest's result and logs every call where the two differ.
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -24,6 +23,7 @@
#if __has_include("patches.h")
#include "patches.h"
#include "platform/platform.h"
#endif
#ifdef RT_PATCH_PAGE
@@ -44,7 +44,7 @@ typedef struct {
} Dict;
static Dict dicts[NDICT];
static int ndicts;
static SRWLOCK dict_lock = SRWLOCK_INIT;
static PlatMutex dict_lock = PLAT_MUTEX_INIT;
static struct {
uint64_t calls, in, out, ns, guest_ns, fallbacks, away, inplace;
@@ -76,23 +76,23 @@ static ZSTD_DDict *dict_for(uint64_t guest, uint64_t content, uint64_t size) {
return NULL;
uint64_t print = fingerprint(gp(content), size);
ZSTD_DDict *dd = NULL;
AcquireSRWLockShared(&dict_lock);
plat_mutex_lock_shared(&dict_lock);
for (int i = 0; i < ndicts && !dd; i++)
if (dicts[i].guest == guest && dicts[i].content == content && dicts[i].size == size && dicts[i].print == print)
dd = dicts[i].dd;
ReleaseSRWLockShared(&dict_lock);
plat_mutex_unlock_shared(&dict_lock);
if (dd) return dd;
// A copy: the guest may free or reuse its buffer. Entries are never freed (another thread may be decoding with
// one); a full table sends further dictionaries to the guest decoder.
ZSTD_DDict *made = ZSTD_createDDict(gp(content), (size_t)size);
if (!made) return NULL;
int slot = -1;
AcquireSRWLockExclusive(&dict_lock);
plat_mutex_lock(&dict_lock);
for (int i = 0; i < ndicts && !dd; i++) // another thread may have made it meanwhile
if (dicts[i].guest == guest && dicts[i].content == content && dicts[i].size == size && dicts[i].print == print)
dd = dicts[i].dd;
if (!dd && ndicts < NDICT) slot = ndicts, dicts[ndicts++] = (Dict){guest, content, size, print, made}, dd = made;
ReleaseSRWLockExclusive(&dict_lock);
plat_mutex_unlock(&dict_lock);
if (dd != made) {
ZSTD_freeDDict(made);
if (!dd) rt_log("zstd: more than %d dictionaries: further ones decode in the guest", NDICT);
+317
View File
@@ -0,0 +1,317 @@
// Frame timing (TOTK_TRACE=perf, the frame-time summary, the performance overlay's counts), the A/B switches (rt_ab,
// rt_knob) and TOTK_ONE_PER_CORE. The sampling profiler (TOTK_PROFILE) is the OS's (prof_win.c); without one the weak
// prof_sampler_* below do nothing.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "flight.h"
#include "gfx.h"
#include "host.h"
#include "perfmon.h"
#include "prof.h"
#include "platform/platform.h"
volatile uint32_t prof_render_tid, prof_main_tid, prof_replay_tid;
__attribute__((weak)) int prof_sampler_start(void) {
if (cfg_get("TOTK_PROFILE")) rt_log("prof: TOTK_PROFILE: no sampling profiler on this OS");
return 0;
}
__attribute__((weak)) void prof_sampler_write(void) {}
__attribute__((weak)) void prof_sampler_frame(uint64_t frame, uint64_t now, uint64_t d) { (void)frame, (void)now, (void)d; }
// TOTK_TRACE=perf, TOTK_FLIGHT=1 or the full performance overlay: event counts are taken. Wait times are always taken
// (frame-time summary). perf_base: the part fixed at start.
static int perf_on, perf_base;
static int overlay; // performance overlay mode (perf_set_overlay): 0 off, 1 fps, 2 full
void perf_set_overlay(int mode) {
__atomic_store_n(&overlay, mode, __ATOMIC_RELAXED);
__atomic_store_n(&perf_on, perf_base || mode == 2, __ATOMIC_RELAXED);
}
int perf_gpu_timing(void) { return (rt_trace & RT_TRACE_PERF) || __atomic_load_n(&overlay, __ATOMIC_RELAXED) == 2; }
static volatile int64_t gpu_busy_total;
void perf_gpu_busy(uint64_t ns) { plat_add64(&gpu_busy_total, (int64_t)ns); }
uint64_t perf_gpu_busy_total(void) { return (uint64_t)__atomic_load_n(&gpu_busy_total, __ATOMIC_RELAXED); }
void prof_start(void) {
flight_init();
atexit(perf_summary_flush); // boot.c's _exit path goes through prof_write instead
perf_base = (rt_trace & RT_TRACE_PERF) || flight_on;
perf_on = perf_base || overlay == 2;
if (prof_sampler_start()) atexit(prof_write); // a --seconds run ends in _exit: boot.c calls prof_write itself first
}
void prof_write(void) {
perf_summary_flush();
prof_sampler_write();
}
// ---- TOTK_TRACE=perf: frame interval and the time the render thread spends waiting, logged every 60 presents. The
// flight recorder (TOTK_FLIGHT=1, flight.c) takes the same numbers per frame.
static uint64_t wait_ns[PERF_N], frame_ns[PERF_N], worst_ns[PERF_N]; // window totals, this frame, the slowest frame
uint64_t perf_begin(void) { return kern_now_ns(); }
void perf_replay_thread(void) { prof_replay_tid = plat_thread_id(); }
void perf_main_thread(void) { prof_main_tid = plat_thread_id(); }
void perf_end(int kind, uint64_t t0) {
if (!t0) return;
uint32_t tid = plat_thread_id();
if (tid != prof_render_tid && tid != prof_replay_tid) return;
uint64_t d = kern_now_ns() - t0;
if (tid == prof_render_tid && !prof_replay_tid) { // one thread adds: no atomics on the synchronous path
wait_ns[kind] += d;
frame_ns[kind] += d;
return;
}
__atomic_fetch_add(&wait_ns[kind], d, __ATOMIC_RELAXED);
__atomic_fetch_add(&frame_ns[kind], d, __ATOMIC_RELAXED);
}
static volatile int64_t other_wait_ns; // perf_gpu_wait on threads other than the render thread, over the window
static volatile int64_t render_wait_ns; // the same on the render thread
void (*perf_gpu_stats)(PerfGpu *out);
void perf_gpu_wait(uint64_t t0) {
if (!t0 || !__atomic_load_n(&perf_on, __ATOMIC_RELAXED)) return;
int64_t d = (int64_t)(kern_now_ns() - t0);
plat_add64(plat_thread_id() == prof_render_tid ? &render_wait_ns : &other_wait_ns, d);
}
static volatile int64_t replay_ns; // perf_replay: summed over the window, any thread
static volatile int64_t frame_replay_ns; // the same since the previous present (frame-time summary)
void perf_replay(uint64_t t0) {
if (!t0) return;
int64_t d = (int64_t)(kern_now_ns() - t0);
plat_add64(&frame_replay_ns, d);
if (__atomic_load_n(&perf_on, __ATOMIC_RELAXED)) plat_add64(&replay_ns, d);
}
static volatile int64_t counts[PERF_CNT_N]; // running totals: the perf log and the flight recorder take differences
void perf_count(int kind) {
if (__atomic_load_n(&perf_on, __ATOMIC_RELAXED)) plat_inc64(&counts[kind]);
}
// ---- Frame-time summary, always on: every 60 s of play and at exit one line of frame count, mean fps, 1% low and the
// frames over 25/50/100 ms, then the worst frames of the period with the render thread's waits in them. Per frame: a
// histogram increment, the compare against the worst list, a memset of the wait array. No allocation.
#define FT_BINS 1024 // 0.25 ms bins up to 256 ms; slower frames land in the last bin
#define FT_WORST 5
#define FT_PERIOD_NS 60000000000ull
typedef struct { uint64_t frame, ns, wait[PERF_N], replay; } FtWorst;
static struct {
uint64_t t0, frames, sum, over25, over50, over100;
uint32_t hist[FT_BINS];
FtWorst worst[FT_WORST]; // slowest first
int nworst;
} ft;
static uint64_t summary_t0; // start of the current period (0: not started)
static void ft_report(uint64_t now, const char *tag) {
if (ft.frames && ft.sum) {
uint64_t cut = (ft.frames + 99) / 100, acc = 0, low = 0; // the slowest 1% of frames
for (int b = FT_BINS - 1; b >= 0; b--) {
acc += ft.hist[b];
if (acc >= cut) { low = (uint64_t)(b + 1) * 250000ull; break; } // upper edge of the bin
}
rt_log("frametime: %s: %llu frames in %.1f s, mean %.1f fps, 1%% low %.1f fps, over 25 ms %llu, 50 ms %llu, 100 ms %llu",
tag, (unsigned long long)ft.frames, (now - ft.t0) / 1e9, ft.frames * 1e9 / ft.sum, low ? 1e9 / low : 0.0,
(unsigned long long)ft.over25, (unsigned long long)ft.over50, (unsigned long long)ft.over100);
for (int i = 0; i < ft.nworst; i++) {
const FtWorst *w = &ft.worst[i];
rt_log("frametime: worst frame %llu: %.1f ms; shader translation %.1f, pipeline build %.1f, texture upload %.1f (astc %.1f), replay %.1f, fence/present wait %.1f, pacing %.1f",
(unsigned long long)w->frame, w->ns / 1e6, w->wait[PERF_XLATE] / 1e6, w->wait[PERF_PIPE] / 1e6,
w->wait[PERF_TEX] / 1e6, w->wait[PERF_ASTC] / 1e6, w->replay / 1e6,
(w->wait[PERF_FENCE] + w->wait[PERF_DRAIN]) / 1e6, w->wait[PERF_PACE] / 1e6);
}
}
memset(&ft, 0, sizeof ft);
ft.t0 = now;
}
__attribute__((weak)) void gfx_exit_log(void) {}
void perf_summary_flush(void) { // at exit (prof_write)
if (summary_t0) {
ft_report(kern_now_ns(), "exit");
gfx_exit_log();
}
summary_t0 = 0;
}
// Returns the replay time since the previous present.
static uint64_t ft_frame(uint64_t frame, uint64_t now, uint64_t d) {
int64_t rep = plat_xchg64(&frame_replay_ns, 0);
if (!summary_t0) { // the first present starts the period; there is no interval yet
summary_t0 = now;
ft.t0 = now;
return (uint64_t)rep;
}
ft.frames++;
ft.sum += d;
uint64_t b = d / 250000ull;
ft.hist[b < FT_BINS ? b : FT_BINS - 1]++;
if (d > 25000000ull) ft.over25++;
if (d > 50000000ull) ft.over50++;
if (d > 100000000ull) ft.over100++;
if (d > 25000000ull && (ft.nworst < FT_WORST || d > ft.worst[FT_WORST - 1].ns)) {
int i = ft.nworst < FT_WORST ? ft.nworst++ : FT_WORST - 1;
while (i > 0 && ft.worst[i - 1].ns < d) ft.worst[i] = ft.worst[i - 1], i--;
ft.worst[i].frame = frame;
ft.worst[i].ns = d;
memcpy(ft.worst[i].wait, frame_ns, sizeof frame_ns);
ft.worst[i].replay = (uint64_t)rep;
}
if (now - summary_t0 >= FT_PERIOD_NS) {
ft_report(now, "60 s");
summary_t0 = now;
}
return (uint64_t)rep;
}
// The second line of the 60-frame window: GPU busy time (timestamps), the time threads waited for GPU fences, VRAM and
// process memory, and which side holds the frame back. pace: the frame waited for pacing (vsync, the frame limiter) at
// least 15% of the window; gpu: the GPU was busy for 80% of the frame interval or more (without timestamps: threads
// waited for fences for half the interval or more); cpu otherwise.
static void perf_gpu_line(uint64_t frame, double interval_ms, double pace_share, uint64_t n) {
PerfGpu g;
memset(&g, 0, sizeof g);
if (perf_gpu_stats) perf_gpu_stats(&g);
double rw = plat_xchg64(&render_wait_ns, 0) / 1e6 / n, ow = plat_xchg64(&other_wait_ns, 0) / 1e6 / n;
double gpu_ms = g.have_gpu ? g.gpu_ns / 1e6 / n : 0;
char gs[96], vs[96];
if (g.have_gpu) snprintf(gs, sizeof gs, "gpu avg %.2f ms (%llu parts timed, %llu not)", gpu_ms,
(unsigned long long)g.gpu_parts, (unsigned long long)g.gpu_missed);
else snprintf(gs, sizeof gs, "gpu avg n/a");
if (g.have_vram) snprintf(vs, sizeof vs, "vram %.0f of %.0f MiB%s", g.vram_used / 1048576.0, g.vram_budget / 1048576.0,
g.vram_exact ? "" : " (own allocations of the heap size)");
else snprintf(vs, sizeof vs, "vram n/a");
uint64_t res, prv;
plat_process_memory(&res, &prv);
double ws = res / 1048576.0, priv = prv / 1048576.0;
const char *bound = pace_share >= 0.15 ? "pace"
: g.have_gpu ? (gpu_ms >= 0.8 * interval_ms ? "gpu" : "cpu")
: (rw + ow >= 0.5 * interval_ms ? "gpu" : "cpu");
rt_log("perf: frame %llu: %s; fence wait per frame: render thread %.2f ms, other threads %.2f ms; %s; ram working set "
"%.0f MiB, private %.0f MiB; bound: %s",
(unsigned long long)frame, gs, rw, ow, vs, ws, priv, bound);
}
int rt_ab(void) {
static int64_t period = -1; // us of world clock; 0: off
if (period < 0) {
const char *e = cfg_get("TOTK_AB");
period = e && *e ? (int64_t)(atof(e) * 1e6) : 0;
}
int64_t w = period ? frame_step_world_us() : -1;
if (w < 0) return 0; // before the world clock: A
int64_t k = w / period % 4;
return k == 1 || k == 2;
}
int rt_knob(const char *name, const char *def) {
char key[64];
snprintf(key, sizeof key, "TOTK_%s", name);
const char *e = cfg_get(key);
if (!e || !*e) e = def;
return !strcmp(e, "ab") ? 2 : strcmp(e, "0") != 0;
}
// TOTK_ONE_PER_CORE=1: the process keeps the first logical processor of each physical core, so busy threads (the
// render thread, the guest cores, the job workers) never share a core's execution units with another; "ab" switches
// it with the A/B blocks. Off by default until measured here; Savage Wind Recomp
// reported +4-10% from the same rule on an 8-core SMT CPU.
static void cpu_placement(void) {
static int mode = -1, applied = -1;
static uint64_t all, one;
if (mode < 0) {
mode = rt_knob("ONE_PER_CORE", "0");
int cores = plat_cpu_cores(&all, &one);
if (mode && one && one != all)
rt_log("cpu: %d cores, %d logical processors: one per core%s", cores, __builtin_popcountll(all),
mode == 2 ? " in the B blocks" : "");
else
mode = 0;
}
int on = RT_KNOB_ON(mode);
if (on == applied) return;
applied = on;
plat_process_affinity(on ? one : all);
}
void perf_present(uint64_t frame) {
static uint64_t last, sum, max, n, win_start, ab_b;
static int64_t win_prev[PERF_CNT_N];
prof_render_tid = plat_thread_id();
if (rt_trace & RT_TRACE_SCHED) kern_sched_report(frame);
uint64_t now = kern_now_ns(), d = last ? now - last : 0;
prof_sampler_frame(frame, now, last ? d : 0);
uint64_t rep = ft_frame(frame, now, d); // d is 0 on the first present: it only starts the period
ab_b += rt_ab();
cpu_placement();
if (__atomic_load_n(&overlay, __ATOMIC_RELAXED)) // the overlay's only cost while it is off: this branch
perfmon_frame(now, d, frame_ns, rep, __atomic_load_n(&counts[PERF_CNT_TEX_UP], __ATOMIC_RELAXED));
kern_core_diag_frame(frame, d);
if (flight_on) flight_frame(frame, now, d, frame_ns, counts);
if (!(rt_trace & RT_TRACE_PERF)) {
memset(frame_ns, 0, sizeof frame_ns);
last = now;
return;
}
static uint64_t slow_ns; // TOTK_PERF_SLOW_MS (default 50): frames at least this long get a line of their own
if (!slow_ns) {
const char *e = cfg_get("TOTK_PERF_SLOW_MS");
slow_ns = (uint64_t)(e && atoi(e) > 0 ? atoi(e) : 50) * 1000000ull;
}
if (last) {
sum += d;
n++;
if (d > max) max = d, memcpy(worst_ns, frame_ns, sizeof worst_ns);
if (d >= slow_ns)
rt_log("perf: slow frame %llu: %.1f ms; blocked pipeline %.1f xlate %.1f texture %.1f astc %.1f fence %.1f pacing %.1f",
(unsigned long long)frame, d / 1e6, frame_ns[PERF_PIPE] / 1e6, frame_ns[PERF_XLATE] / 1e6,
frame_ns[PERF_TEX] / 1e6, frame_ns[PERF_ASTC] / 1e6, frame_ns[PERF_FENCE] / 1e6,
frame_ns[PERF_PACE] / 1e6);
}
memset(frame_ns, 0, sizeof frame_ns);
last = now;
if (!win_start) win_start = now;
if (frame % 60) return;
uint64_t span = now - win_start;
int64_t win[PERF_CNT_N];
for (int i = 0; i < PERF_CNT_N; i++) {
int64_t v = __atomic_load_n(&counts[i], __ATOMIC_RELAXED);
win[i] = v - win_prev[i];
win_prev[i] = v;
}
int64_t async = win[PERF_CNT_PIPE_ASYNC], skips = win[PERF_CNT_PIPE_SKIP], up = win[PERF_CNT_TEX_UP];
int64_t reup = win[PERF_CNT_TEX_REUP], sskip = win[PERF_CNT_SHADER_SKIP], waited = win[PERF_CNT_PIPE_WAIT];
int64_t oneshot = win[PERF_CNT_ONESHOT_WAIT];
uint64_t shader_keys, shader_xlated;
gfx_shader_counts(&shader_keys, &shader_xlated);
if (n && span)
rt_log("perf: frame %llu: interval avg %.2f ms max %.2f ms (%.1f fps); render thread blocked: pacing %.0f%% fence %.0f%% drain %.0f%% pipeline build %.0f%% shader xlate %.0f%% texture %.0f%% (astc %.0f%%); pipelines built in background %lld, draws skipped pending %lld, waited %lld, waiting for a shader %lld, one-shot waited %lld; textures uploaded %lld re-uploaded %lld; slowest frame: xlate %.1f ms texture %.1f ms (astc %.1f) pipeline %.1f ms fence %.1f ms; shaders: %llu keys, %llu translated; render busy avg %.2f ms; replay avg %.2f ms; ab %llu/%llu; world %.1f s",
(unsigned long long)frame, sum / 1e6 / n, max / 1e6, n * 1e9 / sum, 100.0 * wait_ns[PERF_PACE] / span,
100.0 * wait_ns[PERF_FENCE] / span, 100.0 * wait_ns[PERF_DRAIN] / span,
100.0 * wait_ns[PERF_PIPE] / span, 100.0 * wait_ns[PERF_XLATE] / span,
100.0 * wait_ns[PERF_TEX] / span, 100.0 * wait_ns[PERF_ASTC] / span, (long long)async, (long long)skips, (long long)waited, (long long)sskip, (long long)oneshot,
(long long)up, (long long)reup, worst_ns[PERF_XLATE] / 1e6, worst_ns[PERF_TEX] / 1e6,
worst_ns[PERF_ASTC] / 1e6, worst_ns[PERF_PIPE] / 1e6, worst_ns[PERF_FENCE] / 1e6,
(unsigned long long)shader_keys, (unsigned long long)shader_xlated,
(span > wait_ns[PERF_PACE] ? span - wait_ns[PERF_PACE] : 0) / 1e6 / n,
plat_xchg64(&replay_ns, 0) / 1e6 / n, (unsigned long long)ab_b, (unsigned long long)n, frame_step_world_us() / 1e6);
if (n && span) perf_gpu_line(frame, sum / 1e6 / n, (double)wait_ns[PERF_PACE] / span, n);
sum = max = n = ab_b = 0;
memset(wait_ns, 0, sizeof wait_ns);
win_start = now;
}
+1 -1
View File
@@ -304,7 +304,7 @@ static void sampler(void *arg) {
sysinfo_cpu_name(s.cpu_name, sizeof s.cpu_name);
if (!nv.tried) nvml_open(s.gpu_name);
if (!nv.ok && !adl.tried) adl_open(s.gpu_name, bus);
PlatLoad *l = plat_load_open(have_luid ? luid : NULL);
PlatLoad *l = plat_load_open(have_luid ? luid : NULL, bus);
plat_mutex_lock(&g_lock);
while (g_smp.want) {
plat_mutex_unlock(&g_lock);
+88
View File
@@ -0,0 +1,88 @@
// The parts of platform.h built on its locks rather than on the OS: recursive mutexes, one-time initialisation and
// events. Every OS shares them.
#include "platform.h"
// ---- recursive mutex: owner is the holder's thread id (0 when free); only the holder writes depth.
void plat_rmutex_lock(PlatRMutex *m) {
uint32_t me = plat_thread_id();
if (__atomic_load_n(&m->owner, __ATOMIC_RELAXED) == me) {
m->depth++;
return;
}
plat_mutex_lock(&m->m);
__atomic_store_n(&m->owner, me, __ATOMIC_RELAXED);
m->depth = 1;
}
void plat_rmutex_unlock(PlatRMutex *m) {
if (--m->depth) return;
__atomic_store_n(&m->owner, 0, __ATOMIC_RELAXED);
plat_mutex_unlock(&m->m);
}
// ---- once: state 0 not run, 1 running, 2 done. Waiters for a running initialiser sleep on one process-wide
// condition variable; initialisers are rare and short.
static PlatMutex once_lock = PLAT_MUTEX_INIT;
static PlatCond once_done = PLAT_COND_INIT;
void plat_once(PlatOnce *o, void (*fn)(void *arg), void *arg) {
if (__atomic_load_n(&o->state, __ATOMIC_ACQUIRE) == 2) return;
int32_t expected = 0;
if (__atomic_compare_exchange_n(&o->state, &expected, 1, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
fn(arg);
plat_mutex_lock(&once_lock);
__atomic_store_n(&o->state, 2, __ATOMIC_RELEASE);
plat_cond_broadcast(&once_done);
plat_mutex_unlock(&once_lock);
return;
}
plat_mutex_lock(&once_lock);
while (__atomic_load_n(&o->state, __ATOMIC_ACQUIRE) != 2) plat_cond_wait(&once_done, &once_lock);
plat_mutex_unlock(&once_lock);
}
// ---- events
void plat_event_init(PlatEvent *e, int manual, int initially_set) {
plat_mutex_init(&e->m);
plat_cond_init(&e->c);
e->manual = manual;
e->set = initially_set;
}
void plat_event_set(PlatEvent *e) {
plat_mutex_lock(&e->m);
e->set = 1;
if (e->manual) plat_cond_broadcast(&e->c);
else plat_cond_signal(&e->c);
plat_mutex_unlock(&e->m);
}
void plat_event_reset(PlatEvent *e) {
plat_mutex_lock(&e->m);
e->set = 0;
plat_mutex_unlock(&e->m);
}
void plat_event_wait(PlatEvent *e) {
plat_mutex_lock(&e->m);
while (!e->set) plat_cond_wait(&e->c, &e->m);
if (!e->manual) e->set = 0;
plat_mutex_unlock(&e->m);
}
int plat_event_wait_ns(PlatEvent *e, uint64_t ns) {
uint64_t end = plat_now_ns() + ns;
plat_mutex_lock(&e->m);
while (!e->set) {
uint64_t now = plat_now_ns();
if (now >= end) break;
plat_cond_wait_ns(&e->c, &e->m, end - now);
}
int got = e->set;
if (got && !e->manual) e->set = 0;
plat_mutex_unlock(&e->m);
return got;
}
File diff suppressed because it is too large. Load diff
+275 -10
View File
@@ -1,11 +1,15 @@
// Platform layer: threads, time, locks. New runtime code calls this instead of the OS;
// one implementation per OS (win32.c). The FP control register lives in the per-architecture fp_*.h headers,
// inline, because lifted FP code touches it on every operation.
// Platform layer: threads, time, locks, atomics, files. Runtime code calls this instead of the OS; one implementation
// per OS (win32.c, linux.c). The FP control register lives in the per-architecture fp_*.h headers, inline, because
// lifted FP code touches it on every operation.
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#if defined(__x86_64__) || defined(_M_X64)
#include "fp_x86.h"
#include <immintrin.h>
#endif
// ---- threads
@@ -14,15 +18,70 @@ typedef void (*PlatThreadFn)(void *arg);
// Starts fn(arg) on a new thread named name (shown in debuggers and profilers; may be NULL). NULL on failure.
PlatThread *plat_thread_new(PlatThreadFn fn, void *arg, const char *name);
// The same with the stack size given: stack bytes reserved for the thread (0: PLAT_STACK_DEFAULT). The default is the
// same on every OS, so a thread that fits on Windows fits elsewhere.
#define PLAT_STACK_DEFAULT (16u << 20)
PlatThread *plat_thread_new_ex(PlatThreadFn fn, void *arg, const char *name, size_t stack);
// Starts a thread nobody joins (plat_thread_new_ex + plat_thread_detach). 1 on success.
int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack);
void plat_thread_join(PlatThread *t); // waits for fn to return, then frees t
// Waits at most ns for fn to return: 1 when it did (t is freed), 0 when it still runs (t stays valid).
int plat_thread_join_ns(PlatThread *t, uint64_t ns);
void plat_thread_detach(PlatThread *t); // frees t; the thread runs on
// Restricts t (NULL: the calling thread) to the logical processors in mask (bit n = processor n of the first group).
// Returns the previous mask, 0 on failure.
uint64_t plat_thread_affinity(PlatThread *t, uint64_t mask);
uint32_t plat_cpu_count(void); // logical processors
uint32_t plat_cpu_count(void); // logical processors of the machine (system info)
uint32_t plat_cpu_usable(void); // the ones this process may run on (its affinity mask): size thread pools by it
uint64_t plat_thread_cpu_ns(uint32_t tid); // CPU time of the thread with id tid (plat_thread_id) so far; 0 unknown
// The logical processors the process may use (bit n = processor n, the first 64), into *all, and the lowest of each
// physical core among them into *first; returns the number of physical cores, 0 when unknown.
int plat_cpu_cores(uint64_t *all, uint64_t *first);
int plat_process_affinity(uint64_t mask); // every thread of the process to the processors in mask; 1 on success
// Interrupting a thread (kernel.c's asynchronous preemption). A thread makes its own handle; another thread stops it
// wherever it runs, reads where it is, may send it elsewhere, and lets it go on. While it is stopped the caller must not
// wait for anything the stopped thread could hold.
typedef struct PlatIntr PlatIntr;
PlatIntr *plat_intr_self(void); // NULL on failure
void plat_intr_free(PlatIntr *t);
// Stops t and reads its instruction and stack pointers: 1 when stopped, 0 when it did not stop in time (a thread
// blocked in the OS); t then runs on untouched.
int plat_intr_stop(PlatIntr *t, uint64_t *rip, uint64_t *rsp);
// Lets a stopped t go on: at to when to is nonzero, else where it stopped. 0 when the redirection failed (t then goes
// on where it stopped).
int plat_intr_resume(PlatIntr *t, uint64_t to);
// Fibers: execution contexts with stacks of their own that threads switch between; a fiber may resume on another
// thread. Each switch keeps the callee-saved registers and the floating-point control state.
typedef struct PlatFiber PlatFiber;
PlatFiber *plat_fiber_self(void); // the calling thread's own context as a fiber, once before its first switch
void plat_fiber_unself(void); // the calling thread stops being a fiber (it is on its own context again)
// A fiber that runs fn(arg) on a stack of stack bytes (reserved, committed as it grows); fn never returns.
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg);
void plat_fiber_switch(PlatFiber *to); // from the fiber the calling thread runs
void plat_fiber_free(PlatFiber *f); // a fiber nobody runs
// Scheduling priority, Windows' seven levels: of the calling thread, or of t. On Linux a thread lowered without the
// right to raise its priority again (CAP_SYS_NICE, or an RLIMIT_NICE of 20) could never return to normal, and the
// runtime lowers threads that later take shared locks at normal priority; without that right every change is skipped
// and threads keep the normal priority. Levels above normal need the same right.
enum { PLAT_PRIO_IDLE = -15, PLAT_PRIO_LOWEST = -2, PLAT_PRIO_BELOW_NORMAL = -1, PLAT_PRIO_NORMAL = 0,
PLAT_PRIO_ABOVE_NORMAL = 1, PLAT_PRIO_HIGHEST = 2, PLAT_PRIO_TIME_CRITICAL = 15 };
void plat_thread_priority(int prio);
void plat_thread_set_priority(PlatThread *t, int prio);
int plat_thread_priority_get(void); // the calling thread's level
void plat_yield(void); // gives the rest of the time slice to another ready thread
static inline void plat_pause(void) { // spin-wait hint
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#endif
}
// ---- time
uint64_t plat_now_ns(void); // monotonic, high resolution, arbitrary epoch
uint64_t plat_counter(void); // the OS's raw monotonic counter (QPC on Windows), plat_counter_hz per second
uint64_t plat_counter_hz(void);
void plat_timer_1ms(void); // asks the OS for timed waits accurate to 1 ms or better (call before threads)
// t * num / den for a fixed num / den without a 128-bit division per call (__udivti3 took 5% of the game's running
// samples through plat_now_ns and rt_ticks): t * q + (t * f >> 64) with q = num / den and f = (num % den) * 2^64 / den.
// Monotonic in t and at most one below the exact quotient while t * (num % den) / den fits 64 bits.
@@ -34,9 +93,19 @@ static inline uint64_t plat_scaled(PlatScale s, uint64_t t) {
return t * s.q + (uint64_t)(((unsigned __int128)t * s.f) >> 64);
}
void plat_sleep_ns(uint64_t ns); // returns no earlier than ns later; overshoot well under 1 ms
// Sleeps about ns on the OS's high-resolution timer without spinning: wakes up to PLAT_WAIT_LATE_NS late, never early.
void plat_wait_ns(uint64_t ns);
#ifdef _WIN32
#define PLAT_WAIT_LATE_NS 500000u // high-resolution waitable timer
#else
#define PLAT_WAIT_LATE_NS 60000u // clock_nanosleep with a 1 us timer slack (plat_timer_1ms), scheduler wakeup
#endif
static inline uint64_t plat_now_ms(void) { return plat_now_ns() / 1000000u; }
void plat_sleep_ms(uint32_t ms); // coarse sleep: the scheduler's granularity, no spinning
// ---- locks (not recursive) and condition variables; zero-initialised storage is a valid unlocked lock and an empty
// condition variable (PLAT_MUTEX_INIT / PLAT_COND_INIT spell it).
// ---- locks and condition variables. A PlatMutex is a reader-writer lock taken exclusively (plat_mutex_lock) or
// shared (plat_mutex_lock_shared), not recursive, like Windows' SRW lock it is on Windows. Zero-initialised storage is
// a valid unlocked lock and an empty condition variable (PLAT_MUTEX_INIT / PLAT_COND_INIT spell it).
typedef struct { void *p; } PlatMutex;
typedef struct { void *p; } PlatCond;
#define PLAT_MUTEX_INIT {0}
@@ -45,18 +114,139 @@ void plat_mutex_init(PlatMutex *m);
void plat_mutex_lock(PlatMutex *m);
int plat_mutex_trylock(PlatMutex *m); // 1 when acquired
void plat_mutex_unlock(PlatMutex *m);
void plat_mutex_lock_shared(PlatMutex *m);
int plat_mutex_trylock_shared(PlatMutex *m); // 1 when acquired
void plat_mutex_unlock_shared(PlatMutex *m);
void plat_cond_init(PlatCond *c);
void plat_cond_wait(PlatCond *c, PlatMutex *m);
void plat_cond_wait(PlatCond *c, PlatMutex *m); // m held exclusively
int plat_cond_wait_ns(PlatCond *c, PlatMutex *m, uint64_t ns); // 0 on timeout (spurious wakeups return 1 too)
static inline int plat_cond_wait_ms(PlatCond *c, PlatMutex *m, uint32_t ms) {
return plat_cond_wait_ns(c, m, (uint64_t)ms * 1000000u);
}
void plat_cond_signal(PlatCond *c);
void plat_cond_broadcast(PlatCond *c);
// ---- memory
// A mutex the thread that holds it may take again; unlocked as many times as locked. Zero-initialised is unlocked.
typedef struct { PlatMutex m; uint32_t owner, depth; } PlatRMutex;
#define PLAT_RMUTEX_INIT {PLAT_MUTEX_INIT, 0, 0}
void plat_rmutex_lock(PlatRMutex *m);
void plat_rmutex_unlock(PlatRMutex *m);
// One-time initialisation: the first caller runs fn(arg), every other caller waits until it returned. Zero-initialised
// (PLAT_ONCE_INIT) has not run.
typedef struct { volatile int32_t state; } PlatOnce;
#define PLAT_ONCE_INIT {0}
void plat_once(PlatOnce *o, void (*fn)(void *arg), void *arg);
// An event: set wakes its waiters; an auto-reset event lets one waiter through and clears itself, a manual-reset one
// stays set until reset.
typedef struct { PlatMutex m; PlatCond c; int set, manual; } PlatEvent;
void plat_event_init(PlatEvent *e, int manual, int initially_set);
void plat_event_set(PlatEvent *e);
void plat_event_reset(PlatEvent *e);
void plat_event_wait(PlatEvent *e);
int plat_event_wait_ns(PlatEvent *e, uint64_t ns); // 1 when the event was set, 0 on timeout
// ---- atomics: full barriers, explicit widths (Windows' Interlocked* operate on 32-bit LONG). The value returned is
// noted per function: new for inc/dec/add, previous for xadd/xchg/cas/or.
#define PLAT_SC __ATOMIC_SEQ_CST
static inline int32_t plat_inc32(volatile int32_t *p) { return __atomic_add_fetch(p, 1, PLAT_SC); }
static inline int32_t plat_dec32(volatile int32_t *p) { return __atomic_sub_fetch(p, 1, PLAT_SC); }
static inline int32_t plat_add32(volatile int32_t *p, int32_t v) { return __atomic_add_fetch(p, v, PLAT_SC); }
static inline int32_t plat_xadd32(volatile int32_t *p, int32_t v) { return __atomic_fetch_add(p, v, PLAT_SC); }
static inline int32_t plat_xchg32(volatile int32_t *p, int32_t v) { return __atomic_exchange_n(p, v, PLAT_SC); }
static inline int32_t plat_cas32(volatile int32_t *p, int32_t xchg, int32_t cmp) {
__atomic_compare_exchange_n(p, &cmp, xchg, 0, PLAT_SC, PLAT_SC);
return cmp;
}
static inline int64_t plat_inc64(volatile int64_t *p) { return __atomic_add_fetch(p, 1, PLAT_SC); }
static inline int64_t plat_dec64(volatile int64_t *p) { return __atomic_sub_fetch(p, 1, PLAT_SC); }
static inline int64_t plat_add64(volatile int64_t *p, int64_t v) { return __atomic_add_fetch(p, v, PLAT_SC); }
static inline int64_t plat_xadd64(volatile int64_t *p, int64_t v) { return __atomic_fetch_add(p, v, PLAT_SC); }
static inline int64_t plat_xchg64(volatile int64_t *p, int64_t v) { return __atomic_exchange_n(p, v, PLAT_SC); }
static inline int64_t plat_or64(volatile int64_t *p, int64_t v) { return __atomic_fetch_or(p, v, PLAT_SC); }
static inline int64_t plat_cas64(volatile int64_t *p, int64_t xchg, int64_t cmp) {
__atomic_compare_exchange_n(p, &cmp, xchg, 0, PLAT_SC, PLAT_SC);
return cmp;
}
static inline void *plat_xchgp(void *volatile *p, void *v) { return __atomic_exchange_n(p, v, PLAT_SC); }
static inline void *plat_casp(void *volatile *p, void *xchg, void *cmp) {
__atomic_compare_exchange_n(p, &cmp, xchg, 0, PLAT_SC, PLAT_SC);
return cmp;
}
// ---- virtual memory: ranges reserved at fixed addresses below 2^39, their pages committed, decommitted and protected
// at 4 KiB granularity.
enum { PLAT_PROT_NONE, PLAT_PROT_READ, PLAT_PROT_RW };
enum { PLAT_MEM_FREE = -2, PLAT_MEM_RESERVED = -1 }; // plat_mem_state besides PLAT_PROT_* for committed pages
// Reserves [addr, addr + n) inaccessible, exactly there: 0 when any of it is taken. watch: writes to its pages are
// tracked for plat_mem_written.
int plat_mem_reserve(uint64_t addr, uint64_t n, int watch);
// n bytes of read-write memory anywhere, zero, backed only as pages are first written (plat_mem_protect may seal them);
// NULL on failure. Kept until exit.
void *plat_mem_alloc(size_t n);
int plat_mem_commit(uint64_t addr, size_t n); // read-write pages inside a reserved range; 1 on success
int plat_mem_decommit(uint64_t addr, uint64_t n); // contents dropped: committed again, the pages read zero
int plat_mem_protect(uint64_t addr, uint64_t n, int prot); // committed pages only; 1 on success
// The page at addr: PLAT_PROT_* when committed, else PLAT_MEM_RESERVED or PLAT_MEM_FREE (not reserved by us). *end
// gets the end of the run of pages in the same state, at most limit.
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end);
// The pages of a watched reservation written since the last reset, up to max page addresses into pages, in address
// order; reset: those reported start over as unwritten. UINT64_MAX when writes to the range are not tracked.
uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t max, int reset);
// A write that faulted on a read-only page at addr goes to fn first; fn returns 1 when it made the page writable and
// the write should run again, 0 to let the fault go on (to the crash handler). One hook per process.
void plat_mem_fault_hook(int (*fn)(uint64_t addr));
// Starts this program again with the same arguments and ends this process with the new one's exit code (Windows), or
// replaces this process with it (Linux). Environment changes made before the call are passed on.
_Noreturn void plat_reexec(void);
// A crash in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
// overflow) goes to fn, which reports it and must end the process. Faults in other modules (drivers) are left to the
// OS. The write-fault hook above runs first. On Windows abort() (a failed assert, std::terminate) and an invalid
// parameter to a C runtime function go there too, with the host frames in this executable: the C runtime ends the
// process for both with 0xc0000409 and no exception.
typedef struct {
const char *what; // "access violation", "illegal instruction", ...
uint32_t code; // the Windows exception code or the signal number
uintptr_t rip, offset; // the faulting instruction, and its offset in the executable
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
uint64_t addr; // its address when access >= 0
int nframes; // abort and invalid parameter: return addresses in this executable, innermost first
uintptr_t frames[16]; // (rip is frames[0]); 0 for a fault, whose rip is exact
} 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
int plat_mem_readable(uint64_t addr, uint64_t n); // every page of [addr, addr + n) can be read; no fault
void *plat_aligned_alloc(size_t size, size_t align); // free with plat_aligned_free; NULL on failure
void plat_aligned_free(void *p);
// ---- process
_Noreturn void plat_exit(int code); // ends the process at once: other threads stop, no atexit handlers run
int plat_setenv(const char *name, const char *value); // value NULL removes it; 1 on success
uint32_t plat_process_id(void);
// CPU time of the process since it started (user and kernel), and wall time since it started, in ns; 0 when unknown.
uint64_t plat_process_cpu_ns(void);
uint64_t plat_process_uptime_ns(void);
uint64_t plat_unix_time_ns(void); // wall clock, ns since 1970
void plat_local_time(struct tm *out); // the wall clock now in the local time zone
int plat_localtime(int64_t unix_s, struct tm *out); // a POSIX time in the local time zone; 0 out of range
int plat_gmtime(int64_t unix_s, struct tm *out); // the same in UTC
long plat_tz_west(void); // the local zone's standard offset, seconds west of UTC (UTC-5: 18000)
// Runs this executable again with the same arguments and env_name=env_value added to its environment, for probes that
// expect the child to stop with a fatal. Its stdout and stderr go into out (NUL-terminated, the rest discarded); waits
// at most timeout_ms (0: no limit) and kills it after that. Returns the exit code, -1 when it could not start.
int plat_run_self(const char *env_name, const char *env_value, char *out, size_t cap, uint32_t timeout_ms);
// ---- randomness
int plat_random(void *out, size_t n); // cryptographically random bytes; 1 on success
// ---- files. Paths are UTF-8 with either separator; each implementation converts to what the OS wants.
int plat_file_exists(const char *path); // 1 for a file (not a directory)
int plat_dir_exists(const char *path); // 1 for a directory
// path made absolute against the working directory (not resolved further); 0 when it does not fit cap.
int plat_full_path(const char *path, char *out, size_t cap);
void plat_file_mkdir_p(const char *path); // creates every missing directory of path, itself included
// Whole file into a malloc'd buffer with a NUL after the last byte; NULL when missing, empty or over 1 GiB.
uint8_t *plat_file_read(const char *path, uint64_t *size);
@@ -66,6 +256,15 @@ int plat_file_replace(const char *from, const char *to); // rename, replacing t
// one, never a torn or zero-filled one. 1 on success.
int plat_file_write_atomic(const char *path, const void *data, size_t n);
int plat_file_flush(const char *path); // flushes a written file's data to the disk; 1 on success
int plat_file_sync(FILE *f); // fflush, then the file's data to the disk; 1 on success
// fseek and ftell with 64-bit offsets (a long is 32 bits on Windows).
#ifdef _WIN32
static inline int plat_fseek64(FILE *f, int64_t off, int whence) { return _fseeki64(f, off, whence); }
static inline int64_t plat_ftell64(FILE *f) { return _ftelli64(f); }
#else
static inline int plat_fseek64(FILE *f, int64_t off, int whence) { return fseeko(f, (off_t)off, whence); }
static inline int64_t plat_ftell64(FILE *f) { return (int64_t)ftello(f); }
#endif
void plat_file_delete(const char *path);
int plat_file_truncate(const char *path, uint64_t size); // 1 on success
int64_t plat_file_size(const char *path); // bytes of a file, -1 when missing or unreadable
@@ -78,11 +277,73 @@ int plat_append_write(PlatAppend *f, const void *data, size_t n); // 1 when al
// Calls cb(name, ctx) for each file in dir whose name ends in ext (".in"); name has no directory part. cb returns 0 to
// stop. Returns 0 when dir cannot be listed.
int plat_file_list(const char *dir, const char *ext, int (*cb)(const char *name, void *ctx), void *ctx);
// Open files for positional reads: any thread, in parallel on one handle, no file pointer.
typedef struct PlatFile PlatFile;
PlatFile *plat_file_open(const char *path); // read-only; NULL when missing or unreadable
// The same without the OS's file cache (reads go to the disk into the buffer): offsets, sizes and buffers aligned to
// the sector size.
PlatFile *plat_file_open_unbuffered(const char *path);
void plat_file_close(PlatFile *f);
// Bytes read at off (0 at or past the end of the file), -1 on an I/O error, -2 when the OS refused to write into buf
// (a read-only page).
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off);
// What changes when the file is replaced or written: its volume, file id, size, last write and creation (on Linux,
// status change) times in the OS's units. 0 on failure.
typedef struct { uint64_t volume, file_id, size, write_time, create_time; } PlatFileId;
int plat_file_id(PlatFile *f, PlatFileId *out);
const void *plat_file_map(PlatFile *f); // a read-only view of the whole file, kept until exit; NULL
// ---- the host file system under the game's save data (fs_host.c). Calls that can fail return PLAT_FS_OK (0) or the
// reason; other processes may open the same files meanwhile.
enum { PLAT_FS_OK, PLAT_FS_NOT_FOUND, PLAT_FS_EXISTS, PLAT_FS_NOT_EMPTY, PLAT_FS_BUSY, PLAT_FS_ERROR };
enum { PLAT_FS_MISSING = -1, PLAT_FS_FILE, PLAT_FS_DIR, PLAT_FS_LINK }; // plat_fs_kind; LINK: a symbolic link or
// reparse point, never followed
int plat_fs_kind(const char *path);
int plat_fs_mkdir(const char *path); // PLAT_FS_EXISTS when something is there
int plat_fs_rmdir(const char *path); // an empty directory, or a link to one
int plat_fs_unlink(const char *path); // a file or a link; a read-only one too
// Renames a file or directory; never replaces (PLAT_FS_EXISTS). durable: the change is on the disk at return.
int plat_fs_rename(const char *from, const char *to, int durable);
int plat_fs_link(const char *existing, const char *path); // a hard link
int plat_fs_links(const char *path); // the file's hard link count, 0 when unknown
void plat_fs_sync_dir(const char *path); // a directory's entries to the disk, as far as the FS allows
// Why dir could not be renamed, for the log: into out "no permission to rename it" when this process may not delete
// it, else the programs holding a file under it ("explorer.exe 1234, ..."), else an empty string. Windows refuses to
// rename a directory with any file open under it; elsewhere out is always empty.
void plat_fs_busy_reason(const char *dir, char *out, size_t cap);
// cb(name, kind, size, ctx) for each entry of dir but "." and ".."; cb returns 0 to stop. The listing's error.
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx);
// An existing file for reading and, with write, writing; *err (may be NULL) gets the reason on failure.
PlatFile *plat_fs_open(const char *path, int write, int *err);
PlatFile *plat_fs_create(const char *path, int *err); // a new file for writing; PLAT_FS_EXISTS when there
int64_t plat_file_pwrite(PlatFile *f, const void *buf, uint32_t n, uint64_t off); // bytes written, -1 on error
int plat_file_set_size(PlatFile *f, uint64_t size); // PLAT_FS_*
int plat_file_fsync(PlatFile *f); // the file's data to the disk; 1 on success
int plat_temp_dir(char *out, size_t cap); // the user's temporary directory, no trailing separator
int plat_exe_path(char *out, size_t cap); // path of the running executable; 0 on failure
uintptr_t plat_exe_base(void); // where the executable is loaded
// The module (executable or library) holding code address addr: its file name without directories into name, its load
// address into *base. 0 when none.
int plat_module_of(uintptr_t addr, char *name, size_t cap, uintptr_t *base);
#define PLAT_PATH_MAX 1024 // path buffers of the runtime
#ifdef _WIN32
#define PLAT_EXE_SUFFIX ".exe" // of the programs the runtime starts (totk_xlate)
#else
#define PLAT_EXE_SUFFIX ""
#endif
// The last directory separator in path ('/' or '\\'), NULL when there is none.
static inline char *plat_last_sep(const char *path) {
char *last = NULL;
for (const char *p = path; *p; p++)
if (*p == '/' || *p == '\\') last = (char *)p;
return last;
}
// Environment variable as UTF-8 into out; its length, or 0 when unset, empty or too long for cap.
size_t plat_getenv(const char *name, char *out, size_t cap);
// The user's UI language as a BCP-47 tag ("ru-RU", "en-US"); "" when the system gives none. Valid for the process.
const char *plat_ui_language(void);
// The user's regional format (locale) as a BCP-47 tag; "" when the system gives none. Valid for the process.
const char *plat_user_locale(void);
uint32_t plat_thread_id(void); // id of the calling thread, for unique file names
// A system font file with the glyphs of a catalog language ("ja", "ko", "zh-Hans", "zh-Hant"): the menu shows each
// language's name in its own script. Copies the UTF-8 path into out; 0 when the system has none.
@@ -105,8 +366,9 @@ void *plat_lib_sym(void *lib, const char *name);
// the system does not give is -1.
typedef struct PlatLoad PlatLoad;
// Opens the counters: CPU times, the thermal zones, and the GPU engine and memory counters of the adapter whose LUID is
// gpu_luid (8 bytes, Vulkan's deviceLUID; NULL: the busiest adapter). Never NULL.
PlatLoad *plat_load_open(const uint8_t *gpu_luid);
// gpu_luid (8 bytes, Vulkan's deviceLUID; NULL: the busiest adapter) or, where the system names GPUs by PCI address,
// on PCI bus pci_bus (-1: unknown). Never NULL.
PlatLoad *plat_load_open(const uint8_t *gpu_luid, int pci_bus);
void plat_load_close(PlatLoad *l);
// Since the previous call on l (the first call gives -1): CPU use of the machine and of this process, percent of all
// logical processors, and the hottest ACPI thermal zone in degrees Celsius.
@@ -119,6 +381,9 @@ void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb);
// Commit charge in MB: this process's private bytes, the system's commit in use and its limit (RAM plus page files).
// An allocation fails when the system's reaches the limit, however much physical memory is free. -1 when unknown.
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb);
// This process's memory in bytes: resident (the working set) and private (Windows: committed private bytes; Linux: the
// resident anonymous pages). 0 when unknown.
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes);
// ---- child processes: a program with stdin and stdout piped to us (stderr discarded), no console window.
typedef struct PlatChild PlatChild;
+780 -16
View File
@@ -1,11 +1,16 @@
// Windows implementation of platform.h: CreateThread, QueryPerformanceCounter, high-resolution waitable timers,
// SRW locks and condition variables.
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <bcrypt.h>
#include <pdh.h>
#include <pdhmsg.h>
#include <psapi.h>
#include <restartmanager.h>
#include <shellapi.h>
#include <timeapi.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
@@ -31,11 +36,16 @@ static DWORD WINAPI thread_main(LPVOID p) {
}
PlatThread *plat_thread_new(PlatThreadFn fn, void *arg, const char *name) {
return plat_thread_new_ex(fn, arg, name, 0);
}
PlatThread *plat_thread_new_ex(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
PlatThread *t = calloc(1, sizeof *t);
if (!t) return NULL;
t->fn = fn;
t->arg = arg;
t->h = CreateThread(NULL, 0, thread_main, t, CREATE_SUSPENDED, NULL);
t->h = CreateThread(NULL, stack ? stack : PLAT_STACK_DEFAULT, thread_main, t,
CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
if (!t->h) { free(t); return NULL; }
if (name) {
wchar_t w[64];
@@ -51,12 +61,150 @@ void plat_thread_join(PlatThread *t) {
free(t);
}
int plat_thread_join_ns(PlatThread *t, uint64_t ns) {
uint64_t ms = (ns + 999999) / 1000000;
if (WaitForSingleObject(t->h, ms >= INFINITE ? INFINITE - 1 : (DWORD)ms) != WAIT_OBJECT_0) return 0;
CloseHandle(t->h);
free(t);
return 1;
}
// The thread keeps using t->fn and t->arg, so the struct stays allocated (a few bytes per detached thread).
void plat_thread_detach(PlatThread *t) {
CloseHandle(t->h);
t->h = NULL;
}
int plat_cpu_cores(uint64_t *all, uint64_t *first) {
DWORD_PTR proc, sys;
*all = *first = 0;
if (!GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys)) return 0;
*all = proc;
DWORD len = 0;
GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &len);
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buf = len ? malloc(len) : NULL;
int cores = 0;
if (buf && GetLogicalProcessorInformationEx(RelationProcessorCore, buf, &len))
for (DWORD off = 0; off < len;) {
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)((char *)buf + off);
KAFFINITY m = e->Processor.GroupMask[0].Mask & proc;
if (e->Processor.GroupMask[0].Group == 0 && m) *first |= m & -m, cores++; // the lowest of the core's
off += e->Size;
}
free(buf);
return cores;
}
int plat_process_affinity(uint64_t mask) { return SetProcessAffinityMask(GetCurrentProcess(), (DWORD_PTR)mask) != 0; }
uint64_t plat_thread_cpu_ns(uint32_t tid) {
HANDLE th = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tid);
FILETIME a, b, k, u;
uint64_t ns = 0;
if (th && GetThreadTimes(th, &a, &b, &k, &u))
ns = (((uint64_t)k.dwHighDateTime << 32 | k.dwLowDateTime) + ((uint64_t)u.dwHighDateTime << 32 | u.dwLowDateTime)) *
100;
if (th) CloseHandle(th);
return ns;
}
// ---- thread interruption: SuspendThread, Get/SetThreadContext, ResumeThread.
struct PlatIntr {
CONTEXT ctx; // first: CONTEXT wants 16-byte alignment
HANDLE h;
};
PlatIntr *plat_intr_self(void) {
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION,
FALSE, GetCurrentThreadId());
PlatIntr *t = h ? _aligned_malloc(sizeof *t, 16) : NULL;
if (!t) {
if (h) CloseHandle(h);
return NULL;
}
t->h = h;
return t;
}
void plat_intr_free(PlatIntr *t) {
if (!t) return;
CloseHandle(t->h);
_aligned_free(t);
}
int plat_intr_stop(PlatIntr *t, uint64_t *rip, uint64_t *rsp) {
if (SuspendThread(t->h) == (DWORD)-1) return 0;
memset(&t->ctx, 0, sizeof t->ctx);
t->ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; // set back whole, so the kernel restores every GPR
if (!GetThreadContext(t->h, &t->ctx)) {
ResumeThread(t->h);
return 0;
}
*rip = t->ctx.Rip;
*rsp = t->ctx.Rsp;
return 1;
}
int plat_intr_resume(PlatIntr *t, uint64_t to) {
int ok = 1;
if (to) {
t->ctx.Rip = to;
ok = SetThreadContext(t->h, &t->ctx) != 0;
}
ResumeThread(t->h);
return ok;
}
// ---- fibers: Windows fibers with FIBER_FLAG_FLOAT_SWITCH (the MXCSR and x87 control word switch with them).
struct PlatFiber {
void *h;
void (*fn)(void *arg);
void *arg;
};
static __thread PlatFiber self_fiber;
PlatFiber *plat_fiber_self(void) {
self_fiber.h = ConvertThreadToFiberEx(NULL, FIBER_FLAG_FLOAT_SWITCH);
return self_fiber.h ? &self_fiber : NULL;
}
void plat_fiber_unself(void) {
ConvertFiberToThread();
self_fiber.h = NULL;
}
static void WINAPI fiber_start(void *p) {
PlatFiber *f = p;
f->fn(f->arg);
}
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg) {
PlatFiber *f = malloc(sizeof *f);
if (!f) return NULL;
f->fn = fn, f->arg = arg;
f->h = CreateFiberEx(0x10000, stack, FIBER_FLAG_FLOAT_SWITCH, fiber_start, f);
if (!f->h) {
free(f);
return NULL;
}
return f;
}
void plat_fiber_switch(PlatFiber *to) { SwitchToFiber(to->h); }
void plat_fiber_free(PlatFiber *f) {
if (!f) return;
DeleteFiber(f->h);
free(f);
}
int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
PlatThread *t = plat_thread_new_ex(fn, arg, name, stack);
if (t) plat_thread_detach(t);
return t != NULL;
}
uint64_t plat_thread_affinity(PlatThread *t, uint64_t mask) {
return (uint64_t)SetThreadAffinityMask(t ? t->h : GetCurrentThread(), (DWORD_PTR)mask);
}
@@ -66,6 +214,25 @@ uint32_t plat_cpu_count(void) {
return n ? (uint32_t)n : 1;
}
// The processors of the process's affinity mask: what `start /affinity`, Task Manager's "Set affinity" or a launcher
// leaves this process. A process that spans processor groups (more than 64 logical processors) gets no mask from
// GetProcessAffinityMask (both masks 0): every active processor counts then.
uint32_t plat_cpu_usable(void) {
DWORD_PTR proc = 0, sys = 0;
if (GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys) && proc && sys) {
uint32_t n = (uint32_t)__builtin_popcountll((unsigned long long)proc);
if (n) return n;
}
return plat_cpu_count();
}
// PLAT_PRIO_* are the THREAD_PRIORITY_* values.
void plat_thread_priority(int prio) { SetThreadPriority(GetCurrentThread(), prio); }
void plat_thread_set_priority(PlatThread *t, int prio) { SetThreadPriority(t->h, prio); }
int plat_thread_priority_get(void) { return GetThreadPriority(GetCurrentThread()); }
void plat_yield(void) { SwitchToThread(); }
// ---- time
static uint64_t qpc_freq(void) {
@@ -78,6 +245,17 @@ static uint64_t qpc_freq(void) {
return freq;
}
uint64_t plat_counter(void) {
LARGE_INTEGER t;
QueryPerformanceCounter(&t);
return (uint64_t)t.QuadPart;
}
uint64_t plat_counter_hz(void) { return qpc_freq(); }
// The default timer tick is 15.6 ms; a 1 ms wait would cost a full tick.
void plat_timer_1ms(void) { timeBeginPeriod(1); }
static PlatScale qpc_ns; // set before main, so no thread reads it half written
__attribute__((constructor)) static void qpc_ns_init(void) { qpc_ns = plat_scale(1000000000u, qpc_freq()); }
@@ -96,24 +274,26 @@ uint64_t plat_now_ns(void) {
static __thread HANDLE sleep_timer;
static __thread int sleep_timer_tried;
void plat_sleep_ns(uint64_t ns) {
uint64_t end = plat_now_ns() + ns;
void plat_wait_ns(uint64_t ns) {
if (!sleep_timer_tried) {
sleep_timer_tried = 1;
sleep_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
}
if (ns > PLAT_SPIN_NS) {
uint64_t coarse = ns - PLAT_SPIN_NS;
if (sleep_timer) {
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)(coarse / 100)}; // relative, 100 ns units
if (SetWaitableTimerEx(sleep_timer, &due, 0, NULL, NULL, NULL, 0))
WaitForSingleObject(sleep_timer, INFINITE);
else Sleep((DWORD)(coarse / 1000000));
} else Sleep((DWORD)(coarse / 1000000));
}
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)(ns / 100)}; // relative, 100 ns units
if (!due.QuadPart) due.QuadPart = -1;
if (sleep_timer && SetWaitableTimerEx(sleep_timer, &due, 0, NULL, NULL, NULL, 0))
WaitForSingleObject(sleep_timer, INFINITE);
else Sleep((DWORD)((ns + 999999) / 1000000));
}
void plat_sleep_ns(uint64_t ns) {
uint64_t end = plat_now_ns() + ns;
if (ns > PLAT_SPIN_NS) plat_wait_ns(ns - PLAT_SPIN_NS);
while (plat_now_ns() < end) SwitchToThread();
}
void plat_sleep_ms(uint32_t ms) { Sleep(ms); }
// ---- locks and condition variables
void plat_mutex_init(PlatMutex *m) { InitializeSRWLock((PSRWLOCK)m); }
@@ -121,6 +301,10 @@ void plat_mutex_lock(PlatMutex *m) { AcquireSRWLockExclusive((PSRWLOCK)m); }
int plat_mutex_trylock(PlatMutex *m) { return TryAcquireSRWLockExclusive((PSRWLOCK)m) != 0; }
void plat_mutex_unlock(PlatMutex *m) { ReleaseSRWLockExclusive((PSRWLOCK)m); }
void plat_mutex_lock_shared(PlatMutex *m) { AcquireSRWLockShared((PSRWLOCK)m); }
int plat_mutex_trylock_shared(PlatMutex *m) { return TryAcquireSRWLockShared((PSRWLOCK)m) != 0; }
void plat_mutex_unlock_shared(PlatMutex *m) { ReleaseSRWLockShared((PSRWLOCK)m); }
void plat_cond_init(PlatCond *c) { InitializeConditionVariable((PCONDITION_VARIABLE)c); }
void plat_cond_wait(PlatCond *c, PlatMutex *m) { SleepConditionVariableSRW((PCONDITION_VARIABLE)c, (PSRWLOCK)m, INFINITE, 0); }
@@ -150,6 +334,21 @@ int plat_file_exists(const char *path) {
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
}
int plat_dir_exists(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
DWORD a = GetFileAttributesW(w);
return a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY);
}
int plat_full_path(const char *path, char *out, size_t cap) {
wchar_t w[WPATH], full[WPATH];
if (!widen(path, w, WPATH)) return 0;
DWORD n = GetFullPathNameW(w, WPATH, full, NULL);
if (!n || n >= WPATH) return 0;
return WideCharToMultiByte(CP_UTF8, 0, full, -1, out, (int)cap, NULL, NULL) != 0;
}
void plat_file_mkdir_p(const char *path) {
wchar_t tmp[WPATH];
if (!widen(path, tmp, WPATH)) return;
@@ -223,7 +422,7 @@ int plat_file_replace(const char *from, const char *to) {
int plat_file_write_atomic(const char *path, const void *data, size_t n) {
char tmp[WPATH + 48];
wchar_t a[WPATH], b[WPATH];
int len = snprintf(tmp, sizeof tmp, "%s.%lu.%lu.tmp", path, GetCurrentProcessId(), GetCurrentThreadId());
int len = snprintf(tmp, sizeof tmp, "%s.%u.%u.tmp", path, (unsigned)GetCurrentProcessId(), (unsigned)GetCurrentThreadId());
if (len < 0 || len >= (int)sizeof tmp || !widen(tmp, a, WPATH) || !widen(path, b, WPATH)) return 0;
HANDLE h = CreateFileW(a, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return 0;
@@ -245,6 +444,8 @@ int plat_file_flush(const char *path) {
return ok;
}
int plat_file_sync(FILE *f) { return !fflush(f) && !_commit(_fileno(f)); }
void plat_file_delete(const char *path) {
wchar_t w[WPATH];
if (widen(path, w, WPATH)) DeleteFileW(w);
@@ -292,15 +493,558 @@ int plat_file_list(const char *dir, const char *ext, int (*cb)(const char *name,
return 1;
}
// ---- virtual memory: VirtualAlloc reservations, MEM_WRITE_WATCH for the watched ones.
int plat_mem_reserve(uint64_t addr, uint64_t n, int watch) {
void *p = VirtualAlloc((void *)(uintptr_t)addr, n, MEM_RESERVE | (watch ? MEM_WRITE_WATCH : 0), PAGE_NOACCESS);
if (p && p != (void *)(uintptr_t)addr) {
VirtualFree(p, 0, MEM_RELEASE);
p = NULL;
}
return p != NULL;
}
void *plat_mem_alloc(size_t n) { return VirtualAlloc(NULL, n, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); }
int plat_mem_commit(uint64_t addr, size_t n) {
return VirtualAlloc((void *)(uintptr_t)addr, n, MEM_COMMIT, PAGE_READWRITE) != NULL;
}
// Decommitted pages come back zeroed when committed again.
int plat_mem_decommit(uint64_t addr, uint64_t n) { return VirtualFree((void *)(uintptr_t)addr, n, MEM_DECOMMIT) != 0; }
int plat_mem_protect(uint64_t addr, uint64_t n, int prot) {
DWORD old;
DWORD p = prot == PLAT_PROT_RW ? PAGE_READWRITE : prot == PLAT_PROT_READ ? PAGE_READONLY : PAGE_NOACCESS;
return VirtualProtect((void *)(uintptr_t)addr, n, p, &old) != 0;
}
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)addr, &mbi, sizeof mbi)) {
*end = limit;
return PLAT_MEM_FREE;
}
uint64_t e = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
*end = e < limit ? e : limit;
if (mbi.State == MEM_RESERVE) return PLAT_MEM_RESERVED;
if (mbi.State != MEM_COMMIT) return PLAT_MEM_FREE;
DWORD p = mbi.Protect & 0xff;
return p == PAGE_READWRITE ? PLAT_PROT_RW : p == PAGE_READONLY ? PLAT_PROT_READ : PLAT_PROT_NONE;
}
// Adapted from LAS Recomp runtime/platform/windows.c:40-63 @ da35b83 (MIT, Copyright (c) 2026 Peiikai): GetWriteWatch
// with the reset flag.
uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t max, int reset) {
if (!max || !n) return 0;
ULONG_PTR got = (ULONG_PTR)max;
DWORD gran;
_Static_assert(sizeof(PVOID) == sizeof(uint64_t), "page addresses are written straight into a uint64_t array");
if (GetWriteWatch(reset ? WRITE_WATCH_FLAG_RESET : 0, (void *)(uintptr_t)addr, (SIZE_T)n, (PVOID *)pages, &got,
&gran))
return UINT64_MAX;
return got;
}
static int (*fault_hook)(uint64_t addr);
// A write access violation (ExceptionInformation[0] == 1) goes to the hook; registered first, before the crash handler.
static LONG CALLBACK fault_veh(EXCEPTION_POINTERS *ep) {
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
if (er->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || er->NumberParameters < 2 || er->ExceptionInformation[0] != 1)
return EXCEPTION_CONTINUE_SEARCH;
return fault_hook((uint64_t)er->ExceptionInformation[1]) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
}
void plat_mem_fault_hook(int (*fn)(uint64_t addr)) {
fault_hook = fn;
AddVectoredExceptionHandler(1, fault_veh);
}
static void (*crash_fn)(const PlatCrash *c);
static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
DWORD code = ep->ExceptionRecord->ExceptionCode;
const char *what = code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
: code == EXCEPTION_PRIV_INSTRUCTION ? "privileged instruction"
: code == EXCEPTION_STACK_OVERFLOW ? "stack overflow"
: code == EXCEPTION_IN_PAGE_ERROR ? "in-page error" : NULL;
if (!what) return EXCEPTION_CONTINUE_SEARCH;
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
MODULEINFO mi;
if (!GetModuleInformation(GetCurrentProcess(), GetModuleHandleW(NULL), &mi, sizeof mi) ||
rip - (uintptr_t)mi.lpBaseOfDll >= mi.SizeOfImage)
return EXCEPTION_CONTINUE_SEARCH; // not our code: leave it to the handler that expects it
PlatCrash c = {what, (uint32_t)code, rip, rip - (uintptr_t)mi.lpBaseOfDll, -1, 0};
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2)
c.access = (int)ep->ExceptionRecord->ExceptionInformation[0], c.addr = ep->ExceptionRecord->ExceptionInformation[1];
crash_fn(&c);
return EXCEPTION_CONTINUE_SEARCH;
}
// abort() and an invalid parameter to a C runtime function raise no exception: the C runtime ends the process with
// 0xc0000409 after calling the SIGABRT or invalid-parameter handler. The report names the host frames in this
// executable; the innermost is the caller of abort (assert) or of the C runtime function. Skipped frames: this function
// and the handler that called it.
__attribute__((noinline)) static void crash_from_runtime(const char *what, uint32_t code) {
PlatCrash c = {what, code, 0, 0, -1, 0};
void *f[62];
USHORT n = RtlCaptureStackBackTrace(2, 62, f, NULL);
MODULEINFO mi;
if (GetModuleInformation(GetCurrentProcess(), GetModuleHandleW(NULL), &mi, sizeof mi)) {
uintptr_t base = (uintptr_t)mi.lpBaseOfDll;
for (USHORT i = 0; i < n && c.nframes < (int)(sizeof c.frames / sizeof *c.frames); i++)
if ((uintptr_t)f[i] - base < mi.SizeOfImage) c.frames[c.nframes++] = (uintptr_t)f[i];
if (c.nframes) c.rip = c.frames[0], c.offset = c.rip - base;
}
crash_fn(&c);
}
static void abort_signal(int sig) { crash_from_runtime("abort", (uint32_t)sig); }
static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wchar_t *file, unsigned line,
uintptr_t reserved) {
(void)expr, (void)func, (void)file, (void)line, (void)reserved; // NULL and 0 in the release C runtime
crash_from_runtime("invalid parameter to a C runtime function", 0xc0000417u); // STATUS_INVALID_CRUNTIME_PARAMETER
}
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
crash_fn = fn;
AddVectoredExceptionHandler(0, crash_veh);
signal(SIGABRT, abort_signal);
_set_invalid_parameter_handler(invalid_parameter);
}
// A fresh process gets a fresh layout: Windows places DLLs, heaps and thread stacks at random addresses. The parent
// waits and exits with the child's code.
void plat_reexec(void) {
fflush(stdout);
fflush(stderr);
STARTUPINFOW si = {.cb = sizeof si};
PROCESS_INFORMATION pi;
wchar_t *cmd = _wcsdup(GetCommandLineW()); // CreateProcessW may write its command line
if (!cmd || !CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
fprintf(stderr, "FATAL: re-running the process failed (error %lu)\n", GetLastError());
ExitProcess(70);
}
CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 70;
GetExitCodeProcess(pi.hProcess, &code);
ExitProcess(code);
}
int plat_mem_read(uint64_t addr, void *out, size_t n) {
SIZE_T got = 0;
return ReadProcessMemory(GetCurrentProcess(), (const void *)(uintptr_t)addr, out, n, &got) && got == n;
}
int plat_mem_readable(uint64_t addr, uint64_t n) {
for (uint64_t a = addr, end = addr + n; a < end || a == addr;) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
a = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
if (a >= end) return 1;
}
return 1;
}
void *plat_aligned_alloc(size_t size, size_t align) { return _aligned_malloc(size, align); }
void plat_aligned_free(void *p) { _aligned_free(p); }
// ---- process
void plat_exit(int code) {
fflush(stdout);
fflush(stderr);
ExitProcess((UINT)code);
}
// _putenv_s, not SetEnvironmentVariableA: getenv reads the C runtime's copy of the environment, which only the CRT's
// own calls update. An empty value removes the variable.
int plat_setenv(const char *name, const char *value) { return _putenv_s(name, value ? value : "") == 0; }
uint32_t plat_process_id(void) { return GetCurrentProcessId(); }
static uint64_t ft64(FILETIME f) { return (uint64_t)f.dwHighDateTime << 32 | f.dwLowDateTime; }
uint64_t plat_process_cpu_ns(void) {
FILETIME cr, ex, k, u;
if (!GetProcessTimes(GetCurrentProcess(), &cr, &ex, &k, &u)) return 0;
return (ft64(k) + ft64(u)) * 100;
}
uint64_t plat_process_uptime_ns(void) {
FILETIME cr, ex, k, u, now;
if (!GetProcessTimes(GetCurrentProcess(), &cr, &ex, &k, &u)) return 0;
GetSystemTimeAsFileTime(&now);
return ft64(now) > ft64(cr) ? (ft64(now) - ft64(cr)) * 100 : 0;
}
uint64_t plat_unix_time_ns(void) {
FILETIME now;
GetSystemTimeAsFileTime(&now);
return (ft64(now) - 116444736000000000ull) * 100; // FILETIME counts 100 ns from 1601
}
void plat_local_time(struct tm *out) {
time_t t = time(NULL);
localtime_s(out, &t);
}
int plat_localtime(int64_t unix_s, struct tm *out) {
time_t t = (time_t)unix_s;
return !localtime_s(out, &t);
}
int plat_gmtime(int64_t unix_s, struct tm *out) {
time_t t = (time_t)unix_s;
return !gmtime_s(out, &t);
}
long plat_tz_west(void) {
long west = 0;
_tzset();
_get_timezone(&west);
return west;
}
int plat_run_self(const char *env_name, const char *env_value, char *out, size_t cap, uint32_t timeout_ms) {
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
HANDLE rd, wr;
if (!CreatePipe(&rd, &wr, &sa, 0)) return -1;
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdInput = GetStdHandle(STD_INPUT_HANDLE),
.hStdOutput = wr, .hStdError = wr};
PROCESS_INFORMATION pi;
wchar_t *cmdline = _wcsdup(GetCommandLineW());
SetEnvironmentVariableA(env_name, env_value);
BOOL ok = cmdline && CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
SetEnvironmentVariableA(env_name, NULL);
free(cmdline);
CloseHandle(wr);
if (!ok) {
CloseHandle(rd);
return -1;
}
size_t len = 0;
DWORD got;
char sink[512];
while (len + 1 < cap && ReadFile(rd, out + len, (DWORD)(cap - 1 - len), &got, NULL) && got) len += got;
if (cap) out[len] = 0;
while (ReadFile(rd, sink, sizeof sink, &got, NULL) && got) {}
CloseHandle(rd);
if (WaitForSingleObject(pi.hProcess, timeout_ms ? timeout_ms : INFINITE) != WAIT_OBJECT_0)
TerminateProcess(pi.hProcess, 1), WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return (int)code;
}
int plat_random(void *out, size_t n) {
return BCryptGenRandom(NULL, out, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0;
}
// FILE_FLAG_OVERLAPPED handles: a read names its offset, and each thread waits on an event of its own, so concurrent
// reads on one handle do not share the file pointer or the event.
PlatFile *plat_file_open(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return NULL;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
return h == INVALID_HANDLE_VALUE ? NULL : (PlatFile *)h;
}
PlatFile *plat_file_open_unbuffered(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return NULL;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, NULL);
return h == INVALID_HANDLE_VALUE ? NULL : (PlatFile *)h;
}
void plat_file_close(PlatFile *f) {
if (f) CloseHandle((HANDLE)f);
}
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off) {
static __thread HANDLE ev;
if (!ev && !(ev = CreateEventW(NULL, TRUE, FALSE, NULL))) return -1;
OVERLAPPED ov = {.Offset = (DWORD)off, .OffsetHigh = (DWORD)(off >> 32), .hEvent = ev};
DWORD got = 0;
if (ReadFile((HANDLE)f, buf, n, &got, &ov)) return got;
DWORD e = GetLastError();
if (e == ERROR_IO_PENDING) {
if (GetOverlappedResult((HANDLE)f, &ov, &got, TRUE)) return got;
e = GetLastError();
}
return e == ERROR_HANDLE_EOF ? 0 : e == ERROR_NOACCESS ? -2 : -1;
}
int plat_file_id(PlatFile *f, PlatFileId *out) {
BY_HANDLE_FILE_INFORMATION i;
if (!GetFileInformationByHandle((HANDLE)f, &i)) return 0;
*out = (PlatFileId){i.dwVolumeSerialNumber, (uint64_t)i.nFileIndexHigh << 32 | i.nFileIndexLow,
(uint64_t)i.nFileSizeHigh << 32 | i.nFileSizeLow,
(uint64_t)i.ftLastWriteTime.dwHighDateTime << 32 | i.ftLastWriteTime.dwLowDateTime,
(uint64_t)i.ftCreationTime.dwHighDateTime << 32 | i.ftCreationTime.dwLowDateTime};
return 1;
}
const void *plat_file_map(PlatFile *f) {
HANDLE map = CreateFileMappingW((HANDLE)f, NULL, PAGE_READONLY, 0, 0, NULL);
const void *v = map ? MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0) : NULL;
if (map) CloseHandle(map); // the view keeps the mapping alive
return v;
}
static int fs_err(DWORD e) {
switch (e) {
case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_DIRECTORY: return PLAT_FS_NOT_FOUND;
case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: return PLAT_FS_EXISTS;
case ERROR_DIR_NOT_EMPTY: return PLAT_FS_NOT_EMPTY;
case ERROR_ACCESS_DENIED: case ERROR_SHARING_VIOLATION: return PLAT_FS_BUSY;
default: return PLAT_FS_ERROR;
}
}
int plat_fs_kind(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_MISSING;
DWORD a = GetFileAttributesW(w);
if (a == INVALID_FILE_ATTRIBUTES) return PLAT_FS_MISSING;
return a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
}
int plat_fs_mkdir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
return CreateDirectoryW(w, NULL) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_rmdir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
return RemoveDirectoryW(w) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_unlink(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
DWORD a = GetFileAttributesW(w);
if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_READONLY))
SetFileAttributesW(w, a & ~(DWORD)FILE_ATTRIBUTE_READONLY);
return DeleteFileW(w) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_rename(const char *from, const char *to, int durable) {
wchar_t a[WPATH], b[WPATH];
if (!widen(from, a, WPATH) || !widen(to, b, WPATH)) return PLAT_FS_ERROR;
return MoveFileExW(a, b, durable ? MOVEFILE_WRITE_THROUGH : 0) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_link(const char *existing, const char *path) {
wchar_t a[WPATH], b[WPATH];
if (!widen(existing, a, WPATH) || !widen(path, b, WPATH)) return PLAT_FS_ERROR;
return CreateHardLinkW(b, a, NULL) ? PLAT_FS_OK : fs_err(GetLastError());
}
int plat_fs_links(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return 0;
HANDLE f = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
BY_HANDLE_FILE_INFORMATION fi;
int links = f != INVALID_HANDLE_VALUE && GetFileInformationByHandle(f, &fi) ? (int)fi.nNumberOfLinks : 0;
if (f != INVALID_HANDLE_VALUE) CloseHandle(f);
return links;
}
// NTFS journals directory entries; flushing the directory handle is as far as Windows goes.
void plat_fs_sync_dir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return;
HANDLE h = CreateFileW(w, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (h != INVALID_HANDLE_VALUE) {
FlushFileBuffers(h);
CloseHandle(h);
}
}
// Restart Manager (rstrtmgr.dll, loaded on first use) names the processes that hold a file open.
typedef DWORD(WINAPI *RmStartSession_t)(DWORD *, DWORD, WCHAR *);
typedef DWORD(WINAPI *RmRegisterResources_t)(DWORD, UINT, LPCWSTR *, UINT, RM_UNIQUE_PROCESS *, UINT, LPCWSTR *);
typedef DWORD(WINAPI *RmGetList_t)(DWORD, UINT *, UINT *, RM_PROCESS_INFO *, LPDWORD);
typedef DWORD(WINAPI *RmEndSession_t)(DWORD);
static struct {
INIT_ONCE once;
RmStartSession_t start;
RmRegisterResources_t add;
RmGetList_t list;
RmEndSession_t end;
} rm = {INIT_ONCE_STATIC_INIT};
static BOOL CALLBACK rm_load(INIT_ONCE *o, void *p, void **c) {
(void)o, (void)p, (void)c;
HMODULE l = plat_lib_open("rstrtmgr.dll");
if (!l) return TRUE;
rm.add = (RmRegisterResources_t)(void *)GetProcAddress(l, "RmRegisterResources");
rm.list = (RmGetList_t)(void *)GetProcAddress(l, "RmGetList");
rm.end = (RmEndSession_t)(void *)GetProcAddress(l, "RmEndSession");
RmStartSession_t start = (RmStartSession_t)(void *)GetProcAddress(l, "RmStartSession");
if (rm.add && rm.list && rm.end) rm.start = start; // start set: all of them are there
return TRUE;
}
// The files under dir (links not followed), at most BUSY_FILES of them, each a malloc'd path.
#define BUSY_FILES 256
typedef struct { LPCWSTR p[BUSY_FILES]; UINT n; } BusyFiles;
static void busy_files(const wchar_t *dir, BusyFiles *f) {
wchar_t pat[WPATH + 4];
swprintf(pat, WPATH + 4, L"%ls\\*", dir);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pat, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
DWORD a = fd.dwFileAttributes;
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..") || (a & FILE_ATTRIBUTE_REPARSE_POINT)) continue;
size_t n = wcslen(dir) + wcslen(fd.cFileName) + 2;
wchar_t *p = n <= WPATH ? malloc(n * sizeof *p) : NULL;
if (!p) continue;
swprintf(p, n, L"%ls\\%ls", dir, fd.cFileName);
if (a & FILE_ATTRIBUTE_DIRECTORY) busy_files(p, f), free(p);
else f->p[f->n++] = p;
} while (f->n < BUSY_FILES && FindNextFileW(h, &fd));
FindClose(h);
}
void plat_fs_busy_reason(const char *dir, char *out, size_t cap) {
if (!cap) return;
*out = 0;
wchar_t w[WPATH];
if (!widen(dir, w, WPATH)) return;
for (wchar_t *q = w; *q; q++)
if (*q == L'/') *q = L'\\';
// a rename needs DELETE access to the directory: a folder made by an elevated run can refuse it
HANDLE h = CreateFileW(w, DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (h == INVALID_HANDLE_VALUE) {
if (GetLastError() == ERROR_ACCESS_DENIED) snprintf(out, cap, "no permission to rename it");
return;
}
CloseHandle(h);
InitOnceExecuteOnce(&rm.once, rm_load, NULL, NULL);
DWORD session;
WCHAR key[CCH_RM_SESSION_KEY + 1] = {0};
if (!rm.start || rm.start(&session, 0, key) != ERROR_SUCCESS) return;
BusyFiles f = {.n = 0};
busy_files(w, &f);
RM_PROCESS_INFO info[8];
UINT need = 0, n = 8;
DWORD reasons = 0, e = f.n ? rm.add(session, f.n, f.p, 0, NULL, 0, NULL) : ERROR_SUCCESS;
if (f.n && e == ERROR_SUCCESS) e = rm.list(session, &need, &n, info, &reasons);
else n = 0;
rm.end(session);
for (UINT i = 0; i < f.n; i++) free((void *)f.p[i]);
if (e == ERROR_MORE_DATA) snprintf(out, cap, "%u programs hold files in it", need); // info is not filled then
if (e != ERROR_SUCCESS) return;
size_t at = 0;
for (UINT i = 0; i < n && at < cap; i++) { // the program's display name ("Windows Explorer") and process id
char name[CCH_RM_MAX_APP_NAME + 1];
if (!WideCharToMultiByte(CP_UTF8, 0, info[i].strAppName, -1, name, sizeof name, NULL, NULL)) strcpy(name, "?");
int k = snprintf(out + at, cap - at, "%s%s %lu", i ? ", " : "", name, info[i].Process.dwProcessId);
if (k < 0) break;
at += (size_t)k;
}
}
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx) {
wchar_t wd[WPATH], pat[WPATH + 4];
if (!widen(dir, wd, WPATH)) return PLAT_FS_ERROR;
swprintf(pat, WPATH + 4, L"%ls\\*", wd);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pat, &fd);
if (h == INVALID_HANDLE_VALUE) return fs_err(GetLastError());
do {
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue;
char name[1024];
if (!WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, sizeof name, NULL, NULL)) continue;
DWORD a = fd.dwFileAttributes;
int kind = a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR
: PLAT_FS_FILE;
if (!cb(name, kind, (uint64_t)fd.nFileSizeHigh << 32 | fd.nFileSizeLow, ctx)) break;
} while (FindNextFileW(h, &fd));
FindClose(h);
return PLAT_FS_OK;
}
static PlatFile *fs_open(const char *path, DWORD access, DWORD share, DWORD how, int *err) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) {
if (err) *err = PLAT_FS_ERROR;
return NULL;
}
HANDLE h = CreateFileW(w, access, share, NULL, how, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
if (h == INVALID_HANDLE_VALUE) {
if (err) *err = fs_err(GetLastError());
return NULL;
}
return (PlatFile *)h;
}
PlatFile *plat_fs_open(const char *path, int write, int *err) {
return fs_open(path, GENERIC_READ | (write ? GENERIC_WRITE : 0),
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, OPEN_EXISTING, err);
}
PlatFile *plat_fs_create(const char *path, int *err) { return fs_open(path, GENERIC_WRITE, 0, CREATE_NEW, err); }
int64_t plat_file_pwrite(PlatFile *f, const void *buf, uint32_t n, uint64_t off) {
static __thread HANDLE ev;
if (!ev && !(ev = CreateEventW(NULL, TRUE, FALSE, NULL))) return -1;
OVERLAPPED ov = {.Offset = (DWORD)off, .OffsetHigh = (DWORD)(off >> 32), .hEvent = ev};
DWORD put = 0;
if (WriteFile((HANDLE)f, buf, n, &put, &ov)) return put;
if (GetLastError() == ERROR_IO_PENDING && GetOverlappedResult((HANDLE)f, &ov, &put, TRUE)) return put;
return -1;
}
int plat_file_set_size(PlatFile *f, uint64_t size) {
FILE_END_OF_FILE_INFO eof = {.EndOfFile.QuadPart = (LONGLONG)size};
return SetFileInformationByHandle((HANDLE)f, FileEndOfFileInfo, &eof, sizeof eof) ? PLAT_FS_OK
: fs_err(GetLastError());
}
int plat_file_fsync(PlatFile *f) { return FlushFileBuffers((HANDLE)f) != 0; }
int plat_temp_dir(char *out, size_t cap) {
wchar_t w[MAX_PATH + 1];
DWORD n = GetTempPathW(MAX_PATH + 1, w);
if (!n || n > MAX_PATH) return 0;
while (n && (w[n - 1] == L'\\' || w[n - 1] == L'/')) w[--n] = 0;
return WideCharToMultiByte(CP_UTF8, 0, w, -1, out, (int)cap, NULL, NULL) != 0;
}
uintptr_t plat_exe_base(void) { return (uintptr_t)GetModuleHandleW(NULL); }
int plat_module_of(uintptr_t addr, char *name, size_t cap, uintptr_t *base) {
HMODULE m = NULL;
wchar_t w[WPATH];
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCWSTR)addr, &m) ||
!m || !GetModuleFileNameW(m, w, WPATH))
return 0;
const wchar_t *n = wcsrchr(w, L'\\') ? wcsrchr(w, L'\\') + 1 : w;
*base = (uintptr_t)m;
return WideCharToMultiByte(CP_UTF8, 0, n, -1, name, (int)cap, NULL, NULL) != 0;
}
int plat_exe_path(char *out, size_t cap) {
wchar_t w[WPATH];
DWORD n = GetModuleFileNameW(NULL, w, WPATH);
@@ -332,6 +1076,20 @@ const char *plat_ui_language(void) {
return tag;
}
const char *plat_user_locale(void) {
static char tag[LOCALE_NAME_MAX_LENGTH];
static int done;
if (!done) {
wchar_t w[LOCALE_NAME_MAX_LENGTH];
size_t i = 0;
if (GetUserDefaultLocaleName(w, LOCALE_NAME_MAX_LENGTH))
for (; w[i] && w[i] < 0x80 && i + 1 < sizeof tag; i++) tag[i] = (char)w[i];
tag[i] = 0;
done = 1;
}
return tag;
}
int plat_font_path(const char *lang, char *out, size_t cap) {
static const struct { const char *lang; const wchar_t *files[3]; } fonts[] = {
{"ja", {L"YuGothM.ttc", L"meiryo.ttc", L"msgothic.ttc"}},
@@ -500,7 +1258,8 @@ struct PlatLoad {
wchar_t luid[40]; // "luid_0x<high>_0x<low>" of the adapter, lower case; empty for any
};
PlatLoad *plat_load_open(const uint8_t *gpu_luid) {
PlatLoad *plat_load_open(const uint8_t *gpu_luid, int pci_bus) {
(void)pci_bus; // the LUID names the adapter
PlatLoad *l = calloc(1, sizeof *l);
if (!l) return NULL;
if (gpu_luid) { // Vulkan's deviceLUID is the LUID struct's memory: LowPart, then HighPart
@@ -521,8 +1280,6 @@ void plat_load_close(PlatLoad *l) {
free(l);
}
static uint64_t ft64(FILETIME f) { return (uint64_t)f.dwHighDateTime << 32 | f.dwLowDateTime; }
void plat_cpu_load(PlatLoad *l, float *total_pct, float *proc_pct, float *temp_c) {
*total_pct = *proc_pct = *temp_c = -1;
if (!l) return;
@@ -625,6 +1382,13 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
*proc_mb = (double)(pmc.PrivateUsage >> 20);
}
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
PROCESS_MEMORY_COUNTERS_EX pm = {.cb = sizeof pm};
*resident = *private_bytes = 0;
if (K32GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pm, sizeof pm))
*resident = pm.WorkingSetSize, *private_bytes = pm.PrivateUsage;
}
// ---- child processes
struct PlatChild {
+12
View File
@@ -0,0 +1,12 @@
// Between perf.c (frame timing, always built) and the OS's sampling profiler (prof_win.c on Windows).
#pragma once
#include <stdint.h>
extern volatile uint32_t prof_render_tid; // the thread that presents (perf_present)
extern volatile uint32_t prof_main_tid; // the guest main thread (perf_main_thread)
extern volatile uint32_t prof_replay_tid; // the asynchronous replay (perf_replay_thread): its waits hold up the frame too
// The sampler; perf.c's weak versions do nothing.
int prof_sampler_start(void); // TOTK_PROFILE: starts sampling; 1 when it runs
void prof_sampler_write(void); // writes the profile once (prof_write)
void prof_sampler_frame(uint64_t frame, uint64_t now, uint64_t d); // every present; d: the interval, 0 on the first
+15 -307
View File
@@ -28,6 +28,11 @@
#include "gfx.h"
#include "host.h"
#include "perfmon.h"
#include "prof.h"
#define render_tid prof_render_tid
#define main_tid prof_main_tid
#define replay_tid prof_replay_tid
#define PROF_SLOTS (1u << 16) // Rip hash table, open addressing; a full table drops new Rips
#define PROF_TOP 4000 // more than the 200 asked for: the report sums per function
@@ -54,9 +59,7 @@ static const char *out_path;
static HANDLE sampler;
static volatile LONG stop, written;
static uintptr_t exe_lo, exe_hi;
static volatile DWORD render_tid; // the thread that presents (perf_present)
static int only_render; // TOTK_PROFILE_RENDER=1: sample the render thread only
static volatile DWORD main_tid; // the guest main thread (kern_main_thread)
static int only_main; // TOTK_PROFILE_MAIN=1: sample the guest main thread only
static int chains; // call chains are kept: TOTK_PROFILE_RENDER or TOTK_PROFILE_CHAINS
static volatile uint64_t cur_frame; // last presented frame
@@ -73,7 +76,6 @@ static Sample stalls[STALL_RING]; // t: when the stop began; rip[PROF_DEPTH -
static uint64_t stall_n;
static uint64_t slow_ns; // TOTK_PROFILE_SLOW in ns, 0 off
static volatile LONG64 dump_lo, dump_hi, dump_frame; // the render thread asks for a window, the sampler writes it
static volatile DWORD replay_tid; // asynchronous replay (vk_backend.c): its waits hold up the frame too
static void count_rip(uint64_t rip) {
uint32_t i = (uint32_t)((rip * 0x9E3779B97F4A7C15ull) >> 48);
@@ -180,7 +182,7 @@ static void slow_dump(void) {
FILE *f = dumps < SLOW_DUMPS ? fopen(path, "w") : NULL;
if (f) {
dumps++;
fprintf(f, "totk-prof 1\nbase %llx\nperiod_us 1000\nframe %llu interval_ms %.1f\nmain %lu render %lu replay %lu\n",
fprintf(f, "totk-prof 1\nbase %llx\nperiod_us 1000\nframe %llu interval_ms %.1f\nmain %u render %u replay %u\n",
(unsigned long long)(uintptr_t)GetModuleHandleW(NULL), (unsigned long long)frame, (hi - lo) / 1e6 - 20,
main_tid, render_tid, replay_tid);
write_modules(f);
@@ -293,8 +295,7 @@ static int cmp_stack_desc(const void *a, const void *b) {
return (x < y) - (x > y);
}
void prof_write(void) {
perf_summary_flush();
void prof_sampler_write(void) {
if (!out_path || InterlockedExchange(&written, 1)) return;
InterlockedExchange(&stop, 1);
if (WaitForSingleObject(sampler, 2000) != WAIT_OBJECT_0) return; // sampler stuck: no half-written counts
@@ -327,33 +328,11 @@ void prof_write(void) {
rt_log("prof: %llu samples, %d Rips -> %s", (unsigned long long)total, cnt, out_path);
}
// TOTK_TRACE=perf, TOTK_FLIGHT=1 or the full performance overlay: event counts are taken. Wait times are always taken
// (frame-time summary). perf_base: the part fixed at start.
static int perf_on, perf_base;
static int overlay; // performance overlay mode (perf_set_overlay): 0 off, 1 fps, 2 full
void perf_set_overlay(int mode) {
__atomic_store_n(&overlay, mode, __ATOMIC_RELAXED);
__atomic_store_n(&perf_on, perf_base || mode == 2, __ATOMIC_RELAXED);
}
int perf_gpu_timing(void) { return (rt_trace & RT_TRACE_PERF) || __atomic_load_n(&overlay, __ATOMIC_RELAXED) == 2; }
static volatile LONG64 gpu_busy_total;
void perf_gpu_busy(uint64_t ns) { InterlockedAdd64(&gpu_busy_total, (LONG64)ns); }
uint64_t perf_gpu_busy_total(void) { return (uint64_t)__atomic_load_n(&gpu_busy_total, __ATOMIC_RELAXED); }
void prof_start(void) {
flight_init();
atexit(perf_summary_flush); // boot.c's _exit path goes through prof_write instead
perf_base = (rt_trace & RT_TRACE_PERF) || flight_on;
perf_on = perf_base || overlay == 2;
int prof_sampler_start(void) {
out_path = cfg_get("TOTK_PROFILE");
if (!out_path || !*out_path) {
out_path = NULL;
return;
return 0;
}
only_render = cfg_get("TOTK_PROFILE_RENDER") != NULL;
only_main = !only_render && cfg_get("TOTK_PROFILE_MAIN") != NULL;
@@ -375,289 +354,18 @@ void prof_start(void) {
if (!sampler) {
rt_log("prof: cannot start the sampler (error %lu)", GetLastError());
out_path = NULL;
return;
return 0;
}
atexit(prof_write); // a --seconds run ends in _exit: boot.c calls prof_write itself first
return 1;
}
// ---- TOTK_TRACE=perf: frame interval and the time the render thread spends waiting, logged every 60 presents. The
// flight recorder (TOTK_FLIGHT=1, flight.c) takes the same numbers per frame.
static uint64_t wait_ns[PERF_N], frame_ns[PERF_N], worst_ns[PERF_N]; // window totals, this frame, the slowest frame
uint64_t perf_begin(void) { return kern_now_ns(); }
void perf_replay_thread(void) { replay_tid = GetCurrentThreadId(); }
void perf_main_thread(void) { main_tid = GetCurrentThreadId(); }
void perf_end(int kind, uint64_t t0) {
if (!t0) return;
DWORD tid = GetCurrentThreadId();
if (tid != render_tid && tid != replay_tid) return;
uint64_t d = kern_now_ns() - t0;
if (tid == render_tid && !replay_tid) { // one thread adds: no atomics on the synchronous path
wait_ns[kind] += d;
frame_ns[kind] += d;
return;
}
__atomic_fetch_add(&wait_ns[kind], d, __ATOMIC_RELAXED);
__atomic_fetch_add(&frame_ns[kind], d, __ATOMIC_RELAXED);
}
static volatile LONG64 other_wait_ns; // perf_gpu_wait on threads other than the render thread, over the window
static volatile LONG64 render_wait_ns; // the same on the render thread
void (*perf_gpu_stats)(PerfGpu *out);
void perf_gpu_wait(uint64_t t0) {
if (!t0 || !__atomic_load_n(&perf_on, __ATOMIC_RELAXED)) return;
LONG64 d = (LONG64)(kern_now_ns() - t0);
InterlockedAdd64(GetCurrentThreadId() == render_tid ? &render_wait_ns : &other_wait_ns, d);
}
static volatile LONG64 replay_ns; // perf_replay: summed over the window, any thread
static volatile LONG64 frame_replay_ns; // the same since the previous present (frame-time summary)
void perf_replay(uint64_t t0) {
if (!t0) return;
LONG64 d = (LONG64)(kern_now_ns() - t0);
InterlockedAdd64(&frame_replay_ns, d);
if (__atomic_load_n(&perf_on, __ATOMIC_RELAXED)) InterlockedAdd64(&replay_ns, d);
}
static volatile LONG64 counts[PERF_CNT_N]; // running totals: the perf log and the flight recorder take differences
void perf_count(int kind) {
if (__atomic_load_n(&perf_on, __ATOMIC_RELAXED)) InterlockedIncrement64(&counts[kind]);
}
// ---- Frame-time summary, always on: every 60 s of play and at exit one line of frame count, mean fps, 1% low and the
// frames over 25/50/100 ms, then the worst frames of the period with the render thread's waits in them. Per frame: a
// histogram increment, the compare against the worst list, a memset of the wait array. No allocation.
#define FT_BINS 1024 // 0.25 ms bins up to 256 ms; slower frames land in the last bin
#define FT_WORST 5
#define FT_PERIOD_NS 60000000000ull
typedef struct { uint64_t frame, ns, wait[PERF_N], replay; } FtWorst;
static struct {
uint64_t t0, frames, sum, over25, over50, over100;
uint32_t hist[FT_BINS];
FtWorst worst[FT_WORST]; // slowest first
int nworst;
} ft;
static uint64_t summary_t0; // start of the current period (0: not started)
static void ft_report(uint64_t now, const char *tag) {
if (ft.frames && ft.sum) {
uint64_t cut = (ft.frames + 99) / 100, acc = 0, low = 0; // the slowest 1% of frames
for (int b = FT_BINS - 1; b >= 0; b--) {
acc += ft.hist[b];
if (acc >= cut) { low = (uint64_t)(b + 1) * 250000ull; break; } // upper edge of the bin
}
rt_log("frametime: %s: %llu frames in %.1f s, mean %.1f fps, 1%% low %.1f fps, over 25 ms %llu, 50 ms %llu, 100 ms %llu",
tag, (unsigned long long)ft.frames, (now - ft.t0) / 1e9, ft.frames * 1e9 / ft.sum, low ? 1e9 / low : 0.0,
(unsigned long long)ft.over25, (unsigned long long)ft.over50, (unsigned long long)ft.over100);
for (int i = 0; i < ft.nworst; i++) {
const FtWorst *w = &ft.worst[i];
rt_log("frametime: worst frame %llu: %.1f ms; shader translation %.1f, pipeline build %.1f, texture upload %.1f (astc %.1f), replay %.1f, fence/present wait %.1f, pacing %.1f",
(unsigned long long)w->frame, w->ns / 1e6, w->wait[PERF_XLATE] / 1e6, w->wait[PERF_PIPE] / 1e6,
w->wait[PERF_TEX] / 1e6, w->wait[PERF_ASTC] / 1e6, w->replay / 1e6,
(w->wait[PERF_FENCE] + w->wait[PERF_DRAIN]) / 1e6, w->wait[PERF_PACE] / 1e6);
}
}
memset(&ft, 0, sizeof ft);
ft.t0 = now;
}
__attribute__((weak)) void gfx_exit_log(void) {}
void perf_summary_flush(void) { // at exit (prof_write)
if (summary_t0) {
ft_report(kern_now_ns(), "exit");
gfx_exit_log();
}
summary_t0 = 0;
}
// Returns the replay time since the previous present.
static uint64_t ft_frame(uint64_t frame, uint64_t now, uint64_t d) {
LONG64 rep = InterlockedExchange64(&frame_replay_ns, 0);
if (!summary_t0) { // the first present starts the period; there is no interval yet
summary_t0 = now;
ft.t0 = now;
return (uint64_t)rep;
}
ft.frames++;
ft.sum += d;
uint64_t b = d / 250000ull;
ft.hist[b < FT_BINS ? b : FT_BINS - 1]++;
if (d > 25000000ull) ft.over25++;
if (d > 50000000ull) ft.over50++;
if (d > 100000000ull) ft.over100++;
if (d > 25000000ull && (ft.nworst < FT_WORST || d > ft.worst[FT_WORST - 1].ns)) {
int i = ft.nworst < FT_WORST ? ft.nworst++ : FT_WORST - 1;
while (i > 0 && ft.worst[i - 1].ns < d) ft.worst[i] = ft.worst[i - 1], i--;
ft.worst[i].frame = frame;
ft.worst[i].ns = d;
memcpy(ft.worst[i].wait, frame_ns, sizeof frame_ns);
ft.worst[i].replay = (uint64_t)rep;
}
if (now - summary_t0 >= FT_PERIOD_NS) {
ft_report(now, "60 s");
summary_t0 = now;
}
return (uint64_t)rep;
}
// The second line of the 60-frame window: GPU busy time (timestamps), the time threads waited for GPU fences, VRAM and
// process memory, and which side holds the frame back. pace: the frame waited for pacing (vsync, the frame limiter) at
// least 15% of the window; gpu: the GPU was busy for 80% of the frame interval or more (without timestamps: threads
// waited for fences for half the interval or more); cpu otherwise.
static void perf_gpu_line(uint64_t frame, double interval_ms, double pace_share, uint64_t n) {
PerfGpu g;
memset(&g, 0, sizeof g);
if (perf_gpu_stats) perf_gpu_stats(&g);
double rw = InterlockedExchange64(&render_wait_ns, 0) / 1e6 / n, ow = InterlockedExchange64(&other_wait_ns, 0) / 1e6 / n;
double gpu_ms = g.have_gpu ? g.gpu_ns / 1e6 / n : 0;
char gs[96], vs[96];
if (g.have_gpu) snprintf(gs, sizeof gs, "gpu avg %.2f ms (%llu parts timed, %llu not)", gpu_ms,
(unsigned long long)g.gpu_parts, (unsigned long long)g.gpu_missed);
else snprintf(gs, sizeof gs, "gpu avg n/a");
if (g.have_vram) snprintf(vs, sizeof vs, "vram %.0f of %.0f MiB%s", g.vram_used / 1048576.0, g.vram_budget / 1048576.0,
g.vram_exact ? "" : " (own allocations of the heap size)");
else snprintf(vs, sizeof vs, "vram n/a");
PROCESS_MEMORY_COUNTERS_EX pm = {.cb = sizeof pm};
double ws = 0, priv = 0;
if (K32GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pm, sizeof pm))
ws = pm.WorkingSetSize / 1048576.0, priv = pm.PrivateUsage / 1048576.0;
const char *bound = pace_share >= 0.15 ? "pace"
: g.have_gpu ? (gpu_ms >= 0.8 * interval_ms ? "gpu" : "cpu")
: (rw + ow >= 0.5 * interval_ms ? "gpu" : "cpu");
rt_log("perf: frame %llu: %s; fence wait per frame: render thread %.2f ms, other threads %.2f ms; %s; ram working set "
"%.0f MiB, private %.0f MiB; bound: %s",
(unsigned long long)frame, gs, rw, ow, vs, ws, priv, bound);
}
int rt_ab(void) {
static int64_t period = -1; // us of world clock; 0: off
if (period < 0) {
const char *e = cfg_get("TOTK_AB");
period = e && *e ? (int64_t)(atof(e) * 1e6) : 0;
}
int64_t w = period ? frame_step_world_us() : -1;
if (w < 0) return 0; // before the world clock: A
int64_t k = w / period % 4;
return k == 1 || k == 2;
}
int rt_knob(const char *name, const char *def) {
char key[64];
snprintf(key, sizeof key, "TOTK_%s", name);
const char *e = cfg_get(key);
if (!e || !*e) e = def;
return !strcmp(e, "ab") ? 2 : strcmp(e, "0") != 0;
}
// TOTK_ONE_PER_CORE=1: the process keeps the first logical processor of each physical core, so busy threads (the
// render thread, the guest cores, the job workers) never share a core's execution units with another; "ab" switches
// it with the A/B blocks. Off by default until measured here; Savage Wind Recomp
// reported +4-10% from the same rule on an 8-core SMT CPU.
static void cpu_placement(void) {
static int mode = -1, applied = -1;
static DWORD_PTR all, one;
if (mode < 0) {
mode = rt_knob("ONE_PER_CORE", "0");
DWORD_PTR sys;
GetProcessAffinityMask(GetCurrentProcess(), &all, &sys);
DWORD len = 0;
GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &len);
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buf = len ? malloc(len) : NULL;
int cores = 0;
if (buf && GetLogicalProcessorInformationEx(RelationProcessorCore, buf, &len))
for (DWORD off = 0; off < len;) {
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)((char *)buf + off);
KAFFINITY m = e->Processor.GroupMask[0].Mask & all;
if (e->Processor.GroupMask[0].Group == 0 && m) one |= m & -m, cores++; // the lowest of the core's
off += e->Size;
}
free(buf);
if (mode && one && one != all)
rt_log("cpu: %d cores, %d logical processors: one per core%s", cores, __builtin_popcountll(all),
mode == 2 ? " in the B blocks" : "");
else
mode = 0;
}
int on = RT_KNOB_ON(mode);
if (on == applied) return;
applied = on;
SetProcessAffinityMask(GetCurrentProcess(), on ? one : all);
}
void perf_present(uint64_t frame) {
static uint64_t last, sum, max, n, win_start, ab_b;
static LONG64 win_prev[PERF_CNT_N];
render_tid = GetCurrentThreadId();
// perf_present, every present: the frame the sampler's TOTK_PROFILE_FRAMES window counts, and a TOTK_PROFILE_SLOW
// dump of a present at least slow_ns after the previous one (d 0 on the first present).
void prof_sampler_frame(uint64_t frame, uint64_t now, uint64_t d) {
cur_frame = frame;
if (rt_trace & RT_TRACE_SCHED) kern_sched_report(frame);
uint64_t now = kern_now_ns(), d = last ? now - last : 0;
if (slow_ns && last && d >= slow_ns && !dump_hi) {
if (slow_ns && d >= slow_ns && !dump_hi) {
dump_lo = (LONG64)(now - d - 20000000ull);
dump_frame = (LONG64)frame;
InterlockedExchange64(&dump_hi, (LONG64)now);
}
uint64_t rep = ft_frame(frame, now, d); // d is 0 on the first present: it only starts the period
ab_b += rt_ab();
cpu_placement();
if (__atomic_load_n(&overlay, __ATOMIC_RELAXED)) // the overlay's only cost while it is off: this branch
perfmon_frame(now, d, frame_ns, rep, __atomic_load_n(&counts[PERF_CNT_TEX_UP], __ATOMIC_RELAXED));
kern_core_diag_frame(frame, d);
if (flight_on) flight_frame(frame, now, d, frame_ns, counts);
if (!(rt_trace & RT_TRACE_PERF)) {
memset(frame_ns, 0, sizeof frame_ns);
last = now;
return;
}
static uint64_t slow_ns; // TOTK_PERF_SLOW_MS (default 50): frames at least this long get a line of their own
if (!slow_ns) {
const char *e = cfg_get("TOTK_PERF_SLOW_MS");
slow_ns = (uint64_t)(e && atoi(e) > 0 ? atoi(e) : 50) * 1000000ull;
}
if (last) {
sum += d;
n++;
if (d > max) max = d, memcpy(worst_ns, frame_ns, sizeof worst_ns);
if (d >= slow_ns)
rt_log("perf: slow frame %llu: %.1f ms; blocked pipeline %.1f xlate %.1f texture %.1f astc %.1f fence %.1f pacing %.1f",
(unsigned long long)frame, d / 1e6, frame_ns[PERF_PIPE] / 1e6, frame_ns[PERF_XLATE] / 1e6,
frame_ns[PERF_TEX] / 1e6, frame_ns[PERF_ASTC] / 1e6, frame_ns[PERF_FENCE] / 1e6,
frame_ns[PERF_PACE] / 1e6);
}
memset(frame_ns, 0, sizeof frame_ns);
last = now;
if (!win_start) win_start = now;
if (frame % 60) return;
uint64_t span = now - win_start;
LONG64 win[PERF_CNT_N];
for (int i = 0; i < PERF_CNT_N; i++) {
LONG64 v = __atomic_load_n(&counts[i], __ATOMIC_RELAXED);
win[i] = v - win_prev[i];
win_prev[i] = v;
}
LONG64 async = win[PERF_CNT_PIPE_ASYNC], skips = win[PERF_CNT_PIPE_SKIP], up = win[PERF_CNT_TEX_UP];
LONG64 reup = win[PERF_CNT_TEX_REUP], sskip = win[PERF_CNT_SHADER_SKIP], waited = win[PERF_CNT_PIPE_WAIT];
LONG64 oneshot = win[PERF_CNT_ONESHOT_WAIT];
uint64_t shader_keys, shader_xlated;
gfx_shader_counts(&shader_keys, &shader_xlated);
if (n && span)
rt_log("perf: frame %llu: interval avg %.2f ms max %.2f ms (%.1f fps); render thread blocked: pacing %.0f%% fence %.0f%% drain %.0f%% pipeline build %.0f%% shader xlate %.0f%% texture %.0f%% (astc %.0f%%); pipelines built in background %lld, draws skipped pending %lld, waited %lld, waiting for a shader %lld, one-shot waited %lld; textures uploaded %lld re-uploaded %lld; slowest frame: xlate %.1f ms texture %.1f ms (astc %.1f) pipeline %.1f ms fence %.1f ms; shaders: %llu keys, %llu translated; render busy avg %.2f ms; replay avg %.2f ms; ab %llu/%llu; world %.1f s",
(unsigned long long)frame, sum / 1e6 / n, max / 1e6, n * 1e9 / sum, 100.0 * wait_ns[PERF_PACE] / span,
100.0 * wait_ns[PERF_FENCE] / span, 100.0 * wait_ns[PERF_DRAIN] / span,
100.0 * wait_ns[PERF_PIPE] / span, 100.0 * wait_ns[PERF_XLATE] / span,
100.0 * wait_ns[PERF_TEX] / span, 100.0 * wait_ns[PERF_ASTC] / span, (long long)async, (long long)skips, (long long)waited, (long long)sskip, (long long)oneshot,
(long long)up, (long long)reup, worst_ns[PERF_XLATE] / 1e6, worst_ns[PERF_TEX] / 1e6,
worst_ns[PERF_ASTC] / 1e6, worst_ns[PERF_PIPE] / 1e6, worst_ns[PERF_FENCE] / 1e6,
(unsigned long long)shader_keys, (unsigned long long)shader_xlated,
(span > wait_ns[PERF_PACE] ? span - wait_ns[PERF_PACE] : 0) / 1e6 / n,
InterlockedExchange64(&replay_ns, 0) / 1e6 / n, (unsigned long long)ab_b, (unsigned long long)n, frame_step_world_us() / 1e6);
if (n && span) perf_gpu_line(frame, sum / 1e6 / n, (double)wait_ns[PERF_PACE] / span, n);
sum = max = n = ab_b = 0;
memset(wait_ns, 0, sizeof wait_ns);
win_start = now;
}
+17 -11
View File
@@ -1,7 +1,7 @@
// RenderDoc in-app capture (debug tool). TOTK_RDOC=<frame>[,<frame>...] captures those presented frames; TOTK_RDOC_DIR is the
// output directory (default build/captures); TOTK_RDOC_DLL overrides the renderdoc.dll path. Only the API fields used are declared
// here (layout of RENDERDOC_API_1_4_1, renderdoc_app.h, MIT), so no RenderDoc header is vendored.
#include <windows.h>
// output directory (default build/captures); TOTK_RDOC_DLL overrides the library's path (renderdoc.dll, librenderdoc.so).
// Only the API fields used are declared here (layout of RENDERDOC_API_1_4_1, renderdoc_app.h, MIT), so no RenderDoc header
// is vendored.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -10,6 +10,7 @@
#include "cfg.h"
#include "host.h"
#include "vk.h"
#include "platform/platform.h"
#define RDOC_API_1_4_1 10401
#define RDOC_MAX_FRAMES 64
@@ -48,9 +49,14 @@ void rdoc_init(void) {
}
if (!n_targets) { rt_log("rdoc: TOTK_RDOC=%s has no frame numbers", e); return; }
const char *dll = cfg_get("TOTK_RDOC_DLL");
HMODULE m = LoadLibraryExA(dll && *dll ? dll : "C:\\Program Files\\RenderDoc\\renderdoc.dll", NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (!m) { rt_log("rdoc: cannot load renderdoc.dll (error %lu); set TOTK_RDOC_DLL", GetLastError()); return; }
int (*get_api)(int, void **) = (int (*)(int, void **))(void *)GetProcAddress(m, "RENDERDOC_GetAPI");
#ifdef _WIN32
const char *def = "C:/Program Files/RenderDoc/renderdoc.dll";
#else
const char *def = "librenderdoc.so";
#endif
void *m = plat_lib_open(dll && *dll ? dll : def);
if (!m) { rt_log("rdoc: cannot load %s; set TOTK_RDOC_DLL", dll && *dll ? dll : def); return; }
int (*get_api)(int, void **) = (int (*)(int, void **))plat_lib_sym(m, "RENDERDOC_GetAPI");
void *a = NULL;
if (!get_api || get_api(RDOC_API_1_4_1, &a) != 1 || !a) { rt_log("rdoc: RENDERDOC_GetAPI(1.4.1) failed"); return; }
api = a;
@@ -60,10 +66,10 @@ void rdoc_init(void) {
api->SetCaptureOptionU32(8, 1);
api->SetCaptureOptionU32(9, 1);
const char *dir = cfg_get("TOTK_RDOC_DIR");
char full[MAX_PATH], tmpl[MAX_PATH + 16];
if (!_fullpath(full, dir && *dir ? dir : "build/captures", sizeof full)) return;
CreateDirectoryA(full, NULL);
snprintf(tmpl, sizeof tmpl, "%s\totk", full);
char full[PLAT_PATH_MAX], tmpl[PLAT_PATH_MAX + 16];
if (!plat_full_path(dir && *dir ? dir : "build/captures", full, sizeof full)) return;
plat_file_mkdir_p(full);
snprintf(tmpl, sizeof tmpl, "%s/totk", full);
api->SetCaptureFilePathTemplate(tmpl);
rt_log("rdoc: ready, %d frame(s), captures %s", n_targets, full);
}
@@ -75,7 +81,7 @@ void rdoc_after_present(uint64_t frame) {
if (capturing) {
api->EndFrameCapture(NULL, NULL);
capturing = 0;
char path[MAX_PATH];
char path[PLAT_PATH_MAX];
uint32_t len = sizeof path, n = api->GetNumCaptures();
if (n && api->GetCapture(n - 1, path, &len, NULL)) rt_log("rdoc: frame %llu captured: %s", (unsigned long long)frame, path);
else rt_log("rdoc: frame %llu capture failed", (unsigned long long)frame);
+28 -4
View File
@@ -12,7 +12,8 @@
//
// Patch page: +16 the scene size {w, h} as floats, +32 the output size and +48 the swapchain size as {w, h} in one
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
// +56 the camera's aspect ratio as a float when the output is not 16:9 (1.4.3, set aspect).
// +56 the camera's aspect ratio as a float when the output is not 16:9 (set aspect), +60 the field-of-view factor
// (set fov, res_fov_live).
// 1.4.3 scales the scene it reads by output width / 1920 (a 2560x1440 output renders its 1600x900 scene at 2128x1200;
// MacNativeGames measured the same), so its page holds the wanted size divided by that factor and the game's 5/6 needs
// no scene patch. 1.2.1 renders the size it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and
@@ -219,8 +220,8 @@ void res_init(void) {
rt_patch_switch(output, R.ow != BASE_W || R.oh != BASE_H);
rt_patch_switch(scene, scene_on);
R.on = 1;
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v143.txt, set
// aspect), so a 21:9 output shows more of the world at the sides instead of a stretched 16:9 view. The HUD's layouts
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v121.txt and
// patches-v143.txt, set aspect), so a 21:9 output shows more of the world at the sides instead of a stretched 16:9 view. The HUD's layouts
// for that shape come from romfs-patches-<w>x<h> (romfs.c overlay_init, tools/make_romfs_patches.py --aspect).
double ratio = (double)R.ow / R.oh, game = (double)BASE_W / BASE_H;
int aspect = rt_patch_set("aspect");
@@ -248,11 +249,34 @@ int res_is_16x9(unsigned w, unsigned h) {
}
// The game resolution asks for a romfs-patches-<w>x<h> overlay: it is not 16:9 and the game version has the camera
// aspect patch (set aspect, 1.4.3 only); the wide UI layouts only fit a camera that follows the same shape.
// aspect patch (set aspect); the UI layouts of another shape only fit a camera that follows the same shape.
int res_wants_wide_overlay(const unsigned out[2]) {
return !res_is_16x9(out[0], out[1]) && rt_patch_set("aspect") >= 0;
}
// Field of view (TOTK_FOV, percent of the game's own, 70..140, live): the camera's vertical view angle times the
// factor on page +60 (recomp/patches-v143.txt, set fov). The horizontal angle follows through the aspect ratio. 100
// switches the set off, so the camera code is the game's own. Runs on a settings change (gfx_present.c
// window_settings_live), the first time at the first present.
#define FOV_MIN 70
#define FOV_MAX 140
void res_fov_live(void) {
static int set = -2, shown = 100;
if (set == -2) set = rt_patch_set("fov");
int pct = cfg_int("TOTK_FOV");
pct = pct < FOV_MIN ? FOV_MIN : pct > FOV_MAX ? FOV_MAX : pct;
if (pct == shown) return;
shown = pct;
if (set < 0) {
rt_log("resolution: this game version has no field-of-view patch; TOTK_FOV=%d does nothing", pct);
return;
}
float k = pct / 100.0f;
memcpy((uint8_t *)rt_patch_page() + 60, &k, sizeof k); // before the switch: a site that sees it on reads k
rt_patch_switch(set, pct != 100);
rt_log("resolution: field of view %d%% of the game's", pct);
}
void res_wanted(unsigned out[2]) {
decide();
out[0] = R.ow, out[1] = R.oh;
+103 -169
View File
@@ -8,8 +8,8 @@
// TOTK_ROMFS_VERIFY=full; a proof file (.romfs-verified-v1 next to the index, or in TOTK_ROMFS_PROOF_DIR) keyed by
// the file's volume, file id, size and times skips it on later starts, TOTK_ROMFS_VERIFY=force ignores the proof.
//
// Reads are positional (ReadFile with an OVERLAPPED offset on a FILE_FLAG_OVERLAPPED handle, one event per thread),
// so threads read the image in parallel without a file-pointer lock. TOTK_ROMFS_MMAP=1 maps the image instead.
// Reads are positional (platform.h plat_file_pread), so threads read the image in parallel without a file-pointer
// lock. TOTK_ROMFS_MMAP=1 maps the image instead.
//
// Overlays: files listed in a directory's manifest.tsv are served in place of the originals. Each line names a RomFS
// path, the overlay's size and SHA-256 and the original's SHA-256 ("-" adds a file the RomFS lacks; it is numbered
@@ -17,8 +17,6 @@
// TOTK_ROMFS_MODS; unset or "0" = none. A missing path, a size or hash mismatch or a changed original stops the start.
// An overlay file is opened when the game opens it; its identity (volume, file id, size, last write) is kept from the
// start check, and an open that finds another file there stops the game.
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -28,9 +26,11 @@
#include "host.h"
#include "romfs_config.h"
#include "romfs_path.h"
#include "platform/platform.h"
#include "sha256.h"
#define NH 65536 // RomFS handle slots
#define READ_CHUNK (64u << 20) // largest single ReadFile
#define READ_CHUNK (64u << 20) // largest single read
#define HASH_CHUNK (8u << 20)
#define ADDED UINT64_MAX // an added overlay file's index until overlay_init numbers it
@@ -43,15 +43,15 @@ typedef struct { uint64_t index, size; char *path, *name; Identity id; } Overlay
static uint8_t *index_bytes;
static size_t index_size;
static uint64_t file_count, strings_offset;
static HANDLE image = INVALID_HANDLE_VALUE;
static PlatFile *image;
static const uint8_t *image_map;
static char image_file[2048];
static char index_dir[2048]; // the directory of romfs-index.bin: the proof file's default place
static char mount_name[32];
static SRWLOCK lock = SRWLOCK_INIT;
static PlatMutex lock = PLAT_MUTEX_INIT;
static uint64_t handles[NH]; // file index + 1; 0 is free
static uint32_t handle_ovl[NH]; // overlay slot + 1 of an open handle, 0 = original
static HANDLE handle_file[NH]; // the handle's open overlay file, when handle_ovl is set
static PlatFile *handle_file[NH]; // the handle's open overlay file, when handle_ovl is set
static Overlay *overlays; // sorted by RomFS index after overlay_init
static size_t noverlay, overlay_cap;
static char **added; // RomFS paths of the added files, sorted; added[k] is index file_count + k
@@ -68,63 +68,34 @@ static Entry entry(uint64_t i) {
}
static const char *entry_name(Entry e) { return (const char *)index_bytes + strings_offset + e.name; }
static double now_s(void) {
LARGE_INTEGER t, f;
QueryPerformanceCounter(&t);
QueryPerformanceFrequency(&f);
return (double)t.QuadPart / (double)f.QuadPart;
}
static double now_s(void) { return (double)plat_now_ns() / 1e9; }
// ---- positional reads
// Bytes read at off (0 at or past the end of the file), -1 on an I/O error. Works on synchronous and
// FILE_FLAG_OVERLAPPED handles; the event is per thread, so concurrent reads on one handle do not share it.
static int64_t read_at(HANDLE f, void *buf, uint32_t n, uint64_t off) {
static _Thread_local HANDLE ev;
if (!ev && !(ev = CreateEventW(NULL, TRUE, FALSE, NULL)))
rt_fatal("RomFS: CreateEvent failed (%lu)", GetLastError());
OVERLAPPED ov = {.Offset = (DWORD)off, .OffsetHigh = (DWORD)(off >> 32), .hEvent = ev};
DWORD got = 0;
// Bytes read at off (0 at or past the end of the file), -1 on an I/O error.
static int64_t read_at(PlatFile *f, void *buf, uint32_t n, uint64_t off) {
mem_thaw((uintptr_t)buf, n); // the kernel's write would fail on a frozen page instead of faulting
if (!ReadFile(f, buf, n, &got, &ov)) {
DWORD e = GetLastError();
if (e == ERROR_HANDLE_EOF) return 0;
if (e == ERROR_IO_PENDING && !GetOverlappedResult(f, &ov, &got, TRUE)) e = GetLastError();
else if (e == ERROR_IO_PENDING) return got;
if (e == ERROR_HANDLE_EOF) return 0;
if (e == ERROR_NOACCESS) // a read-only page in the buffer: a frozen region missed its thaw
rt_log("RomFS: read of %u bytes into %p refused: the buffer is not writable", n, buf);
return -1;
}
return got;
int64_t got = plat_file_pread(f, buf, n, off);
if (got == -2) // a read-only page in the buffer: a frozen region missed its thaw
rt_log("RomFS: read of %u bytes into %p refused: the buffer is not writable", n, buf);
return got < 0 ? -1 : got;
}
static HANDLE open_read(const char *path, int overlapped) {
wchar_t *w = fs_wide(path);
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | (overlapped ? FILE_FLAG_OVERLAPPED : 0), NULL);
free(w);
return h;
static PlatFile *open_read(const char *path) { return plat_file_open(path); }
static Identity identity_of(PlatFile *f) {
PlatFileId i;
need(plat_file_id(f, &i), "file information");
return (Identity){(uint32_t)i.volume, 0, i.file_id, i.size, i.write_time, i.create_time};
}
static Identity identity_of(HANDLE f) {
BY_HANDLE_FILE_INFORMATION i;
need(GetFileInformationByHandle(f, &i), "file information");
return (Identity){i.dwVolumeSerialNumber, 0, (uint64_t)i.nFileIndexHigh << 32 | i.nFileIndexLow,
(uint64_t)i.nFileSizeHigh << 32 | i.nFileSizeLow,
(uint64_t)i.ftLastWriteTime.dwHighDateTime << 32 | i.ftLastWriteTime.dwLowDateTime,
(uint64_t)i.ftCreationTime.dwHighDateTime << 32 | i.ftCreationTime.dwLowDateTime};
}
// ---- SHA-256 over a file range (BCrypt, streaming)
// ---- SHA-256 over a file range, streaming
static void hex32(const uint8_t d[32], char out[65]) {
for (int i = 0; i < 32; i++) snprintf(out + 2 * i, 3, "%02x", d[i]);
}
static void sha_range(HANDLE f, uint64_t offset, uint64_t size, char hex[65], int progress) {
BCRYPT_ALG_HANDLE alg;
BCRYPT_HASH_HANDLE h;
need(!BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, NULL, 0) &&
!BCryptCreateHash(alg, &h, NULL, 0, NULL, 0, 0), "SHA-256 provider");
static void sha_range(PlatFile *f, uint64_t offset, uint64_t size, char hex[65], int progress) {
Sha256 h;
sha256_init(&h);
uint8_t *buf = malloc(HASH_CHUNK);
need(buf != NULL, "hash buffer");
double t0 = now_s();
@@ -132,16 +103,14 @@ static void sha_range(HANDLE f, uint64_t offset, uint64_t size, char hex[65], in
uint32_t want = size - done > HASH_CHUNK ? HASH_CHUNK : (uint32_t)(size - done);
int64_t n = read_at(f, buf, want, offset + done);
need(n > 0, "read of hash input");
need(!BCryptHashData(h, buf, (ULONG)n, 0), "SHA-256 update");
sha256_update(&h, buf, (size_t)n);
done += (uint64_t)n;
if (progress && (done % (2ull << 30) < (uint64_t)n || done == size))
rt_log("RomFS: hashed %.1f of %.1f GiB (%.0f MiB/s)", done / 1073741824.0, size / 1073741824.0,
done / 1048576.0 / (now_s() - t0 + 1e-9));
}
uint8_t d[32];
need(!BCryptFinishHash(h, d, 32, 0), "SHA-256 final");
BCryptDestroyHash(h);
BCryptCloseAlgorithmProvider(alg, 0);
sha256_final(&h, d);
free(buf);
hex32(d, hex);
}
@@ -158,14 +127,13 @@ static void verify_image(const char *dir) {
memcpy(want.magic, "TOTKRFS-PROOF-1", 16);
memcpy(want.sha256, ROMFS_IMAGE_SHA256, sizeof ROMFS_IMAGE_SHA256 < 65 ? sizeof ROMFS_IMAGE_SHA256 : 65);
want.id = identity_of(image);
char path[2100], tmp[2200];
char path[2100];
need(snprintf(path, sizeof path, "%s/.romfs-verified-v1", dir) < (int)sizeof path, "proof path");
if (strcmp(mode, "force")) {
HANDLE p = open_read(path, 0);
DWORD got = 0;
if (p != INVALID_HANDLE_VALUE) {
int ok = ReadFile(p, &saved, sizeof saved, &got, NULL) && got == sizeof saved;
CloseHandle(p);
PlatFile *p = open_read(path);
if (p) {
int ok = plat_file_pread(p, &saved, sizeof saved, 0) == sizeof saved;
plat_file_close(p);
if (ok && !memcmp(&saved, &want, sizeof want)) {
rt_log("RomFS unchanged since its full SHA-256 check (%s)", path);
return;
@@ -180,17 +148,7 @@ static void verify_image(const char *dir) {
ROMFS_IMAGE_SHA256);
Identity after = identity_of(image);
need(!memcmp(&after, &want.id, sizeof after), "image changed during verification");
snprintf(tmp, sizeof tmp, "%s.%lu.tmp", path, GetCurrentProcessId());
wchar_t *wt = fs_wide(tmp), *wp = fs_wide(path);
HANDLE out = CreateFileW(wt, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
if (out != INVALID_HANDLE_VALUE) {
DWORD put = 0;
int ok = WriteFile(out, &want, sizeof want, &put, NULL) && put == sizeof want && FlushFileBuffers(out);
CloseHandle(out);
if (!ok || !MoveFileExW(wt, wp, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) DeleteFileW(wt);
}
free(wt);
free(wp);
plat_file_write_atomic(path, &want, sizeof want);
rt_log("RomFS: image SHA-256 ok");
}
@@ -251,9 +209,7 @@ static int overlay_load(const char *dir, int optional) {
#define BAD(...) do { if (!optional) rt_fatal(__VA_ARGS__); rt_log(__VA_ARGS__); goto bad; } while (0)
char path[2100];
need(snprintf(path, sizeof path, "%s/manifest.tsv", dir) < (int)sizeof path, "overlay path");
wchar_t *w = fs_wide(path);
FILE *m = _wfopen(w, L"rb");
free(w);
FILE *m = fopen(path, "rb");
if (!m) {
rt_log("RomFS overlay: none (%s)", path);
return 1;
@@ -288,15 +244,15 @@ static int overlay_load(const char *dir, int optional) {
}
char file[8200];
need(snprintf(file, sizeof file, "%s/%s", dir, name) < (int)sizeof file, "overlay file path");
HANDLE f = open_read(file, 1);
if (f == INVALID_HANDLE_VALUE) BAD("RomFS overlay: %s missing", file);
PlatFile *f = open_read(file);
if (!f) BAD("RomFS overlay: %s missing", file);
Identity id = identity_of(f);
if (id.size != size) {
CloseHandle(f);
plat_file_close(f);
BAD("RomFS overlay: %s has %llu bytes, its manifest says %llu", file, (unsigned long long)id.size, size);
}
sha_range(f, 0, size, hex, 0);
CloseHandle(f);
plat_file_close(f);
if (strcmp(hex, sha)) BAD("RomFS overlay: %s has SHA-256 %s, its manifest says %s", file, hex, sha);
hashed += e.size + size;
if (noverlay == overlay_cap) {
@@ -304,7 +260,7 @@ static int overlay_load(const char *dir, int optional) {
overlays = realloc(overlays, overlay_cap * sizeof *overlays);
need(overlays != NULL, "overlay table");
}
char *copy = _strdup(file), *rel = _strdup(name);
char *copy = strdup(file), *rel = strdup(name);
need(copy && rel, "overlay path copy");
overlays[noverlay++] = (Overlay){i, size, copy, rel, id};
if (noverlay - first <= 16)
@@ -373,11 +329,11 @@ static void heapdef_overlay(void) {
Overlay *base = NULL;
for (size_t k = 0; k < noverlay; k++)
if (overlays[k].index == i) base = &overlays[k];
HANDLE f = base ? open_read(base->path, 1) : image;
PlatFile *f = base ? open_read(base->path) : image;
uint64_t off = base ? 0 : entry(i).offset, n = base ? base->size : entry(i).size;
void *in = n && n < (16u << 20) && f != INVALID_HANDLE_VALUE ? malloc((size_t)n) : NULL;
void *in = n && n < (16u << 20) && f ? malloc((size_t)n) : NULL;
int ok = in && read_at(f, in, (uint32_t)n, off) == (int64_t)n;
if (base && f != INVALID_HANDLE_VALUE) CloseHandle(f);
if (base && f) plat_file_close(f);
void *out = NULL;
size_t out_n = 0;
ok = ok && res_heapdef_build(in, (size_t)n, &out, &out_n);
@@ -386,23 +342,17 @@ static void heapdef_overlay(void) {
char dir[2100], file[2200];
need(snprintf(dir, sizeof dir, "%s/resolution", rt_config()->profile) < (int)sizeof dir, "resolution path");
need(snprintf(file, sizeof file, "%s/Product.Nin_NX_NVN.HeapDef.byml.zs", dir) < (int)sizeof file, "resolution path");
wchar_t *wd = fs_wide(dir), *wf = fs_wide(file);
CreateDirectoryW(wd, NULL);
HANDLE w = CreateFileW(wf, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD put = 0;
ok = w != INVALID_HANDLE_VALUE && WriteFile(w, out, (DWORD)out_n, &put, NULL) && put == out_n;
if (w != INVALID_HANDLE_VALUE) CloseHandle(w);
free(wd);
free(wf);
plat_file_mkdir_p(dir);
ok = plat_file_write(file, out, out_n);
free(out);
HANDLE r = ok ? open_read(file, 1) : INVALID_HANDLE_VALUE;
if (r == INVALID_HANDLE_VALUE) {
rt_log("RomFS: cannot write %s (error %lu); the game keeps its own resolution", file, GetLastError());
PlatFile *r = ok ? open_read(file) : NULL;
if (!r) {
rt_log("RomFS: cannot write %s; the game keeps its own resolution", file);
return;
}
Identity id = identity_of(r);
CloseHandle(r);
char *copy = _strdup(file);
plat_file_close(r);
char *copy = strdup(file);
need(copy != NULL, "overlay path copy");
if (base) {
free(base->path);
@@ -415,7 +365,7 @@ static void heapdef_overlay(void) {
overlays = realloc(overlays, overlay_cap * sizeof *overlays);
need(overlays != NULL, "overlay table");
}
char *rel = _strdup(HEAPDEF);
char *rel = strdup(HEAPDEF);
need(rel != NULL, "overlay path copy");
overlays[noverlay++] = (Overlay){i, out_n, copy, rel, id};
}
@@ -432,8 +382,8 @@ int romfs_xbox_prompts_active(void) { return xbox_prompts_active; }
static void overlay_init(void) {
// TOTK_ROMFS_PATCHES unset: romfs-patches next to romfs-index.bin if it has a manifest (tools/make_romfs_patches.py).
// A game resolution that is not 16:9 takes romfs-patches-<w>x<h> instead when it is there: the same overlay with
// the UI laid out for that shape (make_romfs_patches.py --aspect). 1.2.1 has no camera aspect patch, so it keeps
// the 16:9 layout (res_wants_wide_overlay).
// the UI laid out for that shape (make_romfs_patches.py --aspect), when the game version has the camera aspect patch
// the layout goes with (res_wants_wide_overlay).
// With pad_layout = xbox_prompts, romfs-patches-xbox in its place: the same patches with the A/B and X/Y prompts
// swapped (make_romfs_patches.py --xbox-prompts). The game reads the archives once at boot, so the prompts stay
// until restart even though the layout itself changes live. Each folder replaces romfs-patches as a whole and the
@@ -445,7 +395,7 @@ static void overlay_init(void) {
res_wanted(out);
if (!patches && res_wants_wide_overlay(out)) {
if (snprintf(def, sizeof def, "%s/romfs-patches-%ux%u/manifest.tsv", index_dir, out[0], out[1]) < (int)sizeof def &&
GetFileAttributesA(def) != INVALID_FILE_ATTRIBUTES) {
plat_file_exists(def)) {
def[strlen(def) - strlen("/manifest.tsv")] = 0;
patches = def, wide = 1;
} else {
@@ -455,8 +405,8 @@ static void overlay_init(void) {
}
xbox_prompts_ready =
snprintf(xbox, sizeof xbox, "%s/romfs-patches-xbox/manifest.tsv", index_dir) < (int)sizeof xbox &&
GetFileAttributesA(xbox) != INVALID_FILE_ATTRIBUTES;
int want_xbox = !_stricmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
plat_file_exists(xbox);
int want_xbox = !strcasecmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
if (!patches && want_xbox && xbox_prompts_ready) {
xbox[strlen(xbox) - strlen("/manifest.tsv")] = 0;
xbox_prompts_active = overlay_load(xbox, 1);
@@ -469,7 +419,7 @@ static void overlay_init(void) {
: "romfs-patches-xbox has no manifest.tsv (run the setup again)");
}
if (!patches && !xbox_prompts_active && snprintf(def, sizeof def, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof def &&
GetFileAttributesA(def) != INVALID_FILE_ATTRIBUTES) {
plat_file_exists(def)) {
def[strlen(def) - strlen("/manifest.tsv")] = 0;
patches = def;
}
@@ -493,38 +443,25 @@ static void index_path(char *out, size_t cap) {
need(snprintf(out, cap, "%s", e) < (int)cap, "index path");
return;
}
wchar_t exe[MAX_PATH * 2];
DWORD n = GetModuleFileNameW(NULL, exe, MAX_PATH * 2);
need(n && n < MAX_PATH * 2, "executable path");
while (n && exe[n - 1] != L'\\' && exe[n - 1] != L'/') n--;
exe[n] = 0;
int k = WideCharToMultiByte(CP_UTF8, 0, exe, -1, out, (int)cap, NULL, NULL);
need(k > 0 && (size_t)k + 16 < cap, "executable path");
strcat(out, "romfs-index.bin");
need(plat_exe_path(out, cap), "executable path");
char *sep = plat_last_sep(out);
size_t k = sep ? (size_t)(sep - out) + 1 : 0;
need(k + 16 < cap, "executable path");
strcpy(out + k, "romfs-index.bin");
}
static void load_index(void) {
char path[2100];
index_path(path, sizeof path);
snprintf(index_dir, sizeof index_dir, "%s", path);
char *sep = strrchr(index_dir, '\\'), *slash = strrchr(index_dir, '/');
if (slash > sep) sep = slash;
char *sep = plat_last_sep(index_dir);
if (sep) *sep = 0;
else strcpy(index_dir, ".");
HANDLE f = open_read(path, 0);
if (f == INVALID_HANDLE_VALUE)
rt_fatal("RomFS index %s missing (tools/build_game.py writes it next to totk.exe)", path);
LARGE_INTEGER sz;
need(GetFileSizeEx(f, &sz) && sz.QuadPart >= 24 && sz.QuadPart < (128 << 20), "index size");
index_size = (size_t)sz.QuadPart;
index_bytes = malloc(index_size);
need(index_bytes != NULL, "index allocation");
for (size_t done = 0; done < index_size;) {
DWORD got = 0;
need(ReadFile(f, index_bytes + done, (DWORD)(index_size - done), &got, NULL) && got, "index read");
done += got;
}
CloseHandle(f);
uint64_t sz = 0;
index_bytes = plat_file_read(path, &sz);
if (!index_bytes) rt_fatal("RomFS index %s missing (tools/build_game.py writes it next to totk.exe)", path);
need(sz >= 24 && sz < (128 << 20), "index size");
index_size = (size_t)sz;
uint8_t d[32];
char hex[65];
rt_sha256(index_bytes, index_size, d);
@@ -551,8 +488,8 @@ static void load_index(void) {
rt_log("RomFS index %s: SHA-256 ok, %llu files, %zu bytes", path, (unsigned long long)file_count, index_size);
}
static BOOL CALLBACK init_once(PINIT_ONCE once, PVOID param, PVOID *ctx) {
(void)once, (void)param, (void)ctx;
static void init_once(void *param) {
(void)param;
#ifdef ROMFS_MISSING
rt_fatal("this build has no RomFS: tools/build_game.py found no index.json in its --romfs directory");
#else
@@ -560,38 +497,36 @@ static BOOL CALLBACK init_once(PINIT_ONCE once, PVOID param, PVOID *ctx) {
load_index();
const char *dir = rt_config()->romfs;
need(snprintf(image_file, sizeof image_file, "%s/romfs.bin", dir) < (int)sizeof image_file, "image path");
image = open_read(image_file, 1);
if (image == INVALID_HANDLE_VALUE) rt_fatal("RomFS image %s missing (error %lu); put romfs.bin into the romfs folder next to totk.exe or set TOTK_ROMFS",
image_file, GetLastError());
LARGE_INTEGER sz;
need(GetFileSizeEx(image, &sz), "image size");
if ((uint64_t)sz.QuadPart != ROMFS_IMAGE_SIZE)
image = open_read(image_file);
if (!image) rt_fatal("RomFS image %s missing; put romfs.bin into the romfs folder next to totk or set TOTK_ROMFS",
image_file);
PlatFileId sz;
need(plat_file_id(image, &sz), "image size");
if (sz.size != ROMFS_IMAGE_SIZE)
rt_fatal("RomFS image %s has %llu bytes, the index was built for %llu", image_file,
(unsigned long long)sz.QuadPart, (unsigned long long)ROMFS_IMAGE_SIZE);
(unsigned long long)sz.size, (unsigned long long)ROMFS_IMAGE_SIZE);
rt_log("RomFS image %s: size ok (%llu bytes)", image_file, (unsigned long long)ROMFS_IMAGE_SIZE);
verify_image(index_dir);
const char *mm = cfg_get("TOTK_ROMFS_MMAP");
if (mm && strcmp(mm, "0")) {
HANDLE map = CreateFileMappingW(image, NULL, PAGE_READONLY, 0, 0, NULL);
const void *v = map ? MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0) : NULL;
if (map) CloseHandle(map); // the view keeps the mapping alive
const void *v = plat_file_map(image);
if (v) {
image_map = v;
rt_log("RomFS: mapped read path enabled");
} else {
rt_log("RomFS: mapping failed (error %lu); using ReadFile", GetLastError());
rt_log("RomFS: mapping failed; using positional reads");
}
}
overlay_init();
rt_log("RomFS ready: %llu files, %zu overlays, %zu added, %.2f s", (unsigned long long)file_count, noverlay,
nadded, now_s() - t0);
#endif
return TRUE;
return;
}
void fs_init(void) {
static INIT_ONCE once = INIT_ONCE_STATIC_INIT;
InitOnceExecuteOnce(&once, init_once, NULL, NULL);
static PlatOnce once = PLAT_ONCE_INIT;
plat_once(&once, init_once, NULL);
}
// ---- the RomFS interface (fs.h)
@@ -636,10 +571,10 @@ uint32_t romfs_mount(const char *name) {
if (!name || !romfs_path_valid_relative(name) || strlen(name) >= sizeof mount_name || strchr(name, '/') ||
strchr(name, ':'))
return FS_R_INVALID;
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
if (*mount_name && strcmp(name, mount_name)) rt_fatal("second RomFS mount %s (%s is mounted)", name, mount_name);
strcpy(mount_name, name);
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
rt_log("fs mount %s: -> RomFS", name);
return 0;
}
@@ -652,39 +587,39 @@ uint32_t romfs_open(uint64_t *out, const char *path, int mode) {
uint64_t i = file_index(p);
if (i == UINT64_MAX) return FS_R_PATH_NOT_FOUND;
uint32_t ov = overlay_slot(i);
HANDLE f = INVALID_HANDLE_VALUE;
PlatFile *f = NULL;
if (ov) {
const Overlay *o = &overlays[ov - 1];
f = open_read(o->path, 1);
f = open_read(o->path);
Identity id;
if (f != INVALID_HANDLE_VALUE) id = identity_of(f);
if (f == INVALID_HANDLE_VALUE || memcmp(&id, &o->id, sizeof id))
if (f) id = identity_of(f);
if (!f || memcmp(&id, &o->id, sizeof id))
rt_fatal("RomFS overlay %s is missing or changed since the start check (restart the game)", o->path);
}
AcquireSRWLockExclusive(&lock);
plat_mutex_lock(&lock);
for (unsigned j = 1; j < NH; j++)
if (!handles[j]) {
handles[j] = i + 1;
handle_ovl[j] = ov;
handle_file[j] = f;
*out = ROMFS_HANDLE_BASE + j;
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
return 0;
}
ReleaseSRWLockExclusive(&lock);
plat_mutex_unlock(&lock);
rt_fatal("RomFS file handle table full (%d)", NH);
}
// The handle's index row (an added file's is empty: its bytes are all in its overlay), and where its bytes come from:
// the overlay file, or the original's range of the image.
static int handle_source(uint64_t h, HANDLE *f, uint64_t *base, uint64_t *size) {
static int handle_source(uint64_t h, PlatFile **f, uint64_t *base, uint64_t *size) {
if (!romfs_handle(h)) return 0;
uint64_t j = h - ROMFS_HANDLE_BASE;
AcquireSRWLockShared(&lock);
plat_mutex_lock_shared(&lock);
uint64_t i = handles[j];
uint32_t ov = handle_ovl[j];
HANDLE of = handle_file[j];
ReleaseSRWLockShared(&lock);
PlatFile *of = handle_file[j];
plat_mutex_unlock_shared(&lock);
if (!i) return 0;
if (ov) {
*f = of;
@@ -700,7 +635,7 @@ static int handle_source(uint64_t h, HANDLE *f, uint64_t *base, uint64_t *size)
}
uint32_t romfs_read(uint64_t *out, uint64_t h, int64_t offset, void *buffer, uint64_t size) {
HANDLE f;
PlatFile *f;
uint64_t base, fsize;
if (!out || offset < 0 || !handle_source(h, &f, &base, &fsize)) return FS_R_INVALID;
if ((uint64_t)offset > fsize || (!buffer && size)) return FS_R_INVALID;
@@ -712,8 +647,7 @@ uint32_t romfs_read(uint64_t *out, uint64_t h, int64_t offset, void *buffer, uin
while (done < n) {
uint32_t chunk = n - done > READ_CHUNK ? READ_CHUNK : (uint32_t)(n - done);
int64_t r = read_at(f, (uint8_t *)buffer + done, chunk, base + (uint64_t)offset + done);
if (r <= 0) rt_fatal("RomFS read failure at %#llx (error %lu)", (unsigned long long)(base + offset + done),
GetLastError());
if (r <= 0) rt_fatal("RomFS read failure at %#llx", (unsigned long long)(base + offset + done));
done += (uint64_t)r;
}
*out = done;
@@ -721,7 +655,7 @@ uint32_t romfs_read(uint64_t *out, uint64_t h, int64_t offset, void *buffer, uin
}
uint32_t romfs_size(int64_t *out, uint64_t h) {
HANDLE f;
PlatFile *f;
uint64_t base, size;
if (!out || !handle_source(h, &f, &base, &size)) return FS_R_INVALID;
*out = (int64_t)size;
@@ -729,18 +663,18 @@ uint32_t romfs_size(int64_t *out, uint64_t h) {
}
void romfs_close(uint64_t h) {
HANDLE f;
PlatFile *f;
uint64_t base, size;
if (!handle_source(h, &f, &base, &size))
rt_fatal("RomFS: close of invalid file handle %#llx", (unsigned long long)h);
uint64_t j = h - ROMFS_HANDLE_BASE;
AcquireSRWLockExclusive(&lock);
HANDLE of = handle_ovl[j] ? handle_file[j] : INVALID_HANDLE_VALUE;
plat_mutex_lock(&lock);
PlatFile *of = handle_ovl[j] ? handle_file[j] : NULL;
handles[j] = 0;
handle_ovl[j] = 0;
handle_file[j] = INVALID_HANDLE_VALUE;
ReleaseSRWLockExclusive(&lock);
if (of != INVALID_HANDLE_VALUE) CloseHandle(of);
handle_file[j] = NULL;
plat_mutex_unlock(&lock);
if (of) plat_file_close(of);
}
uint32_t romfs_entry_type(int *out, const char *path) {
+130
View File
@@ -0,0 +1,130 @@
// SHA-256 (FIPS 180-4). The block function has two forms: one on the x86 SHA extensions (SHA-NI: Intel since Ice Lake
// and Goldmont, AMD since Zen), which hashes the RomFS image at about the speed of a disk read, and portable C for CPUs
// without them. sha256_selftest in the probes compares both against the FIPS test vectors.
#include "sha256.h"
#include <string.h>
#if defined(__x86_64__)
#include <immintrin.h>
#endif
static const uint32_t K[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
};
static inline uint32_t ror(uint32_t x, int n) { return x >> n | x << (32 - n); }
static void blocks_c(uint32_t h[8], const uint8_t *p, size_t n) {
for (; n; n--, p += 64) {
uint32_t w[64];
for (int i = 0; i < 16; i++)
w[i] = (uint32_t)p[4 * i] << 24 | (uint32_t)p[4 * i + 1] << 16 | (uint32_t)p[4 * i + 2] << 8 | p[4 * i + 3];
for (int i = 16; i < 64; i++) {
uint32_t s0 = ror(w[i - 15], 7) ^ ror(w[i - 15], 18) ^ w[i - 15] >> 3;
uint32_t s1 = ror(w[i - 2], 17) ^ ror(w[i - 2], 19) ^ w[i - 2] >> 10;
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
}
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], k = h[7];
for (int i = 0; i < 64; i++) {
uint32_t t1 = k + (ror(e, 6) ^ ror(e, 11) ^ ror(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
uint32_t t2 = (ror(a, 2) ^ ror(a, 13) ^ ror(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
k = g, g = f, f = e, e = d + t1, d = c, c = b, b = a, a = t1 + t2;
}
h[0] += a, h[1] += b, h[2] += c, h[3] += d, h[4] += e, h[5] += f, h[6] += g, h[7] += k;
}
}
#if defined(__x86_64__)
// The state lives in two registers as the SHA instructions want it: ABEF and CDGH. Each group of four rounds adds the
// round constants to four message words and runs sha256rnds2 twice; the message schedule for words 16..63 is
// msg2(msg1(W[t-16..], W[t-12..]) + W[t-7..], W[t-4..]).
__attribute__((target("sha,sse4.1"))) static void blocks_ni(uint32_t h[8], const uint8_t *p, size_t n) {
const __m128i bswap = _mm_set_epi64x(0x0c0d0e0f08090a0bull, 0x0405060700010203ull);
__m128i t = _mm_shuffle_epi32(_mm_loadu_si128((const __m128i *)&h[0]), 0xb1); // CDAB
__m128i s1 = _mm_shuffle_epi32(_mm_loadu_si128((const __m128i *)&h[4]), 0x1b); // EFGH
__m128i s0 = _mm_alignr_epi8(t, s1, 8); // ABEF
s1 = _mm_blend_epi16(s1, t, 0xf0); // CDGH
for (; n; n--, p += 64) {
__m128i a0 = s0, c0 = s1, m[4];
for (int g = 0; g < 16; g++) {
__m128i *w = &m[g & 3];
if (g < 4) *w = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(p + 16 * g)), bswap);
else
*w = _mm_sha256msg2_epu32(_mm_add_epi32(_mm_sha256msg1_epu32(*w, m[(g + 1) & 3]),
_mm_alignr_epi8(m[(g + 3) & 3], m[(g + 2) & 3], 4)),
m[(g + 3) & 3]);
__m128i k = _mm_add_epi32(*w, _mm_loadu_si128((const __m128i *)&K[4 * g]));
s1 = _mm_sha256rnds2_epu32(s1, s0, k);
s0 = _mm_sha256rnds2_epu32(s0, s1, _mm_shuffle_epi32(k, 0x0e));
}
s0 = _mm_add_epi32(s0, a0);
s1 = _mm_add_epi32(s1, c0);
}
t = _mm_shuffle_epi32(s0, 0x1b); // FEBA
s1 = _mm_shuffle_epi32(s1, 0xb1); // DCHG
_mm_storeu_si128((__m128i *)&h[0], _mm_blend_epi16(t, s1, 0xf0)); // DCBA
_mm_storeu_si128((__m128i *)&h[4], _mm_alignr_epi8(s1, t, 8)); // HGFE
}
#endif
static void blocks(uint32_t h[8], const uint8_t *p, size_t n) {
#if defined(__x86_64__)
static int ni = -1;
if (ni < 0) ni = __builtin_cpu_supports("sha") && __builtin_cpu_supports("sse4.1");
if (ni) return blocks_ni(h, p, n);
#endif
blocks_c(h, p, n);
}
void sha256_init(Sha256 *s) {
static const uint32_t iv[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
memcpy(s->h, iv, sizeof iv);
s->bytes = 0;
s->used = 0;
}
void sha256_update(Sha256 *s, const void *data, size_t n) {
const uint8_t *p = data;
s->bytes += n;
if (s->used) {
size_t k = 64 - s->used < n ? 64 - s->used : n;
memcpy(s->buf + s->used, p, k);
s->used += (uint32_t)k, p += k, n -= k;
if (s->used < 64) return;
blocks(s->h, s->buf, 1);
s->used = 0;
}
if (n >= 64) {
blocks(s->h, p, n / 64);
p += n & ~(size_t)63, n &= 63;
}
memcpy(s->buf, p, n);
s->used = (uint32_t)n;
}
void sha256_final(Sha256 *s, uint8_t out[32]) {
uint64_t bits = s->bytes * 8;
uint8_t pad[72] = {0x80};
size_t k = (s->used < 56 ? 56 : 120) - s->used;
for (int i = 0; i < 8; i++) pad[k + i] = (uint8_t)(bits >> (56 - 8 * i));
sha256_update(s, pad, k + 8);
for (int i = 0; i < 8; i++)
out[4 * i] = (uint8_t)(s->h[i] >> 24), out[4 * i + 1] = (uint8_t)(s->h[i] >> 16),
out[4 * i + 2] = (uint8_t)(s->h[i] >> 8), out[4 * i + 3] = (uint8_t)s->h[i];
}
void sha256(const void *data, size_t n, uint8_t out[32]) {
Sha256 s;
sha256_init(&s);
sha256_update(&s, data, n);
sha256_final(&s, out);
}
+16
View File
@@ -0,0 +1,16 @@
// SHA-256 (FIPS 180-4), streaming. Uses the x86 SHA extensions when the CPU has them, portable C otherwise.
#pragma once
#include <stddef.h>
#include <stdint.h>
typedef struct {
uint32_t h[8];
uint64_t bytes; // message bytes so far
uint8_t buf[64]; // a partial block
uint32_t used;
} Sha256;
void sha256_init(Sha256 *s);
void sha256_update(Sha256 *s, const void *data, size_t n);
void sha256_final(Sha256 *s, uint8_t out[32]);
void sha256(const void *data, size_t n, uint8_t out[32]); // the three above in one call
+23
View File
@@ -0,0 +1,23 @@
#version 450
// Colour adjustment on the present (runtime/vk_color.c): the game's picture, crop rectangle at offset, into an RGBA16F
// image the FSR 1 pass or the blit then scales to the window. Brightness, contrast around mid-grey, saturation against
// Rec. 709 luma, then a power curve: the order and ranges of STS78721's colour profiles (issue #136).
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D src;
layout(binding = 1, rgba16f) uniform writeonly image2D dst;
layout(push_constant) uniform P {
ivec2 offset; // the crop rectangle's corner in src
ivec2 size; // its size, the size of dst
vec4 adj; // brightness (added), contrast, saturation, gamma (exponent)
} pc;
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(p, pc.size))) return;
vec3 c = texelFetch(src, p + pc.offset, 0).rgb;
c += pc.adj.x;
c = (c - 0.5) * pc.adj.y + 0.5;
c = mix(vec3(dot(c, vec3(0.2126, 0.7152, 0.0722))), c, pc.adj.z);
c = pow(max(c, vec3(0.0)), vec3(pc.adj.w));
imageStore(dst, p, vec4(clamp(c, 0.0, 1.0), 1.0));
}
+5 -12
View File
@@ -3,8 +3,6 @@
// The SVC entry of the lifted code: preemption checkpoint, trace, then the thread and
// synchronization SVCs (kernel.c), IPC (ipc.c), memory (memory.c), Break, OutputDebugString, GetInfo and ExitProcess. The 21 SVCs
// TotK has but never reaches on MNG's boot path stop with a named fatal, the caller and the guest backtrace.
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#include <string.h>
@@ -12,6 +10,7 @@
#include "host.h"
#include "kernel.h"
#include "vk.h"
#include "platform/platform.h"
#define R_OK 0u
#define ST_FREE 0u
@@ -26,12 +25,7 @@ RtStats rt_stats(void) {
return (RtStats){__atomic_load_n(&g_svcs, __ATOMIC_RELAXED), kern_threads_created(), hostfn_calls()};
}
static int readable(uint64_t a, uint64_t n) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
return a + n <= (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
}
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
void kern_backtrace(const Cpu *c) {
char d[160];
@@ -122,8 +116,7 @@ static void get_info(Cpu *c, uint64_t pc) {
case 8: v = 0; break; // debugger attached
case 10: v = 0; break; // idle tick count
case 11: // random entropy
if (BCryptGenRandom(NULL, (PUCHAR)&v, sizeof v, BCRYPT_USE_SYSTEM_PREFERRED_RNG))
rt_fatal("GetInfo 11: BCryptGenRandom failed");
if (!plat_random(&v, sizeof v)) rt_fatal("GetInfo 11: no random bytes from the system");
break;
case 18: v = PROGRAM_ID; break;
case 20: v = user_exception_context(); break; // the SDK keeps e.g. the dying message at +0x1C0
@@ -217,7 +210,7 @@ static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
return;
}
case 0x29: get_info(c, pc); return;
case 0x07: { // ExitProcess, as closing the window (gfx_window.c WM_CLOSE): pipeline cache, shader records and
case 0x07: { // ExitProcess, as closing the window (gfx_window_closed): pipeline cache, shader records and
// profile saved, logs flushed, exit code 0
char at[160];
rt_log("guest ExitProcess at %s; exiting", ldr_describe(pc, at, sizeof at));
@@ -226,7 +219,7 @@ static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
prof_write();
fflush(NULL);
if (svc_test_exit_hook) return svc_test_exit_hook(0);
ExitProcess(0);
plat_exit(0);
}
}
svc_fatal(c, imm, pc, "not implemented");
+208
View File
@@ -0,0 +1,208 @@
// Adapted from MacNativeGames games/totk/runtime/swkbd_host.c @ b75be8cf (MIT, Copyright (c) 2026 MacNativeGames
// contributors): the ShowKeyboardArg offsets, the UTF-8/UTF-16 conversions, the disabled-key filter and the result codes.
// The macOS text bar is replaced by a window of the host (swkbd.h text_entry_run: swkbd_win.c with Win32, swkbd_sdl.c
// with SDL).
//
// nn::swkbd::ShowKeyboard: text entry for the game. On the Switch the call starts the keyboard library applet, which
// takes the screen until the player confirms or cancels. TotK calls it in one place, naming a horse at a stable
// (main+0x1ecbf64 -> main+0x1b2b3a0: UserName preset, at most 9 characters, '@' '%' '\' disabled, UTF-16 out, no
// header, guide or initial text; an empty answer counts as not named).
//
// Front end: text_entry_run (swkbd.h), run on the guest thread that called (the call blocks, as on the Switch). The thread gives its virtual core up meanwhile, so the other guest threads keep running; it holds no
// lock across the wait. The text starts as the game's initial text, else a default name from the catalog (ui_text.c),
// selected, so a controller can confirm it: A = OK, B = cancel (each after it was seen released); Enter / Esc and the
// buttons work too. While the window is open, and after it closes until everything is released, the game reads no
// input (input_block). text_entry_run is the only front-end seam: an overlay (ImGui) can replace it.
//
// Environment:
// TOTK_SWKBD_ANSWER=<text>|<cancel>|<initial> answer without a window (probes, scripted runs); <initial> keeps the
// starting text (the game's, else the default name)
// TOTK_SWKBD_TEST=ok|cancel open the window, then press OK / Cancel on the first timer tick
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "input.h"
#include "ui_text.h"
#include "swkbd.h"
#include "platform/platform.h"
// KeyboardConfig then the work buffer {pointer, size}: offsets read from the SDK setters (MNG) and checked against
// the SDK by --probe-services. SDK 20.5 (1.4.3) put a text-check callback at +0x4c8 and grew KeyboardConfig from
// 0x4d0 to 0x518 bytes (SetInitialText, SetUserWordList read the work buffer there); the fields below +0x3d8 stay.
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0)
#define ARG_WORK_SIZE (ARG_WORK + 8)
enum {
KB_TYPE = 0x00, KB_OK_TEXT = 0x04, KB_DISABLED = 0x1c, KB_HEADER = 0x24, KB_SUB = 0xa6, KB_GUIDE = 0x1a8,
KB_MAX_LEN = 0x3ac, KB_MIN_LEN = 0x3b0, KB_PASSWORD = 0x3b4, KB_UTF8 = 0x3bd, KB_INIT_OFF = 0x3c0, KB_INIT_LEN = 0x3c4,
KB_TYPE_NUMPAD = 1, SWKBD_CANCELED = 0x29f,
};
static uint32_t rd32(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v; }
static uint64_t rd64(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return v; }
static size_t u16_len(const uint16_t *s, size_t max) { size_t n = 0; while (n < max && s[n]) n++; return n; }
// UTF-16 (n units) -> NUL-terminated UTF-8 in cap bytes, cut at a character boundary. Returns the bytes written.
size_t swkbd_utf16_to_utf8(const uint16_t *s, size_t n, char *out, size_t cap) {
size_t o = 0;
if (!cap) return 0;
for (size_t i = 0; i < n; i++) {
uint32_t c = s[i];
if (c >= 0xd800 && c < 0xdc00 && i + 1 < n && s[i + 1] >= 0xdc00 && s[i + 1] < 0xe000)
c = 0x10000 + ((c - 0xd800) << 10) + (s[++i] - 0xdc00);
else if (c >= 0xd800 && c < 0xe000) c = 0xfffd;
char b[4];
size_t k;
if (c < 0x80) { b[0] = (char)c; k = 1; }
else if (c < 0x800) { b[0] = (char)(0xc0 | c >> 6); b[1] = (char)(0x80 | (c & 0x3f)); k = 2; }
else if (c < 0x10000) { b[0] = (char)(0xe0 | c >> 12); b[1] = (char)(0x80 | ((c >> 6) & 0x3f)); b[2] = (char)(0x80 | (c & 0x3f)); k = 3; }
else { b[0] = (char)(0xf0 | c >> 18); b[1] = (char)(0x80 | ((c >> 12) & 0x3f)); b[2] = (char)(0x80 | ((c >> 6) & 0x3f)); b[3] = (char)(0x80 | (c & 0x3f)); k = 4; }
if (o + k + 1 > cap) break;
memcpy(out + o, b, k);
o += k;
}
out[o] = 0;
return o;
}
// UTF-8 -> UTF-16, at most cap units (a character that does not fit whole is dropped); bad bytes become U+FFFD.
size_t swkbd_utf8_to_utf16(const char *s, uint16_t *out, size_t cap) {
const unsigned char *p = (const unsigned char *)s;
size_t n = 0;
while (*p) {
uint32_t c;
int k;
if (*p < 0x80) { c = *p; k = 0; }
else if ((*p & 0xe0) == 0xc0) { c = *p & 0x1f; k = 1; }
else if ((*p & 0xf0) == 0xe0) { c = *p & 0x0f; k = 2; }
else if ((*p & 0xf8) == 0xf0) { c = *p & 0x07; k = 3; }
else { c = 0xfffd; k = 0; }
p++;
for (int i = 0; i < k; i++, p++) {
if ((*p & 0xc0) != 0x80) { c = 0xfffd; break; }
c = c << 6 | (*p & 0x3f);
}
if (c > 0x10ffff || (c >= 0xd800 && c < 0xe000)) c = 0xfffd;
if (c >= 0x10000) {
if (n + 2 > cap) break;
out[n++] = (uint16_t)(0xd800 + ((c - 0x10000) >> 10));
out[n++] = (uint16_t)(0xdc00 + ((c - 0x10000) & 0x3ff));
} else {
if (n + 1 > cap) break;
out[n++] = (uint16_t)c;
}
}
return n;
}
// Whether the applet would let this character through (single UTF-16 units; pairs are always allowed). Disabled-key
// bits as the applet's: 1 space, 2 '@', 3 '%', 4 '/', 5 '\', 6 digits.
int swkbd_allowed(uint16_t c, uint32_t disabled, int digits_only) {
int digit = c >= '0' && c <= '9';
if (digits_only && !digit) return 0;
if (c < 0x20 || c == 0x7f) return 0;
if ((disabled & 1u << 1 && c == ' ') || (disabled & 1u << 2 && c == '@') || (disabled & 1u << 3 && c == '%')) return 0;
if ((disabled & 1u << 4 && c == '/') || (disabled & 1u << 5 && c == '\\') || (disabled & 1u << 6 && digit)) return 0;
return 1;
}
// Drop disallowed characters and keep at most max units without splitting a surrogate pair.
static size_t clean(uint16_t *t, size_t n, size_t max, uint32_t disabled, int digits_only) {
size_t o = 0;
for (size_t i = 0; i < n; i++) {
int pair = t[i] >= 0xd800 && t[i] < 0xdc00 && i + 1 < n;
size_t k = pair ? 2 : 1;
if (!pair && !swkbd_allowed(t[i], disabled, digits_only)) continue;
if (o + k > max) break;
t[o++] = t[i];
if (pair) t[o++] = t[++i];
}
return o;
}
// ---- nn::swkbd::ShowKeyboard
static void request_from(const uint8_t *arg, SwkbdRequest *r) {
memset(r, 0, sizeof *r);
int32_t max_len = (int32_t)rd32(arg + KB_MAX_LEN), min_len = (int32_t)rd32(arg + KB_MIN_LEN);
r->max_len = max_len <= 0 || max_len > KB_LIMIT ? KB_LIMIT : max_len;
r->min_len = min_len < 0 ? 0 : min_len > r->max_len ? r->max_len : min_len;
r->password = rd32(arg + KB_PASSWORD) != 0;
r->numeric = rd32(arg + KB_TYPE) == KB_TYPE_NUMPAD;
r->disabled = rd32(arg + KB_DISABLED);
static const struct { int off; size_t max; } texts[] = {{KB_HEADER, 0x40}, {KB_GUIDE, 0x100}, {KB_SUB, 0x80}};
for (size_t i = 0; i < 3 && !r->prompt[0]; i++) {
const uint16_t *t = (const uint16_t *)(arg + texts[i].off);
size_t n = u16_len(t, texts[i].max);
memcpy(r->prompt, t, n * 2);
r->prompt[n] = 0;
}
uint64_t work = rd64(arg + ARG_WORK), work_size = rd64(arg + ARG_WORK_SIZE);
int32_t init_off = (int32_t)rd32(arg + KB_INIT_OFF), init_len = (int32_t)rd32(arg + KB_INIT_LEN);
size_t n = 0;
if (init_len > 0 && work && init_off >= 0 && (uint64_t)init_off + (uint64_t)init_len * 2 <= work_size) {
const uint16_t *t = (const uint16_t *)(uintptr_t)(work + (uint64_t)init_off);
n = u16_len(t, (size_t)(init_len > KB_LIMIT ? KB_LIMIT : init_len));
memcpy(r->initial, t, n * 2);
r->game_initial = n > 0;
}
if (!n) n = swkbd_utf8_to_utf16(ui_text(UI_SWKBD_DEFAULT_NAME), r->initial, KB_LIMIT);
n = clean(r->initial, n, (size_t)r->max_len, r->disabled, r->numeric);
r->initial[n] = 0;
}
// Result ShowKeyboard(String *out, const ShowKeyboardArg &arg); String = {pointer, byte size}
static void ov_show_keyboard(Cpu *c) {
uint64_t *str = arg_p(c, 0);
const uint8_t *arg = arg_p(c, 1);
char lr[96];
if (!str || !arg) rt_fatal("nn::swkbd::ShowKeyboard from %s: null argument", ldr_describe(c->x[30] - 4, lr, sizeof lr));
static SwkbdRequest req; // one keyboard at a time (the applet is modal too)
static PlatMutex one = PLAT_MUTEX_INIT;
int utf8 = arg[KB_UTF8] != 0;
uint16_t u[KB_LIMIT * 2 + 1];
size_t n = 0;
int confirmed;
const char *forced = cfg_get("TOTK_SWKBD_ANSWER");
int away = kern_core_away();
plat_mutex_lock(&one);
request_from(arg, &req);
if (forced && *forced) {
confirmed = strcmp(forced, "<cancel>") != 0;
if (!strcmp(forced, "<initial>")) n = u16_len(req.initial, KB_LIMIT), memcpy(u, req.initial, n * 2);
else if (confirmed) n = swkbd_utf8_to_utf16(forced, u, KB_LIMIT * 2);
} else {
input_block(1);
confirmed = text_entry_run(&req, u, KB_LIMIT * 2, &n);
input_block(0);
}
n = confirmed ? clean(u, n, (size_t)req.max_len, req.disabled, req.numeric) : 0;
if (confirmed && (int)n < req.min_len) confirmed = 0, n = 0;
uint64_t buf = str[0], size = str[1];
if (buf && size) {
if (utf8) swkbd_utf16_to_utf8(u, n, (char *)(uintptr_t)buf, size);
else {
size_t units = size / 2; // one kept for the terminator
if (units) {
if (n > units - 1) { n = units - 1; if (n && u[n - 1] >= 0xd800 && u[n - 1] < 0xdc00) n--; }
memcpy((void *)(uintptr_t)buf, u, n * 2);
((uint16_t *)(uintptr_t)buf)[n] = 0;
}
}
}
char prompt[0x101 * 3 + 1], shown[KB_LIMIT * 3 + 1];
swkbd_utf16_to_utf8(req.prompt, u16_len(req.prompt, 0x100), prompt, sizeof prompt);
swkbd_utf16_to_utf8(u, n, shown, sizeof shown);
if (req.password) snprintf(shown, sizeof shown, "(password, %zu units, not logged)", n);
rt_log("swkbd: from %s: \"%s\" (%d-%d units, disabled keys %#x, %s out, %s start)%s -> %s%s%s",
ldr_describe(c->x[30] - 4, lr, sizeof lr), prompt, req.min_len, req.max_len, req.disabled,
utf8 ? "UTF-8" : "UTF-16", req.game_initial ? "game" : "default", forced && *forced ? " [TOTK_SWKBD_ANSWER]" : "",
confirmed ? "\"" : "cancelled", confirmed ? shown : "", confirmed ? "\"" : "");
plat_mutex_unlock(&one);
kern_core_back(&away);
ret_x(c, confirmed ? 0 : SWKBD_CANCELED);
}
HOST_OVERRIDE("_ZN2nn5swkbd12ShowKeyboardEPNS0_6StringERKNS0_15ShowKeyboardArgE", ov_show_keyboard);
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff. Show more