mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-12 02:11:03 +00:00
SPIR-V leaves a register read before any write undefined and Mesa folds undefined values freely. The unconditional patch 0014 guarded against that and was taken out in #701 because the whole picture shook with DLSS on NVIDIA. Patch 0016 brings the zero start back behind a zero_locals job flag, which vk_caps_driver turns on for RADV, ANV and NVK. TOTK_ZERO_LOCALS overrides it, and gfx_shader_set_host mixes it into the translator identity so the variant has its own cache keys. FXAA_FS gains the flag-on hash of the game's FXAA shader, so vk_dlaa_skip_draw knows it on Mesa too. A game run with TOTK_ZERO_LOCALS=1 on an RX 9070 produced that hash; a TOTK_FXAA=0 run logged no skip with either hash, so the switch itself is not confirmed. All 29736 stored 1.4.3 inputs translate with the flag off and on, 0 spirv-val failures. FXAA hashes to 5c43ae18948d52cb with the flag off and aad42238801d31d8 with it on. Flag-on output matches the old translator byte for byte on a sample of 312 shaders. Not run on a Mesa driver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
8.2 KiB
Python
187 lines
8.2 KiB
Python
"""Build the shader translator: build/xlate/totk_xlate.exe (Ryujinx 1.3.2 + patches, NativeAOT, SPIR-V for Vulkan).
|
|
|
|
Copies tools/vendor/ryujinx to build/xlate/ryujinx, applies tools/xlate/patches/*.patch there (tools/vendor stays
|
|
byte-identical; 0001-0004 are MNG's, 0005-0013, 0015 and 0016 are ours), then runs `dotnet publish`. Needs the .NET 10 SDK, the MSVC build
|
|
tools and vswhere.exe (docs/building.md). Run with .venv. On Linux it builds build/xlate/totk_xlate for linux-x64, which
|
|
NativeAOT links with the system's clang (no MSVC); NativeAOT builds for the OS it runs on only.
|
|
|
|
Next to the exe it writes totk_xlate.id: the translator identity, a sha256 over everything the build reads (this script,
|
|
Program.cs, the csproj, the patches, the vendor tree). runtime/gfx_shader.c puts it into every shader cache key, so a
|
|
translator change re-keys the cache without a hand-made KEY_VERSION bump. --id-only writes it for the exe already built
|
|
(the caller vouches that the exe matches the sources).
|
|
|
|
python tools/build_xlate.py [--id-only] [--out DIR]
|
|
|
|
--out builds into DIR instead of build/xlate (a task build dir: totk.exe takes the translator next to it).
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
VENDOR = ROOT / "tools" / "vendor" / "ryujinx"
|
|
MNG_VENDOR = ROOT / "refs" / "MacNativeGames" / "vendor" / "ryujinx"
|
|
XLATE = ROOT / "tools" / "xlate"
|
|
BUILD = ROOT / "build" / "xlate"
|
|
WIN = os.name == "nt"
|
|
EXE = "totk_xlate.exe" if WIN else "totk_xlate"
|
|
VSWHERE_DIR = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) / "Microsoft Visual Studio" / "Installer"
|
|
|
|
|
|
def tree_hashes(base: Path) -> dict[str, str]:
|
|
"""Hash every source file; src/*/obj and src/*/bin are build output (an IDE restore writes them, the vendor
|
|
.gitignore ignores them and prepare_sources() drops them), so they are left out."""
|
|
def build_output(rel: Path) -> bool:
|
|
return len(rel.parts) > 2 and rel.parts[0] == "src" and rel.parts[2] in ("obj", "bin")
|
|
|
|
return {
|
|
p.relative_to(base).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in sorted(base.rglob("*"))
|
|
if p.is_file() and not build_output(p.relative_to(base))
|
|
}
|
|
|
|
|
|
def mng_pin() -> str:
|
|
"""The MacNativeGames commit tools/fetch_refs.sh pins."""
|
|
for line in (ROOT / "tools" / "fetch_refs.sh").read_text(encoding="utf-8").splitlines():
|
|
f = line.split()
|
|
if len(f) >= 4 and f[:2] == ["fetch", "MacNativeGames"]: # a fifth word asks for the history
|
|
return f[3]
|
|
sys.exit("tools/fetch_refs.sh pins no MacNativeGames commit")
|
|
|
|
|
|
def pinned_blobs() -> dict[str, str]:
|
|
"""Git blob ids of MNG's vendor/ryujinx at the pinned commit. The checkout itself may be newer than the pin or
|
|
carry CRLF line ends (core.autocrlf), so its files are not compared."""
|
|
out = subprocess.run(["git", "-C", str(MNG_VENDOR.parent.parent), "ls-tree", "-r", mng_pin(), "vendor/ryujinx"],
|
|
capture_output=True, text=True)
|
|
if out.returncode != 0:
|
|
sys.exit(f"git ls-tree of the MacNativeGames pin failed: {out.stderr.strip()}")
|
|
blobs = {}
|
|
for line in out.stdout.splitlines():
|
|
meta, path = line.split("\t", 1)
|
|
blobs[path.removeprefix("vendor/ryujinx/")] = meta.split()[2]
|
|
return blobs
|
|
|
|
|
|
def blob_ids(base: Path) -> dict[str, str]:
|
|
"""Git blob ids of the source files tree_hashes() hashes."""
|
|
ids = {}
|
|
for rel in tree_hashes(base):
|
|
data = (base / rel).read_bytes()
|
|
ids[rel] = hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest()
|
|
return ids
|
|
|
|
|
|
def verify_vendor() -> None:
|
|
"""tools/vendor/ryujinx must be byte-identical to MNG's copy at the pinned commit (when refs/ is present)."""
|
|
if not MNG_VENDOR.is_dir():
|
|
print("refs/MacNativeGames missing: vendor comparison skipped")
|
|
return
|
|
ours, theirs = blob_ids(VENDOR), pinned_blobs()
|
|
if ours != theirs:
|
|
diff = sorted(k for k in ours.keys() | theirs.keys() if ours.get(k) != theirs.get(k))
|
|
sys.exit(f"tools/vendor/ryujinx differs from MNG in {len(diff)} files, first: {diff[:5]}")
|
|
print(f"vendor identical to MNG: {len(ours)} files")
|
|
|
|
|
|
def prepare_sources() -> Path:
|
|
src = BUILD / "ryujinx"
|
|
if BUILD.exists():
|
|
shutil.rmtree(BUILD / "ryujinx", ignore_errors=True)
|
|
shutil.copytree(VENDOR, src)
|
|
for junk in list(src.glob("src/*/obj")) + list(src.glob("src/*/bin")):
|
|
shutil.rmtree(junk)
|
|
patches = sorted((XLATE / "patches").glob("*.patch"))
|
|
if len(patches) != 15:
|
|
sys.exit(f"expected 15 patches, found {len(patches)}")
|
|
for patch in patches:
|
|
# git apply works outside a repository when given a directory; --check first so a failure names the patch.
|
|
for extra in (["--check"], []):
|
|
r = subprocess.run(
|
|
["git", "apply", *extra, "--whitespace=nowarn", "--directory", str(src.relative_to(BUILD)), str(patch)],
|
|
cwd=BUILD, capture_output=True, text=True,
|
|
)
|
|
if r.returncode != 0:
|
|
sys.exit(f"patch does not apply: {patch.name}\n{r.stdout}{r.stderr}")
|
|
print(f"applied {patch.name}")
|
|
return src
|
|
|
|
|
|
def identity() -> str:
|
|
"""sha256 over the translator's inputs, in a fixed order: path and content hash of each file."""
|
|
h = hashlib.sha256()
|
|
files = [Path(__file__).resolve(), XLATE / "Program.cs", XLATE / "totk_xlate.csproj",
|
|
*sorted((XLATE / "patches").glob("*.patch"))]
|
|
for f in files:
|
|
h.update(f"{f.name}\0{hashlib.sha256(f.read_bytes()).hexdigest()}\n".encode())
|
|
for rel, digest in tree_hashes(VENDOR).items():
|
|
h.update(f"vendor/{rel}\0{digest}\n".encode())
|
|
return h.hexdigest()
|
|
|
|
|
|
def write_identity(exe: Path) -> None:
|
|
ident = identity()
|
|
exe.with_suffix(".id").write_text(ident + "\n", encoding="ascii")
|
|
print(f"translator identity {ident[:16]} -> {exe.with_suffix('.id')}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--id-only", action="store_true", help="write totk_xlate.id for the exe already built")
|
|
parser.add_argument("--out", type=Path, help="build directory instead of build/xlate")
|
|
parser.add_argument("--llvm", type=Path, help="Linux: an LLVM directory whose bin/clang and lld link the exe")
|
|
parser.add_argument("--sysroot", type=Path, help="Linux: link against this sysroot's glibc instead of the host's "
|
|
"(tools/make_release_linux.py)")
|
|
a = parser.parse_args()
|
|
if a.out:
|
|
global BUILD
|
|
BUILD = a.out.resolve()
|
|
if a.id_only:
|
|
write_identity(BUILD / EXE)
|
|
return
|
|
started = time.time()
|
|
dotnet = shutil.which("dotnet")
|
|
if not dotnet:
|
|
sys.exit("missing: dotnet (.NET 10 SDK)")
|
|
env = dict(os.environ)
|
|
if WIN:
|
|
if not (VSWHERE_DIR / "vswhere.exe").is_file() and not shutil.which("vswhere"):
|
|
sys.exit(f"missing: vswhere.exe (expected in {VSWHERE_DIR}); NativeAOT cannot link without it")
|
|
env["PATH"] = str(VSWHERE_DIR) + os.pathsep + env["PATH"]
|
|
|
|
verify_vendor()
|
|
src = prepare_sources()
|
|
work = BUILD / "xlate"
|
|
shutil.rmtree(work, ignore_errors=True)
|
|
work.mkdir(parents=True)
|
|
for name in ("Program.cs", "totk_xlate.csproj"):
|
|
shutil.copy2(XLATE / name, work / name)
|
|
out = BUILD / "publish"
|
|
shutil.rmtree(out, ignore_errors=True)
|
|
cmd = [
|
|
dotnet, "publish", str(work / "totk_xlate.csproj"), "-c", "Release", "-r", "win-x64" if WIN else "linux-x64",
|
|
"-p:PublishAot=true", "-p:InvariantGlobalization=true", f"-p:RyuSrc={src}", "-o", str(out),
|
|
]
|
|
if a.llvm:
|
|
env["PATH"] = str(a.llvm.resolve() / "bin") + os.pathsep + env["PATH"]
|
|
cmd += ["-p:CppCompilerAndLinker=clang", "-p:LinkerFlavor=lld"]
|
|
if a.sysroot:
|
|
cmd.append(f"-p:SysRoot={a.sysroot.resolve()}")
|
|
if subprocess.run(cmd, env=env).returncode != 0:
|
|
sys.exit("dotnet publish failed")
|
|
exe = BUILD / EXE
|
|
shutil.copy2(out / EXE, exe)
|
|
write_identity(exe)
|
|
print(f"built {exe} ({exe.stat().st_size / 1e6:.1f} MB) in {time.time() - started:.0f} s")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|