Files
d4r/engine/capture.py
T
woutermans e0d1549a21 Engine: native presets K and M in the D3D12 shim, wider game coverage
A standalone Vulkan inference engine (engine/) for DLSS presets K and M that
runs from weights extracted from the user's own nvngx_dlss.dll, with d4r's
own input, network and reconstruction shaders or the native HIP network
layers, recorded into the game's command list by the shim. Opt-in
([Engine] Enabled / D4R_ENGINE=1); anything uncovered falls back to the
CUDA/ZLUDA path with the reason logged.

Byte-identical to NGX's CUDA backend under the phase-capable reference
(1706x960 -> 2560x1440, native HIP network, eleven frames, RX 9070 XT):
- K render- and display-resolution motion vectors, HDR and LDR
- K game-supplied exposure and automatic exposure with any pre-exposure
  and exposure scale (one residual: effective exposure exactly 2 or 4)
- K and M with the DLSS indicator's inverted-axis parameters
- M with any exposure settings (NGX ignores them for M)
K packages need the current compile_k.py (marker D4RO0003).

Also: HIP shared buffers are pooled across feature recreation (ROCm leaks
destroyed imports), hipnet.bin must match the preset, M-only games do not
load the K model, install_d4r_runtime.sh stages k, k-ldr, m, m-ldr with
hip/, the activation table has a generator (gen_activation_m.py), and
package_release.sh can bundle the engine's shaders (never its weights).

Signed-off-by: woutermans <[email protected]>
2026-10-10 20:54:01 +02:00

131 lines
6.3 KiB
Python

