Files
d4r/engine/rr
woutermans af450a92b9 engine: cut the models' and the engines' memory
- the weights go into the command buffer with vkCmdUpdateBuffer (recordContents); the host-visible staging
  copies are gone, and the host copies are dropped once recorded
- an engine's own output image is created only for frames that do not write the caller's texture
  (K, M, RR D/E, RR F), cleared on first use
- preset F: the tile slots share the arena with the network's regions, and the gather records' rows fill the
  gaps between encoder 0's regions (rr.txt "s3", six words on enc0's block; D4R_ENGINE_RR_ARENA keeps the
  old separate layout for arena dumps)
- the shim reserves the render size the feature was created with for the M/L Vulkan network instead of the
  output size; a frame beyond it goes to a second, output-sized engine (1.1 GB less for M, 1.4 for L at
  1280x720 -> 4K); replay_m gains D4R_M_CAPACITY

Every reference capture still byte-identical; through the shim the harness process peaks 159 MiB lower.
2026-10-14 15:14:28 +02:00
..

Ray Reconstruction stages (DLSS-D, NGX feature 13)

d4r's own Vulkan compute shaders for the denoiser in nvngx_dlssd.dll 310.7 (presets D and E). One shader per NVIDIA kernel of the frame; the engine records them in NVIDIA's order:

stage NVIDIA kernel shader grid (640x360 render, 1280x720 output)
exposure cuda_dldn_engine_luma_convert_kernel, reduce_sum x2-3, auto_exposure_copy_kernel engine/exposure_k.comp (MODE 1)
enc0 cuda_dldn_engine_swin_enc0_kernel (D: ..._enc0_diamond_wallaby_kernel) enc0.comp 81x49, 32 threads
enc1-enc5 cuda_dldn_engine_swin_enc1_kernel ... enc5 enc1.comp ... enc5.comp
dec4-dec1 cuda_dldn_engine_swin_dec4_kernel ... dec1 dec4.comp ... dec1.comp
dec0 cuda_dldn_engine_swin_dec0_kernel (D: ..._dec0_diamond_wallaby_kernel) dec0.comp 81x49, 32 threads
output cuda_dldn_engine_hkpn_output_kernel_transformer (D: ..._diamond_wallaby) hkpn.comp 80x45, 16x16 threads

The D variants of enc0, dec0 and the output kernel are different code; the other nine kernels are shared and only their weights differ between presets.

Stage contract

Every stage shader reads the same data NVIDIA's kernel reads, in the same layouts, so each one can be checked on its own against a captured launch (test_layer.py) and the stages chain exactly like NGX's kernels:

  • binding 0: the parameter block, std430 readonly. It is NVIDIA's argument block byte for byte, except that every pointer into the plane arena holds its byte offset in the arena (a 64-bit value), the weight pointer is zero, and a texture or surface handle is 1 where NGX bound one and 0 where it left the argument null (the kernels test optional inputs' handles for null). Fields are read at NVIDIA's argument offsets.
  • binding 1: the layer's weight image exactly as NGX uploads it (argument 40's allocation), std430 readonly.
  • binding 2: the plane arena, std430 read/write.
  • bindings 3, 4, ...: the textures in ascending argument offset (combined image samplers, clamp-to-edge, normalized coordinates, linear or nearest as NVIDIA's texture object; 8-bit unsigned arrays are sampled as UNORM, SNORM_INT8X4 as SNORM), then the surfaces in ascending argument offset (storage images, rgba16f or rgba8_snorm).
  • The dispatch grid is NVIDIA's launch grid. The workgroup size is the shader's choice.

Checking a stage

Captures (bridge launch and replay dumps of a harness run, see rr_capture.py) live outside the repository.

glslangValidator -V --target-env vulkan1.3 engine/rr/enc1.comp -o build/rr/enc1.spv
python3 engine/rr/test_layer.py CAPTURE swin_enc1 2 build/rr/enc1.spv build/rr/work-enc1 [--time 50]

It starts from the arena as it was before NVIDIA's launch, with zeroed surfaces, and compares every surface and the whole arena with what NVIDIA's launch left. build/rr/kvk is vulkan/kvk.cpp (g++ -O2 -std=c++17 -o build/rr/kvk vulkan/kvk.cpp -lvulkan).

Native stages (preset E)

build_native.py CAPTURE MODEL_DIR turns a compile_rr.py model directory into one where every Swin stage is d4r's own cooperative-matrix shader instead of translated PTX. Each was derived phase by phase from ZLUDA replays (phase_dump.py: every mma's operands of one window) into an exact numpy reference, then written against it:

stages reference shader weights
enc1-enc5, dec4-dec1 swin_ref.py (one generic model: C, heads, merge / expand) swin_m.comp swin_pack.py
dec0 dec0_ref.py dec0_m.comp dec0_pack.py
enc0 enc0_ref.py (after the per-pixel features) enc0_m.comp from gen_enc0_m.py enc0_pack.py

enc1_m.comp / dec1_m.comp are the hand-written C = 64 instances swin_m.comp generalises; enc1_ref.py holds the shared numerics (inter, mm, the cubic softmax, the norm trees). enc0 keeps ptx2rr's translation of NVIDIA's feature gather (waves 0-2 run its pixel loop) and is native from the embedding on.

