mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
Compare commits
No files matched your search
@@ -26,6 +26,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- 16:10 (Steam Deck, handhelds), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- Play time across runs on the Game tab of the settings menu, kept in `playtime.txt` next to the saves
|
||||
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
|
||||
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
@@ -34,7 +35,8 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- Shaders prepared at install instead of during play
|
||||
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
|
||||
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
|
||||
([docs/mods.md](docs/mods.md))
|
||||
([docs/mods.md](docs/mods.md)). HD texture packs work: the port grows the texture heap a pack asks for by itself. Tested
|
||||
with the HDPacks.com 4K Texture Pack v1.0 on 1.4.3 and 1.2.1; other packs come with no guarantee
|
||||
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
|
||||
|
||||
## Planned
|
||||
|
||||
@@ -90,6 +90,10 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
|
||||
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
|
||||
`TOTK_UBO_SNAPSHOT`); without it, late draws read another object's matrix.
|
||||
- Uniform updates. `nvnCommandBufferUpdateUniformBuffer` between the draws of a pass is replayed as an overlay: the
|
||||
bytes go to scratch memory, later bindings of the range bind them, and the copies into the game's buffers are
|
||||
recorded behind one barrier when the pass ends (`vk_exec_draw.c` `op_update_ub`, `TOTK_UB_OVERLAY`). A copy at the
|
||||
update itself would end the pass with a full barrier, which the terrain paid before most of its draws.
|
||||
- Driver data. The NVN driver keeps a per-stage constant bank (c0) that shaders read for things like point size, dispatch size and base instance. The back end fills it the way the driver does.
|
||||
- Shaders: the Ryujinx translator 1.3.2 with MacNativeGames' patches and ours (`tools/xlate/patches/`), built with .NET 10 NativeAOT as `totk_xlate.exe` and run as a separate process. Its SPIR-V goes to Vulkan directly. `tools/prepare_shaders.py` and `totk.exe --prepare-shaders` translate and compile the variants of MacNativeGames' shared list ahead of play (29,587 for 1.4.3); the rest are translated on first use and cached on disk.
|
||||
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it. Shader stages are compiled ahead into libraries; a draw that meets a new state links them at once (fast link), and an optimised pipeline replaces the linked one when a background thread finishes it. Pipeline keys seen in play are saved and built at boot. A draw whose pipeline is not ready is skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise, so one-shot passes are never lost; a full-screen draw is skipped as well rather than waiting (`TOTK_PIPE_SKIP`); the stage libraries a waiting draw needs are built on idle workers in parallel (`TOTK_PIPE_WAIT`). Without the extension, a missing pipeline is built on a worker and the draw waits or is skipped by the same rule.
|
||||
|
||||
+11
-1
@@ -118,6 +118,13 @@ of them.
|
||||
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`. `--prompts pc` writes
|
||||
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
|
||||
|
||||
A build from source has no layouts for other shapes than 16:9. At an `output_res` such as 5120x1440 the game then
|
||||
draws the largest 16:9 size inside it (2560x1440) and the present adds bars, instead of a wide camera over a
|
||||
stretched HUD (`runtime/resolution.c`, `shape_fallback`). For the full shape make `romfs-patches-<w>x<h>` once per
|
||||
size: `make_romfs_patches.py --version <ver> --aspect <w>x<h> --out build\<ver>\romfs-patches-<w>x<h>` (the sizes
|
||||
the installer makes are `ASPECT_SIZES` in `installer/steps.py`); `build_game.py` copies every `romfs-patches*`
|
||||
folder next to `totk.exe`.
|
||||
|
||||
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
|
||||
for the shared shader list:
|
||||
|
||||
@@ -231,7 +238,10 @@ Before a commit:
|
||||
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
|
||||
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
|
||||
files); a change to the settings passes `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`, which fails
|
||||
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md).
|
||||
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md). A change to the mods' ExeFS patches
|
||||
(`tools/exefs_patches.py`, `runtime/mods.c`, the `folded.txt` of `recomp/gen.py`) passes
|
||||
`.venv\Scripts\python tests\exefs_patches_test.py`, which makes its own modules and runs `gen.py` on one with a jump
|
||||
table (no game files), and `totk.exe --probe-mods`.
|
||||
|
||||
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
|
||||
change passes these, all run with `.venv`:
|
||||
|
||||
+3
-1
@@ -60,7 +60,9 @@ loads the saves 1.4.3 wrote.
|
||||
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
|
||||
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
|
||||
tools refuse costs those cheats or patches, not the install.
|
||||
tools refuse costs those cheats or patches, not the install. The patch files' writes into main's data go to
|
||||
`build-cache/exefs.data`, which the game stores into main at start; after the lift they are checked against the
|
||||
bytes the lifted code read as constants (`<gen>/main/folded.txt`, its jump tables).
|
||||
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
hashes and the game version's patch page) and link
|
||||
them with the prebuilt runtime library for the host's level (`libtotkrt.a` or `libtotkrt-v2.a`) into `totk.exe`
|
||||
|
||||
@@ -71,6 +71,10 @@ The rebuild needs the setup folder the game was installed from. When that folder
|
||||
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
|
||||
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
|
||||
|
||||
A patch file may also change the game's data (constants and tables in its `.rodata` and `.data`). Those bytes are
|
||||
stored into the loaded game at start, before it runs. When two mods write the same bytes, the higher one wins,
|
||||
as with files.
|
||||
|
||||
## Cheats
|
||||
|
||||
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
|
||||
@@ -87,6 +91,21 @@ The port keys a mod by its folder name. A renamed folder counts as a new mod:
|
||||
|
||||
Rename mod folders before you set them up in the menu, not after.
|
||||
|
||||
## HD texture packs
|
||||
|
||||
Texture packs made for the emulators replace the game's textures with larger ones, so the heap the world's textures
|
||||
stream into must grow. Such a pack grows it in its own `System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs`, sized for
|
||||
the emulators' 8 GB memory option. The port reads that file at start: the texture heap and the game's memory pool grow
|
||||
by the pack's amount, and the game gets the port's own heap table with that growth (the log says
|
||||
`resolution: mod <name> ships a HeapDef with TextureHeap ...`). Nothing needs to be set. For a pack without such a file
|
||||
that still loops the panic Blood Moon, set `tex_heap_extra_mb` under `[mods]` in `totk.ini` (0 to 4096 MiB).
|
||||
|
||||
Tested: **HDPacks.com Tears of the Kingdom 4K Texture Pack v1.0** (`4kTextures`, 10,407 textures, 6.5 GB) on game 1.4.3
|
||||
and 1.2.1, RTX 4080 SUPER: it loads into the game from a save and runs at the same frame rate, with about 3.3 GB more of
|
||||
textures in memory (4.5 GB of game memory pools against 1.2 GB without it). Other packs are not tested and come with no
|
||||
guarantee. Such a pack needs a lot of video memory; on a GPU with less than 8 GB, `astc_bc = 1` under `[performance]`
|
||||
keeps its textures at a quarter of the size.
|
||||
|
||||
## What does not work
|
||||
|
||||
| On the Mods tab | Why | What to do |
|
||||
|
||||
+4
-2
@@ -64,6 +64,7 @@ Many rows below are developer tools that only act together with another setting
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `vram_limit_mib` | Pretends the device-local memory budget is at most this many MiB, for the paths that react to the budget (`tex_vram_pressure`, mirror refusal); the driver knows nothing of it. | Lets a large card drive those paths; a low value drops released textures and refuses mirrors that would have fit. | Caution |
|
||||
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
|
||||
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
|
||||
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
|
||||
@@ -131,9 +132,10 @@ Many rows below are developer tools that only act together with another setting
|
||||
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
|
||||
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
|
||||
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
|
||||
| `rec_end` (default `1`) | `0` lets the submitting thread end a recorded part again, as an older version did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which unused released textures are dropped, 0 to 100; `0` is off. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets a 4 GiB card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `rec_end` (default `1`) | `0` lets the submitting thread begin, end and submit a recorded part again, as older versions did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which released textures are dropped, 0 to 100; `0` is off. First those the game has overwritten with a later texture and left alone for 30 s, then those that can be uploaded again. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets the card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
|
||||
| `ub_overlay` (default `1`) | `0` copies each uniform update inside a pass into its buffer at once, ending the pass with a full barrier; `ab` alternates with the `ab` blocks. | With `0` scenes with many updated blocks (the terrain, the sky islands) take longer on the GPU; `TOTK_TRACE=perf` prints how many updates stayed in their pass. | Caution |
|
||||
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
|
||||
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
|
||||
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
|
||||
|
||||
+41
-16
@@ -16,9 +16,10 @@ logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites only
|
||||
in build/ recompiles only those. A change of the mods' code cheats then costs the lift (about a minute) and the chunks
|
||||
it touches, not a full compile.
|
||||
|
||||
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in
|
||||
its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; totk.exe runs it at start
|
||||
when an enabled mod has code cheats the build lacks (runtime/cheats.c).
|
||||
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status and exefs.data (tools/exefs_patches.py) and
|
||||
rebuild.txt, the command that starts this setup in its rebuild mode on the install folder (installer/gui.py --rebuild),
|
||||
one argument per line; totk.exe runs it at start when an enabled mod has code cheats the build lacks
|
||||
(runtime/cheats.c).
|
||||
"""
|
||||
import hashlib
|
||||
import http.client
|
||||
@@ -855,6 +856,12 @@ def exefs_status(ctx):
|
||||
return ctx.dest / 'build-cache' / 'exefs.status'
|
||||
|
||||
|
||||
def exefs_data(ctx):
|
||||
"""The data writes of the mods' ExeFS patch files (tools/exefs_patches.py --data), which runtime/mods.c stores into
|
||||
main before the game runs."""
|
||||
return ctx.dest / 'build-cache' / 'exefs.data'
|
||||
|
||||
|
||||
def cheats_status(ctx):
|
||||
"""The build's cheat status file (tools/cheats.py --status), which runtime/cheats.c reads: in build-cache, not in a
|
||||
mod's own cheats folder, which the player (or TKMM's merge) may empty."""
|
||||
@@ -869,7 +876,7 @@ class Lift(Step):
|
||||
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
|
||||
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
|
||||
files = cheat_files(ctx) + exefs_files(ctx)
|
||||
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
return fingerprint('lift-5', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
|
||||
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
|
||||
|
||||
@@ -890,36 +897,54 @@ class Lift(Step):
|
||||
if m:
|
||||
rep.progress('lift', 'emit', int(m.group(1)), int(m.group(2)), 'chunks')
|
||||
|
||||
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets (tools/cheats.py,
|
||||
# tools/exefs_patches.py); their status files tell the runtime what the build has. Both are the player's
|
||||
# downloads, so files the tools refuse cost those cheats or patches, not the install: the game is built
|
||||
# without them and the setup warns.
|
||||
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets and data writes
|
||||
# (tools/cheats.py, tools/exefs_patches.py); their status files tell the runtime what the build has. Both are
|
||||
# the player's downloads, so files the tools refuse cost those cheats or patches, not the install: the game is
|
||||
# built without them and the setup warns.
|
||||
plain = layout.patches(game_version(ctx))
|
||||
stages = [('cheats.py', 'patches-cheats.txt', cheats_status(ctx), cheat_files(ctx), 'warn.cheats_skipped'),
|
||||
('exefs_patches.py', 'patches-exefs.txt', exefs_status(ctx), exefs_files(ctx), 'warn.exefs_skipped')]
|
||||
# (tool, patch file it writes, its outputs for the runtime: the status first, then the data file, the files it
|
||||
# reads, the warning when it fails)
|
||||
stages = [('cheats.py', 'patches-cheats.txt', [cheats_status(ctx)], cheat_files(ctx), 'warn.cheats_skipped'),
|
||||
('exefs_patches.py', 'patches-exefs.txt', [exefs_status(ctx), exefs_data(ctx)], exefs_files(ctx),
|
||||
'warn.exefs_skipped')]
|
||||
patches, skipped = plain, {}
|
||||
for tool, out, status, inputs, warn in stages:
|
||||
status.unlink(missing_ok=True) # no status: the runtime treats every such file as not built
|
||||
|
||||
def drop(outputs): # no status: the runtime treats every such file as not built
|
||||
for f in outputs:
|
||||
f.unlink(missing_ok=True)
|
||||
|
||||
for tool, out, outputs, inputs, warn in stages:
|
||||
drop(outputs)
|
||||
if not inputs:
|
||||
continue
|
||||
code, log = run_tool(ctx, 'lift', layout.tools / tool,
|
||||
['--exefs', ctx.dest / 'exefs', '--mods', ctx.dest / 'mods', '--patches', patches,
|
||||
'--out', ctx.temp / out, '--status', status])
|
||||
'--out', ctx.temp / out, '--status', outputs[0],
|
||||
*(['--data', outputs[1]] if len(outputs) > 1 else [])])
|
||||
if code:
|
||||
status.unlink(missing_ok=True)
|
||||
drop(outputs)
|
||||
skipped[warn] = log
|
||||
else:
|
||||
patches = ctx.temp / out
|
||||
args = ['--exefs', ctx.dest / 'exefs', '--out', gen]
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', patches], parse)
|
||||
if code and patches != plain:
|
||||
for tool, out, status, inputs, warn in stages:
|
||||
for tool, out, outputs, inputs, warn in stages:
|
||||
if inputs and warn not in skipped:
|
||||
status.unlink(missing_ok=True)
|
||||
drop(outputs)
|
||||
skipped[warn] = log
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', plain], parse)
|
||||
if code:
|
||||
raise InstallError('lift.failed', code=code, log=str(log))
|
||||
# The mods' data writes must stay off the bytes the lifted code reads as constants (its jump tables), which only
|
||||
# the lift knows: gen.py lists them in folded.txt, and a file that writes them becomes unsupported
|
||||
if exefs_data(ctx).is_file():
|
||||
code, log = run_tool(ctx, 'lift', layout.tools / 'exefs_patches.py',
|
||||
['--exefs', ctx.dest / 'exefs', '--status', exefs_status(ctx), '--data',
|
||||
exefs_data(ctx), '--folded', gen / 'main' / 'folded.txt'])
|
||||
if code:
|
||||
drop([exefs_status(ctx), exefs_data(ctx)])
|
||||
skipped.setdefault('warn.exefs_skipped', log)
|
||||
for warn, log in skipped.items():
|
||||
rep.warning(warn, log=str(log))
|
||||
(gen / STAMP).write_text(ctx.keys['lift'] + '\n')
|
||||
|
||||
+41
-2
@@ -9,6 +9,9 @@ Output, under --out:
|
||||
<mod>/<mod>_NNNN.c one `void f_<addr>(Cpu *c)` per guest function, about --chunk-bytes of C per file
|
||||
<mod>/<mod>_table.c sorted <mod>_fn_addrs[] / <mod>_fn_ptrs[] / <mod>_fn_count for rt_dispatch
|
||||
<mod>/manifest.json chunk list, function count, statistics and timings
|
||||
<mod>/folded.txt the module's bytes the generated code took as constants: what the jump tables read at
|
||||
lift time, as module offsets [start, end); tools/exefs_patches.py keeps a mod's data
|
||||
writes off them
|
||||
patches.h code patch sets for the runtime (recomp/patches.py; no sets without --patches)
|
||||
|
||||
Function discovery runs in this process, vectorised over the module's .text words. Function starts are the module
|
||||
@@ -555,7 +558,8 @@ def _retry(fn, *args):
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ workers
|
||||
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter},
|
||||
# 'reads' {name: [(start, end)]} module bytes read since the last chunk
|
||||
|
||||
|
||||
def _worker_init(mods, starts, table, opts, jobs):
|
||||
@@ -565,6 +569,7 @@ def _worker_init(mods, starts, table, opts, jobs):
|
||||
_W['patches'] = table
|
||||
_W['opts'] = opts # Emitter keyword arguments
|
||||
_W['emitters'] = {}
|
||||
_W['reads'] = {}
|
||||
|
||||
|
||||
def _emitter(name):
|
||||
@@ -572,12 +577,31 @@ def _emitter(name):
|
||||
if e is None:
|
||||
path, base = _W['mods'][name]
|
||||
mod = Module(name, path, base, 'c')
|
||||
# Module.read is how the jump tables (recomp/jumptables.py _eval) read .text and .rodata at lift time: the
|
||||
# bytes it returns become part of the generated code, so <mod>/folded.txt lists them
|
||||
read, reads = mod.read, _W['reads'].setdefault(name, [])
|
||||
|
||||
def recorded(a, n, signed=False):
|
||||
reads.append((a - base, a - base + n))
|
||||
return read(a, n, signed=signed)
|
||||
mod.read = recorded
|
||||
e = _W['emitters'][name] = Emitter(mod, set(_W['starts'][name].tolist()),
|
||||
_W['patches'].for_module(name, base),
|
||||
spins=_W['patches'].spins_for(name, base), **_W['opts'])
|
||||
return e
|
||||
|
||||
|
||||
def merge_ranges(ranges):
|
||||
"""Sorted [start, end) ranges with overlapping and touching ones joined."""
|
||||
out = []
|
||||
for s, e in sorted(ranges):
|
||||
if out and s <= out[-1][1]:
|
||||
out[-1][1] = max(out[-1][1], e)
|
||||
else:
|
||||
out.append([s, e])
|
||||
return out
|
||||
|
||||
|
||||
def _emit_chunk(task):
|
||||
"""Worker: write one chunk. Returns its statistics."""
|
||||
name, number, path, fns = task
|
||||
@@ -595,9 +619,12 @@ def _emit_chunk(task):
|
||||
written = write_if_changed(Path(path), text)
|
||||
except OSError as ex:
|
||||
written, error = False, f'{path}: {ex}'
|
||||
reads = _W['reads'].get(name, [])
|
||||
folded = merge_ranges(reads)
|
||||
reads.clear()
|
||||
return {'module': name, 'number': number, 'file': Path(path).name, 'functions': len(fns), 'bytes': len(text),
|
||||
'written': written, 'error': error, 'seconds': time.perf_counter() - t0, 'stats': dict(e.stats),
|
||||
'unsupported': dict(e.unsupported)}
|
||||
'unsupported': dict(e.unsupported), 'folded': folded}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ driver
|
||||
@@ -613,6 +640,14 @@ def table_text(name, addrs):
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def folded_text(name, ranges):
|
||||
"""<mod>/folded.txt: the module offsets [start, end) of the bytes the jump tables read at lift time, in hex."""
|
||||
lines = [f'# {GENERATED_MARKER}: {name} bytes the generated code took as constants (jump tables), module offsets '
|
||||
f'[start, end), generated by recomp/gen.py from game code. Do not edit or commit.']
|
||||
lines += [f'{s:x} {e:x}' for s, e in ranges]
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def remove_stale(moddir, name, nchunks, errors):
|
||||
"""Delete chunks an earlier run wrote beyond this run's count, and temporary files an interrupted run left."""
|
||||
pat = re.compile(re.escape(name) + r'_(\d{4})\.c')
|
||||
@@ -714,6 +749,7 @@ def main():
|
||||
# Largest chunks first, so the pool does not end on a long tail.
|
||||
tasks.sort(key=lambda t: -t[0])
|
||||
results = {name: [] for name in info}
|
||||
folded = {name: [] for name in info}
|
||||
done_at, errors = {}, []
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
@@ -724,9 +760,11 @@ def main():
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
try:
|
||||
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
|
||||
# Its folded bytes come with its result: a chunk emitted again reads them again in its new worker.
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
|
||||
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
|
||||
results[r['module']].append(r)
|
||||
folded[r['module']] += r['folded']
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
@@ -753,6 +791,7 @@ def main():
|
||||
unsupported.update(r['unsupported'])
|
||||
t = time.perf_counter()
|
||||
table_written = write_if_changed(moddir / f'{name}_table.c', table_text(name, d['emitted']))
|
||||
write_if_changed(moddir / 'folded.txt', folded_text(name, merge_ranges(folded[name])))
|
||||
removed = remove_stale(moddir, name, d['chunks'], errors)
|
||||
manifest = {
|
||||
'module': name,
|
||||
|
||||
@@ -307,6 +307,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
|
||||
fflush(stderr);
|
||||
fs_save_flush();
|
||||
vk_pipeline_save(); // no-op without the Vulkan back end
|
||||
playtime_save();
|
||||
prof_write();
|
||||
fflush(stderr);
|
||||
plat_exit(0);
|
||||
|
||||
+5
-3
@@ -59,8 +59,8 @@ static const char *const known[] = {
|
||||
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
@@ -82,6 +82,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
||||
{"PCSS", "display"},
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"TEX_HEAP_EXTRA_MB", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
|
||||
@@ -99,7 +100,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"VB_USED", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
@@ -173,6 +174,7 @@ static const CfgMeta metas[] = {
|
||||
// render fixes
|
||||
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
||||
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
||||
{"VB_USED", "1", "0|1", RESTART},
|
||||
{"DESC_TEMPLATE", "1", "0|1", RESTART},
|
||||
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
|
||||
{"SPLIT_US", "1000", "0..20000", RESTART},
|
||||
|
||||
@@ -72,6 +72,7 @@ void gfx_window_closed(void) {
|
||||
rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting");
|
||||
fs_save_flush();
|
||||
vk_pipeline_save();
|
||||
playtime_save();
|
||||
prof_write(); // plat_exit runs no atexit handlers
|
||||
if (restart) {
|
||||
plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c)
|
||||
|
||||
@@ -260,6 +260,12 @@ void cam_pack_served(void); // romfs.c serves the
|
||||
// ---- game resolution (resolution.c): TOTK_OUTPUT_RES, TOTK_RENDER_SCALE
|
||||
uint64_t res_heap_extra_bytes(void); // added to the application pool (mem_init), 2 MiB steps
|
||||
uint64_t res_heap_extra_kib(void); // added to the HeapDef texture heap and its parent; 0 = none
|
||||
uint64_t res_tex_heap_extra_bytes(void); // TOTK_TEX_HEAP_EXTRA_MB: added to the application pool and
|
||||
// the heap cap (mem_init), 2 MiB steps; 0 = none
|
||||
int res_heapdef_tex_grown(const void *in, size_t n); // the HeapDef table (.byml.zs) grows TextureHeap over the
|
||||
// shipped size (an HD texture pack's)
|
||||
int res_heapdef_plain(const void *in, size_t n); // a readable HeapDef table whose TextureHeap is not
|
||||
// grown (a content mod's)
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); // the HeapDef table (.byml.zs) with
|
||||
// the grown heaps, malloc'd; 0 = cannot (logged)
|
||||
void res_heapdef_served(void); // romfs.c serves the grown table
|
||||
@@ -272,6 +278,8 @@ void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene
|
||||
// until res_init switched the sets on)
|
||||
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
|
||||
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
|
||||
int romfs_shaped_ready(unsigned w, unsigned h); // romfs-patches-<w>x<h> has a manifest (romfs.c; works
|
||||
// before fs_init)
|
||||
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
|
||||
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
|
||||
// ---- code patches (patches.c) and the frame step (frame_rate.c)
|
||||
@@ -316,6 +324,10 @@ int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe_win.c (Windows only)
|
||||
int probe_mods(void); // mods_probe.c
|
||||
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
|
||||
void playtime_init(void); // playtime.c: the play time thread, game runs only
|
||||
void playtime_save(void); // playtime.c: writes <profile>/playtime.txt; at exit
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c: all runs and this one; 0 without a profile
|
||||
int probe_playtime(void); // playtime.c: the file, the ticks and the save
|
||||
int probe_discord(void); // discord_rpc.c
|
||||
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
|
||||
int probe_gfx_abi(void); // gfx_abi_probe.c
|
||||
|
||||
+24
-5
@@ -35,7 +35,7 @@ typedef struct {
|
||||
const char *name;
|
||||
GuestFn fn;
|
||||
uint64_t original; // observers: the guest definition rt_original calls
|
||||
uint64_t calls;
|
||||
uint64_t calls; // with TOTK_TRACE=host only (hostfn_report)
|
||||
int blocking; // runs with the virtual core given up (hostfn_blocking)
|
||||
} Slot;
|
||||
static Slot slots[MAX_HOSTFN];
|
||||
@@ -44,6 +44,21 @@ static RtHostEntry added[MAX_ADDED];
|
||||
static int nadded;
|
||||
static int sealed;
|
||||
|
||||
// The call count (hostfn_calls): one counter per calling thread, each on its own cache line, written only by that
|
||||
// thread. A locked add on a per-function counter beside the read-mostly slot fields made the game threads share and
|
||||
// bounce those lines on every host call (1.4.3 town: about 1% of the game threads' time in hostfn_entry).
|
||||
#define MAX_COUNTERS 4096
|
||||
typedef struct { _Alignas(64) uint64_t n; } Counter;
|
||||
static Counter counters[MAX_COUNTERS];
|
||||
static uint32_t ncounters;
|
||||
static uint64_t shared_calls; // the threads past MAX_COUNTERS, counted with an atomic add
|
||||
static __thread uint64_t *thread_calls;
|
||||
|
||||
__attribute__((noinline)) static uint64_t *counter_new(void) {
|
||||
uint32_t i = __atomic_fetch_add(&ncounters, 1, __ATOMIC_RELAXED);
|
||||
return thread_calls = i < MAX_COUNTERS ? &counters[i].n : &shared_calls;
|
||||
}
|
||||
|
||||
static GuestFn table_find(const RtHostEntry *start, const RtHostEntry *stop, const char *name) {
|
||||
__asm__("" : "+r"(start), "+r"(stop)); // separate objects: keep the compiler from reasoning about them
|
||||
for (const RtHostEntry *p = start; p < stop; p++)
|
||||
@@ -146,8 +161,11 @@ static void hostfn_entry(Cpu *c) {
|
||||
uint64_t i = (c->pc - RT_HOSTFN_BASE) >> 4;
|
||||
if (__builtin_expect((c->pc & 15) || i >= (uint64_t)nslots, 0)) bad_entry(c);
|
||||
Slot *s = &slots[i];
|
||||
uint64_t n = __atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED);
|
||||
if (__builtin_expect(!n && (rt_trace & RT_TRACE_HOSTCALL), 0)) {
|
||||
uint64_t *k = thread_calls;
|
||||
if (__builtin_expect(!k, 0)) k = counter_new();
|
||||
if (__builtin_expect(k == &shared_calls, 0)) __atomic_fetch_add(k, 1, __ATOMIC_RELAXED);
|
||||
else __atomic_store_n(k, *k + 1, __ATOMIC_RELAXED);
|
||||
if (__builtin_expect(rt_trace & RT_TRACE_HOSTCALL, 0) && !__atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED)) {
|
||||
char lr[96];
|
||||
rt_log("host call %s from %s", s->name, ldr_describe(c->x[30] - 4, lr, sizeof lr));
|
||||
}
|
||||
@@ -178,8 +196,9 @@ void hostfn_seal(void) {
|
||||
}
|
||||
|
||||
uint64_t hostfn_calls(void) {
|
||||
uint64_t n = 0;
|
||||
for (int i = 0; i < nslots; i++) n += __atomic_load_n(&slots[i].calls, __ATOMIC_RELAXED);
|
||||
uint32_t m = __atomic_load_n(&ncounters, __ATOMIC_RELAXED);
|
||||
uint64_t n = __atomic_load_n(&shared_calls, __ATOMIC_RELAXED);
|
||||
for (uint32_t i = 0; i < m && i < MAX_COUNTERS; i++) n += __atomic_load_n(&counters[i].n, __ATOMIC_RELAXED);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -7,6 +7,7 @@
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
|
||||
// --probe-discord the Rich Presence IPC exchange; skips without a Discord client; no game files
|
||||
// --probe-playtime the play time file, its ticks and saves (playtime.c); needs no game files
|
||||
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
|
||||
// --probe-gfx-abi NVN object layouts against nvn.h's reserved sizes; needs no game files
|
||||
// --probe-nvn, --probe-nvn-tex, --probe-window, --probe-shader, --probe-vkmem, --probe-vkexec graphics probes
|
||||
@@ -396,7 +397,9 @@ static void check_memory(void) {
|
||||
CHECK(q.state == 0 && q.base == g->alias.base && q.size == g->alias.size, "QueryMemory on the free alias region");
|
||||
CHECK(mem_map_physical(a, 0x1000) == 0 && LD64(a + 0x10) == 0 && mem_unmap_physical(a, 0x1000) == 0,
|
||||
"remapped physical memory is not zeroed");
|
||||
CHECK(mem_map_physical(a, 0x100000000ull) == 0x10801, "MapPhysicalMemory beyond the pool is not LimitReached");
|
||||
// past the pool whatever its size (TOTK_APP_POOL, TOTK_TEX_HEAP_EXTRA_MB): 4 GiB fits a grown one
|
||||
CHECK(mem_map_physical(a, (g->pool + 0x100000) & ~0xfffffull) == 0x10801,
|
||||
"MapPhysicalMemory beyond the pool is not LimitReached");
|
||||
mem_query(RT_HOSTFN_BASE + 0x40, &q);
|
||||
CHECK(q.state == 0x10 && q.base == RT_HOSTFN_BASE, "QueryMemory on the host-function window");
|
||||
mem_query(g->alias.base + g->alias.size + 0x1000, &q);
|
||||
@@ -463,7 +466,7 @@ static int probe_crash(int null) {
|
||||
|
||||
static int usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-playtime | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
|
||||
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
|
||||
"--prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
|
||||
@@ -553,6 +556,7 @@ int main(int argc, char **argv) {
|
||||
if (!mode) {
|
||||
cfg.boot = cfg.seconds > 0; // --seconds starts the game like --boot, with its checkpoints
|
||||
discord_init(); // the Rich Presence thread (discord_rpc.c); it reads totk.ini itself and idles while off
|
||||
playtime_init(); // the play time thread (playtime.c): the Game tab's play time across runs
|
||||
rt_boot();
|
||||
return 0;
|
||||
}
|
||||
@@ -566,6 +570,7 @@ int main(int argc, char **argv) {
|
||||
#endif
|
||||
if (!strcmp(mode, "--probe-mods")) return probe_mods();
|
||||
if (!strcmp(mode, "--probe-discord")) return probe_discord();
|
||||
if (!strcmp(mode, "--probe-playtime")) return probe_playtime();
|
||||
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
|
||||
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
|
||||
if (!strcmp(mode, "--probe-services")) return probe_services();
|
||||
|
||||
+21
-13
@@ -20,11 +20,11 @@
|
||||
#define PAGE 0x1000ull
|
||||
#define GiB (1ull << 30)
|
||||
#define LOW_SEARCH_FROM 0x100000000ull
|
||||
#define LOW_SIZE (6 * GiB)
|
||||
#define LOW_BASE (6 * GiB) // the stack and heap block without a texture heap extra
|
||||
#define STACK_OFF 0x10000000ull
|
||||
#define STACK_SIZE 0x10000000ull
|
||||
#define HEAP_OFF (STACK_OFF + STACK_SIZE + 0x10000000ull)
|
||||
#define HEAP_CAP (4 * GiB)
|
||||
#define HEAP_CAP (4 * GiB) // without TOTK_TEX_HEAP_EXTRA_MB, which raises it by its own size (mem_init)
|
||||
#define ALIAS_SIZE (64 * GiB)
|
||||
#define ALIAS_ALIGN (4 * GiB)
|
||||
#define ALIAS_PAGES (ALIAS_SIZE / PAGE)
|
||||
@@ -48,6 +48,9 @@ enum { ATTR_PERMISSION_LOCKED = 16 };
|
||||
|
||||
static RtGuestLayout L;
|
||||
static uint64_t mods_lo, mods_hi; // fixed module window
|
||||
// The stack and heap block: LOW_BASE, plus the texture heap extra (tex_heap_extra_mb or a mod's HeapDef,
|
||||
// res_tex_heap_extra_bytes) rounded up to a GiB, so only a player who grows the texture heap reserves more (mem_init).
|
||||
static uint64_t low_size = LOW_BASE;
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT;
|
||||
static uint64_t heap_size, alias_bytes;
|
||||
static uint8_t *alias_pages; // 1 per mapped 4 KiB page of the alias region
|
||||
@@ -78,7 +81,7 @@ static volatile int64_t frz_count[4]; // freezes; thaws by a write fault, by m
|
||||
|
||||
static int watched(uint64_t addr, uint64_t size) {
|
||||
const RtGuestLayout *g = &L;
|
||||
return (addr >= g->aslr.base && size <= LOW_SIZE && addr - g->aslr.base <= LOW_SIZE - size) ||
|
||||
return (addr >= g->aslr.base && size <= low_size && addr - g->aslr.base <= low_size - size) ||
|
||||
(addr >= g->alias.base && size <= ALIAS_SIZE && addr - g->alias.base <= ALIAS_SIZE - size);
|
||||
}
|
||||
|
||||
@@ -221,10 +224,15 @@ void mem_init(void) {
|
||||
|
||||
// Stack and heap block, then the alias region above it, both searched below the module window.
|
||||
uint64_t low = 0, alias = 0;
|
||||
for (uint64_t at = LOW_SEARCH_FROM; at + LOW_SIZE <= mods_lo && !low; at += GiB)
|
||||
if (reserve_at(at, LOW_SIZE, 1)) low = at;
|
||||
if (!low) reexec_or_fail("a 6 GiB stack and heap block below 2^39");
|
||||
for (uint64_t at = align_up(low + LOW_SIZE, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
|
||||
low_size = LOW_BASE + align_up(res_tex_heap_extra_bytes(), GiB);
|
||||
for (uint64_t at = LOW_SEARCH_FROM; at + low_size <= mods_lo && !low; at += GiB)
|
||||
if (reserve_at(at, low_size, 1)) low = at;
|
||||
if (!low) {
|
||||
char what[64];
|
||||
snprintf(what, sizeof what, "a %llu GiB stack and heap block below 2^39", (unsigned long long)(low_size / GiB));
|
||||
reexec_or_fail(what);
|
||||
}
|
||||
for (uint64_t at = align_up(low + low_size, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
|
||||
if (reserve_at(at, ALIAS_SIZE, 1)) alias = at;
|
||||
if (!alias) reexec_or_fail("a 64 GiB alias region below 2^39");
|
||||
|
||||
@@ -238,11 +246,11 @@ void mem_init(void) {
|
||||
L = (RtGuestLayout){
|
||||
.aslr = {low, RT_ADDRESS_LIMIT - low},
|
||||
.stack = {low + STACK_OFF, STACK_SIZE},
|
||||
.heap = {low + HEAP_OFF, HEAP_CAP},
|
||||
.heap = {low + HEAP_OFF, HEAP_CAP + res_tex_heap_extra_bytes()},
|
||||
.alias = {alias, ALIAS_SIZE},
|
||||
.kernel = {RT_KERNEL_BASE, RT_KERNEL_SIZE},
|
||||
.hostfn = {RT_HOSTFN_BASE, RT_HOSTFN_SIZE},
|
||||
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes(), // resolution.c
|
||||
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes() + res_tex_heap_extra_bytes(),
|
||||
};
|
||||
if (!L.pool) rt_fatal("memory: TOTK_APP_POOL is 0");
|
||||
}
|
||||
@@ -276,7 +284,7 @@ static int inside(uint64_t a, uint64_t n, RtRange r) { return a >= r.base && n <
|
||||
// ---- 0x01 SetHeapSize
|
||||
uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
|
||||
plat_mutex_lock(&lock);
|
||||
if (size > HEAP_CAP || size % 0x200000 || size + image_bytes() > L.pool) {
|
||||
if (size > L.heap.size || size % 0x200000 || size + image_bytes() > L.pool) {
|
||||
plat_mutex_unlock(&lock);
|
||||
return R_INVALID_STATE;
|
||||
}
|
||||
@@ -506,7 +514,7 @@ static void taken_gap(uint64_t a, uint64_t *base, uint64_t *size) {
|
||||
uint64_t lo = 0, hi = RT_ADDRESS_LIMIT;
|
||||
RtRange known[3 + MOD_COUNT + 2];
|
||||
int n = 0;
|
||||
known[n++] = (RtRange){L.aslr.base, LOW_SIZE};
|
||||
known[n++] = (RtRange){L.aslr.base, low_size};
|
||||
known[n++] = L.alias;
|
||||
known[n++] = L.kernel;
|
||||
known[n++] = L.hostfn;
|
||||
@@ -549,14 +557,14 @@ uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
|
||||
r.base = 0;
|
||||
r.size = low;
|
||||
r.state = ST_FREE;
|
||||
} else if (a < low + LOW_SIZE) {
|
||||
} else if (a < low + low_size) {
|
||||
uint64_t heap = L.heap.base;
|
||||
if (a >= heap && a < heap + heap_size) {
|
||||
r = (RtMemoryInfo){.base = heap, .size = heap_size, .state = ST_NORMAL, .perm = PERM_RW};
|
||||
} else if (a < heap) {
|
||||
r = (RtMemoryInfo){.base = low, .size = heap - low, .state = ST_FREE};
|
||||
} else {
|
||||
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + LOW_SIZE - heap - heap_size, .state = ST_FREE};
|
||||
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + low_size - heap - heap_size, .state = ST_FREE};
|
||||
}
|
||||
} else if (a - L.alias.base < L.alias.size) {
|
||||
uint64_t p = (a - L.alias.base) / PAGE;
|
||||
|
||||
+248
-45
@@ -15,7 +15,9 @@
|
||||
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
|
||||
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
|
||||
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
|
||||
// rebuild at start (cheats.c), and its mod stays off until then.
|
||||
// rebuild at start (cheats.c), and its mod stays off until then. A file's writes into main's data (.rodata, .data,
|
||||
// .bss) are lines of build-cache/exefs.data, stored into the loaded main before the game runs (mods_exefs_data), as
|
||||
// emulators apply a patch file to the module before its dynamic linker runs.
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -170,22 +172,26 @@ static int same_build(const char *hex, size_t n) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// build-cache/exefs.status next to totk.exe, read once; "" when there is none.
|
||||
static const char *exefs_status(void) {
|
||||
static char *text;
|
||||
if (text) return text;
|
||||
// build-cache/<name> next to totk.exe, read once into *text; "" when there is none (mods_probe_cache sets the text).
|
||||
static char *status_text, *data_text;
|
||||
static const char *build_cache(const char *name, char **text) {
|
||||
if (*text) return *text;
|
||||
char path[2100];
|
||||
uint64_t n = 0;
|
||||
if (plat_exe_path(path, sizeof path)) {
|
||||
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (cut) snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/exefs.status");
|
||||
text = (char *)plat_file_read(path, &n);
|
||||
if (cut && snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/%s", name) <
|
||||
(int)(sizeof path - (size_t)(cut + 1 - path)))
|
||||
*text = (char *)plat_file_read(path, &n);
|
||||
}
|
||||
if (!text) text = calloc(1, 1);
|
||||
return text;
|
||||
if (!*text) *text = calloc(1, 1);
|
||||
return *text;
|
||||
}
|
||||
|
||||
static const char *exefs_status(void) { return build_cache("exefs.status", &status_text); }
|
||||
static const char *exefs_data(void) { return build_cache("exefs.data", &data_text); }
|
||||
|
||||
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
|
||||
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
|
||||
char want[20];
|
||||
@@ -194,8 +200,9 @@ static int status_line(const char *key, const char *content, char *kind, char *o
|
||||
const char *end = strchr(line, '\n');
|
||||
size_t len = end ? (size_t)(end - line) : strlen(line);
|
||||
if (len > 18 && !memcmp(line, want, 18)) {
|
||||
char copy[600];
|
||||
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : sizeof copy - 1), line + 18);
|
||||
char copy[600]; // the line after the two hashes, without the next line
|
||||
size_t rest = len - 18;
|
||||
snprintf(copy, sizeof copy, "%.*s", (int)(rest < sizeof copy ? rest : sizeof copy - 1), line + 18);
|
||||
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
|
||||
if (!t1 || !t2) return 0;
|
||||
*t1 = *t2 = 0;
|
||||
@@ -269,9 +276,14 @@ static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
||||
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
|
||||
size_t k = strlen(m->overrides);
|
||||
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
|
||||
} else if (!strcmp(kind_s, "data") && m->ndata < (int)(sizeof m->data / sizeof *m->data)) {
|
||||
m->data[m->ndata++] = (uint32_t)strtoul(key, NULL, 16);
|
||||
} else if (!strcmp(kind_s, "other")) {
|
||||
m->other_version++;
|
||||
}
|
||||
unsigned long bytes; // a code or data file's data writes: exefs.data must hold them all (mods_exefs_data)
|
||||
if ((!strcmp(kind_s, "code") || !strcmp(kind_s, "data")) && sscanf(note, "data %lu", &bytes) == 1)
|
||||
m->data_bytes += (uint32_t)bytes;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -326,34 +338,62 @@ static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
|
||||
int unsupported = exefs_check(m, path);
|
||||
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
|
||||
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
|
||||
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
|
||||
: unsupported ? MOD_EXEFS_UNSUPPORTED
|
||||
: m->pending ? MOD_EXEFS_LATER
|
||||
: m->nsets || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
||||
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
||||
: MOD_EMPTY;
|
||||
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
|
||||
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
|
||||
: unsupported ? MOD_EXEFS_UNSUPPORTED
|
||||
: m->pending ? MOD_EXEFS_LATER
|
||||
: m->nsets || m->ndata || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
||||
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
||||
: MOD_EMPTY;
|
||||
m->enabled = !listed(disabled, m->name);
|
||||
nmods++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mods_scan(void) {
|
||||
static int done;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
// The mods folder into out: TOTK_MODS_DIR, else <dir of totk.exe>/mods. 0 when the exe's path is unknown.
|
||||
static int mods_folder(char *out, size_t cap) {
|
||||
const char *d = cfg_get("TOTK_MODS_DIR");
|
||||
if (d && *d) {
|
||||
snprintf(dir, sizeof dir, "%s", d);
|
||||
} else {
|
||||
char exe[2048];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return;
|
||||
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (!cut) return;
|
||||
*cut = 0;
|
||||
snprintf(dir, sizeof dir, "%s/mods", exe);
|
||||
}
|
||||
if (d && *d) return snprintf(out, cap, "%s", d) < (int)cap;
|
||||
char exe[2048];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return 0;
|
||||
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (!cut) return 0;
|
||||
*cut = 0;
|
||||
return snprintf(out, cap, "%s/mods", exe) < (int)cap;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *folder, *disabled;
|
||||
void (*cb)(const char *name, const char *romfs, void *ctx);
|
||||
void *ctx;
|
||||
} Peek;
|
||||
|
||||
static int peek_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
const Peek *pk = ctx;
|
||||
if (kind != PLAT_FS_DIR || listed(pk->disabled, name)) return 1;
|
||||
char path[2100], romfs[2048];
|
||||
if (snprintf(path, sizeof path, "%s/%s", pk->folder, name) >= (int)sizeof path) return 1;
|
||||
find_root(path, "romfs", romfs, sizeof romfs);
|
||||
if (!romfs[0]) find_root(path, "romfslite", romfs, sizeof romfs);
|
||||
if (romfs[0]) pk->cb(name, romfs, pk->ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx) {
|
||||
char folder[2048];
|
||||
if (!mods_folder(folder, sizeof folder) || !plat_dir_exists(folder)) return;
|
||||
Peek pk = {folder, cfg_get("TOTK_MODS_DISABLED"), cb, ctx};
|
||||
plat_fs_list(folder, peek_cb, &pk);
|
||||
}
|
||||
|
||||
static int scanned;
|
||||
|
||||
void mods_scan(void) {
|
||||
if (scanned) return;
|
||||
scanned = 1;
|
||||
if (!mods_folder(dir, sizeof dir)) return;
|
||||
if (!plat_dir_exists(dir)) {
|
||||
rt_log("mods: no folder %s", dir);
|
||||
return;
|
||||
@@ -376,18 +416,171 @@ void mods_scan(void) {
|
||||
rt_log("mods: %d folders in %s", nmods, dir);
|
||||
}
|
||||
|
||||
void mods_exefs_switch(void) {
|
||||
mods_scan();
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
const ModInfo *m = &mods[i];
|
||||
if (!start_enabled[i] || m->problem != MOD_OK || !m->nsets) continue;
|
||||
for (int k = 0; k < m->nsets; k++) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
|
||||
int set = rt_patch_set(name);
|
||||
if (set >= 0) rt_patch_switch(set, 1);
|
||||
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
|
||||
// ---- ExeFS data writes (build-cache/exefs.data; tools/exefs_patches.py data_line has the format)
|
||||
|
||||
// 1 when key names one of m's usable ExeFS patch files: one with a patch set, or one with data writes only.
|
||||
static int owns(const ModInfo *m, uint32_t key) {
|
||||
for (int k = 0; k < m->nsets; k++)
|
||||
if (m->sets[k] == key) return 1;
|
||||
for (int k = 0; k < m->ndata; k++)
|
||||
if (m->data[k] == key) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hex_digit(char c) {
|
||||
return c >= '0' && c <= '9' ? c - '0'
|
||||
: c >= 'a' && c <= 'f' ? c - 'a' + 10
|
||||
: c >= 'A' && c <= 'F' ? c - 'A' + 10
|
||||
: -1;
|
||||
}
|
||||
|
||||
// One line: "<file hash>\t<content hash>\t<offset in main>\t<bytes>[\tbase]", all hex. 0 when malformed.
|
||||
typedef struct { char key[9], content[9]; uint64_t off; uint8_t bytes[256]; int n, base; } DataRun;
|
||||
static int parse_run(const char *line, size_t len, DataRun *r) {
|
||||
char copy[1100], *f[6];
|
||||
if (len >= sizeof copy) return 0;
|
||||
memcpy(copy, line, len);
|
||||
copy[len] = 0;
|
||||
int nf = 0;
|
||||
for (char *p = copy; p && nf < 6; nf++) {
|
||||
f[nf] = p;
|
||||
p = strchr(p, '\t');
|
||||
if (p) *p++ = 0;
|
||||
}
|
||||
if (nf < 4 || nf > 5 || strlen(f[0]) != 8 || strlen(f[1]) != 8) return 0;
|
||||
memcpy(r->key, f[0], 9);
|
||||
memcpy(r->content, f[1], 9);
|
||||
char *end;
|
||||
r->off = strtoull(f[2], &end, 16);
|
||||
size_t hex = strlen(f[3]);
|
||||
if (!*f[2] || *end || !hex || hex % 2 || hex / 2 > sizeof r->bytes) return 0;
|
||||
r->n = (int)(hex / 2);
|
||||
for (int i = 0; i < r->n; i++) {
|
||||
int hi = hex_digit(f[3][2 * i]), lo = hex_digit(f[3][2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) return 0;
|
||||
r->bytes[i] = (uint8_t)(hi << 4 | lo);
|
||||
}
|
||||
r->base = nf == 5;
|
||||
return nf == 4 || (!strcmp(f[4], "base") && r->n == 8);
|
||||
}
|
||||
|
||||
// Stores n bytes into the loaded main at a: a read-only page (.rodata, loader.c seal) is writable for its part of the
|
||||
// copy only, as cheats.c wr_main does. 0 when a page is neither readable nor writable.
|
||||
static int store_main(uint64_t a, const uint8_t *from, int n) {
|
||||
while (n > 0) {
|
||||
uint64_t page = a & ~0xFFFull, end;
|
||||
int k = (int)(page + 0x1000 - a);
|
||||
if (k > n) k = n;
|
||||
int prot = plat_mem_state(page, page + 0x1000, &end);
|
||||
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return 0;
|
||||
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return 0;
|
||||
memcpy((void *)(uintptr_t)a, from, (size_t)k);
|
||||
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
|
||||
a += (uint64_t)k;
|
||||
from += k;
|
||||
n -= k;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
|
||||
if (!store) store = store_main;
|
||||
long total = 0, found = 0;
|
||||
for (int pass = 0; pass < 2; pass++) { // pass 0 checks every line, pass 1 stores
|
||||
// exefs.data must hold every byte the status lines count: one missing or cut short would leave the code
|
||||
// patches on without their data
|
||||
if (pass && found != (long)m->data_bytes) {
|
||||
rt_log("mods: %s: build-cache/exefs.data holds %ld of the %u bytes its ExeFS patch files write", m->name,
|
||||
found, m->data_bytes);
|
||||
return -1;
|
||||
}
|
||||
for (const char *line = exefs_data(), *next; *line; line = next) {
|
||||
const char *nl = strchr(line, '\n');
|
||||
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
||||
next = nl ? nl + 1 : line + len;
|
||||
if (len && line[len - 1] == '\r') len--;
|
||||
char key[9];
|
||||
snprintf(key, sizeof key, "%.*s", (int)(len < 8 ? len : 8), line);
|
||||
if (!len || *line == '#' || !owns(m, (uint32_t)strtoul(key, NULL, 16))) continue;
|
||||
DataRun r;
|
||||
char kind[16], over[256], note[256];
|
||||
// a line whose file the status does not list as built (another content) or that leaves main's data: the
|
||||
// two build-cache files and this build disagree
|
||||
if (!parse_run(line, len, &r) || !status_line(r.key, r.content, kind, over, note) ||
|
||||
(strcmp(kind, "code") && strcmp(kind, "data")) || r.off < code_end || r.off > size ||
|
||||
(uint64_t)r.n > size - r.off) {
|
||||
rt_log("mods: %s: a line of build-cache/exefs.data does not fit this build: %.*s", m->name, (int)len,
|
||||
line);
|
||||
return -1;
|
||||
}
|
||||
if (!pass) {
|
||||
found += r.n;
|
||||
continue;
|
||||
}
|
||||
if (r.base) { // a DT_RELR word holds its addend; the dynamic linker adds the module's base
|
||||
uint64_t w;
|
||||
memcpy(&w, r.bytes, 8);
|
||||
w += base;
|
||||
memcpy(r.bytes, &w, 8);
|
||||
}
|
||||
if (!store(base + r.off, r.bytes, r.n)) {
|
||||
rt_log("mods: %s: cannot write main+%#llx", m->name, (unsigned long long)r.off);
|
||||
return -1;
|
||||
}
|
||||
total += r.n;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static char *dup_text(const char *s) {
|
||||
if (!s) return NULL;
|
||||
size_t n = strlen(s) + 1;
|
||||
char *d = malloc(n);
|
||||
if (d) memcpy(d, s, n);
|
||||
return d;
|
||||
}
|
||||
|
||||
void mods_probe_cache(const char *status, const char *data) {
|
||||
free(status_text);
|
||||
free(data_text);
|
||||
status_text = dup_text(status);
|
||||
data_text = dup_text(data);
|
||||
}
|
||||
|
||||
void mods_rescan(void) {
|
||||
free(mods);
|
||||
mods = NULL;
|
||||
nmods = 0;
|
||||
scanned = 0;
|
||||
}
|
||||
|
||||
// Switches the patch set mod_<key> on; 0 when this build lacks it (logged).
|
||||
static int set_on(const ModInfo *m, uint32_t key) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof name, "mod_%08x", key);
|
||||
int set = rt_patch_set(name);
|
||||
if (set >= 0) rt_patch_switch(set, 1);
|
||||
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
|
||||
return set >= 0;
|
||||
}
|
||||
|
||||
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
|
||||
// bottom first: the top mod's data writes land last and win, as its RomFS files do. Patch sets are only switches
|
||||
// (rt_patch_switch), so their order does not matter.
|
||||
for (int i = nmods - 1; i >= 0; i--) {
|
||||
const ModInfo *m = &mods[i];
|
||||
if (!start_enabled[i] || m->problem != MOD_OK || !(m->nsets || m->ndata)) continue;
|
||||
// the data first: a mod whose data writes cannot all be stored keeps its code patches off too, as half of a
|
||||
// patch file is worse than none
|
||||
long stored = size ? mods_exefs_data(m, base, size, code_end, store) : 0;
|
||||
if (stored < 0) {
|
||||
rt_log("mods: %s: its ExeFS patch files stay off: their data writes could not all be stored", m->name);
|
||||
continue;
|
||||
}
|
||||
for (int k = 0; k < m->nsets; k++) set_on(m, m->sets[k]);
|
||||
char over[256];
|
||||
snprintf(over, sizeof over, "%s", m->overrides);
|
||||
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
|
||||
@@ -395,10 +588,20 @@ void mods_exefs_switch(void) {
|
||||
if (set >= 0) rt_patch_switch(set, 0);
|
||||
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
|
||||
}
|
||||
rt_log("mods: %s: %d ExeFS patch files on", m->name, m->nsets);
|
||||
rt_log("mods: %s: %d ExeFS patch files on, %ld bytes of main's data stored", m->name, m->nsets + m->ndata,
|
||||
stored);
|
||||
}
|
||||
}
|
||||
|
||||
void mods_exefs_switch(void) {
|
||||
mods_scan();
|
||||
const RtLoaded *main_mod = NULL;
|
||||
for (int i = 0; ldr_module(i); i++)
|
||||
if (!strcmp(ldr_module(i)->name, "main")) main_mod = ldr_module(i);
|
||||
if (main_mod) mods_exefs_apply(main_mod->base, main_mod->size, main_mod->text_end, NULL);
|
||||
else mods_exefs_apply(0, 0, 0, NULL);
|
||||
}
|
||||
|
||||
int mods_exefs_pending(uint32_t *key) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
|
||||
+27
-2
@@ -2,7 +2,8 @@
|
||||
// them: <mod>/romfs, <mod>/exefs and <mod>/cheats, the Atmosphere layout (atmosphere/contents/<title id>/romfs,
|
||||
// atmosphere/exefs_patches/<name>/*.ips) or TKMM's merged output (romfs or romfslite, exefs, cheats). ExeFS patch
|
||||
// files (.ips, .pchtxt) for the game's main are patch sets the setup builds into the game (tools/exefs_patches.py),
|
||||
// switched with their mod (mods_exefs_switch). The list is scanned once at start (romfs.c overlay_init);
|
||||
// switched with their mod (mods_exefs_switch), and data writes stored into main before the game runs (mods_exefs_data).
|
||||
// The list is scanned once at start (romfs.c overlay_init);
|
||||
// its order and switches live in totk.ini [mods] (TOTK_MODS_ORDER: names top first, separated by '|';
|
||||
// TOTK_MODS_DISABLED: names switched off). The top mod wins a file two mods replace.
|
||||
#pragma once
|
||||
@@ -36,10 +37,17 @@ typedef struct {
|
||||
uint32_t pending, pending_key; // ExeFS patch files the build lacks; FNV-1a of their names and contents
|
||||
int nsets;
|
||||
uint32_t sets[16]; // the built patch sets mod_<hash> of its ExeFS patch files
|
||||
int ndata;
|
||||
uint32_t data[16]; // the hashes of its ExeFS patch files with data writes only (no set)
|
||||
uint32_t data_bytes; // the bytes its usable ExeFS patch files write into main's data, as their
|
||||
// status lines count them ("data <bytes>")
|
||||
char overrides[256]; // the port's own patch sets its patches replace, comma separated
|
||||
} ModInfo;
|
||||
|
||||
void mods_scan(void); // once; later calls return at once
|
||||
// Every switched-on mod folder's RomFS root, without the rest of the scan (no ExeFS checks, no order): for mem_init,
|
||||
// which runs before the paths mods_scan needs are set (resolution.c mod_tex_heap_kib). cb gets the name and root.
|
||||
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx);
|
||||
const char *mods_dir(void); // the folder scanned (default: <dir of totk.exe>/mods)
|
||||
int mods_count(void);
|
||||
ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
|
||||
@@ -48,8 +56,25 @@ ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
|
||||
void mods_set_enabled(int i, int on);
|
||||
void mods_move(int i, int delta); // -1 up, +1 down; nothing at the ends
|
||||
int mods_changed(void); // 1 when the order or a switch differs from the start
|
||||
// After rt_patches_init: the patch sets of the enabled mods' ExeFS patches on, the port's own sets they replace off.
|
||||
// After ldr_load and rt_patches_init, before any game code: the patch sets of the enabled mods' ExeFS patches on, the
|
||||
// port's own sets they replace off, and their data writes stored into main (mods_exefs_data).
|
||||
void mods_exefs_switch(void);
|
||||
// What mods_exefs_switch does with main's image at base (size bytes, code below code_end; size 0: no data stored), for
|
||||
// every enabled mod from the bottom one up, so that the top mod's data writes win where two mods write the same bytes.
|
||||
// store as for mods_exefs_data.
|
||||
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
|
||||
// Stores the data writes of mod m's ExeFS patch files (build-cache/exefs.data) into main's image at base: size bytes,
|
||||
// code below code_end. A DT_RELR word gets base added, as the dynamic linker adds it. Every line is checked before the
|
||||
// first store: one that falls outside [code_end, size) or does not belong to a usable file of m keeps them all out.
|
||||
// store writes n bytes at a guest address, 1 on success; NULL: into the loaded main, whose .rodata is read-only.
|
||||
// Returns the bytes stored, -1 when a line kept them out or a store failed. --probe-mods passes a buffer and memcpy.
|
||||
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
|
||||
// --probe-mods: the texts of build-cache/exefs.status and exefs.data instead of the files (NULL: read the file), and a
|
||||
// fresh scan at the next mods_scan.
|
||||
void mods_probe_cache(const char *status, const char *data);
|
||||
void mods_rescan(void);
|
||||
// The ExeFS patch files of the enabled mods that the build lacks, their FNV-1a mixed into *key (cheats.c starts the
|
||||
// rebuild for them).
|
||||
int mods_exefs_pending(uint32_t *key);
|
||||
|
||||
+88
-1
@@ -39,6 +39,93 @@ static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint32_t fnv(const char *p, size_t n) {
|
||||
uint32_t h = 0x811C9DC5u;
|
||||
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)p[i]) * 0x01000193u;
|
||||
return h;
|
||||
}
|
||||
|
||||
static ModInfo *by_name(const char *name) {
|
||||
for (int i = 0; mods_at(i); i++)
|
||||
if (!strcmp(mods_at(i)->name, name)) return mods_at(i);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// mods_exefs_data's store into a buffer standing in for main
|
||||
static int store_buffer(uint64_t a, const uint8_t *p, int n) {
|
||||
memcpy((void *)(uintptr_t)a, p, (size_t)n);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A second scan of a folder of its own: data writes of ExeFS patch files (build-cache/exefs.data, given as text).
|
||||
static void probe_data(const char *root) {
|
||||
remove_tree(root);
|
||||
plat_file_mkdir_p(root);
|
||||
const char *bid = *mods_main_build_id() ? mods_main_build_id() : "0";
|
||||
char n_ips[160], p_ips[160], q_ips[160], x_ips[160], y_ips[160];
|
||||
snprintf(n_ips, sizeof n_ips, "N/exefs/%s.ips", bid);
|
||||
snprintf(p_ips, sizeof p_ips, "P/exefs/%s.ips", bid);
|
||||
snprintf(q_ips, sizeof q_ips, "Q/exefs/%s.ips", bid);
|
||||
snprintf(x_ips, sizeof x_ips, "X/exefs/%s.ips", bid);
|
||||
snprintf(y_ips, sizeof y_ips, "Y/exefs/%s.ips", bid);
|
||||
const char *files[] = {n_ips, p_ips, q_ips, x_ips, y_ips};
|
||||
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
|
||||
uint32_t kn = fnv(n_ips, strlen(n_ips)), kp = fnv(p_ips, strlen(p_ips)), kq = fnv(q_ips, strlen(q_ips));
|
||||
uint32_t kx = fnv(x_ips, strlen(x_ips)), ky = fnv(y_ips, strlen(y_ips));
|
||||
uint32_t c = fnv("probe", 5);
|
||||
char status[512], data[768];
|
||||
snprintf(status, sizeof status,
|
||||
"%08X\t%08X\tdata\t-\tdata 11\n%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 4\n"
|
||||
"%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 1\n",
|
||||
kn, c, kp, c, kq, c, kx, c, ky, c);
|
||||
snprintf(data, sizeof data,
|
||||
"# file hash, content hash, offset in main, bytes[, base]\n"
|
||||
"%08X\t%08X\t10\t414243\n" // N: three bytes of data
|
||||
"%08X\t%08X\t18\t0800000000000000\tbase\r\n" // N: a DT_RELR word, 8 plus main's base (a CRLF line)
|
||||
"DEADBEEF\t%08X\t20\t99\n" // another file's: not N's
|
||||
"%08X\t%08X\t4\t77\n" // P: below main's data
|
||||
"%08X\t%08X\t30\t66\n" // Q: 1 of the 4 bytes its status counts
|
||||
"%08X\t%08X\t28\tBB\n" // X: the byte Y writes too
|
||||
"%08X\t%08X\t28\tAA\n", // Y, above X in the order: its byte stays
|
||||
kn, c, kn, c, c, kp, c, kq, c, kx, c, ky, c);
|
||||
mods_probe_cache(status, data);
|
||||
cfg_set("TOTK_MODS_DIR", root);
|
||||
cfg_set("TOTK_MODS_ORDER", "Y"); // Y on top, the rest by name: Y N P Q X
|
||||
cfg_set("TOTK_MODS_DISABLED", "");
|
||||
mods_rescan();
|
||||
mods_scan();
|
||||
ModInfo *n = by_name("N"), *p = by_name("P"), *q = by_name("Q");
|
||||
CHECK(n && p && q && by_name("X") && by_name("Y"), "second scan: five mods");
|
||||
if (n && p && q) {
|
||||
CHECK(n->kind == MOD_EXEFS && n->problem == MOD_OK && n->ndata == 1 && !n->nsets,
|
||||
"an ExeFS patch file with data writes only makes a usable mod");
|
||||
uint8_t img[0x40];
|
||||
memset(img, 0, sizeof img);
|
||||
uint64_t base = (uint64_t)(uintptr_t)img, word;
|
||||
long stored = mods_exefs_data(n, base, sizeof img, 8, store_buffer);
|
||||
memcpy(&word, img + 0x18, 8);
|
||||
CHECK(stored == 11 && !memcmp(img + 0x10, "ABC", 3) && word == base + 8 && !img[0x20],
|
||||
"data writes: bytes as they are, a DT_RELR word rebased, other files' lines left");
|
||||
memset(img, 0x5A, sizeof img);
|
||||
int untouched = 1;
|
||||
CHECK(mods_exefs_data(p, base, sizeof img, 8, store_buffer) == -1, "a line below main's data keeps all out");
|
||||
CHECK(n->data_bytes == 11 && q->data_bytes == 4, "the status lines count the data bytes");
|
||||
CHECK(mods_exefs_data(q, base, sizeof img, 8, store_buffer) == -1,
|
||||
"a data file holding fewer bytes than the status counts keeps all out");
|
||||
for (size_t i = 0; i < sizeof img; i++) untouched &= img[i] == 0x5A;
|
||||
CHECK(untouched, "nothing stored when a line does not fit or bytes are missing");
|
||||
// every enabled mod as at start: X and Y write the same byte, and Y, the higher one, wins as its RomFS files
|
||||
// would; P and Q stay out as above
|
||||
memset(img, 0, sizeof img);
|
||||
CHECK(EQ(mods_at(0)->name, "Y") && EQ(mods_at(4)->name, "X"), "mods_order puts Y on top and X at the bottom");
|
||||
mods_exefs_apply(base, sizeof img, 8, store_buffer);
|
||||
CHECK(img[0x28] == 0xAA, "two mods write the same byte: the top mod's byte stays");
|
||||
CHECK(!memcmp(img + 0x10, "ABC", 3) && !img[0x30], "the other mods' data as each one stores it alone");
|
||||
}
|
||||
mods_probe_cache(NULL, NULL);
|
||||
remove_tree(root);
|
||||
}
|
||||
|
||||
// s ends in tail, either path separator matching either.
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), t = strlen(tail);
|
||||
@@ -126,7 +213,7 @@ int probe_mods(void) {
|
||||
CHECK(EQ(cfg_get("TOTK_MODS_DISABLED"), "A"), "switching A off writes it");
|
||||
CHECK(mods_changed(), "changes seen");
|
||||
}
|
||||
remove_tree(root);
|
||||
probe_data(root);
|
||||
printf("mods: %s\n", fails ? "FAIL" : "ok");
|
||||
int table = restbl_probe();
|
||||
return fails || table ? 1 : 0;
|
||||
|
||||
+143
-17
@@ -141,10 +141,13 @@ static void pool_drain(void) {
|
||||
// system-managed page file, so the same request succeeds a moment later, and a stall of some frames is better than
|
||||
// ending the game. Frees the pooled buffers first. NULL only after about 30 s without memory; the caller ends the game.
|
||||
// Never called with a lock held.
|
||||
static void big_drain(void);
|
||||
|
||||
static void *realloc_wait(void *old, size_t n, const char *what) {
|
||||
void *p = realloc(old, n);
|
||||
if (p) return p;
|
||||
pool_drain();
|
||||
big_drain();
|
||||
static uint32_t reports;
|
||||
int report = __atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8;
|
||||
if (report) {
|
||||
@@ -406,8 +409,10 @@ void nvn_pins_stats(uint64_t *live, uint64_t *freed) {
|
||||
// adding control memory again releases the copies recorded there without walking every live copy. 4 KiB pages cost
|
||||
// about 500 lookups per AddControlMemory under the exclusive lock (1.4.3 field: 53 calls a frame, 2 MiB ranges, 3.7
|
||||
// copies per page), and the recording threads waited on it.
|
||||
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs (pin_refs)
|
||||
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins; } copy_prefix;
|
||||
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs; pin_v: those pins, in
|
||||
// record order, or NULL (then pin_refs walks the records); cap: bytes of the block from the large-block cache
|
||||
// (copy_block), 0 for a plain heap block
|
||||
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins, cap; const tex_t **pin_v; } copy_prefix;
|
||||
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
|
||||
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
|
||||
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
|
||||
@@ -429,15 +434,117 @@ static void zombie_add(copy_prefix *pf) { // copy_lock held
|
||||
zombies[nzombies++] = pf;
|
||||
}
|
||||
|
||||
static void copy_free(copy_prefix *pf) { // copy_lock held
|
||||
if (pf->pins) pin_refs((const uint8_t *)(pf + 1), -1);
|
||||
// Blocks of large copies are kept for the next ones. 1.4.3 makes two copies of 0.5-2 MiB a frame (the per-frame
|
||||
// recordings that inline many others); above the heap's direct-allocation threshold (about 512 KiB) every malloc mapped
|
||||
// fresh zero-filled pages and every free unmapped them, so the copy ran at half the speed of a small one and the free
|
||||
// held copy_lock for tens of microseconds. A block is reused for a copy that fills at least half of it.
|
||||
#define BIG_MIN ((size_t)256 << 10) // smaller blocks come from the heap and go back to it
|
||||
#define BIG_MAX ((size_t)16 << 20) // a larger block is never kept
|
||||
#define BIG_SLOTS 8
|
||||
static PlatMutex big_lock = PLAT_MUTEX_INIT; // lock order: copy_lock, then big_lock
|
||||
static struct { copy_prefix *p; size_t cap; } big[BIG_SLOTS];
|
||||
static uint32_t nbig;
|
||||
|
||||
static void big_drain(void) {
|
||||
plat_mutex_lock(&big_lock);
|
||||
while (nbig) free(big[--nbig].p);
|
||||
plat_mutex_unlock(&big_lock);
|
||||
}
|
||||
|
||||
// A block for a copy of n bytes after its prefix; prefix.cap says where it goes back (copy_block_free). wait: a failed
|
||||
// allocation waits for memory (realloc_wait), which is never done with a lock held; else it ends the game at once.
|
||||
static copy_prefix *copy_block(size_t n, int wait, const char *what) {
|
||||
size_t total = sizeof(copy_prefix) + n, cap = 0;
|
||||
copy_prefix *pf = NULL;
|
||||
if (total >= BIG_MIN && total <= BIG_MAX) {
|
||||
plat_mutex_lock(&big_lock);
|
||||
uint32_t best = BIG_SLOTS;
|
||||
for (uint32_t i = 0; i < nbig; i++)
|
||||
if (big[i].cap >= total && big[i].cap / 2 <= total && (best == BIG_SLOTS || big[i].cap < big[best].cap))
|
||||
best = i;
|
||||
if (best < BIG_SLOTS) {
|
||||
pf = big[best].p;
|
||||
cap = big[best].cap;
|
||||
big[best] = big[--nbig];
|
||||
}
|
||||
plat_mutex_unlock(&big_lock);
|
||||
if (!pf) cap = (total + total / 8 + 0xffff) & ~(size_t)0xffff; // room for the next frame's slightly larger one
|
||||
}
|
||||
if (!pf) pf = wait ? realloc_wait(NULL, cap ? cap : total, what) : malloc(cap ? cap : total);
|
||||
if (!pf) rt_fatal("nvn: %s allocation (%zu bytes)", what, cap ? cap : total);
|
||||
pf->cap = cap;
|
||||
return pf;
|
||||
}
|
||||
|
||||
static void copy_block_free(copy_prefix *pf) {
|
||||
size_t cap = pf->cap;
|
||||
if (cap) {
|
||||
plat_mutex_lock(&big_lock);
|
||||
if (nbig < BIG_SLOTS) {
|
||||
big[nbig].p = pf;
|
||||
big[nbig++].cap = cap;
|
||||
pf = NULL;
|
||||
} else {
|
||||
uint32_t small = 0;
|
||||
for (uint32_t i = 1; i < nbig; i++)
|
||||
if (big[i].cap < big[small].cap) small = i;
|
||||
if (big[small].cap < cap) {
|
||||
copy_prefix *drop = big[small].p;
|
||||
big[small].p = pf;
|
||||
big[small].cap = cap;
|
||||
pf = drop;
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&big_lock);
|
||||
}
|
||||
free(pf);
|
||||
}
|
||||
|
||||
static void zombie_sweep(uint64_t replayed) { // copy_lock held
|
||||
// Counts (delta 1) or uncounts (-1) the pins a copy names: from its list, without walking its records. A copy of a
|
||||
// per-frame recording is 0.5-2 MiB of records naming a few pins; walking them again at every free cost about 40 us.
|
||||
static void copy_pins_ref(const copy_prefix *pf, int delta) {
|
||||
if (!pf->pins) return;
|
||||
if (!pf->pin_v) {
|
||||
pin_refs((const uint8_t *)(pf + 1), delta);
|
||||
return;
|
||||
}
|
||||
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
|
||||
for (uint64_t i = 0; i < pf->pins; i++) pin_ref(pf->pin_v[i], delta, now);
|
||||
}
|
||||
|
||||
// A copy in no map and no zombie list: nothing else can reach it, so copy_lock need not be held.
|
||||
static void copy_free(copy_prefix *pf) {
|
||||
copy_pins_ref(pf, -1);
|
||||
free((void *)pf->pin_v);
|
||||
copy_block_free(pf);
|
||||
}
|
||||
|
||||
// Copies retired under copy_lock and freed after it is released: the frees, the unpinning walk over their records and
|
||||
// the large blocks kept the lock exclusive for most of release_copies, while the game's other recording threads waited
|
||||
// for it in copy_touch (1.4.3 town: three threads, 0.6 ms a frame of waiting).
|
||||
typedef struct { copy_prefix **v; uint32_t n, cap; } dead_list;
|
||||
static __thread dead_list dead;
|
||||
|
||||
static void dead_add(copy_prefix *pf) {
|
||||
if (dead.n == dead.cap) {
|
||||
uint32_t cap = dead.cap ? dead.cap * 2 : 256;
|
||||
copy_prefix **v = realloc(dead.v, sizeof *v * cap);
|
||||
if (!v) { copy_free(pf); return; } // out of memory: free it here, under the lock, as before
|
||||
dead.v = v;
|
||||
dead.cap = cap;
|
||||
}
|
||||
dead.v[dead.n++] = pf;
|
||||
}
|
||||
|
||||
static void dead_free(void) { // copy_lock not held
|
||||
for (uint32_t i = 0; i < dead.n; i++) copy_free(dead.v[i]);
|
||||
dead.n = 0;
|
||||
}
|
||||
|
||||
static void zombie_sweep(uint64_t replayed) { // copy_lock held; the swept copies go to the dead list
|
||||
for (uint32_t i = 0; i < nzombies;) {
|
||||
if (zombies[i]->used_at <= replayed) {
|
||||
copy_free(zombies[i]);
|
||||
dead_add(zombies[i]);
|
||||
zombies[i] = zombies[--nzombies];
|
||||
} else {
|
||||
i++;
|
||||
@@ -477,13 +584,16 @@ static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
|
||||
}
|
||||
|
||||
// The recording [p, p + n), OP_BEGIN to OP_END (the recording buffer is one block: recording emits no OP_JUMP), copied
|
||||
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
|
||||
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
|
||||
// into one host block (copy_block: its exact size, or a kept large block). One allocation and one copy: growing the
|
||||
// copy record by record cost the game's main thread about 1 ms a frame in reallocations and small copies (1.4.3,
|
||||
// field). The pins the records name are counted and listed (copy_prefix.pin_v) on the way.
|
||||
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
|
||||
if (!out) rt_fatal("nvn: command copy allocation");
|
||||
copy_prefix *block = copy_block(n, 1, "command copy");
|
||||
uint8_t *out = (uint8_t *)block;
|
||||
memcpy(out + sizeof(copy_prefix), p, n);
|
||||
static __thread struct { const tex_t **v; uint64_t cap; } named; // the pins of this recording, reused
|
||||
uint64_t pins_named = 0;
|
||||
int listed = 1; // 0 after a failed growth: the copy goes without a list
|
||||
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
|
||||
op_hdr h;
|
||||
memcpy(&h, p + at, sizeof h);
|
||||
@@ -493,6 +603,14 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
const tex_t *pinned[9];
|
||||
int k = op_pins(h.op, out + sizeof(copy_prefix) + at + 8, pinned);
|
||||
for (int i = 0; i < k; i++) pin_ref(pinned[i], 1, 0);
|
||||
if (k && listed && pins_named + (uint64_t)k > named.cap) {
|
||||
uint64_t cap = named.cap ? named.cap * 2 : 64;
|
||||
while (cap < pins_named + (uint64_t)k) cap *= 2;
|
||||
const tex_t **v = realloc((void *)named.v, sizeof *v * cap);
|
||||
if (v) named.v = v, named.cap = cap;
|
||||
else listed = 0;
|
||||
}
|
||||
if (k && listed) memcpy((void *)(named.v + pins_named), pinned, sizeof *pinned * (size_t)k);
|
||||
pins_named += (uint64_t)k;
|
||||
at += h.size;
|
||||
if (h.op == OP_END) {
|
||||
@@ -500,7 +618,10 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
*(copy_prefix *)out = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named};
|
||||
const tex_t **pin_v = NULL;
|
||||
if (pins_named && listed && (pin_v = malloc(sizeof *pin_v * pins_named)))
|
||||
memcpy((void *)pin_v, (const void *)named.v, sizeof *pin_v * pins_named);
|
||||
*block = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named, .cap = block->cap, .pin_v = pin_v};
|
||||
return out + sizeof(copy_prefix);
|
||||
}
|
||||
|
||||
@@ -550,7 +671,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
|
||||
copy_bytes -= pf->bytes;
|
||||
pf->magic = 0;
|
||||
if (pf->used_at > replayed) zombie_add(pf); // a queued submission still reads it
|
||||
else copy_free(pf);
|
||||
else dead_add(pf);
|
||||
b->v[i] = b->v[--b->n];
|
||||
}
|
||||
if (!b->n) {
|
||||
@@ -560,6 +681,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(©_lock);
|
||||
dead_free();
|
||||
}
|
||||
|
||||
// ---------- CommandBuffer: object and memory
|
||||
@@ -1182,11 +1304,14 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
|
||||
}
|
||||
out[i] = handles[i];
|
||||
if (pf->used_at > replayed && pf->used_at != now && pf->deferred != 1) {
|
||||
size_t sz = sizeof *pf + pf->bytes;
|
||||
copy_prefix *cl = malloc(sz);
|
||||
if (!cl) rt_fatal("nvn: out of memory (recording clone, %zu bytes)", sz);
|
||||
memcpy(cl, pf, sz);
|
||||
if (cl->pins) pin_refs((const uint8_t *)(cl + 1), 1);
|
||||
copy_prefix *cl = copy_block(pf->bytes, 0, "recording clone"); // copy_lock held
|
||||
size_t cap = cl->cap;
|
||||
memcpy(cl, pf, sizeof *pf + pf->bytes);
|
||||
cl->cap = cap;
|
||||
cl->pin_v = NULL;
|
||||
if (pf->pin_v && (cl->pin_v = malloc(sizeof *cl->pin_v * pf->pins)))
|
||||
memcpy((void *)cl->pin_v, (const void *)pf->pin_v, sizeof *cl->pin_v * pf->pins);
|
||||
copy_pins_ref(cl, 1);
|
||||
cl->magic = 0; // not a live handle: the game never sees it
|
||||
cl->used_at = now;
|
||||
zombie_add(cl);
|
||||
@@ -1197,6 +1322,7 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(©_lock);
|
||||
dead_free();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int nd = resolve_deferred((const uint8_t *)(uintptr_t)out[i], 0);
|
||||
if (out[i] == handles[i]) ((copy_prefix *)(uintptr_t)out[i] - 1)->deferred = nd ? 2 : 1;
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Play time: how long the player has spent in the game, across runs, shown on the Game tab of the settings menu
|
||||
// (ui/menu.cpp tab_game). The game itself keeps no play time a player can see, and the Switch's own play log is not
|
||||
// there to read, so the port counts it.
|
||||
//
|
||||
// One host thread (it never holds a virtual core, as discord_rpc.c's) wakes every second and adds the time since its
|
||||
// last wake when the window has the focus (gfx_window_focused) and the game presented at least one frame meanwhile
|
||||
// (gfx_stats). So the shader prewarm before the first frame, a window left in the background and a hung game do not
|
||||
// count; loading screens and the game's own menus do, as they present frames. One wake adds at most TICK_CAP_MS, so a
|
||||
// machine that slept with the game open adds two seconds, not the night.
|
||||
//
|
||||
// The total lives in <profile>/playtime.txt, next to account.uid and the saves, so it stays with them when the
|
||||
// install is updated or moved. It is plain text, "total_seconds <n>", written atomically (plat_file_write_atomic)
|
||||
// every SAVE_EVERY_MS while it grows and once more when the game exits (playtime_save from boot.c, svc.c and
|
||||
// gfx_present.c, next to prof_write); a crash loses at most SAVE_EVERY_MS. A file that does not read as that is kept
|
||||
// as playtime.txt.bad and the count starts again from zero. A scripted run (--seconds) reads the total and adds nothing.
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define TICK_MS 1000
|
||||
#define TICK_CAP_MS 2000
|
||||
#define SAVE_EVERY_MS 30000
|
||||
#define TOTAL_CAP_S (100ull * 365 * 24 * 3600) // a hundred years: anything larger is a damaged file
|
||||
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT; // the totals and the file
|
||||
static uint64_t total_ms, session_ms, saved_ms;
|
||||
static int loaded; // 1 once the file was read (or found missing), -1 when there is no profile
|
||||
static char path[1100];
|
||||
|
||||
// The seconds of a playtime.txt: "total_seconds <n>", a line end or nothing after it, a UTF-8 byte order mark before it
|
||||
// (an editor may add one). 1 when the file reads so and n is at most TOTAL_CAP_S.
|
||||
static int parse(const uint8_t *b, uint64_t n, uint64_t *seconds) {
|
||||
if (n >= 3 && !memcmp(b, "\xEF\xBB\xBF", 3)) b += 3, n -= 3;
|
||||
char text[64] = {0};
|
||||
memcpy(text, b, n < sizeof text - 1 ? n : sizeof text - 1);
|
||||
unsigned long long s = 0;
|
||||
char tail = 0;
|
||||
if (sscanf(text, "total_seconds %llu%c", &s, &tail) < 1 || (tail && tail != '\n' && tail != '\r') ||
|
||||
s > TOTAL_CAP_S)
|
||||
return 0;
|
||||
*seconds = s;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Holding lock. dir: the profile directory.
|
||||
static void load(const char *dir) {
|
||||
if (loaded) return;
|
||||
if (!dir || !*dir || snprintf(path, sizeof path, "%s/playtime.txt", dir) >= (int)sizeof path) {
|
||||
loaded = -1;
|
||||
rt_log("playtime: no profile directory; play time is not counted");
|
||||
return;
|
||||
}
|
||||
loaded = 1;
|
||||
uint64_t n = 0, s = 0;
|
||||
uint8_t *b = plat_file_read(path, &n);
|
||||
if (!b) {
|
||||
rt_log("playtime: no %s yet; counting from zero", path);
|
||||
return;
|
||||
}
|
||||
if (parse(b, n, &s)) {
|
||||
free(b);
|
||||
total_ms = saved_ms = s * 1000;
|
||||
rt_log("playtime: %llu h %02llu min played before this run (%s)", (unsigned long long)(s / 3600),
|
||||
(unsigned long long)(s / 60 % 60), path);
|
||||
return;
|
||||
}
|
||||
char bad[1110];
|
||||
snprintf(bad, sizeof bad, "%s.bad", path);
|
||||
int kept = plat_file_write_atomic(bad, b, (size_t)n); // the whole file, for the player to recover the number
|
||||
free(b);
|
||||
rt_log("playtime: %s does not read as \"total_seconds <n>\"; %s, counting from zero", path,
|
||||
kept ? "kept as playtime.txt.bad" : "could not keep a copy");
|
||||
}
|
||||
|
||||
// Holding lock.
|
||||
static void save_locked(void) {
|
||||
if (loaded != 1 || total_ms / 1000 == saved_ms / 1000) return;
|
||||
char text[48];
|
||||
int n = snprintf(text, sizeof text, "total_seconds %" PRIu64 "\n", total_ms / 1000);
|
||||
if (plat_file_write_atomic(path, text, (size_t)n)) saved_ms = total_ms;
|
||||
else rt_log("playtime: cannot write %s", path);
|
||||
}
|
||||
|
||||
void playtime_save(void) {
|
||||
plat_mutex_lock(&lock);
|
||||
save_locked();
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s) {
|
||||
plat_mutex_lock(&lock);
|
||||
int ok = loaded == 1;
|
||||
*total_s = total_ms / 1000;
|
||||
*session_s = session_ms / 1000;
|
||||
plat_mutex_unlock(&lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Holding lock: the time since the last tick, d ms, when the game was played meanwhile; the file every SAVE_EVERY_MS.
|
||||
static void tick_locked(uint64_t now, uint64_t d, int playing, uint64_t *last_save) {
|
||||
if (d > TICK_CAP_MS) d = TICK_CAP_MS;
|
||||
if (playing) total_ms += d, session_ms += d;
|
||||
if (now - *last_save >= SAVE_EVERY_MS) save_locked(), *last_save = now;
|
||||
}
|
||||
|
||||
static void playtime_main(void *arg) {
|
||||
(void)arg;
|
||||
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
|
||||
uint64_t last = plat_now_ms(), last_save = last, frames = gfx_stats().frames;
|
||||
for (;;) {
|
||||
plat_sleep_ms(TICK_MS);
|
||||
uint64_t now = plat_now_ms(), f = gfx_stats().frames;
|
||||
int playing = f != frames && gfx_window_focused();
|
||||
plat_mutex_lock(&lock);
|
||||
tick_locked(now, now - last, playing, &last_save);
|
||||
plat_mutex_unlock(&lock);
|
||||
last = now, frames = f;
|
||||
}
|
||||
}
|
||||
|
||||
// The file is read at once, so the Game tab shows the total from the first frame. A scripted run (--seconds: benches,
|
||||
// menu shots, input scripts) shows it but adds nothing: those runs are not play.
|
||||
void playtime_init(void) {
|
||||
static int32_t started;
|
||||
if (plat_xchg32(&started, 1)) return;
|
||||
plat_mutex_lock(&lock);
|
||||
load(rt_config()->profile);
|
||||
int on = loaded == 1;
|
||||
plat_mutex_unlock(&lock);
|
||||
if (!on) return;
|
||||
if (rt_config()->seconds > 0) {
|
||||
rt_log("playtime: a scripted run (--seconds); play time is shown, not counted");
|
||||
return;
|
||||
}
|
||||
if (!plat_thread_spawn(playtime_main, NULL, "playtime", 0))
|
||||
rt_log("playtime: cannot start its thread; play time is not counted");
|
||||
}
|
||||
|
||||
// --probe-playtime: the file's parsing, a missing, a valid and a damaged file, the periodic save and the tick's cap, in
|
||||
// a directory next to totk.exe. Needs no game files.
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL playtime: %s\n", what); } } while (0)
|
||||
|
||||
static void reset(void) { total_ms = session_ms = saved_ms = 0, loaded = 0, path[0] = 0; }
|
||||
|
||||
static int file_is(const char *p, const char *want) {
|
||||
uint64_t n = 0;
|
||||
uint8_t *b = plat_file_read(p, &n);
|
||||
int ok = b && n == strlen(want) && !memcmp(b, want, n);
|
||||
free(b);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int probe_playtime(void) {
|
||||
uint64_t s = 0;
|
||||
CHECK(parse((const uint8_t *)"total_seconds 42\n", 17, &s) && s == 42, "a line with LF");
|
||||
CHECK(parse((const uint8_t *)"total_seconds 42\r\n", 18, &s) && s == 42, "a line with CRLF");
|
||||
CHECK(parse((const uint8_t *)"total_seconds 42", 16, &s) && s == 42, "no line end");
|
||||
CHECK(parse((const uint8_t *)"\xEF\xBB\xBFtotal_seconds 7\n", 19, &s) && s == 7, "a byte order mark");
|
||||
CHECK(!parse((const uint8_t *)"total_seconds 42x", 17, &s), "a character after the number");
|
||||
CHECK(!parse((const uint8_t *)"total_seconds -1\n", 17, &s), "a negative number");
|
||||
CHECK(!parse((const uint8_t *)"total_seconds 3153600001\n", 25, &s), "over a hundred years");
|
||||
CHECK(!parse((const uint8_t *)"seconds 42\n", 11, &s), "another key");
|
||||
CHECK(!parse((const uint8_t *)"", 0, &s), "an empty file");
|
||||
|
||||
char dir[1024];
|
||||
if (!plat_exe_path(dir, sizeof dir)) return 1;
|
||||
char *cut = plat_last_sep(dir);
|
||||
if (!cut) return 1;
|
||||
strcpy(cut + 1, "playtime-probe");
|
||||
plat_file_mkdir_p(dir);
|
||||
char file[1100], bad[1110];
|
||||
snprintf(file, sizeof file, "%s/playtime.txt", dir);
|
||||
snprintf(bad, sizeof bad, "%s.bad", file);
|
||||
plat_file_delete(file);
|
||||
plat_file_delete(bad);
|
||||
|
||||
uint64_t total, session, last_save = 0;
|
||||
reset();
|
||||
load(dir);
|
||||
CHECK(playtime_get(&total, &session) && total == 0, "a missing file counts from zero");
|
||||
tick_locked(1000, 1000, 1, &last_save);
|
||||
tick_locked(2000, 1000, 0, &last_save);
|
||||
CHECK(plat_file_size(file) < 0, "nothing is written before SAVE_EVERY_MS");
|
||||
tick_locked(2000 + 3600000, 3600000, 1, &last_save); // a machine that slept: one tick adds TICK_CAP_MS, and saves
|
||||
playtime_get(&total, &session);
|
||||
CHECK(total == 3 && session == 3, "played ticks count, an idle one does not, a long one is capped");
|
||||
CHECK(file_is(file, "total_seconds 3\n"), "the save after SAVE_EVERY_MS writes the total");
|
||||
|
||||
reset();
|
||||
load(dir);
|
||||
playtime_get(&total, &session);
|
||||
CHECK(total == 3 && session == 0, "the next run reads the total back");
|
||||
|
||||
static const char damaged[] = "total_seconds twelve\n";
|
||||
plat_file_write_atomic(file, damaged, sizeof damaged - 1);
|
||||
reset();
|
||||
load(dir);
|
||||
playtime_get(&total, &session);
|
||||
CHECK(total == 0, "a damaged file counts from zero");
|
||||
CHECK(file_is(bad, damaged), "a damaged file is kept whole as playtime.txt.bad");
|
||||
|
||||
plat_file_delete(file);
|
||||
plat_file_delete(bad);
|
||||
reset();
|
||||
fprintf(stderr, "playtime: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
+164
-15
@@ -39,6 +39,7 @@
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define BASE_W 1920
|
||||
@@ -46,6 +47,7 @@
|
||||
#define SCENE_W 1600
|
||||
#define SCENE_H 900
|
||||
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
|
||||
#define TEX_HEAP_KIB 506880u // TextureHeap, shipped (1.2.1 and 1.4.3): what a mod's own HeapDef is measured against
|
||||
#define SCENE_MAX 4096 // per side, as MacNativeGames' TOTK_RENDER_RES
|
||||
#define SHADOW_BASE 960 // the sun shadow map's base size (its cascades: a D16 array of 4 layers)
|
||||
|
||||
@@ -120,6 +122,25 @@ static double dlss_share(const char *m) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A game resolution that is not 16:9 needs the HUD's layout for its shape (romfs-patches-<w>x<h>, romfs.c
|
||||
// overlay_init), which the installer makes but a developer build only has after tools/make_romfs_patches.py --aspect.
|
||||
// Without it the camera would widen while the 16:9 HUD stretched across the picture, so the game draws the largest
|
||||
// 16:9 size that fits inside the asked one and the camera aspect patch stays off (res_init); gfx_present.c shows the
|
||||
// picture with bars (present setting aspect). Not when TOTK_ROMFS_PATCHES names the overlay itself or the game version
|
||||
// has no camera aspect patch. TOTK_OUTPUT_RES is restart-only (cfg.c, RESTART), so this one decision holds all run.
|
||||
static void shape_fallback(void) {
|
||||
const char *own = cfg_get("TOTK_ROMFS_PATCHES");
|
||||
unsigned out[2] = {R.ow, R.oh};
|
||||
if (own || !res_wants_wide_overlay(out) || romfs_shaped_ready(R.ow, R.oh)) return;
|
||||
unsigned h = R.oh, w = R.ow;
|
||||
if ((uint64_t)w * 9 <= (uint64_t)h * 16) h = (unsigned)((uint64_t)w * 9 / 16) & ~1u;
|
||||
w = (unsigned)((uint64_t)h * 16 / 9) & ~1u;
|
||||
rt_log("resolution: no romfs-patches-%ux%u for the HUD at this shape; the game draws %ux%u (16:9) and the present "
|
||||
"adds bars. Make the layout with: tools/make_romfs_patches.py --version <v121|v143> --aspect %ux%u --out "
|
||||
"build/<version>/romfs-patches-%ux%u", R.ow, R.oh, w, h, R.ow, R.oh, R.ow, R.oh);
|
||||
R.ow = w, R.oh = h;
|
||||
}
|
||||
|
||||
static void decide(void) {
|
||||
if (R.decided) return;
|
||||
R.decided = 1;
|
||||
@@ -131,6 +152,7 @@ static void decide(void) {
|
||||
if (sscanf(o, "%ux%u%c", &w, &h, &tail) == 2 && w >= 1280 && h >= 720 && w <= 7680 && h <= 4320) {
|
||||
R.ow = w & ~1u;
|
||||
R.oh = h & ~1u;
|
||||
shape_fallback();
|
||||
} else {
|
||||
rt_log("resolution: TOTK_OUTPUT_RES=%s is not <width>x<height> in 1280x720..7680x4320; 1920x1080", o);
|
||||
}
|
||||
@@ -189,6 +211,33 @@ uint64_t res_heap_extra_kib(void) {
|
||||
|
||||
uint64_t res_heap_extra_bytes(void) { return ((res_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
||||
|
||||
// TOTK_TEX_HEAP_EXTRA_MB (0 to 4096): more room in "TextureHeap" and its parent "ゲームグラフィックス", the heap the
|
||||
// world's textures stream into. HD texture packs made for emulators with 8 GB of DRAM grow it by about 3.3 GiB in their
|
||||
// own HeapDef; without the room the game runs out and starts its panic Blood Moon over and over. The pool and the heap
|
||||
// cap grow by the same amount (memory.c mem_init).
|
||||
static uint64_t mod_tex_heap_kib(void);
|
||||
|
||||
static uint64_t cfg_tex_heap_kib(void) {
|
||||
const char *v = cfg_get("TOTK_TEX_HEAP_EXTRA_MB");
|
||||
if (!v || !*v) return 0;
|
||||
char *end;
|
||||
long mb = strtol(v, &end, 10);
|
||||
if (end == v || *end || mb < 0 || mb > 4096) {
|
||||
static int logged;
|
||||
if (!logged++) rt_log("resolution: TOTK_TEX_HEAP_EXTRA_MB=%s is not 0..4096; 0", v);
|
||||
return 0;
|
||||
}
|
||||
return (uint64_t)mb << 10;
|
||||
}
|
||||
|
||||
// The larger of TOTK_TEX_HEAP_EXTRA_MB and what an enabled mod's own HeapDef adds to the texture heap (mod_tex_heap_kib).
|
||||
static uint64_t tex_heap_extra_kib(void) {
|
||||
uint64_t c = cfg_tex_heap_kib(), m = mod_tex_heap_kib();
|
||||
return c > m ? c : m;
|
||||
}
|
||||
|
||||
uint64_t res_tex_heap_extra_bytes(void) { return ((tex_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
||||
|
||||
// ---- the HeapDef table. BYML version 2..7, little endian ("YB"): a hash node is {u8 0xC1, u24 count} and count
|
||||
// entries {u24 key index, u8 type, u32 value}; an int (type 0xD1) is stored in the entry, so a size changes in place.
|
||||
typedef struct { uint8_t *p; size_t n; uint32_t keys; } Byml;
|
||||
@@ -226,28 +275,103 @@ static size_t heap_size_field(const Byml *b, uint32_t heaps, const char *heap) {
|
||||
return at && u32_at(b, at, &node) ? field(b, node, "Size", 0xd1) : 0;
|
||||
}
|
||||
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
uint64_t extra = res_heap_extra_kib();
|
||||
*out = NULL;
|
||||
if (!extra) return 0;
|
||||
// A HeapDef in b's place (zstd frame in[0..n) with its content size, no dictionary): b.p malloc'd and *heaps its
|
||||
// "Heaps" node; 0 when it is not one (b.p freed). why: what the log line says the caller keeps then.
|
||||
static int heapdef_open(const void *in, size_t n, Byml *b, uint32_t *heaps, const char *why) {
|
||||
b->p = NULL;
|
||||
if (ZSTD_getDictID_fromFrame(in, n)) {
|
||||
rt_log("resolution: the HeapDef table needs a zstd dictionary; the game keeps its own resolution");
|
||||
rt_log("resolution: the HeapDef table needs a zstd dictionary; %s", why);
|
||||
return 0;
|
||||
}
|
||||
unsigned long long raw = ZSTD_getFrameContentSize(in, n);
|
||||
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > (64u << 20)) {
|
||||
rt_log("resolution: the HeapDef table is not a zstd frame with a size; the game keeps its own resolution");
|
||||
rt_log("resolution: the HeapDef table is not a zstd frame with a size; %s", why);
|
||||
return 0;
|
||||
}
|
||||
Byml b = {malloc((size_t)raw), (size_t)raw, 0};
|
||||
if (!b.p) return 0;
|
||||
size_t got = ZSTD_decompress(b.p, b.n, in, n);
|
||||
uint32_t root, heaps = 0;
|
||||
*b = (Byml){malloc((size_t)raw), (size_t)raw, 0};
|
||||
if (!b->p) return 0;
|
||||
size_t got = ZSTD_decompress(b->p, b->n, in, n);
|
||||
uint32_t root;
|
||||
uint16_t ver = 0;
|
||||
if (!ZSTD_isError(got) && got == b.n && b.n >= 16) memcpy(&ver, b.p + 2, 2);
|
||||
size_t heaps_at = 0, dta_at = 0, gfx_at = 0;
|
||||
if (ver >= 2 && ver <= 7 && !memcmp(b.p, "YB", 2) && u32_at(&b, 4, &b.keys) && u32_at(&b, 12, &root) &&
|
||||
(heaps_at = field(&b, root, "Heaps", 0xc1)) && u32_at(&b, heaps_at, &heaps)) {
|
||||
if (!ZSTD_isError(got) && got == b->n && b->n >= 16) memcpy(&ver, b->p + 2, 2);
|
||||
size_t heaps_at = 0;
|
||||
if (ver >= 2 && ver <= 7 && !memcmp(b->p, "YB", 2) && u32_at(b, 4, &b->keys) && u32_at(b, 12, &root) &&
|
||||
(heaps_at = field(b, root, "Heaps", 0xc1)) && u32_at(b, heaps_at, heaps))
|
||||
return 1;
|
||||
free(b->p);
|
||||
b->p = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// HD texture packs made for the emulators ship their own HeapDef with "TextureHeap" and its parent grown (HDPacks.com's
|
||||
// 4K pack for TotK: by 3,419,000 KiB, 3.3 GiB, for the emulators' 8 GB memory option). Served as it is, the game asks
|
||||
// for that room and the application pool has none: it crashed at start (issue #673). So before mem_init sizes the pool,
|
||||
// every switched-on mod's HeapDef is read (mods.c mods_peek_romfs: mods_scan needs paths main sets later) and the
|
||||
// largest TextureHeap growth over the shipped size counts as TOTK_TEX_HEAP_EXTRA_MB (the larger of the two, at most
|
||||
// 4 GiB). romfs.c heapdef_overlay then serves another table grown by that amount in the pack's place: the table of the
|
||||
// highest switched-on mod below it whose texture heap is not grown, else the game's own, so the other heaps keep the
|
||||
// sizes the content that runs needs.
|
||||
#define HEAPDEF_REL "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
||||
static struct { int done; uint64_t kib; } MT;
|
||||
|
||||
// "TextureHeap" of the HeapDef table in[0..n) in KiB; 0 when the table cannot be read or has no such size (the failure
|
||||
// is logged with why).
|
||||
static int32_t heapdef_tex_kib(const void *in, size_t n, const char *why) {
|
||||
Byml b;
|
||||
uint32_t heaps;
|
||||
size_t th_at = 0;
|
||||
int32_t th = 0;
|
||||
if (!heapdef_open(in, n, &b, &heaps, why)) return 0;
|
||||
if ((th_at = heap_size_field(&b, heaps, "TextureHeap"))) memcpy(&th, b.p + th_at, 4);
|
||||
else rt_log("resolution: the HeapDef table has no TextureHeap size; %s", why);
|
||||
free(b.p);
|
||||
return th;
|
||||
}
|
||||
|
||||
static void mod_heapdef(const char *name, const char *romfs, void *ctx) {
|
||||
(void)ctx;
|
||||
char path[2300], why[400];
|
||||
if (snprintf(path, sizeof path, "%s/%s", romfs, HEAPDEF_REL) >= (int)sizeof path) return;
|
||||
uint64_t n = 0;
|
||||
uint8_t *in = plat_file_read(path, &n);
|
||||
if (!in) return;
|
||||
snprintf(why, sizeof why, "mod %s: its texture heap growth is left out", name);
|
||||
int32_t th = heapdef_tex_kib(in, (size_t)n, why);
|
||||
free(in);
|
||||
if (!th) return;
|
||||
int64_t grow = (int64_t)th - (int64_t)TEX_HEAP_KIB;
|
||||
if (grow > (int64_t)(4096u << 10)) grow = 4096u << 10;
|
||||
if (grow > 0 && (uint64_t)grow > MT.kib) MT.kib = (uint64_t)grow;
|
||||
rt_log("resolution: mod %s ships a HeapDef with TextureHeap %d KiB (the game's %u KiB)%s", name, th, TEX_HEAP_KIB,
|
||||
grow > 0 ? ": the port grows the texture heap and the pool by the difference" : "");
|
||||
}
|
||||
|
||||
static uint64_t mod_tex_heap_kib(void) {
|
||||
if (!MT.done) MT.done = 1, mods_peek_romfs(mod_heapdef, NULL);
|
||||
return MT.kib;
|
||||
}
|
||||
|
||||
// Only such a table is swapped for the game's own (romfs.c heapdef_overlay): a mod table that changes other heaps
|
||||
// keeps them. A table it cannot read stays the base (logged).
|
||||
int res_heapdef_tex_grown(const void *in, size_t n) {
|
||||
return heapdef_tex_kib(in, n, "the mod's table stays the base") > (int32_t)TEX_HEAP_KIB;
|
||||
}
|
||||
|
||||
// A table that can stand in for an HD pack's (romfs.c heapdef_overlay): readable, with TextureHeap at most the shipped
|
||||
// size. A content mod's table (The Second Quest grows RSDB and UI for its own files) is one.
|
||||
int res_heapdef_plain(const void *in, size_t n) {
|
||||
int32_t th = heapdef_tex_kib(in, n, "it does not stand in for the texture pack's table");
|
||||
return th > 0 && th <= (int32_t)TEX_HEAP_KIB;
|
||||
}
|
||||
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
uint64_t extra = res_heap_extra_kib(), tex = tex_heap_extra_kib();
|
||||
*out = NULL;
|
||||
if (!extra && !tex) return 0;
|
||||
Byml b;
|
||||
uint32_t heaps = 0;
|
||||
size_t dta_at = 0, gfx_at = 0;
|
||||
if (heapdef_open(in, n, &b, &heaps, "the game keeps its own resolution")) {
|
||||
dta_at = heap_size_field(&b, heaps, "DynamicTextureAllocatorMem");
|
||||
gfx_at = heap_size_field(&b, heaps, "Graphics");
|
||||
}
|
||||
@@ -263,6 +387,26 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
int32_t dta2 = dta + (int32_t)extra, gfx2 = gfx + (int32_t)extra;
|
||||
memcpy(b.p + dta_at, &dta2, 4);
|
||||
memcpy(b.p + gfx_at, &gfx2, 4);
|
||||
int32_t th = 0, th2 = 0, gg = 0, gg2 = 0;
|
||||
if (tex) { // TOTK_TEX_HEAP_EXTRA_MB: the world texture heap and its parent grow by the same amount
|
||||
static const char parent[] = "\xe3\x82\xb2\xe3\x83\xbc\xe3\x83\xa0\xe3\x82\xb0\xe3\x83\xa9\xe3\x83\x95"
|
||||
"\xe3\x82\xa3\xe3\x83\x83\xe3\x82\xaf\xe3\x82\xb9"; // ゲームグラフィックス
|
||||
size_t th_at = heap_size_field(&b, heaps, "TextureHeap"), gg_at = heap_size_field(&b, heaps, parent);
|
||||
if (th_at && gg_at) {
|
||||
memcpy(&th, b.p + th_at, 4);
|
||||
memcpy(&gg, b.p + gg_at, 4);
|
||||
th2 = th + (int32_t)tex, gg2 = gg + (int32_t)tex;
|
||||
memcpy(b.p + th_at, &th2, 4);
|
||||
memcpy(b.p + gg_at, &gg2, 4);
|
||||
} else {
|
||||
rt_log("resolution: the HeapDef table has no TextureHeap sizes; TOTK_TEX_HEAP_EXTRA_MB left out");
|
||||
tex = 0;
|
||||
if (!extra) {
|
||||
free(b.p);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
size_t cap = ZSTD_compressBound(b.n);
|
||||
void *z = malloc(cap);
|
||||
size_t zn = z ? ZSTD_compress(z, cap, b.p, b.n, 9) : 0;
|
||||
@@ -273,7 +417,12 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
}
|
||||
*out = z;
|
||||
*out_n = zn;
|
||||
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx, gfx2);
|
||||
if (extra)
|
||||
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx,
|
||||
gfx2);
|
||||
if (tex)
|
||||
rt_log("resolution: HeapDef TextureHeap %d -> %d KiB and its parent %d -> %d KiB (TOTK_TEX_HEAP_EXTRA_MB or a "
|
||||
"mod's HeapDef)", th, th2, gg, gg2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+71
-4
@@ -533,6 +533,22 @@ static void added_init(void) {
|
||||
rt_log("RomFS overlay: %zu files added to the RomFS", nadded);
|
||||
}
|
||||
|
||||
// RomFS file `rel` as the image holds it, whatever overlays replace it. malloc'd; NULL when it is missing, unreadable
|
||||
// or 16 MiB or more (logged with `keeps`, what the game does without it).
|
||||
static void *read_original(const char *rel, size_t *n, const char *keeps) {
|
||||
uint64_t i = lower(rel);
|
||||
if (i >= file_count || strcmp(entry_name(entry(i)), rel)) return NULL;
|
||||
uint64_t size = entry(i).size;
|
||||
void *in = size && size < (16u << 20) ? malloc((size_t)size) : NULL;
|
||||
if (!in || read_at(image, in, (uint32_t)size, entry(i).offset) != (int64_t)size) {
|
||||
free(in);
|
||||
rt_log("RomFS: cannot read %s; %s", rel, keeps);
|
||||
return NULL;
|
||||
}
|
||||
*n = (size_t)size;
|
||||
return in;
|
||||
}
|
||||
|
||||
// RomFS file `rel` as the game would read it: from an overlay that replaces it (*base), else from the image. malloc'd;
|
||||
// NULL when it is missing, unreadable or 16 MiB or more (logged with `keeps`, what the game does without it).
|
||||
static void *read_served(const char *rel, Overlay **base, size_t *n, const char *keeps) {
|
||||
@@ -829,17 +845,55 @@ static void restbl_overlay(void) {
|
||||
members, packs, (now_s() - t0) * 1000);
|
||||
}
|
||||
|
||||
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
|
||||
// base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS original; the grown table is
|
||||
// written to <profile>/resolution/ and served in the base's place.
|
||||
// The HeapDef table a bigger game resolution or TOTK_TEX_HEAP_EXTRA_MB needs (resolution.c): its texture heaps and
|
||||
// their parents grow. The base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS
|
||||
// original; the grown table is written to <profile>/resolution/ and served in the base's place. A served mod table
|
||||
// whose own TextureHeap is grown (an HD texture pack's, resolution.c res_heapdef_tex_grown) is not the base: its
|
||||
// growth is already in the amount (resolution.c mod_tex_heap_kib). In its place the table of the highest switched-on
|
||||
// mod below it that changes other heaps only grows by the amount (plain_mod_heapdef: with the pack above The Second
|
||||
// Quest, the default order for HDPacks.com's "4kTextures", the game's own sizes for RSDB and UI were too small for
|
||||
// that mod's files and the game crashed loading a save), else the RomFS original; either is served in the pack's
|
||||
// place. A mod table that changes other heaps only and is served itself stays the base.
|
||||
#define HEAPDEF "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
||||
|
||||
// The HeapDef table of the highest switched-on mod that changes other heaps only (malloc'd, *n, *who its name); NULL
|
||||
// when no mod has one. Mods above the served table ship none, so the first found is the highest below it.
|
||||
static void *plain_mod_heapdef(size_t *n, const char **who) {
|
||||
for (int i = 0; i < mods_count(); i++) {
|
||||
const ModInfo *m = mods_at(i);
|
||||
char path[2300];
|
||||
if (!m->enabled || !m->romfs[0] ||
|
||||
snprintf(path, sizeof path, "%s/%s", m->romfs, HEAPDEF) >= (int)sizeof path)
|
||||
continue;
|
||||
uint64_t fn = 0;
|
||||
uint8_t *f = plat_file_read(path, &fn);
|
||||
if (!f) continue;
|
||||
if (fn < (16u << 20) && res_heapdef_plain(f, (size_t)fn)) {
|
||||
*n = (size_t)fn;
|
||||
*who = m->name;
|
||||
return f;
|
||||
}
|
||||
free(f);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
static void heapdef_overlay(void) {
|
||||
if (!res_heap_extra_kib()) return;
|
||||
if (!res_heap_extra_kib() && !res_tex_heap_extra_bytes()) return;
|
||||
static const char keeps[] = "the game keeps its own resolution";
|
||||
Overlay *base;
|
||||
size_t n;
|
||||
void *in = read_served(HEAPDEF, &base, &n, keeps), *out = NULL;
|
||||
size_t out_n = 0;
|
||||
if (in && base && base->mod && res_heapdef_tex_grown(in, n)) {
|
||||
free(in);
|
||||
const char *who = NULL;
|
||||
if ((in = plain_mod_heapdef(&n, &who)))
|
||||
rt_log("RomFS: %s: the HeapDef of %s with the texture heap growth in place of the mod's table",
|
||||
mods_at(base->mod - 1)->name, who);
|
||||
else if ((in = read_original(HEAPDEF, &n, keeps)))
|
||||
rt_log("RomFS: %s: the game's own HeapDef with the mod's texture heap growth in place of the mod's table",
|
||||
mods_at(base->mod - 1)->name);
|
||||
}
|
||||
int ok = in && res_heapdef_build(in, n, &out, &out_n);
|
||||
free(in);
|
||||
if (ok && serve_generated(HEAPDEF, base, out, out_n, "resolution", "grown heaps", keeps)) res_heapdef_served();
|
||||
@@ -1026,6 +1080,19 @@ static void index_path(char *out, size_t cap) {
|
||||
strcpy(out + k, "romfs-index.bin");
|
||||
}
|
||||
|
||||
// 1 when the game resolution w x h has the UI layout of its shape: romfs-patches-<w>x<h> next to romfs-index.bin with a
|
||||
// manifest, the folder overlay_init looks for. Works before fs_init: index_dir is not set that early (res_init and
|
||||
// resolution.c decide run first), so the index path is worked out again here.
|
||||
int romfs_shaped_ready(unsigned w, unsigned h) {
|
||||
char dir[2100], path[2200];
|
||||
index_path(dir, sizeof dir);
|
||||
char *sep = plat_last_sep(dir);
|
||||
if (sep) *sep = 0;
|
||||
else strcpy(dir, ".");
|
||||
return snprintf(path, sizeof path, "%s/romfs-patches-%ux%u/manifest.tsv", dir, w, h) < (int)sizeof path &&
|
||||
plat_file_exists(path);
|
||||
}
|
||||
|
||||
static void load_index(void) {
|
||||
char path[2100];
|
||||
index_path(path, sizeof path);
|
||||
|
||||
@@ -216,6 +216,7 @@ static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
|
||||
rt_log("guest ExitProcess at %s; exiting", ldr_describe(pc, at, sizeof at));
|
||||
fs_save_flush();
|
||||
vk_pipeline_save();
|
||||
playtime_save();
|
||||
prof_write();
|
||||
fflush(NULL);
|
||||
if (svc_test_exit_hook) return svc_test_exit_hook(0);
|
||||
|
||||
+17
-2
@@ -57,6 +57,7 @@ extern const int rt_game_version;
|
||||
extern const char rt_port_version[];
|
||||
void cheats_frame(uint64_t frame); // cheats.c
|
||||
bool tab_cheats(float scale); // ui/cheats_tab.cpp; true: restart into the rebuild
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c
|
||||
}
|
||||
|
||||
// Roboto Medium (Apache-2.0, ui/fonts/LICENSE-Roboto.txt) with Latin and Cyrillic glyphs.
|
||||
@@ -698,6 +699,19 @@ static void tab_screen(float scale) {
|
||||
opt_end();
|
||||
}
|
||||
|
||||
// Play time across runs (playtime.c), read only. Hidden while it is not counted (no profile directory).
|
||||
static void row_play_time(void) {
|
||||
uint64_t total, session;
|
||||
if (!playtime_get(&total, &session)) return;
|
||||
char value[64], run[64], note[96];
|
||||
snprintf(value, sizeof value, menu_text(UI_MENU_PLAY_TIME_HM), (unsigned long long)(total / 3600),
|
||||
(unsigned)(total / 60 % 60));
|
||||
snprintf(run, sizeof run, menu_text(UI_MENU_PLAY_TIME_HM), (unsigned long long)(session / 3600),
|
||||
(unsigned)(session / 60 % 60));
|
||||
snprintf(note, sizeof note, menu_text(UI_MENU_PLAY_TIME_SESSION), run);
|
||||
opt_text_row("##play_time", menu_text(UI_MENU_PLAY_TIME), value, note, menu_text(UI_MENU_PLAY_TIME_DESC), false);
|
||||
}
|
||||
|
||||
static void tab_game(float scale) {
|
||||
opt_begin("##game", scale);
|
||||
row_fps_limit();
|
||||
@@ -706,6 +720,7 @@ static void tab_game(float scale) {
|
||||
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
|
||||
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
|
||||
opt_toggle("TOTK_ASSUME_FOCUS", UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC);
|
||||
row_play_time();
|
||||
opt_end();
|
||||
}
|
||||
|
||||
@@ -1094,8 +1109,8 @@ static void tab_mods(float scale) {
|
||||
char code[512] = "", problem[512] = "";
|
||||
if (m->nsets && m->overrides[0])
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_REPLACES_BUILTIN), m->nsets, m->overrides);
|
||||
else if (m->nsets)
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets);
|
||||
else if (m->nsets || m->ndata)
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets + m->ndata);
|
||||
switch (m->problem) {
|
||||
case MOD_EMPTY: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EMPTY)); break;
|
||||
case MOD_PLUGIN: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_SUBSDK)); break;
|
||||
|
||||
@@ -192,6 +192,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
"window loses focus, as on the Switch when Home is pressed; a load interrupted that "
|
||||
"way can stop for good.",
|
||||
|
||||
[UI_MENU_PLAY_TIME] = "Play time",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "Time in the game over all runs, counted while the game window has the focus and the game "
|
||||
"draws frames. Kept in playtime.txt in the profile folder, next to the saves.",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu h %02u min", [UI_MENU_PLAY_TIME_SESSION] = "this run: %s",
|
||||
[UI_MENU_MODS_BUILTIN] = "Built into the port", [UI_MENU_MODS_INSTALLED] = "Installed mods",
|
||||
[UI_MENU_MODS_NONE] = "No mods yet. Unpack a mod into the mods folder, one folder per mod with its romfs folder "
|
||||
"inside, and restart the game. TKMM can export its merged mods into a folder there too.",
|
||||
@@ -662,6 +666,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
"когда окно теряет фокус, как на Switch при нажатии Home; прерванная так загрузка "
|
||||
"может остановиться насовсем.",
|
||||
|
||||
[UI_MENU_PLAY_TIME] = "Время в игре",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "Время в игре за все запуски. Считается, пока окно игры в фокусе и игра рисует кадры. "
|
||||
"Хранится в playtime.txt в папке профиля, рядом с сохранениями.",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu ч %02u мин", [UI_MENU_PLAY_TIME_SESSION] = "в этом запуске: %s",
|
||||
[UI_MENU_MODS_BUILTIN] = "Встроенные в порт", [UI_MENU_MODS_INSTALLED] = "Установленные моды",
|
||||
[UI_MENU_MODS_NONE] = "Модов пока нет. Распакуйте мод в папку mods, каждый мод в свою папку с папкой romfs внутри, и "
|
||||
"перезапустите игру. TKMM тоже может выгрузить туда объединённые моды отдельной папкой.",
|
||||
@@ -983,6 +991,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_TAB_SCREEN] = "Tela", [UI_MENU_TAB_GAME] = "Jogo", [UI_MENU_TAB_SOUND] = "Som",
|
||||
[UI_MENU_TAB_CONTROLS] = "Controles", [UI_MENU_TAB_AMIIBO] = "Amiibo", [UI_MENU_TAB_MENU] = "Menu", [UI_MENU_TAB_ADVANCED] = "Avançado",
|
||||
[UI_MENU_TAB_MODS] = "Mods",
|
||||
[UI_MENU_PLAY_TIME] = "Tempo de jogo",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "Tempo no jogo somando todas as sessões, contado enquanto a janela do jogo está em foco e o "
|
||||
"jogo desenha quadros. Fica em playtime.txt na pasta do perfil, junto dos saves.",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu h %02u min", [UI_MENU_PLAY_TIME_SESSION] = "nesta sessão: %s",
|
||||
[UI_MENU_MODS_BUILTIN] = "Embutidos no port", [UI_MENU_MODS_INSTALLED] = "Mods instalados",
|
||||
[UI_MENU_MODS_NONE] = "Nenhum mod ainda. Descompacte um mod na pasta mods, uma pasta por mod com a pasta romfs dentro, "
|
||||
"e reinicie o jogo. O TKMM também pode exportar os mods mesclados para uma pasta ali.",
|
||||
@@ -1492,6 +1504,10 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_TAB_MENU] = "菜单",
|
||||
[UI_MENU_TAB_ADVANCED] = "高级",
|
||||
[UI_MENU_TAB_MODS] = "模组",
|
||||
[UI_MENU_PLAY_TIME] = "游戏时间",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "所有运行累计的游戏时间,仅在游戏窗口处于焦点且游戏正在绘制画面时计时。"
|
||||
"保存在配置文件夹的 playtime.txt 中,与存档放在一起。",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu 小时 %02u 分钟", [UI_MENU_PLAY_TIME_SESSION] = "本次运行:%s",
|
||||
[UI_MENU_MODS_BUILTIN] = "移植版内置", [UI_MENU_MODS_INSTALLED] = "已安装的模组",
|
||||
[UI_MENU_MODS_NONE] = "还没有模组。把模组解压到 mods 文件夹,每个模组一个文件夹,里面放 romfs 文件夹,然后重启游戏。"
|
||||
"TKMM 也可以把合并后的模组导出到那里的一个文件夹。",
|
||||
|
||||
@@ -70,6 +70,9 @@ typedef enum {
|
||||
UI_MENU_GAME_LANG, UI_MENU_GAME_LANG_DESC, UI_MENU_GAME_LANG_AUTO,
|
||||
UI_MENU_OPERATION_MODE, UI_MENU_OPERATION_MODE_DESC, UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD,
|
||||
UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC,
|
||||
UI_MENU_PLAY_TIME, UI_MENU_PLAY_TIME_DESC,
|
||||
UI_MENU_PLAY_TIME_HM, // hours and minutes: "%llu h %02u min" (unsigned long long hours, unsigned minutes)
|
||||
UI_MENU_PLAY_TIME_SESSION, // note under the total: "this run: %s" (the hours and minutes above)
|
||||
|
||||
// Mods: the port's own patches and the player's mods folder (mods.c)
|
||||
UI_MENU_MODS_BUILTIN, UI_MENU_MODS_INSTALLED,
|
||||
|
||||
+5
-2
@@ -428,6 +428,7 @@ void vk_texture_stats(uint64_t *images, uint64_t *uploads, uint64_t *reuploads);
|
||||
// (images not bound into pool memory).
|
||||
typedef struct { uint64_t live, released, own_live, own_released, bytes_live, bytes_released; } VkTexCensus;
|
||||
void vk_texture_census(VkTexCensus *s);
|
||||
void vk_texture_released_log(uint64_t frame); // mem: released lines of vram_log
|
||||
void vk_texture_shutdown(void); // vk_mem_shutdown calls it
|
||||
void vk_texture_new_submit(void); // a queue submit starts: draw bindings check storage again
|
||||
uint32_t vk_texture_epoch(void); // the write-watch snapshot epoch vk_texture_new_submit started
|
||||
@@ -504,6 +505,7 @@ void vk_pipe_state_defaults(VkPipeState *s); // NVN defaults of every state ob
|
||||
enum { VK_PIPE_SUPPORT = GFX_RES_KINDS }; // VkPipeBinding.kind of the support buffer
|
||||
#define VK_PIPE_ZERO_BINDING 16 // vertex binding of shader inputs without an attribute: bind a zeroed
|
||||
// buffer of at least 16 bytes there (stride 0)
|
||||
#define VK_PIPE_ALL_BINDINGS 0x1ffffu // every vertex binding, streams 0-15 and the zero binding
|
||||
typedef struct {
|
||||
uint16_t binding; // in its set
|
||||
uint8_t stage; // GFX_STAGE_* that declared it (support buffer: the first stage)
|
||||
@@ -556,8 +558,9 @@ VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info);
|
||||
// has them dynamic, depth clamp, polygon mode, blend enables, equations, write masks and alpha to coverage. The replay
|
||||
// sets the rest: vkCmdSetViewportWithCount, vkCmdSetScissorWithCount, depth bias, blend constants, stencil compare and
|
||||
// write masks and references; vertex buffers with vkCmdBindVertexBuffers2 (strides NULL when
|
||||
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream).
|
||||
void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
|
||||
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream). Returns the vertex bindings (bit per binding) the
|
||||
// state's vertex input reads, whether set here or made into the pipeline.
|
||||
uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
|
||||
int vk_pipeline_vertex_input_dynamic(void); // 1: vertex input is VK_EXT_vertex_input_dynamic_state
|
||||
// Translator answers from the bound state: attr_type from the vertex attribute formats (vs), frag_out_type from the
|
||||
// colour target formats (fs). Other fields are left as they are; either pointer may be NULL.
|
||||
|
||||
@@ -86,6 +86,10 @@ void vk_exec_frame_log(const tex_t *p, uint64_t frame) {
|
||||
nfw = 0;
|
||||
}
|
||||
|
||||
WEAK void vk_exec_ub_flush(VkExec *ex) { (void)ex; }
|
||||
WEAK void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n) { (void)ex, (void)addr, (void)n; }
|
||||
WEAK void vk_exec_ub_perf(void) {}
|
||||
|
||||
WEAK void vk_exec_op_draw(VkExec *ex, uint32_t op, const void *payload) {
|
||||
(void)ex, (void)payload;
|
||||
rt_fatal("vk: op %s is not replayed yet", nvn_op_name(op));
|
||||
@@ -557,6 +561,7 @@ static VkImageMemoryBarrier2 layout_barrier(VkImage img, VkImageAspectFlags aspe
|
||||
// go back to GENERAL and the new ones leave it, in the same command.
|
||||
static void barrier_to(VkExec *ex, const VkImage *want, const VkImageAspectFlags *wasp, int nwant) {
|
||||
vk_exec_pass_end(ex);
|
||||
vk_exec_ub_flush(ex); // uniform updates overlaid in the pass reach their buffers before the barrier below
|
||||
VkImageMemoryBarrier2 ib[18];
|
||||
uint32_t n = 0;
|
||||
for (int i = 0; i < ex->nfbl; i++)
|
||||
@@ -967,6 +972,7 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
|
||||
}
|
||||
ex->cb = VK_NULL_HANDLE;
|
||||
if (n) n_subs++;
|
||||
if (n && (rt_trace & RT_TRACE_PERF)) vk_exec_ub_perf();
|
||||
if ((rt_trace & RT_TRACE_PERF) && n_subs >= 240) {
|
||||
rt_log("perf: replay command buffer parts per submission %.2f", (double)(n_subs + n_parts) / (double)n_subs);
|
||||
n_subs = n_parts = 0;
|
||||
|
||||
@@ -122,6 +122,12 @@ void vk_exec_pass_end(VkExec *ex);
|
||||
// Full memory barrier (all commands, all memory); ends the open pass first. Copies and dispatches order themselves with
|
||||
// it. Skipped when nothing was recorded since the last one.
|
||||
void vk_exec_barrier(VkExec *ex);
|
||||
// UpdateUniformBuffer overlays (vk_exec_draw.c): the copies into the real buffers that updates inside a pass left for
|
||||
// the next barrier (barrier_to records them first), and a GPU write of another kind into [addr, addr+n) of this
|
||||
// submission (a copy, a fill, a storage write), which retires the overlays it touches.
|
||||
void vk_exec_ub_flush(VkExec *ex);
|
||||
void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n);
|
||||
void vk_exec_ub_perf(void);
|
||||
// vk_texture.c (TOTK_TEX_ALIAS): commands that must run at the replay's current position. vk_exec_inline_begin returns
|
||||
// the command buffer being recorded after a full barrier (an open pass is ended; *reopen says so), or VK_NULL_HANDLE
|
||||
// when this thread is not recording a replay. vk_exec_inline_end begins the pass again when it was open.
|
||||
|
||||
+290
-6
@@ -99,6 +99,8 @@ static struct {
|
||||
VkPipeline pipe[2]; // bound per bind point: 0 graphics, 1 compute
|
||||
VkPipeState st; // the state vk_pipeline_set_dynamic last recorded
|
||||
int st_valid;
|
||||
uint32_t vmask; // the vertex bindings st's vertex input reads (valid with st_valid)
|
||||
uint32_t vres; // the streams whose vb entry was resolved from vin (others: the zero buffer)
|
||||
VkBuffer vb[17]; // vertex buffers last bound
|
||||
VkDeviceSize vo[17], vsz[17], vstr[17];
|
||||
int vcls[17]; // their pool class and pool record, for the census
|
||||
@@ -158,6 +160,35 @@ static struct {
|
||||
uint8_t ck[DS_SIG_BYTES];
|
||||
} RS = {-1};
|
||||
|
||||
// UpdateUniformBuffer overlays of the submission being replayed (op_update_ub, TOTK_UB_OVERLAY). Here because
|
||||
// bind_sig reads them (ov_at).
|
||||
#define OV_N 2048
|
||||
#define OV_FOREIGN 4096
|
||||
static struct {
|
||||
int mode; // rt_knob: 0 off, 1 on, 2 ab; -1 unread
|
||||
uint64_t fence; // the submission the lists belong to
|
||||
uint64_t serial; // numbers the overlays, so bind_sig tells one from the next at the same range
|
||||
uint64_t lo, hi; // the span of the overlays: most bindings lie outside it
|
||||
uint32_t n, np, nf;
|
||||
int f_over; // more foreign writes than f holds: no more overlays in this submission
|
||||
struct { uint64_t lo, hi, serial; VkBuffer buf; VkDeviceSize off; } o[OV_N]; // oldest first, no two equal
|
||||
struct { uint64_t dst; VkBuffer src; VkDeviceSize soff; VkMemSpan sp; uint64_t size; } p[OV_N]; // to copy
|
||||
struct { uint64_t lo, hi; } f[OV_FOREIGN]; // GPU writes of other kinds
|
||||
// since the last perf line (vk_exec_ub_perf)
|
||||
uint64_t c_update, c_overlaid, c_flush, c_bind, c_merged, c_mixed, c_inpass, c_outside, c_early;
|
||||
} OV = {.mode = -1};
|
||||
|
||||
// A binding without a size reaches to the end of its backing: as far as the overlays go, for ov_at.
|
||||
static uint64_t ov_rest(uint64_t addr) { return addr < OV.hi ? OV.hi - addr : 1; }
|
||||
|
||||
// The serial of the newest overlay a binding of [addr, addr+n) reads from, 0 for none (bind_sig).
|
||||
static uint64_t ov_at(const VkExec *ex, uint64_t addr, uint64_t n) {
|
||||
if (!OV.n || OV.fence != ex->fence || addr >= OV.hi || addr + n <= OV.lo) return 0;
|
||||
for (uint32_t i = OV.n; i-- > 0;)
|
||||
if (addr < OV.o[i].hi && addr + n > OV.o[i].lo) return OV.o[i].serial;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The bound-slot bytes of VkExec a pipeline's descriptors read, as ranges merged where they touch, built once per
|
||||
// pipeline info: c0's storage table, the uniform and storage ranges of the bound slots and the texture and image
|
||||
// handles. sup and c0: the support inputs and the base instance count too. p: storage buffers whose address the shader
|
||||
@@ -269,6 +300,9 @@ static uint8_t *scratch(VkExec *ex, uint64_t size, VkBuffer *buf, VkDeviceSize *
|
||||
return p;
|
||||
}
|
||||
|
||||
static int ub_bind(VkExec *ex, uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out);
|
||||
static void ub_written(VkExec *ex, uint64_t addr, uint64_t n);
|
||||
|
||||
static void note_written(VkExec *ex, uint64_t addr, uint64_t size) {
|
||||
CbRes *r = cb_res(ex);
|
||||
for (int i = 0; i < r->nwritten; i++)
|
||||
@@ -807,10 +841,15 @@ static int bind_range(VkExec *ex, uint64_t addr, uint64_t size, uint64_t align,
|
||||
uint64_t n = size && size < sp.avail ? size : sp.avail;
|
||||
if (n > max) n = max;
|
||||
if (!n) return 0;
|
||||
if (!written && (use == VK_MUSE_UBO || use == VK_MUSE_SSBO) && ub_bind(ex, addr, n, align, out)) {
|
||||
count_use(VK_MCLS_SCRATCH, use, n);
|
||||
return 1;
|
||||
}
|
||||
if (sp.offset % align == 0) {
|
||||
VK_MEM_USE(sp);
|
||||
if (written) ub_written(ex, addr, n);
|
||||
*out = (VkDescriptorBufferInfo){sp.buffer, sp.offset, n};
|
||||
if (written) vk_mem_gpu_wrote_shader(&sp, n), vk_usnap_drop(addr, n);
|
||||
if (written) vk_mem_gpu_wrote_shader(&sp, n), vk_usnap_drop(addr, n), vk_exec_ub_foreign(ex, addr, n);
|
||||
count_use(sp.cls, use, n);
|
||||
VK_MEM_BOUND(sp);
|
||||
return 1;
|
||||
@@ -920,6 +959,10 @@ static VkDescriptorBufferInfo ubo_info(VkExec *ex, int st, int slot) {
|
||||
}
|
||||
} else if (slot >= 3 && slot - 3 < 18) {
|
||||
const p_bind_buf *b = &ex->ubo[st][slot - 3];
|
||||
if (b->size && ub_bind(ex, b->addr, b->size < max ? b->size : max, align, &bi)) { // updated in this pass
|
||||
count_use(VK_MCLS_SCRATCH, VK_MUSE_UBO, bi.range);
|
||||
return bi;
|
||||
}
|
||||
if (b->size && vk_usnap_find(b->addr, b->size < max ? b->size : max, align, &bi)) { // as of the submit call
|
||||
count_use(VK_MCLS_SCRATCH, VK_MUSE_UBO, bi.range);
|
||||
return bi;
|
||||
@@ -983,6 +1026,16 @@ static int upd_read(const VkExec *ex, uint64_t a, void *out, size_t n) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Applies the updates of this submission that touch [a, a+n) onto out, oldest first (ub_bind's merged copies).
|
||||
static void upd_apply(const VkExec *ex, uint64_t a, uint8_t *out, size_t n) {
|
||||
if (UPD.fence != ex->fence) return;
|
||||
for (uint32_t i = UPD.n > UPD_N ? UPD.n - UPD_N : 0; i < UPD.n; i++) {
|
||||
const typeof(UPD.u[0]) *e = &UPD.u[i % UPD_N];
|
||||
uint64_t lo = e->lo > a ? e->lo : a, hi = e->hi < a + n ? e->hi : a + n;
|
||||
if (lo < hi) memcpy(out + (lo - a), e->data + (lo - e->lo), hi - lo);
|
||||
}
|
||||
}
|
||||
|
||||
// That address as the bound block holds it at this point of the replay; 0 when the block is unbound or too short. The
|
||||
// block has no size field.
|
||||
static uint64_t ubo_ptr(const VkExec *ex, int st, const GfxShaderRes *r) {
|
||||
@@ -1698,6 +1751,30 @@ static int bind_sig(const VkExec *ex, const VkPipeInfo *info) {
|
||||
}
|
||||
if (l->c0) PUT(c0_binst);
|
||||
for (int i = 0; i < l->np; i++) PUT(ubo_ptr(ex, l->p[i].st, l->p[i].res));
|
||||
// The uniform overlays the draw's uniform and storage bindings read (op_update_ub): a draw of an updated block binds
|
||||
// other bytes after each update, every other draw is left to skip
|
||||
if (OV.n) {
|
||||
uint64_t ov = 0;
|
||||
const size_t lo = offsetof(VkExec, ubo), hi = offsetof(VkExec, ssbo) + sizeof ex->ssbo;
|
||||
for (uint32_t i = 0; i < l->n; i++) {
|
||||
const RsRange *r = &l->r[i];
|
||||
if (r->off + r->len <= lo || r->off >= hi) continue;
|
||||
for (uint32_t o = 0; o + sizeof(p_bind_buf) <= r->len; o += sizeof(p_bind_buf)) {
|
||||
size_t at = r->off + o;
|
||||
if (at < lo || at + sizeof(p_bind_buf) > hi) continue;
|
||||
if ((at < offsetof(VkExec, ssbo) ? at - lo : at - offsetof(VkExec, ssbo)) % sizeof(p_bind_buf)) continue;
|
||||
const p_bind_buf *b = (const p_bind_buf *)((const uint8_t *)ex + at);
|
||||
if (b->addr) ov = ov * 1099511628211ull ^ ov_at(ex, b->addr, b->size ? b->size : ov_rest(b->addr));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < l->np; i++) { // storage read through an address in a uniform block: no size either
|
||||
uint64_t a = ubo_ptr(ex, l->p[i].st, l->p[i].res);
|
||||
if (a) ov = ov * 1099511628211ull ^ ov_at(ex, a, ov_rest(a));
|
||||
}
|
||||
PUT(ov);
|
||||
} else {
|
||||
PUT(0);
|
||||
}
|
||||
#undef PUT
|
||||
diff |= n != RS.hn;
|
||||
RS.hn = n;
|
||||
@@ -1971,21 +2048,34 @@ static void record_fixed(VkExec *ex) {
|
||||
ex->dirty = 0;
|
||||
}
|
||||
|
||||
// TOTK_VB_USED=0: every bound stream is resolved and bound, also those the draw's vertex input does not read.
|
||||
static int vb_used_on(void) {
|
||||
static int on = -1;
|
||||
if (on < 0) on = cfg_bool("TOTK_VB_USED");
|
||||
return on;
|
||||
}
|
||||
|
||||
static void bind_vertex_buffers(VkExec *ex) {
|
||||
VkBuffer vb[17];
|
||||
VkDeviceSize vo[17], vsz[17], vstr[17];
|
||||
int vcls[17] = {0};
|
||||
void *vpool[17] = {0};
|
||||
int dyn = vk_pipeline_vertex_input_dynamic();
|
||||
// Only the streams the draw's vertex input reads (B.vmask, from vk_pipeline_set_dynamic): the game keeps about ten
|
||||
// bound and a draw reads a few. The others get the zero buffer, never a stale one: a buffer bound but not read must
|
||||
// still be a live handle.
|
||||
uint32_t used = vb_used_on() && B.st_valid ? B.vmask : VK_PIPE_ALL_BINDINGS;
|
||||
// A stream whose range is the one its bound buffer was resolved from, with no pool or mirror change since
|
||||
// (vk_mem_gen, read before any resolve), resolves to that buffer again: most draws change one stream or none.
|
||||
uint64_t gen = vk_mem_gen();
|
||||
int same_gen = B.vb_valid && B.vgen == gen;
|
||||
uint32_t res = 0;
|
||||
for (int b = 0; b < 17; b++) {
|
||||
vb[b] = Z.zbuf, vo[b] = 0, vsz[b] = ZERO_BYTES, vstr[b] = b < 16 ? ex->pipe.vstream[b].stride : 0;
|
||||
if (b == VK_PIPE_ZERO_BINDING) continue;
|
||||
if (b == VK_PIPE_ZERO_BINDING || !(used >> b & 1)) continue;
|
||||
res |= 1u << b;
|
||||
const NVNbufferRange *r = &ex->vbuf[b];
|
||||
if (same_gen && B.vin[b].address == r->address && B.vin[b].size == r->size) {
|
||||
if (same_gen && (B.vres >> b & 1) && B.vin[b].address == r->address && B.vin[b].size == r->size) {
|
||||
vb[b] = B.vb[b], vo[b] = B.vo[b], vsz[b] = B.vsz[b], vcls[b] = B.vcls[b], vpool[b] = B.vpool[b];
|
||||
continue;
|
||||
}
|
||||
@@ -1997,7 +2087,7 @@ static void bind_vertex_buffers(VkExec *ex) {
|
||||
}
|
||||
memcpy(B.vcls, vcls, sizeof vcls), memcpy(B.vpool, vpool, sizeof vpool);
|
||||
memcpy(B.vin, ex->vbuf, sizeof B.vin);
|
||||
B.vgen = gen;
|
||||
B.vgen = gen, B.vres = res;
|
||||
if (B.vb_valid && !memcmp(B.vb, vb, sizeof vb) && !memcmp(B.vo, vo, sizeof vo) && !memcmp(B.vsz, vsz, sizeof vsz) &&
|
||||
(dyn || !memcmp(B.vstr, vstr, sizeof vstr)))
|
||||
return;
|
||||
@@ -2199,7 +2289,7 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
|
||||
if (!p || !info) return warn_once(W_PIPELINE, "a graphics pipeline could not be created: draws with it are skipped"), 0;
|
||||
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
|
||||
if (!B.st_valid || memcmp(&B.st, &s, sizeof s)) {
|
||||
vk_pipeline_set_dynamic(ex->cb, &s);
|
||||
B.vmask = 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;
|
||||
@@ -3159,14 +3249,208 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
|
||||
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, &(p_dispatch){0, 0, 0, d->caller});
|
||||
}
|
||||
|
||||
// ---- UpdateUniformBuffer inside a pass (TOTK_UB_OVERLAY)
|
||||
// nvnCommandBufferUpdateUniformBuffer writes a few hundred bytes of constants between draws: the draws recorded after
|
||||
// it read the new bytes, those before it the old ones. Replayed as a buffer copy at that point it needs a barrier
|
||||
// against the earlier draws, and a barrier ends the open pass; the terrain and the sky islands' meshes update before
|
||||
// most of their draws, about 200 updates inside passes in a field submission, each a pass end, a full barrier and a
|
||||
// pass begin. Here an update inside a pass goes into the scratch buffer as an overlay: the later bindings of a range it
|
||||
// covers bind the scratch bytes (ub_bind, from ubo_info and bind_range), and the copies into the real buffers wait for
|
||||
// the next full barrier, where the pass has ended anyway (vk_exec_ub_flush from vk_exec.c barrier_to: a pass end, a
|
||||
// dispatch, a transfer, the end of the submission), behind one barrier of their own. Each draw reads the bytes it would
|
||||
// have read with the copy in between.
|
||||
// - A GPU write of another kind (a copy, a fill, a shader's storage write) is ordered with the copies only by the
|
||||
// command buffer. vk_exec_ub_foreign notes it and retires the overlays it touches; an update over such bytes takes
|
||||
// the old path. A shader that writes a range with copies pending gets them first (ub_written: the pass restarts).
|
||||
// - An update that takes the old path (outside a pass, a full list) retires the overlays it covers (ov_retire).
|
||||
// - A binding an overlay covers only in part, or at an offset the binding's alignment refuses, gets a merged scratch
|
||||
// copy: the snapshot's bytes (vk_usnap.c) or guest memory, with every update of the submission so far (UPD) on top.
|
||||
// - The descriptor build skip (bind_sig) hashes the overlay each uniform and storage binding of the draw reads
|
||||
// (ov_at), so a draw of an updated block binds the newest bytes and the other draws still skip. The lists are per submission and keep one entry per range (ov_add), and a binding outside their span is
|
||||
// answered in one comparison; when one is full, updates take the old path.
|
||||
|
||||
static int ov_on(void) {
|
||||
if (OV.mode < 0) OV.mode = rt_knob("UB_OVERLAY", "1");
|
||||
return RT_KNOB_ON(OV.mode);
|
||||
}
|
||||
|
||||
static void ov_sub(const VkExec *ex) { // the submission being replayed
|
||||
if (OV.fence == ex->fence) return;
|
||||
if (OV.np) // barrier_to records them at the latest at the submission's closing barrier
|
||||
rt_log("vk: %u overlaid uniform updates of submission %llu were never copied into their buffers", OV.np,
|
||||
(unsigned long long)OV.fence);
|
||||
OV.fence = ex->fence, OV.n = OV.np = OV.nf = 0, OV.f_over = 0, OV.lo = UINT64_MAX, OV.hi = 0;
|
||||
}
|
||||
|
||||
static void ov_span(void) {
|
||||
OV.lo = UINT64_MAX, OV.hi = 0;
|
||||
for (uint32_t i = 0; i < OV.n; i++) {
|
||||
if (OV.o[i].lo < OV.lo) OV.lo = OV.o[i].lo;
|
||||
if (OV.o[i].hi > OV.hi) OV.hi = OV.o[i].hi;
|
||||
}
|
||||
}
|
||||
|
||||
// A new overlay of [lo, hi), and its copy. An older overlay or pending copy of exactly that range is dropped: the new
|
||||
// one covers it, and the terrain rewrites one block before each draw, so the lists stay as short as the blocks.
|
||||
static void ov_add(uint64_t lo, uint64_t hi, VkBuffer b, VkDeviceSize o, const VkMemSpan *sp) {
|
||||
for (uint32_t i = 0; i < OV.n; i++)
|
||||
if (OV.o[i].lo == lo && OV.o[i].hi == hi) {
|
||||
memmove(&OV.o[i], &OV.o[i + 1], sizeof OV.o[0] * (OV.n - i - 1));
|
||||
OV.n--;
|
||||
break;
|
||||
}
|
||||
for (uint32_t i = 0; i < OV.np; i++)
|
||||
if (OV.p[i].dst == lo && OV.p[i].size == hi - lo) {
|
||||
memmove(&OV.p[i], &OV.p[i + 1], sizeof OV.p[0] * (OV.np - i - 1));
|
||||
OV.np--;
|
||||
break;
|
||||
}
|
||||
OV.o[OV.n++] = (typeof(OV.o[0])){lo, hi, ++OV.serial, b, o};
|
||||
OV.p[OV.np++] = (typeof(OV.p[0])){lo, b, o, *sp, hi - lo};
|
||||
if (lo < OV.lo) OV.lo = lo;
|
||||
if (hi > OV.hi) OV.hi = hi;
|
||||
}
|
||||
|
||||
static int ov_foreign_hits(uint64_t lo, uint64_t hi) {
|
||||
if (OV.f_over) return 1;
|
||||
for (uint32_t i = 0; i < OV.nf; i++)
|
||||
if (lo < OV.f[i].hi && hi > OV.f[i].lo) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The overlays over [lo, hi) no longer hold the newest bytes: the real buffer does. Their copies were recorded already
|
||||
// (the write came after a barrier, or ub_written brought them forward), so they only stop serving bindings.
|
||||
static void ov_retire(uint64_t lo, uint64_t hi) {
|
||||
if (!OV.n || lo >= OV.hi || hi <= OV.lo) return;
|
||||
uint32_t k = 0;
|
||||
for (uint32_t i = 0; i < OV.n; i++)
|
||||
if (!(lo < OV.o[i].hi && hi > OV.o[i].lo)) OV.o[k++] = OV.o[i];
|
||||
if (k == OV.n) return;
|
||||
OV.n = k;
|
||||
ov_span();
|
||||
}
|
||||
|
||||
// A GPU write of another kind into [addr, addr+n) in this submission (vk_exec_xfer.c note_gpu_write, storage bindings).
|
||||
void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n) {
|
||||
if (OV.mode <= 0 || !n) return;
|
||||
ov_sub(ex);
|
||||
if (OV.nf == OV_FOREIGN) OV.f_over = 1;
|
||||
else OV.f[OV.nf].lo = addr, OV.f[OV.nf].hi = addr + n, OV.nf++;
|
||||
ov_retire(addr, addr + n);
|
||||
}
|
||||
|
||||
// Records the copies of the updates overlaid so far, behind a barrier against the draws that read the old bytes.
|
||||
void vk_exec_ub_flush(VkExec *ex) {
|
||||
if (!OV.np || OV.fence != ex->fence) return;
|
||||
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.srcAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT};
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
||||
vkCmdPipelineBarrier2(ex->cb, &di);
|
||||
ex->recorded++;
|
||||
for (uint32_t i = 0; i < OV.np; i++) {
|
||||
VkBufferCopy cp = {OV.p[i].soff, OV.p[i].sp.offset, OV.p[i].size};
|
||||
vkCmdCopyBuffer(ex->cb, OV.p[i].src, OV.p[i].sp.buffer, 1, &cp);
|
||||
ex->recorded++;
|
||||
}
|
||||
OV.np = 0;
|
||||
OV.c_flush++;
|
||||
}
|
||||
|
||||
// TOTK_TRACE=perf, every 600 submissions: per submission, the updates and how many stayed in their pass.
|
||||
void vk_exec_ub_perf(void) {
|
||||
static unsigned subs;
|
||||
if (OV.mode <= 0 || ++subs % 600) return;
|
||||
const double k = 1 / 600.0;
|
||||
rt_log("perf: uniform updates per submission %.1f: %.1f overlaid in a pass (copied in %.2f batches), %.1f copied "
|
||||
"in a pass, %.1f outside one; bindings from an overlay %.1f (%.1f merged, %.1f over other GPU writes), %.2f "
|
||||
"batches brought forward for a storage write",
|
||||
OV.c_update * k, OV.c_overlaid * k, OV.c_flush * k, OV.c_inpass * k, OV.c_outside * k, OV.c_bind * k,
|
||||
OV.c_merged * k, OV.c_mixed * k, OV.c_early * k);
|
||||
OV.c_update = OV.c_overlaid = OV.c_flush = OV.c_bind = OV.c_merged = OV.c_mixed = 0;
|
||||
OV.c_inpass = OV.c_outside = OV.c_early = 0;
|
||||
}
|
||||
|
||||
// The scratch bytes of a uniform or storage binding [addr, addr+n) an overlay of this submission touches: 1 with *out
|
||||
// set, 0 when none does.
|
||||
static int ub_bind(VkExec *ex, uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out) {
|
||||
if (OV.mode <= 0 || OV.fence != ex->fence || !OV.n || addr >= OV.hi || addr + n <= OV.lo) return 0;
|
||||
uint64_t hi = addr + n;
|
||||
int newest = -1;
|
||||
for (int i = (int)OV.n; i-- > 0;)
|
||||
if (addr < OV.o[i].hi && hi > OV.o[i].lo) { newest = i; break; }
|
||||
if (newest < 0) return 0;
|
||||
OV.c_bind++;
|
||||
if (OV.o[newest].lo <= addr && OV.o[newest].hi >= hi) {
|
||||
VkDeviceSize off = OV.o[newest].off + (addr - OV.o[newest].lo);
|
||||
if (off % align == 0) {
|
||||
*out = (VkDescriptorBufferInfo){OV.o[newest].buf, off, n};
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// Merged: the bytes as of the submit call (the snapshot, else guest memory), then this submission's updates in
|
||||
// order. A transient copy, so the descriptor build skip leaves the draw after it alone (RS.volatile_). Bytes a copy
|
||||
// or a storage write of this submission produced are not in either source: counted.
|
||||
VkBuffer b;
|
||||
VkDeviceSize o;
|
||||
uint8_t *dst = scratch(ex, n, &b, &o);
|
||||
RS.volatile_ = 1;
|
||||
const uint8_t *base = vk_usnap_bytes(addr, n);
|
||||
VkMemSpan sp;
|
||||
if (base) memcpy(dst, base, n);
|
||||
else if (vk_mem_resolve(addr, n, &sp)) memcpy(dst, (const void *)(uintptr_t)addr, n);
|
||||
else memset(dst, 0, n);
|
||||
if (ov_foreign_hits(addr, hi)) OV.c_mixed++;
|
||||
upd_apply(ex, addr, dst, n);
|
||||
*out = (VkDescriptorBufferInfo){b, o, n};
|
||||
OV.c_merged++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Before a binding the shader writes: overlaid updates whose copies are still pending go to the buffer first, behind a
|
||||
// barrier, and the pass restarts, so the shader's writes land after them as they did when every update was copied at
|
||||
// once.
|
||||
static void ub_written(VkExec *ex, uint64_t addr, uint64_t n) {
|
||||
if (OV.mode <= 0 || OV.fence != ex->fence || !OV.np) return;
|
||||
for (uint32_t i = 0; i < OV.np; i++)
|
||||
if (addr < OV.p[i].dst + OV.p[i].size && OV.p[i].dst < addr + n) {
|
||||
int reopen = ex->in_pass;
|
||||
vk_exec_barrier(ex); // records the pending copies (vk_exec_ub_flush) before its barrier
|
||||
if (reopen) vk_exec_pass_begin(ex);
|
||||
OV.c_early++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void op_update_ub(VkExec *ex, const p_update_ub *u) {
|
||||
if (!u->size) return;
|
||||
VkMemSpan sp;
|
||||
if (!vk_mem_resolve(u->dst, u->size, &sp)) return warn_once(W_UB, "UpdateUniformBuffer to %#llx is not backed: dropped", (unsigned long long)u->dst);
|
||||
cb_res(ex);
|
||||
vk_exec_barrier(ex); // earlier draws finished with the old contents
|
||||
VkBuffer b;
|
||||
VkDeviceSize o;
|
||||
if (ov_on()) {
|
||||
ov_sub(ex);
|
||||
OV.c_update++;
|
||||
if (ex->in_pass && OV.n < OV_N && OV.np < OV_N && !ov_foreign_hits(u->dst, u->dst + u->size)) {
|
||||
uint8_t *d = scratch(ex, u->size, &b, &o);
|
||||
memcpy(d, u->data, u->size);
|
||||
VK_MEM_PICK(sp);
|
||||
ov_add(u->dst, u->dst + u->size, b, o, &sp);
|
||||
vk_mem_gpu_wrote(&sp, u->size);
|
||||
vk_usnap_drop(u->dst, u->size);
|
||||
vk_mem_count(sp.cls, VK_MUSE_GPU_WRITE, u->size);
|
||||
note_written(ex, u->dst, u->size);
|
||||
upd_note(ex, u);
|
||||
OV.c_overlaid++;
|
||||
return;
|
||||
}
|
||||
if (ex->in_pass) OV.c_inpass++;
|
||||
else OV.c_outside++;
|
||||
ov_retire(u->dst, u->dst + u->size);
|
||||
}
|
||||
vk_exec_barrier(ex); // earlier draws finished with the old contents; overlaid updates' copies go first
|
||||
memcpy(scratch(ex, u->size, &b, &o), u->data, u->size);
|
||||
VK_MEM_PICK(sp);
|
||||
VkBufferCopy cp = {o, sp.offset, u->size};
|
||||
|
||||
@@ -73,6 +73,7 @@ static void guest_written_cb(void *arg) {
|
||||
// The GPU wrote [addr, addr+size) of CPU-visible memory: tell the texture cache once the fence is reached.
|
||||
static void note_gpu_write(VkExec *ex, uint64_t addr, uint64_t size) {
|
||||
vk_usnap_drop(addr, size);
|
||||
vk_exec_ub_foreign(ex, addr, size);
|
||||
if (nvn_gpu_only(addr)) return;
|
||||
Range *r = malloc(sizeof *r);
|
||||
if (!r) rt_fatal("vk: out of memory");
|
||||
|
||||
@@ -565,10 +565,23 @@ void vk_mem_census(uint64_t frame) {
|
||||
window_start = frame;
|
||||
}
|
||||
|
||||
// TOTK_VRAM_LIMIT_MIB: pretend the device-local budget is at most this many MiB, so the paths that react to the budget
|
||||
// can be driven on a card larger than the one a report came from. The driver knows nothing of it.
|
||||
static uint64_t vram_limit(void) {
|
||||
static int64_t limit = -1;
|
||||
if (limit < 0) {
|
||||
const char *s = cfg_get("TOTK_VRAM_LIMIT_MIB");
|
||||
limit = s && *s ? (int64_t)strtoull(s, NULL, 10) << 20 : 0;
|
||||
if (limit) rt_log("mem: device-local budget limited to %lld MiB (TOTK_VRAM_LIMIT_MIB)", (long long)(limit >> 20));
|
||||
}
|
||||
return (uint64_t)limit;
|
||||
}
|
||||
|
||||
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
|
||||
VkCtx *c = vk_ctx();
|
||||
*used = *budget = 0, *exact = 0;
|
||||
if (!c) return 0;
|
||||
uint64_t limit = vram_limit();
|
||||
const VkPhysicalDeviceMemoryProperties *mp = &c->mem_props;
|
||||
if (c->has_memory_budget) {
|
||||
VkPhysicalDeviceMemoryBudgetPropertiesEXT b = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
@@ -578,6 +591,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
|
||||
if (p2.memoryProperties.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
|
||||
*used += b.heapUsage[h], *budget += b.heapBudget[h];
|
||||
*exact = 1;
|
||||
if (limit && *budget > limit) *budget = limit;
|
||||
return *budget != 0;
|
||||
}
|
||||
VmaBudget vb[VK_MAX_MEMORY_HEAPS];
|
||||
@@ -585,6 +599,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
|
||||
for (uint32_t h = 0; h < mp->memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
|
||||
if (mp->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
|
||||
*used += vb[h].statistics.blockBytes, *budget += mp->memoryHeaps[h].size;
|
||||
if (limit && *budget > limit) *budget = limit;
|
||||
return *budget != 0;
|
||||
}
|
||||
|
||||
@@ -633,6 +648,17 @@ static uint32_t reg_tried[RT_ADDRESS_LIMIT >> REG_SHIFT]; // gen_lock: ww_ep o
|
||||
static uint32_t *fcand; // gen_lock: regions to try at the next freeze pass
|
||||
static int nfcand, cfcand;
|
||||
static uint64_t n_frz_refused; // gen_lock: candidates mem_freeze refused
|
||||
// Regions the GPU writes. Storage-buffer stores, buffer copies and fills land in imported guest memory, directly (import
|
||||
// mode) or through a pool's mirror that vk_mem_mirror_end copies or merges into the imported buffer. They are not CPU
|
||||
// writes: no fault thaws the region and no write-watch bit is set, so a region only the GPU writes looked idle, was
|
||||
// frozen, and the GPU went on writing pages the CPU side had made read-only. LAS Recomp never changes the protection of
|
||||
// imported memory: on NVIDIA, VirtualProtect on pages imported with VK_EXT_external_memory_host left the GPU reading
|
||||
// wrong data (runtime/nvn/vulkan/exec.c, the comment on its write watch); a GPU write there is the likely cause of the
|
||||
// device faults "write invalid" at a page of guest memory on NVIDIA (#788, #802). reg_gpu holds the timeline value of
|
||||
// the newest submission that writes into the region (gpu_writes): freeze_pass leaves the region alone until the GPU is
|
||||
// past that value, and a frozen region is thawed when a write into it is recorded.
|
||||
static uint64_t reg_gpu[RT_ADDRESS_LIMIT >> REG_SHIFT]; // replay thread
|
||||
static volatile int64_t n_frz_gpu, n_thaw_gpu; // freeze candidates kept for a GPU write; thaws for one (mirror_log)
|
||||
|
||||
static uint32_t *gen_slot(uint64_t page_addr, int create) {
|
||||
uint64_t ci = page_addr >> GEN_CHUNK_SHIFT;
|
||||
@@ -1693,8 +1719,26 @@ static void snap_free(void *arg) { // completion thread: the submission is don
|
||||
free(f);
|
||||
}
|
||||
|
||||
// Record time: the submission being recorded writes [off, off+size) of r's guest range, directly into the imported
|
||||
// buffer or into the mirror whose bytes go back at its end. Keeps the regions unfrozen until the GPU is past it
|
||||
// (reg_gpu). Shadow storage needs nothing: the GPU writes the shadow and the CPU copies it back, thawing as it goes.
|
||||
static void gpu_writes(const PoolRec *r, uint64_t off, uint64_t size) {
|
||||
if (!freeze_idle || r->smap) return;
|
||||
uint64_t a = r->import_lo + off, b = a + size;
|
||||
if (b <= a || b > RT_ADDRESS_LIMIT) return;
|
||||
uint64_t f = vk_exec_recording_fence();
|
||||
if (!f) f = vk_sync_issued() + 1;
|
||||
int frozen = 0;
|
||||
for (uint64_t j = a >> REG_SHIFT; j <= (b - 1) >> REG_SHIFT; j++) {
|
||||
if (__atomic_load_n(®_gpu[j], __ATOMIC_RELAXED) < f) __atomic_store_n(®_gpu[j], f, __ATOMIC_RELAXED);
|
||||
frozen |= mem_frozen(j << REG_SHIFT);
|
||||
}
|
||||
if (frozen) mem_thaw(a, b - a), plat_inc64(&n_thaw_gpu);
|
||||
}
|
||||
|
||||
static void gpu_wrote(const VkMemSpan *sp, uint64_t size, int shader) {
|
||||
PoolRec *r = sp->pool;
|
||||
if (r && size) gpu_writes(r, sp->offset, size);
|
||||
if (!r || r->mstate != MS_ON || sp->buffer != r->mbuf || !size) return;
|
||||
// A binding to the end of its pool (no size) can span hundreds of MiB: those stay copies.
|
||||
if (shader && !(sp->offset & 3) && !(size & 3) && size <= MERGE_MAX && merging() && snap_add(r, sp->offset, size))
|
||||
@@ -1778,9 +1822,11 @@ void nvn_pool_flushed(const pool_t *p, uint64_t off, uint64_t size) {
|
||||
// gen_lock exclusive: freezes up to FREEZE_PER_SUB candidate regions still idle. The query after the
|
||||
// protection collects the writes made before it took effect.
|
||||
static void freeze_pass(void) {
|
||||
uint64_t gpu_done = vk_sync_fence_done();
|
||||
for (int done = 0; nfcand && done < FREEZE_PER_SUB;) {
|
||||
uint64_t j = fcand[--nfcand], a = j << REG_SHIFT;
|
||||
if (ww_ep - reg_last[j] < (uint32_t)freeze_idle || mem_frozen(a)) continue;
|
||||
if (__atomic_load_n(®_gpu[j], __ATOMIC_RELAXED) > gpu_done) { plat_inc64(&n_frz_gpu); continue; } // gpu_writes
|
||||
done++;
|
||||
if (!mem_freeze(a)) { n_frz_refused++; continue; }
|
||||
query_stamp(a, a + (1ull << REG_SHIFT), ++gen_now);
|
||||
@@ -1998,6 +2044,10 @@ static void mirror_log(uint64_t frame) {
|
||||
"or mapping change %llu, refused %llu",
|
||||
(unsigned long long)frame, wc / wf, wb / wf / 1048576.0, qpc_ms(wt) / wf, dc / wf, wp / wf, qpc_ms(dt) / wf,
|
||||
fz * 2.0, (unsigned long long)ff, (unsigned long long)ft, (unsigned long long)fo, (unsigned long long)nr);
|
||||
int64_t kg = plat_xchg64(&n_frz_gpu, 0), tg = plat_xchg64(&n_thaw_gpu, 0);
|
||||
if (kg || tg) // gpu_writes: before this, every count here was a region frozen under a GPU write
|
||||
rt_log("mem: frame %llu: regions the GPU writes, since the last line: %lld kept from freezing, %lld frozen ones "
|
||||
"thawed", (unsigned long long)frame, (long long)kg, (long long)tg);
|
||||
if (!vk_mirror_on) return;
|
||||
plat_mutex_lock(&mirror_stat_lock);
|
||||
MirrorAcc a = macc;
|
||||
@@ -2050,6 +2100,7 @@ static void vram_log(uint64_t frame) {
|
||||
alloc_bytes / M, (unsigned long long)t.live, (unsigned long long)t.own_live, t.bytes_live / M,
|
||||
(unsigned long long)t.released, (unsigned long long)t.own_released, t.bytes_released / M, (long long)m_live_n,
|
||||
m_live_bytes / M, dead_pending());
|
||||
vk_texture_released_log(frame);
|
||||
}
|
||||
|
||||
// Device class: INTEGRATED -> uma; a discrete GPU with a DEVICE_LOCAL heap of 1 GiB or more beside a
|
||||
|
||||
+11
-5
@@ -3113,8 +3113,8 @@ int vk_pipeline_warm_shaders(void) {
|
||||
|
||||
// ---- dynamic state
|
||||
|
||||
void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
|
||||
if (!ensure_init()) return;
|
||||
uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
|
||||
if (!ensure_init()) return VK_PIPE_ALL_BINDINGS;
|
||||
const poly_st *pl = &s->poly;
|
||||
const ds_st *d = &s->ds;
|
||||
// The front face maps unchanged: TOTK sets the upper-left window origin, and the y-flipped viewport (y_flip) puts
|
||||
@@ -3137,16 +3137,21 @@ void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
|
||||
vkCmdSetDepthBiasEnable(cb, (pl->offset_en >> mode & 1) != 0);
|
||||
vkCmdSetRasterizerDiscardEnable(cb, s->raster_discard != 0);
|
||||
vkCmdSetPrimitiveRestartEnable(cb, s->prim_restart != 0);
|
||||
if (P.dyn_vi && s->vs) {
|
||||
// The vertex bindings the draw reads: those of its vertex input, set here or made into the pipeline from the same
|
||||
// state by the same vertex_input.
|
||||
uint32_t streams = VK_PIPE_ALL_BINDINGS;
|
||||
if (s->vs) {
|
||||
VtxInput v;
|
||||
vertex_input(s, &v);
|
||||
rec_vertex_input(P.set_vi, cb, v.n_bind, v.b, v.n_attr, v.a);
|
||||
if (P.dyn_vi) rec_vertex_input(P.set_vi, cb, v.n_bind, v.b, v.n_attr, v.a);
|
||||
streams = 0;
|
||||
for (uint32_t i = 0; i < v.n_bind; i++) streams |= 1u << v.b[i].binding;
|
||||
}
|
||||
if (P.dyn_clamp) rec_bool(P.set_clamp, cb, s->depth_clamp != 0);
|
||||
if (P.dyn_poly) rec_polygon_mode(P.set_poly, cb, polygon_mode(pl->mode));
|
||||
if (P.dyn_a2c) rec_bool((PFN_vkCmdSetDepthClampEnableEXT)P.set_a2c, cb, s->ms.a2c != 0); // the same signature
|
||||
int n = target_count(s);
|
||||
if (!n) return;
|
||||
if (!n) return streams;
|
||||
VkBool32 en[8];
|
||||
VkColorBlendEquationEXT eq[8];
|
||||
VkColorComponentFlags wm[8];
|
||||
@@ -3160,6 +3165,7 @@ void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
|
||||
if (P.dyn_blend_en) rec_blend_enable(P.set_blend_en, cb, 0, (uint32_t)n, en);
|
||||
if (P.dyn_blend_eq) rec_blend_equation(P.set_blend_eq, cb, 0, (uint32_t)n, eq);
|
||||
if (P.dyn_wmask) rec_write_mask(P.set_wmask, cb, 0, (uint32_t)n, wm);
|
||||
return streams;
|
||||
}
|
||||
|
||||
// ---- statistics, persistence, teardown
|
||||
|
||||
+156
-48
@@ -3,7 +3,8 @@
|
||||
// call's arguments; its pointers point into the packet itself, so the recorder passes the payload to the driver as
|
||||
// it is. A packet never wraps: one that does not fit before the end is preceded by a pad. The producer publishes head
|
||||
// after each packet; the recorder makes the calls and advances tail, which the producer waits on when the ring is
|
||||
// full and in rec_drain. While a part is recorded the recorder spins on head; idle, it sleeps on an event.
|
||||
// full and in rec_drain. While a part is recorded the recorder spins on head; idle, it sleeps on an event. Packets also
|
||||
// begin, end and submit the parts (rec_begin, rec_end_cb, rec_submit), so each part stays on the recorder throughout.
|
||||
// Design: Savage Wind Recomp #189 and LAS Recomp runtime/nvn/vulkan/record.c (https://github.com/peiikai/LASRecomp
|
||||
// @ a4b53a3, MIT, Copyright (c) 2026 Peiikai); the code is ours.
|
||||
#define VK_REC_IMPL
|
||||
@@ -18,21 +19,35 @@
|
||||
|
||||
#define RING_WORDS (1u << 21) // 16 MiB: a part of a submission is ~1-2 MiB of packets in the town
|
||||
static uint64_t ring[RING_WORDS];
|
||||
static volatile uint64_t head, tail; // words published by the producer / consumed by the recorder
|
||||
static volatile int32_t sleeping; // the recorder waits for wake_ev
|
||||
// The queue state, a cache line per writer. The recorder spins on head while it waits for packets, so a line the
|
||||
// producer writes on every packet and the recorder reads or writes goes back and forth between their cores each time:
|
||||
// with tail, Q.put_h, head and the producer's statistics in one line (the linker's order of these statics), handing
|
||||
// packets over took 6% of the replay thread in the fort, more than building the descriptors.
|
||||
static struct {
|
||||
_Alignas(64) volatile uint64_t head; // words published by the producer; the recorder reads it
|
||||
_Alignas(64) volatile uint64_t tail; // words consumed by the recorder; the producer reads it when it seems full
|
||||
_Alignas(64) volatile int32_t sleeping; // the recorder waits for wake_ev; written only around a sleep
|
||||
_Alignas(64) uint64_t put_h; // producer only: words written
|
||||
uint64_t pub_h; // the last head published
|
||||
uint64_t tail_seen; // tail as last read, at most the real one: the room it leaves is there
|
||||
uint64_t pub_words; // TOTK_REC_PUB: publish head once this many words wait (pub)
|
||||
} Q;
|
||||
static PlatEvent wake_ev; // auto-reset
|
||||
static uint64_t put_h; // producer: words written (published up to head)
|
||||
static VkCommandBuffer rec_cb; // the part being recorded (set while the recorder is idle)
|
||||
static VkCommandBuffer rec_cb; // producer: the part being recorded, whose vkCmd* become packets
|
||||
static VkCommandBuffer r_cb; // recorder: the command buffer its packets go into (set by a BeginCb packet)
|
||||
static int rec_live; // producer: rec_begin started the recorder for rec_cb
|
||||
static int end_mode = -1; // TOTK_REC_END (rec_own)
|
||||
static int sub_pending; // producer: the recorder ended a part that rec_submit has not handed over yet
|
||||
static volatile VkResult r_sub_res; // recorder: the result of its last vkQueueSubmit2, read after the drain
|
||||
static PlatThread *rec_thread;
|
||||
static int rec_prio; // the submitting thread's priority, which the recorder takes
|
||||
static int rec_chk; // TOTK_REC_CHECK (rec_check_handle)
|
||||
static uint64_t rec_delay_ns; // TOTK_REC_DELAY_US (rec_main)
|
||||
static volatile uint64_t part_h0; // put_h at rec_begin: where the packets of the part being recorded start
|
||||
static volatile uint64_t part_h0; // Q.put_h at rec_begin: where the packets of the part being recorded start
|
||||
static volatile uint64_t part_fence; // the value of the part being recorded (rec_begin)
|
||||
|
||||
// Producer statistics, logged by rec_stats_log.
|
||||
static struct { uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on; } RS_;
|
||||
static struct { _Alignas(64) uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on, submits; } RS_;
|
||||
|
||||
enum {
|
||||
R_PAD, R_BeginQuery, R_BeginRendering, R_BindDescriptorSets, R_BindIndexBuffer, R_BindPipeline, R_BindVertexBuffers2,
|
||||
@@ -44,7 +59,7 @@ enum {
|
||||
R_SetRasterizerDiscardEnable, R_SetScissorWithCount, R_SetStencilCompareMask, R_SetStencilOp, R_SetStencilReference,
|
||||
R_SetStencilTestEnable, R_SetStencilWriteMask, R_SetViewportWithCount, R_WriteTimestamp, R_WriteTimestamp2,
|
||||
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask, R_EndCb,
|
||||
R_PassMark,
|
||||
R_PassMark, R_BeginCb, R_Submit,
|
||||
};
|
||||
|
||||
static uint64_t now_ns(void) { return plat_now_ns(); }
|
||||
@@ -58,14 +73,14 @@ static void rec_main(void *arg) {
|
||||
plat_thread_priority(rec_prio);
|
||||
uint64_t t = 0;
|
||||
for (;;) {
|
||||
uint64_t h = __atomic_load_n(&head, __ATOMIC_ACQUIRE);
|
||||
uint64_t h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
|
||||
if (t == h) {
|
||||
for (int i = 0; i < 4096 && t == h; i++) _mm_pause(), h = __atomic_load_n(&head, __ATOMIC_ACQUIRE);
|
||||
for (int i = 0; i < 4096 && t == h; i++) _mm_pause(), h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
|
||||
if (t == h) {
|
||||
__atomic_store_n(&sleeping, 1, __ATOMIC_SEQ_CST);
|
||||
h = __atomic_load_n(&head, __ATOMIC_SEQ_CST);
|
||||
__atomic_store_n(&Q.sleeping, 1, __ATOMIC_SEQ_CST);
|
||||
h = __atomic_load_n(&Q.head, __ATOMIC_SEQ_CST);
|
||||
if (t == h) plat_event_wait_ns(&wake_ev, 2000000); // a missed wake costs at most 2 ms
|
||||
__atomic_store_n(&sleeping, 0, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&Q.sleeping, 0, __ATOMIC_RELAXED);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -77,9 +92,9 @@ static void rec_main(void *arg) {
|
||||
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
|
||||
if (op != R_PAD) exec_packet(op, &ring[t % RING_WORDS + 1]);
|
||||
t += n;
|
||||
if ((k & 63) == 63) __atomic_store_n(&tail, t, __ATOMIC_RELEASE);
|
||||
if ((k & 63) == 63) __atomic_store_n(&Q.tail, t, __ATOMIC_RELEASE);
|
||||
}
|
||||
__atomic_store_n(&tail, t, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&Q.tail, t, __ATOMIC_RELEASE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -88,54 +103,85 @@ static void rec_main(void *arg) {
|
||||
|
||||
static void wake(void) {
|
||||
__atomic_thread_fence(__ATOMIC_SEQ_CST);
|
||||
if (__atomic_load_n(&sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
|
||||
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
|
||||
}
|
||||
|
||||
// Room for need words: the tail read last leaves it in all but about one packet in 16 MiB of them.
|
||||
static void wait_space(uint64_t need) {
|
||||
if (put_h + need - __atomic_load_n(&tail, __ATOMIC_ACQUIRE) <= RING_WORDS) return;
|
||||
if (Q.put_h + need - Q.tail_seen <= RING_WORDS) return;
|
||||
Q.tail_seen = __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE);
|
||||
if (Q.put_h + need - Q.tail_seen <= RING_WORDS) return;
|
||||
RS_.full_waits++;
|
||||
__atomic_store_n(&head, put_h, __ATOMIC_RELEASE);
|
||||
Q.pub_h = Q.put_h;
|
||||
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
|
||||
wake();
|
||||
while (put_h + need - __atomic_load_n(&tail, __ATOMIC_ACQUIRE) > RING_WORDS) _mm_pause();
|
||||
while (Q.put_h + need - (Q.tail_seen = __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE)) > RING_WORDS) _mm_pause();
|
||||
}
|
||||
|
||||
// Room for a packet of bytes of arguments; the caller fills it and calls pub().
|
||||
static void *put(uint32_t op, size_t bytes) {
|
||||
uint64_t n = 1 + (bytes + 7) / 8, pos = put_h % RING_WORDS;
|
||||
uint64_t n = 1 + (bytes + 7) / 8, pos = Q.put_h % RING_WORDS;
|
||||
if (n > RING_WORDS / 4) rt_fatal("vk_rec: a packet of %llu bytes", (unsigned long long)bytes);
|
||||
if (pos + n > RING_WORDS) {
|
||||
uint64_t pad = RING_WORDS - pos;
|
||||
wait_space(pad);
|
||||
ring[pos] = (uint64_t)R_PAD | pad << 32;
|
||||
put_h += pad, pos = 0;
|
||||
Q.put_h += pad, pos = 0;
|
||||
}
|
||||
wait_space(n);
|
||||
ring[pos] = (uint64_t)op | n << 32;
|
||||
// Packers reserve room for optional arrays they may leave unwritten (BeginRendering's depth and stencil); with
|
||||
// TOTK_REC_CHECK those words would still hold handles from the ring's previous lap.
|
||||
if (rec_chk) memset(&ring[pos + 1], 0, (n - 1) * 8);
|
||||
put_h += n;
|
||||
Q.put_h += n;
|
||||
RS_.packets++, RS_.words += n;
|
||||
return &ring[pos + 1];
|
||||
}
|
||||
|
||||
// After a packet: head moves once Q.pub_words words wait, so the recorder reads a line the producer is still filling
|
||||
// once per batch instead of once per packet. Whatever the producer waits for publishes everything first (wait_space,
|
||||
// rec_drain), and the packets that begin and end a part go out at once (pub_now), so the recorder begins and ends
|
||||
// command buffers as early as before; TOTK_REC_CHECK publishes every packet, since rec_check_handle searches the ring
|
||||
// up to head.
|
||||
static void pub_now(void) {
|
||||
Q.pub_h = Q.put_h;
|
||||
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
|
||||
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
|
||||
}
|
||||
|
||||
static void pub(void) {
|
||||
__atomic_store_n(&head, put_h, __ATOMIC_RELEASE);
|
||||
if (__atomic_load_n(&sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
|
||||
if (Q.put_h - Q.pub_h >= Q.pub_words) pub_now();
|
||||
}
|
||||
|
||||
void rec_drain(void) {
|
||||
if (!rec_thread || __atomic_load_n(&tail, __ATOMIC_ACQUIRE) == put_h) return;
|
||||
if (!rec_thread || __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE) == Q.put_h) return;
|
||||
uint64_t t0 = now_ns();
|
||||
__atomic_store_n(&head, put_h, __ATOMIC_RELEASE);
|
||||
Q.pub_h = Q.put_h;
|
||||
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
|
||||
wake();
|
||||
for (int i = 0; __atomic_load_n(&tail, __ATOMIC_ACQUIRE) != put_h; i++)
|
||||
for (int i = 0; __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE) != Q.put_h; i++)
|
||||
if (i < 20000) _mm_pause();
|
||||
else plat_yield();
|
||||
RS_.drains++, RS_.drain_ns += now_ns() - t0;
|
||||
}
|
||||
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence) {
|
||||
static int rec_own(void) {
|
||||
if (end_mode < 0) end_mode = rt_knob("REC_END", "1");
|
||||
return end_mode;
|
||||
}
|
||||
|
||||
// vkBeginCommandBuffer (one submission) and the breadcrumb, on the thread that records cb.
|
||||
static void begin_now(VkCommandBuffer cb, int crumb, uint64_t n) {
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(cb, &bi);
|
||||
if (crumb) vk_crumb(cb, crumb, n);
|
||||
}
|
||||
|
||||
// The command buffer the following packets go into; begin: the recorder begins it (TOTK_REC_END=1).
|
||||
typedef struct { VkCommandBuffer cb; int begin, crumb; uint64_t n; } A_BeginCb;
|
||||
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence, int crumb, uint64_t n) {
|
||||
static int mode = -1;
|
||||
if (mode < 0) {
|
||||
// Default off on AMD GPUs. With the recorder those drivers lost the device or hung the GPU at startup on
|
||||
@@ -154,6 +200,9 @@ void rec_begin(VkCommandBuffer cb, uint64_t fence) {
|
||||
rec_chk = rt_knob("REC_CHECK", "0"); // debug switches, read before the recorder starts
|
||||
const char *d = cfg_get("TOTK_REC_DELAY_US");
|
||||
rec_delay_ns = d && *d && atoi(d) > 0 ? (uint64_t)atoi(d) * 1000 : 0;
|
||||
const char *pw = cfg_get("TOTK_REC_PUB"); // words per publish (pub); 0: every packet
|
||||
Q.pub_words = rec_chk ? 0 : pw && *pw ? (uint64_t)atoi(pw) : 64;
|
||||
if (Q.pub_words > RING_WORDS / 8) Q.pub_words = RING_WORDS / 8;
|
||||
rec_thread = plat_thread_new(rec_main, NULL, "vk recorder");
|
||||
if (!rec_thread) {
|
||||
rt_log("vk_rec: no recorder thread: the replay makes its Vulkan calls itself");
|
||||
@@ -162,17 +211,29 @@ void rec_begin(VkCommandBuffer cb, uint64_t fence) {
|
||||
rt_log("vk_rec: the replay's Vulkan commands are made by a recorder thread%s",
|
||||
mode == 2 ? " in the B blocks (TOTK_AB)" : "");
|
||||
if (rec_chk) rt_log("vk_rec: TOTK_REC_CHECK: deferred and destroyed objects are searched in the ring");
|
||||
if (Q.pub_words != 64) rt_log("vk_rec: TOTK_REC_PUB: head published every %llu words",
|
||||
(unsigned long long)Q.pub_words);
|
||||
if (rec_delay_ns) rt_log("vk_rec: TOTK_REC_DELAY_US: the recorder waits %llu us before each batch",
|
||||
(unsigned long long)(rec_delay_ns / 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
RS_.parts++;
|
||||
if (!RT_KNOB_ON(mode)) return;
|
||||
int on = RT_KNOB_ON(mode), own = on && rec_own();
|
||||
if (!own) {
|
||||
// The submitting thread begins cb. The part pool is the recorder's while it has packets (an A/B switch
|
||||
// between parts): it finishes them first.
|
||||
rec_drain();
|
||||
begin_now(cb, crumb, n);
|
||||
if (!on) return;
|
||||
}
|
||||
RS_.parts_on++;
|
||||
rec_cb = cb; // the recorder is idle: rec_end drained it
|
||||
__atomic_store_n(&part_h0, put_h, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&part_h0, Q.put_h, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&part_fence, fence, __ATOMIC_RELEASE);
|
||||
A_BeginCb *a = put(R_BeginCb, sizeof *a);
|
||||
*a = (A_BeginCb){cb, own, crumb, n};
|
||||
pub_now();
|
||||
rec_cb = cb;
|
||||
rec_live = 1;
|
||||
}
|
||||
|
||||
@@ -182,42 +243,82 @@ void rec_end(void) {
|
||||
rec_live = 0;
|
||||
}
|
||||
|
||||
// TOTK_REC_END (default 1): the thread that recorded the part also ends it, after the breadcrumb crumb (kind 0: none)
|
||||
// that vk_sync.c writes at the end of a submission. On an RX 6700 XT (driver 26.10.1, issue #5) parts the recorder
|
||||
// recorded and the submitting thread ended broke when a submission was split (vk_sync.c vk_sync_split): the GPU hung
|
||||
// or the scene turned green, then white. The handoff in rec_drain orders the two threads, so Vulkan allows the old
|
||||
// split; the driver did not cope with it. TOTK_REC_END=0 brings the old split back.
|
||||
// TOTK_REC_END (default 1): a part the recorder records is the recorder's from its begin to its submission: it begins
|
||||
// it, makes every command, ends it after the breadcrumb crumb (kind 0: none) that vk_sync.c writes at the end of a
|
||||
// submission, and submits it. The submitting thread touches the part pool only while the recorder is idle. That is
|
||||
// LAS Recomp's and Savage Wind Recomp's model (runtime/nvn/vulkan/record.h: command buffers allocated once, then only
|
||||
// the recorder begins, records, ends and submits them). Before, the submitting thread began and submitted parts the
|
||||
// recorder recorded and, before that, also ended them. The handoff in rec_drain orders the two threads, so Vulkan
|
||||
// allows that split, but on an RX 6700 XT (driver 26.10.1, issue #5) parts ended by the other thread broke when a
|
||||
// submission was split (vk_sync.c vk_sync_split): the GPU hung or the scene turned green, then white. Moving the end
|
||||
// fixed that; begin and submit followed. TOTK_REC_END=0 brings the old split back: the submitting thread begins,
|
||||
// ends and submits every part.
|
||||
// The line is written only when a part was recorded by the recorder: with the recorder off (the default on AMD) the
|
||||
// switch changes nothing, and reports read an unconditional line as proof that the recorder was running (#315).
|
||||
typedef struct { int kind; uint64_t n; } A_EndCb;
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n) {
|
||||
static int mode = -1, said;
|
||||
if (mode < 0) mode = rt_knob("REC_END", "1");
|
||||
static int said;
|
||||
int own = rec_own();
|
||||
if (rec_live && cb == rec_cb && !said) {
|
||||
said = 1;
|
||||
rt_log("vk_rec: a part the recorder records is ended by %s (TOTK_REC_END=%d)",
|
||||
mode ? "the recorder" : "the submitting thread", mode);
|
||||
rt_log("vk_rec: a part the recorder records is begun, ended and submitted by %s (TOTK_REC_END=%d)",
|
||||
own ? "the recorder" : "the submitting thread", own);
|
||||
}
|
||||
if (!rec_live || cb != rec_cb || !mode) {
|
||||
if (!rec_live || cb != rec_cb || !own) {
|
||||
rec_end();
|
||||
if (crumb) vk_crumb(cb, crumb, n);
|
||||
return 0;
|
||||
}
|
||||
A_EndCb *a = put(R_EndCb, sizeof *a);
|
||||
*a = (A_EndCb){crumb, n};
|
||||
pub();
|
||||
rec_drain();
|
||||
pub_now();
|
||||
rec_live = 0;
|
||||
sub_pending = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// At most two command buffers and one signal: the shape vk_sync.c submits.
|
||||
typedef struct { uint32_t ncb, nsig; VkCommandBufferSubmitInfo cbi[2]; VkSemaphoreSubmitInfo sig; } A_Submit;
|
||||
VkResult rec_submit(const VkSubmitInfo2 *si) {
|
||||
if (!sub_pending) {
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit2(vk_ctx()->gfx_queue, 1, si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
return r;
|
||||
}
|
||||
sub_pending = 0;
|
||||
if (si->commandBufferInfoCount > 2 || si->signalSemaphoreInfoCount > 1 || si->waitSemaphoreInfoCount || si->pNext)
|
||||
rt_fatal("vk_rec: a submission of %u command buffers, %u waits and %u signals the recorder cannot carry",
|
||||
si->commandBufferInfoCount, si->waitSemaphoreInfoCount, si->signalSemaphoreInfoCount);
|
||||
A_Submit *a = put(R_Submit, sizeof *a);
|
||||
memset(a, 0, sizeof *a);
|
||||
a->ncb = si->commandBufferInfoCount, a->nsig = si->signalSemaphoreInfoCount;
|
||||
for (uint32_t i = 0; i < a->ncb; i++) a->cbi[i] = si->pCommandBufferInfos[i];
|
||||
if (a->nsig) a->sig = si->pSignalSemaphoreInfos[0];
|
||||
pub();
|
||||
// Waited for here, so the queue holds every earlier submission when the caller goes on, as with the recorder off:
|
||||
// the present, uploads, readbacks and device waits on other threads rely on that order.
|
||||
rec_drain();
|
||||
RS_.submits++;
|
||||
return r_sub_res;
|
||||
}
|
||||
|
||||
static void submit_run(const A_Submit *a) {
|
||||
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = a->ncb,
|
||||
.pCommandBufferInfos = a->cbi, .signalSemaphoreInfoCount = a->nsig,
|
||||
.pSignalSemaphoreInfos = a->nsig ? &a->sig : NULL};
|
||||
vk_queue_lock();
|
||||
r_sub_res = vkQueueSubmit2(vk_ctx()->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
}
|
||||
|
||||
void rec_stats_log(void) {
|
||||
if (!(rt_trace & RT_TRACE_PERF) || !RS_.parts) return;
|
||||
rt_log("vk_rec: %llu of %llu parts recorded by the recorder: %llu packets (%.1f MiB), %llu drains (%llu for a call "
|
||||
"without a packet) waited %.2f ms in all, ring full %llu times",
|
||||
"without a packet) waited %.2f ms in all, ring full %llu times, %llu submissions made by the recorder",
|
||||
(unsigned long long)RS_.parts_on, (unsigned long long)RS_.parts, (unsigned long long)RS_.packets,
|
||||
RS_.words * 8 / 1048576.0, (unsigned long long)RS_.drains, (unsigned long long)RS_.fallbacks,
|
||||
RS_.drain_ns / 1e6, (unsigned long long)RS_.full_waits);
|
||||
RS_.drain_ns / 1e6, (unsigned long long)RS_.full_waits, (unsigned long long)RS_.submits);
|
||||
memset(&RS_, 0, sizeof RS_);
|
||||
}
|
||||
|
||||
@@ -235,7 +336,7 @@ static const char *const kind_name[REC_KINDS] = {"image", "view", "buffer"};
|
||||
static volatile int32_t chk_seen[2], chk_named, chk_logged, chk_hz[2][2][REC_KINDS][64]; // [destroy][made][kind][op]
|
||||
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu) {
|
||||
if (!rec_chk || !handle || kind < 0 || kind >= REC_KINDS || !__atomic_load_n(&rec_live, __ATOMIC_RELAXED)) return;
|
||||
uint64_t h = __atomic_load_n(&head, __ATOMIC_ACQUIRE), p = __atomic_load_n(&part_h0, __ATOMIC_ACQUIRE);
|
||||
uint64_t h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE), p = __atomic_load_n(&part_h0, __ATOMIC_ACQUIRE);
|
||||
uint64_t fence = __atomic_load_n(&part_fence, __ATOMIC_ACQUIRE);
|
||||
if (p > h || h - p > RING_WORDS / 2) return; // a part this large is not searched
|
||||
plat_inc32(&chk_seen[destroy != 0]);
|
||||
@@ -246,7 +347,7 @@ void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint6
|
||||
if (op != R_PAD)
|
||||
for (uint32_t i = 1; i < n; i++) {
|
||||
if (ring[(p + i) % RING_WORDS] != handle) continue;
|
||||
int made = p < __atomic_load_n(&tail, __ATOMIC_ACQUIRE);
|
||||
int made = p < __atomic_load_n(&Q.tail, __ATOMIC_ACQUIRE);
|
||||
if (!destroy && tl >= fence) { plat_inc32(&chk_named); return; }
|
||||
plat_inc32(&chk_hz[destroy][made][kind][op & 63]);
|
||||
if (plat_inc32(&chk_logged) <= 64)
|
||||
@@ -642,7 +743,14 @@ TARGET_CMD(rec_write_mask, R_WriteMask, PFN_vkCmdSetColorWriteMaskEXT, VkColorCo
|
||||
// ---- the recorder's side
|
||||
|
||||
static void exec_packet(uint32_t op, void *p) {
|
||||
VkCommandBuffer cb = rec_cb;
|
||||
if (op == R_BeginCb) {
|
||||
A_BeginCb *a = p;
|
||||
r_cb = a->cb;
|
||||
if (a->begin) begin_now(r_cb, a->crumb, a->n);
|
||||
return;
|
||||
}
|
||||
if (op == R_Submit) { submit_run(p); return; }
|
||||
VkCommandBuffer cb = r_cb;
|
||||
switch (op) {
|
||||
#define F(name, ...) case R_##name: { A_##name *a = p; vkCmd##name(cb, __VA_ARGS__); break; }
|
||||
F(BeginQuery, a->p, a->q, a->f)
|
||||
|
||||
+17
-11
@@ -1,11 +1,12 @@
|
||||
// The recorder thread (vk_rec.c): the Vulkan calls the replay makes into its command buffer are made by a thread of
|
||||
// their own. vk_sync.c brackets each part of a submission with rec_begin (after vkBeginCommandBuffer) and rec_end
|
||||
// (before vkEndCommandBuffer). In between, every vkCmd* on that command buffer copies its arguments into a packet of a
|
||||
// ring, in call order, and the recorder makes the real call in that order; calls into any other command buffer (uploads,
|
||||
// the window, probes) are made at once. A call the ring has no packet for (a structure with a pNext chain) waits for
|
||||
// the recorder to empty the ring and is then made at once on the calling thread. rec_end returns once the recorder made
|
||||
// every call. vk_sync.c ends a part through rec_end_cb: the recorder makes the last calls into a part it recorded,
|
||||
// vkEndCommandBuffer included; submitting and the command pool stay on the submitting thread.
|
||||
// The recorder thread (vk_rec.c): the Vulkan calls the replay makes into its command buffers are made by a thread of
|
||||
// their own. vk_sync.c begins each part of a submission (and the mirror prologue) with rec_begin, ends it with
|
||||
// rec_end_cb and submits it with rec_submit. In between, every vkCmd* on that command buffer copies its arguments
|
||||
// into a packet of a ring, in call order, and the recorder makes the real call in that order; calls into any other
|
||||
// command buffer (uploads, the window, probes) are made at once. A call the ring has no packet for (a structure with a
|
||||
// pNext chain) waits for the recorder to empty the ring and is then made at once on the calling thread. With
|
||||
// TOTK_REC_END=1 (default) the recorder also begins, ends and submits the parts it records, so a part's command buffer
|
||||
// never changes threads; rec_submit returns once the recorder submitted it. The submitting thread allocates from the
|
||||
// part pool only while the recorder is idle (vk_sync.c claim_slot).
|
||||
//
|
||||
// Every vkCmd* the runtime calls through vk.h goes through the rc_* wrappers below (the macros at the end); the
|
||||
// extension commands called through function pointers have rec_* wrappers of their own. TOTK_VK_RECORDER=0 makes every
|
||||
@@ -15,11 +16,16 @@
|
||||
// MIT, Copyright (c) 2026 Peiikai): the driver's work behind the commands depends on nothing the replay decides next.
|
||||
#pragma once
|
||||
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence); // the submitting thread, sub_lock held; fence: the part's value
|
||||
// The submitting thread, sub_lock held: vkBeginCommandBuffer (one submission) on cb and the breadcrumb crumb (0: none,
|
||||
// vk_crumbs.c) with number n, made by the recorder when it records the part, else at once; fence: the part's value.
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence, int crumb, uint64_t n);
|
||||
void rec_end(void); // waits for the recorder; no-op when rec_begin did not start it
|
||||
// The end of part cb: the breadcrumb crumb (0: none, vk_crumbs.c), then vkEndCommandBuffer, both made by the recorder
|
||||
// when it records cb, and the wait for it (1); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
|
||||
// The end of part cb: the breadcrumb crumb, then vkEndCommandBuffer, both queued for the recorder when it records cb
|
||||
// (1: rec_submit hands the part over); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
|
||||
// vkQueueSubmit2 of si (at most two command buffers, one signal, no wait, no fence) under vk_queue_lock: by the
|
||||
// recorder after a part it ended, waited for, else at once.
|
||||
VkResult rec_submit(const VkSubmitInfo2 *si);
|
||||
void rec_drain(void); // waits until the recorder made every call queued so far
|
||||
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
|
||||
// TOTK_REC_CHECK=1 (debug, recorder on): vk_memory.c reports each object it defers (destroy 0, stamped tl) or destroys
|
||||
|
||||
+7
-20
@@ -328,6 +328,7 @@ static int claim_slot(VkCtx *c, uint64_t fence) {
|
||||
slots[i].fence = fence;
|
||||
return i;
|
||||
}
|
||||
rec_drain(); // the part pool is the recorder's while it has packets (vk_rec.h): it is idle for the allocation
|
||||
CmdSlot *n = realloc(slots, sizeof *n * (size_t)(nslots + 1));
|
||||
if (!n) rt_fatal("vk: out of memory (command buffers)");
|
||||
slots = n;
|
||||
@@ -353,13 +354,11 @@ static VkCommandBuffer prologue(VkCtx *c, uint64_t fence) {
|
||||
if (!vk_mem_mirror_pending()) return VK_NULL_HANDLE;
|
||||
int i = claim_slot(c, fence); // before reading slots: the claim may move it
|
||||
VkCommandBuffer cb = slots[i].cb;
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(cb, &bi);
|
||||
rec_begin(cb, fence, 0, 0); // a command buffer of the part pool: the recorder's like the parts
|
||||
int pair = ts_open(cb);
|
||||
vk_mem_mirror_prologue(cb);
|
||||
ts_close(cb, pair, fence);
|
||||
vkEndCommandBuffer(cb);
|
||||
if (!rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb);
|
||||
return cb;
|
||||
}
|
||||
|
||||
@@ -375,21 +374,15 @@ VkCommandBuffer vk_sync_split(void) {
|
||||
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = pro ? 2 : 1,
|
||||
.pCommandBufferInfos = pro ? cbi : cbi + 1};
|
||||
ts_submitted();
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
VkResult r = rec_submit(&si); // by the recorder when it recorded the part
|
||||
if (r != VK_SUCCESS) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
|
||||
}
|
||||
vk_mem_note_submit(cur_fence); // objects destroyed from here on may be in use by the part just submitted
|
||||
CmdSlot *s = take_slot(c, cur_fence); // the submitted part keeps its slot until the value is reached
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(s->cb, &bi);
|
||||
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
|
||||
rec_begin(s->cb, cur_fence, VK_CRUMB_SUBMIT, cur_fence);
|
||||
ts_begin(s->cb);
|
||||
rec_begin(s->cb, cur_fence);
|
||||
return s->cb;
|
||||
}
|
||||
|
||||
@@ -412,13 +405,9 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
uint64_t fence = __atomic_load_n(&issued, __ATOMIC_ACQUIRE) + 1;
|
||||
cur_fence = fence;
|
||||
CmdSlot *s = take_slot(c, fence);
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
uint64_t t0 = perf_begin();
|
||||
vkBeginCommandBuffer(s->cb, &bi);
|
||||
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
|
||||
rec_begin(s->cb, fence, VK_CRUMB_SUBMIT, fence); // begun by the recorder when it records the part
|
||||
ts_begin(s->cb);
|
||||
rec_begin(s->cb, fence);
|
||||
int recorded = vk_exec_record(s->cb, handles, n, fence);
|
||||
s = &slots[cur_slot]; // the last part (vk_sync_split)
|
||||
if (recorded) ts_end(s->cb, fence);
|
||||
@@ -439,9 +428,7 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
.pCommandBufferInfos = pro ? cbi : cbi + 1, .signalSemaphoreInfoCount = 1,
|
||||
.pSignalSemaphoreInfos = &sig};
|
||||
ts_submitted();
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
VkResult r = rec_submit(&si); // by the recorder when it recorded the part
|
||||
if (r != VK_SUCCESS) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: vkQueueSubmit2 of value %llu failed (%d)", (unsigned long long)fence, (int)r);
|
||||
|
||||
+236
-12
@@ -61,6 +61,10 @@ typedef struct Tex {
|
||||
uint8_t *snap; // a texture that fills no page: its storage bytes when the GPU write was recorded
|
||||
uint32_t wt_n; // WriteTexels into the storage since the last upload
|
||||
struct Tex *rel_prev, *rel_next; // while released: the list of released entries, oldest first
|
||||
uint64_t rel_ms; // while released: when (coarse_ms)
|
||||
uint64_t use_seq; // order of the last creation or take-back among all entries
|
||||
uint64_t rel_seq; // while released: use_seq of the newest entry placed before this release; an entry
|
||||
// with a later use_seq on overlapping storage replaced this one's bytes
|
||||
} Tex;
|
||||
|
||||
// A slot keeps what vk_texture_by_id resolved for it, so a draw binding skips the key lookup and the address chain: the
|
||||
@@ -540,6 +544,46 @@ static void rel_unlink(Tex *e) {
|
||||
e->rel_prev = e->rel_next = NULL;
|
||||
}
|
||||
|
||||
// A clock read per submit, for stamps on paths that run many times a frame.
|
||||
static uint64_t coarse_ms;
|
||||
static uint64_t use_counter;
|
||||
|
||||
// How long released entries wait until the game takes them back, split into those holding GPU-written content and the
|
||||
// others (TOTK_TRACE=perf, vk_texture_released_log): where transient textures end and the tail that can go begins.
|
||||
enum { AGE_1S, AGE_5S, AGE_30S, AGE_120S, AGE_LONG, AGE_N };
|
||||
static const char *const age_name[AGE_N] = {"<1s", "<5s", "<30s", "<120s", ">=120s"};
|
||||
static uint64_t takeback_n[2][AGE_N];
|
||||
static int age_bucket(uint64_t ms) {
|
||||
return ms < 1000 ? AGE_1S : ms < 5000 ? AGE_5S : ms < 30000 ? AGE_30S : ms < 120000 ? AGE_120S : AGE_LONG;
|
||||
}
|
||||
|
||||
// Keys of entries the pressure path dropped, to count the ones the game asks for again soon after: a drop that buys
|
||||
// nothing and, for GPU-written content, loses it. Direct-mapped, a lost record only undercounts.
|
||||
#define DROPPED_SLOTS 4096u
|
||||
static struct { uint64_t hash, ms; } dropped[DROPPED_SLOTS];
|
||||
static uint64_t n_recreated[2], n_recreated_written[2]; // index: within 10 s, later
|
||||
static uint64_t key_hash(const TexKey *k) {
|
||||
uint64_t h = 0xcbf29ce484222325ull;
|
||||
const uint8_t *b = (const uint8_t *)k;
|
||||
for (size_t i = 0; i < sizeof *k; i++) h = (h ^ b[i]) * 0x100000001b3ull;
|
||||
return h | 1;
|
||||
}
|
||||
static void dropped_note(const Tex *e) {
|
||||
uint64_t h = key_hash(&e->key);
|
||||
dropped[h >> 3 & (DROPPED_SLOTS - 1)].hash = h ^ (uint64_t)(e->pub.gpu_owned != 0) << 1;
|
||||
dropped[h >> 3 & (DROPPED_SLOTS - 1)].ms = coarse_ms;
|
||||
}
|
||||
static void dropped_check(const TexKey *k) {
|
||||
uint64_t h = key_hash(k);
|
||||
unsigned i = h >> 3 & (DROPPED_SLOTS - 1);
|
||||
uint64_t was = dropped[i].hash;
|
||||
if (!was || (was & ~2ull) != (h & ~2ull)) return;
|
||||
int later = coarse_ms - dropped[i].ms >= 10000;
|
||||
n_recreated[later]++;
|
||||
if (was != h) n_recreated_written[later]++;
|
||||
dropped[i].hash = 0;
|
||||
}
|
||||
|
||||
static void destroy_tex(Tex *e) {
|
||||
own_remove(e);
|
||||
tex_gen_bump(e->key.addr);
|
||||
@@ -569,15 +613,112 @@ static void sweep_released(void) {
|
||||
while (nreleased > RELEASED_MAX && rel_head) destroy_tex(rel_head);
|
||||
}
|
||||
|
||||
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each submit drops up to
|
||||
// PRESSURE_STEP of the oldest released entries, so a 4 GiB card does not keep RELEASED_MAX released images (each ASTC
|
||||
// one decoded to RGBA8) while the heap spills into system memory. A few per submit, for the reason above. Only entries
|
||||
// that come back intact are dropped: mirrored ones the GPU never wrote, re-uploaded from their storage. A render target
|
||||
// lives only in its image; dropping it brought the texture back empty (black flashes in a Depths boss fight, ~170
|
||||
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each budget query drops up
|
||||
// to PRESSURE_STEP released entries, oldest first, until the use is back under the line; a few at a time, for the reason
|
||||
// above. Two kinds come back without loss:
|
||||
//
|
||||
// Overwritten: the game placed another texture on the entry's storage after releasing it, so on the Switch its bytes are
|
||||
// gone too, and it has waited DEAD_AGE_MS. Transient render targets are taken back within a second or two (a stand-still
|
||||
// field window: about 19000 take-backs under 1 s, 2 later than that, none after 30 s), while their leftovers at
|
||||
// addresses the game's allocator moved on from stay released for good: 1.9 GiB of 2.0 GiB, nearly all of it render targets
|
||||
// no storage can refill. Not bound through a finalized slot for DEAD_QUIET_SUBMITS either: such a slot still samples the
|
||||
// entry (slot_resolve).
|
||||
//
|
||||
// Clean: mirrored ones the GPU never wrote, re-uploaded from their storage, at the cost of the upload, so after
|
||||
// CLEAN_AGE_MS: without the wait a fight re-created about 500 of them per 10 s. Any other render target lives only in its
|
||||
// image: dropping one that was merely old brought the texture back empty (black flashes in a Depths boss fight, ~170
|
||||
// "left uninitialised" aliases a minute).
|
||||
#define PRESSURE_STEP 32
|
||||
#define CLEAN_AGE_MS 5000u
|
||||
#define DEAD_AGE_MS 30000u
|
||||
#define DEAD_QUIET_SUBMITS 600u
|
||||
#define DEAD_RETRY_QUERIES 8 // budget queries to skip after a search that found nothing
|
||||
static int vram_pressure = -1;
|
||||
static uint64_t n_pressure_drops;
|
||||
static uint64_t n_pressure_drops, n_pressure_overwritten;
|
||||
|
||||
static int tex_by_addr(const void *a, const void *b) {
|
||||
const Tex *x = *(Tex *const *)a, *y = *(Tex *const *)b;
|
||||
return x->pub.addr < y->pub.addr ? -1 : x->pub.addr > y->pub.addr;
|
||||
}
|
||||
|
||||
// All entries sorted by storage address, with the highest storage end up to each one: the input of storage_taken.
|
||||
typedef struct { Tex **e; uint64_t *end; int n; } AddrIndex;
|
||||
|
||||
static int addr_index(AddrIndex *x) {
|
||||
x->n = 0;
|
||||
x->e = malloc(sizeof *x->e * (size_t)(by_key.n + 1));
|
||||
x->end = malloc(sizeof *x->end * (size_t)(by_key.n + 1));
|
||||
if (!x->e || !x->end) { free(x->e), free(x->end); return 0; }
|
||||
for (uint32_t i = 0; i < by_key.cap; i++)
|
||||
if (by_key.vals[i]) x->e[x->n++] = by_key.vals[i];
|
||||
qsort(x->e, (size_t)x->n, sizeof *x->e, tex_by_addr);
|
||||
uint64_t top = 0;
|
||||
for (int i = 0; i < x->n; i++) {
|
||||
uint64_t end = x->e[i]->pub.addr + x->e[i]->t.L.size;
|
||||
x->end[i] = top = end > top ? end : top;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void addr_index_free(AddrIndex *x) { free(x->e), free(x->end); }
|
||||
|
||||
// A texture placed after the release of x->e[i] overlaps its storage.
|
||||
static int storage_taken(const AddrIndex *x, int i) {
|
||||
const Tex *e = x->e[i];
|
||||
for (int j = i; --j >= 0 && x->end[j] > e->pub.addr;)
|
||||
if (x->e[j]->pub.addr + x->e[j]->t.L.size > e->pub.addr && x->e[j]->use_seq > e->rel_seq) return 1;
|
||||
for (int j = i; ++j < x->n && x->e[j]->pub.addr < e->pub.addr + e->t.L.size;)
|
||||
if (x->e[j]->use_seq > e->rel_seq) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t own_bytes(const Tex *e) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!e->alloc || !c) return 0;
|
||||
VmaAllocationInfo ai;
|
||||
vmaGetAllocationInfo(c->vma, e->alloc, &ai);
|
||||
return ai.size;
|
||||
}
|
||||
|
||||
static int dead_candidate(const Tex *e) {
|
||||
return e->alloc && (!e->checked || submit_epoch - e->checked >= DEAD_QUIET_SUBMITS);
|
||||
}
|
||||
|
||||
// Drops overwritten entries until `need` bytes are free or the step is used up. The sort runs only under pressure, while
|
||||
// an old enough entry exists, and rests for a while when it finds nothing to drop. Returns the bytes freed.
|
||||
static uint64_t pressure_search_ns; // longest search since the last mem: released line
|
||||
static uint64_t drop_overwritten(uint64_t need, int *dropped) {
|
||||
static int rest;
|
||||
if (rest > 0) { rest--; return 0; }
|
||||
if (!rel_head || coarse_ms - rel_head->rel_ms < DEAD_AGE_MS) return 0;
|
||||
uint64_t t0 = plat_now_ns(), freed = 0;
|
||||
AddrIndex x;
|
||||
if (!addr_index(&x)) return 0;
|
||||
Tex *cand[PRESSURE_STEP];
|
||||
int nc = 0;
|
||||
for (Tex *e = rel_head; e && nc < PRESSURE_STEP && coarse_ms - e->rel_ms >= DEAD_AGE_MS; e = e->rel_next) {
|
||||
if (!dead_candidate(e)) continue;
|
||||
int lo = 0, hi = x.n; // the first entry at e's address, then e among those
|
||||
while (lo < hi) {
|
||||
int m = (lo + hi) / 2;
|
||||
if (x.e[m]->pub.addr < e->pub.addr) lo = m + 1;
|
||||
else hi = m;
|
||||
}
|
||||
while (lo < x.n && x.e[lo] != e) lo++;
|
||||
if (lo < x.n && storage_taken(&x, lo)) cand[nc++] = e;
|
||||
}
|
||||
addr_index_free(&x);
|
||||
for (int i = 0; i < nc && freed < need; i++) {
|
||||
freed += own_bytes(cand[i]);
|
||||
dropped_note(cand[i]);
|
||||
destroy_tex(cand[i]);
|
||||
(*dropped)++, n_pressure_drops++, n_pressure_overwritten++;
|
||||
}
|
||||
if (!nc) rest = DEAD_RETRY_QUERIES;
|
||||
uint64_t ns = plat_now_ns() - t0;
|
||||
if (ns > pressure_search_ns) pressure_search_ns = ns;
|
||||
return freed;
|
||||
}
|
||||
|
||||
static void sweep_pressure(void) { // tex_lock held
|
||||
if (vram_pressure < 0) {
|
||||
@@ -589,11 +730,16 @@ static void sweep_pressure(void) { // tex_lock held
|
||||
if (!vram_pressure || !rel_head || submit_epoch & 7) return; // the budget query every 8 submits
|
||||
uint64_t used, budget;
|
||||
int exact;
|
||||
if (!vk_mem_vram(&used, &budget, &exact) || used <= budget / 100 * (uint64_t)vram_pressure) return;
|
||||
if (!vk_mem_vram(&used, &budget, &exact)) return;
|
||||
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
|
||||
if (used <= line) return;
|
||||
int dropped = 0;
|
||||
for (Tex *e = rel_head, *next; e && dropped < PRESSURE_STEP; e = next) {
|
||||
uint64_t need = used - line, freed = drop_overwritten(need, &dropped);
|
||||
for (Tex *e = rel_head, *next; e && freed < need && dropped < PRESSURE_STEP && coarse_ms - e->rel_ms >= CLEAN_AGE_MS; e = next) {
|
||||
next = e->rel_next;
|
||||
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
|
||||
freed += own_bytes(e);
|
||||
dropped_note(e);
|
||||
destroy_tex(e);
|
||||
dropped++, n_pressure_drops++;
|
||||
}
|
||||
@@ -947,6 +1093,8 @@ static Tex *entry_of(const tex_t *t) {
|
||||
}
|
||||
if (!(e = create_tex(t, &k, tex_alias))) return NULL;
|
||||
perf_count(PERF_CNT_TEX_NEW);
|
||||
dropped_check(&k);
|
||||
e->use_seq = ++use_counter;
|
||||
// released entries of another descriptor at this address are replaced by the new one; a live one holding
|
||||
// GPU-written content is the alias source, preferring the one that differs only in swizzle (same_but_swizzle)
|
||||
int alias = 0;
|
||||
@@ -982,7 +1130,11 @@ static Tex *entry_of(const tex_t *t) {
|
||||
nvn_hm_put(&by_key, &k, e);
|
||||
tex_gen_bump(k.addr);
|
||||
}
|
||||
if (e->released) e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(k.addr);
|
||||
if (e->released) {
|
||||
takeback_n[e->pub.gpu_owned != 0][age_bucket(coarse_ms - e->rel_ms)]++;
|
||||
e->use_seq = ++use_counter;
|
||||
e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(k.addr);
|
||||
}
|
||||
e->t = *t;
|
||||
return e;
|
||||
}
|
||||
@@ -1073,6 +1225,8 @@ void vk_texture_release(const tex_t *t) {
|
||||
Tex *e = nvn_hm_get(&by_key, &k);
|
||||
if (e && !e->released) {
|
||||
e->released = 1;
|
||||
e->rel_ms = coarse_ms;
|
||||
e->rel_seq = use_counter;
|
||||
nreleased++;
|
||||
rel_append(e);
|
||||
tex_gen_bump(k.addr);
|
||||
@@ -1236,6 +1390,7 @@ uint32_t vk_texture_epoch(void) { return __atomic_load_n(&submit_epoch, __ATOMIC
|
||||
void vk_texture_new_submit(void) {
|
||||
lock();
|
||||
if (!++submit_epoch) submit_epoch = 1;
|
||||
coarse_ms = plat_now_ms();
|
||||
sweep_pressure();
|
||||
static uint64_t log_at;
|
||||
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) / 60;
|
||||
@@ -1255,9 +1410,10 @@ void vk_texture_new_submit(void) {
|
||||
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
|
||||
(unsigned long long)untr);
|
||||
n_checks = 0;
|
||||
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu", (unsigned long long)f * 60,
|
||||
nreleased, (unsigned long long)n_pressure_drops);
|
||||
n_pressure_drops = 0;
|
||||
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu (%llu overwritten)",
|
||||
(unsigned long long)f * 60, nreleased, (unsigned long long)n_pressure_drops,
|
||||
(unsigned long long)n_pressure_overwritten);
|
||||
n_pressure_drops = n_pressure_overwritten = 0;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
@@ -1963,6 +2119,74 @@ void vk_texture_census(VkTexCensus *s) {
|
||||
unlock();
|
||||
}
|
||||
|
||||
// TOTK_TRACE=perf, with mem: vram: what the released entries in own memory hold and how long they waited.
|
||||
void vk_texture_released_log(uint64_t frame) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c) return;
|
||||
enum { CL_CLEAN, CL_BACKED, CL_DIRTY, CL_POOLS, CL_N };
|
||||
uint64_t cl_n[CL_N] = {0}, cl_b[CL_N] = {0}, age_b[AGE_N] = {0}, dead_n = 0, dead_b = 0, dead_dirty_b = 0;
|
||||
uint64_t bound_n = 0, bound_b = 0, ded_n = 0, ded_b = 0;
|
||||
typedef struct { uint32_t w, h, fmt, n; uint64_t b; } Group;
|
||||
Group gr[256];
|
||||
int ng = 0;
|
||||
lock();
|
||||
AddrIndex x = {0};
|
||||
int indexed = addr_index(&x);
|
||||
for (int i = 0; indexed && i < x.n; i++) {
|
||||
Tex *e = x.e[i];
|
||||
if (!e->released || !e->alloc) continue;
|
||||
VmaAllocationInfo2 ai2;
|
||||
vmaGetAllocationInfo2(c->vma, e->alloc, &ai2);
|
||||
const VmaAllocationInfo ai = ai2.allocationInfo;
|
||||
if (ai2.dedicatedMemory) ded_n++, ded_b += ai.size;
|
||||
int cl = !e->pub.mirrored ? CL_POOLS : e->gpu_dirty ? CL_DIRTY : e->pub.gpu_owned ? CL_BACKED : CL_CLEAN;
|
||||
cl_n[cl]++, cl_b[cl] += ai.size;
|
||||
if (cl != CL_CLEAN) age_b[age_bucket(coarse_ms - e->rel_ms)] += ai.size;
|
||||
if (e->checked && submit_epoch - e->checked < 600) bound_n++, bound_b += ai.size;
|
||||
if (storage_taken(&x, i)) dead_n++, dead_b += ai.size, dead_dirty_b += cl != CL_CLEAN ? ai.size : 0;
|
||||
int g = 0;
|
||||
while (g < ng && (gr[g].w != e->t.d.width || gr[g].h != e->t.d.height || gr[g].fmt != (uint32_t)e->t.d.format)) g++;
|
||||
if (g == ng && ng < 256) gr[ng++] = (Group){e->t.d.width, e->t.d.height, (uint32_t)e->t.d.format, 0, 0};
|
||||
if (g < ng) gr[g].n++, gr[g].b += ai.size;
|
||||
}
|
||||
if (indexed) addr_index_free(&x);
|
||||
const double M = 1048576.0;
|
||||
rt_log("mem: released frame %llu: own memory: clean %llu %.0f MiB, GPU-written with storage current %llu %.0f MiB, "
|
||||
"GPU-written image only %llu %.0f MiB, not mirrored %llu %.0f MiB; all but clean by age <1s %.0f <5s %.0f <30s %.0f "
|
||||
"<120s %.0f older %.0f MiB; storage taken by a later texture %llu %.0f MiB (%.0f not clean); bound through a slot "
|
||||
"in the last 600 submits %llu %.0f MiB; in dedicated memory, returned to the driver when dropped, %llu %.0f MiB; "
|
||||
"longest search %.2f ms",
|
||||
(unsigned long long)frame, (unsigned long long)cl_n[CL_CLEAN], cl_b[CL_CLEAN] / M, (unsigned long long)cl_n[CL_BACKED],
|
||||
cl_b[CL_BACKED] / M, (unsigned long long)cl_n[CL_DIRTY], cl_b[CL_DIRTY] / M, (unsigned long long)cl_n[CL_POOLS],
|
||||
cl_b[CL_POOLS] / M, age_b[AGE_1S] / M, age_b[AGE_5S] / M, age_b[AGE_30S] / M, age_b[AGE_120S] / M,
|
||||
age_b[AGE_LONG] / M, (unsigned long long)dead_n, dead_b / M, dead_dirty_b / M, (unsigned long long)bound_n,
|
||||
bound_b / M, (unsigned long long)ded_n, ded_b / M, pressure_search_ns / 1e6);
|
||||
pressure_search_ns = 0;
|
||||
for (int k = 0; k < 2; k++) {
|
||||
char line[256];
|
||||
size_t o = 0;
|
||||
for (int a = 0; a < AGE_N; a++) {
|
||||
o += (size_t)snprintf(line + o, sizeof line - o, " %s %llu", age_name[a], (unsigned long long)takeback_n[k][a]);
|
||||
takeback_n[k][a] = 0;
|
||||
}
|
||||
rt_log("mem: released frame %llu: taken back after,%s:%s", (unsigned long long)frame, k ? " GPU-written" : " others", line);
|
||||
}
|
||||
rt_log("mem: released frame %llu: dropped under pressure and created again: within 10 s %llu (%llu GPU-written), later %llu (%llu)",
|
||||
(unsigned long long)frame, (unsigned long long)n_recreated[0], (unsigned long long)n_recreated_written[0],
|
||||
(unsigned long long)n_recreated[1], (unsigned long long)n_recreated_written[1]);
|
||||
n_recreated[0] = n_recreated[1] = n_recreated_written[0] = n_recreated_written[1] = 0;
|
||||
for (int top = 0; top < 4; top++) {
|
||||
int best = -1;
|
||||
for (int g = 0; g < ng; g++)
|
||||
if (gr[g].n && (best < 0 || gr[g].b > gr[best].b)) best = g;
|
||||
if (best < 0) break;
|
||||
rt_log("mem: released frame %llu: largest %d: %ux%u fmt %#x: %u entries %.0f MiB", (unsigned long long)frame, top + 1,
|
||||
gr[best].w, gr[best].h, gr[best].fmt, gr[best].n, gr[best].b / M);
|
||||
gr[best].n = 0;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
void vk_texture_shutdown(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
lock();
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
"""tools/exefs_patches.py on modules it makes itself: code writes become sites, data writes become runs of the data file
|
||||
as emulators apply a patch file before the dynamic linker (plain bytes, DT_RELR words rebased, writes into words an
|
||||
explicit-addend relocation fills dropped), the writes it refuses (outside the image, into the linker's tables or a
|
||||
lazily bound pointer), the status and data files, and the check after the lift against the bytes recomp/gen.py lists in
|
||||
folded.txt, with gen.py run on a module with one jump table. No game files.
|
||||
|
||||
Run with .venv (lz4 and numpy, as recomp/): python tests/exefs_patches_test.py
|
||||
"""
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import exefs_patches as ep # noqa: E402
|
||||
|
||||
BUILD_ID = bytes.fromhex('0123456789abcdef') + bytes(24)
|
||||
BID_NAME = '0123456789ABCDEF' # an IPS file name may leave the build id's trailing zeros out
|
||||
MOV_W8_100, MOV_W8_200, RET = 0x52802008, 0x52804008, 0xD65F03C0
|
||||
R_RELATIVE, R_GLOB_DAT, R_JUMP_SLOT = 1027, 1025, 1026
|
||||
|
||||
|
||||
def nso(text, ro, data, bss=0x1000):
|
||||
"""An uncompressed NSO with .text at 0, .rodata at 0x1000 and .data at 0x2000."""
|
||||
segs = [(0x0, text), (0x1000, ro), (0x2000, data)]
|
||||
hdr = bytearray(0x100)
|
||||
hdr[0:4] = b'NSO0'
|
||||
off = 0x100
|
||||
for i, (mem, d) in enumerate(segs):
|
||||
struct.pack_into('<IIII', hdr, 0x10 + 0x10 * i, off, mem, len(d), bss if i == 2 else 0)
|
||||
struct.pack_into('<I', hdr, 0x60 + 4 * i, len(d))
|
||||
off += len(d)
|
||||
hdr[0x40:0x60] = BUILD_ID
|
||||
return bytes(hdr) + b''.join(d for _, d in segs)
|
||||
|
||||
|
||||
def put(buf, off, data):
|
||||
buf[off:off + len(data)] = data
|
||||
|
||||
|
||||
def reloc_module():
|
||||
"""fn at 0x8 (mov w8, #0x100; ret); .rodata: constants at 0x1000, MOD0 at 0x1100 and the relocation tables at
|
||||
0x1200 (RELA: a RELATIVE word at 0x2100, a GLOB_DAT word at 0x2108), 0x1300 (JMPREL: a JUMP_SLOT word at 0x2110)
|
||||
and 0x1400 (RELR: the word at 0x2118, addend 0x30); .data: .dynamic at 0x2000, plain bytes at 0x2200."""
|
||||
text = struct.pack('<4I', 0x14000002, 0x1100, MOV_W8_100, RET)
|
||||
ro = bytearray(0x500)
|
||||
put(ro, 0x000, b'constant bytes!!')
|
||||
put(ro, 0x100, b'MOD0' + struct.pack('<6i', 0x2000 - 0x1100, 0, 0, -0x1100, -0x1100, 0))
|
||||
put(ro, 0x200, struct.pack('<QQq', 0x2100, R_RELATIVE, 0x8) + struct.pack('<QQq', 0x2108, R_GLOB_DAT, 0))
|
||||
put(ro, 0x300, struct.pack('<QQq', 0x2110, R_JUMP_SLOT, 0))
|
||||
put(ro, 0x400, struct.pack('<Q', 0x2118))
|
||||
data = bytearray(0x300)
|
||||
dyn = [(7, 0x1200), (8, 48), (23, 0x1300), (2, 24), (0x24, 0x1400), (0x23, 8), (0, 0)]
|
||||
put(data, 0x000, b''.join(struct.pack('<QQ', t, v) for t, v in dyn))
|
||||
put(data, 0x118, struct.pack('<Q', 0x30))
|
||||
put(data, 0x200, b'ABCDEFGH')
|
||||
return nso(text, bytes(ro), bytes(data))
|
||||
|
||||
|
||||
# A module with one jump table (clang's compressed form), assembled from:
|
||||
# b fn ; 0x0
|
||||
# .word mod0 ; 0x4: the MOD0 offset
|
||||
# fn: cmp w0, #3 ; 0x8
|
||||
# b.hi Ldefault
|
||||
# adrp x8, table ; table = 0x1020: .byte 0, 2, 4, 6
|
||||
# add x8, x8, #0x20
|
||||
# adr x9, L0
|
||||
# ldrb w10, [x8, w0, uxtw]
|
||||
# add x9, x9, x10, lsl #2
|
||||
# br x9
|
||||
# L0..L3: mov w0, #10/20/30/40; ret ; Ldefault: mov w0, #0; ret
|
||||
# .rodata: 32 bytes of text before the table, 21 after it, MOD0 at 0x1040 and a RELATIVE relocation to fn (the
|
||||
# lifter's function seed); .data: .dynamic (DT_RELA) and the relocated word.
|
||||
JT_TEXT = bytes.fromhex(
|
||||
'02000014401000001f0c0071e8010054080000b008810091890000100a49603829090a8b20011fd640018052c0035fd680028052'
|
||||
'c0035fd6c0038052c0035fd600058052c0035fd600008052c0035fd6')
|
||||
JT_RO = bytes.fromhex(
|
||||
'756e746f7563686564206279746573206265666f726520746865207461626c6500020406627974657320616674657220746865'
|
||||
'207461626c65000000000000004d4f4430c00f00000000000000000000c0efffffc0efffff00000000000000003020000000000000'
|
||||
'03040000000000000800000000000000')
|
||||
JT_DATA = struct.pack('<7Q', 7, 0x1060, 8, 24, 0, 0, 0) # DT_RELA, DT_RELASZ, DT_NULL; the word the RELATIVE fills
|
||||
|
||||
|
||||
def ips32(writes):
|
||||
"""An IPS32 file of {image offset: bytes} (offsets count the NSO header, as emulators read them)."""
|
||||
body = b''.join((off + ep.NSO_HEADER).to_bytes(4, 'big') + len(d).to_bytes(2, 'big') + d
|
||||
for off, d in sorted(writes.items()))
|
||||
return b'IPS32' + body + b'EEOF'
|
||||
|
||||
|
||||
class Classify(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
path = Path(cls.tmp.name) / 'main'
|
||||
path.write_bytes(reloc_module())
|
||||
cls.m = ep.Main(path)
|
||||
cls.md = ep.MainData(cls.m)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.tmp.cleanup()
|
||||
|
||||
def run_file(self, writes, md=True):
|
||||
return ep.classify(self.m, self.md if md else None, self.m.nso.build_id, Path(f'{BID_NAME}.ips'),
|
||||
ips32(writes))
|
||||
|
||||
def test_relocated_words(self):
|
||||
self.assertEqual(self.md.rela, {0x2100: R_RELATIVE, 0x2108: R_GLOB_DAT, 0x2110: R_JUMP_SLOT})
|
||||
self.assertEqual(self.md.relr, {0x2118})
|
||||
|
||||
def test_code_only(self):
|
||||
kind, note, sites, runs, dropped = self.run_file({0x8: struct.pack('<I', MOV_W8_200)})
|
||||
self.assertEqual((kind, note, runs, dropped), ('code', '', [], 0))
|
||||
self.assertEqual(sites, {0x8: [MOV_W8_200]})
|
||||
|
||||
def test_data_only(self):
|
||||
kind, note, sites, runs, dropped = self.run_file({0x1000: b'\xAA\xBB', 0x2200: b'XY'})
|
||||
self.assertEqual((kind, sites, dropped), ('data', {}, 0))
|
||||
self.assertEqual(runs, [(0x1000, b'\xAA\xBB', False), (0x2200, b'XY', False)])
|
||||
|
||||
def test_code_and_data(self):
|
||||
kind, _, sites, runs, _ = self.run_file({0x8: struct.pack('<I', MOV_W8_200), 0x1004: b'\x01'})
|
||||
self.assertEqual(kind, 'code')
|
||||
self.assertEqual(sites, {0x8: [MOV_W8_200]})
|
||||
self.assertEqual(runs, [(0x1004, b'\x01', False)])
|
||||
|
||||
def test_same_bytes_are_nothing(self):
|
||||
kind, _, _, runs, _ = self.run_file({0x1000: b'constant'})
|
||||
self.assertEqual((kind, runs), ('empty', []))
|
||||
|
||||
def test_explicit_addend_words_change_nothing(self):
|
||||
kind, _, _, runs, dropped = self.run_file({0x2100: b'\x11\x22\x33\x44', 0x2108: b'\x55'})
|
||||
self.assertEqual((kind, runs, dropped), ('empty', [], 5))
|
||||
|
||||
def test_relr_word_is_rebased(self):
|
||||
kind, _, _, runs, _ = self.run_file({0x2118: b'\x31'})
|
||||
self.assertEqual(kind, 'data')
|
||||
self.assertEqual(runs, [(0x2118, struct.pack('<Q', 0x31), True)])
|
||||
|
||||
def test_long_runs_are_split(self):
|
||||
_, _, _, runs, _ = self.run_file({0x2200: bytes(i % 255 + 1 for i in range(ep.RUN_MAX)) + b'\xFF'})
|
||||
self.assertEqual([(o, len(d)) for o, d, _ in runs], [(0x2200, ep.RUN_MAX), (0x2200 + ep.RUN_MAX, 1)])
|
||||
|
||||
def test_refused_writes(self):
|
||||
for writes, why in (({0x2110: b'\x01'}, 'binds lazily'), ({0x2000: b'\x01'}, 'linker\'s tables'),
|
||||
({0x1208: b'\x01'}, 'linker\'s tables'), ({0x4000: b'\x01'}, 'outside main\'s image'),
|
||||
({-0x80: b'\x01'}, 'outside main\'s image')):
|
||||
kind, note, sites, runs, _ = self.run_file({0x8: struct.pack('<I', MOV_W8_200), **writes})
|
||||
self.assertEqual((kind, sites, runs), ('unsupported', {}, []), note)
|
||||
self.assertIn(why, note)
|
||||
|
||||
def test_data_without_a_data_file(self):
|
||||
kind, note, _, _, _ = self.run_file({0x1000: b'\xAA'}, md=False)
|
||||
self.assertEqual(kind, 'unsupported')
|
||||
self.assertIn('only code patches are supported', note)
|
||||
|
||||
def test_pchtxt(self):
|
||||
text = (f'@nsobid-{BID_NAME}\n@flag offset_shift 0x100\n@enabled\n'
|
||||
f'00001000 AABB\n00002200 "XY"\n@disabled\n00002204 CC\n')
|
||||
kind, _, _, runs, _ = ep.classify(self.m, self.md, self.m.nso.build_id, Path('x.pchtxt'), text.encode())
|
||||
self.assertEqual(kind, 'data')
|
||||
self.assertEqual(runs, [(0x1000, b'\xAA\xBB', False), (0x2200, b'XY', False)])
|
||||
|
||||
|
||||
class Tool(unittest.TestCase):
|
||||
"""The command line: build, then the check after the lift."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.d = Path(self.tmp.name)
|
||||
(self.d / 'exefs').mkdir()
|
||||
(self.d / 'patches.txt').write_text('# no sets of the port\n')
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def mod_file(self, mod, writes):
|
||||
f = self.d / 'mods' / mod / 'exefs' / f'{BID_NAME}.ips'
|
||||
f.parent.mkdir(parents=True, exist_ok=True)
|
||||
f.write_bytes(ips32(writes))
|
||||
|
||||
def tool(self, *args):
|
||||
r = subprocess.run([sys.executable, str(ROOT / 'tools' / 'exefs_patches.py'), '--exefs', str(self.d / 'exefs'),
|
||||
*map(str, args)], capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||
return r.stdout
|
||||
|
||||
def build(self):
|
||||
self.tool('--mods', self.d / 'mods', '--patches', self.d / 'patches.txt', '--out', self.d / 'out.txt',
|
||||
'--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data')
|
||||
status = {f[0]: f for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
|
||||
data = [line.split('\t') for line in (self.d / 'exefs.data').read_text().splitlines()
|
||||
if not line.startswith('#')]
|
||||
return status, data
|
||||
|
||||
def key(self, mod):
|
||||
return ep.fnv(f'{mod}/exefs/{BID_NAME}.ips'.encode())
|
||||
|
||||
def test_build_and_check(self):
|
||||
(self.d / 'exefs' / 'main').write_bytes(reloc_module())
|
||||
self.mod_file('Code', {0x8: struct.pack('<I', MOV_W8_200), 0x2200: b'Q'})
|
||||
self.mod_file('Data', {0x1000: b'\xAA', 0x2118: b'\x31'})
|
||||
self.mod_file('Bad', {0x2110: b'\x01'})
|
||||
status, data = self.build()
|
||||
code, dat, bad = self.key('Code'), self.key('Data'), self.key('Bad')
|
||||
self.assertEqual([status[k][2] for k in (code, dat, bad)], ['code', 'data', 'unsupported'])
|
||||
self.assertEqual([status[k][4] for k in (code, dat)], ['data 1', 'data 9']) # bytes its data lines hold
|
||||
self.assertIn(f'set mod_{code.lower()} TOTK_MODPATCH_{code} 0', (self.d / 'out.txt').read_text())
|
||||
self.assertEqual(sorted((f[0], f[2], f[3], f[4:]) for f in data),
|
||||
sorted([(code, '2200', '51', []), (dat, '1000', 'aa', []),
|
||||
(dat, '2118', '3100000000000000', ['base'])]))
|
||||
# after the lift: .rodata bytes the lifted code read make the Data file unsupported; the Code file's data is
|
||||
# in .data, which the lifter never reads
|
||||
folded = self.d / 'folded.txt'
|
||||
folded.write_text('# test\n1000 1001\n')
|
||||
self.tool('--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data', '--folded', folded)
|
||||
status2 = {f[0]: f for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
|
||||
self.assertEqual(status2[code][2], 'code')
|
||||
self.assertEqual(status2[dat][2:4], ['unsupported', '-'])
|
||||
self.assertIn('jump table', status2[dat][4])
|
||||
left = [line.split('\t')[0] for line in (self.d / 'exefs.data').read_text().splitlines()
|
||||
if not line.startswith('#')]
|
||||
self.assertEqual(left, [code])
|
||||
|
||||
def test_check_without_the_lift_list(self):
|
||||
(self.d / 'exefs' / 'main').write_bytes(reloc_module())
|
||||
self.mod_file('Ro', {0x1000: b'\xAA'})
|
||||
self.mod_file('Rw', {0x2200: b'Q'})
|
||||
self.build()
|
||||
self.tool('--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data', '--folded',
|
||||
self.d / 'missing.txt')
|
||||
status = {f[0]: f[2] for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
|
||||
self.assertEqual((status[self.key('Ro')], status[self.key('Rw')]), ('unsupported', 'data'))
|
||||
|
||||
def test_jump_table_through_the_lift(self):
|
||||
"""recomp/gen.py lists the jump table's bytes in folded.txt; a write into them is refused, one beside them
|
||||
is kept."""
|
||||
(self.d / 'exefs' / 'main').write_bytes(nso(JT_TEXT, JT_RO, JT_DATA))
|
||||
r = subprocess.run([sys.executable, str(ROOT / 'recomp' / 'gen.py'), '--exefs', str(self.d / 'exefs'),
|
||||
'--out', str(self.d / 'gen'), '--modules', 'main', '--jobs', '1'],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
folded = self.d / 'gen' / 'main' / 'folded.txt'
|
||||
self.assertEqual(ep.read_ranges(folded), [(0x1020, 0x1024)])
|
||||
self.mod_file('Table', {0x1022: b'\x00'})
|
||||
self.mod_file('Beside', {0x1024: b'B'})
|
||||
self.build()
|
||||
self.tool('--status', self.d / 'exefs.status', '--data', self.d / 'exefs.data', '--folded', folded)
|
||||
status = {f[0]: f[2] for f in (line.split('\t') for line in (self.d / 'exefs.status').read_text().splitlines())}
|
||||
self.assertEqual((status[self.key('Table')], status[self.key('Beside')]), ('unsupported', 'data'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -377,13 +377,19 @@ class DpiTest(unittest.TestCase):
|
||||
code = ('import tkinter\n'
|
||||
'from installer import dpi, gui\n'
|
||||
'root = tkinter.Tk()\n'
|
||||
'print(dpi.awareness(), dpi.scale(), root.tk.call("tk", "scaling"), gui.PAD, gui.WRAP)\n'
|
||||
'print(dpi.awareness(), dpi.scale(), root.tk.call("tk", "scaling"), gui.PAD, gui.WRAP, '
|
||||
'root.winfo_screenwidth())\n'
|
||||
'root.destroy()\n')
|
||||
r = subprocess.run([sys.executable, '-c', code], cwd=ROOT, capture_output=True, text=True, timeout=120)
|
||||
self.assertEqual(r.returncode, 0, r.stderr[-2000:])
|
||||
awareness, scale, scaling, pad, wrap = r.stdout.split()
|
||||
awareness, scale, scaling, pad, wrap, width = r.stdout.split()
|
||||
self.assertGreaterEqual(int(awareness), 1) # 1 system aware, 2 per-monitor aware
|
||||
self.assertAlmostEqual(float(scaling), float(scale) * 96 / 72, places=6)
|
||||
# Tk's win/tkWinX.c TkWinDisplayChanged rounds the nominal screen width to whole millimetres.
|
||||
# Its generic/tkCmds.c ScalingCmd converts that width back to pixels per point, so a 120 DPI display
|
||||
# can report 120.118 DPI. Keep the exact comparison, accounting for that round trip.
|
||||
width_mm = int(25.4 * int(width) / (float(scale) * 96) + 0.5)
|
||||
expected_scaling = 25.4 * int(width) / (72 * width_mm)
|
||||
self.assertAlmostEqual(float(scaling), expected_scaling, places=6)
|
||||
self.assertEqual((int(pad), int(wrap)), (round(14 * float(scale)), round(620 * float(scale))))
|
||||
|
||||
|
||||
|
||||
@@ -315,6 +315,75 @@ class LiftKeyTest(unittest.TestCase):
|
||||
self.assertEqual(self.key(), before)
|
||||
|
||||
|
||||
class LiftRunTest(unittest.TestCase):
|
||||
"""Lift.run with fake tools: the ExeFS patch stage writes the status and data files, the lift lists the bytes it
|
||||
read, and the data writes are checked against that list after it; a failed lift or check drops both files."""
|
||||
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
base = Path(tmp.name)
|
||||
self.dest, temp = base / 'install', base / 'temp'
|
||||
self.dest.mkdir()
|
||||
temp.mkdir()
|
||||
layout = SimpleNamespace(recomp=ROOT / 'recomp', tools=ROOT / 'tools',
|
||||
patches=lambda version: base / 'patches-v143.txt')
|
||||
self.ctx = SimpleNamespace(dest=self.dest, temp=temp, layout=layout, keys={'lift': 'k'},
|
||||
state={'version': '1.4.3'}, reporter=Reporter(out=io.StringIO()))
|
||||
self.addCleanup(self.ctx.reporter.close)
|
||||
mod = self.dest / 'mods' / 'SQ' / 'exefs'
|
||||
mod.mkdir(parents=True)
|
||||
(mod / '277178B7DBA1B6D4.ips').write_bytes(b'IPS32EEOF')
|
||||
self.calls, self.fail = [], set()
|
||||
saved = steps.run_tool
|
||||
self.addCleanup(lambda: setattr(steps, 'run_tool', saved))
|
||||
steps.run_tool = self.run_tool
|
||||
|
||||
def run_tool(self, ctx, step, script, args, parse=None, cwd=None):
|
||||
args = [str(a) for a in args]
|
||||
name = Path(script).name + (' --folded' if '--folded' in args else '')
|
||||
self.calls.append((name, args))
|
||||
log = ctx.temp / 'tool.log'
|
||||
log.write_text('')
|
||||
if name in self.fail:
|
||||
return 1, log
|
||||
if name == 'exefs_patches.py':
|
||||
for flag in ('--status', '--data', '--out'):
|
||||
p = Path(args[args.index(flag) + 1])
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(flag)
|
||||
elif name == 'gen.py':
|
||||
out = Path(args[args.index('--out') + 1]) / 'main'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
(out / 'folded.txt').write_text('# test\n')
|
||||
return 0, log
|
||||
|
||||
def outputs(self):
|
||||
return [p.is_file() for p in (steps.exefs_status(self.ctx), steps.exefs_data(self.ctx))]
|
||||
|
||||
def test_data_file_and_the_check_after_the_lift(self):
|
||||
steps.Lift().run(self.ctx)
|
||||
self.assertEqual([c[0] for c in self.calls], ['exefs_patches.py', 'gen.py', 'exefs_patches.py --folded'])
|
||||
build = self.calls[0][1]
|
||||
self.assertEqual(build[build.index('--data') + 1], str(steps.exefs_data(self.ctx)))
|
||||
check = self.calls[2][1]
|
||||
self.assertEqual(check[check.index('--folded') + 1], str(self.ctx.temp / 'gen' / 'main' / 'folded.txt'))
|
||||
self.assertEqual(check[check.index('--status') + 1], str(steps.exefs_status(self.ctx)))
|
||||
self.assertEqual(self.outputs(), [True, True])
|
||||
|
||||
def test_failed_lift_drops_the_data_file(self):
|
||||
self.fail = {'gen.py'}
|
||||
with self.assertRaises(steps.InstallError):
|
||||
steps.Lift().run(self.ctx)
|
||||
self.assertEqual(self.outputs(), [False, False])
|
||||
|
||||
def test_failed_check_drops_both_files(self):
|
||||
self.fail = {'exefs_patches.py --folded'}
|
||||
steps.Lift().run(self.ctx)
|
||||
self.assertEqual(self.outputs(), [False, False])
|
||||
self.assertIn('warn.exefs_skipped', self.ctx.reporter.out.getvalue())
|
||||
|
||||
|
||||
class IniTest(unittest.TestCase):
|
||||
EXAMPLE = '[display]\n# vk_present = mailbox\n\n[language]\n# locale = en-US\n# lang = en-US\n# x = 1\n'
|
||||
|
||||
|
||||
+7
-4
@@ -271,10 +271,13 @@ def write_if_changed(path, text):
|
||||
def copy_romfs_patches(version, out):
|
||||
"""totk.exe loads romfs-patches beside romfs-index.bin (runtime/romfs.c overlay_init). tools/make_romfs_patches.py
|
||||
writes them into build/<version> only, so a build in another directory ran without the 60 fps overlay (the quick
|
||||
menus' captured background turned black). Copies them when their manifest differs; romfs-patches-xbox,
|
||||
romfs-patches-xbox-label and romfs-patches-pc (the same with Xbox or keyboard and mouse button prompts,
|
||||
make_romfs_patches.py --prompts) the same way when they were made."""
|
||||
for name in ('romfs-patches', 'romfs-patches-xbox', 'romfs-patches-xbox-label', 'romfs-patches-pc'):
|
||||
menus' captured background turned black). Copies every romfs-patches* folder of build/<version> that has a
|
||||
manifest, the same way when it differs: romfs-patches, romfs-patches-xbox, romfs-patches-xbox-label and
|
||||
romfs-patches-pc (the same with Xbox or keyboard and mouse button prompts, make_romfs_patches.py --prompts) and
|
||||
the ones of another shape, romfs-patches-<w>x<h> and romfs-patches-pc-<w>x<h> (make_romfs_patches.py --aspect),
|
||||
which runtime/romfs.c overlay_init needs for a game resolution that is not 16:9."""
|
||||
names = sorted(p.name for p in (ROOT / 'build' / version).glob('romfs-patches*') if p.is_dir())
|
||||
for name in names:
|
||||
src = ROOT / 'build' / version / name
|
||||
dst = out / name
|
||||
if src.resolve() == dst.resolve() or not (src / 'manifest.tsv').is_file():
|
||||
|
||||
@@ -164,6 +164,8 @@ class Main:
|
||||
def __init__(self, path):
|
||||
n = Nso(str(path))
|
||||
self.image = bytes(n.image)
|
||||
n.image = self.image # one copy of the image; tools/exefs_patches.py reads nso's dynamic info
|
||||
self.nso = n
|
||||
self.text = n.text # (offset, size)
|
||||
self.code_end = n.rodata[0] # .text and its padding up to .rodata, where code caves go
|
||||
self.lifter = Lifter(BASE, 'c')
|
||||
|
||||
+256
-44
@@ -1,11 +1,16 @@
|
||||
"""ExeFS patches of the mods folder to code patch sets: IPS, IPS32 and IPSwitch (.pchtxt) files for the game's main.
|
||||
"""ExeFS patches of the mods folder to code patch sets and data writes: IPS, IPS32 and IPSwitch (.pchtxt) files for
|
||||
the game's main.
|
||||
|
||||
Lifted code cannot be byte-patched when the game runs, so a patch file's writes into main's code become `site` lines
|
||||
(recomp/patches.py), one patch set per file, which the runtime switches on while its mod is on (runtime/mods.c). The
|
||||
writes go through the same rules as the mods' code cheats (tools/cheats.py convert): an instruction replaced in place,
|
||||
a branch into a code cave the file writes into main's zero padding inlined at the branch.
|
||||
a branch into a code cave the file writes into main's zero padding inlined at the branch. Writes into main's data
|
||||
(.rodata, .data, .bss) need no lifting: the runtime stores them into the loaded module before the game runs, as
|
||||
emulators and Atmosphere apply a patch file to the module before its dynamic linker runs (data_runs).
|
||||
|
||||
python tools/exefs_patches.py --exefs <dir> --mods <mods folder> --patches <patch file> --out <patch file> --status <file>
|
||||
python tools/exefs_patches.py --exefs <dir> --mods <mods folder> --patches <patch file> --out <patch file>
|
||||
--status <file> --data <file>
|
||||
python tools/exefs_patches.py --exefs <dir> --status <file> --data <file> --folded <gen>/main/folded.txt
|
||||
|
||||
The files are <mod>/exefs/*.ips and *.pchtxt (or the Atmosphere places of the exefs folder, as runtime/mods.c finds
|
||||
them) and <mod>/atmosphere/exefs_patches/<name>/*.ips. A file is for main when its build id matches main's: an IPS
|
||||
@@ -13,19 +18,37 @@ file's name, a pchtxt file's @nsobid line (trailing zeros may be left out). Offs
|
||||
offsets count the NSO header (0x100 bytes) before main's image; a pchtxt offset adds its `@flag offset_shift` first.
|
||||
A pchtxt patch line applies between @enabled and @disabled; @stop ends the file.
|
||||
|
||||
--out gets the --patches file followed by one set per usable file (`mod_<hash>`, switch TOTK_MODPATCH_<HASH>, default
|
||||
off) and its sites. --status gets one line per file, mods by name, tab separated:
|
||||
--out gets the --patches file followed by one set per file with sites (`mod_<hash>`, switch TOTK_MODPATCH_<HASH>,
|
||||
default off) and its sites. --status gets one line per file, mods by name, tab separated:
|
||||
|
||||
<file hash> <content hash> <kind> <built-in sets> <note>
|
||||
|
||||
file hash: FNV-1a 32 of "<mod folder name>/<path in the mod>" (forward slashes); content hash: the same over the file's
|
||||
bytes, so the runtime sees a file that changed after the build. kind:
|
||||
code a set of sites
|
||||
code a set of sites, and data writes when the file has some
|
||||
data data writes only
|
||||
other for another game version or module: not used
|
||||
empty no write into main for this version (or only the game's own words)
|
||||
unsupported <note> says why: a write into main's data, a write the site rules cannot express
|
||||
empty no write into main for this version (or only the game's own bytes)
|
||||
unsupported <note> says why: a write the site rules cannot express, a write outside main's image, into the dynamic
|
||||
linker's tables or into a pointer it binds lazily, or (after the lift) into bytes the lifted code reads
|
||||
as constants
|
||||
built-in sets: the port's own sets (the --patches file's sets that are no cheats) with a site this file patches too,
|
||||
comma separated, or "-": the runtime switches them off while the mod is on, so the mod's code runs.
|
||||
comma separated, or "-": the runtime switches them off while the mod is on, so the mod's code runs. The note of a code
|
||||
or data file with data writes is `data <bytes>`, the bytes its data lines hold: the runtime keeps the mod's patches off
|
||||
when the data file does not hold them all.
|
||||
|
||||
--data gets one line per run of data bytes of the code and data files, tab separated (runtime/mods.c reads it):
|
||||
|
||||
<file hash> <content hash> <offset in main, hex> <bytes, hex> [base]
|
||||
|
||||
`base` marks a DT_RELR word: 8 bytes, little-endian, the word before relocation; the runtime adds main's base, as the
|
||||
dynamic linker does. Without --data a data write makes its file unsupported.
|
||||
|
||||
The lifted code reads some of main's .text and .rodata at lift time instead of at run time: the jump tables
|
||||
(recomp/jumptables.py), whose targets become labels of the generated C. A data write over such bytes would not reach
|
||||
the lifted code. recomp/gen.py lists the ranges it read in <gen>/<module>/folded.txt; the second form above, run after
|
||||
the lift, makes every file with a data write into those ranges unsupported (its status line says so and its data lines
|
||||
go). Without that list, every data write into .rodata counts as one into read bytes.
|
||||
|
||||
A file the rules cannot express is unsupported as a whole: half of a patch is worse than none.
|
||||
|
||||
@@ -39,9 +62,12 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from cheats import TITLE, Main, convert, fnv # noqa: E402
|
||||
from nso import (DT_JMPREL, DT_NULL, DT_PLTRELSZ, DT_RELA, DT_RELASZ, DT_RELR, DT_RELRSZ, DT_STRSZ, # noqa: E402
|
||||
DT_STRTAB, DT_SYMTAB, R_AARCH64_JUMP_SLOT)
|
||||
|
||||
NSO_HEADER = 0x100
|
||||
EXEFS_PLACES = ('exefs', f'atmosphere/contents/{TITLE}/exefs', f'contents/{TITLE}/exefs', f'{TITLE}/exefs')
|
||||
RUN_MAX = 256 # bytes per line of the data file (runtime/mods.c reads lines of at most 1024 characters)
|
||||
|
||||
|
||||
def patch_files(mods):
|
||||
@@ -159,6 +185,99 @@ def word_writes(main, writes):
|
||||
return words
|
||||
|
||||
|
||||
# ---- writes into data
|
||||
|
||||
def relocated(n):
|
||||
"""({offset: type} of the words main's explicit-addend relocations (DT_RELA, DT_JMPREL) fill, {offset} of its
|
||||
DT_RELR words): the 8-byte words the dynamic linker writes at start (runtime/loader.c relocate_table,
|
||||
relocate_relr)."""
|
||||
rela = {}
|
||||
for table, size in ((DT_RELA, DT_RELASZ), (DT_JMPREL, DT_PLTRELSZ)):
|
||||
if table in n.dyn:
|
||||
for i in range(n.dyn[size] // 24):
|
||||
off, info, _ = struct.unpack_from('<QQq', n.image, n.dyn[table] + 24 * i)
|
||||
rela[off] = info & 0xFFFFFFFF
|
||||
relr, where = set(), 0
|
||||
for i in range(n.dyn.get(DT_RELRSZ, 0) // 8):
|
||||
e = struct.unpack_from('<Q', n.image, n.dyn[DT_RELR] + 8 * i)[0]
|
||||
if not e & 1: # an address; the bitmap after it covers the 63 words that follow
|
||||
relr.add(e)
|
||||
where = e + 8
|
||||
else:
|
||||
relr.update(where + 8 * k for k in range(63) if e >> (k + 1) & 1)
|
||||
where += 63 * 8
|
||||
return rela, relr
|
||||
|
||||
|
||||
def linker_tables(n):
|
||||
"""[(start, end)] of main's .dynamic, relocation, symbol and string tables: the dynamic linker reads them at start,
|
||||
so a write into them would change what it does, which no store into the loaded module can follow."""
|
||||
end = n.dynamic
|
||||
while struct.unpack_from('<Q', n.image, end)[0] != DT_NULL:
|
||||
end += 16
|
||||
out = [(n.dynamic, end + 16)]
|
||||
for table, size in ((DT_RELA, DT_RELASZ), (DT_JMPREL, DT_PLTRELSZ), (DT_RELR, DT_RELRSZ), (DT_STRTAB, DT_STRSZ)):
|
||||
if table in n.dyn and size in n.dyn:
|
||||
out.append((n.dyn[table], n.dyn[table] + n.dyn[size]))
|
||||
if DT_SYMTAB in n.dyn:
|
||||
out.append((n.dyn[DT_SYMTAB], n.dyn[DT_SYMTAB] + 24 * n.symbol_count()))
|
||||
return out
|
||||
|
||||
|
||||
class MainData:
|
||||
"""What a write into main's data meets: main's image from .rodata to the end of .bss, the words the dynamic linker
|
||||
fills and the tables it reads."""
|
||||
|
||||
def __init__(self, main):
|
||||
n = main.nso
|
||||
self.image, self.lo, self.size, self.data = main.image, main.code_end, n.size, n.data[0]
|
||||
self.rela, self.relr = relocated(n)
|
||||
self.linker = linker_tables(n)
|
||||
|
||||
|
||||
def data_runs(md, writes):
|
||||
"""The data part of one file ({offset: byte} outside main's code), applied as emulators and Atmosphere apply a patch
|
||||
file: to the module before its dynamic linker runs. Returns ([(offset, bytes, rebase)], dropped):
|
||||
|
||||
- a byte no relocation covers is stored as it is (runs of consecutive bytes, at most RUN_MAX long);
|
||||
- a DT_RELR word holds its addend: the word with the file's bytes in it, rebase set (main's base gets added);
|
||||
- the linker overwrites a word an explicit-addend relocation fills whatever the file wrote there: such bytes
|
||||
change nothing and only count in dropped.
|
||||
|
||||
Bytes equal to main's are left out. ValueError for a write outside main's image (below .rodata: the NSO header),
|
||||
into the dynamic linker's tables, or into a function pointer it binds lazily (JUMP_SLOT): on the Switch it holds
|
||||
the file's value plus main's base until the first call binds it, which no store before the start can reproduce."""
|
||||
plain, words, dropped = {}, {}, 0
|
||||
for off in sorted(writes):
|
||||
if not md.lo <= off < md.size:
|
||||
raise ValueError(f'it writes outside main\'s image at main+{off:X}')
|
||||
if any(a <= off < b for a, b in md.linker):
|
||||
raise ValueError(f'it writes the dynamic linker\'s tables at main+{off:X}')
|
||||
w = next((w for w in range(off - 7, off + 1) if w in md.rela or w in md.relr), None)
|
||||
if w is None:
|
||||
plain[off] = writes[off]
|
||||
elif w in md.relr:
|
||||
words.setdefault(w, bytearray(md.image[w:w + 8]))[off - w] = writes[off]
|
||||
elif md.rela[w] == R_AARCH64_JUMP_SLOT:
|
||||
raise ValueError(f'it writes a function pointer the dynamic linker binds lazily at main+{w:X}')
|
||||
else:
|
||||
dropped += 1
|
||||
runs = []
|
||||
for off in sorted(o for o, b in plain.items() if b != md.image[o]):
|
||||
last = runs[-1] if runs else None
|
||||
if last and last[0] + len(last[1]) == off and len(last[1]) < RUN_MAX:
|
||||
runs[-1] = (last[0], last[1] + bytes([plain[off]]), False)
|
||||
else:
|
||||
runs.append((off, bytes([plain[off]]), False))
|
||||
runs += [(w, bytes(b), True) for w, b in words.items() if bytes(b) != md.image[w:w + 8]]
|
||||
return sorted(runs), dropped
|
||||
|
||||
|
||||
def data_line(key, content, run):
|
||||
off, data, rebase = run
|
||||
return f'{key}\t{content}\t{off:x}\t{data.hex()}' + ('\tbase' if rebase else '')
|
||||
|
||||
|
||||
def builtin_sites(text):
|
||||
"""{offset in main: [set name]} of the patch file's sets that are no cheats or mod patches."""
|
||||
out = {}
|
||||
@@ -169,41 +288,45 @@ def builtin_sites(text):
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--exefs', type=Path, required=True)
|
||||
ap.add_argument('--mods', type=Path, required=True, help='the mods folder next to totk.exe')
|
||||
ap.add_argument('--patches', type=Path, required=True, help='the patch file so far')
|
||||
ap.add_argument('--out', type=Path, required=True, help='the patch file with the mods\' ExeFS patches added')
|
||||
ap.add_argument('--status', type=Path, required=True)
|
||||
args = ap.parse_args()
|
||||
sys.stdout.reconfigure(errors='replace')
|
||||
def classify(m, md, bid, path, raw):
|
||||
"""(kind, note, sites, runs, dropped) of one patch file: path gives its name and suffix, raw its bytes, bid main's
|
||||
build id; md None: no data file, so a write into main's data makes the file unsupported."""
|
||||
try:
|
||||
if path.suffix.lower() == '.ips':
|
||||
for_main = same_build(path.stem, bid)
|
||||
writes = parse_ips(raw) if for_main else {}
|
||||
else:
|
||||
pid, writes = parse_pchtxt(raw.decode('utf-8-sig', errors='replace'))
|
||||
for_main = pid is not None and same_build(pid, bid)
|
||||
if not for_main:
|
||||
return 'other', '', {}, [], 0
|
||||
code = {o: b for o, b in writes.items() if m.in_code(o - o % 4)}
|
||||
data = {o: b for o, b in writes.items() if o not in code}
|
||||
if data and md is None:
|
||||
raise ValueError(f'it writes main\'s data at main+{min(data):X} (only code patches are supported)')
|
||||
sites = convert(m, word_writes(m, code)) if code else {}
|
||||
runs, dropped = data_runs(md, data) if data else ([], 0)
|
||||
return 'code' if sites else 'data' if runs else 'empty', '', sites, runs, dropped
|
||||
except ValueError as e:
|
||||
return 'unsupported', str(e), {}, [], 0
|
||||
|
||||
|
||||
DATA_HEADER = '# file hash, content hash, offset in main, bytes[, base] (tools/exefs_patches.py)'
|
||||
|
||||
|
||||
def build(args):
|
||||
m = Main(args.exefs / 'main')
|
||||
raw = (args.exefs / 'main').read_bytes()
|
||||
bid = raw[0x40:0x60].hex()
|
||||
md = MainData(m) if args.data else None
|
||||
bid = m.nso.build_id
|
||||
text = args.patches.read_text()
|
||||
builtin = builtin_sites(text)
|
||||
files = patch_files(args.mods)
|
||||
sets, sites, status, kinds = [], [], [], {}
|
||||
sets, sites, status, data_lines, kinds = [], [], [], [], {}
|
||||
for mod, rel, f in files:
|
||||
data = f.read_bytes()
|
||||
key, content = fnv(f'{mod}/{rel}'.encode()), fnv(data)
|
||||
kind, note, over, file_sites = 'code', '', [], {}
|
||||
try:
|
||||
if f.suffix.lower() == '.ips':
|
||||
for_main = same_build(f.stem, bid)
|
||||
writes = parse_ips(data) if for_main else {}
|
||||
else:
|
||||
pid, writes = parse_pchtxt(data.decode('utf-8-sig', errors='replace'))
|
||||
for_main = pid is not None and same_build(pid, bid)
|
||||
if not for_main:
|
||||
kind = 'other'
|
||||
else:
|
||||
file_sites = convert(m, word_writes(m, writes))
|
||||
if not file_sites:
|
||||
kind = 'empty'
|
||||
except ValueError as e:
|
||||
kind, note, file_sites = 'unsupported', str(e), {}
|
||||
raw = f.read_bytes()
|
||||
key, content = fnv(f'{mod}/{rel}'.encode()), fnv(raw)
|
||||
kind, note, file_sites, runs, dropped = classify(m, md, bid, f, raw)
|
||||
over = []
|
||||
if file_sites:
|
||||
name = f'mod_{key.lower()}'
|
||||
label = f'{mod}/{rel}'.encode('ascii', 'replace').decode()
|
||||
@@ -211,19 +334,108 @@ def main():
|
||||
for a, repl in sorted(file_sites.items()):
|
||||
sites.append(f'site {name} main {a:x} {m.word(a):08x} {" ".join(f"{w:08x}" for w in repl)}')
|
||||
over += [s for s in builtin.get(a, []) if s not in over]
|
||||
data_lines += [data_line(key, content, r) for r in runs]
|
||||
data_bytes = sum(len(r[1]) for r in runs)
|
||||
kinds[kind] = kinds.get(kind, 0) + 1
|
||||
status.append(f'{key}\t{content}\t{kind}\t{",".join(over) or "-"}\t{note.replace(chr(9), " ")}')
|
||||
print(f'exefs: {kind:11} {mod}/{rel}' + (f' ({note})' if note else '')
|
||||
+ (f' ({len(file_sites)} sites, over {", ".join(over)})' if over else
|
||||
f' ({len(file_sites)} sites)' if file_sites else ''))
|
||||
status_note = f'data {data_bytes}' if runs else note.replace(chr(9), ' ')
|
||||
status.append(f'{key}\t{content}\t{kind}\t{",".join(over) or "-"}\t{status_note}')
|
||||
parts = [note] if note else []
|
||||
if file_sites:
|
||||
parts.append(f'{len(file_sites)} sites' + (f', over {", ".join(over)}' if over else ''))
|
||||
if runs:
|
||||
parts.append(f'{data_bytes} bytes of main\'s data')
|
||||
if dropped:
|
||||
parts.append(f'{dropped} bytes into words the dynamic linker fills, which change nothing')
|
||||
print(f'exefs: {kind:11} {mod}/{rel}' + (f' ({"; ".join(parts)})' if parts else ''))
|
||||
if sets:
|
||||
text += ('\n# ExeFS patches of the mods folder (tools/exefs_patches.py)\n' + '\n'.join(sets) + '\n'
|
||||
+ '\n'.join(sites) + '\n')
|
||||
args.out.write_text(text)
|
||||
args.status.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.status.write_text('\n'.join(status) + ('\n' if status else ''), encoding='utf-8')
|
||||
if args.data:
|
||||
args.data.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.data.write_text('\n'.join([DATA_HEADER] + data_lines) + '\n', encoding='utf-8')
|
||||
print(f'exefs: {len(files)} patch files: ' + ', '.join(f'{n} {k}' for k, n in sorted(kinds.items()))
|
||||
+ f'; {len(sites)} sites')
|
||||
+ f'; {len(sites)} sites, {len(data_lines)} data runs')
|
||||
|
||||
|
||||
def read_ranges(path):
|
||||
"""[(start, end)] of a folded.txt (recomp/gen.py): hex module offsets, one range per line, '#' comments."""
|
||||
out = []
|
||||
for line in path.read_text().splitlines():
|
||||
f = line.split('#', 1)[0].split()
|
||||
if len(f) == 2:
|
||||
out.append((int(f[0], 16), int(f[1], 16)))
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def data_offset(main_path):
|
||||
"""The offset of main's .data segment in its image, from the NSO header (no decompression)."""
|
||||
with open(main_path, 'rb') as f:
|
||||
head = f.read(0x40)
|
||||
return struct.unpack_from('<III', head, 0x30)[1]
|
||||
|
||||
|
||||
def recheck(args):
|
||||
"""After the lift: every file with a data write into bytes the lifted code read at lift time becomes unsupported."""
|
||||
if not args.status.is_file() or not args.data.is_file():
|
||||
print('exefs: no status or data file: nothing to check')
|
||||
return
|
||||
folded = read_ranges(args.folded) if args.folded.is_file() else None
|
||||
data_off = data_offset(args.exefs / 'main')
|
||||
lines = args.data.read_text(encoding='utf-8').splitlines()
|
||||
bad = {}
|
||||
for line in lines:
|
||||
f = line.split('\t')
|
||||
if line.startswith('#') or len(f) < 4 or f[0] in bad:
|
||||
continue
|
||||
a = int(f[2], 16)
|
||||
b = a + len(f[3]) // 2
|
||||
if folded is None and a < data_off:
|
||||
bad[f[0]] = (f'it writes main+{a:X} in .rodata, and the lift left no list of the bytes it reads as '
|
||||
f'constants')
|
||||
elif folded is not None:
|
||||
hit = next((s for s, e in folded if s < b and a < e), None)
|
||||
if hit is not None:
|
||||
bad[f[0]] = (f'it writes main+{max(a, hit):X}, which the lifted code reads when the game is built '
|
||||
f'(a jump table): the change would not reach it')
|
||||
if not bad:
|
||||
print(f'exefs: data writes checked against {"the bytes the lift read" if folded is not None else ".rodata"}: '
|
||||
f'none meets them')
|
||||
return
|
||||
status = []
|
||||
for line in args.status.read_text(encoding='utf-8').splitlines():
|
||||
f = line.split('\t')
|
||||
if len(f) >= 5 and f[0] in bad:
|
||||
line = '\t'.join([f[0], f[1], 'unsupported', '-', bad[f[0]]])
|
||||
print(f'exefs: unsupported file {f[0]} ({bad[f[0]]})')
|
||||
status.append(line)
|
||||
args.status.write_text('\n'.join(status) + '\n', encoding='utf-8')
|
||||
kept = [line for line in lines if line.split('\t')[0] not in bad]
|
||||
args.data.write_text('\n'.join(kept) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--exefs', type=Path, required=True)
|
||||
ap.add_argument('--mods', type=Path, help='the mods folder next to totk.exe')
|
||||
ap.add_argument('--patches', type=Path, help='the patch file so far')
|
||||
ap.add_argument('--out', type=Path, help='the patch file with the mods\' ExeFS patches added')
|
||||
ap.add_argument('--status', type=Path, required=True)
|
||||
ap.add_argument('--data', type=Path, help='the data writes of the mods\' patch files (runtime/mods.c reads them)')
|
||||
ap.add_argument('--folded', type=Path, help='after the lift: <gen>/main/folded.txt; checks --status and --data '
|
||||
'against it instead of building')
|
||||
args = ap.parse_args()
|
||||
sys.stdout.reconfigure(errors='replace')
|
||||
if args.folded:
|
||||
if not args.data:
|
||||
ap.error('--folded needs --data')
|
||||
recheck(args)
|
||||
else:
|
||||
if not (args.mods and args.patches and args.out):
|
||||
ap.error('--mods, --patches and --out are needed to build')
|
||||
build(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+16
-7
@@ -93,13 +93,14 @@
|
||||
# fix_zero = 1
|
||||
# Optimisations, 0 turns each off: skip bindings that did not change, remember shader lookups, reuse the previous
|
||||
# draw's pipeline when its state repeats, remember texture descriptors between draws, update descriptors in bulk,
|
||||
# skip push constants that did not change.
|
||||
# skip push constants that did not change, bind only the vertex buffers a draw reads.
|
||||
# bind_skip = 1
|
||||
# shader_memo = 1
|
||||
# pipe_memo = 1
|
||||
# tex_memo = 1
|
||||
# desc_template = 1
|
||||
# push_skip = 1
|
||||
# vb_used = 1
|
||||
# Submit GPU work in parts every N draws between passes so the GPU starts earlier (0 to 1000; 0 = off), each part at
|
||||
# least split_us microseconds long (0 to 20000).
|
||||
# split_draws = 64
|
||||
@@ -145,12 +146,12 @@
|
||||
# the Steam Deck's 1280x800 (16:10), gives the camera's view that shape and lays the HUD out for it. The installer
|
||||
# prepares the UI layout for fifteen sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160 (21:9), 3840x1080, 5120x1440
|
||||
# (32:9), 1280x800, 1680x1050, 1920x1200, 2560x1600 (16:10), 1920x1280, 2160x1440, 2256x1504, 2880x1920 and 3000x2000
|
||||
# (3:2). Any other size
|
||||
# gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
|
||||
# tools/make_romfs_patches.py --aspect WxH. Read at start. Default: 1920x1080, or on a primary display of one of those
|
||||
# shapes the smallest of its sizes at least 1080 high that fits the display (1920x1280 on a 3:2 laptop, 2560x1080 on
|
||||
# a 21:9 monitor), so the picture fills the screen without black bars; with prompts = xbox or pc it stays 1920x1080,
|
||||
# the only shape the setup builds those prompts for.
|
||||
# (3:2). For any other size, or a source build that has not made the layout, the game draws the largest 16:9 size that
|
||||
# fits inside it (3440x1080 gives 1920x1080) and the present adds bars; build the layout with
|
||||
# tools/make_romfs_patches.py --aspect WxH to use the full shape. Read at start. Default: 1920x1080, or on a primary
|
||||
# display of one of those shapes the smallest of its sizes at least 1080 high that fits the display (1920x1280 on a 3:2
|
||||
# laptop, 2560x1080 on a 21:9 monitor), so the picture fills the screen without black bars; with prompts = xbox or pc it
|
||||
# stays 1920x1080, the only shape the setup builds those prompts for.
|
||||
# output_res = 1920x1080
|
||||
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
|
||||
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
|
||||
@@ -366,6 +367,14 @@
|
||||
# the setup's rebuild at start (a few minutes) and starts again after it. off: the game starts with the code it has.
|
||||
# A rebuild that failed or was cancelled is not started again for the same cheats. Default: auto.
|
||||
# mods_rebuild = auto
|
||||
# Extra room in MiB (0 to 4096) for the heap the world's textures stream into, for HD texture packs. Packs made for
|
||||
# emulators with 8 GB of DRAM grow it in their own HeapDef table (System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs):
|
||||
# the port reads that growth at start and applies it itself (about 3339 MiB for the HDPacks 4K pack), so such a pack
|
||||
# needs nothing here. Set it for a pack that ships textures without a HeapDef. Without the room the game keeps starting
|
||||
# its panic Blood Moon. The memory pool grows by the same amount; the larger of this and a mod's growth counts.
|
||||
# Read at start. Default: 0. Such a pack also needs much more video memory: astc_bc = 1 ([performance]) keeps its
|
||||
# textures at a quarter of it.
|
||||
# tex_heap_extra_mb = 0
|
||||
|
||||
[files]
|
||||
# Developer overlay: a folder with RomFS files and a manifest.tsv (tools/make_romfs_patches.py format). Players put
|
||||
|
||||
Reference in new issue
Block a user