engine/rr: native Ray Reconstruction stages with capture/replay and shim integration
- engine/rr: d4r's own shaders for the denoiser frame (exposure, six encoders,
five decoders, hkpn output stage) at NGX layouts, plus PTX->GLSL/HIP packing
scripts, reference implementations and weight extraction
- runtime.h/runtime_rr.inc, game_rr.{h,cpp}: GameDenoiser wired into the engine
build (d4r-rr replay binary) and the D3D12 shim's feature-13 path
- shim: answer GetFeatureRequirements from d4r's own state only (the old
cross-binary CUDA handoff crashed OptiScaler's scan); drop the core's
CUDA GetFeatureRequirements import
- shim/harness: exact guide capture/replay of a frame's RR inputs
- kernels/rr: expanded enc0/engine prototypes; kernels/tools: ptxsim rewrite
plus rrsim and an engine test; vulkan: ptx2glsl FP8/cooperative-matrix
support, kvk inout buffers and KVK_DISABLE_OPTIMIZATION
- bridge: report the surface array in cuSurfObjectCreate traces
- packaging: RR settings and the ICU launch overrides in D4R_README
This commit is contained in:
1 parent
30472ae195
commit
50fa65d87b
67 files changed
+11926
-504
No files matched your search
@@ -0,0 +1,156 @@
|
||||
"""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)
|
||||
|
||||
@property
|
||||
def weights(self):
|
||||
"""the weight image (argument 40) as NGX uploaded it; 16 zero bytes for a stage without one (the output stage)"""
|
||||
pointer = next((p for p in self.pointers if p.offset == 40), None)
|
||||
return self.pre(pointer.alloc) if pointer is not None else bytes(16)
|
||||
|
||||
@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 if p.offset != 40]
|
||||
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]
|
||||
Reference in new issue
Block a user