31 Commits
Author SHA1 Message Date
Valiantsin DzerakhandClaude Opus 5.5 fb06577c23 Target Windows 10 in the runtime flags so llvm-mingw sees the DPI functions
llvm-mingw's headers default to Windows 7 and hide AdjustWindowRectExForDpi, GetDpiForWindow and the other per-monitor
DPI functions, so the release toolchain failed on gfx_window.c and dpi_probe.c. MSYS2's headers already default to
Windows 10, the port's minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:54:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6c27e70249 Raise the version to 0.1.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:51:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b0ec992afd Merge task/cubeflag: cube compatible only for cube textures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:30:12 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4d7fa271d4 Make only cube textures cube compatible
Every square 2D array of 6 or more layers was created cube compatible, though the replay makes cube views only of
images whose own target is a cubemap or cubemap array (vk_exec_draw.c get_view). On AMD's Windows driver the flag
changes the image layout, and Savage Wind Recomp traced rainbow blocks on terrain that moved with the mip level to
it (their issue #59, BotW on an RX 580 and a 7800 XT); our AMD and Intel reports show the same symptom (#42, #67,
#81). The flag now goes to cube targets only, on every driver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:15:39 +03:00
Valiantsin DzerakhandClaude Opus 5.5 332dfce18e Merge task/cmpfmt: depth twins for compare-sampled colour textures in compute passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:09:42 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3b5469a83f Sample compare-sampled R32F and R16 textures of compute passes through a depth twin
TotK's compute passes sample a 4x4 R32_SFLOAT texture through a shadow sampler. Vulkan allows depth comparison only
on formats with SAMPLED_IMAGE_DEPTH_COMPARISON, which AMD's drivers do not give R32_SFLOAT, so the result there was
undefined (20 validation errors in 40 s of field play). Such an image now gets a D32_SFLOAT (or D16_UNORM) twin
copied through a buffer right before each dispatch that samples it, and the dispatch samples the twin. Formats the
driver compares as they are keep the colour image. TOTK_DEPTH_TWIN=0 turns it off. Design after Savage Wind Recomp's
depth twins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:09:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 fb13c05870 Merge PR #86: drop released textures that reload intact when video memory runs low
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:56:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1a689a5b1d Merge task/gpucrash: recorder thread off on AMD, GPL off on AMD's RDNA 1/2 driver, GPU breadcrumbs on a lost device
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:53:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 43704c0d9f Turn off the recorder thread on AMD GPUs and GPL on AMD's RDNA 1/2 driver, and log GPU breadcrumbs on a lost device
AMD drivers lost the device at boot or at menu opens with the recorder thread; vk_recorder = 0 ended it for three
RX 9070 XT users (issue #5), and RADV saw an illegal opcode at the end of a command buffer (issue #40). It now
defaults to off on vendor 0x1002; TOTK_VK_RECORDER=1 turns it back on.

On AMD's separate driver branch for RDNA 1/2 ("AMD proprietary shader compiler") every device loss came with a cold
pipeline cache and draws waiting on fast-linked pipelines, and the recorder setting did not help there. TOTK_GPL now
has no fixed default: off on that branch, on elsewhere.

VK_AMD_buffer_marker breadcrumbs (vk_crumbs.c) mark the start and end of each submission part, upload batch, ASTC
decode chunk and present; on device lost the log names the last point the GPU reached and the last one it finished.

vk_fsr.c ensure_images held no queue lock around vkDeviceWaitIdle, which needs every queue externally synchronized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:48:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 473acfda2e Merge task/nosleep: keep the display awake while the game window is up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:37:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 09cf49ee25 Merge task/shaderlist: read the packed pipeline keys of the current shader list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:35:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3332250df4 Merge task/dpiaware: per-monitor DPI awareness for the window, fullscreen and the overlay
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:35:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 389a516f8d Read the packed pipeline keys of the current MacNativeGames shader list in prepare_shaders
Since MacNativeGames f024ef04 the R and B rows keep the key as "<vs variant>:<fs variant>:<rest>" instead of plain hex,
and parse_list stopped with a ValueError on a source checkout. The pinned release list parses as before (identical
variants and priorities); the current one gives 58053 variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:35:10 +03:00
Valiantsin Dzerakh f930e56825 Merge task/fade: floor the 1.4.3 LOD and pre-actor fade increments at 1 2026-10-30 15:30:46 +03:00
Valiantsin DzerakhandClaude Opus 5.5 afa3607259 Keep the display and the system awake while the game window is up and not minimized
Windows turned the monitor off after its idle timeout during play, since gamepad input does not count as activity (#7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:30:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 39ab925588 Merge task/wbcap: diagnostic fence write-back limit and its trace log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:28:18 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6dbecab25d Merge task/amd-devlost: log the VK_EXT_device_fault report on device lost
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:27:58 +03:00
Valiantsin DzerakhandClaude Opus 5.5 bb1b675381 Floor the 1.4.3 LOD and pre-actor fade increments at 1
Above about 63.5 fps the fade step trunc(2.1 * ratio) is 0, so objects that start a fade stay translucent or
invisible until a slower frame moves them on (issues #10, #37). Switching to 60 fps and back cleared it because the
60 fps step is 1. Port of MacNativeGames' high-FPS fade fix as a fade set under TOTK_HIGHFPS_FIX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:27:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 abb00c500c Add a diagnostics key for the fence write-back size limit and trace the targets it skips
wb_fence_limit (TOTK_WB_FENCE_LIMIT, default 65536) replaces the fixed 64 KiB limit of the write-back done when the CPU waits for a fence. With trace = gfx the log names each dirty target left on the GPU because of its size and each target over 64 KiB that a raised limit wrote back. Meant to test whether the fuse weapon switch bug (#18) and the Purah Pad camera bug (#13, #63) come from a target the game reads on the CPU without an invalidate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:27:42 +03:00
arthurhfcardosoandClaude Sonnet 5.5 444abcbebb Drop released textures that reload intact when video memory runs low
On a 4 GiB GPU the released-texture list (up to 2048 entries, each ASTC texture decoded to RGBA8) filled video memory and the heap spilled into system memory: a Depths lava scene went from 12 ms to 60 ms of GPU time per frame and about 16 fps. Above tex_vram_pressure percent of the device-local budget (default 90, 0 = off), each submit now destroys up to 32 of the oldest released entries.

Only entries that come back intact are dropped: mirrored textures the GPU never wrote, which are uploaded again from their storage. A render target exists only in its image; dropping those made textures come back empty (black flashes in the Depths boss fight, about 170 "left uninitialised" aliases a minute).

Adds the setting to totk.ini and the Advanced tab, and a perf log line with the released count and drops.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:26:36 -03:00
Valiantsin DzerakhandClaude Opus 5.5 fcaecabad4 Make totk.exe per-monitor DPI aware so fullscreen and the window use physical pixels
The manifest declared no DPI awareness, so Windows ran the process DPI-unaware: on a 4K monitor at 150% scale the
fullscreen window and its swapchain were 2560x1440, the 4K game image was shrunk to it and Windows stretched the
result back up, blurred (issues #25, #56, #31).

The manifest now declares PerMonitorV2 (true/pm for older Windows 10). The window frame is measured at the window's
DPI, a framed window keeps its client size when it moves to a monitor at another scale, WM_DPICHANGED takes the
suggested rect or keeps fullscreen over the monitor, and window_size means screen pixels. The menu's minimum text
size follows the window's DPI, and the text entry dialog is measured at the game window's DPI. --probe-dpi prints
the process awareness and fails unless it is per-monitor v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:40:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8eaf198443 Merge task/fps90: offer 90 FPS in the frame rate limit menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:30:29 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d8547a84ac Offer 90 FPS in the frame rate limit menu
GitHub issue #21 asks for a 90 fps limit. fps_limit = 90 in totk.ini already worked, because frame_rate.c accepts any whole
rate from 30 to 360 and cfg.c does not validate the values column. The menu row and the documented value lists did not
name it. Added 90 to the menu between 60 and 120 (new catalog id UI_MENU_FPS_90 in both languages), to the FPS_LIMIT
row in cfg.c and to totk.ini.example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:30:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 184d5798cd Merge task/zhlang: map zh-CN and zh-TW to the Chinese codes TotK accepts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:27:46 +03:00
Valiantsin DzerakhandClaude Opus 5.5 67116a387f Map the zh-CN and zh-TW game languages to zh-Hans and zh-Hant
Choosing Simplified Chinese in the installer wrote lang = zh-CN to totk.ini and the game showed Japanese: ov_language() handed the code to nn::oe::GetDesiredLanguage unchanged, and TotK only accepts zh-Hans and zh-Hant for Chinese. Reported in #72 and #68.

ov_language() now maps both old spellings, so existing totk.ini files keep working and cfg.c still accepts them. The F1 menu offers one row per language under the canonical codes. The installer lists only zh-Hans and zh-Hant, writes them, and reads zh-CN or zh-TW from the CLI or from a recorded totk-install.json as the canonical code, so a repair or update of an old install does not fail with check.lang_unknown and rewrites the ini. --probe-services prints the configured language and checks the four Chinese codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:27:19 +03:00
Valiantsin DzerakhandClaude Opus 5.5 112fb2dd74 Log the VK_EXT_device_fault report when the device is lost
vk_device_fault_report queries vkGetDeviceFaultInfoEXT once per process after VK_ERROR_DEVICE_LOST and logs the
description, the faulting addresses with type and precision, and the vendor codes. The submit, upload, occlusion
query and wait fatals call it before rt_fatal, and the present path calls it on a failed acquire, blit submit or
present. Threads that lose the device together wait until the report is written. The extension counts as usable only
when the driver reports the deviceFault feature. No cost unless the device is lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:21:14 +03:00
Valiantsin DzerakhandClaude Opus 5.5 30c6c13161 Merge task/linkcomma: link build folders that contain a comma
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:49:23 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4a961a8b8d Pass the linker map path with -Xlinker so build folders containing a comma link
The installer names its temp folder after the game title (Legend of Zelda, The - ...), and clang splits -Wl, values at every comma, so lld received a truncated --Map path and a stray input file. Fixes issue 47.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:49:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 31c8caa45e Merge task/genmem: size the code generator's worker pool by free memory
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:46:33 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a5711539e1 Size the code generator's worker pool by free memory so it stops running out of RAM
gen.py started one emit worker per CPU, about 1.3 GiB each on 1.4.3, so many-core machines with modest RAM died with a MemoryError during "Translating code to C" (issues 17, 28, probably 66). The default is now min(CPU count, 80% of the free physical memory / 1.5 GiB), at least 1, capped by the memory limit of the enclosing job object if there is one. --jobs still overrides it, and the chosen count and its reason are logged. The memory helper is tools/hostmem.py, shipped with the release tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:43:50 +03:00
Valiantsin Dzerakh c3b9a4e7f6 Update README.md 2026-10-29 18:05:56 +03:00
41 changed files with 800 additions and 93 deletions

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- Native code, not emulation
- Game updates 1.2.1 and 1.4.3, with saves that carry over between them
- 60 fps by default; up to 360 fps or the monitor's refresh rate (experimental)
- 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
- In-game settings menu (F1) and performance overlay (F10)
- Keyboard, mouse and gamepad with remapping, rumble and gyro
+1 -1
View File
@@ -1 +1 @@
0.1.1
0.1.6
+3 -2
View File
@@ -266,8 +266,9 @@ class App:
state = form.installed_state()
if state is not None:
self.update_hint.configure(text=self.cat('ui.update_mode'))
if state.get('lang') in ini.LANGUAGES and not self.lang_touched:
self.vars['lang'].set(self.lang_label(state['lang']))
recorded = ini.canonical_language(state.get('lang'))
if recorded in ini.LANGUAGES and not self.lang_touched:
self.vars['lang'].set(self.lang_label(recorded))
else:
self.update_hint.configure(text='')
self.fit()
+12 -4
View File
@@ -9,19 +9,27 @@ import os
import re
from pathlib import Path
# runtime/ov_system.c: the codes TOTK_LANG accepts.
# runtime/ov_system.c: the codes TOTK_LANG offers. Each language appears once, under the code the game accepts.
LANGUAGES = ('ja', 'en-US', 'en-GB', 'fr', 'fr-CA', 'de', 'it', 'es', 'es-419', 'nl', 'pt', 'pt-BR', 'ru', 'ko',
'zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant')
'zh-Hans', 'zh-Hant')
# Older installer builds wrote these two codes; they are read as the canonical ones and rewritten on the next install.
ALIASES = {'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant'}
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the installer does not extract.
NEED_SHARED_FONTS = ('ko', 'zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant')
NEED_SHARED_FONTS = ('ko', 'zh-Hans', 'zh-Hant')
ACTIVE = re.compile(r'^(\s*)(?:totk_)?lang(\s*=\s*)(.*?)(\s*)$', re.IGNORECASE)
COMMENTED = re.compile(r'^\s*[#;]\s*(?:totk_)?lang\s*=.*$', re.IGNORECASE)
SECTION = re.compile(r'^\s*\[(.*)\]\s*$')
def canonical_language(lang):
"""The code LANGUAGES lists for lang: an alias of an older installer build is mapped, anything else is kept."""
return ALIASES.get(lang, lang)
def set_language(text, lang):
"""text with `lang = <lang>`; see the module docstring."""
"""text with `lang = <lang>` (an alias written as its canonical code); see the module docstring."""
lang = canonical_language(lang)
lines = text.splitlines()
for i, line in enumerate(lines):
m = ACTIVE.match(line)
+1 -1
View File
@@ -203,7 +203,7 @@ class Check(Step):
raise InstallError('check.dest_not_folder', path=str(dest))
if ctx.mode == 'install' and dest.is_dir() and any(dest.iterdir()):
raise InstallError('check.dest_not_empty', path=str(dest))
lang = ctx.options.lang or ctx.state.get('lang')
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
if not lang:
raise InstallError('check.lang_missing')
if lang not in ini.LANGUAGES:
-2
View File
@@ -166,8 +166,6 @@
"lang.pt-BR": "Portuguese (Brazil)",
"lang.ru": "Russian",
"lang.ko": "Korean",
"lang.zh-CN": "Chinese (Simplified, China)",
"lang.zh-TW": "Chinese (Traditional, Taiwan)",
"lang.zh-Hans": "Chinese (Simplified)",
"lang.zh-Hant": "Chinese (Traditional)"
}
-2
View File
@@ -166,8 +166,6 @@
"lang.pt-BR": "Португальский (Бразилия)",
"lang.ru": "Русский",
"lang.ko": "Корейский",
"lang.zh-CN": "Китайский (упрощённый, КНР)",
"lang.zh-TW": "Китайский (традиционный, Тайвань)",
"lang.zh-Hans": "Китайский (упрощённый)",
"lang.zh-Hant": "Китайский (традиционный)"
}
+22 -2
View File
@@ -39,6 +39,7 @@ sys.path.insert(0, str(ROOT / 'tools'))
sys.path.insert(0, str(ROOT / 'recomp'))
import flags # noqa: E402
import hostmem # noqa: E402
import jumptables # noqa: E402
import patches # noqa: E402
import regs # noqa: E402
@@ -587,12 +588,29 @@ def remove_stale(moddir, name, nchunks, errors):
return removed
# Peak memory of one emit worker on 1.4.3 is 1.3 GiB (1.5 GiB with margin); the parent holds about 0.7 GiB.
WORKER_MEM = 1536 << 20
MEM_SHARE = 0.8 # of the free memory the workers may take, the rest is for the system and other programs
def pick_jobs(requested):
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
if requested:
return requested, '--jobs'
cpus = os.cpu_count() or 4
free = hostmem.available_ram()
if free is None:
return cpus, f'{cpus} CPUs, free memory unknown'
by_mem = max(1, int(free * MEM_SHARE) // WORKER_MEM)
return min(cpus, by_mem), f'{cpus} CPUs, {free / 2**30:.1f} GiB free memory allows {by_mem}'
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--exefs', type=Path, required=True)
ap.add_argument('--out', type=Path, required=True)
ap.add_argument('--modules', default='main,subsdk0,sdk')
ap.add_argument('--jobs', type=int, default=os.cpu_count() or 4)
ap.add_argument('--jobs', type=int, help='emit workers (default: by free memory and CPU count)')
ap.add_argument('--limit', type=int, default=0, help='emit only the first N functions of each module')
ap.add_argument('--chunk-fns', type=int, default=3000, help='functions per chunk at most')
ap.add_argument('--chunk-bytes', type=int, default=3000000, help='estimated bytes of C per chunk')
@@ -651,7 +669,9 @@ def main():
done_at, errors = {}, []
t_emit = time.perf_counter()
ctx = mp.get_context('spawn')
with ctx.Pool(max(1, args.jobs), initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
jobs, why = pick_jobs(args.jobs)
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
for i, r in enumerate(pool.imap_unordered(_emit_chunk, [t for _, t in tasks], chunksize=1)):
results[r['module']].append(r)
if r['error']:
+12
View File
@@ -47,6 +47,18 @@ site cloth main 28bc98 9400546a @page:x8 bd400104 1e240842 1e240863 9400546a #
# s1 is set again by the fmov two instructions on.
site bounce main 562a64 bd402920 bd402920 bd403921 1e210800 # ldr s0, [x9, #0x28]; ldr s1, [x9, #0x38]; fmul s0, s0, s1
# Fades (MacNativeGames game_patches_v143.c patch_fade_increment, upstream 3e9e7f67): the distance/LOD mesh fades and
# the pre-actor fades add trunc(2.1 * ratio) to a counter each frame: 2 at 30 fps, 1 at 60, 0 above about 63.5 fps
# (ratio below 1 / 2.1), where a fade stops and its object stays translucent or invisible until a slower frame moves
# it on. Each site floors the increment at 1 without the flags: clz is 32 only for 0, so increment += clz >> 5. The
# scratch register is one the next instruction writes (w10 at 0x0a7ac4: ldrb w10; w11 at the pre-actor sites: add
# w11), or at 0x95dd24 a temporary the function no longer reads (add, strb, return).
set fade TOTK_HIGHFPS_FIX 1
site fade main 0a7ac4 1e380008 1e380008 5ac0110a 53057d4a 0b0a0108 # fcvtzs w8, s0; clz w10, w8; lsr w10, w10, #5; add w8, w8, w10
site fade main 95dd24 1e380008 1e380008 5ac0110a 53057d4a 0b0a0108 # the same, second mesh fade
site fade main 6155e4 1e38000a 1e38000a 5ac0114b 53057d6b 0b0b014a # fcvtzs w10, s0; clz w11, w10; lsr w11, w11, #5; add w10, w10, w11
site fade main 615660 1e38000a 1e38000a 5ac0114b 53057d6b 0b0b014a # the same, second pre-actor fade path
# Output size (upstream game_patches_v143.c patch_output_res; runtime/resolution.c switches the set on). The swapchain
# size nnMain writes comes from page +48; the display configuration's canvas (+0x10) and active output (+0x508) from
# page +32, and the configuration goes to page +40. x9 is dead at the publication point.
+8 -6
View File
@@ -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", "SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"SWKBD_TEST", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "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",
"WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
"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).
@@ -74,11 +74,12 @@ static const struct { const char *key, *section; } sections[] = {
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
{"TEX_ALIAS", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "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"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "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,7 +95,7 @@ static const CfgMeta metas[] = {
{"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},
// Game
{"FPS_LIMIT", "60", "30|60|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
{"FPS_LIMIT", "60", "30|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},
@@ -111,10 +112,10 @@ static const CfgMeta metas[] = {
// shaders and pipelines
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART},
// textures and memory
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
// render fixes
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
@@ -126,6 +127,7 @@ static const CfgMeta metas[] = {
{"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},
{"INPUT_RECORD", "", NULL, RESTART},
};
+33
View File
@@ -0,0 +1,33 @@
// --probe-dpi: the process DPI awareness that totk.manifest declares, and what the primary monitor reports under it.
// A DPI-unaware process gets every window and monitor size scaled down by the display scale and its output stretched
// back up by Windows; the probe fails unless the process is per-monitor aware (v2). Needs no game files and no GPU.
#include <stdio.h>
#include <windows.h>
#include "host.h"
static const char *awareness_name(DPI_AWARENESS_CONTEXT ctx) {
if (AreDpiAwarenessContextsEqual(ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) return "per-monitor aware v2";
switch (GetAwarenessFromDpiAwarenessContext(ctx)) {
case DPI_AWARENESS_UNAWARE: return "unaware";
case DPI_AWARENESS_SYSTEM_AWARE: return "system aware";
case DPI_AWARENESS_PER_MONITOR_AWARE: return "per-monitor aware v1";
default: return "invalid";
}
}
int probe_dpi(void) {
DPI_AWARENESS_CONTEXT ctx = GetThreadDpiAwarenessContext();
MONITORINFO mi = {sizeof mi};
HMONITOR mon = MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
int have = GetMonitorInfoW(mon, &mi);
printf("dpi: awareness %s\n", awareness_name(ctx));
printf("dpi: system dpi %u (scale %u%%)\n", GetDpiForSystem(), GetDpiForSystem() * 100 / 96);
if (have)
printf("dpi: primary monitor %ldx%ld at %ld,%ld; screen metrics %dx%d\n", mi.rcMonitor.right - mi.rcMonitor.left,
mi.rcMonitor.bottom - mi.rcMonitor.top, mi.rcMonitor.left, mi.rcMonitor.top, GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
int ok = AreDpiAwarenessContextsEqual(ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) && have;
printf("probe-dpi: %s\n", ok ? "PASS" : "FAIL (the process is not per-monitor aware v2)");
return ok ? 0 : 1;
}
+1 -1
View File
@@ -90,7 +90,7 @@ void frame_step_init(void) {
int frame_step_realtime(void) { return step_word != NULL; }
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 120, 144, any whole rate from 30 to 360, or display (the
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 360, or display (the
// refresh rate of the window's monitor). Unset, the older TOTK_FPS60=0 still means 30. A limit of 30 switches the set
// fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum
// interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60
+79 -18
View File
@@ -12,6 +12,10 @@
// 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).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -48,6 +52,7 @@ 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
// ---- swapchain and present state (g_pl)
static CRITICAL_SECTION g_pl;
@@ -189,6 +194,18 @@ static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window b
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) {
@@ -203,6 +220,16 @@ static void cfg_window_size(int *w, int *h) {
*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.
@@ -242,20 +269,18 @@ 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;
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
RECT r = {0, 0, cw, ch};
AdjustWindowRect(&r, WND_STYLE, FALSE);
outer_size(h, cw, ch, &ow, &oh);
if (is_fullscreen(h)) {
RECT *n = &g_saved_place.rcNormalPosition;
n->right = n->left + (r.right - r.left);
n->bottom = n->top + (r.bottom - r.top);
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, r.right - r.left, r.bottom - r.top,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
}
@@ -372,6 +397,7 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
}
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;
@@ -381,6 +407,32 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
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);
@@ -417,23 +469,25 @@ static DWORD WINAPI window_main(void *arg) {
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
RegisterClassExW(&wc);
int cw, ch;
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
RECT r = {0, 0, cw, ch};
AdjustWindowRect(&r, WND_STYLE, FALSE);
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,
r.right - r.left, r.bottom - r.top, NULL, NULL, inst, NULL);
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 {
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
// 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);
@@ -441,6 +495,7 @@ static DWORD WINAPI window_main(void *arg) {
}
note_applied();
refresh_rate_update(g_hwnd, 1);
keep_awake(1);
}
SetEvent(g_wnd_ready);
if (g_wnd_failed) return 1;
@@ -490,6 +545,7 @@ static void flip_locked(void) {
vk_queue_lock();
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);
}
@@ -763,6 +819,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
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);
vkResetFences(c->device, 1, &g_fence[slot]);
@@ -792,6 +849,8 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(cb, &bi);
uint64_t crumb = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
vk_crumb(cb, VK_CRUMB_PRESENT, crumb);
VkImage dst = g_sc_img[idx];
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);
@@ -852,6 +911,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
record_thumb(cb, dst, slot);
}
sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, last_stage, last_write, VK_PIPELINE_STAGE_2_NONE, 0);
vk_crumb(cb, VK_CRUMB_PRESENT_END, crumb);
vkEndCommandBuffer(cb);
VkSemaphoreSubmitInfo wait = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = g_acquire[slot],
@@ -870,6 +930,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
g_flip_idx = idx;
if (!defer) flip_locked();
} else {
vk_device_fault_report(r);
rt_log("gfx: present submit failed (%d)", (int)r);
}
g_frame++;
@@ -901,9 +962,9 @@ void vk_present_flip(void) {
void gfx_window_resize(int cw, int chh) {
gfx_window_start();
RECT r = {0, 0, cw, chh};
AdjustWindowRect(&r, WND_STYLE, FALSE);
SetWindowPos(g_hwnd, NULL, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
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);
create_swapchain();
LeaveCriticalSection(&g_pl);
@@ -979,8 +1040,8 @@ void gfx_window_start(void) {
LeaveCriticalSection(&g_pl);
RECT rc = {0};
GetClientRect(g_hwnd, &rc);
rt_log("gfx: window %ldx%ld, swapchain %ux%u format %d", rc.right, rc.bottom, g_sc_ext.width, g_sc_ext.height,
(int)g_sc_fmt);
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);
}
LeaveCriticalSection(&g_start_lock);
vsync_ensure();
+1
View File
@@ -281,6 +281,7 @@ 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_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
+3 -1
View File
@@ -4,6 +4,7 @@
// --selftest layout, dispatch table and host-function registry; needs no game files
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
// --probe-gfx-abi NVN object layouts against nvn.h's reserved sizes; needs no game files
// --probe-nvn, --probe-nvn-tex, --probe-window, --probe-shader, --probe-vkmem, --probe-vkexec graphics probes
// --probe-astc the compute ASTC decoder against astc_decode_ldr; needs no game files
@@ -472,7 +473,7 @@ static int selftest(void) {
static int usage(const char *argv0) {
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
"--probe-cfg | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-cfg | --probe-dpi | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
"--probe-vkexec | --probe-astc | --probe-movie | --prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
return 2;
@@ -559,6 +560,7 @@ 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();
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
if (!strcmp(mode, "--probe-services")) return probe_services();
+3 -1
View File
@@ -250,8 +250,10 @@ static void probe_oe(void) {
{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"},
{NULL, "zh-CN", "zh-Hans"}, {NULL, "en-GB", "en-GB"}, {NULL, "ru", "ru"}, {"ja", "fr-FR", "ja"},
{"en-US", NULL, "en-US"}};
{"en-US", NULL, "en-US"}, {"zh-CN", NULL, "zh-Hans"}, {"zh-TW", NULL, "zh-Hant"},
{"zh-Hans", NULL, "zh-Hans"}, {"zh-Hant", NULL, "zh-Hant"}};
int bad = 0;
printf("oe: configured language (TOTK_LANG=%s) is %s\n", keep_lang ? keep_lang : "", lang());
for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
set_env("TOTK_LANG", cases[i].lang);
set_env("TOTK_LOCALE", cases[i].locale);
+7 -3
View File
@@ -88,12 +88,16 @@ static const char *switch_language(const char *tag) {
return "en-US";
}
// The game's language as a Switch language code (TOTK_LANG, else TOTK_LOCALE, else the Windows user locale).
// The game's language as a Switch language code (TOTK_LANG, else TOTK_LOCALE, else the Windows user locale). TotK
// accepts "zh-Hans" and "zh-Hant" for Chinese and falls back to Japanese for "zh-CN" and "zh-TW", so those two
// spellings, which older totk.ini files and menus wrote, are mapped to them.
const char *ov_language(void) {
static const char *const codes[] = {"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"};
static const char *const codes[] = {"ja", "en-US", "fr", "de", "it", "es", "ko", "nl", "pt", "ru",
"en-GB", "fr-CA", "es-419", "zh-Hans", "zh-Hant", "pt-BR"};
const char *code = cfg_get("TOTK_LANG"); // no fixed default: TOTK_LOCALE or the Windows user locale decides when unset
if (code && *code) {
if (!strcmp(code, "zh-CN")) return "zh-Hans";
if (!strcmp(code, "zh-TW")) return "zh-Hant";
for (size_t i = 0; i < sizeof codes / sizeof *codes; i++)
if (!strcmp(code, codes[i])) return codes[i];
rt_fatal("TOTK_LANG '%s' is not a Switch language code", code);
+6 -3
View File
@@ -217,8 +217,12 @@ static int text_entry_run(const SwkbdRequest *req, uint16_t *out, size_t cap, si
Entry e = {.req = req, .armed = 0};
const char *test = cfg_get("TOTK_SWKBD_TEST");
if (test && *test) e.test = !strcmp(test, "cancel") ? -1 : 1;
// The process is per-monitor DPI aware: the font and the layout below are measured at the game window's DPI, the
// monitor the dialog opens on, not at the system DPI that plain SystemParametersInfo and AdjustWindowRectEx use.
HWND owner = gfx_window_handle();
UINT dpi = owner ? GetDpiForWindow(owner) : GetDpiForSystem();
NONCLIENTMETRICSW ncm = {.cbSize = sizeof ncm};
SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0);
SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0, dpi);
HFONT font = CreateFontIndirectW(&ncm.lfMessageFont);
HDC dc = GetDC(NULL);
HGDIOBJ old = SelectObject(dc, font);
@@ -230,10 +234,9 @@ static int text_entry_run(const SwkbdRequest *req, uint16_t *out, size_t cap, si
int y_prompt = pad, h_prompt = u * 5 / 2, y_edit = y_prompt + h_prompt + u / 4, h_edit = u * 9 / 5;
int y_hint = y_edit + h_edit + u / 2, y_btn = y_hint + u * 3 / 2, ch = y_btn + bh + pad;
HWND owner = gfx_window_handle();
RECT wr = {0, 0, cw + 2 * pad, ch};
DWORD style = WS_POPUP | WS_CAPTION | WS_SYSMENU, ex = WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT;
AdjustWindowRectEx(&wr, style, FALSE, ex);
AdjustWindowRectExForDpi(&wr, style, FALSE, ex, dpi);
int ww = wr.right - wr.left, wh = wr.bottom - wr.top, x = CW_USEDEFAULT, y = CW_USEDEFAULT;
RECT orc;
if (owner && GetWindowRect(owner, &orc)) {
+7 -1
View File
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Embedded by totk.rc. activeCodePage makes UTF-8 the process code page (Windows 10 1903 and later): the narrow file
functions (fopen, CreateFileA, GetModuleFileNameA) and getenv then carry UTF-8, the same encoding platform/win32.c
and fs_host.c convert with, so an install folder or user profile with a non-ASCII name works. -->
and fs_host.c convert with, so an install folder or user profile with a non-ASCII name works.
dpiAwareness PerMonitorV2 (dpiAware true/pm on Windows 10 before 1607): window, monitor and cursor sizes are physical
pixels and the swapchain matches the screen. A DPI-unaware process gets them divided by the display scale and its
image stretched back up by Windows, so 4K at 150% presented a 2560x1440 image, blurred. gfx_window.c follows a
change of the window's DPI (WM_GETDPISCALEDSIZE, WM_DPICHANGED). -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
@@ -13,6 +17,8 @@
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
+12 -5
View File
@@ -171,8 +171,13 @@ static ImGuiStyle g_base_style; // the menu's style at 1080 lines
static uint64_t g_last_ns;
static uint64_t g_cost_ns, g_cost_frames; // CPU time of layout and recording while open, logged per 600 frames
// Sizes are tuned for 1080 lines and follow the window's height; under 720 lines they stay at the 720 size.
static float menu_scale(uint32_t h) { return (h >= 720 ? h : 720) / 1080.0f; }
// Sizes are tuned for 1080 lines and follow the window's height. Under 720 lines at 100% display scale they stay at the
// 720 size; that floor grows with the window's DPI, so a small window on a 150% or 200% display (sizes are physical
// pixels) still gets text as large as the display scale asks for.
static float menu_scale(uint32_t h) {
float least = 720.0f * (float)gfx_window_dpi() / 96.0f;
return ((float)h >= least ? (float)h : least) / 1080.0f;
}
// SavageWindRecomp's colours: dark green panels, gold accents.
static void style_init(ImGuiStyle &s) {
@@ -458,7 +463,7 @@ static void row_game_lang(void) {
{"en-US", UI_MENU_LANGNAME_EN}, {"ja", UI_MENU_LANGNAME_JA}, {"fr", UI_MENU_LANGNAME_FR},
{"de", UI_MENU_LANGNAME_DE}, {"it", UI_MENU_LANGNAME_IT}, {"es", UI_MENU_LANGNAME_ES},
{"nl", UI_MENU_LANGNAME_NL}, {"ru", UI_MENU_LANGNAME_RU},
{"zh-CN", UI_MENU_LANGNAME_ZH_HANS}, {"zh-TW", UI_MENU_LANGNAME_ZH_HANT}, {"ko", UI_MENU_LANGNAME_KO},
{"zh-Hans", UI_MENU_LANGNAME_ZH_HANS}, {"zh-Hant", UI_MENU_LANGNAME_ZH_HANT}, {"ko", UI_MENU_LANGNAME_KO},
};
enum { N = sizeof langs / sizeof *langs + 1 };
const char *vals[N], *names[N];
@@ -476,8 +481,9 @@ static void row_game_lang(void) {
// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives.
static void row_fps_limit() {
static const char *const vals[] = {"30", "60", "120", "144", "display"};
static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_120, UI_MENU_FPS_144, UI_MENU_FPS_DISPLAY};
static const char *const vals[] = {"30", "60", "90", "120", "144", "display"};
static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_90, UI_MENU_FPS_120, UI_MENU_FPS_144,
UI_MENU_FPS_DISPLAY};
enum { N = sizeof vals / sizeof *vals };
const char *names[N];
for (int i = 0; i < N; i++) names[i] = menu_text(ids[i]);
@@ -663,6 +669,7 @@ static void tab_advanced(float scale) {
UI_MENU_MEMORY_VERIFY};
opt_choice("TOTK_MEMORY", UI_MENU_MEMORY, mem, 4, UI_MENU_MEMORY_DESC);
opt_toggle("TOTK_TEX_ALIAS", UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC);
opt_slider("TOTK_TEX_VRAM_PRESSURE", UI_MENU_TEX_VRAM_PRESSURE, 0, 100, UI_MENU_TEX_VRAM_PRESSURE_DESC, 1, "%.0f%%");
opt_group(UI_MENU_GROUP_FIXES);
opt_toggle("TOTK_LOD_GUARD", UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC);
+1
View File
@@ -59,6 +59,7 @@ void gfx_window_apply_settings(void); // TOTK_FULLSCREEN or TOTK_WINDOW_SIZE
void gfx_window_request_close(void); // as the window's close button: the process ends
int gfx_window_monitor_size(int *w, int *h); // pixels of the monitor the window is on; 0 when unknown
double gfx_window_refresh_hz(void); // its refresh rate (TOTK_FPS_LIMIT=display); 0 when unknown
unsigned gfx_window_dpi(void); // the window's DPI (96 at 100% display scale); any thread
// The present mode the swapchain got, as a TOTK_VK_PRESENT value ("fifo" when mailbox is missing); "" before the first
// swapchain. Any thread.
const char *gfx_window_present_mode(void);
+8 -6
View File
@@ -67,15 +67,15 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_FPS_LIMIT] = "Frame rate limit",
[UI_MENU_FPS_LIMIT_DESC] = "The most frames per second the game draws. Above 60 is experimental and needs Game speed "
"follows the clock.",
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
// the monitor's refresh rate; printf format with the rate in Hz when it is known
[UI_MENU_FPS_DISPLAY] = "Monitor rate", [UI_MENU_FPS_DISPLAY_HZ] = "Monitor rate (%.0f Hz)",
[UI_MENU_REALTIME] = "Game speed follows the clock",
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
[UI_MENU_CLOTH_REALTIME_DESC] = "Steps cloth and hair by real time as well.",
[UI_MENU_HIGHFPS_FIX] = "High frame rate physics fix",
[UI_MENU_HIGHFPS_FIX_DESC] = "Corrects physics that misbehave above 30 frames per second.",
[UI_MENU_HIGHFPS_FIX] = "High frame rate fixes",
[UI_MENU_HIGHFPS_FIX_DESC] = "Corrects physics that misbehave above 30 frames per second, and objects that stay see-through or invisible above 60.",
[UI_MENU_GAME_LANG] = "Game language", [UI_MENU_GAME_LANG_DESC] = "Language of the game's text and voices.",
[UI_MENU_GAME_LANG_AUTO] = "Automatic",
[UI_MENU_OPERATION_MODE] = "Console mode",
@@ -184,6 +184,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_AUTO] = "Automatic", [UI_MENU_MEMORY_IMPORT] = "Import", [UI_MENU_MEMORY_MIRROR] = "Mirror",
[UI_MENU_MEMORY_VERIFY] = "Verify",
[UI_MENU_TEX_ALIAS] = "Texture aliasing", [UI_MENU_TEX_ALIAS_DESC] = "A new texture created over a live texture's memory starts with that content instead of empty.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Texture memory limit", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Percent of video memory above which textures the game released are dropped (they come back from storage when used again). 0 keeps them.",
[UI_MENU_LOD_GUARD] = "LOD guard", [UI_MENU_LOD_GUARD_DESC] = "Prevents objects flashing in the wrong place or a flat far field for a frame, by waiting for the game to finish rewriting level-of-detail data before the GPU reads it.",
[UI_MENU_FIX_C0] = "Constant buffer fix",
[UI_MENU_FIX_C0_DESC] = "Fills the driver-reserved constant bank c0 (storage sizes, dispatch size, point size, base instance) instead of reading zeros.",
@@ -285,14 +286,14 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_FPS_LIMIT] = "Ограничение частоты кадров",
[UI_MENU_FPS_LIMIT_DESC] = "Наибольшее число кадров в секунду. Выше 60 — экспериментальный режим, нужна «Скорость "
"игры по часам».",
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
[UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)",
[UI_MENU_REALTIME] = "Скорость игры по часам",
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
[UI_MENU_CLOTH_REALTIME_DESC] = "Ткань и волосы тоже идут по реальному времени.",
[UI_MENU_HIGHFPS_FIX] = "Исправление физики на высоком FPS",
[UI_MENU_HIGHFPS_FIX_DESC] = "Исправляет физику, которая ломается при частоте выше 30 кадров в секунду.",
[UI_MENU_HIGHFPS_FIX] = "Исправления для высокого FPS",
[UI_MENU_HIGHFPS_FIX_DESC] = "Исправляет физику, которая ломается при частоте выше 30 кадров в секунду, и объекты, которые выше 60 остаются полупрозрачными или невидимыми.",
[UI_MENU_GAME_LANG] = "Язык игры", [UI_MENU_GAME_LANG_DESC] = "Язык текста и озвучки игры.",
[UI_MENU_GAME_LANG_AUTO] = "Автоматически",
[UI_MENU_OPERATION_MODE] = "Режим консоли",
@@ -405,6 +406,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_AUTO] = "Автоматически", [UI_MENU_MEMORY_IMPORT] = "Импорт", [UI_MENU_MEMORY_MIRROR] = "Зеркало",
[UI_MENU_MEMORY_VERIFY] = "Проверка",
[UI_MENU_TEX_ALIAS] = "Наследование содержимого текстур", [UI_MENU_TEX_ALIAS_DESC] = "Новая текстура поверх памяти живой текстуры получает её содержимое, а не пустую.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Предел памяти текстур", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Процент видеопамяти, выше которого освобождённые игрой текстуры удаляются (при повторном использовании загружаются снова). 0 — не удалять.",
[UI_MENU_LOD_GUARD] = "Защита LOD", [UI_MENU_LOD_GUARD_DESC] = "Не даёт объектам мелькать не на своём месте, а дальнему плану — на кадр становиться плоским: ждёт, пока игра допишет данные уровней детализации, и только потом их читает GPU.",
[UI_MENU_FIX_C0] = "Исправление константного буфера",
[UI_MENU_FIX_C0_DESC] = "Заполняет зарезервированный драйвером банк констант c0 (размеры хранилищ, размер запуска, размер точки, базовый экземпляр) вместо чтения нулей.",
+3 -1
View File
@@ -38,7 +38,8 @@ typedef enum {
UI_MENU_SHARPNESS, UI_MENU_SHARPNESS_DESC,
// Game
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_120, UI_MENU_FPS_144,
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_90, UI_MENU_FPS_120,
UI_MENU_FPS_144,
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ,
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
@@ -117,6 +118,7 @@ typedef enum {
UI_MENU_MEMORY, UI_MENU_MEMORY_DESC, UI_MENU_MEMORY_AUTO, UI_MENU_MEMORY_IMPORT, UI_MENU_MEMORY_MIRROR,
UI_MENU_MEMORY_VERIFY,
UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC,
UI_MENU_TEX_VRAM_PRESSURE, UI_MENU_TEX_VRAM_PRESSURE_DESC,
UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC,
UI_MENU_FIX_C0, UI_MENU_FIX_C0_DESC,
UI_MENU_FIX_ZERO, UI_MENU_FIX_ZERO_DESC,
+20 -1
View File
@@ -36,7 +36,7 @@ typedef struct VkCtx {
int validation; // layer active
int has_custom_border_color, has_eds, has_eds2, has_eds3, has_vertex_input_dynamic_state,
has_conditional_rendering, has_transform_feedback, has_provoking_vertex, has_device_fault, has_memory_budget,
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock,
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock, has_buffer_marker,
has_astc_ldr; // textureCompressionASTC_LDR (enabled when reported)
// Feedback loops (maxwell.md 1.9, vk_exec_draw.c feedback_prepare): a draw samples a target of its open pass that
// an earlier draw of the pass wrote. TOTK_FEEDBACK=0: VK_FEEDBACK_OFF (sampled in GENERAL without a barrier, the
@@ -56,8 +56,11 @@ typedef struct {
uint32_t sg_size; // requiredSubgroupSize on sg_pin_stages (32)
VkShaderStageFlags sg_pin_stages; // 0: no pin, stages run at the device's subgroupSize
int gpl; // VK_EXT_graphics_pipeline_library enabled with fast linking (TOTK_GPL=0: off)
int amd; // an AMD GPU (vendor 0x1002), any driver
int amd_sc; // AMD's Windows driver branch for RDNA 1 and 2 ("AMD proprietary shader compiler")
} VkCaps;
extern VkCaps vk_caps;
void vk_caps_driver(VkPhysicalDevice p); // first, before vkCreateDevice: vk_caps.amd, amd_sc
void vk_caps_features(VkPhysicalDeviceFeatures *f); // before vkCreateDevice (TOTK_VK_NO_FLOAT64 clears shaderFloat64)
// Before vkCreateDevice, with the driver's graphics pipeline library feature and properties: sets vk_caps.gpl,
// 1 = enable the extensions (VK_KHR_pipeline_library, VK_EXT_graphics_pipeline_library) and the feature.
@@ -69,6 +72,22 @@ VkCtx *vk_ctx(void); // NULL until vk_init() succeeded
int vk_init(void); // idempotent; 1 on success, 0 on failure (reason logged with rt_log)
void vk_shutdown(void);
int vk_selftest(void); // 0 on success
// Call with the result of a failed queue submit, present or wait before stopping on it: VK_ERROR_DEVICE_LOST logs
// what VK_EXT_device_fault reports (description, faulting addresses with their precision, vendor codes) once per
// process, after the GPU breadcrumbs (vk_crumbs_report). Any other result returns at once.
void vk_device_fault_report(VkResult r);
// ---- GPU breadcrumbs (vk_crumbs.c): with VK_AMD_buffer_marker, each crumb has the GPU write its code into host memory
// twice, when the command processor reaches it and when all work before it has finished. After a lost device the two
// codes bracket the work that hung. Without the extension every call returns at once.
enum { VK_CRUMB_SUBMIT = 1, VK_CRUMB_SUBMIT_END, VK_CRUMB_UPLOAD, VK_CRUMB_UPLOAD_END, VK_CRUMB_ASTC, VK_CRUMB_PRESENT,
VK_CRUMB_PRESENT_END };
void vk_crumbs_init(VkCtx *c); // after the allocator exists
void vk_crumbs_release(void);
// Records crumb kind with number n (a timeline value, a batch number; the low 28 bits are kept) into cb. Only the
// thread that records cb may call it: never into a replay part while the recorder thread owns it (vk_rec.h).
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n);
void vk_crumbs_report(void); // logs the two codes and what they name
// ---- window and present (gfx_window.c). Host threads only.
// vk_present_image: blits the crop rectangle {x, y, w, h} of src (NULL, or w/h <= 0: the whole w x h image), scaled
+2 -1
View File
@@ -104,7 +104,7 @@ static struct {
VmaAllocation tab_alloc;
int tab_uploaded;
} G = {.mode = -1};
static uint64_t n_dispatch, n_texels;
static uint64_t n_dispatch, n_texels, n_chunks; // n_chunks: the breadcrumb number of the next scratch chunk
// TOTK_ASTC_TIME=1 (or vk_astc_time_enable): timestamps around the dispatches of each scratch chunk (the decode
// alone) and around a whole vk_astc_record call (one texture upload or WriteTexels: copies and barriers included),
@@ -449,6 +449,7 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
bcp[i1 - i0] = (VkBufferCopy){rg[i1].src, in, region_in(&rg[i1], bw, bh)};
in += region_in(&rg[i1], bw, bh), out += region_out(&rg[i1]);
}
vk_crumb(cb, VK_CRUMB_ASTC, ++n_chunks);
if (chunks) // the previous chunk's dispatches and image copy are done with the scratch
mem_barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
+20 -4
View File
@@ -6,8 +6,8 @@
// - minStorageBufferOffsetAlignment above 4 (TOTK_VK_SSBO_ALIGN=<power of two> raises it): the translator computes
// storage offsets from the guest address rounded down to that alignment (Ryujinx's unaligned storage path), and
// vk_exec_draw.c ssbo_info binds the rounded-down address, so shader writes reach guest memory instead of a copy.
// - no graphics pipeline library with fast linking (TOTK_GPL=0 forces it): vk_caps.gpl = 0, the extension stays
// disabled, and pipelines are built whole.
// - no graphics pipeline library with fast linking (TOTK_GPL=0 forces it; the default on AMD's RDNA 1/2 driver):
// vk_caps.gpl = 0, the extension stays disabled, and pipelines are built whole.
// - no 32-lane pin (no subgroupSizeControl, 32 outside min..max, or TOTK_VK_SUBGROUP_PIN=0): stages run at the
// device's subgroupSize (SPIR-V 1.3 without a required size) and the translator is told that size.
// TOTK_VK_SUBGROUP=<n> pins n instead of 32 where the driver allows it.
@@ -36,11 +36,25 @@ void vk_caps_features(VkPhysicalDeviceFeatures *f) {
vk_caps.float64 = f->shaderFloat64;
}
void vk_caps_driver(VkPhysicalDevice p) {
VkPhysicalDeviceDriverProperties drv = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &drv};
vkGetPhysicalDeviceProperties2(p, &p2);
vk_caps.amd = p2.properties.vendorID == 0x1002;
// AMD split RDNA 1 and 2 off into a driver branch of their own; its driverInfo reads
// "26.10.1 (AMD proprietary shader compiler)" where the RDNA 3 and 4 driver names LLPC.
vk_caps.amd_sc = drv.driverID == VK_DRIVER_ID_AMD_PROPRIETARY && strstr(drv.driverInfo, "shader compiler");
}
// TOTK_GPL=0/1 forces graphics pipeline libraries off or on. Default off on AMD's RDNA 1/2 driver branch: every boot
// that lost the device there had a cold pipeline cache and draws waiting on fast-linked pipelines, and a run with GPL
// off booted (issue #5); Savage Wind Recomp turns GPL off on RDNA 4 under RADV for a GPU hang at the title screen.
int vk_caps_gpl(const VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT *f,
const VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT *p) {
gpl_lib = f->graphicsPipelineLibrary;
gpl_fast = p->graphicsPipelineLibraryFastLinking;
vk_caps.gpl = gpl_lib && gpl_fast && cfg_bool("TOTK_GPL");
const char *e = cfg_get("TOTK_GPL"); // no fixed default: it depends on the driver, see above
vk_caps.gpl = gpl_lib && gpl_fast && (e && *e ? strcmp(e, "0") != 0 : !vk_caps.amd_sc);
return vk_caps.gpl;
}
@@ -117,8 +131,10 @@ void vk_caps_init(VkCtx *c) {
has_ext(ep, ne, "VK_EXT_graphics_pipeline_library"), depth_ok(p, VK_FORMAT_D16_UNORM),
depth_ok(p, VK_FORMAT_D24_UNORM_S8_UINT), depth_ok(p, VK_FORMAT_X8_D24_UNORM_PACK32), depth_ok(p, VK_FORMAT_D32_SFLOAT),
depth_ok(p, VK_FORMAT_D32_SFLOAT_S8_UINT));
const char *ge = cfg_get("TOTK_GPL");
rt_log("vk caps: gpl %s fast-link %s (graphicsPipelineLibrary %u%s)", vk_caps.gpl ? "yes" : "no",
gpl_fast ? "yes" : "no", (unsigned)gpl_lib, gpl_lib && gpl_fast && !vk_caps.gpl ? ", off by TOTK_GPL=0" : "");
gpl_fast ? "yes" : "no", (unsigned)gpl_lib,
!gpl_lib || !gpl_fast || vk_caps.gpl ? "" : ge && *ge ? ", off by TOTK_GPL=0" : ", off on AMD's RDNA 1/2 driver");
rt_log("vk caps: fallbacks: float64 %s, storage buffers %s %u, subgroup pin %u on stages %#x, translator subgroup vs/fs/cs %u/%u/%u",
vk_caps.float64 ? "native" : "softfloat64", vk_caps.sbuf_align ? "rounded down to" : "exact, alignment",
(unsigned)l->minStorageBufferOffsetAlignment, want, (unsigned)vk_caps.sg_pin_stages, told[0], told[1], told[2]);
+67
View File
@@ -0,0 +1,67 @@
// GPU breadcrumbs for a lost device (vk.h vk_crumb). VK_AMD_buffer_marker writes a 32-bit code into a buffer from
// the command stream: at TOP_OF_PIPE once the command processor reaches the marker, at BOTTOM_OF_PIPE once every
// command before it has finished. Each crumb writes both, into two words of one host-coherent buffer, so after a
// device loss word 0 names the newest point the GPU reached and word 1 the newest point it had finished: the hang lies
// between them. A code is the crumb kind in the top 4 bits and its number in the low 28. The markers cost two small
// writes per crumb, and crumbs mark submissions, upload batches, ASTC chunks and presents, not draws.
#include <stdint.h>
#include <stdio.h>
#include "host.h"
#include "vk.h"
static PFN_vkCmdWriteBufferMarkerAMD write_marker;
static VkBuffer buf;
static VmaAllocation alloc;
static volatile uint32_t *words;
void vk_crumbs_init(VkCtx *c) {
if (!c->has_buffer_marker) return;
write_marker = (PFN_vkCmdWriteBufferMarkerAMD)vkGetDeviceProcAddr(c->device, "vkCmdWriteBufferMarkerAMD");
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 16,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
VmaAllocationInfo info;
if (!write_marker || vmaCreateBuffer(c->vma, &bi, &ai, &buf, &alloc, &info) != VK_SUCCESS) {
rt_log("vk: GPU breadcrumbs unavailable");
write_marker = NULL;
return;
}
words = info.pMappedData;
words[0] = words[1] = 0;
rt_log("vk: GPU breadcrumbs on (VK_AMD_buffer_marker)");
}
void vk_crumbs_release(void) {
VkCtx *c = vk_ctx();
if (buf && c) vmaDestroyBuffer(c->vma, buf, alloc);
buf = VK_NULL_HANDLE, alloc = NULL, words = NULL, write_marker = NULL;
}
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n) {
if (!write_marker || !cb) return;
uint32_t code = (uint32_t)kind << 28 | (uint32_t)(n & 0x0fffffff);
write_marker(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf, 0, code);
write_marker(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, buf, 4, code);
}
static void describe(uint32_t code, char *o, size_t size) {
static const char *const kinds[] = {"nothing", "the start of a part of submission", "the end of submission",
"the start of upload batch", "the end of upload batch",
"ASTC decode chunk", "the start of present",
"the end of present"};
unsigned k = code >> 28;
if (!code) snprintf(o, size, "nothing yet");
else if (k < sizeof kinds / sizeof *kinds) snprintf(o, size, "%s %u", kinds[k], code & 0x0fffffff);
else snprintf(o, size, "unknown code %#x", code);
}
void vk_crumbs_report(void) {
if (!words) return;
char reached[96], finished[96];
describe(words[0], reached, sizeof reached);
describe(words[1], finished, sizeof finished);
rt_log("vk: GPU breadcrumbs: the GPU last reached %s; everything before %s had finished", reached, finished);
}
+65
View File
@@ -233,6 +233,8 @@ static int create_device(void) {
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
}
if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }
// GPU breadcrumbs for a lost device (vk_crumbs.c): AMD's drivers and RADV report it.
if (has_ext(ep, ne, VK_AMD_BUFFER_MARKER_EXTENSION_NAME)) { g_vk.has_buffer_marker = 1; ext[n_ext++] = VK_AMD_BUFFER_MARKER_EXTENSION_NAME; }
// Graphics pipeline library: chained last so it can be dropped again when vk_caps_gpl declines it.
void **gpl_link = NULL;
if (has_ext(ep, ne, VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && has_ext(ep, ne, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME)) {
@@ -246,6 +248,7 @@ static int create_device(void) {
// The driver fills the chain with what it supports; the same chain then enables all of it (robustBufferAccess
// stays on: guest shaders index constant buffers freely and out-of-range reads must stay harmless).
vkGetPhysicalDeviceFeatures2(g_vk.phys, &c->f2);
vk_caps_driver(g_vk.phys);
vk_caps_features(&c->f2.features);
if (gpl_link) {
VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT gp = {
@@ -267,6 +270,7 @@ static int create_device(void) {
else if (!h->geometryShader) miss = "geometryShader";
else if (!h->textureCompressionBC) miss = "textureCompressionBC";
if (miss) { rt_log("vk: required feature missing on %s: %s", g_vk.props.deviceName, miss); free(c); return 0; }
if (!c->fault.deviceFault) g_vk.has_device_fault = 0; // vkGetDeviceFaultInfoEXT needs the feature, not just the extension
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
{
@@ -319,6 +323,7 @@ static int create_device(void) {
rt_log("vk: timeline semaphore creation failed (%d)", (int)r);
return 0;
}
vk_crumbs_init(&g_vk);
return 1;
}
@@ -328,6 +333,7 @@ static void destroy_all(void) {
gfx_window_release_vk();
if (g_vk.device) vkDeviceWaitIdle(g_vk.device);
if (g_vk.device) vk_astc_release();
if (g_vk.device) vk_crumbs_release();
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
vk_headroom_stop();
if (g_vk.vma) vmaDestroyAllocator(g_vk.vma);
@@ -364,6 +370,65 @@ void vk_shutdown(void) {
VkCtx *vk_ctx(void) { return g_ready ? &g_vk : NULL; }
static const char *fault_address_type(VkDeviceFaultAddressTypeEXT t) {
switch (t) {
case VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT: return "none";
case VK_DEVICE_FAULT_ADDRESS_TYPE_READ_INVALID_EXT: return "read invalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_EXT: return "write invalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_EXECUTE_INVALID_EXT: return "execute invalid";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_UNKNOWN_EXT: return "instruction pointer (unknown)";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_INVALID_EXT: return "instruction pointer (invalid)";
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_FAULT_EXT: return "instruction pointer (fault)";
default: return "unknown type";
}
}
// The query runs once per process. Threads that hit the loss at the same time wait on the lock until it is logged, so
// none of them reaches its rt_fatal and ends the process before the report is written.
void vk_device_fault_report(VkResult r) {
static SRWLOCK lock = SRWLOCK_INIT;
static int done;
if (r != VK_ERROR_DEVICE_LOST) return;
AcquireSRWLockExclusive(&lock);
if (done) { ReleaseSRWLockExclusive(&lock); return; }
done = 1;
vk_crumbs_report();
VkCtx *c = vk_ctx();
PFN_vkGetDeviceFaultInfoEXT get =
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
: NULL;
if (!get) {
rt_log("vk: device lost; no fault report (VK_EXT_device_fault %s)", c && c->has_device_fault ? "has no entry point"
: "not enabled");
ReleaseSRWLockExclusive(&lock);
return;
}
VkDeviceFaultCountsEXT n = {.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT};
VkResult q = get(c->device, &n, NULL);
VkDeviceFaultAddressInfoEXT *ai = calloc(n.addressInfoCount + 1, sizeof *ai);
VkDeviceFaultVendorInfoEXT *vi = calloc(n.vendorInfoCount + 1, sizeof *vi);
VkDeviceFaultInfoEXT info = {.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT, .pAddressInfos = ai, .pVendorInfos = vi};
uint64_t binary = n.vendorBinarySize;
n.vendorBinarySize = 0; // the vendor binary is not fetched; its size alone is logged
if (q == VK_SUCCESS && ai && vi) q = get(c->device, &n, &info);
if (q != VK_SUCCESS && q != VK_INCOMPLETE) {
rt_log("vk: device lost; vkGetDeviceFaultInfoEXT failed (%d)", (int)q);
} else {
rt_log("vk: device fault: \"%s\" (%u address infos, %u vendor infos, vendor binary %llu bytes)", info.description,
n.addressInfoCount, n.vendorInfoCount, (unsigned long long)binary);
// The faulting access lies in the precision-aligned range around the reported address.
for (uint32_t i = 0; i < n.addressInfoCount; i++)
rt_log("vk: device fault address %u: %s at %#llx, precision %#llx", i, fault_address_type(ai[i].addressType),
(unsigned long long)ai[i].reportedAddress, (unsigned long long)ai[i].addressPrecision);
for (uint32_t i = 0; i < n.vendorInfoCount; i++)
rt_log("vk: device fault vendor info %u: \"%s\" code %#llx data %#llx", i, vi[i].description,
(unsigned long long)vi[i].vendorFaultCode, (unsigned long long)vi[i].vendorFaultData);
}
free(ai);
free(vi);
ReleaseSRWLockExclusive(&lock);
}
int vk_selftest(void) {
if (!vk_init()) return 1;
VkCtx *c = &g_vk;
+182 -2
View File
@@ -366,10 +366,14 @@ static void dummies_init(void) {
Z.ready = 1;
}
// Releases the stand-ins. The device must be idle; the game path leaves them to process exit like the pipelines.
static void twins_release(VkCtx *c);
// Releases the stand-ins and the depth twins. The device must be idle; the game path leaves them to process exit like
// the pipelines.
void vk_exec_draw_shutdown(void) {
if (!Z.ready) return;
VkCtx *c = vk_ctx();
twins_release(c);
for (int i = 0; i < DV_COUNT; i++) {
if (Z.img[i].sview != Z.img[i].view) vkDestroyImageView(c->device, Z.img[i].sview, NULL);
vkDestroyImageView(c->device, Z.img[i].view, NULL);
@@ -1146,7 +1150,8 @@ static struct {
VkDescriptorImageInfo out;
} TD[TD_N];
static void tex_info(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate, VkDescriptorImageInfo *out) {
static void tex_info_lookup(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate,
VkDescriptorImageInfo *out) {
int td_on = td_mode();
VkImage image;
if (!h || storage || !td_on) return tex_info_full(ex, r, h, storage, separate, out, &image);
@@ -1207,6 +1212,177 @@ static void tex_info_full(VkExec *ex, const GfxShaderRes *r, uint64_t h, int sto
*out = (VkDescriptorImageInfo){separate || storage ? VK_NULL_HANDLE : samp, view, layout};
}
// ---- depth twins: colour images under a compare sampler in a compute shader
// TotK's compute passes sample an R32_SFLOAT texture through a shadow sampler. Maxwell compares any single-channel
// float or unorm texel; Vulkan only formats with SAMPLED_IMAGE_DEPTH_COMPARISON (VUID-vkCmdDispatch-None-06479), which
// AMD's drivers do not give R32_SFLOAT, so the result there is undefined. Such an image gets a depth twin of the same
// size and bits (R32_SFLOAT -> D32_SFLOAT, R16_UNORM -> D16_UNORM), copied through a buffer right before each dispatch
// that samples it (a dispatch is outside any render pass), and the dispatch samples the twin. Draws keep the colour
// image: a copy cannot go inside their pass. Design: Savage Wind Recomp runtime/nvn/vulkan/exec.c depth_twin_update
// (https://github.com/Revivedx/SavageWindRecomp, MIT); the code is ours. TOTK_DEPTH_TWIN=0: off.
#define TWINS 8
typedef struct { VkImage src, img; VmaAllocation mem; VkImageView view[2]; uint32_t w, h, layers; VkFormat fmt; } Twin;
static Twin twins[TWINS];
static unsigned twin_next;
static int twin_dispatch; // op_dispatch*: the resources being bound are a dispatch's
static VkBuffer twin_buf;
static VmaAllocation twin_buf_mem;
static VkDeviceSize twin_buf_cap;
static void twin_free(void *arg) { // completion thread, after the last submission that may sample it
Twin *t = arg;
VkCtx *c = vk_ctx();
for (int k = 0; k < 2; k++)
if (t->view[k]) vkDestroyImageView(c->device, t->view[k], NULL);
vmaDestroyImage(c->vma, t->img, t->mem);
free(t);
}
typedef struct { VkBuffer buf; VmaAllocation mem; } TwinBuf;
static void twin_buf_free(void *arg) { // completion thread, after the last submission that may copy through it
TwinBuf *b = arg;
vmaDestroyBuffer(vk_ctx()->vma, b->buf, b->mem);
free(b);
}
static VkFormat twin_format(const VkTexImage *img) {
static int on = -1;
if (on < 0) on = rt_knob("DEPTH_TWIN", "1");
if (!on || (img->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || img->samples > 1 || img->depth > 1) return VK_FORMAT_UNDEFINED;
VkFormat df = img->format == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT
: img->format == VK_FORMAT_R16_UNORM ? VK_FORMAT_D16_UNORM : VK_FORMAT_UNDEFINED;
if (!df) return df;
static int compares[2] = {-1, -1}; // per source format: the driver compares it as it is
int i = df == VK_FORMAT_D16_UNORM;
if (compares[i] < 0) {
VkFormatProperties3 p3 = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3};
VkFormatProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2, .pNext = &p3};
vkGetPhysicalDeviceFormatProperties2(vk_ctx()->phys, img->format, &p2);
compares[i] = (p3.optimalTilingFeatures & VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_DEPTH_COMPARISON_BIT) != 0;
rt_log("vk_exec: %s under a compare sampler: %s", i ? "R16_UNORM" : "R32_SFLOAT",
compares[i] ? "compared by the driver" : "sampled through a depth twin in compute passes");
}
return compares[i] ? VK_FORMAT_UNDEFINED : df;
}
static void twins_release(VkCtx *c) {
for (int i = 0; i < TWINS; i++) {
for (int k = 0; k < 2; k++)
if (twins[i].view[k]) vkDestroyImageView(c->device, twins[i].view[k], NULL);
if (twins[i].img) vmaDestroyImage(c->vma, twins[i].img, twins[i].mem);
}
memset(twins, 0, sizeof twins);
if (twin_buf) vmaDestroyBuffer(c->vma, twin_buf, twin_buf_mem);
twin_buf = VK_NULL_HANDLE, twin_buf_mem = NULL, twin_buf_cap = 0;
}
// The twin of img holding img's current texels, the copies recorded into ex->cb; NULL when img needs none or it cannot
// be made.
static Twin *twin_update(VkExec *ex, const VkTexImage *img) {
VkFormat df = twin_format(img);
if (!df) return NULL;
VkCtx *c = vk_ctx();
uint32_t bpp = df == VK_FORMAT_D32_SFLOAT ? 4 : 2, layers = img->layers ? img->layers : 1;
VkDeviceSize bytes = (VkDeviceSize)img->width * img->height * layers * bpp;
if (bytes > (64u << 20)) return NULL;
if (bytes > twin_buf_cap) { // one copy buffer, grown; the old one goes after the submission being recorded
VkDeviceSize want = twin_buf_cap ? twin_buf_cap * 2 : 1u << 16;
while (want < bytes) want *= 2;
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = want,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
VkBuffer nb;
VmaAllocation nm;
if (vmaCreateBuffer(c->vma, &bi, &ai, &nb, &nm, NULL) != VK_SUCCESS) return NULL;
if (twin_buf) {
TwinBuf *old = malloc(sizeof *old);
if (!old) rt_fatal("vk: out of memory (depth twin)");
*old = (TwinBuf){twin_buf, twin_buf_mem};
vk_completion_add(vk_sync_issued() + 1, twin_buf_free, old);
}
twin_buf = nb, twin_buf_mem = nm, twin_buf_cap = want;
}
Twin *t = NULL;
for (int i = 0; i < TWINS && !t; i++)
if (twins[i].img && twins[i].src == img->image && twins[i].w == img->width && twins[i].h == img->height &&
twins[i].layers == layers && twins[i].fmt == df)
t = &twins[i];
if (!t) {
t = &twins[twin_next++ % TWINS];
if (t->img) { // the submission being recorded may still sample the replaced twin
Twin *old = malloc(sizeof *old);
if (!old) rt_fatal("vk: out of memory (depth twin)");
*old = *t;
vk_completion_add(vk_sync_issued() + 1, twin_free, old);
}
*t = (Twin){.src = img->image, .w = img->width, .h = img->height, .layers = layers, .fmt = df};
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = df,
.extent = {img->width, img->height, 1}, .mipLevels = 1, .arrayLayers = layers,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ci, &ai, &t->img, &t->mem, NULL) != VK_SUCCESS) {
*t = (Twin){0};
return NULL;
}
for (int k = 0; k < 2; k++) {
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = t->img,
.viewType = k ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, .format = df,
.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, k ? layers : 1}};
if (vkCreateImageView(c->device, &vi, NULL, &t->view[k]) != VK_SUCCESS) t->view[k] = VK_NULL_HANDLE;
}
static int nlog;
if (nlog++ < 8)
rt_log("vk_exec: depth twin %ux%ux%u %s for a compute pass's compare sampler", img->width, img->height,
layers, df == VK_FORMAT_D32_SFLOAT ? "D32_SFLOAT" : "D16_UNORM");
}
// The source's earlier writes and the twin's earlier readers before the copies; the twin's old texels are dropped.
VkMemoryBarrier2 m0 = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT};
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = t->img, .subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, layers}};
VkDependencyInfo d0 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m0,
.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
vkCmdPipelineBarrier2(ex->cb, &d0);
VkBufferImageCopy src = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layers},
.imageExtent = {img->width, img->height, 1}};
vkCmdCopyImageToBuffer(ex->cb, img->image, VK_IMAGE_LAYOUT_GENERAL, twin_buf, 1, &src);
VkMemoryBarrier2 m1 = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT};
VkDependencyInfo d1 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m1};
vkCmdPipelineBarrier2(ex->cb, &d1);
VkBufferImageCopy dst = {.imageSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, layers},
.imageExtent = {img->width, img->height, 1}};
vkCmdCopyBufferToImage(ex->cb, twin_buf, t->img, VK_IMAGE_LAYOUT_GENERAL, 1, &dst);
// The twin's texels before the dispatch; the source's next writers and the buffer's next copy after these copies.
VkMemoryBarrier2 m2 = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
VkDependencyInfo d2 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m2};
vkCmdPipelineBarrier2(ex->cb, &d2);
ex->recorded += 5;
return t;
}
// The image descriptor of slot h; in a dispatch, a compare-sampled colour image the driver cannot compare is swapped
// for its depth twin.
static void tex_info(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate, VkDescriptorImageInfo *out) {
tex_info_lookup(ex, r, h, storage, separate, out);
if (!twin_dispatch || storage || separate || !h || !(r->type & 0x400)) return;
VkImageViewType vt = res_view_type(r->type);
if (vt != VK_IMAGE_VIEW_TYPE_2D && vt != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return;
const VkTexImage *img = vk_texture_by_id_locked((int)(h & 0xfffff), NULL, NULL);
Twin *t = img ? twin_update(ex, img) : NULL;
VkImageView v = t ? t->view[vt == VK_IMAGE_VIEW_TYPE_2D_ARRAY] : VK_NULL_HANDLE;
if (v) out->imageView = v, out->imageLayout = VK_IMAGE_LAYOUT_GENERAL;
}
static VkDescriptorSet alloc_set(VkExec *ex, VkDescriptorSetLayout layout) {
CbRes *r = cb_res(ex);
VkCtx *c = vk_ctx();
@@ -2076,7 +2252,9 @@ static void op_dispatch(VkExec *ex, const p_dispatch *d) {
vk_exec_barrier(ex); // ends the pass; earlier writes are visible to the shader
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_COMPUTE, p);
c0_dim[0] = (uint32_t)d->x, c0_dim[1] = (uint32_t)d->y, c0_dim[2] = (uint32_t)d->z;
twin_dispatch = 1;
bind_resources(ex, info);
twin_dispatch = 0;
vkCmdDispatch(ex->cb, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
ex->recorded++;
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, d);
@@ -2102,7 +2280,9 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_COMPUTE, p);
c0_dim[0] = c0_dim[1] = c0_dim[2] = 0;
B.c0_valid[2] = 0; // a fresh c0 range: the copy below must not change one an earlier dispatch reads
twin_dispatch = 1;
bind_resources(ex, info);
twin_dispatch = 0;
if (B.c0_valid[2]) { // the shader binds c0
VkBufferCopy cp = {ip.offset, B.c0_off[2] + 0x200, sizeof(VkDispatchIndirectCommand)};
vkCmdCopyBuffer(ex->cb, ip.buffer, B.c0_buf[2], 1, &cp);
+4 -1
View File
@@ -211,7 +211,10 @@ static void oq_cb(void *arg) {
// completion thread, and with it every later callback, for good. It counts as visible (one sample).
VkResult r = vkGetQueryPoolResults(dev, oq_pool, s->q[i], j - i, sizeof v, v, 2 * sizeof v[0],
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (r != VK_SUCCESS && r != VK_NOT_READY) rt_fatal("vk: occlusion query results failed (%d)", (int)r);
if (r != VK_SUCCESS && r != VK_NOT_READY) {
vk_device_fault_report(r);
rt_fatal("vk: occlusion query results failed (%d)", (int)r);
}
vkResetQueryPool(dev, oq_pool, s->q[i], j - i);
for (uint32_t k = i; k < j; k++) {
if (v[2 * (k - i) + 1]) sum += v[2 * (k - i)];
+5 -1
View File
@@ -99,7 +99,11 @@ static void free_images(VkCtx *c) {
static int ensure_images(VkCtx *c, uint32_t w, uint32_t h) {
if (F.img[0] && F.w == w && F.h == h) return 1;
vkDeviceWaitIdle(c->device); // a swapchain size change: earlier presents may still read the old images
// A swapchain size change: earlier presents may still read the old images. vkDeviceWaitIdle needs every queue
// externally synchronized, so it holds the queue lock like a submission.
vk_queue_lock();
vkDeviceWaitIdle(c->device);
vk_queue_unlock();
free_images(c);
for (int i = 0; i < 2; i++) {
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
+6 -1
View File
@@ -1657,6 +1657,7 @@ VkCommandBuffer vk_upload_begin(void) {
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(open_b.cb, &bi);
open_b.seq = ++next_seq;
vk_crumb(open_b.cb, VK_CRUMB_UPLOAD, open_b.seq);
open_active = 1;
return open_b.cb;
}
@@ -1717,12 +1718,16 @@ uint64_t vk_upload_flush(int wait) {
uint64_t seq = next_seq;
if (c && open_active) {
for (int i = 0; i < open_b.nstg; i++) vmaFlushAllocation(c->vma, open_b.stg[i].alloc, 0, VK_WHOLE_SIZE);
vk_crumb(open_b.cb, VK_CRUMB_UPLOAD_END, open_b.seq);
vkEndCommandBuffer(open_b.cb);
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &open_b.cb};
vk_queue_lock();
VkResult r = vkQueueSubmit(c->gfx_queue, 1, &si, open_b.fence);
vk_queue_unlock();
if (r != VK_SUCCESS) rt_fatal("vk: upload submit failed (%d)", (int)r);
if (r != VK_SUCCESS) {
vk_device_fault_report(r);
rt_fatal("vk: upload submit failed (%d)", (int)r);
}
if (ninflight == cap_inflight) grow((void **)&inflight, &cap_inflight, sizeof *inflight);
inflight[ninflight++] = open_b;
open_active = 0;
+8 -1
View File
@@ -12,6 +12,7 @@
#include <string.h>
#include <windows.h>
#include "cfg.h"
#include "host.h"
#include "vk.h"
@@ -128,7 +129,13 @@ void rec_drain(void) {
void rec_begin(VkCommandBuffer cb) {
static int mode = -1;
if (mode < 0) {
mode = rt_knob("VK_RECORDER", "1");
// Default off on AMD GPUs: with the recorder their drivers lost the device within the first frames or at menu
// opens, 1 boot in 3 on an RX 9070 XT, and TOTK_VK_RECORDER=0 ended it for three users (issue #5); under RADV
// the kernel saw an illegal opcode at the end of a command buffer (issue #40). The cause is not found.
const char *set = cfg_get("TOTK_VK_RECORDER");
mode = rt_knob("VK_RECORDER", vk_caps.amd ? "0" : "1");
if (!mode && vk_caps.amd && !(set && *set))
rt_log("vk_rec: no recorder thread on AMD GPUs (TOTK_VK_RECORDER=1 turns it on)");
if (mode) {
wake_ev = CreateEventA(NULL, FALSE, FALSE, NULL);
rec_thread = wake_ev ? CreateThread(NULL, 0, rec_main, NULL, 0, NULL) : NULL;
+15 -3
View File
@@ -186,7 +186,10 @@ static DWORD WINAPI completion_main(void *arg) {
VkSemaphoreWaitInfo wi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1,
.pSemaphores = &c->timeline, .pValues = &want};
VkResult r = vkWaitSemaphores(c->device, &wi, 100000000ull); // 100 ms: stop is checked between waits
if (r != VK_SUCCESS && r != VK_TIMEOUT) rt_fatal("vk: vkWaitSemaphores failed (%d; device lost?)", (int)r);
if (r != VK_SUCCESS && r != VK_TIMEOUT) {
vk_device_fault_report(r);
rt_fatal("vk: vkWaitSemaphores failed (%d; device lost?)", (int)r);
}
vkGetSemaphoreCounterValue(c->device, c->timeline, &reached);
AcquireSRWLockExclusive(&st_lock);
if (reached > issued) reached = issued;
@@ -302,12 +305,16 @@ VkCommandBuffer vk_sync_split(void) {
vk_queue_lock();
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
if (r != VK_SUCCESS) rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
if (r != VK_SUCCESS) {
vk_device_fault_report(r);
rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
}
vk_mem_note_submit(cur_fence); // objects destroyed from here on may be in use by the part just submitted
CmdSlot *s = take_slot(c, cur_fence); // the submitted part keeps its slot until the value is reached
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
ts_begin(s->cb);
rec_begin(s->cb);
return s->cb;
@@ -336,11 +343,13 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
uint64_t t0 = perf_begin();
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
ts_begin(s->cb);
rec_begin(s->cb);
int recorded = vk_exec_record(s->cb, handles, n, fence);
s = &slots[cur_slot]; // the last part (vk_sync_split)
rec_end();
vk_crumb(s->cb, VK_CRUMB_SUBMIT_END, fence); // the recorder is done with the part
if (recorded) ts_end(s->cb, fence);
else ts_drop();
vkEndCommandBuffer(s->cb);
@@ -360,7 +369,10 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
vk_queue_lock();
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
if (r != VK_SUCCESS) rt_fatal("vk: vkQueueSubmit2 of value %llu failed (%d)", (unsigned long long)fence, (int)r);
if (r != VK_SUCCESS) {
vk_device_fault_report(r);
rt_fatal("vk: vkQueueSubmit2 of value %llu failed (%d)", (unsigned long long)fence, (int)r);
}
s->fence = recorded ? fence : 0;
vk_mem_note_submit(fence);
AcquireSRWLockExclusive(&st_lock);
+92 -8
View File
@@ -313,7 +313,13 @@ static Tex *create_tex(const tex_t *t, const TexKey *k, int defer_init) {
if ((d->flags & NVN_TEXTURE_FLAGS_IMAGE) && (ff & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && samples == 1)
usage |= VK_IMAGE_USAGE_STORAGE_BIT;
VkImageCreateFlags flags = ds ? 0 : VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; // format-reinterpreting views
if (type == VK_IMAGE_TYPE_2D && layers >= 6 && w == h && samples == 1) flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
// Cube compatible only for cube targets: the replay makes cube views of nothing else (vk_exec_draw.c get_view checks
// the image's own view type). The flag on every square array of 6+ layers changed how AMD's Windows driver lays such
// an image out, and terrain material arrays sampled through it came out in rainbow blocks that moved with the mip
// level (Savage Wind Recomp issue #59, BotW on an RX 580 and a 7800 XT).
if (type == VK_IMAGE_TYPE_2D && layers >= 6 && w == h && samples == 1 &&
(d->target == NVN_TEXTURE_TARGET_CUBEMAP || d->target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY))
flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
// NVN renders into a 3D texture slice by slice (a view's min layer is the depth plane, nvn_mtl.m), e.g. the
// grading LUT of the menu background blur. Vulkan needs a 2D view of the slice for that.
if (type == VK_IMAGE_TYPE_3D && (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) flags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
@@ -475,6 +481,36 @@ static void sweep_released(void) {
while (nreleased > RELEASED_MAX && rel_head) destroy_tex(rel_head);
}
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each submit drops up to
// PRESSURE_STEP of the oldest released entries, so a 4 GiB card does not keep RELEASED_MAX released images (each ASTC
// one decoded to RGBA8) while the heap spills into system memory. A few per submit, for the reason above. Only entries
// that come back intact are dropped: mirrored ones the GPU never wrote, re-uploaded from their storage. A render target
// lives only in its image; dropping it brought the texture back empty (black flashes in a Depths boss fight, ~170
// "left uninitialised" aliases a minute).
#define PRESSURE_STEP 32
static int vram_pressure = -1;
static uint64_t n_pressure_drops;
static void sweep_pressure(void) { // tex_lock held
if (vram_pressure < 0) {
const char *s = cfg_get("TOTK_TEX_VRAM_PRESSURE");
vram_pressure = s && *s ? atoi(s) : 90;
if (vram_pressure < 0 || vram_pressure > 100) vram_pressure = 90;
rt_log("vk: released textures under VRAM pressure: %s%d%%", vram_pressure ? "dropped above " : "off", vram_pressure);
}
if (!vram_pressure || !rel_head || submit_epoch & 7) return; // the budget query every 8 submits
uint64_t used, budget;
int exact;
if (!vk_mem_vram(&used, &budget, &exact) || used <= budget / 100 * (uint64_t)vram_pressure) return;
int dropped = 0;
for (Tex *e = rel_head, *next; e && dropped < PRESSURE_STEP; e = next) {
next = e->rel_next;
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
destroy_tex(e);
dropped++, n_pressure_drops++;
}
}
// ---------------------------------------------------------------- uploads
static uint32_t texel_alpha_one(int format, uint32_t *elem) {
switch (format) {
@@ -841,9 +877,22 @@ const VkTexImage *vk_texture_get(const tex_t *t) {
}
// Small render targets the CPU reads without NVN's invalidate (vk_texture_write_back_small); small_pending: one may be
// dirty.
enum { WB_SMALL = 64 << 10 };
// dirty. wb_limit is the largest target (bytes) that goes back when the CPU waits for a fence: TOTK_WB_FENCE_LIMIT,
// read once. wb_trace: TOTK_TRACE=gfx, read together with it.
enum { WB_SMALL_DEFAULT = 64 << 10 };
static int small_pending;
static uint64_t wb_limit = WB_SMALL_DEFAULT;
static int wb_trace;
static int wb_cfg_read;
static void wb_cfg_init(void) {
if (wb_cfg_read) return;
int v = cfg_int("TOTK_WB_FENCE_LIMIT");
wb_limit = v < 0 ? 0 : (uint64_t)v;
wb_trace = (rt_trace & RT_TRACE_GFX) != 0;
wb_cfg_read = 1;
if (wb_limit != WB_SMALL_DEFAULT) rt_log("vk: fence write-back limit %llu bytes", (unsigned long long)wb_limit);
}
void vk_texture_gpu_written(const tex_t *t) {
if (!t || !t->mem) return;
@@ -860,7 +909,8 @@ void vk_texture_gpu_written(const tex_t *t) {
storage_rewritten(e, 0, &e->gpu_gen); // CPU writes up to here are older than this render
e->gpu_seq = ++gpu_writes;
e->gpu_fence = vk_exec_recording_fence();
if (e->t.L.size <= WB_SMALL) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
wb_cfg_init();
if (e->t.L.size <= wb_limit) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
own_add(e);
}
}
@@ -1032,6 +1082,7 @@ uint32_t vk_texture_epoch(void) { return __atomic_load_n(&submit_epoch, __ATOMIC
void vk_texture_new_submit(void) {
lock();
if (!++submit_epoch) submit_epoch = 1;
sweep_pressure();
static uint64_t log_at;
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) / 60;
if ((rt_trace & RT_TRACE_PERF) && f != log_at && tex_alias > 0) {
@@ -1050,6 +1101,9 @@ void vk_texture_new_submit(void) {
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
(unsigned long long)untr);
n_checks = 0;
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu", (unsigned long long)f * 60,
nreleased, (unsigned long long)n_pressure_drops);
n_pressure_drops = 0;
}
unlock();
}
@@ -1367,24 +1421,54 @@ void vk_texture_write_back(uint64_t addr, uint64_t size) {
// TotK's grass system renders a 128x128 RGBA8 map of cut and trampled grass into a CPU-cached pool every frame and its
// CPU code reads the pool bytes without nvnMemoryPoolInvalidateMappedRange (cut reactions: blades, insects, the cut
// itself). On the Switch the bytes are there once the frame's fence has passed. So when the
// CPU waits for a fence, mirrored targets up to WB_SMALL bytes rendered by work up to that fence go back to their
// CPU waits for a fence, mirrored targets up to wb_limit bytes rendered by work up to that fence go back to their
// storage, unless the CPU rewrote it since. Larger targets still go back on an invalidate, memcpy or ReadTexels.
int vk_texture_small_dirty(void) { return __atomic_load_n(&small_pending, __ATOMIC_ACQUIRE); }
// TOTK_TRACE=gfx: a dirty target the fence write-back leaves alone because it is over the limit (what a CPU read
// after the wait would miss), or sends back although it is over the default limit (what a raised limit changed). One
// line per target address and outcome; the table is small and a full one stops logging.
static void wb_note(const Tex *e, int skipped) {
static uint64_t seen[2][64];
static int n_seen[2];
for (int i = 0; i < n_seen[skipped]; i++)
if (seen[skipped][i] == e->pub.addr) return;
if (n_seen[skipped] == 64) return;
seen[skipped][n_seen[skipped]++] = e->pub.addr;
const pool_t *p = e->t.pool;
unsigned fl = p ? p->flags : 0;
rt_log("vk: fence write-back %s %#llx %ux%u fmt %#x size %llu pool %s (limit %llu)",
skipped ? "skipped-for-size" : "over-default", (unsigned long long)e->pub.addr, e->t.d.width, e->t.d.height,
e->t.d.format, (unsigned long long)e->t.L.size,
!p ? "none" : (fl & NVN_MEMORY_POOL_FLAGS_CPU_CACHED) ? "cpu-cached" : (fl & NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED) ? "cpu-uncached" : "other",
(unsigned long long)wb_limit);
}
void vk_texture_write_back_small(uint64_t fence) {
lock();
wb_cfg_init();
__atomic_store_n(&small_pending, 0, __ATOMIC_RELAXED);
AcquireSRWLockShared(&owned_lock);
int n = 0, later = 0;
for (int i = 0; i < nowned; i++) n += owned[i]->gpu_dirty && owned[i]->t.L.size <= WB_SMALL;
for (int i = 0; i < nowned; i++) n += owned[i]->gpu_dirty && owned[i]->t.L.size <= wb_limit;
Tex **hit = n ? malloc(sizeof *hit * (size_t)n) : NULL;
if (n && !hit) rt_fatal("vk: out of memory for a texture write-back");
n = 0;
for (int i = 0; i < nowned; i++) {
Tex *e = owned[i];
if (!e->gpu_dirty || e->t.L.size > WB_SMALL) continue;
if (!e->gpu_dirty) continue;
if (e->t.L.size > wb_limit) {
if (wb_trace && e->gpu_fence <= fence) {
wb_note(e, 1);
perf_count(PERF_CNT_WB_SKIP);
}
continue;
}
if (e->gpu_fence > fence) later = 1;
else hit[n++] = e;
else {
if (wb_trace && e->t.L.size > WB_SMALL_DEFAULT) wb_note(e, 0);
hit[n++] = e;
}
}
ReleaseSRWLockShared(&owned_lock);
if (later) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
+10
View File
@@ -217,6 +217,16 @@ class IniTest(unittest.TestCase):
self.assertEqual(ini.set_language('[language]\nlocale = de-DE\n', 'fr'),
'[language]\nlang = fr\nlocale = de-DE\n')
def test_old_chinese_codes_are_written_as_the_canonical_ones(self):
self.assertEqual(ini.canonical_language('zh-CN'), 'zh-Hans')
self.assertEqual(ini.canonical_language('zh-TW'), 'zh-Hant')
self.assertEqual(ini.canonical_language('ja'), 'ja')
self.assertNotIn('zh-CN', ini.LANGUAGES)
self.assertNotIn('zh-TW', ini.LANGUAGES)
out = ini.set_language(self.EXAMPLE, 'zh-CN')
self.assertEqual(ini.get_language(out), 'zh-Hans')
self.assertEqual(ini.get_language(ini.set_language(out, 'zh-TW')), 'zh-Hant')
def test_write_keeps_user_lines(self):
with tempfile.TemporaryDirectory() as d:
d = Path(d)
+5 -2
View File
@@ -76,8 +76,11 @@ SDL3_DLLS = [UCRT / 'SDL3.dll', UCRT / 'libiconv-2.dll', UCRT / 'libopus-0.dll']
# Flags for generated code as LAS Recomp builds it (Makefile GENFLAGS, Windows), plus -march for the FP/SIMD lowering.
GEN_FLAGS = ['-O1', '-fno-strict-aliasing', '-Wno-return-type', '-Wno-unused-label', '-fvisibility=hidden',
'-march=x86-64-v3']
RT_FLAGS = ['-O2', '-g', '-Wall', '-fno-strict-aliasing', '-march=x86-64-v3']
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '"-Wl,--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
# _WIN32_WINNT: Windows 10, the port's minimum. MSYS2's headers default to it, llvm-mingw's (the release toolchain) to
# Windows 7, which hides the per-monitor DPI functions gfx_window.c and dpi_probe.c call.
RT_FLAGS = ['-O2', '-g', '-Wall', '-fno-strict-aliasing', '-march=x86-64-v3', '-D_WIN32_WINNT=0x0A00']
# -Xlinker passes the map path as one argument; -Wl, would split it at any comma in the build dir path
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '-Xlinker', '"--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-lwinmm', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
VMA_INCLUDE = UCRT.parent / 'include' / 'vma' # vk_mem_alloc.h; MSYS2 package vulkan-memory-allocator
# --toolchain: an llvm-mingw tree. The libraries, headers and DLLs MSYS2 supplies come from the deps directory instead.
+52
View File
@@ -0,0 +1,52 @@
"""Memory the machine can still give to new processes, for sizing worker pools (stdlib only, Windows)."""
import ctypes
import ctypes.wintypes as wt
import sys
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9
LIMIT_PROCESS_MEMORY = 0x100
LIMIT_JOB_MEMORY = 0x200
class _MemStatus(ctypes.Structure):
_fields_ = [('dwLength', wt.DWORD), ('dwMemoryLoad', wt.DWORD)] + \
[(n, ctypes.c_ulonglong) for n in ('total', 'avail', 'tpf', 'apf', 'tv', 'av', 'aev')]
class _JobLimits(ctypes.Structure):
_fields_ = [('PerProcessUserTimeLimit', ctypes.c_longlong), ('PerJobUserTimeLimit', ctypes.c_longlong),
('LimitFlags', wt.DWORD), ('MinimumWorkingSetSize', ctypes.c_size_t),
('MaximumWorkingSetSize', ctypes.c_size_t), ('ActiveProcessLimit', wt.DWORD),
('Affinity', ctypes.c_size_t), ('PriorityClass', wt.DWORD), ('SchedulingClass', wt.DWORD),
('r_ops', ctypes.c_ulonglong), ('w_ops', ctypes.c_ulonglong), ('o_ops', ctypes.c_ulonglong),
('r_bytes', ctypes.c_ulonglong), ('w_bytes', ctypes.c_ulonglong), ('o_bytes', ctypes.c_ulonglong),
('ProcessMemoryLimit', ctypes.c_size_t), ('JobMemoryLimit', ctypes.c_size_t),
('PeakProcessMemoryUsed', ctypes.c_size_t), ('PeakJobMemoryUsed', ctypes.c_size_t)]
def _job_headroom():
"""Bytes left under the memory limit of the job object this process runs in, or None without such a limit."""
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.QueryInformationJobObject.argtypes = [wt.HANDLE, ctypes.c_int, ctypes.c_void_p, wt.DWORD, ctypes.c_void_p]
info = _JobLimits()
# A null handle asks about the job of the calling process.
if not k32.QueryInformationJobObject(None, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, ctypes.byref(info),
ctypes.sizeof(info), None):
return None
if info.LimitFlags & LIMIT_JOB_MEMORY:
return max(0, info.JobMemoryLimit - info.PeakJobMemoryUsed)
if info.LimitFlags & LIMIT_PROCESS_MEMORY:
return info.ProcessMemoryLimit
return None
def available_ram():
"""Physical memory not in use right now, capped by the job memory limit if there is one. None off Windows."""
if sys.platform != 'win32':
return None
status = _MemStatus()
status.dwLength = ctypes.sizeof(status)
if not ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):
return None
headroom = _job_headroom()
return status.avail if headroom is None else min(status.avail, headroom)
+1 -1
View File
@@ -97,7 +97,7 @@ LLVM_SKIP_DIRS = {'aarch64-w64-mingw32', 'armv7-w64-mingw32', 'i686-w64-mingw32'
LLVM_SKIP_SUFFIXES = {'.idl', '.tlb'}
# tools/ files the installer steps run (and what they import), copied from the repo.
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'check_content.py', 'extract.py', 'make_romfs_patches.py',
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'check_content.py', 'extract.py', 'hostmem.py', 'make_romfs_patches.py',
'prepare_shaders.py', 'romfs_index.py', 'shader_census.py', 'shader_list.py']
TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes
RUNTIME_SUFFIXES = {'.h', '.inc', '.ico'} # .ico: the installer window icon
+11 -3
View File
@@ -68,6 +68,15 @@ def read_list(path):
return shader_list.history(ROOT / 'refs' / 'MacNativeGames')
def pipeline_variants(key):
"""The vertex and fragment variant names of an R/B row's pipeline key. Since MacNativeGames f024ef04 (2026-10-28) the
TotK list keeps keys packed, "<vs variant>:<fs variant>:<the rest in hex>"; older revisions and contribution files
hold the whole key in hex, "<vs variant>\\0<fs variant>\\0" + a Metal descriptor (docs/shader-list.md there)."""
if ':' in key:
return key.split(':', 2)[:2]
return [n.decode('ascii', 'replace') for n in bytes.fromhex(key).split(b'\0')[:2]]
def parse_list(text):
"""V rows as (key, deps); priority per shader key from the warm rows (variant names start with the key)."""
variants, prio = [], {}
@@ -85,10 +94,9 @@ def parse_list(text):
elif f[0] == 'C' and len(f) == 3:
warm(f[1][:64], f[2])
elif f[0] in ('R', 'B') and len(f) >= 3:
names = bytes.fromhex(f[1]).split(b'\0')[:2] # "<vs variant>\0<fs variant>\0" + a Metal descriptor
for n in names:
for n in pipeline_variants(f[1]):
if len(n) >= 64:
warm(n[:64].decode('ascii', 'replace'), f[-1])
warm(n[:64], f[-1])
return variants, prio
+11 -3
View File
@@ -3,7 +3,7 @@
# file. Keys are case-insensitive; '#' and ';' start a comment; [sections] only group lines.
[frame rate]
# Frame rate limit: 30, 60, 120, 144, any whole rate from 30 to 360, or display (the refresh rate of the window's
# Frame rate limit: 30, 60, 90, 120, 144, any whole rate from 30 to 360, or display (the refresh rate of the window's
# monitor). Above 60 is experimental and needs realtime = 1. Changes while the game runs take effect at once.
# fps_limit = 60
# Older key, read only while fps_limit is unset: fps60 = 0 means a limit of 30.
@@ -44,7 +44,7 @@
# pipe_prebuild = 1
# Threads that translate known shaders at start (1 to 64; default: half the CPU threads, at most 16).
# prewarm_xlate = 8
# Vulkan graphics pipeline library. 0 if the driver misbehaves with it.
# Vulkan graphics pipeline library, 0 or 1. Default: on, except on AMD's driver for RDNA 1 and 2 (RX 5000 and 6000).
# gpl = 1
# Textures and memory.
@@ -87,6 +87,8 @@
# Vulkan.
# Vulkan validation layer: checks every call and slows the game down a lot. Needs the layer installed (Vulkan SDK).
# vk_validation = 0
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on, except on AMD GPUs.
# vk_recorder = 1
# Dynamic vertex input, 0 or 1. Default: on, except on Intel's older integrated GPU drivers (Vulkan 1.3.215 and older).
# vk_dyn_vertex = 1
# Pin the shader subgroup size to 32 when the GPU allows it.
@@ -95,7 +97,8 @@
[display]
# Presentation mode: mailbox (default when the driver offers it, vsync otherwise), fifo (vsync) or immediate.
# vk_present = mailbox
# Window client area in pixels (640x360 to 7680x4320), and borderless fullscreen. F11 switches fullscreen and saves it
# Window client area in screen pixels (640x360 to 7680x4320), and borderless fullscreen. Windows display scaling does
# not apply: at 150% scale, 1920x1080 covers the area 1280x720 covers at 100%. F11 switches fullscreen and saves it
# here. Changes while the game runs take effect at once.
# window_size = 1920x1080
# fullscreen = 0
@@ -180,3 +183,8 @@
# takes longer than flight_spike_ms (0 to 60000).
# flight = 0
# flight_spike_ms = 100
# Largest render target (bytes, 0 to 1073741824) that goes back to its game memory when the game waits for a GPU fence.
# A game that reads a larger target without invalidating it sees stale bytes at the default. With trace = gfx the log
# names each dirty target over the limit that stays on the GPU ("fence write-back skipped-for-size") and each one over
# 65536 bytes that went back ("over-default"). Raising it costs a copy per such target and wait.
# wb_fence_limit = 65536