"""Capture decoding shared by full-frame validation and model packaging.
Captures are reference data only. The runtime has no dependency on NGX, CUDA,
ZLUDA, PTX or the pointer addresses contained in a capture.
"""
from dataclasses import dataclass
from pathlib import Path
import re
import struct
import numpy as np
INPUT = 'hiluma_engine_input_depthinv_mvlo_hdr_v2_rel'
OUTPUT = 'hiluma_engine_output_depthinv_mvlo_hdr_max_v2_rel'
ORDER = ['enc0', 'enc1', 'enc2', 'enc3', 'enc4', 'dec5', 'dec4', 'dec3', 'dec2', 'dec1', 'dec0']
@dataclass
class Resource:
path: Path
width: int
height: int
channels: int
bits: int
def validate(self):
if self.path.stat().st_size != self.width * self.height * self.channels * self.bits // 8:
raise ValueError(f'truncated texture/surface: {self.path}')
class Launch:
def __init__(self, capture, directory):
self.capture, self.directory = Path(capture), Path(directory)
self.sequence = int(re.search(r'replay-(\d+)-', self.directory.name).group(1))
self.args = (self.directory / 'args.bin').read_bytes()
self.manifest = (self.directory / 'manifest.txt').read_text().splitlines()
self.kernel = self.manifest[0].split()[1]
self.resources = {}
for f in (self.capture / 'dump').glob(f'launch-{self.sequence:03d}-{self.kernel}-arg*'):
match = re.search(r'-arg(\d+)-(texture|surface)-(\d+)x(\d+)x(\d+)-fmt(\d+)\.bin$', f.name)
if match:
offset, kind, w, h, c, fmt = match.groups()
resource = Resource(f.resolve(), int(w), int(h), int(c), 32 if fmt == '32' else 16)
self.resources[int(offset)] = resource
def f2(self, offset): return struct.unpack_from('<2f', self.args, offset)
def i2(self, offset): return struct.unpack_from('<2i', self.args, offset)
def f1(self, offset): return struct.unpack_from('<f', self.args, offset)[0]
def h1(self, offset): return float(np.frombuffer(self.args, np.float16, 1, offset)[0])
def exposure(self, offset):
self.resources[offset].validate()
value = float(np.fromfile(self.resources[offset].path, np.float16, 1)[0])
return value if value != 0 else 1.0
def buffer(self, offset, size):
pointer = struct.unpack_from('<Q', self.args, offset)[0]
# Post-launch allocation captures include each entire allocation, not just this argument's slice.
for path in (self.capture / 'dump').glob(f'launch-{self.sequence:03d}-{self.kernel}-arg*-buffer-*'):
base = int(re.search(r'-base0x([0-9a-f]+)', path.name).group(1), 16)
delta = pointer - base
if 0 <= delta and delta + size <= path.stat().st_size:
with path.open('rb') as stream:
stream.seek(delta)
return stream.read(size)
for line in self.manifest:
fields = line.split()
if fields[0] == 'pointer' and int(fields[1]) == offset:
path = self.directory / f'alloc-{fields[2]}.bin'
if path.exists():
with path.open('rb') as stream:
stream.seek(int(fields[3])); data = stream.read(size)
if len(data) == size: return data
raise ValueError(f'{self.directory}: missing {size}-byte buffer at argument {offset}')
def buffer_location(self, offset):
"""(file holding the whole allocation, byte offset of the argument's pointer within it)"""
pointer = struct.unpack_from('<Q', self.args, offset)[0]
for path in (self.capture / 'dump').glob(f'launch-{self.sequence:03d}-{self.kernel}-arg*-buffer-*'):
base = int(re.search(r'-base0x([0-9a-f]+)', path.name).group(1), 16)
if 0 <= pointer - base < path.stat().st_size:
return path, pointer - base
for line in self.manifest:
fields = line.split()
if fields[0] == 'pointer' and int(fields[1]) == offset and (self.directory / f'alloc-{fields[2]}.bin').exists():
return self.directory / f'alloc-{fields[2]}.bin', int(fields[3])
raise ValueError(f'{self.directory}: missing buffer at argument {offset}')
def buffer_offset(self, offset):
return self.buffer_location(offset)[1]
def allocation(self, offset, size):
data = self.buffer_location(offset)[0].read_bytes()
if len(data) != size:
raise ValueError(f'{self.directory}: the allocation at argument {offset} has {len(data)} bytes, expected {size}')
return data
def launches(capture, kernel):
return [Launch(capture, d) for d in sorted((Path(capture) / 'replay').glob('replay-*-'+kernel))]
def input_params(p):
f = np.float32
a, b, c = (f(p.exposure(o)) for o in (208, 200, 216))
f152, f158 = f(p.f1(144)) * a, f(p.f1(144)) * b
expo = f(p.f1(156)) * f158
if expo == 0: expo = f(1)
expo_in, ratio = f(p.f1(148)) * c / f158, f158 / f152
blob = struct.pack('<14f', *p.f2(40), *p.f2(24), *p.f2(8), *p.f2(16), *p.f2(120), *p.f2(136), *p.f2(128))
blob += struct.pack('<12i', *p.i2(72), *p.i2(80), *p.i2(88), *p.i2(96), *p.i2(104), *p.i2(112))
blob += struct.pack('<iI4f', struct.unpack_from('<i', p.args, 64)[0], p.args[32], float(expo), float(expo_in), float(ratio), p.f1(160))
assert len(blob) == 128
return blob
def output_params(p):
if any(p.args[89:92]):
raise ValueError('native K supports standard HDR output only (flag bytes 89..91 must be zero)')
f = np.float32
a, b = f(p.exposure(264)), f(p.exposure(256))
s160, s168 = f(p.f1(160)), f(p.f1(168))
expo = s168 * (s160 * b)
if expo == 0: expo = f(1)
ratio = (s160 * b) / (s160 * a)
blob = struct.pack('<18f', *p.f2(40), *p.f2(16), *p.f2(56), *p.f2(48), *p.f2(64), *p.f2(0), *p.f2(8), *p.f2(144), *p.f2(152))
blob += struct.pack('<16i', *p.i2(208), *p.i2(216), *p.i2(96), *p.i2(104), *p.i2(112), *p.i2(120), *p.i2(128), *p.i2(136))
# flags: reset, and in bits 8..11 the reconstruction table the launch pointed at within NGX's sixteen
blob += struct.pack('<2iI', *p.i2(80), p.args[32] | (p.buffer_offset(280) // 32768) << 8)
blob += struct.pack('<10f', float(expo), float(ratio), *struct.unpack_from('<3f', p.args, 172), p.h1(188), p.h1(190), p.h1(192), p.h1(184), p.h1(186))
assert len(blob) == 188
return blob