Files
d4r/engine/compile_sr_e.py
T
woutermans 3f66081add engine: presets E/F AutoExposure, the game's exposure fields and NGX's jitter order
NGX's CNN takes this frame's exposure scale and pre-exposure in both translated kernels (input words 244-256,
reconstruction 592-604) and the previous frame's exposure as the input kernel's texture 352
(cuda_copy_exposure_kernel's half surface). The runtime left all of it at the model dump's values, so HDR games
with a pre-exposure other than 1 or a changing exposure measured 18-39 dB against NGX; now 54-64 dB.

AutoExposure (flag 0x40), which the shim refused for E/F: NGX runs K's log-mean measurement on M's grid every
second frame. exposure_k.comp MODE 3 writes it to the 1x1 image the kernels sample; sr/exposure_copy_e.comp
keeps the previous exposure. 60-66 dB over eleven frames at 960x540 -> 1920x1080, pre-exposure 19-21.

The reconstruction's jitter words 544-556 are (y, x); the runtime wrote (-x, -y), which matches only the
harness's first three jitters, so every later frame of a game's jitter sequence measured 26-28 dB. Twelve-frame
captures now measure 60-66 dB HDR and LDR; on the Steam Deck through the shim 51-67 dB against ZLUDA.

The D3D12 harness takes per-frame lists in D4R_HARNESS_PRE_EXPOSURE / _EXPOSURE_SCALE; d4r-e takes the
AutoExposure flag, per-frame exposure values and the game's exposure texture. E/F models need rebuilding
(the install's model cache key changes); older models keep the old binding and refuse AutoExposure.
2026-10-19 02:42:48 +02:00

214 lines
12 KiB
Python

#!/usr/bin/env python3
"""Build the engine's DLSS Super Resolution CNN model directory (presets E and F) from a one-frame dump.
usage: compile_sr_e.py REPLAY_DIR NVNGX_DLSS_DLL OUT_DIR --preset E|F [--ptx-dir DIR]
REPLAY_DIR: the replay dump (D4R_CUDA_REPLAY_DUMP_DIR) of one frame of preset E or F through NGX, any size, any
flags: its uploads hold the network's weights, which nvngx_dlss.dll keeps packed. The frame (NGX 310.7):
input kernel cuda_engine_input_kernel_rel_{ldr,hdr}_mvdiff_{mvhi,mvlo}: the game's textures (and the warped
history) to the network's 16 input channels per token (4 x 4 output pixels)
network eleven dltss_nchw8 kernels: d4r's own engine/sr/e_net.comp (packed binary16)
reconstruction dltss_nchw8_..._aniso_gaussian_dfn_{fastpath_3x3,mid_3x3,3x3_256,fastpath_5x5,5x5_256}: the last
decoder level, the 40-channel head and the reconstruction of the output and its history
The input and reconstruction kernels are translated from the DLL's PTX (engine/rr/ptx2rr.py) in every variant the
runtime selects from (engine/runtime_e.inc). The reconstruction head defaults to packed half FMAs for RDNA2,
like the rest of the network. --head-accumulation f32 retains the reference's per-MMA rounding; all translated
variants use aligned word accesses for their packed tensors. Writes:
e.txt the manifest
weights.bin the network's weights packed for e_net.comp, then the reconstruction kernels' four
head tensors as NGX uploads them (copied into the arena by the runtime)
<stage>.spv the network stages
input-<v>.spv / .globals.bin, final-<v>.spv / .globals.bin the translated kernels
input.args, final.args their argument blocks from the dump (the runtime rewrites every frame-dependent field)
exposure_e0.spv / _e1.spv NGX's AutoExposure measurement (engine/exposure_k.comp MODE 3)
exposure_copy_e.spv NGX's copy of the frame's exposure that the next frame reads (engine/sr/exposure_copy_e.comp)
Everything but d4r's shaders is derived from the user's DLL: it stays on their machine and is never packaged.
"""
import argparse
import glob
import json
import os
import re
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
import numpy as np
HERE = Path(__file__).resolve().parent
NET_SRC = HERE / 'sr' / 'e_net.comp'
# the network: name, the kernel's index in the frame's dltss launches, mode, defines (PS: pixel sets per lane, 2 on the
# large levels), the weights to pack
# (argument offset, layout, in, out) and the biases (argument offsets). Layouts: k3 = NGX's [K/8][tap][N][8],
# k1 = [K/8][N][8], oi = [N][K]; all packed to [tap][k][n] / [k][n] binary16.
NET = [
('enc0', 0, 'ENC0', dict(CIN=16, C=32), [(8, 'k3', 16, 32), (40, 'k1', 32, 64)], [88, 176]),
('enc1', 1, 'ENC3', dict(CIN=64, C=128, IN8=1), [(8, 'k3', 64, 128)], [56]),
('enc2', 2, 'ENC3', dict(CIN=128, C=128), [(8, 'k3', 128, 128)], [56]),
('enc3', 3, 'ENC3', dict(CIN=128, C=256), [(8, 'k3', 128, 256)], [56]),
('mid0', 4, 'CONV1', dict(CIN=256, C=512), [(8, 'k1', 256, 512)], [24]),
('mid1', 5, 'CONV1', dict(CIN=512, C=256), [(8, 'k1', 512, 256)], [24]),
('up4', 6, 'UP1', dict(CIN=256, C=256), [(16, 'k1', 256, 256)], [32]),
('red4', 7, 'CONV1', dict(CIN=256, C=128), [(8, 'k1', 256, 128)], [24]),
('dec3', 8, 'DEC2', dict(CIN=128, C=128, C2=128), [(16, 'k1', 128, 128), (240, 'oi', 128, 128)], [24, 248]),
('dec2', 9, 'DEC2', dict(CIN=128, C=128, C2=64, OUT8=1), [(16, 'k1', 128, 128), (240, 'oi', 128, 64)], [24, 248]),
('dec1', 10, 'DEC2', dict(CIN=64, C=64, C2=32, LOW8=1, SKIP8=1, OUT8=1), [(16, 'k1', 64, 64), (240, 'oi', 64, 32)],
[24, 248]),
]
# the reconstruction kernels' weight arguments (the last decoder level's two convs; NGX's layouts, kept as they are)
HEAD = [(16, 2048), (24, 64), (240, 2560), (248, 80)]
INPUT_VARIANTS = ['ldr_mvdiff_mvhi', 'ldr_mvdiff_mvlo', 'hdr_mvdiff_mvhi', 'hdr_mvdiff_mvlo']
FINAL_VARIANTS = ['fastpath_3x3_128', 'mid_3x3_128', '3x3_256', 'fastpath_5x5_256', '5x5_256']
INPUT_KERNEL = 'cuda_engine_input_kernel_rel_{}'
FINAL_KERNEL = 'dltss_nchw8_bilinear_upsample_conv_1x1_conv_1x1_aniso_gaussian_dfn_{}_032_032_048_032_e5m3_fp16_kernel'
# the translated kernels' argument contract: pointers (arena byte offsets), surfaces and their formats
INPUT_POINTERS, INPUT_SURFACES = [376], {328: 'rg16f', 368: 'rgba16f', 384: 'rgba16f'}
FINAL_POINTERS, FINAL_SURFACES = [0, 8, 16, 24, 240, 248, 256], {488: 'rgba16f', 496: 'rgba16f'}
class Launch:
def __init__(self, d):
self.dir = Path(d)
self.name = self.dir.name.split('-', 2)[2]
self.allocs, self.ptr = {}, {}
for line in (self.dir / 'manifest.txt').read_text().splitlines():
p = line.split()
if p and p[0] == 'alloc':
self.allocs[int(p[1])] = (int(p[2], 16), int(p[3]))
elif p and p[0] == 'pointer':
self.ptr[int(p[1])] = (int(p[2]), int(p[3]))
self.args = (self.dir / 'args.bin').read_bytes()
def arg(self, off, n):
a, d = self.ptr[off]
data = (self.dir / f'alloc-{a}.bin').read_bytes()
if d + n > len(data):
raise ValueError(f'{self.name}: argument {off} is shorter than {n} bytes')
return data[d:d + n]
def pack(raw, layout, cin, cout):
w = np.frombuffer(raw, np.float16)
if layout == 'k3':
return w.reshape(cin // 8, 9, cout, 8).transpose(1, 0, 3, 2).reshape(9 * cin, cout)
if layout == 'k1':
return w.reshape(cin // 8, cout, 8).transpose(0, 2, 1).reshape(cin, cout)
return w.reshape(cout, cin).T
def glsl(source, out, defines=()):
subprocess.run(['glslangValidator', '-V', '--target-env', 'vulkan1.3', *defines, str(source), '-o', str(out)],
check=True, stdout=subprocess.DEVNULL)
def find_module(ptx_dir, kernel):
found = [p for p in sorted(Path(ptx_dir).glob('*.ptx')) if f'.entry {kernel}(' in p.read_text(errors='replace')]
for p in found:
if '.target sm_89' in p.read_text(errors='replace')[:400]:
return p
if found:
return found[-1]
raise FileNotFoundError(f'no PTX module with the entry {kernel}')
def translate(ptx_dir, kernel, out, name, block, pointers, surfaces, packed=False):
comp = out / f'{name}.generated.comp'
cmd = [sys.executable, str(HERE / 'rr' / 'ptx2rr.py'), str(find_module(ptx_dir, kernel)), kernel, str(comp),
'--block', ','.join(map(str, block)), '--weights-arg', '999', '--unified-arena']
cmd += [f'--buffer={b}' for b in pointers] + [f'--surface={o}={f}' for o, f in surfaces.items()]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL)
glsl(comp, out / f'{name}.spv', ['-DD4R_WORD_ARENA', *(['-DD4R_MMA_PACKED'] if packed else [])])
g = comp.with_suffix('.globals.bin')
(out / f'{name}.globals.bin').write_bytes(g.read_bytes() if g.exists() else bytes(16))
info = json.loads(comp.with_suffix('.json').read_text())
for f in (comp, comp.with_suffix('.json'), g):
if f.exists():
f.unlink()
tex = ','.join(map(str, info['textures'])) or '-'
surf = ','.join(f'{o}:{f}' for o, f in info['surfaces']) or '-'
return f'{name}.spv {name}.globals.bin {tex} {surf}'
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('replay', type=Path)
ap.add_argument('dll')
ap.add_argument('output', type=Path)
ap.add_argument('--preset', choices=['E', 'F'], required=True, help='the preset the dump ran (the two differ in weights '
'and in two of the kernels\' constants)')
ap.add_argument('--ptx-dir')
ap.add_argument('--head-accumulation', choices=['f16', 'f32'], default='f16',
help='f16: packed half FMAs for the reconstruction head (RDNA2); f32: reference matrix accumulation')
a = ap.parse_args()
launches = [Launch(d) for d in sorted(glob.glob(str(a.replay / 'replay-*')))]
first = next((i for i, l in enumerate(launches) if l.name.startswith('cuda_engine_input_kernel_rel')), None)
if first is None:
raise SystemExit(f'{a.replay}: no preset E/F frame (cuda_engine_input_kernel_rel_*)')
net = [l for l in launches[first + 1:] if l.name.startswith('dltss_nchw8_')][:12]
if len(net) != 12 or 'aniso_gaussian' not in net[11].name:
raise SystemExit(f'{a.replay}: the frame does not hold the CNN network and its reconstruction')
out = a.output
out.mkdir(parents=True, exist_ok=True)
blob = bytearray()
def put(data):
at = len(blob)
blob.extend(data)
blob.extend(bytes(-len(blob) % 256))
return at
# the reconstruction's per-preset constant at 584 is k / (rx ry): k from the dump's own ratios
fin = net[11].args
ow0, oh0 = struct.unpack_from('<II', fin, 508)[0], struct.unpack_from('<I', fin, 504)[0]
rx0, ry0 = struct.unpack_from('<f', fin, 536)[0], struct.unpack_from('<f', fin, 528)[0]
k584 = struct.unpack_from('<f', fin, 584)[0] * rx0 * ry0
lines = ['D4RE0002', f'preset {5 if a.preset == "E" else 6}', f'kspread {k584:.9g}', 'weights weights.bin',
f'head_accumulation {a.head_accumulation}']
for name, index, mode, defines, ws, bs in NET:
l = net[index]
offs = []
for arg, layout, cin, cout in ws:
offs.append(put(np.ascontiguousarray(pack(l.arg(arg, cin * cout * (9 if layout == 'k3' else 1) * 2),
layout, cin, cout)).tobytes()))
cout_of = [ws[0][3]] + ([ws[1][3]] if len(ws) > 1 else [])
for arg, n in zip(bs, cout_of):
offs.append(put(l.arg(arg, n * 2)))
# word offsets in the order the parameter block takes them: w1 b1 w2 b2
wb = [offs[0] // 4, offs[len(ws)] // 4, (offs[1] // 4) if len(ws) > 1 else 0, (offs[len(ws) + 1] // 4) if len(bs) > 1 else 0]
glsl(NET_SRC, out / f'{name}.spv', [f'-DMODE_{mode}', *[f'-D{k}={v}' for k, v in defines.items()]])
lines.append(f'stage {name} {name}.spv ' + ' '.join(map(str, wb)) + f' {defines.get("PS", 1)}')
head = [put(net[11].arg(arg, n)) for arg, n in HEAD]
lines.append('head ' + ' '.join(f'{arg}:{at}:{n}' for (arg, n), at in zip(HEAD, head)))
(out / 'weights.bin').write_bytes(bytes(blob))
with tempfile.TemporaryDirectory() as tmp:
ptx = a.ptx_dir or tmp
if not a.ptx_dir:
subprocess.run([sys.executable, str(HERE.parent / 'kernels' / 'tools' / 'extract_dlss_ptx.py'), a.dll, tmp],
check=True, stdout=subprocess.DEVNULL)
for v in INPUT_VARIANTS:
lines.append(f'input {v} ' + translate(ptx, INPUT_KERNEL.format(v), out, f'input-{v}', (128, 1, 1),
INPUT_POINTERS, INPUT_SURFACES))
for v in FINAL_VARIANTS:
lines.append(f'final {v} ' + translate(ptx, FINAL_KERNEL.format(v), out, f'final-{v}', (256, 1, 1),
FINAL_POINTERS, FINAL_SURFACES, packed=a.head_accumulation == 'f16'))
# the argument blocks, pointers and texture handles cleared (the runtime fills them)
for name, l, size in (('input', launches[first], 408), ('final', net[11], 656)):
block = bytearray(l.args[:size].ljust(size, b'\0'))
for off in l.ptr:
block[off:off + 8] = bytes(8)
(out / f'{name}.args').write_bytes(bytes(block))
for p in '01':
glsl(HERE / 'exposure_k.comp', out / f'exposure_e{p}.spv', ['-DMODE=3', f'-DPASS={p}'])
glsl(HERE / 'sr' / 'exposure_copy_e.comp', out / 'exposure_copy_e.spv')
lines += ['template input input.args', 'template final final.args', 'exposure exposure_e0.spv exposure_e1.spv',
'exposure_copy exposure_copy_e.spv']
(out / 'e.txt').write_text('\n'.join(lines) + '\n')
print(f'Super Resolution CNN model in {out} ({len(blob)} weight bytes)')
if __name__ == '__main__':
main()