Files
d4r/tools/windows/install_dependencies.py
T
ayo a617c818de TL;DR: Add an experimental FSR Ray Regeneration backend on Windows, run the K
network natively on HIP with Vulkan->HIP shared memory, extend K kernels to
gfx11 (RDNA3), and add dependency/packaging/test tooling around them.

Windows: FSR RR backend, native HIP K runner, gfx11 K kernels, tooling

FSR Ray Regeneration (experimental)
- Add fsr_rr_backend.{h,cpp}, fsr_rr_passes.h and the fsr_rr/ prepare and
  compose compute shaders. They run AMD's FidelityFX denoiser
  (amd_fidelityfx_denoiser_dx12.dll, SDK 2.3, not bundled) on a private
  vkd3d-proton device.
- Add the amdxc64 shim (tools/windows/amdxc64_shim) and two vkd3d-proton
  patches for libcxx includes. AMD's amdxc64.dll is never loaded.
- d4r_nvngx_windows: RrFeature now selects between the native DLSS RR
  backend and the FSR backend (D4R_RR_BACKEND=fsr). After the denoiser it
  runs an internal Super Sampling stage (feature 1) to upscale. If SR is
  unavailable and output != render size, the frame fails; it is never
  returned noisy.
- d4r.ini / player_config: new [RayReconstruction] FsrDenoiser,
  FsrStabilityBias (auto or >= 0) and FsrDetail (0-1), with validation, and
  matching D4R_FSR_* environment variables.
- Add the FSR RR fixture, output-check and ngx test scripts, and a shader
  compile script.

Native HIP K runner (Windows)
- CMake: add d4r_engine_k_hip (d4r-k-hip) plus separate volk/engine copies,
  because VK_USE_PLATFORM_WIN32_KHR changes volk's entry points.
- Share Vulkan memory with HIP via Win32 opaque handles on Windows
  (VK_KHR_external_memory_win32) and via fds on Linux. Handle types are now
  intptr_t, and the platform choice is in D4R_EXT_MEMORY_* macros.
- HipNetwork: add debugRepeatHashes and debugNodeMicros, exposed as
  D4R_HIP_REPEAT_CHECK, D4R_HIP_REPEAT_ALL and D4R_HIP_NODE_PROFILE in
  replay.cpp.
- replay.cpp: display-resolution-motion frames (FrameFlagDisplayMotion) no
  longer require near2rnd == out2rnd. Both values must still be in (0, 1].

K kernels on gfx11
- Allow position-token and enc1 tiling on __GFX11__ as well as gfx12.
- Add an opt-in ds_bpermute cross-half exchange
  (D4R_WINDOWS_GFX11_DS_EXCHANGE, enabled for K on Windows gfx11). M keeps
  its LDS path.
- Fix a block-barrier race in the position layer. Without POS_VSHARE a fast
  wave could overwrite its hb rows while a slower wave still read them for a
  later head, giving rare wrong results on gfx1101 (enc0 outputs differing
  between identical launches).

Packaging and install
- install.bat, d4r.ini, WINDOWS_README: FSR option, and
  install_dependencies.py to install pinned NVIDIA dependencies with staged,
  hash-verified replacement and rollback.
- Update the release package, local package refresh and test scripts.
- Add the InteropBatchInputCopies option (default false). Batching is faster
  on some synthetic runs, but player temporal exactness is unresolved.

Tooling and tests
- K model: capture_k_weights, build_k_model, compare_k_outputs,
  benchmark_k_hip, build_capture_runtime, test_k_model and the
  zluda-k-output-capture patch. Captured weights and models are private
  local assets and must not enter release archives.
- Synthetic probes: test_m_ngx, test_sr_dynamic,
  prepare_rr_dynamic_fixture and the d3d12/rr_ngx probe updates.
- Add tests/windows/test_install_dependencies.py.

Docs
- Update the windows-fsr-rr, k-performance, local-package, rr-interop and
  rr-model-cache docs. Add windows-parity-handoff.md and
  windows-parity-continuation-prompt.md.
2026-10-17 01:43:06 -04:00

352 lines
18 KiB
Python

