Files
d4r/engine/rr/rr_capture.py
woutermans 8eb7a94533 engine/rr: native preset F (DLSS 4.5 Ray Reconstruction) and the reference's fused roundings
- engine/compile_rr2.py, runtime_rr2.inc, engine/rr/rr2_m.comp, exposure_f.comp,
  gen_rr2_enc0.py, rr2_pack.py, rr2_ref.py: the rr2_* model (nvngx_dlssd.dll 310.9's
  third network) as d4r's own shaders - exposure, enc0 gather/block split, enc1-5 and
  dec4-0 through one matrix shader, translated output passes; weights packed from the
  DLL, argument blocks from the capture
- ptx2rr: match the driver's fused roundings - opaque-zero OR around cvt.rn.f16.f32
  against v_fma_mix, half-away-from-zero formatted stores, UNORM 10:10:10:2 as RGBA32F
  of q/1023, OCML tanhF with the AMDGPU exp lowering, binary16 fma and fetch roundings;
  --probe for register bisection against ZLUDA
- dec0_m/swin_m/enc0_*: RR_MIX build option (D4R_RR_MIX=1), one v_fma_mix rounding per
  32-deep step, ~5% faster, not bit-exact; default build stays exact
- runtime_rr.inc: RModel/REngine dispatch to the D4RR0002 model; replay_rr: RGBA32F and
  A2B10G10R1010 texel sizes, roughnessPacked bit, larger debug staging
- shim: engine\rr-f model slot, 310.9 version probe so preset F is only claimed when the
  CUDA path has it; d4r.ini / d4r_config.py Model = F; run_rr_engine_compare.sh D4R_RR_DLL
- kernels/rr/swin_native.h + rr_swin_native_test.cpp: HIP port of encoder 1 for the
  FP8-product and rounding cost measurements
- engine/rr/README: preset F's block, options, capture-by-capture results, timings and
  the open camera-matrix difference
2026-10-13 13:59:17 +02:00

161 lines
6.4 KiB
Python

