Stop a launcher release and a build kit on an nx2navi_dlssg.dll that lacks an export runtime/dlssg/nx2_sl.h declares, before they download anything.

The review found that nothing on the release path ran the new export check: tools/make_launcher.py build_app and
tools/make_kit.py copied whatever --dlssg DLL they were given, and the release script still named the DLL of 0.8.0,
which lacks nx2sl_reflex2. Shipped that way, Reflex would never hold the frame limit while players still got this
branch's other changes. make_launcher.py check_dlssg and make_kit.py main now run build_dlssg.check on the --dlssg DLL
beside the translator's check, before anything is resolved, downloaded or deleted; build_dlssg.check also stops on a
missing file and on a file that is no DLL, with a message instead of a traceback. tests/dlssg_exports_check.py adds a
DLL with every declared export (it passes), the two-export DLL, a missing file and two files that are no DLL (they
stop, naming why), make_launcher.check_dlssg on both DLLs, and that both scripts' main makes the check before
streamline_pin.resolve and shutil.rmtree; it fails when the call is taken out of make_launcher.py main. The DLL built
from this branch passes make_launcher.check_dlssg; the DLL of 0.8.0 is refused, naming nx2sl_reflex2.
docs/launcher.md and docs/building.md "Checks" say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:23:03 -05:00
parent 9f4f51f010
commit 0131b1b263
6 changed files with 87 additions and 11 deletions

View File

@@ -773,8 +773,11 @@ on screen once and to the pacer the second time. No GPU, no game files.
A change to `runtime/dlssg/nx2_sl.h` or to the export check in `tools/build_dlssg.py` (`declared_exports`,
`pe_exports`) passes `python tests\dlssg_exports_check.py`, which builds a small DLL with MSYS2's clang and checks that
the export it lacks is the one named, and that every function `nx2_sl.h` declares is read. The DLL itself is checked
when `tools/build_dlssg.py` builds it, or with `--check <dll>`. No MSVC, no Streamline SDK, no game files.
the export it lacks is the one named; that `build_dlssg.check` stops on that DLL, a missing file and a file that is no
DLL, and passes a DLL with every export `nx2_sl.h` declares; that every function `nx2_sl.h` declares is read; and that
`tools/make_launcher.py` and `tools/make_kit.py` make that check of their `--dlssg` DLL before they download anything.
The DLL itself is checked when `tools/build_dlssg.py` builds it, or with `--check <dll>`. No MSVC, no Streamline SDK,
no game files.
A change to which frames the flight recorder takes for a one-frame flash (`runtime/flight_flash.h`, `runtime/flight.c`
`flight_cells`) passes `python tests\flight_flash_check.py`, which builds the header alone, draws pictures of the

View File

