Files
Valiantsin DzerakhandClaude Opus 5.5 282130a128 Build the game code with an execution profile when the release has one
build_game.py --pgo gen builds an instrumented totk.exe (llvm-mingw: MSYS2's clang has no profile runtime) that adds
its counts to <out>/pgo/*.profraw every minute and at exit; plat_exit ends the process with ExitProcess, which skips
compiler-rt's atexit write. tools/pgo.py merges them into build/pgo/pgo-<ver>.profdata with a record of the recomp/
tree it fits. build_game.py --pgo FILE compiles the generated code at -O2 and the runtime with that profile.

make_release.py ships build/pgo's profiles in data/ and builds the runtime libraries with them; the installer passes
the update's profile to build_game.py, and builds at -O1 without one. Profiles name lifted functions by address and
hold branch counts and control-flow hashes of the generated C, no bytes of the game.

1.4.3 on machine A, mean frame by world clock: the fight2 bench 11.49 -> 10.06 ms (-12.5%), a fight outside the
training 13.00 -> 12.24 ms (-5.8%); every window CPU-bound. The profile was trained on scripted runs and 24 minutes
of play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:04:31 +03:00

194 lines
10 KiB
Python

"""Where the installer finds what it runs: the one place that maps the release folder and a repository checkout.
Release (docs/installer.md "Release layout"), root = the unpacked folder:
python/python.exe runs every tool (numpy, lz4, cryptography, compression.zstd); its ._pth puts .., ../tools
and ../recomp on sys.path, so `python -m installer` imports from the release folder
recomp/, tools/ the lifter and the scripts; recomp/patches-<v>.txt, tools/data/expected-v<version>.json for
each supported update (SUPPORTED)
llvm/ llvm-mingw (bin/clang.exe)
runtime/ libtotkrt.a and libtotkrt-v2.a (x86-64-v3 / v2), the headers, install-side sources (romfs.c,
patches.c, ov_zstd.c) and totk.ico; runtime/deps/ holds
include/ + lib/ of Vulkan, SDL3, Opus, libvpx and VMA (build_game.py --deps)
bin/ totk_xlate.exe (+ totk_xlate.id), hactool.exe, ninja.exe, SDL3.dll, libopus-0.dll
data/shader_list.tsv the MacNativeGames shader list
data/pgo-<v>.profdata the execution profile the update's code is compiled with (tools/pgo.py; Windows only);
without one the game is built at -O1
totk.ini.example
VERSION the release version, one line
temp/ the default temp folder (created)
Linux release (the AppImage's folder, docs/installer.md "Linux release"): python/bin/python3 (python-build-standalone,
with tkinter), llvm/ with clang, lld and llvm-ar plus llvm/sysroot (the glibc headers and libraries the game links
against), runtime/deps/ with include/ and the static Opus, libvpx and zstd, bin/ without .exe and with libSDL3.so.0.
Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed/py/python.exe (the embeddable
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
runtime/ as the sources with build/install-rtlib/libtotkrt.a and libtotkrt-v2.a (tools/build_game.py --make-runtime-lib
--out build/install-rtlib, once per --cpu); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
refs/MacNativeGames; build/pgo/pgo-<v>.profdata when there is one; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
"""
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
WIN = os.name == 'nt'
EXE = '.exe' if WIN else ''
UCRT = Path(r'C:\msys64\ucrt64\bin')
SUPPORTED = ('1.2.1', '1.4.3', '1.1.0') # updates the installer builds; the check identifies the player's by hash
def version_tag(version):
"""'1.4.3' -> 'v143': the version name of build_game.py, make_romfs_patches.py and the patch tables."""
return 'v' + version.replace('.', '')
@dataclass(frozen=True)
class Layout:
root: Path
dev: bool
release: str
python: Path # runs the tools
tools: Path
recomp: Path
llvm: Path # llvm-mingw tree; on Linux the tree with bin/clang (a release's, or the system's /usr)
deps: Path | None # include/ and lib/ for the link; None: the system's (Linux dev mode)
runtime: Path # runtime headers and install-side sources
runtime_lib: Path # libtotkrt.a
ninja: Path
hactool: Path
xlate: Path # totk_xlate.exe; a totk_xlate.id beside it is copied too
dlls: tuple = field(default_factory=tuple)
shader_list: Path | None = None # None: dev, read from refs/MacNativeGames through git
ini_example: Path | None = None
temp: Path | None = None
sysroot: Path | None = None # Linux release: what the game links against, instead of the host's
runtime_lib_v2: Path | None = None # libtotkrt-v2.a: the runtime for CPUs without AVX2 (build_game.py --cpu v2)
pgo_dir: Path | None = None # pgo-<v>.profdata (tools/pgo.py); None: no profiles (Linux)
@property
def clang(self):
return self.llvm / 'bin' / ('clang' + EXE)
def runtime_lib_for(self, cpu):
"""The prebuilt runtime library for cpu (v2 or v3). None in dev mode when it was not built: the runtime is
then compiled from the sources in runtime/ instead."""
lib = self.runtime_lib_v2 if cpu == 'v2' else self.runtime_lib
if lib and lib.is_file():
return lib
return None if self.dev else lib
def pgo(self, version):
"""The execution profile build_game.py --pgo compiles the update's code with, None without one (the game is
then built at -O1)."""
p = self.pgo_dir / f'pgo-{version_tag(version)}.profdata' if self.pgo_dir else None
return p if p and p.is_file() else None
def patches(self, version):
"""The code patch table for recomp/gen.py."""
return self.recomp / f'patches-{version_tag(version)}.txt'
def expected(self, version):
"""The expected hashes of the update."""
return self.tools / 'data' / f'expected-v{version}.json'
def missing(self, cpu='v3'):
"""Paths the steps need that do not exist. cpu picks the runtime library the host will use."""
lib = self.runtime_lib_for(cpu)
need = [self.python, self.tools / 'extract.py', self.tools / 'ncz.py', self.tools / 'build_game.py',
self.tools / 'prepare_shaders.py',
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
self.tools / 'button_icons.py',
self.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py',
self.recomp / 'gen.py',
self.clang,
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, *([lib] if lib else ()),
self.ninja, self.hactool,
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
if self.shader_list:
need.append(self.shader_list)
if self.sysroot:
need.append(self.sysroot)
return [str(p) for p in need if not p.exists()]
def release_python(root):
return root / 'python' / ('python.exe' if WIN else 'bin/python3')
def is_release(root):
return release_python(root).is_file()
def release_layout(root):
version = root / 'VERSION'
return Layout(
root=root, dev=False,
release=version.read_text(encoding='utf-8').strip() if version.is_file() else 'unknown',
python=release_python(root),
tools=root / 'tools', recomp=root / 'recomp',
llvm=root / 'llvm', deps=root / 'runtime' / 'deps', runtime=root / 'runtime',
runtime_lib=root / 'runtime' / 'libtotkrt.a',
runtime_lib_v2=root / 'runtime' / 'libtotkrt-v2.a',
ninja=root / 'bin' / ('ninja' + EXE), hactool=root / 'bin' / ('hactool' + EXE),
xlate=root / 'bin' / ('totk_xlate' + EXE),
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example',
pgo_dir=root / 'data' if WIN else None,
# Linux: None, the install folder's .setup-temp (installer/cli.py): the AppImage is mounted read-only, and on
# a Steam Deck /home is often too full for the generated C while the install folder is on the SD card
temp=root / 'temp' if WIN else None,
sysroot=None if WIN else root / 'llvm' / 'sysroot')
def dev_layout(root):
if not WIN:
return dev_layout_posix(root)
commit = subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
text=True).stdout.strip()
embed = root / 'build' / 'py314-embed' / 'py' / ('python' + EXE)
trees = sorted(p.parent.parent for p in (root / 'toolchain').glob('llvm-mingw-*/llvm-mingw-*/bin/clang' + EXE))
deps = root / 'toolchain' / 'deps-mingw'
semver = (root / 'VERSION').read_text(encoding='utf-8').strip()
return Layout(
root=root, dev=True, release=f'{semver}-dev+{commit or "unknown"}',
python=embed if embed.is_file() else Path(sys.executable),
tools=root / 'tools', recomp=root / 'recomp',
llvm=trees[-1] if trees else root / 'toolchain' / 'llvm-mingw-missing', deps=deps,
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
ninja=UCRT / ('ninja' + EXE), hactool=root / 'toolchain' / 'hactool' / ('hactool' + EXE),
xlate=root / 'build' / 'xlate' / ('totk_xlate' + EXE),
dlls=tuple(sorted((deps / 'bin').glob('*.dll'))),
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp',
pgo_dir=root / 'build' / 'pgo')
def dev_commit(root):
return subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
text=True).stdout.strip()
def dev_layout_posix(root):
clang, ninja = shutil.which('clang'), shutil.which('ninja')
semver = (root / 'VERSION').read_text(encoding='utf-8').strip()
return Layout(
root=root, dev=True, release=f'{semver}-dev+{dev_commit(root) or "unknown"}',
python=Path(sys.executable), tools=root / 'tools', recomp=root / 'recomp',
llvm=Path(clang).parent.parent if clang else root / 'toolchain' / 'clang-missing', deps=None,
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
ninja=Path(ninja) if ninja else root / 'toolchain' / 'ninja-missing',
hactool=root / 'toolchain' / 'hactool' / 'hactool', xlate=root / 'build' / 'xlate' / 'totk_xlate',
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
def find(root=None):
"""The layout of root (default: the folder above this package)."""
root = Path(root or Path(__file__).resolve().parent.parent).resolve()
return release_layout(root) if is_release(root) else dev_layout(root)