"""Ray Reconstruction captures: the launches of one denoiser frame as the CUDA bridge recorded them.
A capture directory holds what the bridge writes with
D4R_CUDA_VERBOSE=1 D4R_CUDA_REPLAY_DUMP_DIR=CAP/replay D4R_CUDA_REPLAY_DUMP_FILTER=cuda_dldn
D4R_CUDA_LAUNCH_STATS=1 D4R_CUDA_LAUNCH_STATS_FILTER=cuda_dldn D4R_CUDA_LAUNCH_DUMP_DIR=CAP/dump
plus the run's log as CAP/run/run.log (texture sampler settings and array shapes come from the verbose
cuTexObjectCreate / cuSurfObjectCreate / cuArrayCreate lines).
replay/replay-NNNNNN-KERNEL/ args.bin, manifest.txt and every buffer the launch points into, *before* it ran
dump/launch-NNN-KERNEL-argOOO-{buffer,texture,surface}-... every resource *after* the launch ran
Captures are reference data only: nothing at runtime reads them.
"""
from dataclasses import dataclass, field
from pathlib import Path
import re
import struct
# CUarray_format codes the denoiser uses, and how a Vulkan image of the same texels is described
# (kvk's BITS column: 8 = UNORM bytes, 9 = SNORM bytes, 16 = binary16, 32 = binary32)
ARRAY_FORMATS = {16: 16, 32: 32, 1: 8, 200: 9, 194: 8}
@dataclass
class Image:
offset: int
kind: str # 'texture' or 'surface'
width: int
height: int
channels: int
format: int # CUarray_format
path: Path # post-launch contents
linear: bool = False # textures: bilinear filtering
@property
def bits(self):
return ARRAY_FORMATS[self.format]
@property
def texel_bytes(self):
return self.channels * (1 if self.bits in (8, 9) else self.bits // 8)
@dataclass
class Pointer:
offset: int
alloc: int
delta: int
@dataclass
class Launch:
sequence: int
kernel: str
directory: Path
grid: tuple
block: tuple
args: bytes
allocs: dict = field(default_factory=dict) # index -> (base, size)
pointers: list = field(default_factory=list)
images: list = field(default_factory=list)
def pre(self, alloc):
"""an allocation's contents before the launch"""
return (self.directory / f'alloc-{alloc}.bin').read_bytes()
def pointer(self, offset):
return next(p for p in self.pointers if p.offset == offset)
def image(self, offset):
return next(i for i in self.images if i.offset == offset)
def weights_at(self, offset):
"""the weight image (the pointer at argument OFFSET) as NGX uploaded it; 16 zero bytes for a stage without one"""
pointer = next((p for p in self.pointers if p.offset == offset), None)
return self.pre(pointer.alloc) if pointer is not None else bytes(16)
@property
def weights(self):
"""the DLSS-D kernels' weight image (argument 40)"""
return self.weights_at(40)
@property
def arena_alloc(self):
"""the plane arena: the allocation every arena pointer of the layer points into"""
candidates = [p.alloc for p in self.pointers]
return max(set(candidates), key=lambda a: self.allocs[a][1]) if candidates else None
def post_buffer(self, capture, alloc):
base = self.allocs[alloc][0]
for path in (capture.root / 'dump').glob(f'launch-{self.sequence:03d}-{self.kernel}-arg*-buffer-*'):
m = re.search(r'-base0x([0-9a-f]+)\+', path.name)
if m and int(m.group(1), 16) == base:
return path.read_bytes()
raise FileNotFoundError(f'no post-launch dump of allocation {alloc} for launch {self.sequence}')
def u32(self, offset):
return struct.unpack_from('<I', self.args, offset)[0]
def f32(self, offset):
return struct.unpack_from('<f', self.args, offset)[0]
class Capture:
def __init__(self, root):
self.root = Path(root)
log_path = self.root / 'run' / 'run.log'
self.log = log_path.read_text(errors='replace') if log_path.exists() else ''
# (log position, texture handle, filter mode) of every texture object creation, in order
self._tex_events = [(m.start(), int(m.group(2), 16), int(m.group(1))) for m in re.finditer(
r'cuTexObjectCreate .*? filter=(\d+) .*? object=(0x[0-9a-f]+)', self.log)]
self.launches = []
for d in sorted((self.root / 'replay').iterdir()):
self.launches.append(self._launch(d))
def filter_at(self, handle, position):
value = None
for pos, h, f in self._tex_events:
if pos > position:
break
if h == handle:
value = f
return value
def _launch(self, directory):
manifest = (directory / 'manifest.txt').read_text().split('\n')
kernel = manifest[0].split()[1]
launch = [int(x) for x in manifest[1].split()[1:]]
sequence = int(re.search(r'replay-(\d+)-', directory.name).group(1))
args = (directory / 'args.bin').read_bytes()
result = Launch(sequence, kernel, directory, tuple(launch[0:3]), tuple(launch[3:6]), args)
for line in manifest[2:]:
f = line.split()
if not f:
continue
if f[0] == 'alloc':
result.allocs[int(f[1])] = (int(f[2], 16), int(f[3]))
elif f[0] == 'pointer':
result.pointers.append(Pointer(int(f[1]), int(f[2]), int(f[3])))
# the log position of this launch, so a reused texture handle resolves to its settings at the time
position = self.log.find(f'launch[{sequence}] {kernel}')
if position < 0:
position = len(self.log)
for path in sorted((self.root / 'dump').glob(f'launch-{sequence:03d}-{kernel}-arg*')):
m = re.search(r'-arg(\d+)-(texture|surface)-(\d+)x(\d+)x(\d+)-fmt(-?\d+)\.bin$', path.name)
if not m:
continue
offset = int(m.group(1))
image = Image(offset, m.group(2), int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6)), path)
if image.kind == 'texture':
handle = struct.unpack_from('<Q', args, offset)[0]
image.linear = self.filter_at(handle, position) == 1
result.images.append(image)
result.images.sort(key=lambda i: (i.kind != 'texture', i.offset))
return result
def find(self, kernel_part, frame=None):
"""the launches whose kernel name contains KERNEL_PART, in order; FRAME (1-based) selects one"""
found = [l for l in self.launches if kernel_part in l.kernel]
return found if frame is None else found[frame - 1]