@@ -516,9 +516,11 @@ stays playable and Rebuild the Game, or Begin Installation, goes on from the ste
`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). `tools/build_dlssg.py` fails a DLL that lacks an export `runtime/dlssg/nx2_sl.h`
declares, and `python tools/build_dlssg.py --check <dll>` makes that check of a DLL built earlier: the runtime
takes a missing optional export for an older DLL and leaves that feature out, Reflex's frame limit among them.
files must be the pinned ones). The runtime takes a missing optional export for an older DLL and leaves that
feature out, Reflex's frame limit among them, so a DLL that lacks an export `runtime/dlssg/nx2_sl.h` declares fails
`tools/build_dlssg.py`, and the release stops on such a `--dlssg` DLL before anything is downloaded
(`make_launcher.py` `check_dlssg`; `make_kit.py` the same). `python tools/build_dlssg.py --check <dll>` makes that
check of a DLL built earlier.
`--pgo FILE` puts an execution profile and its record in the release (`app/<version>/data`), and the prebuilt
runtime is compiled with it; the release stops when the record names another `recomp/` than the checkout's, and

View File

@@ -3,10 +3,17 @@ has, and names those the DLL lacks. A DLL built from older sources would otherwi
optional export (nx2sl_reflex2, nx2sl_frames_max) for an older DLL and leaves its feature out without an error.
Builds a small DLL with two of three declared exports with MSYS2 UCRT64's clang (no MSVC, no Streamline SDK, no game
files) and checks that the third is the one reported; then that nx2_sl.h's own declarations are read in full.
files) and checks that the third is the one reported; then that nx2_sl.h's own declarations are read in full. The
release gate: build_dlssg.check stops on that DLL (nx2_sl.h lists nx2sl_reflex2), on a missing file and on files that
are no DLL, and passes a DLL built with every export nx2_sl.h declares; tools/make_launcher.py and tools/make_kit.py
run it on their --dlssg DLL before they resolve, download or delete anything (a release that shipped the DLL of 0.8.0
would never let Reflex hold the frame limit).
Usage: dlssg_exports_check.py
"""
import contextlib
import inspect
import io
import os
import shutil
import subprocess
@@ -37,6 +44,16 @@ def compiler():
return shutil.which('clang') or 'clang'
def refused(fn, *args):
"""The message fn(*args) exits with, None when it returns."""
try:
with contextlib.redirect_stdout(io.StringIO()):
fn(*args)
except SystemExit as e:
return str(e.code)
return None
def main():
if os.name != 'nt':
print('dlssg_exports_check: Windows only (it builds a DLL)')
@@ -67,6 +84,35 @@ def main():
expect(f'{name} declared in nx2_sl.h', name in declared, True)
# the header names its version and log types too, which are no exports of their own
expect('types that are no functions', {'nx2sl_' + t for t in ('Image', 'Camera', 'Log')} & declared, set())
# The release gate. A DLL with every export nx2_sl.h declares passes; the two-export one, a missing file and files
# that are no DLL stop it, naming why.
(work / 'all.c').write_text(''.join(f'__declspec(dllexport) void {n}(void) {{}}\n' for n in sorted(declared)),
encoding='utf-8')
full = work / 'all.dll'
subprocess.check_call([compiler(), '-shared', '-O1', str(work / 'all.c'), '-o', str(full)], env=env)
expect('missing from a DLL with every declared export', build_dlssg.missing_exports(full), [])
expect('check of a DLL with every declared export', refused(build_dlssg.check, full), None)
why = refused(build_dlssg.check, dll) or ''
expect('check of the two-export DLL names nx2sl_reflex2', 'nx2sl_reflex2' in why and 'build it again' in why, True)
expect('check of a missing DLL', 'is missing' in (refused(build_dlssg.check, work / 'none.dll') or ''), True)
(work / 'tiny.dll').write_bytes(b'MZ')
for name in ('two.c', 'tiny.dll'):
expect(f'check of {name}, no DLL', 'no DLL' in (refused(build_dlssg.check, work / name) or ''), True)
# make_launcher.py's check_dlssg is that check, and both packaging scripts make it before they resolve the
# Streamline files (a download) or delete their output folder.
import make_kit
import make_launcher
expect('make_launcher.check_dlssg passes the full DLL', refused(make_launcher.check_dlssg, full), None)
expect('make_launcher.check_dlssg names nx2sl_reflex2',
'nx2sl_reflex2' in (refused(make_launcher.check_dlssg, dll) or ''), True)
for mod, call, later in ((make_launcher, 'check_dlssg(a.dlssg)', ('streamline_pin.resolve(', 'shutil.rmtree(')),
(make_kit, 'build_dlssg.check(a.dlssg)', ('streamline_pin.resolve(', 'shutil.rmtree('))):
src = inspect.getsource(mod.main)
at = src.find(call)
for what in later:
expect(f'{mod.__name__}.main checks the DLL before {what}', 0 <= at < src.find(what), True)
print(f'dlssg_exports_check: {"ok" if not fails else f"{fails} failed"} ({len(declared)} exports in nx2_sl.h)')
return 1 if fails else 0

View File

@@ -12,7 +12,8 @@ tools/make_game.py place_dlssg puts them beside the game.
The built DLL must export every function runtime/dlssg/nx2_sl.h declares (each PFN_nx2sl_<name> is the export
nx2sl_<name>), or the build fails naming the ones it lacks: the runtime takes a missing optional export for an older
DLL and leaves its feature out without an error. --check makes the same check of a DLL built before, such as the one
a release is about to ship.
a release is about to ship; tools/make_launcher.py and tools/make_kit.py make it on their --dlssg DLL first and stop
on one that fails.
"""
import argparse
import os
@@ -83,8 +84,14 @@ def missing_exports(dll, header=HEADER):
def check(dll):
"""Exit naming the exports nx2_sl.h declares that dll lacks."""
missing = missing_exports(dll)
"""Exit naming the exports nx2_sl.h declares that dll lacks, or saying that dll is missing or no PE file.
tools/make_launcher.py and tools/make_kit.py run it on their --dlssg DLL before anything is downloaded or built."""
if not Path(dll).is_file():
sys.exit(f'{dll} is missing')
try:
missing = missing_exports(dll)
except (ValueError, TypeError, struct.error) as e: # pe_exports: no PE signature, or a table outside the file
sys.exit(f'{dll} is no DLL that can be read ({e})')
if missing:
sys.exit(f'{dll} lacks exports runtime/dlssg/nx2_sl.h declares: {", ".join(missing)} '
'(a DLL built from older sources: build it again)')

View File

@@ -14,7 +14,8 @@ tools/build_dlssg.py beside NVIDIA Streamline's runtime, tools/streamline_pin.py
SDK release and checked, or --streamline), AMD's FSR 3.1 library with --ffx, its two FSR 4 libraries with --ffx4, 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. The --xlate folder must hold a translator built from this
checkout's sources (tools/build_xlate.py check_built); the script stops first if not.
checkout's sources (tools/build_xlate.py check_built), and the --dlssg DLL must export every function
runtime/dlssg/nx2_sl.h declares (tools/build_dlssg.py check); the script stops first if not.
--prompt-art names folders that hold XGamepad, P5Gamepad and Keyboard_Mouse, or those three folders themselves: the
pictures tools/pad_prompts.py reads go into the kit's prompt-art folder (tools/prompt_art.py art_files), and
@@ -73,6 +74,7 @@ def main():
ap.add_argument('--zip', action='store_true')
a = ap.parse_args()
sys.path.insert(0, str(ROOT / 'tools'))
import build_dlssg
import build_shaders
import build_xlate
import dlss_pin
@@ -83,6 +85,8 @@ def main():
reason = build_xlate.check_built(a.xlate, 'nx2navi_xlate.exe') # the translator of this checkout's sources
if reason:
sys.exit(reason)
if a.dlssg: # a DLL older than runtime/dlssg/nx2_sl.h would leave features out at run time without an error
build_dlssg.check(a.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')

View File

@@ -26,7 +26,8 @@ The release folder (--out) holds:
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),
licences and a notice; the release stops first on a DLL that lacks an export
runtime/dlssg/nx2_sl.h declares, tools/build_dlssg.py check),
lib/libnx2navirt.a (the runtime, prebuilt with the release's compiler),
data/pgo-v111.profdata and .json with --pgo (the execution profile make_game.py
compiles the lifted code and the runtime with, the prebuilt runtime included, and
@@ -209,6 +210,17 @@ def check_xlate(xlate):
print(f'translator identity {have[:16]}: built from this checkout\'s sources')
def check_dlssg(dll):
"""A release carries an nx2navi_dlssg.dll that exports every function runtime/dlssg/nx2_sl.h declares. The runtime
takes a missing optional export (nx2sl_reflex2, nx2sl_frames_max) for an older DLL and leaves that feature out
with one log line, so a DLL built from older sources would ship without an error: with the DLL of 0.8.0, which
lacks nx2sl_reflex2, Reflex would never hold the frame limit. Such a DLL, or a missing one, stops the build
(build_dlssg.check); main runs this before anything is resolved, downloaded or deleted."""
import build_dlssg
build_dlssg.check(dll)
print(f'{dll}: exports the {len(build_dlssg.declared_exports())} functions runtime/dlssg/nx2_sl.h declares')
def build_app(app, ver, xlate, feed_repo, lock, ffx=None, dlss=None, art=None, dlssg=None, pipelines=None, ffx4=None,
pgo_profile=None):
# The runtime's own shaders are source in the repository and SPIR-V headers in a release: a player's PC compiles
@@ -404,6 +416,8 @@ def main():
if a.pgo:
pgo.fitting(a.pgo) # the profile fits this checkout's recomp/ before anything is downloaded or built
check_xlate(a.xlate) # the translator is this checkout's before anything is downloaded, built or deleted
if a.dlssg:
check_dlssg(a.dlssg) # and so is nx2navi_dlssg.dll
out = a.out
shutil.rmtree(out, ignore_errors=True)
folder = out / 'NX2navi'