From ef08fed45f85b9bc30008cf753b11d50946a92c9 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Mon, 9 Nov 2026 04:42:32 -0500 Subject: [PATCH] Ship NVIDIA's DLSS library in the setup and the build kit, and put it beside the game. The release carried AMD's FSR 3.1 library but not nvngx_dlss.dll, so DLSS ran only where the NVIDIA driver supplied the model itself; elsewhere players had to fetch the DLL by hand. tools/make_launcher.py and tools/make_kit.py now download NVIDIA's own build from the DLSS SDK repository at a pinned tag (tools/dlss_pin.py: 310.9.1, checked against its size and SHA-256, cached), and put it in the release's bin/ with a notice naming the NVIDIA RTX SDKs License that covers it. --dlss FILE takes a copy on disk instead, --no-dlss leaves it out. tools/make_game.py copies the DLL and its notice beside the game. A different nvngx_dlss.dll the player put there is kept; the one an earlier build placed (named by the notice's sha256 line) is replaced, so a newer release can bring a newer DLSS. The compile step's key covers make_game.py and the kit version, so existing installs get the DLL with their next update. Co-Authored-By: Claude Opus 5.5 --- THIRD_PARTY_NOTICES.md | 1 + docs/README-BuildKit.md | 5 ++-- docs/launcher.md | 6 ++++ tests/launcher_test.py | 35 ++++++++++++++++++++++ tools/dlss_pin.py | 66 +++++++++++++++++++++++++++++++++++++++++ tools/make_game.py | 36 ++++++++++++++++++++++ tools/make_kit.py | 21 +++++++++---- tools/make_launcher.py | 18 +++++++++-- 8 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 tools/dlss_pin.py diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 5e6d9ec2..9b7be8a9 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -122,6 +122,7 @@ folder's `licenses/` and in each component's own folder. | truststore 0.10.5, unmodified (Windows' own certificate check for `launcher/net.py`) | https://pypi.org/project/truststore/0.10.5/ (`truststore-0.10.5-py3-none-any.whl`, pinned with its sha256 in `tools/requirements-launcher.txt`), Copyright (c) 2022 Seth Michael Larson | MIT | `python/Lib/site-packages/truststore/`, its licence in `truststore-0.10.5.dist-info/licenses/` | | zlib 1.4 (MSYS2 `mingw-w64-ucrt-x86_64-zlib`), static library | https://zlib.net, Copyright (C) Jean-loup Gailly and Mark Adler | zlib | linked into `NX2navi-Setup.exe` (its unzip) | | mingw-w64 CRT startup code and winpthreads 14.0.0, GCC 16.2.0's `libgcc` (MSYS2 packages), static | https://www.mingw-w64.org, https://gcc.gnu.org | mingw-w64: ZPL-2.1 / MIT / public domain; libgcc: GPL-3.0 with the GCC Runtime Library Exception | linked into `NX2navi.exe` and `NX2navi-Setup.exe` | +| NVIDIA DLSS 310.9.1 (`nvngx_dlss.dll`, NVIDIA's own build, unmodified; pinned with its size and sha256 in `tools/dlss_pin.py`) | https://github.com/NVIDIA/DLSS @ `v310.9.1` (`lib/Windows_x86_64/rel/nvngx_dlss.dll`), Copyright NVIDIA Corporation | NVIDIA RTX SDKs License (https://github.com/NVIDIA/DLSS/blob/v310.9.1/LICENSE.txt); licensed for systems with NVIDIA GPUs only | `app//bin/` with `nvngx_dlss-NOTICE.txt`, also in the build kit's `bin/`; `tools/make_game.py` copies both beside the built game, where NGX loads the library (`runtime/vk_dlaa.c`) | Downloaded on the player's PC by the launcher, not distributed: the MSYS2 UCRT64 packages that `toolchain.lock.json` pins (clang and LLVM, Apache-2.0 with LLVM exception; GCC and binutils, GPL-3.0; the diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index b48aea06..16720c29 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -68,8 +68,9 @@ When it says `finished`, start `C:\Games\Ocarina\ocarina.exe`. You can delete `C - **Screen** in the menu has Upscaling (DLSS on NVIDIA RTX cards, FSR 3.1 on any card), Frame generation (FSR 3.1; it needs DLSS or FSR 3.1 upscaling on), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the resolution. Those marked "after restart" take effect when the game starts again. -- DLSS needs `nvngx_dlss.dll` from NVIDIA beside `ocarina.exe` (the kit does not include it); FSR 3.1's - `amd_fidelityfx_vk.dll` is copied there by the build. +- The build copies NVIDIA's DLSS library `nvngx_dlss.dll` (with `nvngx_dlss-NOTICE.txt`, which names its licence) + and FSR 3.1's `amd_fidelityfx_vk.dll` beside `ocarina.exe` from the kit's `bin\`. To use another DLSS version, put + your own `nvngx_dlss.dll` there: later builds keep it. - Logs are in `logs\`. When reporting a problem, say where you were in the game and which graphics card you have, and attach `logs\nx2navi.log`. Never send game files. diff --git a/docs/launcher.md b/docs/launcher.md index 5c099935..b105e908 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -195,6 +195,12 @@ game folder is the default `\game`. The dump is never touched. ``` `--ffx` puts AMD's signed `amd_fidelityfx_vk.dll` (FidelityFX SDK 1.1.4) in the release for FSR 3.1. + NVIDIA's `nvngx_dlss.dll` goes in by itself: the script downloads the version `tools/dlss_pin.py` pins from + NVIDIA's DLSS SDK repository (cached in `toolchain/launcher-dl/`), checks its size and sha256, and puts it in + `bin/` with `nvngx_dlss-NOTICE.txt`; `make_game.py` copies both beside the game. `--dlss FILE` takes a copy on disk + instead (it must be the pinned file), `--no-dlss` leaves DLSS out. NVIDIA's RTX SDKs License lets an application + ship the library; it also asks that a commercial release be announced to NVIDIA first + (https://developer.nvidia.com/sw-notification) and that the use of DLSS be credited. On Linux it cross-compiles the two programs with clang, lld and llvm-windres against the same MSYS2 packages and unpacks Tcl/Tk with msiextract (msitools). It writes into `build/launcher-release`: `NX2navi-Setup-.exe` diff --git a/tests/launcher_test.py b/tests/launcher_test.py index 8a05126e..fbebeb22 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -1061,6 +1061,41 @@ class MakeGameTest(Tmp): self.assertEqual((compiled, len(bad)), (1, 1)) self.assertNotIn('b.o', json.loads((obj / 'objects.json').read_text())) + def test_dlss_beside_the_game(self): + """The kit's nvngx_dlss.dll and its notice go beside the game; a newer kit replaces the copy an earlier build + placed, and a DLSS the player put there is kept.""" + import dlss_pin + from unittest import mock + kit, out = self.tmp / 'kit', self.tmp / 'game' + (kit / 'bin').mkdir(parents=True) + out.mkdir() + + def kit_dll(data, when): + (kit / 'bin' / 'nvngx_dlss.dll').write_bytes(data) + os.utime(kit / 'bin' / 'nvngx_dlss.dll', (when, when)) + (kit / 'bin' / 'nvngx_dlss-NOTICE.txt').write_text(f'NVIDIA DLSS\n\nsha256 {hashlib.sha256(data).hexdigest()}\n') + + dll, note = out / 'nvngx_dlss.dll', out / 'nvngx_dlss-NOTICE.txt' + with mock.patch.object(self.mg, 'KIT', kit): + self.assertIsNone(self.mg.place_dlss(out)) # a kit without DLSS: nothing + self.assertFalse(dll.exists()) + kit_dll(b'dlss one', 1_700_000_000) + self.assertIsNone(self.mg.place_dlss(out)) + self.assertEqual(dll.read_bytes(), b'dlss one') + self.assertTrue(note.is_file()) + self.assertIsNone(self.mg.place_dlss(out)) # unchanged + kit_dll(b'dlss two', 1_700_100_000) # a newer kit: the copy the last build placed goes + self.assertIsNone(self.mg.place_dlss(out)) + self.assertEqual(dll.read_bytes(), b'dlss two') + self.assertIn(hashlib.sha256(b'dlss two').hexdigest(), note.read_text()) + dll.write_bytes(b"the player's own DLSS") # another version the player put there: kept + self.assertIn('kept', self.mg.place_dlss(out)) + self.assertEqual(dll.read_bytes(), b"the player's own DLSS") + # the notice the release scripts write is the one make_game reads + (self.tmp / 'n.txt').write_text(dlss_pin.notice_text(), encoding='utf-8') + self.assertEqual(self.mg.notice_sha256(self.tmp / 'n.txt'), dlss_pin.SHA256) + self.assertEqual((dlss_pin.NAME, dlss_pin.NOTICE), (self.mg.DLSS_DLL, self.mg.DLSS_NOTICE)) + @unittest.skipIf(os.name == 'nt', 'a script stands in for windres.exe') def test_compile_resources(self): # The toolchain, the kit and the game under folders with spaces, as under C:\Users\Ali Sayed. windres must diff --git a/tools/dlss_pin.py b/tools/dlss_pin.py new file mode 100644 index 00000000..ba3e758d --- /dev/null +++ b/tools/dlss_pin.py @@ -0,0 +1,66 @@ +"""NVIDIA's DLSS library (nvngx_dlss.dll) for the releases: tools/make_launcher.py and tools/make_kit.py put it in the +release's bin/ beside AMD's FSR 3.1 library, and tools/make_game.py copies it beside the game, where NGX loads it +(runtime/vk_dlaa.c). Without it DLSS runs only where the NVIDIA driver supplies the model itself. + +The file is NVIDIA's own build from the DLSS SDK repository at a pinned tag, unmodified and checked against its size +and SHA-256. NVIDIA's RTX SDKs License lets an application ship it in object form; the notice written beside it names +the version and the licence that covers it. +""" +from pathlib import Path + +NAME = 'nvngx_dlss.dll' +NOTICE = 'nvngx_dlss-NOTICE.txt' # beside the DLL; make_game.py reads its sha256 line +VERSION = '310.9.1' +URL = f'https://raw.githubusercontent.com/NVIDIA/DLSS/v{VERSION}/lib/Windows_x86_64/rel/{NAME}' +SIZE = 58956912 +SHA256 = '3975567b8943c53acce397f2b72380092f84f162d00b0d2c7d08a1025c563983' +LICENSE_URL = f'https://github.com/NVIDIA/DLSS/blob/v{VERSION}/LICENSE.txt' + + +def notice_text(): + return (f'{NAME} is NVIDIA DLSS {VERSION}, unmodified, as published in NVIDIA\'s DLSS SDK\n' + f'(https://github.com/NVIDIA/DLSS, tag v{VERSION}). Copyright NVIDIA Corporation.\n' + '\n' + 'It is not part of NX2navi and NX2navi\'s licence does not cover it. It is distributed under the\n' + f'NVIDIA RTX SDKs License, and its use is subject to that licence: {LICENSE_URL}\n' + 'NVIDIA licenses DLSS only for use on systems with NVIDIA GPUs.\n' + '\n' + f'sha256 {SHA256}\n') + + +def sha256_file(path): + import hashlib + h = hashlib.sha256() + with open(path, 'rb') as f: + while b := f.read(1 << 20): + h.update(b) + return h.hexdigest() + + +def resolve(local, cache_dir): + """The pinned DLL: local when given (it must be the pinned file), else the copy in cache_dir, downloaded when + missing (launcher/net.py: resumed, checked against the size and SHA-256).""" + if local: + local = Path(local) + if not local.is_file(): + raise SystemExit(f'{local} does not exist') + if local.stat().st_size != SIZE or sha256_file(local) != SHA256: + raise SystemExit(f'{local} is not NVIDIA DLSS {VERSION} (size or SHA-256 differ); a newer DLSS needs ' + 'VERSION, SIZE and SHA256 in tools/dlss_pin.py changed first') + return local + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + from launcher import net + dest = Path(cache_dir) / f'nvngx_dlss-{VERSION}.dll' + if not (dest.is_file() and sha256_file(dest) == SHA256): + print(f'download {URL}', flush=True) + net.download(URL, dest, sha256=SHA256, size=SIZE) + return dest + + +def place(src, bin_dir): + """The DLL and its notice into a release's bin/.""" + import shutil + bin_dir = Path(bin_dir) + shutil.copy2(src, bin_dir / NAME) + (bin_dir / NOTICE).write_text(notice_text(), encoding='utf-8') diff --git a/tools/make_game.py b/tools/make_game.py index d11568d0..aacf5e99 100644 --- a/tools/make_game.py +++ b/tools/make_game.py @@ -497,6 +497,39 @@ def copy_if_changed(src, dst): return True +DLSS_DLL, DLSS_NOTICE = 'nvngx_dlss.dll', 'nvngx_dlss-NOTICE.txt' # tools/dlss_pin.py, in the kit's bin/ + + +def notice_sha256(path): + """The sha256 line of a DLSS notice: the DLL a kit placed beside the game.""" + try: + for line in path.read_text(encoding='utf-8').splitlines(): + if line.startswith('sha256 '): + return line.split()[1].lower() + except (OSError, UnicodeDecodeError): + pass + return None + + +def place_dlss(out): + """NVIDIA's DLSS library from the kit's bin/ beside the game, where NGX loads it (runtime/vk_dlaa.c), with its + notice. A different nvngx_dlss.dll the player put there (another DLSS version) is kept; the one an earlier build + placed, named by the notice beside it, is replaced. Returns a line for the build's log when it kept the player's.""" + src, dst, note = KIT / 'bin' / DLSS_DLL, out / DLSS_DLL, KIT / 'bin' / DLSS_NOTICE + if not src.is_file(): + return None + if dst.is_file(): + s, d = os.stat(src), os.stat(dst) + if d.st_size != s.st_size or int(d.st_mtime) != int(s.st_mtime): + have = sha256_file(dst) + if have != notice_sha256(out / DLSS_NOTICE) and have != sha256_file(src): + return f"{DLSS_DLL} kept: the one already beside the game is not the kit's (another DLSS version)" + copy_if_changed(src, dst) + if note.is_file() and (not (out / DLSS_NOTICE).is_file() or (out / DLSS_NOTICE).read_bytes() != note.read_bytes()): + shutil.copy2(note, out / DLSS_NOTICE) # by content: two versions' notices can share size and second + return None + + def copy_dlls(exe, msys_bin, out): seen, todo, copied = set(), [exe], [] while todo: @@ -977,6 +1010,9 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None): copy_if_changed(KIT / 'bin' / name, out / name) if (KIT / 'bin' / 'amd_fidelityfx_vk.dll').is_file(): # FSR 3.1 (runtime/vk_ffx.cpp) copy_if_changed(KIT / 'bin' / 'amd_fidelityfx_vk.dll', out / 'amd_fidelityfx_vk.dll') + kept = place_dlss(out) # NVIDIA DLSS (runtime/vk_dlaa.c) + if kept: + say(f' {kept}') say(f' {EXE_NAME} {exe.stat().st_size / 1e6:.0f} MB; beside it: {", ".join(copy_dlls(exe, msys / "bin", out))}') (out / 'profile').mkdir(exist_ok=True) diff --git a/tools/make_kit.py b/tools/make_kit.py index d0494d83..7822ebd3 100644 --- a/tools/make_kit.py +++ b/tools/make_kit.py @@ -1,10 +1,12 @@ """Assemble the build kit: what a player needs to build Ocarina of Time on their own PC with tools/make_game.py. - python tools/make_kit.py --xlate [--out build/kit] [--zip] + python tools/make_kit.py --xlate [--ffx amd_fidelityfx_vk.dll] + [--dlss nvngx_dlss.dll | --no-dlss] [--out build/kit] [--zip] -The kit holds this repository's runtime/, recomp/, the build tools, the shader translator built from tools/xlate and -README-BuildKit.md; no game code, data or anything generated from the game. check_content runs over the folder before -it is zipped. +The kit holds this repository's runtime/, recomp/, the build tools, the shader translator built from tools/xlate, +NVIDIA's DLSS library with its notice (tools/dlss_pin.py: downloaded from NVIDIA's DLSS SDK repository and checked, +or --dlss; not with --no-dlss), AMD's FSR 3.1 library with --ffx, and README-BuildKit.md; no game code, data or +anything generated from the game. check_content runs over the folder before it is zipped. """ import argparse import shutil @@ -34,9 +36,15 @@ def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument('--xlate', type=Path, required=True, help='folder with nx2navi_xlate.exe and nx2navi_xlate.id') ap.add_argument('--ffx', type=Path, help="AMD's signed amd_fidelityfx_vk.dll (FidelityFX SDK 1.1.4, MIT) for FSR 3.1") + ap.add_argument('--dlss', type=Path, help="NVIDIA's nvngx_dlss.dll from disk instead of downloading it from " + "NVIDIA's DLSS SDK repository") + ap.add_argument('--no-dlss', action='store_true', help='leave NVIDIA DLSS out of the kit') ap.add_argument('--out', type=Path, default=ROOT / 'build' / 'kit') ap.add_argument('--zip', action='store_true') a = ap.parse_args() + sys.path.insert(0, str(ROOT / 'tools')) + import dlss_pin + dlss = None if a.no_dlss else dlss_pin.resolve(a.dlss, ROOT / 'build' / 'dl') kit = a.out / NAME if kit.exists(): shutil.rmtree(kit) @@ -55,10 +63,11 @@ def main(): shutil.copy2(a.xlate / n, kit / 'bin' / n) if a.ffx: # FSR 3.1 loads it from beside the game (runtime/vk_ffx.cpp); its licence is runtime/third_party/ffx_api shutil.copy2(a.ffx, kit / 'bin' / 'amd_fidelityfx_vk.dll') - sys.path.insert(0, str(ROOT / 'tools')) + if dlss: # DLSS: NGX loads it from beside the game (runtime/vk_dlaa.c); make_game.py copies it there + dlss_pin.place(dlss, kit / 'bin') import check_content bad = [f'{p.relative_to(kit)}: {m}' for p in sorted(kit.rglob('*')) if p.is_file() - for m in check_content.problems_for(p, allow_large=p.suffix in ('.exe', '.dll'))] # the translator, AMD's DLL + for m in check_content.problems_for(p, allow_large=p.suffix in ('.exe', '.dll'))] # the translator, the DLLs if bad: sys.exit('check_content refuses these kit files, not zipped:\n ' + '\n '.join(bad[:20])) size = sum(p.stat().st_size for p in kit.rglob('*') if p.is_file()) diff --git a/tools/make_launcher.py b/tools/make_launcher.py index 198abb29..83974d58 100644 --- a/tools/make_launcher.py +++ b/tools/make_launcher.py @@ -1,6 +1,7 @@ """Build a release of the NX2navi launcher (docs/launcher.md, "Releases"). - python tools/make_launcher.py --xlate build/xlate [--ffx amd_fidelityfx_vk.dll] [--out build/launcher-release] + python tools/make_launcher.py --xlate build/xlate [--ffx amd_fidelityfx_vk.dll] [--dlss nvngx_dlss.dll | --no-dlss] + [--out build/launcher-release] [--feed owner/name] [--notes notes.md] [--publish] @@ -17,6 +18,9 @@ The release folder (--out) holds: the packages of tools/requirements-launcher.txt, runtime-id.txt app/current.txt, app// launcher/ (without native/), runtime/, recomp/, the tools make_game.py uses, bin/nx2navi_xlate.exe and .id (and amd_fidelityfx_vk.dll with --ffx), + bin/nvngx_dlss.dll and its notice (NVIDIA DLSS at the version tools/dlss_pin.py + pins, downloaded from NVIDIA's DLSS SDK repository and checked, or --dlss; not with + --no-dlss), lib/libnx2navirt.a (the runtime, prebuilt with the release's compiler), toolchain.lock.json (the MSYS2 packages of today, pinned), feed.json, VERSION, the licence files @@ -44,6 +48,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / 'tools')) import check_content # noqa: E402 +import dlss_pin # noqa: E402 from launcher import toolchain # noqa: E402 from launcher.paths import Layout # noqa: E402 @@ -158,7 +163,7 @@ def build_python(py): # ---------------------------------------------------------------- app// -def build_app(app, ver, xlate, feed_repo, lock, ffx=None): +def build_app(app, ver, xlate, feed_repo, lock, ffx=None, dlss=None): copy_tree(ROOT / 'launcher', app / 'launcher') for d in ('runtime', 'recomp'): copy_tree(ROOT / d, app / d) @@ -176,6 +181,8 @@ def build_app(app, ver, xlate, feed_repo, lock, ffx=None): shutil.copy2(xlate / n, app / 'bin' / n) if ffx: # FSR 3.1 loads it from beside the game (runtime/vk_ffx.cpp); make_game.py copies it there shutil.copy2(ffx, app / 'bin' / 'amd_fidelityfx_vk.dll') + if dlss: # DLSS: NGX loads it from beside the game (runtime/vk_dlaa.c); make_game.py copies it there + dlss_pin.place(dlss, app / 'bin') (app / 'toolchain.lock.json').write_text(json.dumps(lock, indent=1), encoding='utf-8') (app / 'feed.json').write_text(json.dumps({'type': 'github', 'repo': feed_repo}, indent=1), encoding='utf-8') @@ -285,6 +292,9 @@ def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument('--xlate', type=Path, required=True, help='folder with nx2navi_xlate.exe and nx2navi_xlate.id') ap.add_argument('--ffx', type=Path, help="AMD's signed amd_fidelityfx_vk.dll (FidelityFX SDK 1.1.4, MIT) for FSR 3.1") + ap.add_argument('--dlss', type=Path, help=f"NVIDIA's nvngx_dlss.dll (DLSS {dlss_pin.VERSION}) from disk instead of " + "downloading it from NVIDIA's DLSS SDK repository") + ap.add_argument('--no-dlss', action='store_true', help='leave NVIDIA DLSS out of the release') ap.add_argument('--out', type=Path, default=MAIN_ROOT / 'build' / 'launcher-release') ap.add_argument('--feed', default=DEFAULT_FEED, help='the GitHub repository the launcher looks for releases in') ap.add_argument('--notes', type=Path, help='release notes (markdown) for the manifest and the GitHub release') @@ -308,7 +318,9 @@ def main(): else toolchain.make_lock(fast_repo=toolchain.FAST_REPO)) say(f'release {build}: toolchain lock with {len(lock["packages"])} packages' + (f' (CLANG64 clang left out: {lock["fast_skipped"]})' if lock.get('fast_skipped') else '')) - build_app(folder / 'app' / ver, ver, a.xlate, a.feed, lock, a.ffx) + dlss = None if a.no_dlss else dlss_pin.resolve(a.dlss, DL) + say(f'NVIDIA DLSS {dlss_pin.VERSION}: {dlss}' if dlss else 'NVIDIA DLSS left out (--no-dlss)') + build_app(folder / 'app' / ver, ver, a.xlate, a.feed, lock, a.ffx, dlss) (folder / 'app' / 'current.txt').write_text(ver + '\n', encoding='utf-8') build_python(folder / 'python') cc, windres = native_compiler(lock)