mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Merge dev into the 5.1 surround output
The device sink loop now writes every output channel; upstream's TOTK_AUDIO_DUMP gets the front pair before clipping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
5d254e462c
66 files changed
+1634
-216
No files matched your search
+6
-1
@@ -95,7 +95,12 @@ of them.
|
||||
```
|
||||
|
||||
`--cpu v2` targets x86-64-v2 (SSE4.2, no AVX2/FMA) for older CPUs; the default `v3` needs AVX2. The v2 build links
|
||||
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower.
|
||||
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower. Inline assembly,
|
||||
target attributes and intrinsics are not bound by `-march`, so a v2 build runs `tools/check_isa.py` over the runtime
|
||||
objects before it links: any AVX, BMI or other instruction above x86-64-v2 outside a function listed there as
|
||||
dispatched by a runtime CPU check stops the build.
|
||||
|
||||
`gen.py` sizes its emit pool from the CPU count and the free memory; `--jobs N` sets it, and so does the environment variable `TOTK_LIFT_JOBS=N`, which the installer's lift step honours too (it has no `--jobs`). When a worker runs out of memory, the pool restarts with half the workers (`emit: out of memory with N workers, retrying with M`), down to one; only an out-of-memory error with one worker fails the step.
|
||||
|
||||
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
|
||||
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
|
||||
|
||||
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
|
||||
|
||||
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `TOTK_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
|
||||
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `TOTK_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
|
||||
|
||||
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
|
||||
|
||||
|
||||
+13
-1
@@ -149,11 +149,23 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
|
||||
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
|
||||
finishes.
|
||||
|
||||
## Switch system fonts
|
||||
|
||||
Simplified Chinese, Traditional Chinese and Korean text needs the Switch system fonts (`0-standard.bfttf` and the other
|
||||
nn::pl shared fonts). The game does not ship them. Without them it exits before its first frame in those languages
|
||||
(`rt_fatal` in `runtime/ov_system.c`). Supply them from your own console, in either way:
|
||||
|
||||
- Give the setup the firmware folder (`--firmware DIR`, or the window's firmware field); the Fonts step extracts them to
|
||||
`shared-fonts/` in the install folder.
|
||||
- Run `python tools/extract_fonts.py --firmware <dir>` with `.venv-py314` and the keys from `config.local.toml`, or
|
||||
point `TOTK_SHARED_FONTS` at a folder that holds the extracted fonts.
|
||||
|
||||
## Linux release
|
||||
|
||||
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `TotK-Setup-x86_64.AppImage` (the static
|
||||
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
|
||||
Desktop Mode. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
|
||||
Desktop Mode. The command-line setup needs the desktop session's display (`DISPLAY` or `WAYLAND_DISPLAY`) because preparing the
|
||||
shaders opens a window; the check step fails at once without it. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
|
||||
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
|
||||
`libSDL3.so.0` beside it. The folder inside the AppImage:
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ Source shorthands:
|
||||
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | yuzu: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); TotK on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `TOTK_GATHER_BIAS=0` turns it off. | — |
|
||||
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
|
||||
| 2.13 | **TXD gradient register order**: TotK's terrain shaders fill the gradient registers of a `TXD` per coordinate (du/dx, du/dy, dv/dx, dv/dy). | Vulkan's `Grad` takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). | Ryujinx's `EmitTxd` takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (1.4.3, the Depths; found by the user from their F9 frame, a gradient sample's coordinate and gradients traced back to `DPdx` and `DPdy` of the interpolants). Ours: `gfx_shader.c` `patch_txd_transpose` swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). `TOTK_TXD_TRANSPOSE=0` turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. | — |
|
||||
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. yuzu offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `TOTK_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
|
||||
| 2.14 | **Linear CPU-written textures** (`FLAGS_LINEAR`, small ramps, glyph caches): CPU writes visible to the GPU directly. | Must re-upload or use linear images over host memory. | NXN Mario: sky black without its 64×1 RGBA16F ramps. | NXN |
|
||||
|
||||
@@ -39,6 +39,10 @@ build\<ver>\totk.exe
|
||||
`TOTK_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
|
||||
do one run with it and one without (`set TOTK_VK_VALIDATION=`).
|
||||
|
||||
If the game stops with a lost device (the log says `device lost` or `VK_ERROR_DEVICE_LOST`), the lines starting with
|
||||
`vk: pass marks` name the render pass or dispatch the GPU was running. AMD GPUs write them by default. On an NVIDIA GPU,
|
||||
`set TOTK_PASS_CRUMBS=1` (or GPU pass marks: On in the Advanced tab) before the run that reproduces the crash.
|
||||
|
||||
The game keeps running behind other windows (`assume_focus = 0` in `totk.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
|
||||
the first minutes of play can stutter.
|
||||
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
|
||||
|
||||
+6
-1
@@ -349,6 +349,11 @@ class Check(Step):
|
||||
raise InstallError('check.keys_unreadable') from None
|
||||
if 'header_key' not in key_values:
|
||||
raise InstallError('check.keys_no_header_key')
|
||||
if (sys.platform == 'linux' and ctx.will_run('prepare')
|
||||
and not (os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))):
|
||||
# totk --prepare-shaders opens a window and needs a present-capable queue family; without a display it
|
||||
# fails after lift and compile (~15 min), so stop here
|
||||
raise InstallError('check.no_display')
|
||||
ctx.lang = lang
|
||||
ctx.temp.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -838,7 +843,7 @@ def runtime_key(ctx):
|
||||
tools. Shader preparation depends on these, not on the code cheats a lift adds."""
|
||||
layout, cpu = ctx.layout, getattr(ctx, 'cpu', 'v3')
|
||||
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
|
||||
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py')]
|
||||
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py', 'check_isa.py')]
|
||||
return fingerprint('runtime-1', cpu, layout.runtime_lib_for(cpu), layout.runtime, layout.deps or 'system',
|
||||
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"check.vulkan_missing": "No Vulkan driver is installed. Install the latest driver for your graphics card.",
|
||||
"check.vulkan_no_device": "Vulkan finds no graphics card. Install the latest driver for your graphics card.",
|
||||
"check.vulkan_too_old": "The game needs a graphics card with Vulkan 1.3; found: {devices}. Update the graphics driver.",
|
||||
"check.no_display": "Preparing the shaders needs a display. Run the setup from the desktop session, or export DISPLAY=:0 (on a Steam Deck in Game Mode also XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.0 or 1.2.1 or 1.4.3 update and prod.keys.",
|
||||
"check.input_missing": "File not found ({input}): {path}",
|
||||
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
|
||||
@@ -93,7 +94,7 @@
|
||||
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
|
||||
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts. Choose the firmware folder (Contents/registered) to install them; without it some text may not show.",
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, and none were given. The game will not start in this language until they are added. Choose the firmware folder (Contents/registered), or see \"Switch system fonts\" in docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
|
||||
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"check.vulkan_missing": "Nenhum driver Vulkan está instalado. Instale o driver mais recente da sua placa de vídeo.",
|
||||
"check.vulkan_no_device": "O Vulkan não encontra nenhuma placa de vídeo. Instale o driver mais recente da sua placa de vídeo.",
|
||||
"check.vulkan_too_old": "O jogo precisa de uma placa de vídeo com Vulkan 1.3; encontrado: {devices}. Atualize o driver de vídeo.",
|
||||
"check.no_display": "Preparar os shaders exige uma tela. Execute a instalação na sessão da área de trabalho ou use export DISPLAY=:0 (no Steam Deck em Game Mode, também XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Os arquivos do jogo são necessários: escolha o jogo base, a atualização 1.1.0, 1.2.1 ou 1.4.3 e o prod.keys.",
|
||||
"check.input_missing": "Arquivo não encontrado ({input}): {path}",
|
||||
"check.keys_unreadable": "Não foi possível ler o arquivo de chaves. Escolha um arquivo prod.keys.",
|
||||
@@ -93,7 +94,7 @@
|
||||
"prepare.pack_failed": "Falha ao juntar o cache de shaders (código de saída {code}). Detalhes: {log}",
|
||||
"cleanup.ini_failed": "Não foi possível gravar o totk.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, que o instalador não extrai; parte do texto pode não aparecer.",
|
||||
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, e nenhuma foi informada. O jogo não inicia neste idioma até que sejam adicionadas. Escolha a pasta do firmware (Contents/registered) ou veja \"Switch system fonts\" em docs/installer.md.",
|
||||
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"check.vulkan_missing": "Драйвер Vulkan не установлен. Установите свежий драйвер видеокарты.",
|
||||
"check.vulkan_no_device": "Vulkan не находит видеокарту. Установите свежий драйвер видеокарты.",
|
||||
"check.vulkan_too_old": "Игре нужна видеокарта с Vulkan 1.3; найдено: {devices}. Обновите драйвер видеокарты.",
|
||||
"check.no_display": "Для подготовки шейдеров нужен дисплей. Запустите установку из сеанса рабочего стола или выполните export DISPLAY=:0 (на Steam Deck в игровом режиме ещё XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.0 или 1.2.1 или 1.4.3 и prod.keys.",
|
||||
"check.input_missing": "Файл не найден ({input}): {path}",
|
||||
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
|
||||
@@ -93,7 +94,7 @@
|
||||
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch. Выберите папку прошивки (Contents/registered), чтобы установить их; без неё часть текста может не отображаться.",
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, а они не указаны. Игра не запустится на этом языке, пока их не добавят. Выберите папку прошивки (Contents/registered) или см. раздел «Switch system fonts» в docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
|
||||
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"check.vulkan_missing": "未安装 Vulkan 驱动。请为你的显卡安装最新的驱动程序。",
|
||||
"check.vulkan_no_device": "Vulkan 找不到显卡。请为你的显卡安装最新的驱动程序。",
|
||||
"check.vulkan_too_old": "游戏需要支持 Vulkan 1.3 的显卡;检测到:{devices}。请更新显卡驱动程序。",
|
||||
"check.no_display": "准备着色器需要显示器。请在桌面会话中运行安装程序,或执行 export DISPLAY=:0(Steam Deck 游戏模式下还需 XDG_RUNTIME_DIR=/run/user/1000)。",
|
||||
"check.inputs_needed": "需要游戏文件:请选择游戏本体、1.1.0、1.2.1 或 1.4.3 更新以及 prod.keys。",
|
||||
"check.input_missing": "找不到文件({input}):{path}",
|
||||
"check.keys_unreadable": "无法读取密钥文件。请选择一个 prod.keys 文件。",
|
||||
@@ -93,7 +94,7 @@
|
||||
"prepare.pack_failed": "打包着色器缓存失败(退出代码 {code})。详情:{log}",
|
||||
"cleanup.ini_failed": "无法写入 totk.ini:{detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,而安装程序不会提取这些字体;部分文字可能无法显示。",
|
||||
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,但未提供。在添加这些字体之前,游戏无法以该语言启动。请选择固件文件夹(Contents/registered),或参阅 docs/installer.md 中的“Switch system fonts”一节。",
|
||||
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
|
||||
|
||||
+39
-11
@@ -558,7 +558,8 @@ def _retry(fn, *args):
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
|
||||
|
||||
|
||||
def _worker_init(mods, starts, table, opts):
|
||||
def _worker_init(mods, starts, table, opts, jobs):
|
||||
_W['jobs'] = jobs
|
||||
_W['mods'] = mods
|
||||
_W['starts'] = starts
|
||||
_W['patches'] = table
|
||||
@@ -581,6 +582,10 @@ def _emit_chunk(task):
|
||||
"""Worker: write one chunk. Returns its statistics."""
|
||||
name, number, path, fns = task
|
||||
t0 = time.perf_counter()
|
||||
# Test hook: a pool wider than this raises the MemoryError a busy machine does, so the retry can be exercised.
|
||||
limit = os.environ.get('TOTK_LIFT_TEST_OOM_ABOVE')
|
||||
if limit and _W['jobs'] > int(limit) and number % 2 == 1:
|
||||
raise MemoryError
|
||||
e = _emitter(name)
|
||||
e.stats.clear()
|
||||
e.unsupported.clear()
|
||||
@@ -632,6 +637,14 @@ 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'
|
||||
env = os.environ.get('TOTK_LIFT_JOBS', '').strip()
|
||||
if env:
|
||||
try:
|
||||
if int(env) >= 1:
|
||||
return int(env), 'TOTK_LIFT_JOBS'
|
||||
except ValueError:
|
||||
pass
|
||||
print(f'ignoring TOTK_LIFT_JOBS={env!r}: not a positive integer', file=sys.stderr, flush=True)
|
||||
cpus = os.cpu_count() or 4
|
||||
free = hostmem.available_ram()
|
||||
if free is None:
|
||||
@@ -705,16 +718,31 @@ def main():
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
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']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
if (i + 1) % 200 == 0 or i + 1 == len(tasks):
|
||||
print(f'emitted {i + 1}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s', file=sys.stderr,
|
||||
flush=True)
|
||||
pending = [t for _, t in tasks]
|
||||
emitted = 0
|
||||
while True:
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
try:
|
||||
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
|
||||
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
|
||||
results[r['module']].append(r)
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
emitted += 1
|
||||
if emitted % 200 == 0 or emitted == len(tasks):
|
||||
print(f'emitted {emitted}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s',
|
||||
file=sys.stderr, flush=True)
|
||||
break
|
||||
except MemoryError:
|
||||
if jobs == 1:
|
||||
raise
|
||||
smaller = jobs // 2
|
||||
print(f'emit: out of memory with {jobs} workers, retrying with {smaller}', file=sys.stderr, flush=True)
|
||||
jobs, why = smaller, 'after out of memory'
|
||||
done = {(r['module'], r['number']) for rs in results.values() for r in rs}
|
||||
pending = [t for t in pending if (t[0], t[1]) not in done]
|
||||
|
||||
for name, d in info.items():
|
||||
rs = sorted(results[name], key=lambda r: r['number'])
|
||||
|
||||
@@ -82,6 +82,17 @@ site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0
|
||||
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
|
||||
# Texture pool offsets past 2 GiB (MacNativeGames ca311e2d). The texture loader keeps a texture's place in its pool as a
|
||||
# 32-bit offset (main+0xad4294: sub x25, x9, x10, stored as the word w25) and widens it as a signed number for
|
||||
# nvnTextureBuilderSetStorage's offset and for SetPackagedTextureData's source (the pool's CPU address + offset). The
|
||||
# shipped heap table keeps every pool far below 2 GiB; a texture pack's heap table with TOTK_APP_POOL raised can make
|
||||
# one larger (MacNativeGames saw a 3.58 GiB pool), and every texture past 2 GiB then got storage 4 GiB below its pool.
|
||||
# The offset is widened unsigned instead, the same value below 2 GiB, right up to 4 GiB. 1.4.3 needs no patch: its
|
||||
# loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits.
|
||||
set pooloff TOTK_POOL_OFFSET_PATCH 1
|
||||
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
|
||||
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
|
||||
+23
-5
@@ -29,9 +29,10 @@ static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C).
|
||||
// Byte 0x0C must be 0xE1 (NDEF Magic / Type 2 Tag CC container byte).
|
||||
if (data[0x0C] != 0xE1) {
|
||||
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C). An amiibo's is F1 10 FF EE, not the E1 of a plain
|
||||
// NDEF tag (3dbrew, "Amiibo"); the CC is not encrypted, so encrypted dumps carry it too.
|
||||
static const uint8_t amiibo_cc[4] = {0xF1, 0x10, 0xFF, 0xEE};
|
||||
if (memcmp(data + 0x0C, amiibo_cc, sizeof amiibo_cc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -106,17 +107,25 @@ bool amiibo_is_device_present(void) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// While the game waits for a tag it stops and starts detection again several times a second (tens with the menu open),
|
||||
// so the log names the first few and then a count every 200th start.
|
||||
static uint64_t g_detect_starts;
|
||||
static uint64_t g_detect_stop_ns; // the last stop: the menu holds "searching" across the game's restarts
|
||||
|
||||
void amiibo_start_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_detecting = true;
|
||||
rt_log("amiibo: start detection");
|
||||
uint64_t n = ++g_detect_starts;
|
||||
if (n <= 3) rt_log("amiibo: start detection");
|
||||
else if (n % 200 == 0) rt_log("amiibo: detection started %llu times so far", (unsigned long long)n);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_stop_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_detecting) g_detect_stop_ns = plat_now_ns();
|
||||
g_detecting = false;
|
||||
rt_log("amiibo: stop detection");
|
||||
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
@@ -175,6 +184,15 @@ AmiiboDeviceState amiibo_get_device_state(void) {
|
||||
return AMIIBO_DEV_STATE_INITIALIZED;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_reader_state(void) {
|
||||
AmiiboDeviceState state = amiibo_get_device_state();
|
||||
if (state != AMIIBO_DEV_STATE_INITIALIZED) return state;
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool recent = g_detect_stop_ns && plat_now_ns() - g_detect_stop_ns < 1000000000ull;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return recent ? AMIIBO_DEV_STATE_SEARCHING : state;
|
||||
}
|
||||
|
||||
bool amiibo_load_file(const char *path) {
|
||||
if (!path || !*path) return false;
|
||||
uint64_t size = 0;
|
||||
|
||||
@@ -51,6 +51,9 @@ bool amiibo_is_mounted(void);
|
||||
|
||||
// State query
|
||||
AmiiboDeviceState amiibo_get_device_state(void);
|
||||
// The state the menu shows: searching also within a second after a stop, since the game restarts detection many times
|
||||
// a second while it waits for a tag. The game reads amiibo_get_device_state.
|
||||
AmiiboDeviceState amiibo_get_reader_state(void);
|
||||
|
||||
// File loading & triggering actions
|
||||
bool amiibo_load_file(const char *path);
|
||||
|
||||
@@ -22,8 +22,11 @@ struct AudioEffectDsp {
|
||||
};
|
||||
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
|
||||
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
|
||||
/* The reference DelayLine wraps at CurrentSampleCount-1, including when its
|
||||
backing allocation is larger. Preserve this timing for sample comparisons. */
|
||||
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
|
||||
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
|
||||
per turn every tap reads a slot that is never written (a one-sample dropout
|
||||
per tap, audible as grain). The cursor and the taps share one ring length here;
|
||||
each delay is one sample longer than the reference. */
|
||||
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
|
||||
static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
|
||||
@@ -32,12 +35,12 @@ static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
static float line_read(const DelayLine *d) { return d->data[d->at]; }
|
||||
static float line_update(DelayLine *d,float v) {
|
||||
float old=line_read(d);d->data[d->at++]=v;
|
||||
if(d->at >= (d->count?d->count-1:0))d->at=0;
|
||||
if(d->at >= d->count)d->at=0;
|
||||
return old;
|
||||
}
|
||||
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
|
||||
int at=(int)d->at-(int)sample-offset;
|
||||
if(at<0)at+=(int)d->count;
|
||||
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
|
||||
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
|
||||
return d->data[at];
|
||||
}
|
||||
|
||||
+61
-5
@@ -9,7 +9,7 @@
|
||||
// with TOTK_AUDIO_CHANNELS, output_channels) into a single-producer single-consumer ring. The SDL audio thread drains
|
||||
// that ring without taking any lock: an empty ring
|
||||
// plays silence and never blocks the render thread or the guest. TOTK_AUDIO=none (or TOTK_AUDIO_OFF) mixes without a
|
||||
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20) is the queued audio the output waits for after start or underrun.
|
||||
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
|
||||
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
|
||||
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
|
||||
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
|
||||
@@ -93,8 +93,15 @@ static void report_once(Mixer *m, unsigned bit, const char *msg, int value) {
|
||||
// ---- output: SDL3 audio stream fed from the ring
|
||||
// How much audio the output waits for before it starts, and again after an underrun: TOTK_AUDIO_PREFILL_MS rounded up
|
||||
// to whole 5 ms render blocks. Returns -1 for a value outside 0..500 ms.
|
||||
// The Linux default is higher: at 20 ms PipeWire's queue (max(prefill, two 1024-sample periods)) underran
|
||||
// thousands of times within minutes in busy scenes, and 60 ms removed the underruns.
|
||||
#ifdef __linux__
|
||||
#define PREFILL_DEFAULT_MS 60
|
||||
#else
|
||||
#define PREFILL_DEFAULT_MS 20
|
||||
#endif
|
||||
static int prefill_blocks_for(const char *e) {
|
||||
if (!e || !*e) return 4;
|
||||
if (!e || !*e) return PREFILL_DEFAULT_MS / 5;
|
||||
char *end;
|
||||
double ms = strtod(e, &end);
|
||||
if (end == e || *end || !(ms >= 0) || ms > 500) return -1;
|
||||
@@ -264,6 +271,34 @@ static void output_push(Mixer *m, const float *out, int frames) {
|
||||
if (!m->device_ok) atomic_store(&m->rd, wr + (uint64_t)frames); // no device: the ring only keeps the latest frames
|
||||
}
|
||||
|
||||
// TOTK_AUDIO_DUMP=<path>: the sink mix before the clamp as raw interleaved float32 stereo, and every 5 s one log line with
|
||||
// how many frames were rendered less than 1 ms after the one before (a burst refill). File writes on the render thread:
|
||||
// for diagnosis only.
|
||||
static void sink_dump(const float *raw, int frames) {
|
||||
static FILE *f;
|
||||
static int opened;
|
||||
static uint64_t last, window_start, total, bursts;
|
||||
if (!opened) {
|
||||
opened = 1;
|
||||
const char *path = cfg_get("TOTK_AUDIO_DUMP");
|
||||
if (path && *path) f = fopen(path, "wb");
|
||||
}
|
||||
if (!f) return;
|
||||
fwrite(raw, sizeof(float), (size_t)frames * 2, f);
|
||||
uint64_t now = kern_now_ns();
|
||||
total++;
|
||||
if (last && now - last < 1000000ull) bursts++;
|
||||
last = now;
|
||||
if (!window_start) window_start = now;
|
||||
if (now - window_start >= 5000000000ull) {
|
||||
rt_log("audio: dump: %llu frames in %.1f s, %llu rendered under 1 ms after the previous", (unsigned long long)total,
|
||||
(now - window_start) / 1e9, (unsigned long long)bursts);
|
||||
total = bursts = 0;
|
||||
window_start = now;
|
||||
fflush(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Level meter (TOTK_TRACE=audio): one line per 5 s of output, and the first nonzero frame.
|
||||
static void level_note(AudioRenderer *r, Mixer *m, const float *out, int frames) {
|
||||
if (!(rt_trace & RT_TRACE_AUDIO)) return;
|
||||
@@ -322,8 +357,8 @@ void audio_mixer_open(AudioRenderer *r) {
|
||||
const char *pe = cfg_get("TOTK_AUDIO_PREFILL_MS");
|
||||
m->prefill = prefill_blocks_for(pe);
|
||||
if (m->prefill < 0) {
|
||||
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using 20", pe);
|
||||
m->prefill = 4;
|
||||
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using %d", pe, PREFILL_DEFAULT_MS);
|
||||
m->prefill = PREFILL_DEFAULT_MS / 5;
|
||||
}
|
||||
m->channels = output_channels();
|
||||
r->mixer = m;
|
||||
@@ -538,8 +573,25 @@ static void walk_step(Walk *w) { /* the same transitions as advance_voice in a
|
||||
walk_skip_empty(w);
|
||||
}
|
||||
/* Decodes `frames` output samples per channel into out[c][k]; the renderer advances the voice afterwards by the same amount. */
|
||||
static double voice_step(const AudioRenderer *r, const ServerVoice *v) {
|
||||
return v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
}
|
||||
/* Source samples the voice consumes over `frames` output samples, counted with the exact accumulation voice_render steps by
|
||||
(float rounding makes `fraction + seconds * rate * pitch` differ from it by one sample on some frames, a seam click);
|
||||
leaves the fractional position in v->fraction. */
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames) {
|
||||
const double step = voice_step(r, v);
|
||||
double frac = v->fraction;
|
||||
uint64_t consumed = 0;
|
||||
for (int k = 0; k < frames; k++) {
|
||||
frac += step;
|
||||
while (frac >= 1.0) { frac -= 1.0; consumed++; }
|
||||
}
|
||||
v->fraction = frac;
|
||||
return consumed;
|
||||
}
|
||||
static void voice_render(AudioRenderer *r, ServerVoice *v, float out[6][MAX_FRAME], int frames) {
|
||||
const double step = v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
const double step = voice_step(r, v);
|
||||
Walk w = {v, v->index, v->offset, v->loops_done, {0}, 0, v->adpcm.history[0], v->adpcm.history[1], v->adpcm.buffer, v->adpcm.at};
|
||||
memcpy(w.valid, v->valid, sizeof w.valid);
|
||||
walk_skip_empty(&w);
|
||||
@@ -578,6 +630,7 @@ static void mix_aux(AudioRenderer *r, int mix, const AudrenEffectIn *e) {
|
||||
uint8_t *send=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4), *ret=ipc_guest(p.return_info,128+(uint64_t)p.capacity*4);
|
||||
uint32_t *sh=(uint32_t *)(send+64),*rh=(uint32_t *)(ret+64); int32_t *sd=(int32_t *)(send+128),*rd=(int32_t *)(ret+128);
|
||||
uint32_t wr=sh[1],read=rh[0],total=p.channels*(uint32_t)m->frame;
|
||||
if(e->enabled) report_once(m,6,"auxiliary effect in use, channels",(int)p.channels);
|
||||
for(unsigned c=0;c<p.channels;c++) {
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("auxiliary effect mix buffer");
|
||||
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
|
||||
@@ -836,6 +889,7 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
}
|
||||
const float gain = host_volume() * atomic_load(&device_volume) * (r->mixes[0].in_use ? r->mixes[0].volume : 1.0f);
|
||||
const float *ch[6] = {0};
|
||||
float raw[2 * MAX_FRAME];
|
||||
for (uint32_t c = 0; c < sink->input_count; c++) if (sink->inputs[c] >= 0 && sink->inputs[c] < m->nbuf) ch[c] = mixbuf(m, sink->inputs[c]);
|
||||
for (int k = 0; k < frames; k++) {
|
||||
float s[OUT_MAX] = {0}; /* FL FR C LFE RL RR */
|
||||
@@ -848,11 +902,13 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
s[1] = ff * (ch[1] ? ch[1][k] : 0) + cf * C + sf * (ch[5] ? ch[5][k] : 0);
|
||||
} else if (sink->input_count >= 2) { s[0] = ch[0] ? ch[0][k] : 0; s[1] = ch[1] ? ch[1][k] : 0; }
|
||||
else s[0] = s[1] = ch[0] ? ch[0][k] : 0;
|
||||
raw[2 * k] = s[0] * gain; raw[2 * k + 1] = s[1] * gain; /* TOTK_AUDIO_DUMP: the front pair before clipping */
|
||||
for (int c = 0; c < oc; c++) {
|
||||
float x = s[c] * gain;
|
||||
out[oc * k + c] = x > 1 ? 1 : x < -1 ? -1 : x;
|
||||
}
|
||||
}
|
||||
sink_dump(raw, frames);
|
||||
} else if (!sink && !circular) report_once(m, 5, "no device sink in use", r->param.sink_count);
|
||||
level_note(r, m, out, frames);
|
||||
output_push(m, out, frames);
|
||||
|
||||
@@ -24,8 +24,8 @@ static void delay_set(Delay *d, unsigned n) { if (n > d->cap) rt_fatal("reverb d
|
||||
static void delay_init(Delay *d, unsigned khz, float ms) { d->cap = samples(khz, ms); d->data = calloc(d->cap + 1, sizeof(float)); if (!d->data) rt_fatal("reverb delay allocation"); delay_set(d, d->cap); }
|
||||
static float delay_update(Delay *d, float v) {
|
||||
d->data[d->write++] = v; float out = d->data[d->read++];
|
||||
if (d->read >= d->cap) d->read = 0;
|
||||
if (d->write >= d->cap) d->write = 0;
|
||||
if (d->read > d->cap) d->read = 0;
|
||||
if (d->write > d->cap) d->write = 0;
|
||||
return out;
|
||||
}
|
||||
static float delay_tap(const Delay *d, unsigned at) { int i = (int)d->write - (int)at - 1; if (i < 0) i += (int)d->cap + 1; return d->data[i]; }
|
||||
|
||||
+21
-6
@@ -318,11 +318,9 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
|
||||
|
||||
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
|
||||
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v) {
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
|
||||
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
|
||||
uint64_t samples = (uint64_t)want;
|
||||
v->fraction = want - (double)samples;
|
||||
uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count);
|
||||
while (samples && v->valid[v->index]) {
|
||||
AudrenWaveBuffer *b = &v->buffers[v->index];
|
||||
uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset;
|
||||
@@ -354,7 +352,19 @@ static void render_thread(void *p) {
|
||||
plat_thread_priority(PLAT_PRIO_HIGHEST);
|
||||
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
|
||||
const double frame = (double)r->param.sample_count / r->param.sample_rate;
|
||||
uint64_t next = kern_now_ns();
|
||||
// When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target
|
||||
// and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and
|
||||
// the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux
|
||||
// reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame
|
||||
// apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls
|
||||
// 480-sample periods, and a sink dump (TOTK_AUDIO_DUMP) of the bursts there showed no more seam spikes than
|
||||
// chance, so a wait on this thread buys nothing.
|
||||
#ifdef __linux__
|
||||
const uint64_t gap_ns = frame_ns * 4 / 5;
|
||||
#else
|
||||
const uint64_t gap_ns = 0;
|
||||
#endif
|
||||
uint64_t next = kern_now_ns(), last = 0;
|
||||
while (plat_cas32(&r->running, 0, 0)) {
|
||||
uint64_t now = kern_now_ns();
|
||||
int64_t pace = audio_mixer_pace_ns(r, now);
|
||||
@@ -362,6 +372,10 @@ static void render_thread(void *p) {
|
||||
plat_wait_ns((uint64_t)pace);
|
||||
continue;
|
||||
}
|
||||
if (pace == 0 && last && now - last < gap_ns) {
|
||||
plat_wait_ns(gap_ns - (now - last));
|
||||
continue;
|
||||
}
|
||||
if (pace == 0) {
|
||||
next = now + frame_ns; // where the timer goes on if the device stops pacing
|
||||
} else {
|
||||
@@ -369,11 +383,12 @@ static void render_thread(void *p) {
|
||||
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
next += frame_ns;
|
||||
}
|
||||
last = kern_now_ns();
|
||||
plat_mutex_lock(&r->lock);
|
||||
if (r->state == 0) {
|
||||
voices_prepare(r);
|
||||
audio_mixer_render(r, frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i], frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]);
|
||||
r->elapsed_frames++;
|
||||
uint64_t given = r->stop_released + r->empty_played + r->flushed;
|
||||
if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) {
|
||||
|
||||
@@ -184,5 +184,6 @@ void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voic
|
||||
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
|
||||
// device stalled).
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
|
||||
+5
-4
@@ -44,7 +44,7 @@ static const char *const known[] = {
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
@@ -53,7 +53,7 @@ static const char *const known[] = {
|
||||
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
@@ -95,7 +95,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"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_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ static const CfgMeta metas[] = {
|
||||
{"HDR", "0", "0|1", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "80..500", LIVE},
|
||||
{"HDR_UI_NITS", "203", "80..500", LIVE},
|
||||
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
|
||||
{"SHADOW_RES", "game", "game|512|768|1024|1536|2048", RESTART},
|
||||
{"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART},
|
||||
{"SHADOW_SCALE", "1", "1..4", RESTART},
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
@@ -167,6 +167,7 @@ static const CfgMeta metas[] = {
|
||||
// diagnostics
|
||||
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
|
||||
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
|
||||
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
|
||||
{"INPUT_RECORD", "", NULL, RESTART},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
|
||||
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
|
||||
typedef void (*FolderPickDone)(const char *path, void *ctx);
|
||||
|
||||
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
|
||||
// The caller keeps one dialog open at a time.
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
|
||||
// thread, which runs SDL's event loop that delivers the portal's answer.
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_sdl.h"
|
||||
|
||||
typedef struct {
|
||||
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
} PickReq;
|
||||
|
||||
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
|
||||
(void)filter;
|
||||
PickReq *r = userdata;
|
||||
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
|
||||
r->done(list && list[0] ? list[0] : NULL, r->ctx);
|
||||
free(r);
|
||||
}
|
||||
|
||||
static void show(void *arg) {
|
||||
PickReq *r = arg;
|
||||
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
|
||||
}
|
||||
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = calloc(1, sizeof *r);
|
||||
if (!r) {
|
||||
done(NULL, ctx);
|
||||
return;
|
||||
}
|
||||
r->done = done;
|
||||
r->ctx = ctx;
|
||||
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
|
||||
gfx_window_sdl_run(show, r);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
|
||||
// game window, on a thread of its own so the present thread that draws the menu keeps running.
|
||||
#include <windows.h>
|
||||
#include <shobjidl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_win.h"
|
||||
#include "platform/platform.h"
|
||||
}
|
||||
|
||||
struct PickReq {
|
||||
std::string start;
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
static std::wstring wide(const std::string &utf8) {
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
|
||||
std::wstring w(n > 0 ? n : 1, L'\0');
|
||||
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
|
||||
w.resize(wcslen(w.c_str()));
|
||||
return w;
|
||||
}
|
||||
|
||||
static std::string utf8(const wchar_t *w) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
|
||||
std::string s(n > 0 ? n : 1, '\0');
|
||||
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
|
||||
s.resize(strlen(s.c_str()));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
|
||||
static void set_start(IFileOpenDialog *d, const std::string &start) {
|
||||
wchar_t full[MAX_PATH * 2];
|
||||
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
|
||||
d->SetFolder(item);
|
||||
item->Release();
|
||||
}
|
||||
}
|
||||
|
||||
static void pick_main(void *arg) {
|
||||
PickReq *r = (PickReq *)arg;
|
||||
std::string chosen;
|
||||
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
IFileOpenDialog *d = NULL;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
|
||||
if (SUCCEEDED(hr)) {
|
||||
FILEOPENDIALOGOPTIONS opt = 0;
|
||||
d->GetOptions(&opt);
|
||||
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
||||
set_start(d, r->start);
|
||||
hr = d->Show(gfx_window_handle());
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
|
||||
PWSTR path = NULL;
|
||||
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
||||
chosen = utf8(path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
item->Release();
|
||||
}
|
||||
d->Release();
|
||||
}
|
||||
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
|
||||
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
|
||||
if (SUCCEEDED(co)) CoUninitialize();
|
||||
delete r;
|
||||
}
|
||||
|
||||
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = new PickReq{start ? start : "", done, ctx};
|
||||
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
|
||||
rt_log("folder_pick: cannot start the dialog thread");
|
||||
done(NULL, ctx);
|
||||
delete r;
|
||||
}
|
||||
}
|
||||
+48
-5
@@ -125,12 +125,30 @@ static void make_dirs(const char *p) {
|
||||
if (!make_dir(buf)) rt_fatal("fs: cannot create directory %s", buf);
|
||||
}
|
||||
|
||||
// Remove a directory tree; a link (reparse point, symbolic link) is removed as a link, never followed. 0 also when
|
||||
// The first path remove_tree could not delete and the OS error code, for the message of a fatal.
|
||||
static char remove_failed[1100];
|
||||
static int remove_error;
|
||||
static void remove_note(const char *path) {
|
||||
if (*remove_failed) return;
|
||||
snprintf(remove_failed, sizeof remove_failed, "%s", path);
|
||||
remove_error = plat_fs_os_error();
|
||||
}
|
||||
|
||||
// Remove a directory tree; a link (symbolic link, junction) is removed as a link, never followed. 0 also when
|
||||
// nothing is there.
|
||||
static int remove_tree(const char *path) {
|
||||
int kind = plat_fs_kind(path);
|
||||
if (kind == PLAT_FS_MISSING) return 0;
|
||||
if (kind == PLAT_FS_FILE) return plat_fs_unlink(path) ? -1 : 0;
|
||||
if (kind == PLAT_FS_FILE) {
|
||||
if (!plat_fs_unlink(path)) return 0;
|
||||
remove_note(path);
|
||||
return -1;
|
||||
}
|
||||
if (kind == PLAT_FS_LINK) { // a link to a file goes by unlink, a link to a directory by rmdir
|
||||
if (!plat_fs_unlink(path) || !plat_fs_rmdir(path)) return 0;
|
||||
remove_note(path);
|
||||
return -1;
|
||||
}
|
||||
int bad = 0;
|
||||
if (kind == PLAT_FS_DIR) {
|
||||
Listing l;
|
||||
@@ -142,7 +160,23 @@ static int remove_tree(const char *path) {
|
||||
}
|
||||
listing_free(l.e, l.n);
|
||||
}
|
||||
return bad | (plat_fs_rmdir(path) ? -1 : 0);
|
||||
if (plat_fs_rmdir(path)) {
|
||||
remove_note(path);
|
||||
bad |= -1;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
// remove_tree, tried again every 50 ms for up to 2 s while another process (the previous run still exiting, a virus
|
||||
// scanner, a sync client) holds a file in the tree. The failing path and its OS error stay in remove_failed.
|
||||
#define MOVE_TRIES 40 // 2 s, also move_dir's
|
||||
static int remove_tree_retry(const char *path) {
|
||||
for (int i = 0;; i++) {
|
||||
*remove_failed = 0;
|
||||
if (!remove_tree(path)) return 0;
|
||||
if (i == MOVE_TRIES) return -1;
|
||||
plat_sleep_ms(50);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- save transactions
|
||||
@@ -190,7 +224,6 @@ static int exists(const char *p) { return plat_fs_kind(p) != PLAT_FS_MISSING; }
|
||||
|
||||
// Rename a directory, durably; retried every 50 ms, tries times in all, while another process (a virus scanner, the
|
||||
// indexer) holds a file in it. 0 on success.
|
||||
#define MOVE_TRIES 40 // 2 s
|
||||
static int move_dir(const char *from, const char *to, int tries) {
|
||||
for (int i = 0; i < tries; i++) {
|
||||
int e = plat_fs_rename(from, to, 1);
|
||||
@@ -446,7 +479,8 @@ static void save_recover(void) {
|
||||
did = swap_in() ? "a finished commit still waits in save-tx; the game loads it from there"
|
||||
: "completed a finished commit";
|
||||
if (exists(work_root)) {
|
||||
if (remove_tree(work_root)) rt_fatal("fs: cannot delete the old save working copy");
|
||||
if (remove_tree_retry(work_root))
|
||||
rt_fatal("fs: cannot delete the old save working copy: %s (OS error %d)", remove_failed, remove_error);
|
||||
if (!strcmp(did, "nothing to recover"))
|
||||
did = "deleted the last run's working copy (changes after its last Commit)";
|
||||
}
|
||||
@@ -1575,6 +1609,15 @@ static void probe_save_tx(const uint64_t *uid) {
|
||||
CALL(S_UNMOUNT, U("ro"));
|
||||
CHECK(RES(S_DELETE_FILE, U(a)) == 0 && RES(S_DELETE_FILE, U(b)) == 0 && tx_commit() == 0 &&
|
||||
tx_read_committed("tx_a.sav") == -1, "Commit of deletions");
|
||||
// a run that left read-only folders and files in the working copy (attributes another emulator's save carried):
|
||||
// the next start deletes them and names the failing path if it cannot
|
||||
char *ro_dir = join(work_root, "ro_dir"), *ro_file = join(ro_dir, "ro.sav");
|
||||
CHECK(!plat_fs_mkdir(ro_dir) && plat_file_write(ro_file, "x", 1) && plat_file_set_readonly(ro_file, 1) &&
|
||||
plat_file_set_readonly(ro_dir, 1), "read-only leftovers in the working copy");
|
||||
probe_restart(uid);
|
||||
CHECK(!exists(ro_dir), "the next start deleted a working copy with read-only folders");
|
||||
free(ro_dir);
|
||||
free(ro_file);
|
||||
printf("save tx: uncommitted writes visible to the game and dropped by a restart and by Unmount, commits kept; a "
|
||||
"crash at each of %d commit steps recovers to the old save %d times, the new one %d times, mixed %d "
|
||||
"times; %d backups\n", steps, olds, news, mixed, backups);
|
||||
|
||||
@@ -759,6 +759,67 @@ out:
|
||||
free(loc), free(packed), free(ttype), free(tcomps), free(pointee);
|
||||
}
|
||||
|
||||
// Every gradient sample (OpImageSampleExplicitLod with Grad) whose two gradients are each built from two components gets
|
||||
// them transposed, dx = (a, c) and dy = (b, d) from dx = (a, b) and dy = (c, d), by two new OpCompositeConstruct before
|
||||
// it. TotK's shaders fill a TXD's gradient registers per coordinate (du/dx, du/dy, dv/dx, dv/dy), and the translator
|
||||
// (Ryujinx, InstEmitTexture EmitTxd) builds dx = (first two registers), dy = (last two) = (du/dx, du/dy) and
|
||||
// (dv/dx, dv/dy): a mip level chosen from the wrong derivatives, too sharp on terrain faces turned across the screen,
|
||||
// which crawled in patches that followed the faces (1.4.3, the Depths). TOTK_TXD_TRANSPOSE=0 turns it off.
|
||||
// Applied to the SPIR-V after it is loaded, so the shader cache is untouched.
|
||||
enum { OP_COMPOSITE_CONSTRUCT = 80, OP_SAMPLE_EXPLICIT = 88, IO_GRAD = 4 };
|
||||
|
||||
static void patch_txd_transpose(GfxShader *s) {
|
||||
static int on = -1;
|
||||
static volatile int32_t n_logged;
|
||||
if (on < 0) {
|
||||
const char *e = cfg_get("TOTK_TXD_TRANSPOSE");
|
||||
on = !(e && *e == '0');
|
||||
}
|
||||
uint32_t *w = s->spirv;
|
||||
uint64_t n = s->words;
|
||||
if (!on || n < 5) return;
|
||||
uint32_t bound = w[3] < (1u << 20) ? w[3] : (1u << 20);
|
||||
uint64_t *cc = calloc(bound, sizeof *cc); // word index of a 2-constituent OpCompositeConstruct defining the id
|
||||
int found = 0;
|
||||
if (!cc) return;
|
||||
for (uint64_t i = 5; i < n;) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
if (!len || i + len > n) goto out;
|
||||
if (op == OP_COMPOSITE_CONSTRUCT && len == 5 && w[i + 2] < bound) cc[w[i + 2]] = i;
|
||||
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
|
||||
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1])
|
||||
found++;
|
||||
i += len;
|
||||
}
|
||||
if (!found) goto out;
|
||||
uint32_t *o = malloc((n + (uint64_t)found * 10) * sizeof *o);
|
||||
if (!o) goto out;
|
||||
uint64_t k = 0;
|
||||
uint32_t next = w[3];
|
||||
for (uint64_t i = 0; i < 5; i++) o[k++] = w[i];
|
||||
for (uint64_t i = 5; i < n;) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
|
||||
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1]) {
|
||||
const uint32_t *dx = w + cc[w[i + 6]], *dy = w + cc[w[i + 7]]; // [hdr, type, id, a, b], [hdr, type, id, c, d]
|
||||
uint32_t ndx = next++, ndy = next++;
|
||||
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndx, o[k++] = dx[3], o[k++] = dy[3];
|
||||
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndy, o[k++] = dx[4], o[k++] = dy[4];
|
||||
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
|
||||
o[k - len + 6] = ndx, o[k - len + 7] = ndy;
|
||||
} else {
|
||||
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
o[3] = next;
|
||||
free(s->spirv);
|
||||
s->spirv = o, s->words = k;
|
||||
if (plat_inc32(&n_logged) <= 4) rt_log("gfx_shader: %d gradient samples now read their derivatives per coordinate", found);
|
||||
out:
|
||||
free(cc);
|
||||
}
|
||||
|
||||
static void reflect(GfxShader *s) {
|
||||
static const char *const kinds[GFX_RES_KINDS] = {"cbufs", "sbufs", "textures", "images"};
|
||||
const char *js = s->reflection;
|
||||
@@ -771,6 +832,7 @@ static void reflect(GfxShader *s) {
|
||||
json_objects(js, kinds[k], add_res, s, k);
|
||||
}
|
||||
reflect_spirv(s);
|
||||
patch_txd_transpose(s);
|
||||
}
|
||||
|
||||
// ---- use masks (see the cache key at the top)
|
||||
|
||||
@@ -64,6 +64,8 @@ double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, _
|
||||
|
||||
SDL_Window *gfx_window_sdl(void) { return g_win; }
|
||||
|
||||
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
|
||||
|
||||
// ---- settings: size and fullscreen
|
||||
static char g_applied_size[32];
|
||||
static int g_applied_fs;
|
||||
|
||||
@@ -11,6 +11,7 @@ extern "C" {
|
||||
// gfx_window_sdl.c
|
||||
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
|
||||
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
|
||||
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg); // runs fn on the window thread and waits; any thread
|
||||
|
||||
// input_sdl.c
|
||||
void input_sdl_window(SDL_Window *w); // the game window was created
|
||||
|
||||
+101
-72
@@ -415,6 +415,18 @@ static int ap_disabled(const RtThread *t) { return g_ap > 0 && ap_tlr(t)[0]; }
|
||||
// in kern_ap_yield and jumps back to kern_ap_rip(), whose value it first stores in the return slot it reserved at
|
||||
// entry, so a stack walk sees the interrupted function as its caller. No ret: the jump keeps call/return pairs
|
||||
// balanced. The waiter never writes the owner's stack.
|
||||
// An x86-64-v2 build (no __AVX__) saves XMM0-15 with SSE moves into the same 32-byte slots instead: its game code
|
||||
// keeps nothing in the upper YMM halves, and the CPUs it is for have no VEX encoding, so a vmovdqa there faults with
|
||||
// an illegal instruction the first time a core is interrupted.
|
||||
#ifdef __AVX__
|
||||
#define AP_VMOV "vmovdqa"
|
||||
#define AP_V "%ymm"
|
||||
#define AP_VZEROUPPER " vzeroupper\n"
|
||||
#else
|
||||
#define AP_VMOV "movdqa"
|
||||
#define AP_V "%xmm"
|
||||
#define AP_VZEROUPPER ""
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
__asm__(".text\n"
|
||||
".globl kern_ap_stub\n"
|
||||
@@ -446,48 +458,48 @@ __asm__(".text\n"
|
||||
".seh_setframe %rbp, 0\n"
|
||||
".seh_endprologue\n"
|
||||
" andq $-32, %rsp\n"
|
||||
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
|
||||
" vmovdqa %ymm0, 64(%rsp)\n"
|
||||
" vmovdqa %ymm1, 96(%rsp)\n"
|
||||
" vmovdqa %ymm2, 128(%rsp)\n"
|
||||
" vmovdqa %ymm3, 160(%rsp)\n"
|
||||
" vmovdqa %ymm4, 192(%rsp)\n"
|
||||
" vmovdqa %ymm5, 224(%rsp)\n"
|
||||
" vmovdqa %ymm6, 256(%rsp)\n"
|
||||
" vmovdqa %ymm7, 288(%rsp)\n"
|
||||
" vmovdqa %ymm8, 320(%rsp)\n"
|
||||
" vmovdqa %ymm9, 352(%rsp)\n"
|
||||
" vmovdqa %ymm10, 384(%rsp)\n"
|
||||
" vmovdqa %ymm11, 416(%rsp)\n"
|
||||
" vmovdqa %ymm12, 448(%rsp)\n"
|
||||
" vmovdqa %ymm13, 480(%rsp)\n"
|
||||
" vmovdqa %ymm14, 512(%rsp)\n"
|
||||
" vmovdqa %ymm15, 544(%rsp)\n"
|
||||
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
|
||||
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
|
||||
" stmxcsr 32(%rsp)\n"
|
||||
" movl $0x1f80, 36(%rsp)\n"
|
||||
" ldmxcsr 36(%rsp)\n"
|
||||
" cld\n"
|
||||
" vzeroupper\n"
|
||||
AP_VZEROUPPER
|
||||
" call kern_ap_rip\n"
|
||||
" movq %rax, 72(%rbp)\n" // the return slot: rbp + 8 pushes + pushfq
|
||||
" call kern_ap_yield\n"
|
||||
" ldmxcsr 32(%rsp)\n"
|
||||
" vmovdqa 64(%rsp), %ymm0\n"
|
||||
" vmovdqa 96(%rsp), %ymm1\n"
|
||||
" vmovdqa 128(%rsp), %ymm2\n"
|
||||
" vmovdqa 160(%rsp), %ymm3\n"
|
||||
" vmovdqa 192(%rsp), %ymm4\n"
|
||||
" vmovdqa 224(%rsp), %ymm5\n"
|
||||
" vmovdqa 256(%rsp), %ymm6\n"
|
||||
" vmovdqa 288(%rsp), %ymm7\n"
|
||||
" vmovdqa 320(%rsp), %ymm8\n"
|
||||
" vmovdqa 352(%rsp), %ymm9\n"
|
||||
" vmovdqa 384(%rsp), %ymm10\n"
|
||||
" vmovdqa 416(%rsp), %ymm11\n"
|
||||
" vmovdqa 448(%rsp), %ymm12\n"
|
||||
" vmovdqa 480(%rsp), %ymm13\n"
|
||||
" vmovdqa 512(%rsp), %ymm14\n"
|
||||
" vmovdqa 544(%rsp), %ymm15\n"
|
||||
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
|
||||
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
|
||||
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
|
||||
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
|
||||
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
|
||||
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
|
||||
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
|
||||
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
|
||||
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
|
||||
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
|
||||
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
|
||||
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
|
||||
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
|
||||
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
|
||||
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
|
||||
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
|
||||
" movq %rbp, %rsp\n"
|
||||
" popq %rbp\n"
|
||||
" popq %r11\n"
|
||||
@@ -525,48 +537,48 @@ __asm__(".text\n"
|
||||
" pushq %rbp\n"
|
||||
" movq %rsp, %rbp\n"
|
||||
" andq $-32, %rsp\n"
|
||||
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
|
||||
" vmovdqa %ymm0, 64(%rsp)\n"
|
||||
" vmovdqa %ymm1, 96(%rsp)\n"
|
||||
" vmovdqa %ymm2, 128(%rsp)\n"
|
||||
" vmovdqa %ymm3, 160(%rsp)\n"
|
||||
" vmovdqa %ymm4, 192(%rsp)\n"
|
||||
" vmovdqa %ymm5, 224(%rsp)\n"
|
||||
" vmovdqa %ymm6, 256(%rsp)\n"
|
||||
" vmovdqa %ymm7, 288(%rsp)\n"
|
||||
" vmovdqa %ymm8, 320(%rsp)\n"
|
||||
" vmovdqa %ymm9, 352(%rsp)\n"
|
||||
" vmovdqa %ymm10, 384(%rsp)\n"
|
||||
" vmovdqa %ymm11, 416(%rsp)\n"
|
||||
" vmovdqa %ymm12, 448(%rsp)\n"
|
||||
" vmovdqa %ymm13, 480(%rsp)\n"
|
||||
" vmovdqa %ymm14, 512(%rsp)\n"
|
||||
" vmovdqa %ymm15, 544(%rsp)\n"
|
||||
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
|
||||
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
|
||||
" stmxcsr 32(%rsp)\n"
|
||||
" movl $0x1f80, 36(%rsp)\n"
|
||||
" ldmxcsr 36(%rsp)\n"
|
||||
" cld\n"
|
||||
" vzeroupper\n"
|
||||
AP_VZEROUPPER
|
||||
" call kern_ap_rip\n"
|
||||
" movq %rax, 88(%rbp)\n" // the return slot: rbp + 10 pushes + pushfq
|
||||
" call kern_ap_yield\n"
|
||||
" ldmxcsr 32(%rsp)\n"
|
||||
" vmovdqa 64(%rsp), %ymm0\n"
|
||||
" vmovdqa 96(%rsp), %ymm1\n"
|
||||
" vmovdqa 128(%rsp), %ymm2\n"
|
||||
" vmovdqa 160(%rsp), %ymm3\n"
|
||||
" vmovdqa 192(%rsp), %ymm4\n"
|
||||
" vmovdqa 224(%rsp), %ymm5\n"
|
||||
" vmovdqa 256(%rsp), %ymm6\n"
|
||||
" vmovdqa 288(%rsp), %ymm7\n"
|
||||
" vmovdqa 320(%rsp), %ymm8\n"
|
||||
" vmovdqa 352(%rsp), %ymm9\n"
|
||||
" vmovdqa 384(%rsp), %ymm10\n"
|
||||
" vmovdqa 416(%rsp), %ymm11\n"
|
||||
" vmovdqa 448(%rsp), %ymm12\n"
|
||||
" vmovdqa 480(%rsp), %ymm13\n"
|
||||
" vmovdqa 512(%rsp), %ymm14\n"
|
||||
" vmovdqa 544(%rsp), %ymm15\n"
|
||||
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
|
||||
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
|
||||
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
|
||||
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
|
||||
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
|
||||
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
|
||||
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
|
||||
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
|
||||
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
|
||||
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
|
||||
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
|
||||
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
|
||||
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
|
||||
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
|
||||
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
|
||||
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
|
||||
" movq %rbp, %rsp\n"
|
||||
" popq %rbp\n"
|
||||
" popq %r11\n"
|
||||
@@ -1888,7 +1900,9 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
if (type != 0) {
|
||||
int32_t nv = val + 1;
|
||||
if (type == 2) {
|
||||
if (cnt <= 0) nv = nw > 0 ? val - 1 : val + 1;
|
||||
// Waking all (count <= 0) with waiters subtracts 2, not 1 (mesosphere
|
||||
// KAddressArbiter::SignalAndModifyByWaitingCountIfEqual; yuzu, LAS Recomp and nxnative agree).
|
||||
if (cnt <= 0) nv = nw > 0 ? val - 2 : val + 1;
|
||||
else nv = nw == 0 ? val + 1 : nw <= cnt ? val - 1 : val;
|
||||
}
|
||||
uint32_t e = (uint32_t)val;
|
||||
@@ -1952,8 +1966,13 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
int idx = -1;
|
||||
lock();
|
||||
g_sync_calls++;
|
||||
// Horizon's order (mesosphere KSynchronizationObject::Wait): a signalled object, then timeout 0, then a pending
|
||||
// CancelSynchronization. The cancel is for the next wait that would block: a poll or a wait on a signalled
|
||||
// object leaves it pending. A cancel that wakes this wait (CancelSynchronization wakes it with R_CANCELLED; a
|
||||
// signal wakes it with -1 to look again) ends it cancelled, signalled objects or not.
|
||||
uint32_t woke = 0;
|
||||
for (;;) {
|
||||
if (t->cancel) {
|
||||
if (woke == R_CANCELLED) {
|
||||
t->cancel = 0;
|
||||
r = R_CANCELLED;
|
||||
break;
|
||||
@@ -1969,6 +1988,15 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
r = R_OK;
|
||||
break;
|
||||
}
|
||||
if (ns == 0) {
|
||||
r = R_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
if (t->cancel) {
|
||||
t->cancel = 0;
|
||||
r = R_CANCELLED;
|
||||
break;
|
||||
}
|
||||
int64_t left = ns;
|
||||
if (ns > 0) {
|
||||
uint64_t spent = kern_now_ns() - start;
|
||||
@@ -1980,7 +2008,8 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
}
|
||||
t->wobj = n ? hs[0] | (uint64_t)n << 32 : 0;
|
||||
g_sync_blocks++;
|
||||
if (block(t, W_SYNC, left) == R_TIMEOUT) {
|
||||
woke = block(t, W_SYNC, left);
|
||||
if (woke == R_TIMEOUT) {
|
||||
r = R_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
@@ -1995,7 +2024,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
RtThread *t = h_get((uint32_t)x[0], O_THREAD);
|
||||
if (t) {
|
||||
t->cancel = 1;
|
||||
if (t->wkind == W_SYNC) wake(t, (uint32_t)-1);
|
||||
if (t->wkind == W_SYNC) wake(t, R_CANCELLED);
|
||||
}
|
||||
unlock();
|
||||
x[0] = t ? R_OK : R_BAD_HANDLE;
|
||||
|
||||
+40
-4
@@ -305,9 +305,18 @@ static void test_arbiter(Cpu *c) {
|
||||
CHECK(sig(c, 2, 5, 0) == 0 && g_arb == 6, "signal type 2, no waiter, count 0: value %d (want 6)", (int32_t)g_arb);
|
||||
ArbJob jc = {.type = 2, .value = 6, .timeout = -1};
|
||||
h = arb_spawn(c, &jc);
|
||||
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 5, "signal type 2, 1 waiter, count -1: value %d (want 5)", (int32_t)g_arb);
|
||||
// Waking all (count <= 0) with waiters subtracts 2 (mesosphere KAddressArbiter::SignalAndModifyByWaitingCountIfEqual).
|
||||
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count -1: value %d (want 4)", (int32_t)g_arb);
|
||||
join(c, h);
|
||||
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0, "wait type 2 results %#x %#x %#x", ja.r, jb.r, jc.r);
|
||||
g_arb = 6;
|
||||
ArbJob jd = {.type = 2, .value = 6, .timeout = -1}, je = jd;
|
||||
uint32_t hd = arb_spawn(c, &jd), he = arb_spawn(c, &je);
|
||||
CHECK(sig(c, 2, 6, 0) == 0 && g_arb == 4, "signal type 2, 2 waiters, count 0: value %d (want 4)", (int32_t)g_arb);
|
||||
join(c, hd);
|
||||
join(c, he);
|
||||
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0 && jd.r == 0 && je.r == 0, "wait type 2 results %#x %#x %#x %#x %#x", ja.r,
|
||||
jb.r, jc.r, jd.r, je.r);
|
||||
g_arb = 5;
|
||||
CHECK(sig(c, 2, 7, 1) == KERN_R_BAD_STATE, "signal type 2 with a different value");
|
||||
// timeout, invalid type
|
||||
uint64_t t0 = kern_now_ns();
|
||||
@@ -320,11 +329,17 @@ static void test_arbiter(Cpu *c) {
|
||||
}
|
||||
|
||||
// ---- events
|
||||
typedef struct { uint32_t h, r, idx; int64_t timeout; volatile int delay, go; } EvJob;
|
||||
// pre: a first wait on h0 with timeout t0 (result r0, index idx0) before the wait on h.
|
||||
typedef struct { uint32_t h, r, idx, h0, r0, idx0; int64_t timeout, t0; volatile int delay, go; int pre; } EvJob;
|
||||
|
||||
static void event_waiter(Cpu *c) {
|
||||
EvJob *j = arg_p(c, 0);
|
||||
while (j->delay && !j->go) sleep_ms(c, 1);
|
||||
if (j->pre) {
|
||||
uint32_t h0[1] = {j->h0};
|
||||
j->r0 = sv(c, 0x18, 0, ga(h0), 1, (uint64_t)j->t0);
|
||||
j->idx0 = (uint32_t)c->x[1];
|
||||
}
|
||||
uint32_t hs[1] = {j->h};
|
||||
j->r = sv(c, 0x18, 0, ga(hs), 1, (uint64_t)j->timeout);
|
||||
j->idx = (uint32_t)c->x[1];
|
||||
@@ -375,6 +390,26 @@ static void test_events(Cpu *c) {
|
||||
join(c, h);
|
||||
CHECK(g.r == KERN_R_CANCELLED, "wait after a cancel: %#x", g.r);
|
||||
|
||||
// A pending cancel is for the next WaitSynchronization that would block (mesosphere KSynchronizationObject::Wait:
|
||||
// signalled objects first, then timeout 0, then the cancel): a poll times out and a wait on a signalled object
|
||||
// succeeds, both leaving it pending; the next blocking wait ends cancelled. The blocking waits have a 500 ms
|
||||
// timeout, so a cancel used up too early shows as a timeout instead of a hang.
|
||||
CHECK(sv(c, 0x11, hw2, 0, 0, 0) == 0, "SignalEvent of the second event");
|
||||
EvJob p = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr, .t0 = 0};
|
||||
EvJob q = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr2, .t0 = -1};
|
||||
EvJob *pq[2] = {&p, &q};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
h = create(c, a_event_waiter, pq[i], 44, -2);
|
||||
start(c, h);
|
||||
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the %s", i ? "signalled wait" : "poll");
|
||||
pq[i]->go = 1;
|
||||
join(c, h);
|
||||
}
|
||||
CHECK(p.r0 == KERN_R_TIMEOUT, "poll (timeout 0) with a cancel pending: %#x (want timeout)", p.r0);
|
||||
CHECK(p.r == KERN_R_CANCELLED, "blocking wait after the poll: %#x (want cancelled)", p.r);
|
||||
CHECK(q.r0 == 0 && q.idx0 == 0, "wait on a signalled event with a cancel pending: %#x (want success)", q.r0);
|
||||
CHECK(q.r == KERN_R_CANCELLED, "blocking wait after the signalled wait: %#x (want cancelled)", q.r);
|
||||
|
||||
uint32_t sw = kern_event_new(), sr = kern_event_readable(sw);
|
||||
CHECK(wait1(c, sr, 0) == KERN_R_TIMEOUT, "service event before the signal");
|
||||
kern_event_signal(sw);
|
||||
@@ -382,7 +417,8 @@ static void test_events(Cpu *c) {
|
||||
uint32_t handles[] = {sr, sw, hr2, hw2, hr, hw};
|
||||
for (unsigned i = 0; i < 6; i++) CHECK(sv(c, 0x16, handles[i], 0, 0, 0) == 0, "CloseHandle %#x", handles[i]);
|
||||
CHECK(sv(c, 0x16, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "CloseHandle twice");
|
||||
printf("events: signal, clear, reset, two handles, cancel while waiting and before, service events, 10 ms "
|
||||
printf("events: signal, clear, reset, two handles, cancel while waiting and before, a pending cancel kept by a poll "
|
||||
"and a signalled wait, service events, 10 ms "
|
||||
"timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ NVN_API(nvnTextureInitialize) {
|
||||
o->magic = NVN_TEX_MAGIC;
|
||||
o->d = s->d;
|
||||
o->pool = s->pool;
|
||||
o->pool_flags = s->pool ? (uint16_t)s->pool->flags : 0;
|
||||
o->off = s->off;
|
||||
o->has_layout = s->has_layout;
|
||||
memcpy(o->layout, s->layout, 8);
|
||||
|
||||
@@ -48,6 +48,9 @@ typedef struct {
|
||||
} tex_builder_t; FITS(tex_builder_t, NVNtextureBuilder);
|
||||
typedef struct {
|
||||
uint32_t magic; nvn_texdesc d; uint8_t has_layout, layout[8];
|
||||
uint16_t pool_flags; // the pool's flags at Initialize (in the padding): a game may finalize and free
|
||||
// the pool object while the back end still looks the texture up, and the
|
||||
// driver keeps no pointer to it either
|
||||
nvn_texlayout L; uint8_t *mem; pool_t *pool; int64_t off;
|
||||
uint64_t serial; // Initialization serial: the back end uses it to find its texture in the host table (the game may copy the object by value, so object pointers cannot be stored)
|
||||
} tex_t; FITS(tex_t, NVNtexture);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "amiibo.h"
|
||||
#include "cfg.h"
|
||||
#include "fs.h"
|
||||
#include "gfx.h"
|
||||
@@ -398,6 +399,18 @@ static void probe_applet(void) {
|
||||
int reattached = event_works(activate, 0) && event_works(deactivate, 0);
|
||||
CHECK(reattached, "the nfp events do not work after DestroySystemEvent and a second Attach");
|
||||
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection again");
|
||||
// An amiibo's NTAG215 header: UID with its two check bytes, static lock 0F E0, capability container F1 10 FF EE.
|
||||
// The loader takes it and refuses the same file with a plain NDEF container (E1).
|
||||
static const uint8_t head[16] = {0x04, 0x11, 0x22, 0x88 ^ 0x04 ^ 0x11 ^ 0x22, 0x33, 0x44, 0x55, 0x66,
|
||||
0x33 ^ 0x44 ^ 0x55 ^ 0x66, 0x48, 0x0F, 0xE0, 0xF1, 0x10, 0xFF, 0xEE};
|
||||
uint8_t dump[AMIIBO_DUMP_SIZE] = {0};
|
||||
memcpy(dump, head, sizeof head);
|
||||
const char *dump_path = "probe-amiibo.bin";
|
||||
int amiibo_ok = plat_file_write(dump_path, dump, sizeof dump) && amiibo_load_file(dump_path);
|
||||
dump[0x0C] = 0xE1;
|
||||
amiibo_ok = amiibo_ok && plat_file_write(dump_path, dump, sizeof dump) && !amiibo_load_file(dump_path);
|
||||
plat_fs_unlink(dump_path);
|
||||
CHECK(amiibo_ok, "the amiibo loader refuses a dump with the amiibo capability container, or takes a plain NDEF one");
|
||||
CHECK(CALL("nfp.Finalize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 0, "nfp finalize");
|
||||
CALL("err.ShowError", 0x2a8);
|
||||
CALL("nndiagStartup", 0);
|
||||
|
||||
@@ -1038,6 +1038,8 @@ const void *plat_file_map(PlatFile *f) {
|
||||
return v == MAP_FAILED ? NULL : v;
|
||||
}
|
||||
|
||||
int plat_fs_os_error(void) { return errno; }
|
||||
|
||||
static int fs_err(int e) {
|
||||
switch (e) {
|
||||
case ENOENT: case ENOTDIR: return PLAT_FS_NOT_FOUND;
|
||||
|
||||
@@ -316,8 +316,9 @@ enum { PLAT_FS_OK, PLAT_FS_NOT_FOUND, PLAT_FS_EXISTS, PLAT_FS_NOT_EMPTY, PLAT_FS
|
||||
enum { PLAT_FS_MISSING = -1, PLAT_FS_FILE, PLAT_FS_DIR, PLAT_FS_LINK }; // plat_fs_kind; LINK: a symbolic link or
|
||||
// reparse point, never followed
|
||||
int plat_fs_kind(const char *path);
|
||||
int plat_fs_os_error(void); // the OS error code (GetLastError, errno) of this thread's last failed plat_fs_* call
|
||||
int plat_fs_mkdir(const char *path); // PLAT_FS_EXISTS when something is there
|
||||
int plat_fs_rmdir(const char *path); // an empty directory, or a link to one
|
||||
int plat_fs_rmdir(const char *path); // an empty directory (a read-only one too), or a link to one
|
||||
int plat_fs_unlink(const char *path); // a file or a link; a read-only one too
|
||||
// Renames a file or directory; never replaces (PLAT_FS_EXISTS). durable: the change is on the disk at return.
|
||||
int plat_fs_rename(const char *from, const char *to, int durable);
|
||||
|
||||
@@ -866,7 +866,11 @@ const void *plat_file_map(PlatFile *f) {
|
||||
return v;
|
||||
}
|
||||
|
||||
static __thread DWORD fs_last_error;
|
||||
int plat_fs_os_error(void) { return (int)fs_last_error; }
|
||||
|
||||
static int fs_err(DWORD e) {
|
||||
fs_last_error = e;
|
||||
switch (e) {
|
||||
case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_DIRECTORY: return PLAT_FS_NOT_FOUND;
|
||||
case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: return PLAT_FS_EXISTS;
|
||||
@@ -876,12 +880,28 @@ static int fs_err(DWORD e) {
|
||||
}
|
||||
}
|
||||
|
||||
// A symbolic link or junction is a link: removed as a link, never followed. Any other reparse point (OneDrive's cloud
|
||||
// placeholders, deduplicated files) is an ordinary file or directory whose content the file system fetches on access.
|
||||
static int fs_kind_of(const WIN32_FIND_DATAW *fd) {
|
||||
DWORD a = fd->dwFileAttributes;
|
||||
if (a & FILE_ATTRIBUTE_REPARSE_POINT && (fd->dwReserved0 == IO_REPARSE_TAG_SYMLINK || fd->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT))
|
||||
return PLAT_FS_LINK;
|
||||
return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
|
||||
}
|
||||
|
||||
int plat_fs_kind(const char *path) {
|
||||
wchar_t w[WPATH];
|
||||
if (!widen(path, w, WPATH)) return PLAT_FS_MISSING;
|
||||
DWORD a = GetFileAttributesW(w);
|
||||
if (a == INVALID_FILE_ATTRIBUTES) return PLAT_FS_MISSING;
|
||||
return a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
|
||||
if (!(a & FILE_ATTRIBUTE_REPARSE_POINT)) return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
|
||||
// the reparse tag comes with the find data only; FindFirstFileW is asked only here, since it fails on a path
|
||||
// with a trailing separator and reads wildcards
|
||||
WIN32_FIND_DATAW fd;
|
||||
HANDLE h = FindFirstFileW(w, &fd);
|
||||
if (h == INVALID_HANDLE_VALUE) return PLAT_FS_LINK;
|
||||
FindClose(h);
|
||||
return fs_kind_of(&fd);
|
||||
}
|
||||
|
||||
int plat_fs_mkdir(const char *path) {
|
||||
@@ -893,7 +913,15 @@ int plat_fs_mkdir(const char *path) {
|
||||
int plat_fs_rmdir(const char *path) {
|
||||
wchar_t w[WPATH];
|
||||
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
|
||||
return RemoveDirectoryW(w) ? PLAT_FS_OK : fs_err(GetLastError());
|
||||
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
|
||||
DWORD e = GetLastError(), a = GetFileAttributesW(w);
|
||||
if (e == ERROR_ACCESS_DENIED && a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_READONLY)) {
|
||||
// a read-only directory (copied over from another emulator's save folder) refuses RemoveDirectoryW
|
||||
SetFileAttributesW(w, a & ~(DWORD)FILE_ATTRIBUTE_READONLY);
|
||||
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
|
||||
e = GetLastError();
|
||||
}
|
||||
return fs_err(e);
|
||||
}
|
||||
|
||||
int plat_fs_unlink(const char *path) {
|
||||
@@ -1038,9 +1066,7 @@ int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t
|
||||
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue;
|
||||
char name[1024];
|
||||
if (!WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, sizeof name, NULL, NULL)) continue;
|
||||
DWORD a = fd.dwFileAttributes;
|
||||
int kind = a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR
|
||||
: PLAT_FS_FILE;
|
||||
int kind = fs_kind_of(&fd);
|
||||
if (!cb(name, kind, (uint64_t)fd.nFileSizeHigh << 32 | fd.nFileSizeLow, ctx)) break;
|
||||
} while (FindNextFileW(h, &fd));
|
||||
FindClose(h);
|
||||
|
||||
+29
-2
@@ -36,6 +36,7 @@
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define BASE_W 1920
|
||||
#define BASE_H 1080
|
||||
@@ -55,6 +56,22 @@ static struct {
|
||||
double dlss_k; // TOTK_DLSS quality/balanced/performance: the scene's share of the output, else 0
|
||||
} R;
|
||||
|
||||
// The scene size is decided before the device exists, so NGX cannot be asked yet. NGX loads DLSS from nvngx_dlss.dll
|
||||
// next to totk.exe (vk_dlaa.c vk_dlaa_init): without that file DLSS never runs, and a DLSS mode would leave the game's
|
||||
// plain scaling at 50..67 % of the output. Off Windows vk_dlaa.c is built as no-op stubs, so there is no DLSS at all.
|
||||
static int dlss_dll(void) {
|
||||
#ifdef _WIN32
|
||||
char exe[1024], path[1024 + 32];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return 1;
|
||||
char *last = plat_last_sep(exe);
|
||||
if (!last) return 1;
|
||||
snprintf(path, sizeof path, "%.*s/nvngx_dlss.dll", (int)(last - exe), exe);
|
||||
return plat_file_exists(path);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void decide(void) {
|
||||
if (R.decided) return;
|
||||
R.decided = 1;
|
||||
@@ -71,11 +88,21 @@ static void decide(void) {
|
||||
}
|
||||
const char *s = cfg_value("TOTK_RENDER_SCALE");
|
||||
double k = 5.0 / 6.0;
|
||||
const char *m = cfg_value("TOTK_DLSS"); // a DLSS mode other than dlaa sets the scene size
|
||||
// a DLSS mode other than dlaa sets the scene size, only while DLSS/DLAA is on: switched off, the mode the menu
|
||||
// still holds would shrink the scene for the plain upscaler
|
||||
const char *m = cfg_value("TOTK_DLSS");
|
||||
static const struct { const char *name; double k; } dlss[] = {
|
||||
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}};
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int i = 0; cfg_bool("TOTK_DLAA") && i < 3; i++)
|
||||
if (!strcmp(m, dlss[i].name)) R.dlss_k = dlss[i].k;
|
||||
if (R.dlss_k && !dlss_dll()) {
|
||||
#ifdef _WIN32
|
||||
rt_log("resolution: DLSS %s needs nvngx_dlss.dll next to totk.exe, which is missing: the scene follows render_scale", m);
|
||||
#else
|
||||
rt_log("resolution: DLSS %s runs on Windows only: the scene follows render_scale", m);
|
||||
#endif
|
||||
R.dlss_k = 0;
|
||||
}
|
||||
if (R.dlss_k) {
|
||||
k = R.dlss_k;
|
||||
R.scene_set = 1;
|
||||
|
||||
+58
-11
@@ -59,16 +59,60 @@ SN T FN(nan3)(Cpu *c, T a, T b, T d) {
|
||||
return FN(fv)(DNAN);
|
||||
}
|
||||
|
||||
// ---- FMA flavours. FMA_S / FMA_V: fast paths, host MXCSR is round-to-nearest there (simd_directed() is checked first).
|
||||
// FMA_X: scalar FMA for the slow paths, any host rounding mode.
|
||||
// Without hardware FMA (Ivy Bridge, no -mfma) clang turns __builtin_elementwise_fma into a call to the C runtime's
|
||||
// fmaf/fma for every lane, which is very slow and keeps the thread outside generated code. For float the product of
|
||||
// two floats is exact in a double (24 x 24 = 48 bits), so the fused op is done in double. A plain
|
||||
// (float)(x * y + z) rounds twice and can differ by one ulp; rounding the double sum to odd first (Boldo-Melquiond)
|
||||
// makes the final float rounding exact: TwoSum gives the error e of s = fl(p + z), and when e != 0 and s has an even
|
||||
// last bit, s is moved one ulp toward the exact value (the odd neighbour). Inf/NaN lanes are left alone (e is NaN
|
||||
// there, so the ordered compares below are false). Double has no wider type: it keeps the C runtime's fma.
|
||||
#if defined(__FMA__) || defined(__aarch64__) || NB == 64
|
||||
#define FMA_S(x, y, z) __builtin_elementwise_fma(x, y, z)
|
||||
#define FMA_V(x, y, z) __builtin_elementwise_fma(x, y, z)
|
||||
#define FMA_X(x, y, z) __builtin_elementwise_fma(x, y, z)
|
||||
#else
|
||||
typedef double simd_vf64x4 __attribute__((vector_size(32)));
|
||||
typedef uint64_t simd_vu64x4 __attribute__((vector_size(32)));
|
||||
SI float fma_rn_s(float x, float y, float z) {
|
||||
double p = (double)x * (double)y, zd = (double)z, s = p + zd;
|
||||
double bb = s - p, e = (p - (s - bb)) + (zd - bb); // TwoSum: s + e == p + zd exactly
|
||||
uint64_t u;
|
||||
memcpy(&u, &s, sizeof u);
|
||||
if (((e < 0.0) | (e > 0.0)) && !(u & 1)) u += (e > 0.0) == (s > 0.0) ? 1 : (uint64_t)-1;
|
||||
memcpy(&s, &u, sizeof s);
|
||||
return (float)s;
|
||||
}
|
||||
SI vf32 fma_rn_v(vf32 x, vf32 y, vf32 z) {
|
||||
simd_vf64x4 p = CVT(x, simd_vf64x4) * CVT(y, simd_vf64x4), zd = CVT(z, simd_vf64x4), s = p + zd;
|
||||
simd_vf64x4 bb = s - p, e = (p - (s - bb)) + (zd - bb);
|
||||
simd_vu64x4 u = (simd_vu64x4)s;
|
||||
simd_vu64x4 fix = (simd_vu64x4)((e < 0.0) | (e > 0.0)) & (simd_vu64x4)((u & 1) == 0);
|
||||
simd_vu64x4 up = (simd_vu64x4)((e > 0.0) == (s > 0.0));
|
||||
u += fix & ((up & 1) | ~up); // +1 toward the exact value, i.e. +1 if e and s share a sign, else -1
|
||||
return CVT((simd_vf64x4)u, vf32);
|
||||
}
|
||||
// Any rounding mode: directed modes are exact with a plain double (same-direction double rounding is harmless).
|
||||
SI float fma_any_s(float x, float y, float z) {
|
||||
if (__builtin_expect(_mm_getcsr() & 0x6000u, 0)) return (float)((double)x * (double)y + (double)z);
|
||||
return fma_rn_s(x, y, z);
|
||||
}
|
||||
#define FMA_S(x, y, z) fma_rn_s(x, y, z)
|
||||
#define FMA_V(x, y, z) fma_rn_v(x, y, z)
|
||||
#define FMA_X(x, y, z) fma_any_s(x, y, z)
|
||||
#endif
|
||||
|
||||
// The arithmetic itself, for scalars (KK = K) and vectors (KK = VK). MADD: a + b*d (a = addend).
|
||||
// RECPS/RSQRTS get their first operand already negated (FPNeg happens before NaN processing).
|
||||
#define FCALC(op, a, b, d, KK) \
|
||||
#define FCALC(op, a, b, d, KK, FMA) \
|
||||
((op) == FOP_ADD ? (a) + (b) : (op) == FOP_SUB ? (a) - (b) \
|
||||
: (op) == FOP_MUL || (op) == FOP_MULX ? (a) * (b) : (op) == FOP_NMUL ? -((a) * (b)) \
|
||||
: (op) == FOP_DIV ? (a) / (b) : (op) == FOP_ABD ? __builtin_elementwise_abs((a) - (b)) \
|
||||
: (op) == FOP_SQRT ? __builtin_elementwise_sqrt(a) \
|
||||
: (op) == FOP_MADD ? __builtin_elementwise_fma(b, d, a) \
|
||||
: (op) == FOP_RECPS ? __builtin_elementwise_fma(a, b, KK(2)) \
|
||||
: (op) == FOP_RSQRTS ? __builtin_elementwise_fma(a, b, KK(3)) * KK(0.5) : (a))
|
||||
: (op) == FOP_MADD ? FMA(b, d, a) \
|
||||
: (op) == FOP_RECPS ? FMA(a, b, KK(2)) \
|
||||
: (op) == FOP_RSQRTS ? FMA(a, b, KK(3)) * KK(0.5) : (a))
|
||||
|
||||
// Lanes whose fast result needs fixing: NaN results (NaN operand or invalid op), RSQRTS overflow.
|
||||
#define FBAD(op, r) ((op) == FOP_RSQRTS ? ((r) != (r)) | (__builtin_elementwise_abs(r) == K(INFINITY)) : (r) != (r))
|
||||
@@ -76,9 +120,9 @@ SN T FN(nan3)(Cpu *c, T a, T b, T d) {
|
||||
// (3 + a*b) / 2 with one rounding (a already negated): halve a normal operand first, so a product beyond
|
||||
// the format's range (which the fast path rounds to inf, or to the largest normal when rounding down) is exact.
|
||||
SN T FN(rsqrts)(T a, T b) {
|
||||
if (__builtin_elementwise_abs(a) >= FN(fv)(2 * MINN)) return __builtin_elementwise_fma(a * K(0.5), b, K(1.5));
|
||||
if (__builtin_elementwise_abs(b) >= FN(fv)(2 * MINN)) return __builtin_elementwise_fma(a, b * K(0.5), K(1.5));
|
||||
return __builtin_elementwise_fma(a, b, K(3)) * K(0.5);
|
||||
if (__builtin_elementwise_abs(a) >= FN(fv)(2 * MINN)) return FMA_X(a * K(0.5), b, K(1.5));
|
||||
if (__builtin_elementwise_abs(b) >= FN(fv)(2 * MINN)) return FMA_X(a, b * K(0.5), K(1.5));
|
||||
return FMA_X(a, b, K(3)) * K(0.5);
|
||||
}
|
||||
|
||||
// ARM result for a lane flagged by FBAD (operands already flushed).
|
||||
@@ -136,13 +180,13 @@ SN T FN(slow)(Cpu *c, int op, T a, T b, T d) {
|
||||
if (FMINMAX(op)) return FN(minmax)(c, op, a, b);
|
||||
unsigned old = simd_rm_enter(fpcr);
|
||||
volatile T va = a, vb = b, vd = d;
|
||||
volatile T vr = op == FOP_RSQRTS ? FN(rsqrts)(va, vb) : FCALC(op, va, vb, vd, K);
|
||||
volatile T vr = op == FOP_RSQRTS ? FN(rsqrts)(va, vb) : FCALC(op, va, vb, vd, K, FMA_X);
|
||||
T r = vr;
|
||||
if (FBAD(op, r)) vr = FN(fix)(c, op, a, b, d), r = vr;
|
||||
if ((fpcr & FPCR_FZ) && (FN(fb)(r) & ~SGN) <= MINN && !FN(iszero)(FN(fb)(r))) {
|
||||
if ((FN(fb)(r) & ~SGN) == MINN) {
|
||||
simd_rm_rz();
|
||||
volatile T vz = FCALC(op, va, vb, vd, K);
|
||||
volatile T vz = FCALC(op, va, vb, vd, K, FMA_X);
|
||||
if ((FN(fb)(vz) & ~SGN) < MINN) r = FN(fv)(FN(fb)(r) & SGN);
|
||||
} else {
|
||||
r = FN(fv)(FN(fb)(r) & SGN);
|
||||
@@ -172,7 +216,7 @@ SI T FN(op)(Cpu *c, int op, T a, T b, T d) {
|
||||
if (c->fpcr & FPCR_FZ) a = FN(flush)(a), b = FN(flush)(b);
|
||||
return FN(minmax)(c, op, a, b);
|
||||
}
|
||||
T r = FCALC(op, a, b, d, K);
|
||||
T r = FCALC(op, a, b, d, K, FMA_S);
|
||||
if (__builtin_expect(op == FOP_RSQRTS ? FREDO(op, r) : !FOK(r), 0)) r = FN(redo)(c, op, a, b, d, r);
|
||||
return r;
|
||||
}
|
||||
@@ -207,7 +251,7 @@ SI VT FN(vop)(Cpu *c, int op, VT a, VT b, VT d) {
|
||||
r = (VT)FN(vsel)(a < b, x, FN(vsel)(a == b, x | y, y));
|
||||
return r;
|
||||
}
|
||||
r = FCALC(op, a, b, d, VK);
|
||||
r = FCALC(op, a, b, d, VK, FMA_V);
|
||||
if (__builtin_expect(op == FOP_RSQRTS ? __builtin_reduce_or(FREDO(op, r)) != 0 : __builtin_reduce_and(FOK(r)) == 0, 0))
|
||||
return FN(vslow)(c, op, a, b, d);
|
||||
return r;
|
||||
@@ -363,3 +407,6 @@ SI T FN(recpx)(Cpu *c, T a) {
|
||||
#undef MINN
|
||||
#undef VK
|
||||
#undef K
|
||||
#undef FMA_S
|
||||
#undef FMA_V
|
||||
#undef FMA_X
|
||||
+40
-11
@@ -17,6 +17,7 @@
|
||||
extern "C" {
|
||||
#include "amiibo.h"
|
||||
#include "cfg.h"
|
||||
#include "folder_pick.h"
|
||||
#include "platform/platform.h"
|
||||
#include "ui_text.h"
|
||||
}
|
||||
@@ -30,7 +31,6 @@ struct FileEntry {
|
||||
|
||||
static const char kDefaultAmiiboDir[] = "./amiibo";
|
||||
static std::string g_current_dir = kDefaultAmiiboDir;
|
||||
static char g_dir_input[512] = "./amiibo";
|
||||
static std::vector<FileEntry> g_entries;
|
||||
static int g_selected_entry = -1;
|
||||
// The last action: a catalog id and the file or folder it names, formatted when drawn (the menu language can change)
|
||||
@@ -61,10 +61,24 @@ static bool iequals(std::string_view a, std::string_view b) {
|
||||
|
||||
static void set_current_dir(const std::string &path) {
|
||||
g_current_dir = path;
|
||||
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
|
||||
cfg_set("TOTK_AMIIBO_DIR", g_current_dir.c_str());
|
||||
}
|
||||
|
||||
// The host's folder dialog (folder_pick.h) answers on its own thread; the page takes the answer when it draws.
|
||||
static PlatMutex g_pick_lock = PLAT_MUTEX_INIT;
|
||||
static bool g_pick_open;
|
||||
static bool g_pick_ready;
|
||||
static std::string g_pick_path;
|
||||
|
||||
static void on_folder_picked(const char *path, void *ctx) {
|
||||
(void)ctx;
|
||||
plat_mutex_lock(&g_pick_lock);
|
||||
g_pick_ready = path != NULL;
|
||||
if (path) g_pick_path = path;
|
||||
g_pick_open = false;
|
||||
plat_mutex_unlock(&g_pick_lock);
|
||||
}
|
||||
|
||||
static void refresh_dir(void) {
|
||||
g_entries.clear();
|
||||
g_selected_entry = -1;
|
||||
@@ -122,11 +136,21 @@ void tab_amiibo(float scale) {
|
||||
} else {
|
||||
g_current_dir = kDefaultAmiiboDir;
|
||||
}
|
||||
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
|
||||
refresh_dir();
|
||||
s_inited = true;
|
||||
}
|
||||
|
||||
plat_mutex_lock(&g_pick_lock);
|
||||
bool picking = g_pick_open;
|
||||
std::string picked;
|
||||
if (g_pick_ready) picked.swap(g_pick_path);
|
||||
g_pick_ready = false;
|
||||
plat_mutex_unlock(&g_pick_lock);
|
||||
if (!picked.empty()) {
|
||||
set_current_dir(picked);
|
||||
refresh_dir();
|
||||
}
|
||||
|
||||
// Two panes: Left (Directory Browser & Tag List), Right (Amiibo Tag Details & Controls)
|
||||
const float content_h = ImGui::GetContentRegionAvail().y - opt_bottom(); // Continue and Quit stay visible
|
||||
const float left_w = ImGui::GetContentRegionAvail().x * 0.58f;
|
||||
@@ -136,17 +160,17 @@ void tab_amiibo(float scale) {
|
||||
ImGui::BeginChild("##amiibo_explorer", ImVec2(left_w, content_h), true);
|
||||
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_DIR));
|
||||
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (ImGui::InputText("##amiibo_dir_input", g_dir_input, sizeof(g_dir_input), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
set_current_dir(g_dir_input);
|
||||
refresh_dir();
|
||||
}
|
||||
ImGui::TextWrapped("%s", g_current_dir.c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginDisabled(picking);
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_OPEN))) {
|
||||
set_current_dir(g_dir_input);
|
||||
refresh_dir();
|
||||
plat_mutex_lock(&g_pick_lock);
|
||||
g_pick_open = true;
|
||||
plat_mutex_unlock(&g_pick_lock);
|
||||
folder_pick(g_current_dir.c_str(), on_folder_picked, NULL);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_DEFAULT_DIR))) {
|
||||
set_current_dir(kDefaultAmiiboDir);
|
||||
@@ -161,6 +185,11 @@ void tab_amiibo(float scale) {
|
||||
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_PICK));
|
||||
|
||||
ImGui::BeginChild("##file_list", ImVec2(0, 0), true);
|
||||
if (g_status_id == UI_MENU_AMIIBO_NO_DIR) {
|
||||
char missing[512];
|
||||
snprintf(missing, sizeof missing, menu_text(UI_MENU_AMIIBO_NO_DIR), g_current_dir.c_str());
|
||||
ImGui::TextWrapped("%s", missing);
|
||||
}
|
||||
for (size_t i = 0; i < g_entries.size(); i++) {
|
||||
const auto &e = g_entries[i];
|
||||
std::string label = e.is_dir ? e.name + "/" : e.name;
|
||||
@@ -198,7 +227,7 @@ void tab_amiibo(float scale) {
|
||||
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_READER));
|
||||
ImGui::Separator();
|
||||
|
||||
AmiiboDeviceState state = amiibo_get_device_state();
|
||||
AmiiboDeviceState state = amiibo_get_reader_state();
|
||||
UiTextId state_id = UI_MENU_AMIIBO_STATE_IDLE;
|
||||
ImVec4 state_col = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
|
||||
switch (state) {
|
||||
|
||||
+9
-6
@@ -518,7 +518,7 @@ static void row_render_scale(void) {
|
||||
}
|
||||
|
||||
static void row_shadow_res(void) {
|
||||
static const char *const vals[] = {"game", "512", "768", "1024", "1536", "2048"};
|
||||
static const char *const vals[] = {"game", "256", "512", "768", "1024", "1536", "2048"};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
const char *names[N];
|
||||
names[0] = menu_text(UI_MENU_SHADOW_RES_GAME);
|
||||
@@ -824,8 +824,13 @@ static void tab_controls(float scale) {
|
||||
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
|
||||
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
|
||||
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
|
||||
// One row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With a gamepad connected each
|
||||
// row shows its stick's deflection now, before the dead zone: a drifting stick's resting value, to set it above.
|
||||
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
|
||||
row_walk_key();
|
||||
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
|
||||
// Analog sticks: one dead zone row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With
|
||||
// a gamepad connected each row shows its stick's deflection now, before the dead zone: a drifting stick's resting
|
||||
// value, to set it above.
|
||||
opt_group(UI_MENU_GROUP_STICKS);
|
||||
double now[2];
|
||||
bool pad = input_pad_stick_percent(now) != 0;
|
||||
char stick_note[2][64];
|
||||
@@ -836,9 +841,6 @@ static void tab_controls(float scale) {
|
||||
opt_slider("TOTK_RSTICK_DEADZONE", UI_MENU_RSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
|
||||
pad ? stick_note[1] : nullptr, "TOTK_STICK_DEADZONE");
|
||||
opt_slider("TOTK_STICK_SENS", UI_MENU_STICK_SENS, 0.1f, 3, UI_MENU_STICK_SENS_DESC, 0.05f, "%.2f");
|
||||
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
|
||||
row_walk_key();
|
||||
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
|
||||
binds_rows(scale);
|
||||
opt_end();
|
||||
binds_after_page();
|
||||
@@ -962,6 +964,7 @@ static void tab_advanced(float scale) {
|
||||
row_set_or_not("TOTK_TRACE", UI_MENU_TRACE, UI_MENU_TRACE_DESC, "perf");
|
||||
opt_toggle("TOTK_FLIGHT", UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC);
|
||||
opt_slider("TOTK_FLIGHT_SPIKE_MS", UI_MENU_FLIGHT_SPIKE_MS, 20, 1000, UI_MENU_FLIGHT_SPIKE_MS_DESC, 10, "%.0f");
|
||||
opt_choice("TOTK_PASS_CRUMBS", UI_MENU_PASS_CRUMBS, dyn, 3, UI_MENU_PASS_CRUMBS_DESC); // vk_crumbs.c pass marks
|
||||
static char rec_path[600]; // the recording goes beside the log
|
||||
if (!rec_path[0]) {
|
||||
char dir[512];
|
||||
|
||||
@@ -353,7 +353,10 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
|
||||
const char *set = cfg_get(key);
|
||||
const char *src = fallback && !(set && *set) ? fallback : key; // unset: show the value in effect, the fallback's
|
||||
const CfgMeta *m = cfg_meta(src);
|
||||
double v = cfg_double(src), def = m ? strtod(m->def, NULL) : lo;
|
||||
// A key without a table default of its own (the per-stick dead zones) defaults to its fallback: the "default" mark
|
||||
// follows the fallback's default, not 0, even once the key is set.
|
||||
if (fallback && m && !m->def[0]) m = cfg_meta(fallback);
|
||||
double v = cfg_double(src), def = m && m->def[0] ? strtod(m->def, NULL) : lo;
|
||||
char shown[48];
|
||||
snprintf(shown, sizeof shown, fmt, v);
|
||||
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note};
|
||||
@@ -367,7 +370,8 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
|
||||
char out[32];
|
||||
if (step >= 1) snprintf(out, sizeof out, "%d", (int)lround(n));
|
||||
else snprintf(out, sizeof out, "%g", round(n / step) * step);
|
||||
apply(key, out);
|
||||
// Landing on the fallback's value unsets the key (its line leaves totk.ini), so the row follows the fallback again.
|
||||
apply(key, fallback && fabs(n - cfg_double(fallback)) < step * 0.01 ? "" : out);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@ void opt_group_text(const char *title);
|
||||
void opt_text_str(const char *text, bool warn = false);
|
||||
// lo..hi in steps of step (left and right held for a second go ten times as fast); fmt formats the value ("%.0f %%").
|
||||
// note: a short line under the value in place of the "default" mark (NULL: none). fallback: the setting that applies
|
||||
// while key is unset; the row shows its value then, and a change sets key.
|
||||
// while key is unset; the row shows its value then, and a change sets key. A key with no table default of its own takes
|
||||
// the "default" mark from the fallback's default, and a step onto the fallback's value unsets key again.
|
||||
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step = 1,
|
||||
const char *fmt = "%g", const char *note = nullptr, const char *fallback = nullptr);
|
||||
|
||||
|
||||
+19
-4
@@ -74,7 +74,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_SHADOW_RES] = "Shadow map size",
|
||||
[UI_MENU_SHADOW_RES_DESC] = "Size of the sun's shadow map. The game's own size changes with the view, down to about 400 "
|
||||
"near the ground, so leaf shadows look blocky and jump; a fixed size keeps them still and "
|
||||
"sharper. Above 1024 the shadows cost more GPU time and video memory.",
|
||||
"sharper. Above 1024 the shadows cost more GPU time and video memory; 256 is coarser than the "
|
||||
"game's own and is for integrated GPUs short of memory bandwidth.",
|
||||
[UI_MENU_SHADOW_RES_GAME] = "Game (changes)",
|
||||
[UI_MENU_ANISO] = "Texture filtering",
|
||||
[UI_MENU_ANISO_DESC] = "Anisotropic filtering of surface textures: ground, roads and walls seen at a shallow angle stay sharp into the distance instead of blurring. Game: the level the game asks for, as on the Switch.",
|
||||
@@ -230,6 +231,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_RESTART_CONFIRM] = "Restart the game now? Progress since your last save is lost.",
|
||||
[UI_MENU_RESTART_YES] = "Restart",
|
||||
[UI_MENU_RESTART_SAVING] = "The game is saving: Restart is possible in a moment.",
|
||||
[UI_MENU_GROUP_STICKS] = "Analog sticks",
|
||||
[UI_MENU_LSTICK_DEADZONE] = "Left stick dead zone", [UI_MENU_RSTICK_DEADZONE] = "Right stick dead zone",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own: let go of the stick, read \"stick now\" and set the dead zone a few percent above it.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
|
||||
@@ -392,6 +394,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FLIGHT_DESC] = "Keeps a short history and saves it when a frame freezes.",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS] = "Freeze threshold (ms)",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Frame time that counts as a freeze.",
|
||||
[UI_MENU_PASS_CRUMBS] = "GPU pass marks",
|
||||
[UI_MENU_PASS_CRUMBS_DESC] = "After a GPU crash, the log names the render pass that was running. Automatic: AMD only. On: also NVIDIA, at a small cost.",
|
||||
[UI_MENU_INPUT_RECORD] = "Input recording",
|
||||
[UI_MENU_INPUT_RECORD_DESC] = "Writes your inputs to a file for bug reports.",
|
||||
[UI_MENU_OPEN_LOG] = "Open log folder", [UI_MENU_OPEN_LOG_DESC] = "Shows the folder with the log files.",
|
||||
@@ -502,7 +506,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_SHADOW_RES] = "Размер карты теней",
|
||||
[UI_MENU_SHADOW_RES_DESC] = "Размер карты теней от солнца. Собственный размер игры меняется вместе с видом, у земли до "
|
||||
"примерно 400, поэтому тени листвы выглядят квадратными и дёргаются; постоянный размер делает "
|
||||
"их ровными и чётче. Выше 1024 тени требуют больше времени GPU и видеопамяти.",
|
||||
"их ровными и чётче. Выше 1024 тени требуют больше времени GPU и видеопамяти; 256 грубее "
|
||||
"собственного размера игры и предназначен для встроенных GPU с нехваткой пропускной способности памяти.",
|
||||
[UI_MENU_SHADOW_RES_GAME] = "Как в игре (меняется)",
|
||||
[UI_MENU_ANISO] = "Фильтрация текстур",
|
||||
[UI_MENU_ANISO_DESC] = "Анизотропная фильтрация текстур поверхностей: земля, дороги и стены под острым углом остаются чёткими вдали, а не размываются. Игра: уровень, который запрашивает игра, как на Switch.",
|
||||
@@ -656,6 +661,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_RESTART_CONFIRM] = "Перезапустить игру сейчас? Прогресс после последнего сохранения будет потерян.",
|
||||
[UI_MENU_RESTART_YES] = "Перезапустить",
|
||||
[UI_MENU_RESTART_SAVING] = "Игра сохраняется: перезапуск будет доступен через мгновение.",
|
||||
[UI_MENU_GROUP_STICKS] = "Стики",
|
||||
[UI_MENU_LSTICK_DEADZONE] = "Мёртвая зона левого стика", [UI_MENU_RSTICK_DEADZONE] = "Мёртвая зона правого стика",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами: отпустите стик, посмотрите «стик сейчас» и поставьте мёртвую зону на несколько процентов выше.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
|
||||
@@ -824,6 +830,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FLIGHT_DESC] = "Хранит короткую историю и сохраняет её, когда кадр подвисает.",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS] = "Порог подвисания (мс)",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Время кадра, которое считается подвисанием.",
|
||||
[UI_MENU_PASS_CRUMBS] = "Метки проходов GPU",
|
||||
[UI_MENU_PASS_CRUMBS_DESC] = "После сбоя GPU журнал называет проход отрисовки, который выполнялся. Автоматически: только AMD. Вкл.: также NVIDIA, с небольшой нагрузкой.",
|
||||
[UI_MENU_INPUT_RECORD] = "Запись ввода",
|
||||
[UI_MENU_INPUT_RECORD_DESC] = "Пишет ваш ввод в файл для отчёта об ошибке.",
|
||||
[UI_MENU_OPEN_LOG] = "Открыть папку журналов", [UI_MENU_OPEN_LOG_DESC] = "Показывает папку с файлами журнала.",
|
||||
@@ -1006,6 +1014,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PAD_LAYOUT_AUTO] = "Automático", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A à direita)",
|
||||
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A embaixo)",
|
||||
[UI_MENU_PAD_LAYOUT_IN_USE] = "em uso: %s",
|
||||
[UI_MENU_GROUP_STICKS] = "Analógicos",
|
||||
[UI_MENU_LSTICK_DEADZONE] = "Zona morta do analógico esquerdo",
|
||||
[UI_MENU_RSTICK_DEADZONE] = "Zona morta do analógico direito",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "Quanto um analógico do controle precisa se mover antes que o jogo perceba. Aumente "
|
||||
@@ -1171,6 +1180,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FLIGHT_DESC] = "Guarda um histórico curto e o salva quando um quadro trava.",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS] = "Limite de travamento (ms)",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Tempo de quadro que conta como travamento.",
|
||||
[UI_MENU_PASS_CRUMBS] = "Marcas de passagem da GPU",
|
||||
[UI_MENU_PASS_CRUMBS_DESC] = "Após uma falha da GPU, o log indica a passagem de renderização que estava em execução. Automático: só AMD. Ligado: também NVIDIA, com um pequeno custo.",
|
||||
[UI_MENU_INPUT_RECORD] = "Gravação de comandos",
|
||||
[UI_MENU_INPUT_RECORD_DESC] = "Grava os seus comandos num arquivo para relatórios de bug.",
|
||||
[UI_MENU_OPEN_LOG] = "Abrir pasta de logs", [UI_MENU_OPEN_LOG_DESC] = "Mostra a pasta com os arquivos de log.",
|
||||
@@ -1204,7 +1215,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_SHADOW_RES_DESC] = "Tamanho do mapa de sombras do sol. O tamanho do jogo muda com a vista, até cerca de "
|
||||
"400 perto do chão, então as sombras das folhas ficam quadriculadas e pulam; um "
|
||||
"tamanho fixo as deixa paradas e mais nítidas. Acima de 1024 as sombras custam mais "
|
||||
"tempo de GPU e memória de vídeo.",
|
||||
"tempo de GPU e memória de vídeo; 256 é mais grosseiro que o do jogo e serve para GPUs "
|
||||
"integradas com pouca largura de banda de memória.",
|
||||
[UI_MENU_SHADOW_RES_GAME] = "Do jogo (varia)",
|
||||
[UI_MENU_ANISO] = "Filtragem de texturas",
|
||||
[UI_MENU_ANISO_DESC] = "Filtragem anisotrópica das texturas de superfície: chão, estradas e paredes vistos em "
|
||||
@@ -1505,6 +1517,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo(A 在右)",
|
||||
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox(A 在下)",
|
||||
[UI_MENU_PAD_LAYOUT_IN_USE] = "使用中:%s",
|
||||
[UI_MENU_GROUP_STICKS] = "摇杆",
|
||||
[UI_MENU_LSTICK_DEADZONE] = "左摇杆死区", [UI_MENU_RSTICK_DEADZONE] = "右摇杆死区",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "手柄摇杆要推动多远游戏才会响应。如果角色或视角会自己移动,"
|
||||
"请调高此值:松开摇杆,查看“当前摇杆”,把死区设得比它高几个百分点。",
|
||||
@@ -1751,6 +1764,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FLIGHT_DESC] = "保留一小段历史记录,在画面卡住时保存。",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS] = "卡顿阈值(ms)",
|
||||
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "算作卡顿的帧时间。",
|
||||
[UI_MENU_PASS_CRUMBS] = "GPU 渲染通道标记",
|
||||
[UI_MENU_PASS_CRUMBS_DESC] = "GPU 崩溃后,日志会写出当时正在运行的渲染通道。自动:仅 AMD。开:NVIDIA 也启用,有少量开销。",
|
||||
[UI_MENU_INPUT_RECORD] = "输入录制",
|
||||
[UI_MENU_INPUT_RECORD_DESC] = "把你的输入写入文件,用于反馈问题。",
|
||||
[UI_MENU_OPEN_LOG] = "打开日志文件夹",
|
||||
@@ -1803,7 +1818,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_OV_SCALE_BILINEAR] = "双线性",
|
||||
[UI_MENU_SHADOW_RES] = "阴影贴图尺寸",
|
||||
[UI_MENU_SHADOW_RES_DESC] = "太阳阴影贴图的尺寸。游戏自身的尺寸随视角变化,靠近地面时降到约 400,因此树叶阴影显得块状并且跳动;固定尺寸能让阴影稳定、更清晰。高于 1024 时阴影会占用更多 "
|
||||
"GPU 时间和显存。",
|
||||
"GPU 时间和显存;256 比游戏自身的尺寸更粗糙,适合显存带宽不足的集成显卡。",
|
||||
[UI_MENU_SHADOW_RES_GAME] = "游戏默认(会变化)",
|
||||
[UI_MENU_ANISO] = "纹理过滤",
|
||||
[UI_MENU_ANISO_DESC] = "表面纹理的各向异性过滤:以小角度看到的地面、道路和墙壁在远处依然清晰,而不会模糊。游戏:使用游戏请求的级别,与 Switch 上相同。",
|
||||
|
||||
+2
-1
@@ -103,7 +103,7 @@ typedef enum {
|
||||
UI_MENU_PROMPTS_OFF_MODS, UI_MENU_PROMPTS_OFF_PATCHES,
|
||||
// "Restart now" under the prompts while they change after restart, and its confirmation (Cancel: UI_MENU_RESET_NO)
|
||||
UI_MENU_RESTART, UI_MENU_RESTART_DESC, UI_MENU_RESTART_CONFIRM, UI_MENU_RESTART_YES, UI_MENU_RESTART_SAVING,
|
||||
UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
|
||||
UI_MENU_GROUP_STICKS, UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
|
||||
UI_MENU_STICK_DEADZONE_NOW, UI_MENU_STICK_SENS, UI_MENU_STICK_SENS_DESC,
|
||||
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
|
||||
UI_MENU_WALK_KEY, UI_MENU_WALK_KEY_DESC, UI_MENU_WALK_KEY_TAKEN, UI_MENU_WALK_SCALE, UI_MENU_WALK_SCALE_DESC,
|
||||
@@ -195,6 +195,7 @@ typedef enum {
|
||||
UI_MENU_TRACE, UI_MENU_TRACE_DESC,
|
||||
UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC,
|
||||
UI_MENU_FLIGHT_SPIKE_MS, UI_MENU_FLIGHT_SPIKE_MS_DESC,
|
||||
UI_MENU_PASS_CRUMBS, UI_MENU_PASS_CRUMBS_DESC,
|
||||
UI_MENU_INPUT_RECORD, UI_MENU_INPUT_RECORD_DESC,
|
||||
UI_MENU_OPEN_LOG, UI_MENU_OPEN_LOG_DESC,
|
||||
UI_MENU_COPY_INFO, UI_MENU_COPY_INFO_DESC, UI_MENU_COPY_INFO_DONE,
|
||||
|
||||
@@ -33,6 +33,7 @@ typedef struct VkCtx {
|
||||
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_buffer_marker,
|
||||
has_nv_checkpoints, // VK_NV_device_diagnostic_checkpoints (TOTK_PASS_CRUMBS=1, vk_crumbs.c)
|
||||
has_astc_ldr, // textureCompressionASTC_LDR (enabled when reported)
|
||||
has_calibrated, // VK_KHR/EXT_calibrated_timestamps (GPU idle split in the perf line)
|
||||
has_hdr_metadata; // VK_EXT_hdr_metadata (vk_hdr.c vk_hdr_metadata)
|
||||
@@ -87,6 +88,45 @@ void vk_crumbs_release(void);
|
||||
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n);
|
||||
void vk_crumbs_report(void); // logs the two codes and what they name
|
||||
|
||||
// ---- pass marks (vk_crumbs.c): the replay (vk_exec.c vk_exec_pass_begin / vk_exec_pass_end, vk_exec_draw.c
|
||||
// op_dispatch) marks where each render pass and dispatch starts and where it ends, so that a device loss names the
|
||||
// pass the GPU was in. A mark is a number; a ring of VK_PASS_MARKS records keeps what it was. With
|
||||
// VK_AMD_buffer_marker the GPU writes the number of the last mark it started (TOP_OF_PIPE, before vkCmdBeginRendering
|
||||
// or the dispatch) and of the last it finished (BOTTOM_OF_PIPE, after vkCmdEndRendering or the dispatch) into the
|
||||
// breadcrumb buffer; with VK_NV_device_diagnostic_checkpoints the same two points are checkpoints, read with
|
||||
// vkGetQueueCheckpointData(2)NV after the loss. TOTK_PASS_CRUMBS: unset, AMD buffer marks only; 1, also NVIDIA
|
||||
// checkpoints (the extension is enabled at device creation); 0, none.
|
||||
#define VK_PASS_MARKS 4096 // power of two
|
||||
enum { VK_MARK_PASS = 1, VK_MARK_DISPATCH };
|
||||
typedef struct {
|
||||
uint32_t id; // the mark this record belongs to (0: never written)
|
||||
uint8_t kind; // VK_MARK_*
|
||||
uint8_t ncolor; // colour attachments
|
||||
uint32_t step; // index of the pass, or of the dispatch, within its submission, from 0
|
||||
uint32_t w, h, d; // render area and layers; a dispatch: group counts (0 0 0: indirect)
|
||||
uint32_t draws; // draws recorded in the pass; UINT32_MAX until its end is recorded
|
||||
uint64_t fence; // the submission: the timeline value it signals
|
||||
VkFormat color[8], depth; // attachment formats (VK_FORMAT_UNDEFINED: none)
|
||||
const GfxShader *first[2], *last[2];// vertex and fragment shader of the first and last draw; a dispatch: first[0]
|
||||
} VkPassMark;
|
||||
// Pass marks wanted and possible (TOTK_PASS_CRUMBS and the extensions): set by vk_crumbs_init.
|
||||
extern int vk_pass_marks_on;
|
||||
// vk_device.c create_device, before vkCreateDevice: 1 when TOTK_PASS_CRUMBS asks for NVIDIA checkpoints.
|
||||
int vk_pass_marks_want_nv(void);
|
||||
// The next mark: records its start into cb (through the recorder thread when it owns cb) and returns its ring record,
|
||||
// id, kind and fence set, for the caller to fill. NULL when pass marks are off. Replay thread only.
|
||||
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence);
|
||||
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m); // records the end of mark m into cb
|
||||
// The recorder thread's side of both (vk_rec.c): the marker and checkpoint commands themselves.
|
||||
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end);
|
||||
// AMD only: the last mark the GPU started and the last it finished (0: none); 0 without buffer marks.
|
||||
int vk_pass_marks_read(uint32_t *started, uint32_t *finished);
|
||||
const VkPassMark *vk_pass_mark_record(uint32_t id); // the ring record of mark id; NULL once a later mark took its slot
|
||||
// The report's text for a started and a finished mark, three lines without a trailing newline: which marks (source:
|
||||
// the extension they came from), then each with its ring record. vk_pass_marks_report logs it; the probe checks it.
|
||||
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size);
|
||||
void vk_pass_marks_report(void); // vk_device_fault_report: the AMD words and the NVIDIA checkpoints, formatted
|
||||
|
||||
// ---- window and present (gfx_present.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
|
||||
// to the swapchain extent, into the acquired swapchain image and presents it. src is in layout, which is
|
||||
|
||||
@@ -315,6 +315,7 @@ static void replay_log(uint64_t frame, int total) {
|
||||
// vk_backend_shutdown.
|
||||
void gfx_exit_log(void) {
|
||||
static int32_t done;
|
||||
rec_check_log();
|
||||
if (gfx_replay_async() && r_started && !plat_xchg32(&done, 1)) replay_log(game_frames, 1);
|
||||
}
|
||||
|
||||
|
||||
+202
-4
@@ -4,19 +4,31 @@
|
||||
// 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.
|
||||
//
|
||||
// Pass marks (vk.h vk_pass_mark_begin) use words 2 and 3 of the same buffer: the replay marks the start and the end of
|
||||
// every render pass and dispatch, and a ring of records says what each mark was. The design follows Savage Wind
|
||||
// Recomp's GPU checkpoints (refs/SavageWindRecomp runtime/nvn/vulkan/exec.c ck_mark and vk_checkpoints_report: a mark
|
||||
// per pass and dispatch, numbers in a ring of 4096, text only at the device loss); the code is ours.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
|
||||
static PFN_vkCmdWriteBufferMarkerAMD write_marker;
|
||||
static VkBuffer buf;
|
||||
static VmaAllocation alloc;
|
||||
static volatile uint32_t *words;
|
||||
static volatile uint32_t *words; // 0, 1: crumbs; 2, 3: pass marks
|
||||
|
||||
void vk_crumbs_init(VkCtx *c) {
|
||||
if (!c->has_buffer_marker) return;
|
||||
// Pass marks (below)
|
||||
int vk_pass_marks_on;
|
||||
static int pm_amd; // marks into words 2 and 3
|
||||
static PFN_vkCmdSetCheckpointNV set_checkpoint;
|
||||
static void pass_marks_init(VkCtx *c);
|
||||
|
||||
static void buffer_init(VkCtx *c) {
|
||||
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};
|
||||
@@ -30,14 +42,20 @@ void vk_crumbs_init(VkCtx *c) {
|
||||
return;
|
||||
}
|
||||
words = info.pMappedData;
|
||||
words[0] = words[1] = 0;
|
||||
words[0] = words[1] = words[2] = words[3] = 0;
|
||||
rt_log("vk: GPU breadcrumbs on (VK_AMD_buffer_marker)");
|
||||
}
|
||||
|
||||
void vk_crumbs_init(VkCtx *c) {
|
||||
if (c->has_buffer_marker) buffer_init(c);
|
||||
pass_marks_init(c);
|
||||
}
|
||||
|
||||
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;
|
||||
vk_pass_marks_on = pm_amd = 0, set_checkpoint = NULL;
|
||||
}
|
||||
|
||||
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n) {
|
||||
@@ -65,3 +83,183 @@ void vk_crumbs_report(void) {
|
||||
describe(words[1], finished, sizeof finished);
|
||||
rt_log("vk: GPU breadcrumbs: the GPU last reached %s; everything before %s had finished", reached, finished);
|
||||
}
|
||||
|
||||
// ---- pass marks
|
||||
|
||||
// Cost per pass or dispatch: about twenty stores into the ring and two commands, one at the start and one at the end,
|
||||
// each a marker write or a checkpoint. Per draw nothing, except that the branch of vk_exec_draw.c prepare_draw that
|
||||
// records changed pipeline state also notes the shaders of a pass's first draw. A full barrier already precedes every
|
||||
// pass and dispatch (vk_exec.c vk_exec_pass_begin, vk_exec_draw.c op_dispatch), so the end-of-pipe write adds no wait.
|
||||
static VkPassMark ring[VK_PASS_MARKS];
|
||||
static uint32_t seq; // marks made; only the replay thread writes it
|
||||
|
||||
// TOTK_PASS_CRUMBS: unset, buffer marks where VK_AMD_buffer_marker exists; 1, also NVIDIA checkpoints; 0, neither.
|
||||
int vk_pass_marks_want_nv(void) {
|
||||
const char *e = cfg_get("TOTK_PASS_CRUMBS");
|
||||
return e && *e && strcmp(e, "0") != 0;
|
||||
}
|
||||
|
||||
static void pass_marks_init(VkCtx *c) {
|
||||
const char *e = cfg_get("TOTK_PASS_CRUMBS");
|
||||
int off = e && !strcmp(e, "0");
|
||||
set_checkpoint = c->has_nv_checkpoints
|
||||
? (PFN_vkCmdSetCheckpointNV)vkGetDeviceProcAddr(c->device, "vkCmdSetCheckpointNV") : NULL;
|
||||
pm_amd = !off && write_marker && words;
|
||||
vk_pass_marks_on = pm_amd || set_checkpoint;
|
||||
if (vk_pass_marks_on)
|
||||
rt_log("vk: pass marks on (%s%s%s; TOTK_PASS_CRUMBS=0 turns them off%s)", pm_amd ? "VK_AMD_buffer_marker" : "",
|
||||
pm_amd && set_checkpoint ? " and " : "", set_checkpoint ? "VK_NV_device_diagnostic_checkpoints" : "",
|
||||
set_checkpoint ? "" : ", =1 adds NVIDIA checkpoints where the driver has them");
|
||||
else
|
||||
rt_log("vk: pass marks off (%s)", off ? "TOTK_PASS_CRUMBS=0"
|
||||
: "no VK_AMD_buffer_marker; TOTK_PASS_CRUMBS=1 asks for NVIDIA checkpoints");
|
||||
}
|
||||
|
||||
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end) {
|
||||
if (pm_amd)
|
||||
write_marker(cb, end ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf,
|
||||
end ? 12 : 8, id);
|
||||
if (set_checkpoint) set_checkpoint(cb, (const void *)(uintptr_t)((uint64_t)id << 1 | (end != 0)));
|
||||
}
|
||||
|
||||
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence) {
|
||||
if (!vk_pass_marks_on || !cb) return NULL;
|
||||
uint32_t id = ++seq;
|
||||
if (!id) id = ++seq; // 0 means no mark
|
||||
VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
|
||||
m->id = id, m->kind = (uint8_t)kind, m->fence = fence, m->draws = UINT32_MAX;
|
||||
rec_pass_mark(cb, id, 0);
|
||||
return m;
|
||||
}
|
||||
|
||||
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m) {
|
||||
if (m && cb) rec_pass_mark(cb, m->id, 1);
|
||||
}
|
||||
|
||||
int vk_pass_marks_read(uint32_t *started, uint32_t *finished) {
|
||||
if (!pm_amd) return 0;
|
||||
*started = words[2], *finished = words[3];
|
||||
return 1;
|
||||
}
|
||||
|
||||
const VkPassMark *vk_pass_mark_record(uint32_t id) {
|
||||
const VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
|
||||
return id && m->id == id ? m : NULL;
|
||||
}
|
||||
|
||||
// The first 8 bytes of the shader's input id (gfx_shader.c), the start of its stored input's name <cache>/in/<id>.in.
|
||||
static unsigned long long shader_id(const GfxShader *s) {
|
||||
if (!s) return 0;
|
||||
const uint8_t *iid = gfx_shader_input_id(s);
|
||||
unsigned long long v = 0;
|
||||
for (int i = 0; i < 8; i++) v = v << 8 | iid[i];
|
||||
return v;
|
||||
}
|
||||
|
||||
static void describe_mark(uint32_t id, char *o, size_t size) {
|
||||
const VkPassMark *m = vk_pass_mark_record(id);
|
||||
if (!id) {
|
||||
snprintf(o, size, "nothing yet");
|
||||
return;
|
||||
}
|
||||
if (!m) {
|
||||
snprintf(o, size, "no record (a later mark took its slot)");
|
||||
return;
|
||||
}
|
||||
if (m->kind == VK_MARK_DISPATCH) {
|
||||
snprintf(o, size, "dispatch %u of submission %llu: %ux%ux%u groups%s, cs %016llx", m->step,
|
||||
(unsigned long long)m->fence, m->w, m->h, m->d, m->w ? "" : " (indirect)", shader_id(m->first[0]));
|
||||
return;
|
||||
}
|
||||
size_t n = (size_t)snprintf(o, size, "pass %u of submission %llu: %ux%u, %u layer%s, colour formats", m->step,
|
||||
(unsigned long long)m->fence, m->w, m->h, m->d, m->d == 1 ? "" : "s");
|
||||
for (int i = 0; i < m->ncolor && i < 8 && n < size; i++)
|
||||
n += (size_t)snprintf(o + n, size - n, "%s%d", i ? "," : " ", (int)m->color[i]);
|
||||
if (!m->ncolor && n < size) n += (size_t)snprintf(o + n, size - n, " none");
|
||||
if (n < size) n += (size_t)snprintf(o + n, size - n, ", depth format %d", (int)m->depth);
|
||||
if (n >= size) return;
|
||||
if (m->draws == UINT32_MAX)
|
||||
snprintf(o + n, size - n, ", still recording; first draw vs %016llx fs %016llx", shader_id(m->first[0]),
|
||||
shader_id(m->first[1]));
|
||||
else if (!m->draws)
|
||||
snprintf(o + n, size - n, ", no draws");
|
||||
else
|
||||
snprintf(o + n, size - n, ", %u draws; first vs %016llx fs %016llx, last vs %016llx fs %016llx", m->draws,
|
||||
shader_id(m->first[0]), shader_id(m->first[1]), shader_id(m->last[0]), shader_id(m->last[1]));
|
||||
}
|
||||
|
||||
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size) {
|
||||
char a[640], b[640];
|
||||
describe_mark(started, a, sizeof a);
|
||||
describe_mark(finished, b, sizeof b);
|
||||
snprintf(o, size,
|
||||
"vk: pass marks (%s, %u made): the GPU last started mark %u and last finished mark %u\n"
|
||||
"vk: last started %u: %s\n"
|
||||
"vk: last finished %u: %s",
|
||||
source, (unsigned)seq, (unsigned)started, (unsigned)finished, (unsigned)started, a, (unsigned)finished, b);
|
||||
}
|
||||
|
||||
static void log_lines(char *t) {
|
||||
for (char *line = t, *nl; line && *line; line = nl ? nl + 1 : NULL) {
|
||||
nl = strchr(line, '\n');
|
||||
if (nl) *nl = 0;
|
||||
rt_log("%s", line);
|
||||
}
|
||||
}
|
||||
|
||||
// vkGetQueueCheckpointData2NV, else vkGetQueueCheckpointDataNV, on the graphics queue (the replay's): one entry per
|
||||
// stage with the last checkpoint that stage executed. A checkpoint is the mark's id times 2, plus 1 at its end, so the
|
||||
// top-of-pipe entry names the last start or end the GPU reached, and the bottom-of-pipe entry the last one before which
|
||||
// everything had finished: at a start, that is the end of the mark before it.
|
||||
static void nv_report(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || !set_checkpoint) return;
|
||||
PFN_vkGetQueueCheckpointData2NV get2 =
|
||||
(PFN_vkGetQueueCheckpointData2NV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointData2NV");
|
||||
PFN_vkGetQueueCheckpointDataNV get1 =
|
||||
(PFN_vkGetQueueCheckpointDataNV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointDataNV");
|
||||
enum { MAXN = 32 };
|
||||
uint64_t stage[MAXN], marker[MAXN];
|
||||
uint32_t n = 0;
|
||||
if (get2) {
|
||||
VkCheckpointData2NV d[MAXN];
|
||||
get2(c->gfx_queue, &n, NULL);
|
||||
if (n > MAXN) n = MAXN;
|
||||
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointData2NV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_2_NV};
|
||||
get2(c->gfx_queue, &n, d);
|
||||
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
|
||||
} else if (get1) {
|
||||
VkCheckpointDataNV d[MAXN];
|
||||
get1(c->gfx_queue, &n, NULL);
|
||||
if (n > MAXN) n = MAXN;
|
||||
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointDataNV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV};
|
||||
get1(c->gfx_queue, &n, d);
|
||||
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
|
||||
} else {
|
||||
rt_log("vk: pass marks: no vkGetQueueCheckpointDataNV entry point");
|
||||
return;
|
||||
}
|
||||
uint32_t started = 0, finished = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
uint32_t id = (uint32_t)(marker[i] >> 1);
|
||||
int end = (int)(marker[i] & 1);
|
||||
rt_log("vk: checkpoint of stage %#llx: the %s of mark %u", (unsigned long long)stage[i], end ? "end" : "start", id);
|
||||
if (stage[i] == VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT) started = id;
|
||||
if (stage[i] == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT) finished = end ? id : id ? id - 1 : 0;
|
||||
}
|
||||
if (!n) rt_log("vk: pass marks: the driver reported no checkpoints");
|
||||
char t[2048];
|
||||
vk_pass_marks_format("VK_NV_device_diagnostic_checkpoints", started, finished, t, sizeof t);
|
||||
log_lines(t);
|
||||
}
|
||||
|
||||
void vk_pass_marks_report(void) {
|
||||
if (!vk_pass_marks_on) return;
|
||||
uint32_t started, finished;
|
||||
char t[2048];
|
||||
if (vk_pass_marks_read(&started, &finished)) {
|
||||
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, t, sizeof t);
|
||||
log_lines(t);
|
||||
}
|
||||
nv_report();
|
||||
}
|
||||
+10
-3
@@ -261,7 +261,7 @@ static int create_device(void) {
|
||||
CHAIN(fbl, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT);
|
||||
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
|
||||
}
|
||||
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the ones below
|
||||
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the seven below
|
||||
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 timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not.
|
||||
if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
|
||||
@@ -272,6 +272,12 @@ static int create_device(void) {
|
||||
}
|
||||
// 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; }
|
||||
// Pass marks on NVIDIA (vk_crumbs.c): checkpoints only with TOTK_PASS_CRUMBS=1, since NVIDIA documents a cost for
|
||||
// the extension. NVIDIA's drivers report it, AMD's do not.
|
||||
if (vk_pass_marks_want_nv() && has_ext(ep, ne, VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME)) {
|
||||
g_vk.has_nv_checkpoints = 1;
|
||||
ext[n_ext++] = VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_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)) {
|
||||
@@ -430,6 +436,7 @@ void vk_device_fault_report(VkResult r) {
|
||||
if (done) { plat_mutex_unlock(&lock); return; }
|
||||
done = 1;
|
||||
vk_crumbs_report();
|
||||
vk_pass_marks_report();
|
||||
VkCtx *c = vk_ctx();
|
||||
PFN_vkGetDeviceFaultInfoEXT get =
|
||||
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
|
||||
@@ -500,11 +507,11 @@ int probe_vkdevice(void) {
|
||||
c->device_index, c->props.deviceName, VK_API_VERSION_MAJOR(v), VK_API_VERSION_MINOR(v), VK_API_VERSION_PATCH(v),
|
||||
c->gfx_family, c->xfer_family, c->xfer_shared ? " (shared queue)" : "", c->min_imported_host_alignment);
|
||||
printf("probe-vkdevice: optional: custom_border=%d eds=%d eds2=%d eds3=%d vertex_input=%d cond_render=%d xfb=%d provoking=%d "
|
||||
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d validation=%d\n",
|
||||
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d buffer_marker=%d nv_checkpoints=%d validation=%d\n",
|
||||
c->has_custom_border_color, c->has_eds, c->has_eds2, c->has_eds3, c->has_vertex_input_dynamic_state,
|
||||
c->has_conditional_rendering, c->has_transform_feedback, c->has_provoking_vertex, c->has_robustness2,
|
||||
c->has_device_fault, c->has_memory_budget, c->has_buffer_device_address, c->has_fragment_shader_interlock,
|
||||
c->validation);
|
||||
c->has_buffer_marker, c->has_nv_checkpoints, c->validation);
|
||||
vk_shutdown();
|
||||
printf("probe-vkdevice: %s (validation errors %ld)\n", g_errors_total ? "FAILED" : "passed", g_errors_total);
|
||||
return g_errors_total ? 1 : 0;
|
||||
|
||||
+33
-6
@@ -202,6 +202,8 @@ void vk_dlaa_init(void) {
|
||||
driver ? "; the driver needs an update" : "");
|
||||
}
|
||||
|
||||
int vk_dlaa_available(void) { return vk_dlaa_wanted() && N.available; }
|
||||
|
||||
// ---------------------------------------------------------------- where DLAA runs in TotK's frame
|
||||
// Measured (TOTK_DUMP, frame 2100 at Lookout Landing, 2560x1440 scene): the G-buffer passes draw into five targets with
|
||||
// the scene depth (d32f at the scene size), one of them the R11G11B10F light target; lighting and the transparents
|
||||
@@ -254,6 +256,8 @@ static struct {
|
||||
uint64_t mv_frames, mv_none;
|
||||
uint64_t why[3]; // history resets: scene size changed, no camera, scene target changed
|
||||
int on; // the dlaa setting, read at each present (the menu switches it live)
|
||||
int scaler_skipped; // this frame: the game's scaling draw over DLSS's picture was skipped
|
||||
uint64_t scaler_missed; // frames DLSS scaled whose scaling draw was not found (vk_dlaa_skip_draw)
|
||||
} D = {.mv_ok = -1};
|
||||
|
||||
static int dl_active(void) { return N.available && !D.failed && D.on; }
|
||||
@@ -624,7 +628,10 @@ void vk_dlaa_frame(void) {
|
||||
if (D.jit_used && !D.done) D.hold = 60, D.holds++; // jittered but not resolved: that frame shook
|
||||
else if (D.hold) D.hold--;
|
||||
if (!D.done) D.jit_ok = 0;
|
||||
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = 0;
|
||||
if (D.up && !D.scaler_skipped && ++D.scaler_missed == 1)
|
||||
rt_log("dlaa: the game's scaling draw over DLSS's picture was not found (its fragment shader is not in FXAA_FS): "
|
||||
"the jittered scene reaches the screen and shakes");
|
||||
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = D.scaler_skipped = 0;
|
||||
D.cam_addr = 0;
|
||||
}
|
||||
|
||||
@@ -638,8 +645,10 @@ void vk_dlaa_frame(void) {
|
||||
// scaled picture into the output-sized target, in a pass without the depth attachment the in-place FXAA has: there it
|
||||
// is always drawn, whatever fxaa says (skipping it would leave the 3D picture out), unless DLSS already wrote that
|
||||
// target this frame.
|
||||
// The stock translator's hash, and the one with tools/xlate patch 0010 (PCSS, PR #89) applied.
|
||||
static const uint64_t FXAA_FS[] = {0xf450003ebeb3dc52ull, 0x0a12f4bc2a9a4fbeull};
|
||||
// The stock translator's hash, the one with tools/xlate patch 0010 (PCSS, PR #89) applied, and the one with patches
|
||||
// 0010 and 0011 (the shadow filter spread) applied. A translator change that gives the shader another hash is reported
|
||||
// by vk_dlaa_frame: the game's scaling draw then covers DLSS's picture with the jittered scene, which shakes.
|
||||
static const uint64_t FXAA_FS[] = {0xf450003ebeb3dc52ull, 0x0a12f4bc2a9a4fbeull, 0x5c43ae18948d52cbull};
|
||||
|
||||
static uint64_t spirv_hash(const GfxShader *s) {
|
||||
static struct { const GfxShader *s; uint64_t h; } tab[8192];
|
||||
@@ -660,13 +669,30 @@ static uint64_t spirv_hash(const GfxShader *s) {
|
||||
int vk_dlaa_skip_draw(VkExec *ex, const GfxShader *fs) {
|
||||
static uint64_t frame_read = ~0ull;
|
||||
static int fxaa = 1;
|
||||
// fxaa = 0 reports once whether it found the game's FXAA draw: the first skip, or a minute of frames (3600) with
|
||||
// none, which is what a translator change that moves FXAA_FS looks like on every GPU, with or without DLSS
|
||||
static uint64_t fx_off_frames;
|
||||
static int fx_found, fx_reported;
|
||||
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
if (f != frame_read) frame_read = f, fxaa = cfg_bool("TOTK_FXAA"); // once a frame: the menu switches it live
|
||||
if (f != frame_read) { // once a frame: the menu switches it live
|
||||
frame_read = f, fxaa = cfg_bool("TOTK_FXAA");
|
||||
if (!fxaa && !fx_found && !fx_reported && ++fx_off_frames == 3600) {
|
||||
fx_reported = 1;
|
||||
rt_log("dlaa: fxaa = 0 found no draw of the game's FXAA in %llu frames (its fragment shader is not in "
|
||||
"FXAA_FS): the FXAA stays on", (unsigned long long)fx_off_frames);
|
||||
}
|
||||
}
|
||||
if (fxaa && !D.up) return 0; // nothing to skip: no shader lookup per draw at the defaults
|
||||
if (!fs) return 0;
|
||||
uint64_t h = spirv_hash(fs);
|
||||
if (h != FXAA_FS[0] && h != FXAA_FS[1]) return 0;
|
||||
if (!ex->depth.image) return D.up; // the scaling pass
|
||||
int known = 0;
|
||||
for (size_t i = 0; i < sizeof FXAA_FS / sizeof FXAA_FS[0]; i++) known |= h == FXAA_FS[i];
|
||||
if (!known) return 0;
|
||||
if (!ex->depth.image) return D.scaler_skipped = D.up; // the scaling pass
|
||||
if (!fxaa && !fx_found) {
|
||||
fx_found = 1;
|
||||
rt_log("dlaa: fxaa = 0 skips the game's FXAA (fragment shader %016llx)", (unsigned long long)h);
|
||||
}
|
||||
return !fxaa;
|
||||
}
|
||||
|
||||
@@ -680,6 +706,7 @@ void vk_dlaa_device_exts(const VkExtensionProperties *have, uint32_t nhave, cons
|
||||
(void)have, (void)nhave, (void)ext, (void)n, (void)max;
|
||||
}
|
||||
void vk_dlaa_init(void) {}
|
||||
int vk_dlaa_available(void) { return 0; }
|
||||
void vk_dlaa_pass(VkExec *ex) { (void)ex; }
|
||||
void vk_dlaa_frame(void) {}
|
||||
void vk_dlaa_draw(VkExec *ex) { (void)ex; }
|
||||
|
||||
@@ -8,6 +8,8 @@ int vk_dlaa_wanted(void); // the setting is on (the device may still lack DLSS
|
||||
void vk_dlaa_instance_exts(const char **ext, uint32_t *n, uint32_t max);
|
||||
void vk_dlaa_device_exts(const VkExtensionProperties *have, uint32_t nhave, const char **ext, uint32_t *n, uint32_t max);
|
||||
void vk_dlaa_init(void);
|
||||
// 1 when dlaa is on and NGX came up with DLSS (after vk_dlaa_init): a DLSS mode's smaller scene is scaled up by DLSS.
|
||||
int vk_dlaa_available(void);
|
||||
// vk_exec.c vk_exec_pass_begin, once the pass's targets are resolved and before its barrier: arms the frame on the
|
||||
// scene's light target or, at the first quarter-width pass after it, runs DLAA on that target. Each present ends the
|
||||
// frame (vk_dlaa_frame).
|
||||
|
||||
@@ -612,6 +612,20 @@ void vk_exec_pass_begin(VkExec *ex) {
|
||||
if (d->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) da = a;
|
||||
if (d->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) sa = a;
|
||||
}
|
||||
// The start mark goes between the barrier and vkCmdBeginRendering, its end right after vkCmdEndRendering
|
||||
// (vk_exec_pass_end): outside the pass, and without breaking it.
|
||||
VkPassMark *m = ex->mark = vk_pass_mark_begin(ex->cb, VK_MARK_PASS, ex->fence);
|
||||
if (m) {
|
||||
m->step = ex->mark_passes++;
|
||||
m->w = ex->pass_w, m->h = ex->pass_h, m->d = ex->pass_layers;
|
||||
m->ncolor = (uint8_t)(ex->pass_ncolor < 8 ? ex->pass_ncolor : 8);
|
||||
for (int i = 0; i < m->ncolor; i++) m->color[i] = ex->pass_color[i];
|
||||
m->depth = ex->pass_depth ? ex->pass_depth : ex->pass_stencil;
|
||||
// The shaders bound now are the first draw's when it repeats the pipeline state; when it does not,
|
||||
// vk_exec_draw.c prepare_draw replaces them as it records the new state.
|
||||
vk_exec_draw_shaders(&m->first[0], &m->first[1]);
|
||||
ex->mark_draws0 = vk_exec_frame.draws;
|
||||
}
|
||||
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, {ex->pass_w, ex->pass_h}},
|
||||
.layerCount = ex->pass_layers, .colorAttachmentCount = ex->pass_ncolor,
|
||||
.pColorAttachments = ca,
|
||||
@@ -629,6 +643,14 @@ void vk_exec_pass_end(VkExec *ex) {
|
||||
vkCmdEndRendering(ex->cb);
|
||||
ex->recorded++;
|
||||
ex->in_pass = 0;
|
||||
if (ex->mark) {
|
||||
// vk_exec_frame_log resets the frame counters at a present (TOTK_TRACE=gfx): then only the draws since it count
|
||||
uint64_t d = vk_exec_frame.draws, d0 = ex->mark_draws0;
|
||||
ex->mark->draws = (uint32_t)(d >= d0 ? d - d0 : d);
|
||||
if (ex->mark->draws) vk_exec_draw_shaders(&ex->mark->last[0], &ex->mark->last[1]);
|
||||
vk_pass_mark_end(ex->cb, ex->mark);
|
||||
ex->mark = NULL;
|
||||
}
|
||||
for (int i = 0; i < ex->pass_ncolor && i < 8; i++) // stand-ins of 3D BC targets go to their textures
|
||||
if (ex->color[i].image && ex->color[i].img && ex->color[i].img->image != ex->color[i].image)
|
||||
bc_copy_back(ex, &ex->color[i]);
|
||||
@@ -901,6 +923,8 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
|
||||
ex->fence = fence;
|
||||
ex->recorded = 0;
|
||||
ex->in_pass = 0;
|
||||
ex->mark = NULL;
|
||||
ex->mark_passes = ex->mark_dispatches = 0;
|
||||
ex->dirty = VK_EXEC_DIRTY_ALL;
|
||||
ex->rt_resolved = 0; // images may have changed since the last command buffer
|
||||
ex->nviews = 0;
|
||||
|
||||
@@ -47,6 +47,11 @@ typedef struct VkExec {
|
||||
int recorded; // Vulkan commands recorded so far; every handler adds the ones it records
|
||||
int in_pass; // inside vkCmdBeginRendering
|
||||
uint32_t dirty; // VK_EXEC_DIRTY_*
|
||||
// Pass marks (vk.h vk_pass_mark_begin): the open pass's record (NULL: none or marks off), vk_exec_frame.draws when
|
||||
// it began, and the passes and dispatches this submission marked so far.
|
||||
VkPassMark *mark;
|
||||
uint64_t mark_draws0;
|
||||
uint32_t mark_passes, mark_dispatches;
|
||||
|
||||
// ---- render targets (OP_SET_RT) and the current pass
|
||||
p_set_rt rt; // last OP_SET_RT payload (a change ends the open pass); rt.n 0, no depth: none
|
||||
@@ -126,6 +131,8 @@ void vk_exec_inline_end(int reopen);
|
||||
// vk_exec_draw.c: forget what the command buffer being recorded has bound (pipeline, sets, dynamic state, vertex
|
||||
// buffers): the replay continues in a new one (vk_sync_split).
|
||||
void vk_exec_draw_new_cb(void);
|
||||
// vk_exec_draw.c: the vertex and fragment shader of the last draw recorded into this command buffer (NULL: none yet).
|
||||
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs);
|
||||
VkBuffer vk_exec_inline_buffer(VkDeviceSize size);
|
||||
// vk_exec.c: the stand-in a 3D BC render target is drawn through goes with its image (vk_exec_image_gone).
|
||||
void vk_exec_bc_gone(VkImage image);
|
||||
|
||||
+28
-2
@@ -198,6 +198,21 @@ static void cb_done(void *arg) { // completion thread
|
||||
|
||||
void vk_exec_draw_new_cb(void) { memset(&B, 0, sizeof B); }
|
||||
|
||||
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs) {
|
||||
*vs = B.st_valid ? B.st.vs : NULL;
|
||||
*fs = B.st_valid ? B.st.fs : NULL;
|
||||
}
|
||||
|
||||
// Marks a dispatch about to be recorded (vk.h vk_pass_mark_begin); the caller ends it after the dispatch.
|
||||
static VkPassMark *dispatch_mark(VkExec *ex, const GfxShader *cs, uint32_t x, uint32_t y, uint32_t z) {
|
||||
VkPassMark *m = vk_pass_mark_begin(ex->cb, VK_MARK_DISPATCH, ex->fence);
|
||||
if (!m) return NULL;
|
||||
m->step = ex->mark_dispatches++;
|
||||
m->w = x, m->h = y, m->d = z, m->draws = 0, m->ncolor = 0;
|
||||
m->first[0] = cs, m->first[1] = m->last[0] = m->last[1] = NULL;
|
||||
return m;
|
||||
}
|
||||
|
||||
static CbRes *cb_res(VkExec *ex) {
|
||||
if (cur && cur_fence == ex->fence) return cur;
|
||||
cur = calloc(1, sizeof *cur);
|
||||
@@ -234,8 +249,13 @@ static uint8_t *scratch(VkExec *ex, uint64_t size, VkBuffer *buf, VkDeviceSize *
|
||||
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
// HOST_COHERENT: the CPU fills these chunks and never flushes them. Intel's integrated GPUs (ANV, Meteor Lake)
|
||||
// offer host-cached memory only without coherence, which VMA picks for random host access: the GPU then read
|
||||
// what an earlier submission left in the chunk (an UpdateUniformBuffer payload of another draw: a lighting
|
||||
// shader's step count of 0x3f800000 instead of 128, and a loop of a billion steps).
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
||||
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
|
||||
.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;
|
||||
VkResult res = vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &c->buf, &c->alloc, &info);
|
||||
if (res != VK_SUCCESS) rt_fatal("vk: transient buffer of %llu bytes failed (%d)", (unsigned long long)n, (int)res);
|
||||
@@ -351,7 +371,8 @@ static void dummies_init(void) {
|
||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
||||
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
|
||||
.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}; // the memset below is not flushed
|
||||
VmaAllocationInfo info;
|
||||
if (vmaCreateBuffer(c->vma, &bi, &ai, &Z.zbuf, &Z.zalloc, &info) != VK_SUCCESS) rt_fatal("vk: zero buffer failed");
|
||||
memset(info.pMappedData, 0, ZERO_BYTES);
|
||||
@@ -2161,6 +2182,7 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
|
||||
vk_pipeline_set_dynamic(ex->cb, &s);
|
||||
ex->recorded++;
|
||||
B.st = s, B.st_valid = 1;
|
||||
if (ex->mark && vk_exec_frame.draws == ex->mark_draws0) ex->mark->first[0] = vs, ex->mark->first[1] = fs;
|
||||
}
|
||||
vk_dlaa_draw(ex);
|
||||
record_fixed(ex);
|
||||
@@ -2312,7 +2334,9 @@ static void op_dispatch(VkExec *ex, const p_dispatch *d) {
|
||||
twin_dispatch = 1;
|
||||
bind_resources(ex, info);
|
||||
twin_dispatch = 0;
|
||||
VkPassMark *m = dispatch_mark(ex, cs, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
|
||||
vkCmdDispatch(ex->cb, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
|
||||
vk_pass_mark_end(ex->cb, m);
|
||||
ex->recorded++;
|
||||
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, d);
|
||||
}
|
||||
@@ -2347,7 +2371,9 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
|
||||
vk_exec_barrier(ex);
|
||||
B.c0_valid[2] = 0; // the range no longer holds B.c0[2]
|
||||
}
|
||||
VkPassMark *m = dispatch_mark(ex, cs, 0, 0, 0);
|
||||
vkCmdDispatchIndirect(ex->cb, ip.buffer, ip.offset);
|
||||
vk_pass_mark_end(ex->cb, m);
|
||||
ex->recorded++;
|
||||
vk_mem_count(ip.cls, VK_MUSE_IND, sizeof(VkDispatchIndirectCommand));
|
||||
VK_MEM_BOUND(ip);
|
||||
|
||||
+107
-2
@@ -3,7 +3,8 @@
|
||||
// the completion thread; a guest-memory word a callback writes is visible once fence_wait returns; finish drains every
|
||||
// submission; a present of a cleared vk_texture.c image (layout GENERAL) reads back exactly through the swapchain
|
||||
// capture. Clear part: a hand-built op stream (SET_RT to an RGBA8 and a D32 texture, whole-target clears,
|
||||
// then scissored clears) through submit reads back exactly with read_texels.
|
||||
// then scissored clears) through submit reads back exactly with read_texels. Pass marks part (test_marks): the GPU's
|
||||
// buffer marks and the ring records name the last pass of a submission, and the device-lost report text says so.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -175,7 +176,7 @@ static void init_tex(tex_t *t, pool_t *pool, int64_t off, int format) {
|
||||
t->d.target = NVN_TEXTURE_TARGET_2D, t->d.format = format, t->d.width = TEX_W, t->d.height = TEX_H;
|
||||
t->d.depth = 1, t->d.levels = 1;
|
||||
nvn_tex_layout(&t->d, &t->L);
|
||||
t->pool = pool, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
|
||||
t->pool = pool, t->pool_flags = (uint16_t)pool->flags, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
|
||||
}
|
||||
|
||||
// Pixel of the RGBA8 target after the stream: the scissored second clear inside the rectangle, the first elsewhere.
|
||||
@@ -386,6 +387,74 @@ static uint8_t *draw_prefix(uint8_t *q, prog_t *prog, const pool_t *pool, tex_t
|
||||
return q;
|
||||
}
|
||||
|
||||
// ---- pass marks part (vk_crumbs.c): one submission with a pass of 2 draws into the draw part's target, a dispatch and
|
||||
// a pass of 3 draws into a second target. Once its fence is done, the buffer marks must name the last pass as both
|
||||
// started and finished, the ring records must hold what the three were, and vk_pass_marks_format (the text
|
||||
// vk_device_fault_report logs) must name it. Without VK_AMD_buffer_marker or with TOTK_PASS_CRUMBS=0 it is skipped:
|
||||
// NVIDIA's checkpoints can only be read once the device is lost.
|
||||
static char marks_text[2048];
|
||||
static int marks_skipped;
|
||||
|
||||
static int test_marks(const GfxBackend *b, pool_t *pool, tex_t *color, prog_t *gfx, prog_t *cs) {
|
||||
uint32_t s0, f0;
|
||||
if (!vk_pass_marks_read(&s0, &f0)) {
|
||||
marks_skipped = 1;
|
||||
return 0;
|
||||
}
|
||||
static tex_t second;
|
||||
init_tex(&second, pool, 0x40000, NVN_FORMAT_RGBA8);
|
||||
CHECK(0x40000 + second.L.size <= 0x80000, "pass-marks target exceeds its pool slot");
|
||||
static _Alignas(8) uint8_t stream[8192];
|
||||
uint8_t *q = draw_prefix(stream, gfx, pool, color);
|
||||
const p_draw d = {NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 3, 0, 0};
|
||||
for (int i = 0; i < 2; i++) q = put(q, OP_DRAW, &d, sizeof d);
|
||||
const p_bind_program bc = {cs, 1 << 5};
|
||||
q = put(q, OP_BIND_PROGRAM, &bc, sizeof bc);
|
||||
const p_dispatch dp = {2, 1, 1, 0};
|
||||
q = put(q, OP_DISPATCH, &dp, sizeof dp);
|
||||
const p_bind_program bg = {gfx, 3};
|
||||
q = put(q, OP_BIND_PROGRAM, &bg, sizeof bg);
|
||||
p_set_rt rt;
|
||||
memset(&rt, 0, sizeof rt);
|
||||
rt.n = 1, rt.color[0] = &second;
|
||||
q = put(q, OP_SET_RT, &rt, sizeof rt);
|
||||
for (int i = 0; i < 3; i++) q = put(q, OP_DRAW, &d, sizeof d);
|
||||
q = put(q, OP_END, NULL, 0);
|
||||
CHECK(q <= stream + sizeof stream, "pass-marks op stream overflow");
|
||||
uint64_t handle = (uint64_t)(uintptr_t)stream;
|
||||
uint64_t f = b->submit(&handle, 1);
|
||||
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the pass-marks submission timed out");
|
||||
uint32_t started = 0, finished = 0;
|
||||
vk_pass_marks_read(&started, &finished);
|
||||
CHECK(started && started == finished, "after the fence the GPU last started mark %u and finished mark %u (before "
|
||||
"the submission: %u and %u)", started, finished, s0, f0);
|
||||
const VkPassMark *p1 = vk_pass_mark_record(finished), *dm = vk_pass_mark_record(finished - 1),
|
||||
*p0 = vk_pass_mark_record(finished - 2);
|
||||
CHECK(p1 && p1->kind == VK_MARK_PASS && p1->fence == f && p1->step == 1,
|
||||
"the last finished mark %u is not pass 1 of submission %llu", finished, (unsigned long long)f);
|
||||
CHECK(p1->w == TEX_W && p1->h == TEX_H && p1->d == 1 && p1->ncolor == 1 &&
|
||||
p1->color[0] == VK_FORMAT_R8G8B8A8_UNORM && p1->depth == VK_FORMAT_UNDEFINED,
|
||||
"pass 1 has %ux%u, %u layers, %u colour targets, format %d, depth %d", p1->w, p1->h, p1->d, p1->ncolor,
|
||||
(int)p1->color[0], (int)p1->depth);
|
||||
CHECK(p1->draws == 3 && p1->first[0] && p1->first[1] && p1->first[0] == p1->last[0] && p1->first[1] == p1->last[1],
|
||||
"pass 1 has %u draws (3 expected) and shaders %p %p ... %p %p", p1->draws, (const void *)p1->first[0],
|
||||
(const void *)p1->first[1], (const void *)p1->last[0], (const void *)p1->last[1]);
|
||||
CHECK(dm && dm->kind == VK_MARK_DISPATCH && dm->fence == f && dm->step == 0 && dm->w == 2 && dm->h == 1 &&
|
||||
dm->d == 1 && dm->first[0],
|
||||
"the mark before pass 1 is not dispatch 0 of 2x1x1 groups of submission %llu", (unsigned long long)f);
|
||||
CHECK(p0 && p0->kind == VK_MARK_PASS && p0->fence == f && p0->step == 0 && p0->draws == 2 &&
|
||||
p0->first[0] == p1->first[0],
|
||||
"the mark before the dispatch is not pass 0 of submission %llu with 2 draws", (unsigned long long)f);
|
||||
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, marks_text, sizeof marks_text);
|
||||
char want[128];
|
||||
snprintf(want, sizeof want, "last finished %u: pass 1 of submission %llu: %ux%u, 1 layer, colour formats %d, ",
|
||||
finished, (unsigned long long)f, TEX_W, TEX_H, (int)VK_FORMAT_R8G8B8A8_UNORM);
|
||||
CHECK(strstr(marks_text, want) && strstr(marks_text, ", 3 draws; first vs "),
|
||||
"the report text lacks \"%s\" or the draw count:\n%s", want, marks_text);
|
||||
b->texture_release(&second);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_tries, int *n_cs) {
|
||||
char root[PLAT_PATH_MAX * 2], path[PLAT_PATH_MAX * 2 + 64];
|
||||
CHECK(probe_census_dir(root, sizeof root), "no build/v%d/shaders/jobs.json above the exe (run tools/shader_census.py)",
|
||||
@@ -503,10 +572,12 @@ static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_t
|
||||
|
||||
// Compute: a few census compute shaders, one dispatch each, resources unbound (zero buffers and stand-in images).
|
||||
int dispatched = 0;
|
||||
prog_t *cs_prog = NULL;
|
||||
for (int t = 0; t < 4 && t < nc; t++) {
|
||||
const DrawEnt *one[1] = {&e[cs[(long long)t * nc / (nc < 4 ? nc : 4)]]};
|
||||
prog_t *prog = dr_prog(root, one, 1);
|
||||
CHECK(prog, "census code file of %.16s is missing", one[0]->key);
|
||||
cs_prog = prog;
|
||||
uint8_t *q = stream;
|
||||
q = put(q, OP_BEGIN, NULL, 0);
|
||||
const p_bind_program bp = {prog, 1 << 5};
|
||||
@@ -530,6 +601,7 @@ static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_t
|
||||
dispatched++;
|
||||
ind_dispatches++;
|
||||
}
|
||||
if (test_marks(b, &pool, &color, hit, cs_prog)) return 1;
|
||||
b->texture_release(&color);
|
||||
b->pool_fini(&pool);
|
||||
free(pairs), free(cs), free(e), free(jobs);
|
||||
@@ -845,6 +917,31 @@ static int test_mirror(const GfxBackend *b, uint8_t *guest, int *bytes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A pool of zero bytes (TotK makes some now and then; MacNativeGames hit one on 1.2.1) initialises as an empty pool: no
|
||||
// Vulkan buffer or memory (a buffer or import of 0 bytes is invalid usage) and no pool index entry, so a lookup at its
|
||||
// address still finds the pool that starts there, before and after the empty pool's Finalize.
|
||||
static int test_empty_pool(const GfxBackend *b, uint8_t *guest) {
|
||||
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0x100000,
|
||||
.game_mem = guest + 0xE00000};
|
||||
pool_t empty = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0,
|
||||
.game_mem = guest + 0xE00000};
|
||||
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the empty-pool part");
|
||||
long errors = vk_validation_errors_total();
|
||||
CHECK(b->pool_init(&empty), "pool_init of a zero-byte pool");
|
||||
uint64_t a = (uint64_t)(uintptr_t)pool.mem;
|
||||
VkMemSpan whole, at, after;
|
||||
CHECK(vk_mem_resolve(a + 16, 16, &whole) && whole.buffer, "the pool beside an empty pool is not resolved");
|
||||
CHECK(vk_mem_resolve(a, 0, &at) && at.buffer == whole.buffer && at.offset == whole.offset - 16,
|
||||
"a lookup at the start of a pool finds the empty pool there");
|
||||
CHECK(vk_validation_errors_total() == errors, "the zero-byte pool raised %ld validation errors",
|
||||
vk_validation_errors_total() - errors);
|
||||
b->pool_fini(&empty);
|
||||
CHECK(vk_mem_resolve(a + 16, 16, &after) && after.buffer == whole.buffer,
|
||||
"the empty pool's Finalize took the pool beside it out of the index");
|
||||
b->pool_fini(&pool);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int probe_vkexec(void) {
|
||||
vk_hdr_off(); // the present checks read 8-bit swapchain pixels
|
||||
layouts_equal = 0;
|
||||
@@ -866,6 +963,7 @@ int probe_vkexec(void) {
|
||||
int nvnx_bytes = 0, mirror_bytes = 0;
|
||||
if (!bad) bad = test_nvnx(b, guest, &nvnx_bytes);
|
||||
if (!bad) bad = test_mirror(b, guest, &mirror_bytes);
|
||||
if (!bad) bad = test_empty_pool(b, guest);
|
||||
b->finish();
|
||||
vk_pipeline_shutdown(); // the pipelines and the draw part's stand-ins go before the device
|
||||
vk_exec_draw_shutdown();
|
||||
@@ -910,5 +1008,12 @@ int probe_vkexec(void) {
|
||||
"between submissions mirrored, CPU bytes beside a fill kept, %d bytes compared)\n", mirror_bytes);
|
||||
else
|
||||
printf("probe-vkexec: mirror part skipped (memory mode import)\n");
|
||||
if (marks_skipped)
|
||||
printf("probe-vkexec: pass marks part skipped (no VK_AMD_buffer_marker, or TOTK_PASS_CRUMBS=0)\n");
|
||||
else
|
||||
printf("probe-vkexec: pass marks part passed (pass, dispatch, pass in one submission: the GPU's last started and "
|
||||
"finished mark is the last pass, ring records exact); the report reads:\n%s\n", marks_text);
|
||||
printf("probe-vkexec: empty pool part passed (a zero-byte pool at the start of another: no Vulkan objects, lookups "
|
||||
"there find the other pool before and after its Finalize)\n");
|
||||
return 0;
|
||||
}
|
||||
+23
-2
@@ -20,6 +20,7 @@
|
||||
#include "nvn_api.h"
|
||||
#include "nvn_pool_index.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
// Timestamps for the tracking cost, in ns; qpc_ms converts.
|
||||
@@ -245,6 +246,11 @@ int vk_pool_init(pool_t *p) {
|
||||
// virtual pools that map it. Either gets device-local memory: 1.4.3's CPU never writes its 0x18c pool's storage
|
||||
// (unchanged over 3600 frames of the intro), so the guest storage and the GPU copy need no sync.
|
||||
if ((p->flags & NVN_MEMORY_POOL_FLAGS_GPU_NO_ACCESS) && !(p->flags & NVN_MEMORY_POOL_FLAGS_PHYSICAL)) return 1;
|
||||
// A pool of zero bytes (TotK makes some now and then) holds no address the GPU can use: it gets no record. A record
|
||||
// would import 0 bytes (invalid usage; the driver refuses the memory) and put an empty range into the index at the
|
||||
// pool's address, where a lookup of the pool that starts there found the empty one first. vk_pool_fini finds no
|
||||
// record for it and does nothing.
|
||||
if (!p->size) return 1;
|
||||
PoolRec *r = calloc(1, sizeof *r);
|
||||
if (!r) rt_fatal("vk: out of memory for a pool record");
|
||||
r->key = p;
|
||||
@@ -1801,6 +1807,8 @@ static void collect_locked(VkCtx *c) {
|
||||
for (int i = 0; i < ndead; i++) {
|
||||
Dead *d = &dead[i];
|
||||
if (d->seq > done_seq || d->tl > tl) { dead[k++] = *d; continue; }
|
||||
rec_check_handle(d->kind == DEAD_VIEW ? REC_VIEW : d->kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, d->handle, 1,
|
||||
d->tl, tl);
|
||||
switch (d->kind) {
|
||||
case DEAD_IMAGE: vkDestroyImage(c->device, (VkImage)(uintptr_t)d->handle, NULL); break;
|
||||
case DEAD_IMAGE_VMA: vmaDestroyImage(c->vma, (VkImage)(uintptr_t)d->handle, d->alloc); break;
|
||||
@@ -1932,8 +1940,21 @@ static void defer(int kind, uint64_t handle, VmaAllocation alloc) {
|
||||
plat_once(&cmd_once, cmd_init, NULL);
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
if (ndead == cap_dead) grow((void **)&dead, &cap_dead, sizeof *dead);
|
||||
// The open batch may record into the object: it counts as the newest user.
|
||||
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl_noted};
|
||||
// The open batch may record into the object: it counts as the newest user. The GPU must also be past every
|
||||
// submission that names it. tl_noted covers the submitted ones; an object the replay lets go of while it records
|
||||
// a submission (vk_exec_recording_fence) may be named by that one too, not yet submitted: vk_texture.c
|
||||
// slot_resolve hands a draw a released entry, and entry_of destroys released entries at that address later in
|
||||
// the same submission. Stamped with the last submitted value, the object went as soon as the GPU reached it,
|
||||
// before the submission that names it ran, and with the recorder thread (vk_rec.c) behind, before the recorder
|
||||
// made the call that names it. LAS Recomp and Savage Wind Recomp stamp every deferral with the submission being
|
||||
// recorded (runtime/nvn/vulkan/textures.c defer: vk_pending_seq). TOTK_DEFER_STAMP=0 brings back the old stamp.
|
||||
static int stamp = -1;
|
||||
if (stamp < 0) stamp = rt_knob("DEFER_STAMP", "1") != 0;
|
||||
uint64_t tl = tl_noted, rf = stamp ? vk_exec_recording_fence() : 0;
|
||||
if (rf > tl) tl = rf;
|
||||
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl};
|
||||
rec_check_handle(kind == DEAD_VIEW ? REC_VIEW : kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, handle, 0, tl,
|
||||
vk_sync_fence_done());
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
|
||||
|
||||
+87
-1
@@ -26,6 +26,10 @@ static VkCommandBuffer rec_cb; // the part being recorded (set while the
|
||||
static int rec_live; // producer: rec_begin started the recorder for rec_cb
|
||||
static PlatThread *rec_thread;
|
||||
static int rec_prio; // the submitting thread's priority, which the recorder takes
|
||||
static int rec_chk; // TOTK_REC_CHECK (rec_check_handle)
|
||||
static uint64_t rec_delay_ns; // TOTK_REC_DELAY_US (rec_main)
|
||||
static volatile uint64_t part_h0; // put_h at rec_begin: where the packets of the part being recorded start
|
||||
static volatile uint64_t part_fence; // the value of the part being recorded (rec_begin)
|
||||
|
||||
// Producer statistics, logged by rec_stats_log.
|
||||
static struct { uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on; } RS_;
|
||||
@@ -40,6 +44,7 @@ enum {
|
||||
R_SetRasterizerDiscardEnable, R_SetScissorWithCount, R_SetStencilCompareMask, R_SetStencilOp, R_SetStencilReference,
|
||||
R_SetStencilTestEnable, R_SetStencilWriteMask, R_SetViewportWithCount, R_WriteTimestamp, R_WriteTimestamp2,
|
||||
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask, R_EndCb,
|
||||
R_PassMark,
|
||||
};
|
||||
|
||||
static uint64_t now_ns(void) { return plat_now_ns(); }
|
||||
@@ -64,6 +69,9 @@ static void rec_main(void *arg) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// TOTK_REC_DELAY_US (debug): the recorder falls this far behind each batch it finds, as on a slow machine,
|
||||
// so objects the replay lets go of meet packets it has not made yet (TOTK_REC_CHECK).
|
||||
if (rec_delay_ns) plat_sleep_ns(rec_delay_ns);
|
||||
for (int k = 0; t != h; k++) {
|
||||
uint64_t w = ring[t % RING_WORDS];
|
||||
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
|
||||
@@ -103,6 +111,9 @@ static void *put(uint32_t op, size_t bytes) {
|
||||
}
|
||||
wait_space(n);
|
||||
ring[pos] = (uint64_t)op | n << 32;
|
||||
// Packers reserve room for optional arrays they may leave unwritten (BeginRendering's depth and stencil); with
|
||||
// TOTK_REC_CHECK those words would still hold handles from the ring's previous lap.
|
||||
if (rec_chk) memset(&ring[pos + 1], 0, (n - 1) * 8);
|
||||
put_h += n;
|
||||
RS_.packets++, RS_.words += n;
|
||||
return &ring[pos + 1];
|
||||
@@ -124,7 +135,7 @@ void rec_drain(void) {
|
||||
RS_.drains++, RS_.drain_ns += now_ns() - t0;
|
||||
}
|
||||
|
||||
void rec_begin(VkCommandBuffer cb) {
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence) {
|
||||
static int mode = -1;
|
||||
if (mode < 0) {
|
||||
// Default off on AMD GPUs. With the recorder those drivers lost the device or hung the GPU at startup on
|
||||
@@ -140,6 +151,9 @@ void rec_begin(VkCommandBuffer cb) {
|
||||
if (mode) {
|
||||
plat_event_init(&wake_ev, 0, 0);
|
||||
rec_prio = plat_thread_priority_get();
|
||||
rec_chk = rt_knob("REC_CHECK", "0"); // debug switches, read before the recorder starts
|
||||
const char *d = cfg_get("TOTK_REC_DELAY_US");
|
||||
rec_delay_ns = d && *d && atoi(d) > 0 ? (uint64_t)atoi(d) * 1000 : 0;
|
||||
rec_thread = plat_thread_new(rec_main, NULL, "vk recorder");
|
||||
if (!rec_thread) {
|
||||
rt_log("vk_rec: no recorder thread: the replay makes its Vulkan calls itself");
|
||||
@@ -147,6 +161,9 @@ void rec_begin(VkCommandBuffer cb) {
|
||||
} else {
|
||||
rt_log("vk_rec: the replay's Vulkan commands are made by a recorder thread%s",
|
||||
mode == 2 ? " in the B blocks (TOTK_AB)" : "");
|
||||
if (rec_chk) rt_log("vk_rec: TOTK_REC_CHECK: deferred and destroyed objects are searched in the ring");
|
||||
if (rec_delay_ns) rt_log("vk_rec: TOTK_REC_DELAY_US: the recorder waits %llu us before each batch",
|
||||
(unsigned long long)(rec_delay_ns / 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +171,8 @@ void rec_begin(VkCommandBuffer cb) {
|
||||
if (!RT_KNOB_ON(mode)) return;
|
||||
RS_.parts_on++;
|
||||
rec_cb = cb; // the recorder is idle: rec_end drained it
|
||||
__atomic_store_n(&part_h0, put_h, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&part_fence, fence, __ATOMIC_RELEASE);
|
||||
rec_live = 1;
|
||||
}
|
||||
|
||||
@@ -202,6 +221,64 @@ void rec_stats_log(void) {
|
||||
memset(&RS_, 0, sizeof RS_);
|
||||
}
|
||||
|
||||
// TOTK_REC_CHECK=1 (debug, recorder on): the packets of the part being recorded are searched word by word for an object
|
||||
// vk_memory.c lets go of. put() zeroes each packet's room, so only words a packer wrote can match. A hit is a hazard
|
||||
// - at a deferral (destroy 0) when the stamp tl is below the part's value: vk_memory.c collect_locked may destroy the
|
||||
// object once the GPU reaches tl, before the part ran and, with the recorder behind, before it made the call;
|
||||
// - at a destruction (destroy 1) always: a packet not made yet hands the driver a dead handle, a made one leaves it
|
||||
// in a command buffer not yet submitted.
|
||||
// "Made" means before the tail the recorder published, which trails it by up to 64 packets. Hazards are logged (the
|
||||
// first 64) and counted by kind, packet and case for rec_check_log; named deferrals that wait long enough are only
|
||||
// counted. Reads race the producer only in words below head, which stay put until the ring
|
||||
// wraps (16 MiB later).
|
||||
static const char *const kind_name[REC_KINDS] = {"image", "view", "buffer"};
|
||||
static volatile int32_t chk_seen[2], chk_named, chk_logged, chk_hz[2][2][REC_KINDS][64]; // [destroy][made][kind][op]
|
||||
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu) {
|
||||
if (!rec_chk || !handle || kind < 0 || kind >= REC_KINDS || !__atomic_load_n(&rec_live, __ATOMIC_RELAXED)) return;
|
||||
uint64_t h = __atomic_load_n(&head, __ATOMIC_ACQUIRE), p = __atomic_load_n(&part_h0, __ATOMIC_ACQUIRE);
|
||||
uint64_t fence = __atomic_load_n(&part_fence, __ATOMIC_ACQUIRE);
|
||||
if (p > h || h - p > RING_WORDS / 2) return; // a part this large is not searched
|
||||
plat_inc32(&chk_seen[destroy != 0]);
|
||||
while (p < h) {
|
||||
uint64_t w = ring[p % RING_WORDS];
|
||||
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
|
||||
if (!n) return; // overwritten meanwhile
|
||||
if (op != R_PAD)
|
||||
for (uint32_t i = 1; i < n; i++) {
|
||||
if (ring[(p + i) % RING_WORDS] != handle) continue;
|
||||
int made = p < __atomic_load_n(&tail, __ATOMIC_ACQUIRE);
|
||||
if (!destroy && tl >= fence) { plat_inc32(&chk_named); return; }
|
||||
plat_inc32(&chk_hz[destroy][made][kind][op & 63]);
|
||||
if (plat_inc32(&chk_logged) <= 64)
|
||||
rt_log("vk_rec: check: HAZARD: %s %#llx %s while packet op %u (byte %u of its arguments) of the "
|
||||
"part being recorded names it, %s; deferral stamp %llu, part value %llu, GPU at %llu",
|
||||
kind_name[kind], (unsigned long long)handle, destroy ? "destroyed" : "deferred", op,
|
||||
8 * (i - 1), made ? "made" : "not known made", (unsigned long long)tl,
|
||||
(unsigned long long)fence, (unsigned long long)gpu);
|
||||
return;
|
||||
}
|
||||
p += n;
|
||||
}
|
||||
}
|
||||
|
||||
void rec_check_log(void) {
|
||||
static volatile int32_t said;
|
||||
if (!rec_chk || plat_xchg32(&said, 1)) return;
|
||||
int any = 0;
|
||||
for (int d = 0; d < 2; d++)
|
||||
for (int m = 0; m < 2; m++)
|
||||
for (int k = 0; k < REC_KINDS; k++)
|
||||
for (int op = 0; op < 64; op++)
|
||||
if (chk_hz[d][m][k][op]) {
|
||||
any = 1;
|
||||
rt_log("vk_rec: check summary: %d hazards: %s %s, packet op %d %s", (int)chk_hz[d][m][k][op],
|
||||
kind_name[k], d ? "destroyed" : "deferred", op, m ? "made" : "not known made");
|
||||
}
|
||||
rt_log("vk_rec: check summary: %s; searched for %d objects deferred and %d destroyed while a part was recorded; %d "
|
||||
"deferred ones the part named waited for its value", any ? "hazards above" : "no hazards", (int)chk_seen[0],
|
||||
(int)chk_seen[1], (int)chk_named);
|
||||
}
|
||||
|
||||
#define OWNED(cb) (rec_live && (cb) == rec_cb)
|
||||
// A call the ring takes no packet for: everything queued first, then the call here.
|
||||
#define FALLBACK() (rec_drain(), RS_.fallbacks++)
|
||||
@@ -531,6 +608,14 @@ void rec_polygon_mode(PFN_vkCmdSetPolygonModeEXT fn, VkCommandBuffer cb, VkPolyg
|
||||
pub();
|
||||
}
|
||||
|
||||
typedef struct { uint32_t id; int end; } A_PassMark;
|
||||
void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end) {
|
||||
if (!OWNED(cb)) { vk_pass_mark_cmd(cb, id, end); return; }
|
||||
A_PassMark *a = put(R_PassMark, sizeof *a);
|
||||
*a = (A_PassMark){id, end};
|
||||
pub();
|
||||
}
|
||||
|
||||
typedef struct { void *fn; uint32_t first, n; const void *v; } A_Targets;
|
||||
#define TARGET_CMD(fname, R, PFN, T) \
|
||||
void fname(PFN fn, VkCommandBuffer cb, uint32_t first, uint32_t n, const T *v) { \
|
||||
@@ -618,6 +703,7 @@ static void exec_packet(uint32_t op, void *p) {
|
||||
case R_BlendEnable: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEnableEXT)a->fn)(cb, a->first, a->n, a->v); break; }
|
||||
case R_BlendEquation: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEquationEXT)a->fn)(cb, a->first, a->n, a->v); break; }
|
||||
case R_WriteMask: { A_Targets *a = p; ((PFN_vkCmdSetColorWriteMaskEXT)a->fn)(cb, a->first, a->n, a->v); break; }
|
||||
case R_PassMark: { A_PassMark *a = p; vk_pass_mark_cmd(cb, a->id, a->end); break; }
|
||||
case R_EndCb: {
|
||||
A_EndCb *a = p;
|
||||
if (a->kind) vk_crumb(cb, a->kind, a->n);
|
||||
|
||||
+9
-1
@@ -15,13 +15,19 @@
|
||||
// MIT, Copyright (c) 2026 Peiikai): the driver's work behind the commands depends on nothing the replay decides next.
|
||||
#pragma once
|
||||
|
||||
void rec_begin(VkCommandBuffer cb); // the submitting thread, sub_lock held
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence); // the submitting thread, sub_lock held; fence: the part's value
|
||||
void rec_end(void); // waits for the recorder; no-op when rec_begin did not start it
|
||||
// The end of part cb: the breadcrumb crumb (0: none, vk_crumbs.c), then vkEndCommandBuffer, both made by the recorder
|
||||
// when it records cb, and the wait for it (1); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
|
||||
void rec_drain(void); // waits until the recorder made every call queued so far
|
||||
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
|
||||
// TOTK_REC_CHECK=1 (debug, recorder on): vk_memory.c reports each object it defers (destroy 0, stamped tl) or destroys
|
||||
// (destroy 1) with the value the GPU reached (gpu); a packet of the part being recorded that still names it is logged
|
||||
// as a hazard when the object can go before the part ran. rec_check_log: the counts by kind and packet, at exit.
|
||||
enum { REC_IMAGE, REC_VIEW, REC_BUFFER, REC_KINDS };
|
||||
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu);
|
||||
void rec_check_log(void);
|
||||
|
||||
// Extension commands called through device function pointers.
|
||||
void rec_push_tpl(PFN_vkCmdPushDescriptorSetWithTemplateKHR fn, VkCommandBuffer cb, VkDescriptorUpdateTemplate tpl,
|
||||
@@ -38,6 +44,8 @@ void rec_blend_equation(PFN_vkCmdSetColorBlendEquationEXT fn, VkCommandBuffer cb
|
||||
const VkColorBlendEquationEXT *v);
|
||||
void rec_write_mask(PFN_vkCmdSetColorWriteMaskEXT fn, VkCommandBuffer cb, uint32_t first, uint32_t n,
|
||||
const VkColorComponentFlags *v);
|
||||
// A pass mark (vk_crumbs.c vk_pass_mark_cmd: a buffer marker and a checkpoint), in order with the commands around it.
|
||||
void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end);
|
||||
|
||||
#define REC_CMDS(X) \
|
||||
X(BeginQuery, (VkCommandBuffer cb, VkQueryPool p, uint32_t q, VkQueryControlFlags f), (cb, p, q, f)) \
|
||||
|
||||
+2
-2
@@ -389,7 +389,7 @@ VkCommandBuffer vk_sync_split(void) {
|
||||
vkBeginCommandBuffer(s->cb, &bi);
|
||||
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
|
||||
ts_begin(s->cb);
|
||||
rec_begin(s->cb);
|
||||
rec_begin(s->cb, cur_fence);
|
||||
return s->cb;
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
vkBeginCommandBuffer(s->cb, &bi);
|
||||
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
|
||||
ts_begin(s->cb);
|
||||
rec_begin(s->cb);
|
||||
rec_begin(s->cb, fence);
|
||||
int recorded = vk_exec_record(s->cb, handles, n, fence);
|
||||
s = &slots[cur_slot]; // the last part (vk_sync_split)
|
||||
if (recorded) ts_end(s->cb, fence);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "nvn_tex.h"
|
||||
#include "tex.h"
|
||||
#include "vk.h"
|
||||
#include "vk_dlaa.h"
|
||||
#include "vk_exec.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
@@ -166,8 +167,7 @@ static int same_but_swizzle(const TexKey *a, const TexKey *b) {
|
||||
static int tex_mirrored(const tex_t *t) {
|
||||
if (!t->mem || nvn_gpu_only((uintptr_t)t->mem)) return 0;
|
||||
if (t->has_layout) return 1;
|
||||
const pool_t *p = t->pool;
|
||||
if (!p || !(p->flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
|
||||
if (!(t->pool_flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
|
||||
if (t->d.flags & (NVN_TEXTURE_FLAGS_DISPLAY | NVN_TEXTURE_FLAGS_COMPRESSIBLE | NVN_TEXTURE_FLAGS_ADAPTIVE_ZCULL |
|
||||
NVN_TEXTURE_FLAGS_DEPTH16_PREFER_FAST_CLEAR | NVN_TEXTURE_FLAGS_ZCULL_SUPPORT_STENCIL))
|
||||
return 0;
|
||||
@@ -1681,8 +1681,9 @@ static VkSampler make_sampler(VkCtx *c, const samp_par *d) {
|
||||
if (si.maxLod < si.minLod) si.maxLod = si.minLod;
|
||||
float lim = c->props.limits.maxSamplerLodBias, bias = d->lod_bias;
|
||||
// DLSS upscaling (resolution.c): the scene's material textures, the filtered mipmapped ones that repeat, are
|
||||
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias.
|
||||
if (!d->cmp_mode && mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR &&
|
||||
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias. Without DLSS
|
||||
// running (dlaa = 0, or NGX without DLSS) nothing accumulates the extra detail, and the sharper mips only shimmer.
|
||||
if (vk_dlaa_available() && !d->cmp_mode && mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR &&
|
||||
(si.addressModeU == VK_SAMPLER_ADDRESS_MODE_REPEAT || si.addressModeU == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT ||
|
||||
si.addressModeV == VK_SAMPLER_ADDRESS_MODE_REPEAT || si.addressModeV == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT))
|
||||
bias += res_dlss_lod_bias();
|
||||
|
||||
@@ -533,9 +533,10 @@ class EventsTest(unittest.TestCase):
|
||||
catalog = json.loads((ROOT / 'installer' / 'text' / 'en.json').read_text(encoding='utf-8'))
|
||||
used = set()
|
||||
for path in (ROOT / 'installer').glob('*.py'):
|
||||
used |= set(re.findall(r"InstallError\('([a-z_.]+)'", path.read_text()))
|
||||
used |= set(re.findall(r"warning\('([a-z_.]+)'", path.read_text()))
|
||||
used |= {f'phase.{s}.{p}' for s, p in re.findall(r"progress\('(\w+)', '(\w+)'[,)]", path.read_text())}
|
||||
text = path.read_text(encoding='utf-8')
|
||||
used |= set(re.findall(r"InstallError\('([a-z_.]+)'", text))
|
||||
used |= set(re.findall(r"warning\('([a-z_.]+)'", text))
|
||||
used |= {f'phase.{s}.{p}' for s, p in re.findall(r"progress\('(\w+)', '(\w+)'[,)]", text)}
|
||||
self.assertEqual(sorted(i for i in used if i not in catalog), [])
|
||||
|
||||
|
||||
|
||||
+19
-2
@@ -68,6 +68,7 @@ import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import check_isa
|
||||
import romfs_index
|
||||
from build_shaders import stale as stale_shaders
|
||||
from check_content import GENERATED_MARKER
|
||||
@@ -114,7 +115,7 @@ if WIN:
|
||||
RT_FLAGS.append('-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', '-limm32', '-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)
|
||||
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-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
|
||||
else:
|
||||
# -no-pie: the lifted modules' function tables (hundreds of thousands of pointers) need no load-time relocation.
|
||||
@@ -150,7 +151,7 @@ def vma_include():
|
||||
|
||||
|
||||
# --toolchain: an llvm-mingw tree. The libraries, headers and DLLs MSYS2 supplies come from the deps directory instead.
|
||||
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lvulkan-1', '-lSDL3', '-lopus',
|
||||
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lvulkan-1', '-lSDL3', '-lopus',
|
||||
'-l:libvpx.a', '-l:libzstd.a', '-l:libc++.a', '-l:libc++abi.a', '-l:libunwind.a', '-l:libwinpthread.a']
|
||||
DEFAULT_DEPS = ROOT / 'toolchain' / 'deps-mingw'
|
||||
# Runtime sources compiled per install, not in the library: romfs.c includes romfs_config.h, patches.c and ov_zstd.c
|
||||
@@ -715,6 +716,16 @@ def elf_sections(path):
|
||||
return out, hi - (lo or 0)
|
||||
|
||||
|
||||
def objdump_tool():
|
||||
"""The disassembler for tools/check_isa.py: llvm-objdump of --toolchain, else MSYS2's or the one on PATH."""
|
||||
suffix = '.exe' if WIN else ''
|
||||
for d in (TOOLCHAIN_BIN, UCRT if WIN else None):
|
||||
for name in ('llvm-objdump', 'objdump'):
|
||||
if d and (d / (name + suffix)).is_file():
|
||||
return str(d / (name + suffix))
|
||||
return check_isa.find_objdump()
|
||||
|
||||
|
||||
def mib(n):
|
||||
return f'{n / (1 << 20):,.1f} MiB'
|
||||
|
||||
@@ -886,6 +897,12 @@ def main():
|
||||
size = sum(o.stat().st_size for o in files)
|
||||
print(f' {mod:8} {len(files):5} objects {mib(size):>14}')
|
||||
verify_objects(out, objs, pairs is not None, main_out, pairs, base, env)
|
||||
if args.cpu == 'v2':
|
||||
# Inline assembly, target attributes and intrinsics pass -march=x86-64-v2 by; one vmovdqa of a YMM register
|
||||
# stops a CPU without AVX (tools/check_isa.py)
|
||||
fail = check_isa.check_objects(objs['runtime'], objdump_tool(), out / 'obj' / 'runtime' / 'isa-v2.json')
|
||||
if fail:
|
||||
sys.exit(f'build_game: {fail}')
|
||||
if args.compile_only:
|
||||
return
|
||||
if args.make_runtime_lib:
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Refuse instructions above x86-64-v2 in objects built for --cpu v2.
|
||||
|
||||
The v2 build is for CPUs without AVX, AVX2, FMA, BMI1/2, F16C, LZCNT and MOVBE (a Pentium Gold G6400, an older Core
|
||||
i7). The compiler keeps generated code inside that set by -march=x86-64-v2, but inline assembly, target attributes and
|
||||
intrinsics pass through: kern_ap_stub (runtime/kernel.c) saved YMM registers with vmovdqa in the v2 library of 0.3.1
|
||||
and crashed such a CPU with an illegal instruction the first time a core was interrupted. tools/build_game.py --cpu v2
|
||||
runs this over every runtime object before it links or archives them.
|
||||
|
||||
A function that holds such an instruction behind a runtime CPU check goes in DISPATCHED with that check named.
|
||||
|
||||
python tools/check_isa.py [--objdump PATH] FILE... (objects, archives or executables)
|
||||
|
||||
Standard library only.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import concurrent.futures
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Mnemonics outside x86-64-v2 that are not VEX/EVEX encoded, or VEX encoded but without a v prefix (BMI1/2).
|
||||
# lzcnt and tzcnt decode as bsr and bsf on older CPUs and give other results; tzcnt is left out because the
|
||||
# compiler emits it on any x86-64 level where bsf would do (a nonzero input, or one it checks first).
|
||||
NON_V2 = {'andn', 'bextr', 'blsi', 'blsmsk', 'blsr', 'bzhi', 'shlx', 'shrx', 'sarx', 'rorx', 'pdep', 'pext', 'mulx',
|
||||
'lzcnt', 'movbe', 'adcx', 'adox', 'sha1rnds4', 'sha1nexte', 'sha1msg1', 'sha1msg2', 'sha256rnds2',
|
||||
'sha256msg1', 'sha256msg2', 'aesenc', 'aesenclast', 'aesdec', 'aesdeclast', 'aesimc', 'aeskeygenassist',
|
||||
'pclmulqdq', 'rdrand', 'rdseed'}
|
||||
PREFIXES = {'rep', 'repe', 'repz', 'repne', 'repnz', 'lock', 'notrack', 'data16', 'cs', 'ds', 'es', 'ss', 'fs', 'gs'}
|
||||
WIDE = re.compile(r'%[yz]mm\d|%k[0-7]\b')
|
||||
|
||||
# Function -> the runtime check that keeps it off CPUs without the instructions.
|
||||
DISPATCHED = {
|
||||
'blocks_ni': 'runtime/sha256.c sha256_blocks: __builtin_cpu_supports("sha") && __builtin_cpu_supports("sse4.1")',
|
||||
}
|
||||
|
||||
|
||||
def find_objdump(hint=None):
|
||||
"""llvm-objdump next to the compiler in use, else on PATH, else GNU objdump; None when there is none."""
|
||||
for p in ([Path(hint)] if hint else []):
|
||||
if p.is_file():
|
||||
return str(p)
|
||||
for name in ('llvm-objdump', 'objdump'):
|
||||
found = shutil.which(name)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def mnemonic(line):
|
||||
"""The mnemonic of one disassembly line (llvm-objdump or GNU objdump, AT&T syntax), or None."""
|
||||
s = line.strip()
|
||||
m = re.match(r'^[0-9a-f]+:\s*', s)
|
||||
if m:
|
||||
s = s[m.end():]
|
||||
words = s.split()
|
||||
while words and words[0] in PREFIXES:
|
||||
words = words[1:]
|
||||
return words[0] if words else None
|
||||
|
||||
|
||||
def is_non_v2(line, mn):
|
||||
if mn.startswith('v') and mn not in ('verr', 'verw'):
|
||||
return True # VEX or EVEX: AVX, AVX2, FMA, F16C, AVX-512
|
||||
if mn.startswith('k') and WIDE.search(line):
|
||||
return True # AVX-512 mask registers
|
||||
return mn in NON_V2 or bool(WIDE.search(line))
|
||||
|
||||
|
||||
def scan_one(path, objdump):
|
||||
"""{(file, function): Counter of mnemonics} of one file."""
|
||||
hits = collections.defaultdict(collections.Counter)
|
||||
r = subprocess.run([objdump, '-d', '--no-show-raw-insn', str(path)], capture_output=True, text=True,
|
||||
errors='replace')
|
||||
if r.returncode:
|
||||
sys.exit(f'check_isa: {objdump} failed on {path}: {r.stderr.strip()[:300]}')
|
||||
obj = str(path)
|
||||
func = None
|
||||
for line in r.stdout.splitlines():
|
||||
if 'file format' in line:
|
||||
obj = line.split(':\tfile format')[0].split(': file format')[0].strip()
|
||||
continue
|
||||
m = re.match(r'^[0-9a-f]*\s*<(.+)>:$', line)
|
||||
if m:
|
||||
func = m.group(1)
|
||||
continue
|
||||
if not line[:1].isspace():
|
||||
continue
|
||||
mn = mnemonic(line)
|
||||
if mn and func not in DISPATCHED and is_non_v2(line, mn):
|
||||
hits[(obj, func)][mn] += 1
|
||||
return hits
|
||||
|
||||
|
||||
def scan(paths, objdump):
|
||||
"""{(file, function): Counter of mnemonics} for every function outside DISPATCHED with a non-v2 instruction."""
|
||||
hits = {}
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=min(16, os.cpu_count() or 1)) as pool:
|
||||
for h in pool.map(lambda p: scan_one(p, objdump), paths):
|
||||
hits.update(h)
|
||||
return hits
|
||||
|
||||
|
||||
def check_objects(paths, objdump, record):
|
||||
"""Scan the objects changed since the last clean scan (their sizes and mtimes in the JSON file record); return
|
||||
the failure text, or None. Without an objdump it warns and passes: the check guards the build, it is no part
|
||||
of it."""
|
||||
if not objdump:
|
||||
print('check_isa: no llvm-objdump or objdump; the v2 instruction check is skipped', file=sys.stderr)
|
||||
return None
|
||||
try:
|
||||
seen = json.loads(record.read_text())
|
||||
except (OSError, ValueError):
|
||||
seen = {}
|
||||
stamp = {str(p): [p.stat().st_size, p.stat().st_mtime_ns] for p in paths}
|
||||
todo = [p for p in paths if seen.get(str(p)) != stamp[str(p)]]
|
||||
hits = scan(todo, objdump)
|
||||
if hits:
|
||||
record.unlink(missing_ok=True)
|
||||
return report(hits)
|
||||
record.write_text(json.dumps(stamp))
|
||||
print(f'check_isa: {len(todo)} of {len(paths)} runtime objects scanned, none above x86-64-v2')
|
||||
return None
|
||||
|
||||
|
||||
def report(hits):
|
||||
lines = [f' {f} in {o}: ' + ', '.join(f'{k} x{v}' for k, v in c.most_common(6))
|
||||
for (o, f), c in sorted(hits.items())]
|
||||
return ('instructions above x86-64-v2 in a --cpu v2 build (a CPU without AVX/BMI stops on them with an illegal '
|
||||
'instruction); use SSE4.2 code under #ifndef __AVX__, or a runtime CPU check and an entry in '
|
||||
'tools/check_isa.py DISPATCHED:\n' + '\n'.join(lines))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('files', nargs='+', type=Path)
|
||||
ap.add_argument('--objdump', help='llvm-objdump or objdump to use (default: PATH)')
|
||||
args = ap.parse_args()
|
||||
objdump = find_objdump(args.objdump)
|
||||
if not objdump:
|
||||
sys.exit('check_isa: no llvm-objdump or objdump found')
|
||||
hits = scan(args.files, objdump)
|
||||
if hits:
|
||||
sys.exit(report(hits))
|
||||
print(f'check_isa: {len(args.files)} files, no instructions above x86-64-v2')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+5
-5
@@ -31,10 +31,10 @@ fetch() {
|
||||
echo "$name: $commit"
|
||||
}
|
||||
|
||||
fetch LASRecomp https://github.com/peiikai/LASRecomp.git da35b83a9611d046a9a681aae668d7085954d37f
|
||||
fetch MacNativeGames https://github.com/ly05010419/MacNativeGames.git b75be8cfa704df3701e86130c8ced268e79d6b11 history
|
||||
fetch d2r-mobile https://github.com/jhaizhou-ops/d2r-mobile.git 53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53
|
||||
fetch nxnative https://github.com/seedds/nxnative.git d5b380b3a80495609c2e751bb994911a638935ad
|
||||
fetch LASRecomp https://github.com/peiikai/LASRecomp.git a6b4f03844ecfb4012f18deff96da8b410aeafcd
|
||||
fetch MacNativeGames https://github.com/ly05010419/MacNativeGames.git d1509b4b62211265fad5449cf53cc9f13484ecc5 history
|
||||
fetch d2r-mobile https://github.com/jhaizhou-ops/d2r-mobile.git fae8c680fe98f79179cf3fab85f7a6c14ee28600
|
||||
fetch nxnative https://github.com/seedds/nxnative.git 99330eb81c873c3aafe9fc92194c9687d77fcdbb
|
||||
fetch SwitchRecomp https://github.com/bgyss/SwitchRecomp.git 0f99e1c5c1e78fdc8e01e58a0f8dc8622e82976b
|
||||
fetch deko3d https://github.com/devkitPro/deko3d.git 373bfd43d8b2d805fa2eb7d406dbb823032df958
|
||||
fetch SavageWindRecomp https://github.com/Revivedx/SavageWindRecomp.git 575ad88e02f9a89a9a885d90039d48af33c982f4
|
||||
fetch SavageWindRecomp https://github.com/Revivedx/SavageWindRecomp.git bf6146c14c37fee8781a601e5a544b93dff6d75e
|
||||
@@ -105,7 +105,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', 'button_icons.py', 'check_content.py', 'extract.py', 'hostmem.py',
|
||||
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'button_icons.py', 'check_content.py', 'check_isa.py', 'extract.py', 'hostmem.py',
|
||||
'make_romfs_patches.py', 'pack_shader_cache.py', 'prepare_shaders.py', 'romfs_index.py', 'shader_census.py',
|
||||
'shader_list.py', 'ultrawide_layout.py', 'cheats.py', 'exefs_patches.py']
|
||||
TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes
|
||||
|
||||
@@ -22,6 +22,19 @@ FIELD = {'x': 44, 'y': 48, 'scale_x': 68, 'scale_y': 72}
|
||||
KEEP_ROOT = {'AppMenuBG_00', 'PauseMenuBG_00', 'ShopMenuParasailBG_00', 'ShortCutBG_00', 'TitleMenuBG_00', 'EventFade_00',
|
||||
'EventFadeWipe_00', 'CameraShutter_00', 'PaAppPictureBookDetail_00', 'AppMenuOverlay_00'}
|
||||
|
||||
# Layouts whose markers the game places over a point in the world (the lock-on arrow, the name of the item or character
|
||||
# in front of Link, enemy life bars and notices, speech balloons): the code moves a pane under the root to the target's
|
||||
# projected position in the 1920x1080 grid, which spans the whole canvas. Under a scaled root that position would be
|
||||
# scaled too, so the marker lands short of its target, toward the middle of the screen (at half the distance on 32:9).
|
||||
# Their root keeps scale 1 and the scale goes one level down instead, to the panes listed here: ones the code does not
|
||||
# move and no animation scales, so the markers keep their 16:9 proportions.
|
||||
MARKER_ROOT = {
|
||||
'AttentionBalloon_00': ('C_Capture_00', 'N_InOut_00'), # N_PosRoot_00 moves; N_InOut_00 animates y and alpha
|
||||
'AttentionLockOn_00': ('N_Offset_00',), # N_PosRoot_00 moves; N_Offset_00 animates y
|
||||
'EnemyInfo_00': ('Pa_EnemyLife_00', 'Pa_EnemyNotice_00'), # the parts move; N_EnemyLifeArea_* stay screen-sized
|
||||
'BalloonMessage_00': ('Pa_BalloonMessage_00',),
|
||||
}
|
||||
|
||||
def edits(k, hud_corner):
|
||||
"""(layout, pane, field, value) for the aspect: k = 16/9 over the aspect (the root's scale), w = how far a 1920-wide
|
||||
layout's edge moves out in its own units (960 * (1 / k - 1)). OFF moves a pane out of sight."""
|
||||
@@ -222,8 +235,8 @@ def apply(blarc, members, width, height, hud_corner=True):
|
||||
assert k < 1, 'only for canvases of another shape than 16:9'
|
||||
root_scale = 'scale_x' if wide else 'scale_y'
|
||||
changed, layouts = set(), {}
|
||||
unknown = sorted(n for n in KEEP_ROOT if f'blyt/{n}.bflyt' not in members)
|
||||
if unknown: raise SystemExit(f'KEEP_ROOT names no layout: {unknown}')
|
||||
unknown = sorted(n for n in KEEP_ROOT | MARKER_ROOT.keys() if f'blyt/{n}.bflyt' not in members)
|
||||
if unknown: raise SystemExit(f'KEEP_ROOT or MARKER_ROOT names no layout: {unknown}')
|
||||
def pane_offset(layout, pane):
|
||||
member = f'blyt/{layout}.bflyt'
|
||||
if member not in members: raise SystemExit(f'{layout}: no layout {member} in the archive')
|
||||
@@ -232,6 +245,14 @@ def apply(blarc, members, width, height, hud_corner=True):
|
||||
return find_pane(layouts[member], layout, pane)
|
||||
for member in sorted(m for m in members if m.startswith('blyt/') and m.endswith('.bflyt')):
|
||||
if member[5:-6] in KEEP_ROOT: continue
|
||||
if member[5:-6] in MARKER_ROOT: # the root stays at 1, its listed panes take the scale
|
||||
layout = member[5:-6]
|
||||
for pane in ('RootPane',) + MARKER_ROOT[layout]:
|
||||
at = pane_offset(layout, pane) + FIELD[root_scale]
|
||||
have = struct.unpack_from('<f', blarc, at)[0]
|
||||
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{layout} {pane} {root_scale}: {have}, expected 1.0')
|
||||
if pane != 'RootPane': struct.pack_into('<f', blarc, at, k)
|
||||
continue
|
||||
at = pane_offset(member[5:-6], 'RootPane') + FIELD[root_scale]
|
||||
have = struct.unpack_from('<f', blarc, at)[0]
|
||||
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{member[5:-6]} RootPane {root_scale}: {have}, expected 1.0')
|
||||
|
||||
+20
-5
@@ -7,6 +7,13 @@
|
||||
# Frame rate limit: 30, 40, 45, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the refresh rate of the
|
||||
# window's monitor, at most 240). A higher rate gives 240: above it the game's physics runs out of memory. Above 60
|
||||
# is experimental and needs realtime = 1. Changes while the game runs take effect at once.
|
||||
# With mailbox or fifo presentation (vk_present, the default) a frame reaches the screen at a vertical blank, so a limit
|
||||
# that does not divide the monitor's refresh rate keeps each frame on screen for a different number of refreshes. On a
|
||||
# 320 Hz monitor 80, 64, 40 and 32 divide it and 90 does not: 90 fps alternates 3 and 4 refreshes per frame, which reads
|
||||
# as judder while the frame counter still shows 90. Likewise on 240 Hz 60, 48 and 40 divide it and on 144 Hz 72, 48 and
|
||||
# 36 do, while 60 does not. vk_present = immediate shows a frame as soon as it is presented, without this quantisation,
|
||||
# at the cost of tearing. A monitor running with variable refresh (FreeSync, G-Sync) follows the frames instead, so
|
||||
# there any limit inside its range is even.
|
||||
# fps_limit = 60
|
||||
# Older key, read only while fps_limit is unset: fps60 = 0 means a limit of 30.
|
||||
# fps60 = 1
|
||||
@@ -57,7 +64,10 @@
|
||||
# astc = auto
|
||||
# Game memory shared with the GPU: auto (mirror on a discrete GPU, import on an integrated one), import (host memory
|
||||
# imported into Vulkan), mirror (pools used as buffers get a copy in video memory) or verify (mirror plus a check of
|
||||
# every copy; slow).
|
||||
# every copy; slow). On a discrete GPU import can be far slower than mirror, not slightly slower: the GPU reads the
|
||||
# game's pools over PCIe. A 5070 Ti Laptop went from 6 ms to 135-230 ms of GPU time per frame with import (5 fps on the
|
||||
# title screen, bound: gpu, every line of the perf window waiting on a fence). The log names the mode in use: "mem:
|
||||
# mode import" or "mem: mode mirror".
|
||||
# memory = auto
|
||||
# A new texture created over a live texture's memory starts with that content instead of empty.
|
||||
# tex_alias = 1
|
||||
@@ -160,8 +170,8 @@
|
||||
# color_saturation = 100
|
||||
# color_gamma = 1
|
||||
# Sun shadow map size: game (the game's own, which changes with the view and drops to about 400 near the ground, so
|
||||
# leaf shadows look blocky and jump) or a fixed 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with it.
|
||||
# Read at start.
|
||||
# leaf shadows look blocky and jump) or a fixed 256, 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with
|
||||
# it. 256 is coarser than the game's own and is for integrated GPUs short of memory bandwidth. Read at start.
|
||||
# shadow_res = game
|
||||
|
||||
# Shadow map resolution: the game's sun cascades are 960x960 (and its static shadow maps 1440 and 720) whatever the game
|
||||
@@ -185,8 +195,9 @@
|
||||
# totk.exe; elsewhere the setting does nothing. The HUD is drawn after it and is not touched. Read at start.
|
||||
# dlaa = 0
|
||||
# What DLSS works from: dlaa (the 3D scene at full size), quality, balanced or performance (the scene at 67, 58 or
|
||||
# 50 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering to match). With dlaa
|
||||
# = 0 the game's own scaling is used at that size. Read at start.
|
||||
# 50 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering to match). They apply
|
||||
# only with dlaa = 1 and nvngx_dlss.dll next to totk.exe, on Windows; otherwise the scene follows render_scale. Read at
|
||||
# start.
|
||||
# dlss = dlaa
|
||||
# The game's own FXAA after the post effects; with DLAA on it is not needed. Changes while the game runs take effect at
|
||||
# once.
|
||||
@@ -316,6 +327,10 @@
|
||||
# takes longer than flight_spike_ms (0 to 60000).
|
||||
# flight = 0
|
||||
# flight_spike_ms = 100
|
||||
# GPU pass marks: after a GPU crash (device lost) the log names the render pass or dispatch the GPU had started and the
|
||||
# last one it finished. Unset: on AMD GPUs (VK_AMD_buffer_marker). 1: also on NVIDIA GPUs
|
||||
# (VK_NV_device_diagnostic_checkpoints), which NVIDIA says costs a little. 0: off.
|
||||
# pass_crumbs = 1
|
||||
# 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
|
||||
|
||||
Reference in new issue
Block a user