Fail tools/build_dlssg.py when nx2navi_dlssg.dll lacks an export nx2_sl.h declares, and let it check a DLL built earlier.

The runtime takes a missing optional export (nx2sl_reflex2, nx2sl_frames_max) for an older DLL and leaves the
feature out with one log line, so a release that ships a DLL built from older sources than its runtime loses Reflex's
frame limit without an error; the release script took the DLL of 0.8.0 from a fixed path. After the build,
build_dlssg.py now reads every PFN_nx2sl_<name> in runtime/dlssg/nx2_sl.h and the DLL's export directory (a small
PE reader of its own, PE32 and PE32+) and exits naming each export that is missing; --check <dll> makes the same
check without building. tests/dlssg_exports_check.py builds a DLL with two of three declared exports with MSYS2's
clang and checks that the third is named, and that nx2_sl.h's own declarations are read in full; docs/building.md
"Checks" and docs/launcher.md name both. The DLL built from this branch with the pinned Streamline 2.14.16 SDK passes
(15 exports); the one built before nx2sl_reflex2 fails, naming it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:00:05 -05:00
parent e0843439a7
commit 9f4f51f010
4 changed files with 150 additions and 2 deletions

View File

@@ -771,6 +771,11 @@ first host present, after 200 ms without one and with the setting off, that fram
give it to the pacer for the rest of the run, and that frames the multiplier periods apart switch Reflex to the frames
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.
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
thumbnails' size and checks that a patch or a whole frame shown once is a flash, that a camera cut, a fade, a pan and

View File