Binding 1 differs from the stage contract above: it holds the packed weights (enc0: NGX's image, then the packed blob). Everything else is the contract, so test_layer.py --weights FILE checks a native stage the same way. Against capE3 every native stage leaves the arena byte-identical to NVIDIA's; surfaces differ in a few values by one binary16 ulp (exp2 approximation, and decoder texture 88 where NVIDIA's bilinear unit rounds differently), and the whole network's output is ~96 dB from NVIDIA's.

The shaders run every product transposed (weights x activations) so results feed the next product in registers; enc1_m.comp's header explains the layouts and rr_exact.glsl the binary16 rounding RADV needs (it folds float(float16_t(x)) and does not fuse f16 fma).

Native models and preset D

engine/rr/build_native_model.sh CAPTURE OUT_DIR [NVNGX_DLSSD_DLL] builds a model whose eleven network stages are the native shaders (swin_m.comp, dec0_m.comp, the generated enc0_m.comp) for the preset of the capture, with the translated output stage.

The weights are read from the user's DLL itself (dll_weights.py), not from the capture. The DLL holds each model's tensors as plain binary16 arrays in natural [out][in] order, stage after stage; nothing names them, so they are found by structure: every attention head has a 100 x 100 relative position bias table whose entries depend only on the offset between two window positions, and a model is eleven consecutive stages with 2 2 4 4 8 8 8 4 4 2 2 heads. What NGX uploads is the same data rearranged: FP8 codes in the kernels' fragment order, the norm gains times sqrt(C), the bias times 2 sqrt(2) for the kernels' 64 x 96 query / key order, and (preset E) the 4096-byte noise table encoder 0 dithers with. dll_weights.py DLL OUT --model N --check DIR compares a reading with a phase-dump extraction: for 310.7 every tensor of both presets is identical, and so are the packed weight files. The capture only selects which of the DLL's models it ran (and supplies the two argument-block templates); a DLL whose network kernels are not the ones the shaders were written against (native_kernels.txt, SHA-256 of the PTX modules) is refused unless D4R_RR_ANY_KERNELS=1.

compile_rr.py uses the translated output kernel for both presets: engine/rr/hkpn.comp has the reset path only (--reset-only selects it for captures that reset every frame).

Preset D (..._diamond_wallaby kernels) differs from E in three places:

  • enc0: another feature gather (kept as the translation, like E's) with 30 input features for E's 32, and no noise table (its weight image is 4096 bytes smaller).
  • dec0: the eight bytes per pixel for the output stage are channels 12, 13, 0, 18, 19, 20, 21, 0 (-DRR_PRESET_D; E: 12, 13, 18-23). Everything else is E's kernel.
  • output stage: its own kernel (closest-depth motion from texture 296, a 4x4 bicubic history fetch). It is the translated stage; it has no subgroup operations and runs with 64-wide waves (rr.txt).

The reset flag (enc0 byte 365, output stage byte 440) follows Reset, not the jitter. With Reset = 0 the output stage reprojects and blends its history: check stages against a temporal capture (the harness with D4R_HARNESS_JITTER_SCENE=1, and D4R_CUDA_REPLAY_DUMP_LIMIT=400 so every launch is saved), not only against captures that reset every frame. engine/rr/hkpn.comp implements the reset path only.

Preset D, RX 9070 XT, RADV 26.2.4, 2026-10-12, 1280x720 -> 3840x2160 (d4r-rr, D4R_RR_TIME_REPEAT=20): 5.0-5.2 ms per frame (network 4.5, output 0.55-0.65), from 17.5 ms with every stage translated. Every network stage reproduces the capture's arena byte for byte on reset and temporal frames. The presented image against the CUDA path over temporal frames: 96-106 dB on the reset frame, then 41-52 dB (one-ulp differences in the filtered surfaces are amplified by the recurrent feature; twelve frames stay at 41-45 dB).

RE Requiem (2026-10-12, 1280x720 -> 3840x2160, R9G9B9E5 colour, roughness packed in the normals' w): with hkpn.comp the picture was 3-pixel blocks that shook with the jitter (no history); with the translated output stage presets D and E both give a stable, reconstructed image on the engine. Not compared with the CUDA path in that game.

Matching the reference bit for bit (2026-10-12)

The reference is the CUDA path through ZLUDA on the same GPU, so its rcp.approx, ex2.approx and bilinear texture fetches are the GPU's own instructions and texture unit, not correctly rounded arithmetic. Two places in the native filters had been "corrected" to exact arithmetic and were one ulp off in ~0.01-0.2% of the filtered surfaces:

  • rcp16 (the filters' softmax normalisation and gate) is now the plain hardware reciprocal;
  • the decoders' texture 88 goes through the bilinear sampler at NVIDIA's coordinates (rr_decfilter.glsl, dec0_m.comp) instead of a hand-computed 1/4, 3/4 blend.

With both, every network stage (arena and surfaces 112 / 120 / 128 / 144 / 304) is identical to the capture on every frame of capDT12, capD4K, capD1080 (preset D) and capE3 (preset E). What remains is the translated output stage: ~0.004% of surface 224 / 240 one ulp off (single channels, values just above 0.5; cause not found). The output is 107-112 dB on frames where the network's inputs are still exact. At 640x360 -> 1080p that holds for all twelve frames of capDT12 (was 96 dB, then 41-45); at 1280x720 and 1920x1080 -> 4K the history's differing values reach encoder 0 through texture 496 and the output falls to 52 / 45 / 43 dB (capD4K), 53 / 49 dB (capD1080) as before.

Speed: the compiled stages spend about half their instructions on float conversions. spv_exact_convert.py marks the conversions NoContraction so RADV keeps a binary16 rounding without the opaque OR (RR_EXACT_CONVERT), which is bit-exact and ~8% faster: preset D 4.8 ms at 1280x720 -> 4K, 9.5 ms at 1920x1080 -> 4K (was 5.1 / 10.3). D4R_RR_RELAXED=1|2 builds drop NVIDIA's double rounding instead (8.2 / 7.6 ms at 1080p) but are not exact: about 1% of a layer's FP8 codes change and the output is 39-44 dB from the reference on every frame.

Profile of a full-resolution stage at 1080p (dec0, 3.0 ms): expand 0.31, key norm 0.16, attention 1.37 (per head: projections 0.15, scores + softmax 0.26, P V 0.17, out-projection 0.10), MLP 0.69, output filter 0.50. The 136 matrix products themselves are 0.45-0.5 ms; weight and table memory traffic is under 8%. enc0's translated feature gather is 0.28 of its 3.0 ms.

Optional inputs: specular hit distance and disocclusion mask (2026-10-12)

RE Requiem passes DLSSD.SpecularHitDistance (R32_FLOAT) and DLSS.DisocclusionMask (R16_FLOAT). Harness captures with synthetic ones (D4R_HARNESS_RR_EXTRA=hit,mask; capDX, capDXhit, capDXmask) show the wiring:

input encoder 0 texture encoder 0 flag byte output stage
specular hit distance argument 248 536 flag byte 160
disocclusion mask argument 296 539 -

Encoder 0 also tests the handle at argument 248 for null before it takes the hit distance path (it writes a reprojected specular motion into channels 2 and 3 of surface 304), which is why the parameter block now carries the handles' presence. With both bound the network is identical to the capture on every frame (capDX). The output stage then differs from the CUDA path on the image's border pixels only (63-67 dB on temporal frames): NVIDIA's kernel fetches neighbours outside the image there, CUDA clamps such integer fetches to the edge (every denoiser texture's address mode is clamp; the translation and rrsim do the same), and ZLUDA's image load returns zero. With zero outside the image the stage matches the capture like before (99.997%); the engine keeps CUDA's clamp.

Exact speed-ups since the first native build, all bit-identical per stage: conversions kept by NoContraction instead of an opaque OR, the MLP activation and its FP8 conversion from a 64 KiB table (enc1_pack.act_table), FP8 conversions in pairs, no saturation clamp on attention probabilities. Preset D: 4.4 ms at 1280x720 -> 4K, 8.6 ms at 1920x1080 -> 4K.

Output stage, hypotheses tested for its remaining ~0.004% one-ulp values and rejected (2026-10-12): correctly rounded exp2 / reciprocal (rrsim's exact arithmetic is further from the capture than the GPU), fusing the kernel's 34 single-use multiply-subtract pairs into fma, merging the two constant multiplications after the log transform, precise on the inlined exp2, NoContraction on its conversions. The differing values come in spatial clusters, one colour channel at a time, always one ulp, mostly just above 0.5 in surface 224.

HIP against Vulkan, and the fused rounding (2026-10-12)

Encoder 1 ported line for line to a standalone HIP kernel (kernels/rr/swin_native.h, runner kernels/tools/rr_swin_native_test.cpp; capD1080 frame 2, 5 launches per round): 835 us against the Vulkan stage's 770-788 us, the arena byte-identical. A HIP port of the network is not worth making. What the port did show:

  • The FP8 product alone costs 0.61 us per product per wave on this grid, 2.07 us when each is followed by the f32 -> binary16 -> f32 rounding of its eight accumulator registers. Products and that rounding are about half of a stage on either backend.
  • Keeping the accumulator as binary16 and taking f16(P + acc) with v_fma_mix (as kernels/m does) halves the rounding's instructions: HIP 780 us. RADV emits the same instruction for float16_t(p + float(acc)), so the Vulkan stages have it as a build option: D4R_RR_MIX=1 build_native_model.sh ... (RR_MIX, the shaders' K32).

RR_MIX is one rounding of the exact sum where the reference has two, so it is not bit-exact by construction. Preset D: every network stage still identical to the capture on all 21 frames of capDT12 / capD4K / capD1080 / capDX / capDXcp / capDXhw (the same figures as the default build), 4.22 ms at 1280x720 -> 4K and 8.3 ms at 1920x1080 -> 4K (default 4.45 / 8.7). Preset E (capE3): frames 1 and 3 have a value of encoder 0 one step off (127 dB), which the later stages amplify to 41 dB at the output; frame 2 is identical. The default build stays the exact one.

Cyberpunk 2077: more inputs and settings (2026-10-12)

Found on harness captures (D4R_HARNESS_RR_EXTRA=particles,sss, D4R_HARNESS_RR_HW_DEPTH=1, D4R_RR_PRESET=N):

what the game passes where NGX puts it
DLSSD.ScreenSpaceSubsurfaceScatteringGuide (R16F) encoder 0 texture 264 (its handle is the only switch)
DLSSD.ColorBeforeParticles (RGBA16F) output stage texture 416, flag byte 432, and a second output-size history: surface 384, read back next frame as texture 392 (linear)
DLSS.Use.HW.Depth = 1 encoder 0 byte 980, output stage byte 443
preset hint 5 runs preset E; every other value (0-4, 6, 0x80000002, 0x80000005) runs preset D

Integer texture fetches outside the image return zero (fetch0), which is what the reference does; CUDA would clamp. With Cyberpunk's set (hit distance, particles, subsurface guide, hardware depth) the engine's presented image is byte-identical to the CUDA path through the shim on the frames the harness can compare (640x360 -> 1920x1080), and with RE Requiem's set the network is identical on every frame (output 107-109 dB). At 1280x720 and 1920x1080 -> 4K without these inputs the output still falls to 43-53 dB after the first frame (the output stage's one-ulp values); a correctly rounded log2 there matches the capture worse than the GPU instruction does.

The output stage's one-step differences: fused multiply-convert (2026-10-13)

The rare one-binary16-step values of the translated output stage (above: ~0.004% of surfaces 224 / 240, "cause not found") were the driver fusing an f32 product (or sum) with the conversion that follows it into v_fma_mix, which rounds the exact result once. The kernel rounds twice, to f32 and then to binary16, and the two differ when the f32 result is exactly a binary16 midpoint, as log2(x) * c1 * c2 of the output stage's tone curve sometimes is. ptx2rr.py now ORs an opaque zero into the f32 bits before every cvt.rn.f16.f32 (and into the binary16 bits after, against the fold of float(float16_t(x))). With it presets D and E are byte-identical to the CUDA path on every frame of every capture (capDT12, capD4K, capD1080, capDX, capDXcp, capDXhw, capE3; history and output), at +0.05 ms: 4.27 / 4.45 ms at 1280x720 -> 4K. Found with build/rr-style bisection: probe every register of a block through ZLUDA (debug stores added to the PTX, zluda_replay.py) and through the translated shader (ptx2rr.py --probe) and list the first that differ.

Other reference behaviours the translation now follows, all found the same way on preset F's kernels:

  • formatted stores to normalized surfaces round half away from zero in f32 (ZLUDA's surface_formatted_store_bits); UNORM 10:10:10:2 arrays are RGBA32F images holding the quantised values q / 1023 (the correctly rounded quotient, from a table: the driver divides by multiplying with a reciprocal);
  • tanh.approx.f32 is OCML's tanhF with LLVM's AMDGPU lowering of exp (lowerFEXP, fast-FMA path): the same operations, the fused ones through f64 (the driver's f32 fma is a product and a sum whenever the product also exists on its own); tanh.approx.f16 is the correctly rounded value (a table);
  • the driver's GLSL fma on binary16 is a true fused operation, as the reference's is: no emulation there;
  • texture fetches with a binary16 result keep that rounding.

Preset F: DLSS 4.5 Ray Reconstruction (2026-10-13)

nvngx_dlssd.dll 310.9 has a third model with its own kernels, rr2_*, and runs it for every preset hint but 4 (D) and 5 (E): none, 6, Cyberpunk 2077's 0x80000002. The engine runs it from a D4RR0002 model:

python3 kernels/tools/extract_dlss_ptx.py NVNGX_DLSSD_DLL ~/.cache/d4r-rr-corpus-f       # its PTX (zstd in 310.9)
python3 engine/compile_rr2.py CAPTURE OUT_DIR --native NVNGX_DLSSD_DLL [--particles CAPTURE_WITH_PARTICLES]

The runtime is engine/runtime_rr2.inc (behind RModel / REngine); the shim uses engine\rr-f (or D4R_ENGINE_MODEL_RR_F_DIR) and, when that model exists, NGX 310.9's hint mapping.

A frame, as NGX launches it:

pass NVIDIA kernel engine
exposure rr2_histogram_auto_exposure_basic_kernel exposure_f.comp (two passes)
enc0 rr2_enc0_kernel translated feature gather and guide tail around the native block (gen_rr2_enc0.py)
enc1 - enc5 rr2_enc1_kernel ... rr2_m.comp
dec4 - dec1 rr2_decN_kernel_0, rr2_decN_kernel_1 rr2_m.comp
dec0 rr2_dec0_kernel_0, rr2_dec0_kernel_1 rr2_m.comp; the second: native block, translated output tail
output rr2_post_kernel, rr2_downsample_kernel_static_hdr[_no_transparency] per tile, rr2_pooled_history2_kernel translated (ptx2rr.py)

The network (rr2_ref.py is its exact numpy reference and documents the block): D/E's levels, window grids and arithmetic (FP8 operands, binary16 sums rounded per 32-deep step, the cubic softmax, the MLP activation), with a different block. Each kernel is one block on 8x8-token windows that alternate their shift per frame: heads that share one projection for queries and keys (so the scores are symmetric; bias (T + T^T) sqrt(2) of a 64 x 64 relative position table), all reading the block's input and summing their out-projections; MLP branches likewise; both sums enter the residual times a binary16 scalar from the argument block. Encoders merge 2x2 for the next level, a decoder's first kernel expands the level below and adds the skip times a third scalar. Next to the tokens every level carries a three-channel guide image: encoders gate it (sigmoid(head) * s0) and average it 2x2 for the next level, decoders upsample it bilinearly, gate it and blend it with the encoder's. All stages exchange data through the arena (3152 units of W0 * H0 / 32 bytes; NGX's region offsets, which scale with the render size), tokens as FP8 planes of 32 channels, guides as four binary16 per token.

Weights: the DLL holds the model as plain binary16 tensors in natural [out][in] order, kernel after kernel (rr2_ref.dll_weights; found by the sixteen kernels' relative position tables, 2 2 4 4 8 8 8 8 4 4 4 4 2 2 2 2 heads). rr2_pack.py packs them for rr2_m.comp; nothing of the network comes from the capture. The capture still supplies every kernel's argument block (the runtime keeps their constants and fills the geometry, the frame's values and the pointers) and encoder 0's second table.

The output stage filters an intermediate image of 1.5 times the output size in tiles (columns of at most 720, rows of at most 1620 intermediate pixels, 4 pixels of overlap inside), reduces each tile to the output, and keeps two histories for the next frame: the intermediate image, and a half-size copy whose quarter-size reduction (rr2_pooled_history2_kernel) encoder 0 also reads (UNORM 10:10:10:2 arrays, see above). NGX runs filter and reduction concurrently with progress counters; the kernels skip that when the counter pointers are null, which is how the engine runs them, in order. They have no matrix products and run with 64-wide waves (their warp-level shuffles address the lane's own half).

Options (harness captures capFX*): encoder 0 byte 466 packed roughness, 568 inverted depth, 569 / texture 272 DLSSD.SpecularHitDistance, 571 / texture 336 DLSS.DisocclusionMask, texture 288 the subsurface guide, 1028 DLSS.Use.HW.Depth; DLSSD.ColorBeforeParticles selects the other reduction kernel (model stage downp, texture 208, an output-size history 256 <-> 264, flag bytes 196-198). Encoder 0's surfaces 376 / 384 are the output filter's textures 64 / 72. Pre-exposure does not reach these kernels.

Checked (d4r-rr on rr_inputs.py inputs; every stage surface and the presented image against the CUDA path): byte-identical on every frame of capF (640x360 -> 1080p, 3 frames), capF900 (800x450 -> 1600x900), capF4K (1280x720 -> 4K, 16 tiles) and of the option captures (motion, a real camera, inverted and hardware depth, packed roughness, hit distance, disocclusion mask, subsurface guide, particles); through the shim (D3D12 harness under Proton) the engine's frames are byte-identical to the CUDA backend's too (D4R_RR_PRESET=6 D4R_RR_DLL=<310.9 DLL> scripts/run_rr_engine_compare.sh; that script still compares frame N with frame N, and the harness presents the CUDA result one frame late in the jitter scene).

Speed, RX 9070 XT, 1280x720 -> 3840x2160: 6.2 / 6.6 ms per frame (exposure 0.03, network 4.1 - 4.2, output 2.1 - 2.4), from 14.5 ms with every kernel translated; preset D is 4.3 / 4.5. Per stage (us): enc0 1076 (its block 521), dec0_1 775, dec0_0 630, enc1 340, dec1_0 316, dec1_1 269, the rest under 180; filter and reduction about 130 per tile, 16 tiles. Not done: native output passes and feature gather.

Where the translated parts' time went (2026-10-13, later)

Now about 5.8 / 6.2 ms (5.7 / 6.1 with the 10-bit histories that were later withdrawn), still byte-identical on every capture and through the shim. Profiling the translated code (cutting it at points behind an opaque condition, D4R_ENGINE_RR_SKIP=post,down, pooled) showed its arithmetic is spread evenly - no hot spot a rewrite would remove - and that the losses were in how the passes were arranged:

  • Encoder 0 in two passes (-0.2 ms). One shader ran NVIDIA's 32-lane gather on one of the block's four waves while the other three waited: 0.40 ms for the gather, 0.24 ms as a pass of its own in 32-thread groups. gen_rr2_enc0.py --split gather | block writes both; the gather pass (stage enc0g, the same bindings and parameter block as enc0) stores each window's record in the arena buffer (see Memory below): the first product's A fragments (1024 words) and, per lane, the values the tail reads from the gather (six binary16 on the reference build; the generator finds them and recomputes the tail's index arithmetic). The record's address is a word compile_rr2.py appends to enc0's argument block. Cost: 76 bytes per render pixel of arena. D4R_RR2_SPLIT=0 builds the single shader.
  • Tile passes in overlapping groups (-0.15 ms). The 16 tiles' post and down passes ran one after another, each behind a barrier, and the gaps between 32 short dispatches added up. Each tile now has a slot (its two regions; eight slots, 58 MB at 4K) and a group's post passes run without barriers between them, as do its down passes together with the next group's post passes on the other half of the slots - what NGX does with two slots and its progress counters. D4R_ENGINE_RR_SKIP=serial restores the old order.
  • Pooled histories as real 10:10:10:2 images: withdrawn. 4 bytes a pixel instead of RGBA32F's 16 saved 0.1 ms and was identical at 3x and 2x scaling, but the hardware's bilinear filter rounds a UNORM image differently from a float one: at 1.5x (capFQ) temporal frames came out at 44 - 51 dB. The default is RGBA32F again; D4R_RR2_POOLED=rgb10_a2 builds the other form.

Tried without gain: 32-bit arena accesses for the translated code's paired 16-bit ones (0.01 ms), dropping the subgroup shuffles or the depth search (they cost nothing measurable). What is left is NVIDIA's arithmetic: gather 0.24 - 0.33 ms, decoder 0's output tail 0.18, post 1.1 - 1.3, down 0.7.

Other scaling ratios, and what a game-like configuration showed (2026-10-13, later)

Asked whether instability seen in RE Requiem came from this implementation, the engine was compared with the CUDA path through the shim in configurations the earlier captures did not cover (D4R_RR_PRESET=6 D4R_RR_DLL=<310.9> scripts/run_rr_engine_compare.sh, frames aligned by one):

  • Render widths that are not a multiple of 32 were wrong (1114x626 -> 1080p: 15 dB): the post pass's argument 8 is the row stride of the network's output, the padded width W0, not the render width. Fixed; identical now.
  • Below 4/3 scaling NGX runs another output filter, rr2_post_kernel_3_2 (1456x820 -> 1080p and larger render sizes, DLAA included; 1440x810 and smaller use rr2_post_kernel). The model has no such stage, so the shim hands those features to the CUDA backend. rr2_histogram_auto_exposure_histogram_kernel was never launched in any run.
  • DLSS.Roughness.Mode 1 with a roughness texture bound: NGX follows the mode, the shim followed the texture. Fixed for preset F (RE Requiem binds none, so it was not affected).
  • 1.5x, 1.72x, 2x and 4/3 scaling, and RE Requiem's inputs together (inverted and hardware depth, hit distance, mask, packed roughness, scaled motion vectors, a moving scene): identical.
  • Open: the camera matrices are not NGX's bit for bit. NGX computes them in single precision: argument 644 is the f32 product view x clip, 836 its inverse by cofactors times 1 / det (glm::inverse's operation order; both reproduced bit for bit in build/rr/f/matfit.py), 900 is an inverse of the pixel matrix (it carries that inverse's residues, and 0.9999999 for 1 at 1114x626) and 964 was not reproduced. The engine computes them in double and rounds, a last-bit difference in a few elements. With a non-identity camera, hit distance and mask bound and a moving scene that showed as 40 - 50 dB on later frames in a synthetic scene whose affected feature is rounding noise around zero in both paths; no other configuration was affected.

Memory (2026-10-14)

At 1280x720 -> 4K the engine needs about 600 MB for preset F; the two 1.5x output histories (2 x 149 MB, NGX has the same) are half of it. NVIDIA's DLL, its allocations traced through the CUDA path and replayed on an RTX PRO 4500 Blackwell, takes 526 MiB of feature state, 106 MiB of input / output arrays and 2 MiB of code, plus 262 MiB of CUDA context. Three cuts, every capture still byte-identical (verify.sh, verify_x.sh) and the shim's frames byte-identical to the previous build, same speed:

  • The tile slots share the arena with the network's regions. Every arena region is written and read within one frame; in the output stage only the network's output (dec0_1's argument 64) and something after it are read, so the slots that fit before the network's output go there (all 8 at 720p -> 4K) and the rest after the arena. A first try that put the slots right after the network's output broke 360p -> 1080p's output stage.
  • The gather records' rows of windows fill the gaps between encoder 0's regions: the two largest (units 1216 - 2240 and 2304 - 2560) and then from the end of its last region (unit 2816) on, instead of one piece after the tile slots. The records (76 bytes a render pixel, about 2450 units) are larger than all the gaps, so 46 of 93 rows at 720p still reach past the arena (36 MB). Models from compile_rr2.py mark this with "s3" on the enc0g line and append six words to enc0's block (three ranges' addresses, the first two's rows; gen_rr2_enc0.py); older models keep the one-piece layout. Same speed as one piece (four alternating runs at 720p -> 4K).
  • The engine's own output image is created only for frames that do not write the game's texture, and the weights go into the command buffer with vkCmdUpdateBuffer instead of through a host-visible staging copy that stayed allocated.

Through the shim (direct output) the harness process peaks 159 MiB lower (1100 -> 941 MiB). Not exact, so not done: the pooled histories as RGB10A2 (D4R_RR2_POOLED=rgb10_a2, -70 MB, see above); they hold q / 1023, which neither binary16 nor UNORM16 represents exactly. D4R_ENGINE_RR_ARENA keeps the old separate layout, so that arena dumps show the network's regions as NGX leaves them.

Against a real NVIDIA card: D4R_RR_FUSE (2026-10-13)

Every reference above is the CUDA path through ZLUDA. NVIDIA's own kernels (310.9.1's rr2_* PTX, replayed through plain CUDA on an RTX PRO 4500 Blackwell, driver 580; tools and logs in build/rr/f/nv/) give an image 39-42 dB from that reference on every frame of capF, capF4K and capFQ. Register bisection between the two (probe_pack.py, probe_cmp.py: a debug store after every instruction, run on both) found why:

  • Fused multiply-adds. NVIDIA's compiler contracts a mul.f16 / mul.f16x2 whose product only feeds an add / sub into one fused multiply-add; the kernels give those instructions no rounding modifier. ZLUDA runs them as written. ptx_contract.py is the rule (single-use product, same block, the first operand when both are products): with it ZLUDA's result of every network kernel matches the card's (0 of ~5900 4 KiB chunks for most launches, a handful where an ex2.approx / div.approx differs by one f32 step). The FP8 products, the e4m3 conversions and every unfiltered texture fetch were already identical. The sites are the residual steps out = in + branch * s, the MLP activation x (0.5 + y (0.412 - 0.081 |y|)) and the guide's bilinear blend. Probing a product stops the card from fusing it: compare unprobed results, or probe everything.
  • Filtered fetches of 8-bit textures. A filtered fetch of the SNORM8 normals returns this card's value (8-bit weights) rounded to a 16-bit step: f32(roundEven(v * 32767) / 32767), exact on 256 of 256 probed values. The output filter's UNORM8 fetches differ too; no model for those yet.
  • Approximate instructions. lg2.approx near 1 differs by ~8e-8 absolute, ex2.approx / rcp.approx / div.approx by one f32 step. Not emulated.

D4R_RR_FUSE=1 compile_rr2.py CAPTURE OUT --ptx-dir <build/rr/f/nv/mkptx.sh output> --native DLL builds the model with the first two: rr2_m.comp -DRR_FUSE (MADD / MADD4), the activation table from enc1_ref.act with FUSE, the translated parts from the rewritten modules, encoder 0's normals through ptx2rr.py --snorm16 528. All 14 native stages are byte-identical to ZLUDA running the rewritten modules (build/rr/f/nv/nfuse.sh), and so is the whole model on capF's three frames. Presented image against the NVIDIA card, 640x360 -> 1080p:

frame default build D4R_RR_FUSE
1 (reset) 40.9 dB, 27.3% of values equal 85.6 dB, 99.5% equal
2 39.2 dB 43.0 dB
3 39.3 dB 41.8 dB

Temporal frames stay near 42 dB: the remaining one-step differences (approximate instructions, the UNORM8 fetches) enter encoder 0's reprojection and the recurrent history amplifies them, as the one-step values of the output stage did before the fused-convert fix. 1280x720 -> 4K: 5.68 / 6.24 ms (default 5.78 / 6.35). The default build is unchanged and still byte-identical to the captures.

This GPU's own arithmetic for preset F: D4R_RR_RELAXED (2026-10-13)

Two NVIDIA setups differ from each other as much as a relaxed build differs from the reference: NVIDIA's kernels on an RTX 4000 Ada (driver 595) and on the Blackwell cards (driver 580) give images 41.0 / 39.0 / 38.9 dB apart on capF's three frames, although their approximate instructions and filtered texture fetches are bit-identical (build/rr/f/nv/math/); the D4R_RR_FUSE build matches the Blackwell result only. So there is no single NVIDIA image to reproduce, and compile_rr2.py now takes D4R_RR_RELAXED=1|2 like build_native_model.sh does for D / E (rr_exact.glsl: f32 accumulators across the 32-deep steps, one rounding into FP8; 2 also leaves the conversions to the driver). Preset F, 1280x720 -> 4K: 5.40 / 5.99 ms against 5.78 / 6.35 (network 3.45 / 3.77 against 3.83 / 4.14); 40.8 / 39.1 / 39.2 dB from the exact build and 39-41 dB from either NVIDIA card. The default stays exact.

Why only 6%: with the rounding gone the products are about a sixth of a full-resolution stage. dec0's first kernel at 720p, relaxed (build/rr/f/nv/exp/t.sh, cutting parts out): 626 us, of which the two attention heads ~370, the four MLP branches ~100-160, expansion / residual / store ~120-160, an empty dispatch 10; inside the attention the softmax table ~30, the bias tiles ~40, weight loads ~12. Its 88 products alone would take ~108 us at the measured 198 T MAC/s. The compiled stage still has 338 binary16 <-> f32 conversions, 176 FP8 conversions and 128 clamps for 44 product instructions (biases and tables are binary16, the activation and softmax tables are indexed by binary16 bits, FP8 conversions saturate): what is left is format traffic and the four waves' shared-memory hand-offs, not arithmetic. NVIDIA's kernel runs the same stage in 203 us with one 32-thread warp per window.

D4R_RR_RELAXED=3 adds RR_NATIVE (rr2_m.comp): the residual steps and the MLP activation in f32, straight into FP8, with no binary16 in between and no activation table. 1280x720 -> 4K: 5.17 / 5.72 ms (network 3.24 / 3.48), still 39-41 dB from the exact build and from both NVIDIA cards. dec0's first kernel at that level: 588 us; the barriers cost nothing; reading the constants (weight tiles, bias tiles, the softmax table) costs 170 us; the products about 108; the rest is the tokens' way through shared memory and the arena, conversions and the expansion (build/rr/f/nv/exp/run1.sh).

What bounds a stage (2026-10-13; build/rr/f/nv/exp/). dec0's first kernel at level 3 takes 581 us at 720p and a wave executes about 2500 instructions (dyn.py): 96 products, 356 binary16 <-> f32 conversions, 296 FP8 conversions, 224 clamps of the saturating FP8 conversions, 142 buffer loads, 75 shared-memory operations, about 400 wait / delay markers. What was tried on top of level 3, none of it a gain:

  • the clamps removed: -30 us (2.2 ps per instruction and wave), but not usable: without the clamp the hardware conversion returns NaN above 464 where the clamp gives 448 (satprobe.comp);
  • f32 copies of the biases: 176 fewer conversions and 78 more loads per wave, slightly slower (587-595 us): a load costs about three arithmetic instructions;
  • the softmax cubic computed, not read from its table: the same time;
  • no barriers: the same time;
  • one wave per window walking the four token slices, every weight, key and value tile read once (rr2_w.comp, byte-identical to the four-wave build): 27% fewer instructions per window, 30% fewer buffer loads, 60% fewer shared-memory loads, and the same time (600 us); it needs all 256 vector registers (four waves: 192).

So time here is not a function of the instruction count alone, and there is no model yet that predicts both the clamp result and the one-wave result. The output stage is compute-bound too: 100 / 300 extra integer steps per thread in the filter add 0.45 / 1.33 ms (dz.sh). NVIDIA's kernel runs dec0's first kernel in 203 us.

Shared memory throttles this GPU (2026-10-13, build/rr/f/nv/cost/occ.py, occ2.py). A dependent chain of f32 multiply-adds runs at full rate while the workgroup declares up to about 4 KiB of shared storage per wave; above that the rate falls in proportion (one wave per workgroup: 6 KiB 1.24x slower, 8 KiB 1.55x, 13 KiB 3.1x, 62 KiB 12x; four waves: 13 KiB full rate, 26 KiB 1.55x, 52 KiB 3.1x). The register count does not matter (0 to 224 live values: the same rate). The decoders' first kernels held 21 to 46 KiB (LOW, EX, XE): rr2_m.comp now keeps the 4 x 4 low tokens in KVS and the block's unquantised input in each lane's registers (xr), byte-identical on every stage checked, and the network is 0.10 - 0.13 ms faster at 1280x720 -> 4K: exact build 5.68 / 6.25 ms (was 5.78 / 6.35), D4R_RR_RELAXED=3 5.07 / 5.56 ms (was 5.17 / 5.72). It is also why the first one-wave prototype (13 KiB in one wave) showed no gain: with 4 KiB (exp/rr2_w.comp: the low tokens borrow XT, the expansion borrows KVS, V^T is stored, read into registers and overwritten by QK) it takes 534 us against the four-wave shader's 581, still byte-identical to it. Extra arithmetic injected into either shader costs one instruction per cycle on about 128 wave slots (3.8e11 wave-instructions per second); NVIDIA's card measures 7.5e11 (build/rr/f/nv/sass/rate.cpp), and its compiled kernel for this stage is 3,729 instructions per window (sass/nv/dec0_0.sass) against about 10,000 executed here. What an instruction costs here (cost/cost.py, cost2.py): an f32 multiply-add is the unit; a 32-bit integer multiply about seven; converting a pair to FP8 with the clamp about eight; a buffer read about 0.1 ps per byte and lane (16 bytes: some forty multiply-adds), nearly twice that from a 1 MiB table.

engine/rr/rr2_w.comp (2026-10-13) is decoder 0's first kernel for D4R_RR_RELAXED=3 with one wave per window: every weight, key and value tile read once per window, the expansion plus skip computed once and kept in registers, the scores' cross quadrant computed once and reused transposed (368 products for 384), FP8 values converted in pairs, 4 KiB of shared memory. compile_rr2.py uses it for dec0_0 at level 3 (ONE_WAVE). The stage alone: 453 us against 581 us at 1280x720, arena byte-identical to rr2_m.comp at that level (build/rr/f/nv/invest-20261013/REPORT.md: 20 variants, each change's instruction count and time). In the engine every image of capF's three frames is identical to the four-wave level 3 model's, and 1280x720 -> 4K is about 5.00 / 5.55 ms (four-wave level 3: 5.10 / 5.61; network 3.05 / 3.31 against 3.15 / 3.39). The shader covers C = 32 only; the other stages still use rr2_m.comp.

rr2_w.comp also covers encoder 0's block now (KIND 4, embedded by gen_rr2_enc0.py --one-wave in NVIDIA's own 32-thread group, the translated guide tail on the same wave; compile_rr2.py ONE_WAVE = {'dec0_0', 'enc0'} at D4R_RR_RELAXED=3). All 45 images of capF's three frames are identical to the four-wave level 3 model's. 1280x720 -> 4K, four interleaved runs each: four-wave 5.10 / 5.68 ms, dec0_0 on one wave 5.04 / 5.62, encoder 0 as well 4.98 / 5.59. Decoder 0's second kernel is not converted: it hands r and the MLP sum (8 KiB of binary16) to its translated tail through shared memory, which a single wave cannot hold without losing the 4 KiB rate.

Presets D and E against a real NVIDIA card, and level 3 for them (2026-10-14)

NVIDIA's own kernels for presets D and E were replayed on an RTX PRO 4500 Blackwell (drivers 595.91 and 580.178: the two give identical images; build/rr/f/nv/pack.py --corpus ~/.cache/d4r-rr-corpus --first luma_convert, captures capD / capE3 at 640x360 -> 1280x720, every frame a reset frame). D4R_RR_RELAXED=3 now also exists for build_native_model.sh: level 2 plus RR_NATIVE (rr_exact.glsl ACTQ8: the MLP activation in f32, two elements per saturating conversion, no activation table), and from level 2 on the modules are no longer passed through spv_exact_convert.py. Presented image against the NVIDIA card, and the time at 1280x720 -> 4K (reset / next frame):

build D, dB D, ms E, dB E, ms
exact (identical to the CUDA path through ZLUDA) 38.5 / 39.0 / 38.5 4.4 / 4.7 40.0 / 39.9 / 40.0 4.34 / 4.54
D4R_RR_RELAXED=1 36.8 / 37.4 / 36.8 3.86 / 4.08 38.5 / 38.3 / 38.3 3.74 / 3.98
D4R_RR_RELAXED=2 36.8 / 37.4 / 36.8 3.76 / 3.99 38.5 / 38.3 / 38.3 3.65 / 3.82
D4R_RR_RELAXED=3 36.7 / 37.4 / 36.7 3.65 / 3.95 38.5 / 38.3 / 38.3 3.51 / 3.70

The NVIDIA card runs D at 1280x720 -> 4K in 2.19 / 2.39 ms. Unlike preset F, relaxing costs about 1.6 dB here. What was measured and not built for D / E (build/rr/f/nv/dstage.sh, dnl.sh, level 3, 720p):

  • shared memory: encoder 1 with 4 / 8 / 16 KiB of extra shared storage takes 287 -> ~305 / ~320 / ~343 us, so the stages above the limit (decoder 1 at 21 KiB, decoder 0 at 17 KiB) would gain a few per cent each, under 1% of a frame;
  • one wave per window: with every weight tile replaced by one tile loaded once (wrong values, the ceiling of sharing loads between a window's waves) decoder 0 takes the same time (803-818 us), encoder 1 the same, decoder 1 10% less. These windows also have 96 key tokens (3 KiB each for the tokens, the keys and the values at C = 32), which does not fit the 4 KiB a single wave can hold at full rate.