From 7341eec7279cb38d22c49b5b5c08fc8ff5ffe60e Mon Sep 17 00:00:00 2001 From: itaskaev2 <165683571+itaskaev2@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:32:32 +0100 Subject: [PATCH] Download the pinned Streamline runtime for DLSS Frame Generation, and let the build kit carry it too. make_launcher.py --dlssg needed --streamline, a Streamline SDK folder unpacked by hand, and make_kit.py could not carry DLSS Frame Generation at all, so a game built from the kit always left it off. tools/streamline_pin.py now pins the Streamline 2.14.16 SDK release (its archive's size and SHA-256, and the SHA-256 of each file the game loads): without --streamline the scripts download the archive once, check it, keep only sl.interposer.dll, sl.common.dll, sl.dlss_g.dll, sl.pcl.dll, sl.reflex.dll, nvngx_dlssg.dll and the two licence texts, each checked, and delete the archive. A folder given with --streamline must hold the pinned files. The bin/dlssg/ assembly moves from make_launcher.py into streamline_pin.place, so the release and the kit write the same set and the same dlssg-NOTICE.txt: a kit made with --dlssg and the 0.6.0 release's nx2navi_dlssg.dll has a bin/dlssg/ identical to the 0.6.0 release's, byte for byte. make_kit.py takes --dlssg [--streamline] like the launcher. nx2navi_dlssg.dll itself still comes from tools/build_dlssg.py. tests/launcher_test.py test_streamline_pin covers the extraction, the cache, refused archives and folders, and the notice. Co-Authored-By: Claude Opus 5.5 --- THIRD_PARTY_NOTICES.md | 2 +- docs/README-BuildKit.md | 3 + docs/launcher.md | 8 +++ tests/launcher_test.py | 56 ++++++++++++++++++- tools/build_dlssg.py | 5 +- tools/make_kit.py | 18 +++++- tools/make_launcher.py | 66 +++++----------------- tools/streamline_pin.py | 118 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 61 deletions(-) create mode 100644 tools/streamline_pin.py diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 9867f5d3..46e8a33a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -123,7 +123,7 @@ folder's `licenses/` and in each component's own folder. | 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`) | -| NVIDIA Streamline 2.14.16 runtime (`sl.interposer.dll`, `sl.common.dll`, `sl.dlss_g.dll`, `sl.pcl.dll`, `sl.reflex.dll`, NVIDIA's own builds, unmodified) and NVIDIA DLSS Frame Generation 310.9.16 (`nvngx_dlssg.dll`, as the Streamline SDK ships it) | https://github.com/NVIDIA-RTX/Streamline, SDK release 2.14.16 (`bin/x64/`), Copyright NVIDIA Corporation | Streamline: MIT; `nvngx_dlssg.dll`: NVIDIA RTX SDKs License, licensed for systems with NVIDIA GPUs only | `app//bin/dlssg/` with both licence texts and `dlssg-NOTICE.txt` (each file's sha256), when the release is made with `tools/make_launcher.py --dlssg --streamline`; `tools/make_game.py` copies them beside the built game, where `nx2navi_dlssg.dll` (this repository's `runtime/dlssg/`, built with the Streamline SDK's MIT headers) loads them (`runtime/vk_dlssg.c`) | +| NVIDIA Streamline 2.14.16 runtime (`sl.interposer.dll`, `sl.common.dll`, `sl.dlss_g.dll`, `sl.pcl.dll`, `sl.reflex.dll`, NVIDIA's own builds, unmodified) and NVIDIA DLSS Frame Generation 310.9.16 (`nvngx_dlssg.dll`, as the Streamline SDK ships it) | https://github.com/NVIDIA-RTX/Streamline, SDK release 2.14.16 (`bin/x64/`), Copyright NVIDIA Corporation | Streamline: MIT; `nvngx_dlssg.dll`: NVIDIA RTX SDKs License, licensed for systems with NVIDIA GPUs only | `app//bin/dlssg/` with both licence texts and `dlssg-NOTICE.txt` (each file's sha256), when the release is made with `tools/make_launcher.py --dlssg`, and the build kit's `bin/dlssg/` with `tools/make_kit.py --dlssg` (the SDK release pinned with its size and sha256 and each file's sha256 in `tools/streamline_pin.py`); `tools/make_game.py` copies them beside the built game, where `nx2navi_dlssg.dll` (this repository's `runtime/dlssg/`, built with the Streamline SDK's MIT headers) loads them (`runtime/vk_dlssg.c`) | | Button prompt art: the 128x128 PNGs of the Input Prompts Pack V7 (`XGamepad`, `P5Gamepad` and `Keyboard_Mouse` styles), unmodified | the pack as licensed to the repository owner; never in this repository | the pack's licence, which allows redistributing these pictures in NX2navi's releases (confirmed by the repository owner, 2026-11-09) | `app//prompt-art/` and the build kit's `prompt-art/` when made with `--prompt-art` (`tools/prompt_art.py`); `tools/pad_prompts.py` draws the game's Xbox, PlayStation and keyboard and mouse prompts from them on the player's PC | Downloaded on the player's PC by the launcher, not distributed: the MSYS2 UCRT64 packages that diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index 06727f90..bce5dbe2 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -119,6 +119,9 @@ beside the game. - 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. +- A kit with `bin\dlssg\` (made with `tools/make_kit.py --dlssg`) also brings DLSS Frame Generation on GeForce RTX 40 + and 50 cards: the build copies `nx2navi_dlssg.dll`, NVIDIA Streamline's runtime, `nvngx_dlssg.dll` and their licence + texts beside `ocarina.exe`, and Frame generation in the Screen menu then offers DLSS too. - 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 9c51544e..71cea9bd 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -268,6 +268,14 @@ game folder is the default `\game`. The dump is never touched. 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. + `--dlssg nx2navi_dlssg.dll` (built by `tools/build_dlssg.py`, which needs Visual Studio's C++ compiler) adds DLSS + Frame Generation in `bin/dlssg/`: beside it NVIDIA Streamline's runtime (`sl.interposer.dll`, `sl.common.dll`, + `sl.dlss_g.dll`, `sl.pcl.dll`, `sl.reflex.dll`) and `nvngx_dlssg.dll`, taken from the Streamline SDK release that + `tools/streamline_pin.py` pins. The script downloads that release once (cached in `toolchain/launcher-dl/`), checks + the archive's size and sha256 and each file's sha256, and writes `dlssg-NOTICE.txt` with the two licence texts; + `make_game.py` copies the whole set beside the game. `--streamline SDK` takes an SDK folder on disk instead (its + files must be the pinned ones). + `--prompt-art FOLDER ...` puts the button art in the release (`app//prompt-art`): folders that hold `XGamepad`, `P5Gamepad` and `Keyboard_Mouse` (the Source folders of button prompt art packs), or those three folders themselves; a set named itself wins over one found inside a folder named, and the same set twice is diff --git a/tests/launcher_test.py b/tests/launcher_test.py index 8ad14529..6ed6230c 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -1657,13 +1657,13 @@ class MakeGameTest(Tmp): def test_dlssg_beside_the_game(self): """DLSS Frame Generation's files go beside the game only as a whole set, with the licence texts; the release script (tools/make_launcher.py) and the build agree on the set.""" - import make_launcher + import streamline_pin from unittest import mock kit, out = self.tmp / 'kit', self.tmp / 'game' src = kit / 'bin' / 'dlssg' src.mkdir(parents=True) out.mkdir() - self.assertEqual(make_launcher.DLSSG_FILES, self.mg.DLSSG_FILES) + self.assertEqual(streamline_pin.FILES, self.mg.DLSSG_FILES) with mock.patch.object(self.mg, 'KIT', kit): for n in self.mg.DLSSG_FILES[:-1]: (src / n).write_bytes(n.encode()) @@ -1676,6 +1676,58 @@ class MakeGameTest(Tmp): self.assertEqual((out / n).read_bytes(), (src / n).read_bytes()) self.assertEqual((out / 'streamline-LICENSE.txt').read_text(), 'MIT') + def test_streamline_pin(self): + """The Streamline files come out of the SDK archive checked one by one; the archive goes once they are out and + a filled cache needs no download; an SDK folder on disk must hold the pinned files; bin/dlssg/ gets the set + make_game.py places, the two licence texts and a notice with every file's sha256.""" + import streamline_pin + from unittest import mock + files = {n: f'{n} of the SDK'.encode() for n in streamline_pin.SDK_FILES} + pins = {n: hashlib.sha256(d).hexdigest() for n, d in files.items()} + cache = self.tmp / 'dl' + cache.mkdir() + archive = cache / f'streamline-sdk-v{streamline_pin.VERSION}.zip' + with zipfile.ZipFile(archive, 'w') as z: + for n, d in files.items(): + z.writestr(n, d) + z.writestr('bin/x64/sl.nis.dll', b'a plugin the game does not load') + fetched = [] + + def download(url, dest, sha256=None, size=None): # the archive is already where net.download writes it + fetched.append(url) + self.assertEqual(Path(dest), archive) + return dest + + with mock.patch.dict(streamline_pin.SDK_FILES, pins), mock.patch('launcher.net.download', download): + sdk = streamline_pin.resolve(None, cache) + self.assertEqual(fetched, [streamline_pin.URL]) + self.assertFalse(archive.exists()) + self.assertEqual(streamline_pin.wrong_files(sdk), []) + self.assertFalse((sdk / 'bin' / 'x64' / 'sl.nis.dll').exists()) + self.assertEqual(streamline_pin.resolve(None, cache), sdk) # filled: no second download + self.assertEqual(len(fetched), 1) + self.assertEqual(streamline_pin.resolve(sdk, cache), sdk) # a folder on disk with the pinned files + (sdk / 'bin' / 'x64' / 'sl.common.dll').write_bytes(b'another Streamline') + with self.assertRaises(SystemExit): + streamline_pin.resolve(sdk, cache) + (sdk / 'bin' / 'x64' / 'sl.common.dll').write_bytes(files['bin/x64/sl.common.dll']) + with zipfile.ZipFile(archive, 'w') as z: # an archive whose file differs is refused + for n, d in files.items(): + z.writestr(n, b'changed' if n == 'license.txt' else d) + with self.assertRaises(SystemExit): + streamline_pin.extract(archive, self.tmp / 'x') + dll = self.tmp / 'nx2navi_dlssg.dll' + dll.write_bytes(b'our frame generation shim') + dest = self.tmp / 'bin' / 'dlssg' + streamline_pin.place(dll, sdk, dest) + self.assertEqual(sorted(p.name for p in dest.iterdir()), + sorted((*streamline_pin.FILES, *streamline_pin.TEXTS, streamline_pin.NOTICE))) + self.assertEqual((dest / 'streamline-LICENSE.txt').read_bytes(), files['license.txt']) + note = (dest / streamline_pin.NOTICE).read_text(encoding='utf-8') + self.assertIn(f'Streamline {streamline_pin.VERSION} SDK', note) + self.assertIn(f'nx2navi_dlssg.dll sha256 {hashlib.sha256(dll.read_bytes()).hexdigest()}', note) + self.assertIn(f'nvngx_dlssg.dll sha256 {pins["bin/x64/nvngx_dlssg.dll"]}', note) + @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/build_dlssg.py b/tools/build_dlssg.py index 7bef0796..4b671454 100644 --- a/tools/build_dlssg.py +++ b/tools/build_dlssg.py @@ -4,8 +4,9 @@ Streamline's C++ types have MSVC's ABI, so this one DLL is built with Visual Studio's compiler (the Build Tools are enough; vswhere finds them), against the SDK's headers, which stay where they are. The Vulkan headers are MSYS2's. -A launcher release ships the DLL ready-made beside Streamline's own runtime DLLs (tools/make_launcher.py --dlssg -and --streamline), and tools/make_game.py place_dlssg puts them beside the game. +A launcher release and the build kit ship the DLL ready-made beside Streamline's own runtime DLLs +(tools/make_launcher.py and tools/make_kit.py --dlssg, with the Streamline files tools/streamline_pin.py pins), and +tools/make_game.py place_dlssg puts them beside the game. """ import argparse import os diff --git a/tools/make_kit.py b/tools/make_kit.py index d5c574f4..78d043ee 100644 --- a/tools/make_kit.py +++ b/tools/make_kit.py @@ -1,12 +1,14 @@ """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 [--ffx amd_fidelityfx_vk.dll] - [--dlss nvngx_dlss.dll | --no-dlss] [--prompt-art FOLDER [FOLDER ...]] - [--out build/kit] [--zip] + [--dlss nvngx_dlss.dll | --no-dlss] [--dlssg nx2navi_dlssg.dll [--streamline SDK]] + [--prompt-art FOLDER [FOLDER ...]] [--out build/kit] [--zip] 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, the button art with --prompt-art, and +or --dlss; not with --no-dlss), DLSS Frame Generation in bin/dlssg/ with --dlssg (nx2navi_dlssg.dll from +tools/build_dlssg.py beside NVIDIA Streamline's runtime, tools/streamline_pin.py: downloaded from NVIDIA's Streamline +SDK release and checked, or --streamline), AMD's FSR 3.1 library with --ffx, the button art with --prompt-art, and README-BuildKit.md; no game code, data or anything generated from the game. check_content runs over the folder before it is zipped. @@ -48,6 +50,10 @@ def main(): 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('--dlssg', type=Path, metavar='DLL', + help='nx2navi_dlssg.dll (tools/build_dlssg.py) for DLSS Frame Generation') + ap.add_argument('--streamline', type=Path, metavar='SDK', + help='the Streamline SDK folder instead of downloading the SDK release with --dlssg') ap.add_argument('--prompt-art', type=Path, nargs='+', metavar='FOLDER', help='the button art: folders holding XGamepad, P5Gamepad and Keyboard_Mouse, or those folders') ap.add_argument('--out', type=Path, default=ROOT / 'build' / 'kit') @@ -56,9 +62,13 @@ def main(): sys.path.insert(0, str(ROOT / 'tools')) import dlss_pin import prompt_art + import streamline_pin + if a.streamline and not a.dlssg: + sys.exit('--streamline goes with --dlssg') if a.prompt_art: prompt_art.art_files(a.prompt_art) # the art is all there before anything is downloaded dlss = None if a.no_dlss else dlss_pin.resolve(a.dlss, ROOT / 'build' / 'dl') + sdk = streamline_pin.resolve(a.streamline, ROOT / 'build' / 'dl') if a.dlssg else None kit = a.out / NAME if kit.exists(): shutil.rmtree(kit) @@ -79,6 +89,8 @@ def main(): shutil.copy2(a.ffx, kit / '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, kit / 'bin') + if sdk: # DLSS Frame Generation (runtime/vk_dlssg.c): make_game.py place_dlssg copies it beside the game + streamline_pin.place(a.dlssg, sdk, kit / 'bin' / 'dlssg') if a.prompt_art: # make_game.py draws the button prompts from it (its prompts step) n, size = prompt_art.copy_art(a.prompt_art, kit / prompt_art.FOLDER) print(f'button art: {n} pictures, {size / 1e6:.1f} MB') diff --git a/tools/make_launcher.py b/tools/make_launcher.py index 06ad2492..ee39bbf9 100644 --- a/tools/make_launcher.py +++ b/tools/make_launcher.py @@ -21,8 +21,10 @@ The release folder (--out) holds: 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), - bin/dlssg/ with --dlssg and --streamline (DLSS Frame Generation: nx2navi_dlssg.dll, - Streamline's runtime DLLs and nvngx_dlssg.dll, their licences and a notice), + bin/dlssg/ with --dlssg (DLSS Frame Generation: nx2navi_dlssg.dll, Streamline's + runtime DLLs and nvngx_dlssg.dll at the version tools/streamline_pin.py pins, + downloaded from NVIDIA's Streamline SDK release and checked, or --streamline; their + licences and a notice), lib/libnx2navirt.a (the runtime, prebuilt with the release's compiler), prompt-art/ with --prompt-art (the button art make_game.py draws the Xbox, PlayStation and keyboard and mouse prompts from, tools/prompt_art.py art_files), @@ -58,6 +60,7 @@ sys.path.insert(0, str(ROOT / 'tools')) import check_content # noqa: E402 import dlss_pin # noqa: E402 import prompt_art # noqa: E402 +import streamline_pin # noqa: E402 from launcher import toolchain # noqa: E402 from launcher.paths import Layout # noqa: E402 @@ -172,50 +175,6 @@ def build_python(py): # ---------------------------------------------------------------- app// -# DLSS Frame Generation: the files tools/make_game.py DLSSG_FILES names, put beside the game from bin/dlssg/. -DLSSG_FILES = ('nx2navi_dlssg.dll', 'sl.interposer.dll', 'sl.common.dll', 'sl.dlss_g.dll', 'sl.pcl.dll', 'sl.reflex.dll', - 'nvngx_dlssg.dll') -DLSSG_NOTICE = 'dlssg-NOTICE.txt' - - -def place_dlssg(dll, sdk, dest): - """DLSS Frame Generation into dest (the release's bin/dlssg/): nx2navi_dlssg.dll (tools/build_dlssg.py), NVIDIA - Streamline's interposer and the plugins it loads, and NGX's nvngx_dlssg.dll, unmodified from the Streamline SDK's - bin/x64, with Streamline's licence (MIT), the NVIDIA RTX SDKs License that covers nvngx_dlssg.dll, and a notice - naming each file's sha256.""" - sdk_bin = sdk / 'bin' / 'x64' - srcs = {n: (dll if n == 'nx2navi_dlssg.dll' else sdk_bin / n) for n in DLSSG_FILES} - texts = {'streamline-LICENSE.txt': sdk / 'license.txt', - 'nvidia-rtx-sdks-LICENSE.txt': sdk_bin / 'nvngx_dlss.license.txt'} - missing = [str(p) for p in (*srcs.values(), *texts.values()) if not p.is_file()] - if missing: - sys.exit(f'DLSS Frame Generation: {", ".join(missing)} missing') - dest.mkdir(parents=True, exist_ok=True) - lines = ['DLSS Frame Generation for NX2navi. nx2navi_dlssg.dll is NX2navi\'s own code (runtime/dlssg/), built', - 'with the Streamline SDK\'s headers. The other files are NVIDIA\'s own builds, unmodified, from the', - f'{streamline_version(sdk)} SDK (https://github.com/NVIDIA-RTX/Streamline): the sl.*.dll files under its', - 'MIT licence (streamline-LICENSE.txt), nvngx_dlssg.dll under the NVIDIA RTX SDKs License', - '(nvidia-rtx-sdks-LICENSE.txt), which licenses it only for use on systems with NVIDIA GPUs. They are not', - 'part of NX2navi and NX2navi\'s licence does not cover them.', ''] - for n, p in srcs.items(): - shutil.copy2(p, dest / n) - lines.append(f'{n} sha256 {sha256_file(p)}') - for n, p in texts.items(): - shutil.copy2(p, dest / n) - (dest / DLSSG_NOTICE).write_text('\n'.join(lines) + '\n', encoding='utf-8') - - -def streamline_version(sdk): - """'Streamline 2.14.16' from the SDK's README title line, or 'Streamline' when it names none.""" - try: - for line in (sdk / 'README.md').read_text(encoding='utf-8', errors='replace').splitlines()[:5]: - if 'Version' in line: - return 'Streamline ' + line.rsplit('Version', 1)[1].strip() - except OSError: - pass - return 'Streamline' - - def build_app(app, ver, xlate, feed_repo, lock, ffx=None, dlss=None, art=None, dlssg=None): copy_tree(ROOT / 'launcher', app / 'launcher') for d in ('runtime', 'recomp'): @@ -237,7 +196,7 @@ def build_app(app, ver, xlate, feed_repo, lock, ffx=None, dlss=None, art=None, d 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') if dlssg: # DLSS Frame Generation (runtime/vk_dlssg.c): make_game.py place_dlssg copies it beside the game - place_dlssg(dlssg[0], dlssg[1], app / 'bin' / 'dlssg') + streamline_pin.place(dlssg[0], dlssg[1], app / 'bin' / 'dlssg') if art: # make_game.py draws the button prompts from it (its prompts step) n, size = prompt_art.copy_art(art, app / prompt_art.FOLDER) say(f'button art: {n} pictures, {size / 1e6:.1f} MB') @@ -354,9 +313,10 @@ def main(): "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('--dlssg', type=Path, metavar='DLL', - help='nx2navi_dlssg.dll (tools/build_dlssg.py) for DLSS Frame Generation, with --streamline') + help='nx2navi_dlssg.dll (tools/build_dlssg.py) for DLSS Frame Generation') ap.add_argument('--streamline', type=Path, metavar='SDK', - help='the Streamline SDK folder whose bin/x64 gives the Streamline and nvngx_dlssg.dll files') + help=f'the Streamline {streamline_pin.VERSION} SDK folder whose bin/x64 gives the Streamline and ' + 'nvngx_dlssg.dll files, instead of downloading the SDK release with --dlssg') ap.add_argument('--prompt-art', type=Path, nargs='+', metavar='FOLDER', help='the button art: folders holding XGamepad, P5Gamepad and Keyboard_Mouse, or those folders') ap.add_argument('--out', type=Path, default=MAIN_ROOT / 'build' / 'launcher-release') @@ -386,10 +346,10 @@ def main(): + (f' (CLANG64 clang left out: {lock["fast_skipped"]})' if lock.get('fast_skipped') else '')) 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)') - if bool(a.dlssg) != bool(a.streamline): - sys.exit('--dlssg and --streamline go together') - dlssg = (a.dlssg, a.streamline) if a.dlssg else None - say(f'DLSS Frame Generation: {a.dlssg}, {streamline_version(a.streamline)}' if dlssg + if a.streamline and not a.dlssg: + sys.exit('--streamline goes with --dlssg') + dlssg = (a.dlssg, streamline_pin.resolve(a.streamline, DL)) if a.dlssg else None + say(f'DLSS Frame Generation: {a.dlssg}, Streamline {streamline_pin.VERSION}: {dlssg[1]}' if dlssg else 'DLSS Frame Generation left out (no --dlssg)') build_app(folder / 'app' / ver, ver, a.xlate, a.feed, lock, a.ffx, dlss, a.prompt_art, dlssg) (folder / 'app' / 'current.txt').write_text(ver + '\n', encoding='utf-8') diff --git a/tools/streamline_pin.py b/tools/streamline_pin.py new file mode 100644 index 00000000..442341e0 --- /dev/null +++ b/tools/streamline_pin.py @@ -0,0 +1,118 @@ +"""NVIDIA Streamline's runtime for DLSS Frame Generation (runtime/vk_dlssg.c): tools/make_launcher.py and +tools/make_kit.py put it in the release's or the kit's bin/dlssg/ beside nx2navi_dlssg.dll (tools/build_dlssg.py), and +tools/make_game.py place_dlssg copies the whole set beside the game. + +The files are NVIDIA's own builds from the Streamline SDK release at a pinned version, unmodified: the SDK archive is +downloaded once, checked against its size and SHA-256, and only the files the game loads are kept, each checked +against its own SHA-256. The sl.*.dll files are under Streamline's MIT licence; nvngx_dlssg.dll is under the NVIDIA RTX +SDKs License, which licenses it only for use on systems with NVIDIA GPUs. The notice written beside them names the +version and each file's sha256. +""" +import shutil +import sys +import zipfile +from pathlib import Path + +VERSION = '2.14.16' +URL = f'https://github.com/NVIDIA-RTX/Streamline/releases/download/v{VERSION}/streamline-sdk-v{VERSION}.zip' +SIZE = 276016170 +SHA256 = '32041e51db0cab709a271b72a352281b690c2abbd669a827809401a5dd23956b' + +DLL = 'nx2navi_dlssg.dll' # this repository's runtime/dlssg/, built by tools/build_dlssg.py +# The set tools/make_game.py DLSSG_FILES names: without all of it the game leaves DLSS Frame Generation off. +FILES = (DLL, 'sl.interposer.dll', 'sl.common.dll', 'sl.dlss_g.dll', 'sl.pcl.dll', 'sl.reflex.dll', 'nvngx_dlssg.dll') +NOTICE = 'dlssg-NOTICE.txt' +# The SDK's files the set takes, by their path in the SDK, with their sha256. +SDK_FILES = { + 'bin/x64/sl.interposer.dll': 'b1d7395aebd7b9722841d64dcf7433680020b6a8e421f851c16d5ba0c2182edf', + 'bin/x64/sl.common.dll': '38eea3b4209afc455636ea372b5933d65fcc13becec6c84d101dcfc4d8648f75', + 'bin/x64/sl.dlss_g.dll': '3ed9538df724993bea8a050195a6a78b9c8c8a566d5b05ade477aab3c390e90f', + 'bin/x64/sl.pcl.dll': '5698c38e722318681f215c9fe68e45b9bedd51378acc699d8eee26e961e57770', + 'bin/x64/sl.reflex.dll': '443577e869352436ff7ec8f4d9c90418d3e95368436ce219261e0f4f07a38122', + 'bin/x64/nvngx_dlssg.dll': '8d59d49b36410dd6b62fef2503a5871d347415bd85d943fa0848563a95dd50c7', + 'license.txt': '7b6f23e7d6f3ad6292f9308d2b42cdc3d82ae4e9b2abb55f230279d83bedd43d', + 'bin/x64/nvngx_dlss.license.txt': '3027f23ca5a46dd9cb8183fbd522983a86f64d7daac5982912bf9f214671f294', +} +# The licence texts beside the DLLs, by the SDK file each is copied from. +TEXTS = {'streamline-LICENSE.txt': 'license.txt', 'nvidia-rtx-sdks-LICENSE.txt': 'bin/x64/nvngx_dlss.license.txt'} + + +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 wrong_files(sdk): + """The pinned SDK files that are missing from sdk or differ from the pin.""" + sdk = Path(sdk) + return [n for n, h in SDK_FILES.items() if not (sdk / n).is_file() or sha256_file(sdk / n) != h] + + +def resolve(sdk, cache_dir): + """A folder laid out as the SDK is that holds the pinned files: sdk when given (its files must be the pinned + ones), else cache_dir/streamline-, filled from the SDK archive when missing (downloaded with + launcher/net.py: resumed, checked against the size and SHA-256, and deleted once its files are out).""" + if sdk: + sdk = Path(sdk) + if not sdk.is_dir(): + raise SystemExit(f'{sdk} does not exist') + bad = wrong_files(sdk) + if bad: + raise SystemExit(f'{sdk} is not the Streamline {VERSION} SDK ({", ".join(bad)} missing or different); ' + 'another Streamline needs VERSION, SIZE, SHA256 and SDK_FILES in tools/streamline_pin.py ' + 'changed first') + return sdk + dest = Path(cache_dir) / f'streamline-{VERSION}' + if not wrong_files(dest): + return dest + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + from launcher import net + archive = Path(cache_dir) / f'streamline-sdk-v{VERSION}.zip' + if not (archive.is_file() and archive.stat().st_size == SIZE): + print(f'download {URL} ({SIZE / 1e6:.0f} MB)', flush=True) + net.download(URL, archive, sha256=SHA256, size=SIZE) + extract(archive, dest) + archive.unlink() + return dest + + +def extract(archive, dest): + """The pinned files out of the SDK archive into dest, each checked against its sha256.""" + import hashlib + with zipfile.ZipFile(archive) as z: + names = {n.replace('\\', '/'): n for n in z.namelist()} + for n, h in SDK_FILES.items(): + if n not in names: + raise SystemExit(f'{archive.name} has no {n}') + data = z.read(names[n]) + if hashlib.sha256(data).hexdigest() != h: + raise SystemExit(f'{n} in {archive.name} differs from the pinned file') + (dest / n).parent.mkdir(parents=True, exist_ok=True) + (dest / n).write_bytes(data) + + +def place(dll, sdk, dest): + """DLSS Frame Generation into dest (a release's or the kit's bin/dlssg/): nx2navi_dlssg.dll, Streamline's + interposer and the plugins it loads, and NGX's nvngx_dlssg.dll from the SDK folder resolve returned, the two + licence texts, and a notice naming each file's sha256.""" + dll, sdk, dest = Path(dll), Path(sdk), Path(dest) + if not dll.is_file(): + raise SystemExit(f'{dll} does not exist') + srcs = {n: (dll if n == DLL else sdk / 'bin' / 'x64' / n) for n in FILES} + dest.mkdir(parents=True, exist_ok=True) + lines = ['DLSS Frame Generation for NX2navi. nx2navi_dlssg.dll is NX2navi\'s own code (runtime/dlssg/), built', + 'with the Streamline SDK\'s headers. The other files are NVIDIA\'s own builds, unmodified, from the', + f'Streamline {VERSION} SDK (https://github.com/NVIDIA-RTX/Streamline): the sl.*.dll files under its', + 'MIT licence (streamline-LICENSE.txt), nvngx_dlssg.dll under the NVIDIA RTX SDKs License', + '(nvidia-rtx-sdks-LICENSE.txt), which licenses it only for use on systems with NVIDIA GPUs. They are not', + 'part of NX2navi and NX2navi\'s licence does not cover them.', ''] + for n, p in srcs.items(): + shutil.copy2(p, dest / n) + lines.append(f'{n} sha256 {sha256_file(p)}') + for n, src in TEXTS.items(): + shutil.copy2(sdk / src, dest / n) + (dest / NOTICE).write_text('\n'.join(lines) + '\n', encoding='utf-8')