mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
75
tests/dlssg_exports_check.py
Normal file
75
tests/dlssg_exports_check.py
Normal 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())
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user