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 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-09 04:42:32 -05:00
parent adbbbedf1e
commit ef08fed45f
8 changed files with 177 additions and 11 deletions

View File

@@ -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/<version>/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

View File

@@ -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.

View File

@@ -195,6 +195,12 @@ game folder is the default `<launcher>\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-<version>.exe`

View File

@@ -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

66
tools/dlss_pin.py Normal file
View File

@@ -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')

View File

@@ -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)

View File

@@ -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 <dir with nx2navi_xlate.exe and .id> [--out build/kit] [--zip]
python tools/make_kit.py --xlate <dir with nx2navi_xlate.exe and .id> [--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())

View File

@@ -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/<version>/ 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/<version>/
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)