diff --git a/docs/building.md b/docs/building.md index 7a9e0530..0e85afda 100644 --- a/docs/building.md +++ b/docs/building.md @@ -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 `. 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 `. 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 diff --git a/docs/launcher.md b/docs/launcher.md index 080eae12..64f974d1 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -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 ` 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 ` makes that + check of a DLL built earlier. `--pgo FILE` puts an execution profile and its record in the release (`app//data`), and the prebuilt runtime is compiled with it; the release stops when the record names another `recomp/` than the checkout's, and diff --git a/tests/dlssg_exports_check.py b/tests/dlssg_exports_check.py index b233a319..c44ba443 100644 --- a/tests/dlssg_exports_check.py +++ b/tests/dlssg_exports_check.py @@ -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 diff --git a/tools/build_dlssg.py b/tools/build_dlssg.py index bee04f07..48eb992e 100644 --- a/tools/build_dlssg.py +++ b/tools/build_dlssg.py @@ -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_ is the export nx2sl_), 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)') diff --git a/tools/make_kit.py b/tools/make_kit.py index 0483eabe..6d61ef34 100644 --- a/tools/make_kit.py +++ b/tools/make_kit.py @@ -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') diff --git a/tools/make_launcher.py b/tools/make_launcher.py index 99688afc..3fbc028a 100644 --- a/tools/make_launcher.py +++ b/tools/make_launcher.py @@ -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'