"""Install pinned local NVIDIA dependencies without PowerShell or driver installation.
All downloads and backups stay outside the game. Runtime replacements are staged,
hash verified and rolled back on failure. No model generation is implicit.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import subprocess
import tempfile
import urllib.request
import uuid
PINS = {
'dlss': ('nvngx_dlss.dll', 'dlss/nvngx_dlss.dll',
'https://raw.githubusercontent.com/NVIDIA/DLSS/374959484e79a640feaba44c93ac8cfb0a03f5b5/lib/Windows_x86_64/rel/nvngx_dlss.dll',
'3975567b8943c53acce397f2b72380092f84f162d00b0d2c7d08a1025c563983'),
'dlssd': ('nvngx_dlssd.dll', 'rr/nvngx_dlssd.dll',
'https://raw.githubusercontent.com/NVIDIA/DLSS/374959484e79a640feaba44c93ac8cfb0a03f5b5/lib/Windows_x86_64/rel/nvngx_dlssd.dll',
'4bc7ea5fcb2f32cf86bc2cb072e8d2860914a0be511cbdcb2fc206c1d9d83b80'),
'ngx': ('_nvngx.dll', 'ngx/_nvngx.dll', None,
'66767018c36b3bab46398dade3adf173daa3730fda75965689ea848c9bc4e79b'),
}
# Optional: AMD's FSR Ray Regeneration denoiser (FidelityFX SDK 2.3), only for [RayReconstruction] Backend = fsr.
# AMD's file, never bundled; the same pin as scripts/windows/setup-fsr-rr.ps1.
FSR_DENOISER = ('amd_fidelityfx_denoiser_dx12.dll', 'amd_fidelityfx_denoiser_dx12.dll',
'https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v2.3.0/Kits/FidelityFX/signedbin/amd_fidelityfx_denoiser_dx12.dll',
'48f1e5888ba6a0a3d59a98b9751e37c392b0f7b8c223d0082d5c1f40642879d3')
DRIVER = ('https://us.download.nvidia.com/Windows/617.14/617.14-desktop-win10-win11-64bit-international-dch-whql.exe',
'63d3da5ed52547bf3fd86204bb9ea1455947b24e2aa2519f359126f9a6efe13f')
SEVENZIP = ('https://github.com/ip7z/7zip/releases/download/26.04/7zr.exe',
'256feca8e274e5da655e2a284fabafd9f554365eb164862089dacd4e8276d282')
def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def external(path, game):
if path.resolve().is_relative_to(game.resolve()):
raise RuntimeError('Cache and backups must stay outside game directory: ' + str(path))
def verified_download(path, url, expected, offline=False):
if path.exists():
if path.is_file() and sha(path) == expected:
return path
if offline:
raise RuntimeError('Invalid offline cache: ' + str(path))
path.rename(path.with_name(path.name + '.bad-' + uuid.uuid4().hex))
if offline:
raise RuntimeError('Missing offline cache: ' + str(path))
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + '.part-' + uuid.uuid4().hex)
try:
print('DOWNLOAD ' + path.name, flush=True)
with urllib.request.urlopen(url, timeout=60) as response, temporary.open('xb') as out:
shutil.copyfileobj(response, out)
if sha(temporary) != expected:
raise RuntimeError('Download SHA256 mismatch: ' + path.name)
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
return path
def ini_selects_fsr(runtime):
"""True when d4r.ini sets [RayReconstruction] Backend = fsr (comments and case ignored)."""
section = ''
for line in (runtime / 'd4r.ini').read_text(encoding='utf-8-sig').splitlines():
line = line.split(';', 1)[0].split('#', 1)[0].strip()
if line.startswith('[') and line.endswith(']'):
section = line[1:-1].strip().lower()
elif section == 'rayreconstruction' and '=' in line:
key, value = line.split('=', 1)
if key.strip().lower() == 'backend' and value.strip().lower() == 'fsr':
return True
return False
def select_backend(data, backend):
"""Return d4r.ini bytes with [RayReconstruction] Backend set to `backend`; everything else is preserved."""
bom = data.startswith(b'\xef\xbb\xbf')
text = data.decode('utf-8-sig')
newline = '\r\n' if '\r\n' in text else '\n'
result, in_rr, found, done = [], False, False, False
for line in text.splitlines(keepends=True):
stripped = line.strip()
if stripped.startswith('['):
if in_rr and not done:
result.append('Backend = ' + backend + newline)
done = True
in_rr = stripped.split(']', 1)[0][1:].strip().casefold() == 'rayreconstruction'
found = found or in_rr
elif in_rr and '=' in line and not stripped.startswith((';', '#')) and line.split('=', 1)[0].strip().casefold() == 'backend':
prefix, body = line.split('=', 1)
ending = '\r\n' if body.endswith('\r\n') else '\n' if body.endswith('\n') else ''
body = body[:len(body) - len(ending)]
comment = re.search(r'\s[;#]', body)
result.append(prefix + '= ' + backend + (body[comment.start():] if comment else '') + ending)
done = True
continue
result.append(line)
if in_rr and not done:
if result and not result[-1].endswith(('\n', '\r')):
result.append(newline)
result.append('Backend = ' + backend + newline)
elif not found:
if result and not result[-1].endswith(('\n', '\r')):
result.append(newline)
result.extend((newline, '[RayReconstruction]' + newline, 'Backend = ' + backend + newline))
return (b'\xef\xbb\xbf' if bom else b'') + ''.join(result).encode('utf-8')
def enable_fsr(runtime, backup_root):
"""Select the FSR backend in d4r.ini with an external backup; idempotent."""
config = runtime / 'd4r.ini'
original = config.read_bytes()
updated = select_backend(original, 'fsr')
if updated == original:
return None
backup_root.mkdir(parents=True, exist_ok=True)
backup = backup_root / ('fsr-config-' + uuid.uuid4().hex + '.ini')
with backup.open('xb') as stream:
stream.write(original)
temporary = config.with_name('d4r.ini.' + uuid.uuid4().hex + '.tmp')
with temporary.open('xb') as stream:
stream.write(updated)
if config.read_bytes() != original:
temporary.unlink()
raise RuntimeError('d4r.ini changed during installation; preserved current configuration')
os.replace(temporary, config)
return backup
def collect(cache, offline, fsr=False):
assets = []
for kind, (name, relative, url, expected) in PINS.items():
cached = cache / 'nvidia' / name
if kind == 'ngx' and not (cached.is_file() and sha(cached) == expected):
if offline:
raise RuntimeError('Missing or invalid offline NGX 617.14 core')
driver = verified_download(cache / 'nvidia-driver-617.14.exe', *DRIVER)
extractor = verified_download(cache / 'tools/7zr-26.04.exe', *SEVENZIP)
with tempfile.TemporaryDirectory(prefix='d4r-ngx-') as work:
subprocess.run([str(extractor), 'e', '-y', '-o' + work, str(driver),
'Display.Driver/_nvngx.dll'], check=True, timeout=600)
core = Path(work) / name
if sha(core) != expected:
raise RuntimeError('Extracted NGX core SHA256 mismatch')
cached.parent.mkdir(parents=True, exist_ok=True)
if cached.exists():
cached.rename(cached.with_name(name + '.bad-' + uuid.uuid4().hex))
staged = cached.with_name(name + '.part-' + uuid.uuid4().hex)
try:
shutil.copy2(core, staged)
if sha(staged) != expected:
raise RuntimeError('Staged core SHA256 mismatch')
os.replace(staged, cached)
finally:
staged.unlink(missing_ok=True)
else:
verified_download(cached, url, expected, offline)
assets.append((cached, relative, expected))
if fsr:
name, relative, url, expected = FSR_DENOISER
assets.append((verified_download(cache / 'amd' / name, url, expected, offline), relative, expected))
return assets
def transaction(runtime, assets, backup_root):
"""Back up every replacement before first mutation; preserve concurrent edits."""
entries, applied, staged_files = [], [], []
backup = backup_root / ('dlss-install-' + uuid.uuid4().hex)
try:
for source, relative, expected in assets:
if Path(relative).is_absolute() or '..' in Path(relative).parts:
raise RuntimeError('Invalid runtime destination')
destination = runtime / relative
if not destination.resolve().is_relative_to(runtime.resolve()):
raise RuntimeError('Runtime destination escapes runtime')
if destination.exists() and not destination.is_file():
raise RuntimeError('DLL destination is not a file: ' + str(destination))
previous = sha(destination) if destination.exists() else None
if previous == expected:
continue
original = backup / relative
if previous is not None:
original.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(destination, original)
if sha(original) != previous:
raise RuntimeError('Backup SHA256 mismatch')
entries.append(dict(path=str(destination), sha256=previous, installHash=expected,
backup=str(original) if previous is not None else None))
destination.parent.mkdir(parents=True, exist_ok=True)
staged = destination.with_name(destination.name + '.install-' + uuid.uuid4().hex)
staged_files.append(staged)
shutil.copy2(source, staged)
if sha(staged) != expected:
raise RuntimeError('Staged DLL SHA256 mismatch')
if entries:
backup.mkdir(parents=True, exist_ok=True)
(backup / 'backup-manifest.json').write_text(json.dumps({'files': entries}, indent=2) + '\n')
for entry, staged in zip(entries, staged_files):
destination = Path(entry['path'])
current = sha(destination) if destination.exists() else None
if current != entry['sha256']:
raise RuntimeError('Destination changed after backup: ' + str(destination))
os.replace(staged, destination)
applied.append(entry)
if sha(destination) != entry['installHash']:
raise RuntimeError('Installed DLL SHA256 mismatch')
return {'changed': len(entries), 'backup': str(backup) if entries else None,
'files': {relative: sha(runtime / relative) for _, relative, _ in assets}}
except BaseException:
errors = []
for entry in reversed(applied):
destination = Path(entry['path'])
try:
if not destination.is_file() or sha(destination) != entry['installHash']:
errors.append('Concurrent edit preserved: ' + str(destination))
continue
if entry['backup']:
original = Path(entry['backup'])
if sha(original) != entry['sha256']:
raise RuntimeError('Rollback backup changed')
staged = destination.with_name(destination.name + '.rollback-' + uuid.uuid4().hex)
staged_files.append(staged)
shutil.copy2(original, staged)
os.replace(staged, destination)
else:
destination.unlink()
except Exception as error:
errors.append(str(error))
if errors:
print('ROLLBACK ATTENTION: ' + '; '.join(errors), flush=True)
raise
finally:
for staged in staged_files:
staged.unlink(missing_ok=True)
def prepare_model_tools(bundle, cache):
"""Copy only verified clean inputs; generated products never become inputs."""
data = (bundle / 'rr-tools.json').read_bytes()
manifest = json.loads(data)
if (manifest.get('kind') != 'local-rr-model-tools' or manifest.get('modelsBundled') is not False
or manifest.get('nvidiaBinariesBundled') is not False):
raise RuntimeError('Unsupported model-tool manifest')
target = cache / ('rr-tools-' + hashlib.sha256(data).hexdigest()[:16])
if not target.resolve().is_relative_to(cache.resolve()):
raise RuntimeError('Model-tool cache escapes cache root')
files = manifest['files']
required = {'python/python.exe', 'recipe/tools/windows/build_rr_model.py',
'recipe/tools/windows/deploy_rr_model.py', 'recipe/engine/rr/native_kernels.txt'}
if not required.issubset(files):
raise RuntimeError('Incomplete model-tool bundle')
for name, expected in files.items():
parts = PurePosixPath(name)
if parts.is_absolute() or '..' in parts.parts or '\\' in name or ':' in name:
raise RuntimeError('Unsafe model-tool path')
source = bundle.joinpath(*parts.parts)
if not source.resolve().is_relative_to(bundle.resolve()) or sha(source) != expected:
raise RuntimeError('Model-tool input changed: ' + name)
destination = target.joinpath(*parts.parts)
if not destination.resolve().is_relative_to(target.resolve()):
raise RuntimeError('Model-tool cache path escaped')
if destination.exists():
if sha(destination) != expected:
raise RuntimeError('Model-tool cache changed; preserved: ' + str(destination))
else:
destination.parent.mkdir(parents=True, exist_ok=True)
with source.open('rb') as src, destination.open('xb') as out:
shutil.copyfileobj(src, out)
if sha(destination) != expected:
raise RuntimeError('Model-tool copy SHA256 mismatch')
# A stale or used cache is never swept into a package. Build reuse validates
# generated products separately through the existing builder's identity.
return target
def install_model(runtime, cache, backup, preset, enable):
metadata = json.loads((runtime / 'package.json').read_text(encoding='utf-8-sig'))
if set(metadata['targets']) != {'gfx1101'}:
raise RuntimeError('Automatic RR build currently requires gfx1101-only package')
tools = prepare_model_tools(runtime / 'model-tools', cache)
python = tools / 'python/python.exe'
recipe = tools / 'recipe'
output = recipe / 'build' / ('rr-' + preset.lower())
# Each builder child has its own 600s deadline and kills a timed-out tree.
subprocess.run([str(python), str(recipe / 'tools/windows/build_rr_model.py'),
'--dll', str(runtime / 'rr/nvngx_dlssd.dll'), '--preset', preset,
'--output', str(output), '--reuse'], check=True)
command = [str(python), str(recipe / 'tools/windows/deploy_rr_model.py'),
'--model', str(output), '--runtime', str(runtime), '--backup-root', str(backup)]
if enable:
command.append('--enable')
subprocess.run(command, check=True, timeout=120)
def main():
local = Path(os.environ.get('LOCALAPPDATA', str(Path.home()))) / 'd4r'
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--package-root', required=True, type=lambda p: Path(p).resolve())
parser.add_argument('--cache-root', type=lambda p: Path(p).resolve(), default=local / 'cache')
parser.add_argument('--backup-root', type=lambda p: Path(p).resolve(), default=local / 'backups')
parser.add_argument('--offline', action='store_true')
parser.add_argument('--yes', action='store_true')
parser.add_argument('--rr-preset', choices=('D', 'E'), help='build and deploy optional exact gfx1101 RR model')
parser.add_argument('--enable-rr', action='store_true', help='select generated native RR model in d4r.ini')
parser.add_argument('--enable-fsr', action='store_true', help='install the FSR denoiser and select Backend = fsr in d4r.ini')
parser.add_argument('--install-fsr', action='store_true',
help="also install AMD's pinned FSR Ray Regeneration denoiser (automatic when d4r.ini selects Backend = fsr)")
args = parser.parse_args()
runtime = args.package_root / 'd4r' if (args.package_root / 'd4r/d4r.ini').is_file() else args.package_root
if not (runtime / 'd4r.ini').is_file():
parser.error('Extract Windows release first; runtime d4r.ini missing')
game = runtime.parent
external(args.cache_root, game)
external(args.backup_root, game)
if args.enable_rr and not args.rr_preset:
parser.error('--enable-rr requires --rr-preset D or E')
pins = (runtime / 'dll-pins.txt').read_text(encoding='utf-8-sig')
for kind, (_, _, _, expected) in PINS.items():
if not re.search(r'^\s*' + kind + r'\s+' + expected + r'\s+\S+\s+\S+\s*$', pins, re.M | re.I):
raise RuntimeError('Release does not recognize pinned ' + kind)
if args.enable_fsr and args.rr_preset:
parser.error('--enable-fsr and --rr-preset select different RR backends')
fsr = args.install_fsr or args.enable_fsr or ini_selects_fsr(runtime)
if fsr and not all((runtime / 'fsr' / name).is_file() for name in ('d4r_vkd3d_core.dll', 'amdxc64.dll')):
parser.error("FSR Ray Regeneration needs this release's d4r/fsr payload (d4r_vkd3d_core.dll, amdxc64.dll); extract the complete ZIP")
if not args.yes and input('Download pinned DLSS/RR 310.9.1 and NGX 617.14 (~945 MiB archive)? [Y/n] ').lower().startswith('n'):
return
result = transaction(runtime, collect(args.cache_root, args.offline, fsr), args.backup_root)
print(json.dumps(result, indent=2))
if args.enable_fsr:
backup = enable_fsr(runtime, args.backup_root)
print(json.dumps({'fsrBackendSelected': True, 'configBackup': str(backup) if backup else None}, indent=2))
if args.rr_preset:
print('DLL installation verified. Building optional private RR model; a model failure leaves verified DLLs installed.', flush=True)
install_model(runtime, args.cache_root, args.backup_root, args.rr_preset, args.enable_rr)
if __name__ == '__main__':
main()