@@ -516,7 +516,9 @@ 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).
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.
`--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

@@ -0,0 +1,75 @@
"""Check tools/build_dlssg.py's export check: it reads the exports runtime/dlssg/nx2_sl.h declares and the ones a DLL
has, and names those the DLL lacks. A DLL built from older sources would otherwise ship: the runtime takes a missing
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.
Usage: dlssg_exports_check.py
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
sys.path.insert(0, str(ROOT / 'tools'))
import build_dlssg # noqa: E402
HEADER = '''typedef int (*PFN_nx2sl_version)(void);
typedef void (*PFN_nx2sl_reflex)(int mode);
typedef int ( * PFN_nx2sl_reflex2 )(int mode, unsigned frame_limit_us);
'''
SOURCE = '''__declspec(dllexport) int nx2sl_version(void) { return 3; }
__declspec(dllexport) void nx2sl_reflex(int mode) { (void)mode; }
int nx2sl_not_exported(void) { return 0; }
'''
def compiler():
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/wb_lazy_check.py picks it)."""
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
def main():
if os.name != 'nt':
print('dlssg_exports_check: Windows only (it builds a DLL)')
return 0
work = ROOT / 'build' / 'test' / 'dlssg_exports'
work.mkdir(parents=True, exist_ok=True)
(work / 'two.h').write_text(HEADER, encoding='utf-8')
(work / 'two.c').write_text(SOURCE, encoding='utf-8')
env = dict(os.environ)
if MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
dll = work / 'two.dll'
subprocess.check_call([compiler(), '-shared', '-O1', str(work / 'two.c'), '-o', str(dll)], env=env)
fails = 0
def expect(what, got, want):
nonlocal fails
if got != want:
print(f'FAIL {what}: {got!r}, not {want!r}')
fails += 1
expect('declared in the test header', build_dlssg.declared_exports(work / 'two.h'),
{'nx2sl_version', 'nx2sl_reflex', 'nx2sl_reflex2'})
expect('exported by the test DLL', build_dlssg.pe_exports(dll), {'nx2sl_version', 'nx2sl_reflex'})
expect('missing from the test DLL', build_dlssg.missing_exports(dll, work / 'two.h'), ['nx2sl_reflex2'])
declared = build_dlssg.declared_exports()
for name in ('nx2sl_version', 'nx2sl_init', 'nx2sl_reflex', 'nx2sl_reflex2', 'nx2sl_frames_max', 'nx2sl_shutdown'):
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())
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
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,22 +1,31 @@
"""Build nx2navi_dlssg.dll (runtime/dlssg/nx2_sl.cpp, DLSS Frame Generation through NVIDIA Streamline) with MSVC.
python tools/build_dlssg.py --sl <Streamline SDK folder with include/> [--out build/dlssg]
python tools/build_dlssg.py --check <nx2navi_dlssg.dll>
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 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.
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.
"""
import argparse
import os
import re
import shutil
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
HEADER = ROOT / 'runtime' / 'dlssg' / 'nx2_sl.h'
VSWHERE = Path(os.environ.get('ProgramFiles(x86)', r'C:\Program Files (x86)')) / 'Microsoft Visual Studio' / 'Installer' / 'vswhere.exe'
@@ -32,12 +41,68 @@ def vcvars():
return bat
def declared_exports(header=HEADER):
"""The exports nx2_sl.h declares: nx2sl_<name> for each function pointer type PFN_nx2sl_<name>."""
text = Path(header).read_text(encoding='utf-8')
return {'nx2sl_' + name for name in re.findall(r'\(\s*\*\s*PFN_nx2sl_(\w+)\s*\)', text)}
def pe_exports(path):
"""The names a PE file (PE32 or PE32+) exports: its export directory's name table. Empty without one."""
d = Path(path).read_bytes()
pe = struct.unpack_from('<I', d, 0x3C)[0]
if d[pe:pe + 4] != b'PE\0\0':
raise ValueError(f'{path} is not a PE file')
nsec, opt = struct.unpack_from('<H', d, pe + 6)[0], struct.unpack_from('<H', d, pe + 20)[0]
magic = struct.unpack_from('<H', d, pe + 24)[0]
sec = pe + 24 + opt
# data directory 0, the exports: 112 bytes into a PE32+ optional header, 96 into a PE32 one
rva = struct.unpack_from('<I', d, pe + 24 + (112 if magic == 0x20b else 96))[0]
def off(r):
for i in range(nsec):
va, vs, raw = (struct.unpack_from('<I', d, sec + 40 * i + k)[0] for k in (12, 8, 20))
if va <= r < va + max(vs, 1):
return r - va + raw
return None
at = off(rva) if rva else None
if at is None:
return set()
n, names = struct.unpack_from('<I', d, at + 24)[0], off(struct.unpack_from('<I', d, at + 32)[0])
out = set()
for i in range(n):
o = off(struct.unpack_from('<I', d, names + 4 * i)[0])
out.add(d[o:d.index(b'\0', o)].decode('ascii'))
return out
def missing_exports(dll, header=HEADER):
"""The exports header declares that dll lacks, sorted."""
return sorted(declared_exports(header) - pe_exports(dll))
def check(dll):
"""Exit naming the exports nx2_sl.h declares that dll lacks."""
missing = missing_exports(dll)
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)')
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--sl', type=Path, required=True, help='the Streamline SDK folder (holds include/sl.h)')
ap.add_argument('--sl', type=Path, help='the Streamline SDK folder (holds include/sl.h)')
ap.add_argument('--vulkan', type=Path, default=Path(r'C:\msys64\ucrt64\include'), help='folder holding vulkan/vulkan.h')
ap.add_argument('--out', type=Path, default=ROOT / 'build' / 'dlssg')
ap.add_argument('--check', type=Path, metavar='DLL', help='only check that DLL exports what nx2_sl.h declares')
a = ap.parse_args()
if a.check:
check(a.check)
print(f'{a.check}: the {len(declared_exports())} exports nx2_sl.h declares')
return
if not a.sl:
ap.error('--sl is required to build')
if not (a.sl / 'include' / 'sl.h').is_file():
sys.exit(f'{a.sl / "include" / "sl.h"} is missing')
a.out.mkdir(parents=True, exist_ok=True)
@@ -60,6 +125,7 @@ def main():
os.unlink(script)
if r.returncode or not (a.out / 'nx2navi_dlssg.dll').is_file():
sys.exit(f'the build failed ({r.returncode})')
check(a.out / 'nx2navi_dlssg.dll')
print(a.out / 'nx2navi_dlssg.dll')