mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
Compare commits
No files matched your search
@@ -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
|
||||
|
||||
+3
-2
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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
@@ -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']:
|
||||
|
||||
@@ -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
@@ -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},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user