Commit Graph
138 Commits
Author SHA1 Message Date
itchyfeetleech fe5999dfbf engine: README: the opt-in M_RESIDUAL_F32, D4R_RR2_UNGUARDED and D4R_RR2_POOLED builds measured
Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:03:34 +13:00
itchyfeetleech 44433c320a engine/rr: opt-in D4R_RR2_UNGUARDED, the translated code without its exactness guards
The parts of preset F that are NVIDIA's PTX translated by ptx2rr.py (encoder
0's and decoder 0's per-pixel code around the native blocks, the output
passes) keep NVIDIA's arithmetic bit for bit with three kinds of guard:
every register precise, an opaque zero OR-ed into binary16 stores and
conversions (SUST_Z), and the NoContraction pass spv_exact_convert.py on
their modules. At level 3 the native stages already run this GPU's own
arithmetic, so D4R_RR2_UNGUARDED=1 drops the guards there too:
ptx2rr.py --unguarded (no precise, SUST_Z 0) and no spv_exact_convert.py on
those modules. compile_rr2.py refuses it below level 3. Off by default: the
default model's files are byte-identical to the previous ones.

Presented image against the default, RGB, peak 1: 1280x720 -> 1440p 56.0 dB
on the reset frame, then 43.0 - 45.6 dB; 1114x626 -> 1080p 48.5, then 43.4
- 44.1; the moderate stress input 52.7, then 25 - 30. The exact build
(level 0) is as far from the default (43.0 - 44.4; 43.2 - 44.3; 23.8 -
24.3) as from this build (43.0 - 44.4; 43.4 - 44.3; 23.8 - 24.3). Builds
with D4R_RR2_SPLIT=1 too.

RX 9070 XT, d4r-rr 720p -> 1440p, interleaved: frame 3.526 -> 3.501 ms
(output 649 -> 624 us, dec0_1 446 -> 442).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:03:21 +13:00
itchyfeetleech e94c4018e7 engine: opt-in M_RESIDUAL_F32, the SR residual stream in f32 across the MLP
With M_RELAXED, layer_m.comp's MLP adds every 32-channel chunk's products to
the binary16 residual stream and rounds it again: one f32 -> f16 -> f32 round
trip per chunk and element, 4 to 16 chunks a layer. M_RESIDUAL_F32 seeds an
f32 accumulator from the residual before the MLP, adds every chunk to it
with plain multiply-adds and rounds to binary16 once after the last chunk.
It needs M_RELAXED (#error otherwise); compile_m.py builds it into every
layer_m.comp stage with M_RESIDUAL_F32=1 and refuses it with M_EXACT=1.

Off by default: the default build's shaders (M, L, HDR and LDR) and the
exact build's are byte-identical to the previous ones.

Against the default build, final RGB, peak 1, 1280x720 -> 1440p: six smooth
temporal frames M 43.3 - 45.9 dB (L 44.6 - 49.2), at most 1.5 off; four
frames of the noise stress input M 17 - 18 dB (L 19 - 22). The exact build is
as far from the default (M 43.2 - 44.6 dB, L 44.5 - 47.0; noise M 16.7 - 17.9,
L 19 - 20) and as far from this one (M 43.4 - 44.8, L 44.6 - 47.0): no
further from NVIDIA's arithmetic than the default.

RX 9070 XT, d4r-m 720p -> 1440p, interleaved: M 1521 -> 1410 us (network
953 -> 841), L 2195 -> 2077 us (network 944 -> 829).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:03:21 +13:00
itchyfeetleech b42faa9928 engine: README: the byte-identical speed-ups for presets M, L and F
Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:03:21 +13:00
itchyfeetleech 6c7f881d9c engine/rr: preset F's exposure in one pass with 64-bit atomics
exposure_f.comp ran NGX's auto exposure in two passes: the 16x16 blocks wrote
their fixed-point luma sums, then a single workgroup added them after a
barrier and finished (11 us of a 25 us stage). PASS 2 does what NVIDIA's
kernel does: each block adds its sum to a 64-bit total with an atomic add
and counts itself in with an acquire-release add; the last block reads the
total, finishes and leaves both words zero for the next frame. The total is
an integer, the same in any order, so the exposure image and the values are
the two passes' bit for bit. compile_rr2.py builds exposure_f2.spv next to
the two passes' shaders (the block sum and the finish are shared functions
now; their arithmetic is unchanged).

64-bit buffer atomics are a device feature (shaderBufferInt64Atomics), so
RModel takes whether the device has it enabled (default no) and preset F
uses the one pass only then and only with a model that has exposure_f2.spv;
otherwise the two passes as before. vkd3d-proton creates its device with
every feature the GPU reports except a list it turns off, which does not
include this one (it backs D3D12's 64-bit atomics with it); the shim reads
the device's features through the interop interface (GetDeviceFeatures) and
logs the result. d4r-rr enables every feature the GPU has, and
D4R_RR_TEST_NO_INT64_ATOMICS=1 turns this one off to run the two passes.
Ray Reconstruction never runs on the native Vulkan frontend's device.

Every image of every frame byte-identical to the previous build (1280x720
-> 1440p, 1114x626 -> 1080p, 960x540 -> 1080p, the stress inputs, with
D4R_RR_TEST_ROBUST2=1, at levels 0 and 3, with D4R_RR2_SPLIT=1), and so is
the two-pass fallback of the new build; presets D and E unchanged. Controls:
the last block finishing with twice the total changes 64 of the 90 images of
the 1280x720 sequence (and nothing under the fallback), finishing without
its own block's sum 58 of 90.

RX 9070 XT, d4r-rr 720p -> 1440p, interleaved: exposure 25.2 -> 14.9 us,
frame 3.536 -> 3.526 ms (the fallback 25.1 us).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:55 +13:00
itchyfeetleech f70adc8202 engine: preset L's expansion stage at two windows per wave
layer_m.comp's L expansion (KIND 4) wrote a window's block output to shared
memory and ran the per-pixel head after a workgroup barrier, both inside the
windows' loop, so it could not walk two windows per wave (#error for KIND
>= 3). Every window now writes its output first and the head runs once after
the barrier; L's input stage (KIND 3) gathers its features once, before its
windows' loop, the same way. At one window per wave RADV emits the same
machine code for both stages as before. WPW > 2 stays an error: the head and
the gather need the block's 64 pixels on 64 invocations.

compile_m.py builds dec0_l (and dec0_l_planes) with two windows per wave,
which load each weight tile once for both: 96 -> 216 vector registers, 14 ->
7 waves per SIMD. Not under M_EXACT=1, like the network layers (it would gain
7 us there, 516 -> 509); M_DEFS_DEC0L=-DWPW=1 builds the previous layout. The
input stage at two windows per wave is identical too but slower (452 -> 465
us; 144 -> 256 registers, 10 -> 5 waves), so it stays at one.

Every output frame byte-identical (L, HDR and LDR, 1280x720 -> 1440p,
1114x626 -> 1080p, 1920x1080 -> 1440p, the stress inputs) and so is
everything D4R_M_DUMP writes - with the new default, with both stages at one
window and at two, and in the exact build (one window, and the expansion at
two). The windows' outputs swapped in the head's staging changes every frame.
Preset M's shaders are unchanged.

RX 9070 XT, d4r-m L 720p -> 1440p, interleaved: expansion 383 -> 366 us
(1114x626 -> 1080p: 342 -> 326), frame 2205 -> 2191 us.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:55 +13:00
itchyfeetleech c90dc62112 engine: SR downsample's horizontal pass reads its texels into registers
down_m.comp (presets M and L) unpacked the block's 16x100 source texels into
a shared tile, waited for it, filtered the rows from there and waited again.
Each invocation of the horizontal pass now loads the ten packed texels of
four output columns of one source row straight into registers and filters
them (200 such items for the block's 512 invocations): no shared source tile
(12.5 KiB) and one barrier fewer. 15 -> 5 KiB of shared memory, 36 -> 17
shared-memory instructions, 36 -> 48 vector registers (32 waves as before).

An output column's parity stays a run-time value (gl_NumWorkGroups.z - 1
added, the dispatch's z count is 1): with constant weights the compiler
shares products between neighbouring filters and splits their fused
multiply-adds, and 30% of the output pixels change (62 dB) - that build is
the positive control. ACO now unpacks with a fused multiply-convert
(v_fma_mix*_f16: one rounding instead of f32, then binary16), which gives
the same value for each of the 2048 / 1024 codes of a channel (checked
exhaustively, HDR and LDR constants).

Every output frame byte-identical (M and L, HDR and LDR, and the RGB10
output variant bound in the plain one's place; 1280x720 -> 1440p, 1114x626
-> 1080p, 1920x1080 -> 1440p, the stress inputs). Only down_m.spv and
down_m_rgb10.spv change.

RX 9070 XT, d4r-m, interleaved, downsample stage: 720p -> 1440p 102 -> 91 us
(L; M 117 -> 113), 1114x626 -> 1080p 62 -> 47 (L 61 -> 47); noise (the stress
inputs, whose output does not compress) unchanged, 144 -> 144 (L 139 -> 138).
Frame (L) 2213 -> 2202 us and 1963 -> 1951. Two or eight columns per
invocation instead of four: 92 / 93 us.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:55 +13:00
itchyfeetleech 8ffb0aee45 engine: SR reconstruction computes both rows' Gaussians in one pass
post_m.comp's invocations own a vertical pixel pair and ran the 3x3 (L 4x4)
Gaussian taps once per row, reading the three shared tile entries of every
tap twice. The two rows have the same column and texels and differ only in
dy, so both are now accumulated in one loop: one set of tile reads per tap,
dy, the exponent and the third accumulator as binary16 pairs (element 0 the
even row), and every operation of a row the one it had on its own, lane by
lane. The odd row keeps its own-cell residual rounding.

Every output frame byte-identical (M and L, HDR and LDR, the three tile
tiers: 1280x720 -> 1440p, 1114x626 -> 1080p, 1920x1080 -> 1440p, and the
stress inputs), and so is everything D4R_M_DUMP writes. Controls that must
differ: the odd row's weight sum times 1.01 (every frame of every input), the
rows' third accumulators swapped (every frame; in the dump only the packed
image, as g.z only enters it), the odd row's residual cast once (1114x626 ->
1080p, the one input where the rows' cells differ). Shader: 64 -> 37 shared
memory reads, 2105 -> 1969 instructions, 60 vector registers as before.

RX 9070 XT, d4r-m 720p -> 1440p, interleaved, reconstruction stage: 287 ->
265 us (L 355 -> 320), compact tier 193 -> 177 (L 244 -> 223), random motion
360 -> 347 (L 400 -> 367), 15x15 tier at 1920x1080 -> 1440p 424 -> 423 (L 427
-> 421). On the uniform-motion frames M's downsample after it measures 105 ->
117 us with the same shader (L's does not move), so the frame gains 7 us there
(1527 -> 1520), L 36 (2249 -> 2212).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:55 +13:00
itchyfeetleech 8c0635242b engine: SR reconstruction samples the history once when both candidates have one vector
post_m.comp (presets M and L) blends the history along the two most likely of
three motion candidates and sampled it once per candidate (five bilinear
taps; L also expands each sample to linear light). Where the motion is
uniform the two candidates usually have the same vector: the same position,
so the same bicubic sample or the same fallback colour (a NaN vector never
compares equal, +0 and -0 give the same position). That case now takes the
first candidate's value.

Every output frame byte-identical (M and L, HDR and LDR; 1280x720 -> 1440p,
1114x626 -> 1080p for the compact tile tier, 1920x1080 -> 1440p for the 15x15
tier, the stress inputs), and so is everything D4R_M_DUMP writes, the
history, half history and packed image included. Taking 0.99 times the first
sample there instead changes every frame of every input, the reset frames
too. Only post_m*.spv / post_l*.spv change.

RX 9070 XT, d4r-m 720p -> 1440p, interleaved, reconstruction stage:
uniform motion 325 -> 288 us (L 401 -> 354), compact tier at 1114x626 ->
1080p 219 -> 193 (L 275 -> 244); random motion unchanged (377 -> 374,
L 404 -> 401). Frame 1570 -> 1541 us (L 2299 -> 2249) on the uniform-motion
frames.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
itchyfeetleech 9c9145ac02 engine: D4R_M_DUMP also writes the reconstruction's outputs
d4r-m's D4R_M_DUMP now also writes what the reconstruction (post_m.comp)
writes: the history (hist, RGBA16F at the reconstruction resolution), its
half-resolution mean (half, RGBA32F) and the packed 11-11-10 image the
downsample reads (packed). The presented image is the packed one filtered and
rounded again, so a change in the history shows up there a frame later, if at
all; the dump shows it in the frame it happens.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
itchyfeetleech 9aa402e70f engine/rr: OCML's tanh in f32 where f64 was not needed
ptx2rr.py's tanh_ocml (tanh.approx.f32; only preset F's dec0_1 has it) ran
every fused operation through f64, which this GPU runs at a small fraction of
the f32 rate. They are f32 fma now, with two exceptions that keep every result:
- exp's error term fma(w, c, -ph) next to its own product ph = w * c comes out
  of the driver as that product and a sum, exactly 0, even with precise or
  opaque operands. It is Dekker's exact product error: w split into halves of
  12 bits (w * 4097), log2(e) into 1.4423828125 + 0.0003122091293334961, every
  partial product exact.
- the polynomial below 0.625 stays in f64: the driver always folded the f32
  rounding between its four nested f64 steps away (f2f64(f2f32(x)) -> x, the
  conversions are not NoContraction; seen in the NIR), so the old code
  computed it in f64 with one rounding at the end. A true f32 polynomial
  rounds 24,836 arguments one f32 step differently (2 in binary16).
A NaN argument keeps its payload, quieted and positive, as the f64 conversion
left it.

A GPU probe of the old against the new function: identical on all 2^32
arguments, NaN payloads included (with a plain fma for the error term instead:
3,976,308 differ, 490 in binary16). Only dec0_1.spv changes, at every level and
with D4R_RR2_SPLIT=1; presets D and E have no tanh.approx.f32 and their modules
are byte-identical. Every image of every frame byte-identical to the previous
model at levels 0 to 3 (1280x720 -> 1440p, a finite stress sequence with random
motion, a reset and a moving camera, and at level 3 also 1114x626 -> 1080p, the
NaN / Inf stress inputs and D4R_RR_TEST_ROBUST2=1); a model with tanh's
constants perturbed differs in 50 of 90 images, so the comparison sees it.

RX 9070 XT, RADV 26.2.2, d4r-rr 720p -> 1440p, interleaved: dec0_1 475 ->
446 us, frame 3.568 -> 3.537 ms; exact build (D4R_RR_RELAXED=0) 4.411 ->
4.388 ms. The polynomial in f32 as well would be 419 us.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
itchyfeetleech b7b559ff6a engine: D4R_M_DUMP also writes the M / L network's activations layer by layer
d4r-m's D4R_M_DUMP wrote the network's input (tokens) and output (head); it now
also writes skip1, merged1, skip2, merged2, tubeA, tubeB and dec2, so that a
difference between two builds can be found at the first layer it appears.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
itchyfeetleech 0ba235a05b engine: per-layer GPU timestamps for the M / L network (D4R_ENGINE_M_LAYER_STAMPS)
Timing only: with D4R_ENGINE_M_LAYER_STAMPS=1 the runtime writes a timestamp
after each of the ten network layers (queries first + 7 on) and d4r-m prints
the per-layer medians. RX 9070 XT, 1280x720 -> 1440p: enc1 210, enc2 115,
tube 6 x 50, dec2 114, dec1 209 us.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
itchyfeetleech 912bef3fb8 engine: SR layers keep their V tiles in a buffer that is dead during the attention
layer_m.comp's V tiles (VSH, 4 KiB, per wave and head, accessed only as
matrices and only during the attention) had their own shared memory. Shared
memory caps how many waves this GPU runs, and ACO gives a shader the
registers that many waves leave, so the network layers ran at 3 - 6 waves
per SIMD with registers to spare.

VSH now takes the place of a buffer that is dead during the attention:
- the merge rows NB (enc1, enc2, L's input stage), written after it;
- the expansion NEX (dec1, dec2), read before it;
- HO (L's expansion stage), written after it.
One barrier each before the buffer's own use (none was needed while the
buffers were apart). The tube keeps its own 4 KiB.

| layer        | shared memory | vector registers | waves per SIMD |
| enc1         | 13 -> 9 KiB   | 256 -> 216       | 4 -> 7         |
| enc2         | 17 -> 13 KiB  | 256              | 3 -> 4         |
| dec2         | 13 -> 9 KiB   | 256 -> 240       | 4 -> 6         |
| dec1         | 10 -> 6 KiB   | 240 -> 192       | 6 -> 8         |
| L input      | 16 -> 12 KiB  | 192 -> 144       | 8 -> 10        |
| L expansion  | 13 -> 9 KiB   | 192 -> 96        | 8 -> 14        |

Output byte-identical (M and L, six temporal frames at 1280x720 -> 1440p
and 1114x626 -> 1080p). RX 9070 XT, d4r-m 720p -> 1440p, with the previous
commit: M 1583 -> 1566 us (network 967 -> 951), L 2349 -> 2293 us (input
479 -> 453, expansion 401 -> 382). The tube on one window per wave instead
of two (192 registers, 8 waves) is 9 % slower and stays as it is.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
itchyfeetleech 0aab224003 engine: hardware FP8 saturation where the SR layers' values cannot be infinite
layer_m.comp (M / L, M_RELAXED) converts to FP8 with saturatedConvertEXT,
which RADV lowers as a NaN-preserving clamp per element and the conversion.
As in engine/rr/rr_exact.glsl (FINITE), a value that is never infinite can be
written as "an infinity becomes NaN, then saturate", which RADV turns into the
hardware conversion with FP16_OVFL (f2e4m3fn_satfn, Mesa >= 25.2): the same
result for every finite value and NaN.

Only two sources are provably finite and take it (SATF2_ / MK8_F8F):
- the attention's f32 sums of FP8 products (o);
- the normalised tokens (H1N): the norm sums the squares of the very values
  it scales, so an infinite channel makes the reciprocal 0 and the product
  NaN, never infinite.
The binary16 accumulators (q, the MLP, the expansion, the residual) can
overflow to infinity, or carry an infinite input pixel, and keep the clamp.

Per network shader 352 -> 64 clamps (one 384 -> 160), 3-4 % fewer
instructions. Output byte-identical (M and L, six temporal frames at
1280x720 -> 1440p and 1114x626 -> 1080p). RX 9070 XT, d4r-m 720p -> 1440p:
M unchanged (1579 / 1583 us, its network is not issue-bound), L 2348 ->
2329 us (input 479 -> 471, expansion 400 -> 386).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-19 21:02:50 +13:00
ayo 689ac6f270 Merge pull request #37 from itchyfeetleech/rr-f-perf
engine/rr: preset F 7.4% faster (shared memory, conversion guards, hardware FP8 saturation, bounds checks, two-wave tail), byte-identical. Thank you for the PR!
2026-10-18 08:22:20 -04:00
ayo a40d2b4ec3 Merge pull request #36 from itchyfeetleech/vulkan-engine
vulkan: run the engine on the game's device, without staging. Thank you for the PR!
2026-10-18 08:22:10 -04:00
itchyfeetleech c29076b38f engine/rr: run dec0_1's translated tail on two waves
dec0_1's translated output tail ran on wave 0 while the block's other three
waves had finished. It is a prologue (y = r + MLP * s2 and the shuffles that
give each lane its two pixels) followed by the same per-pixel code twice.

gen_rr2_enc0.py --tail-waves N gives each of N waves its own copy of the
tail with only its N-th of the stores (in order: the first pixel's six, then
the second's), and the compiler drops what a copy's stores do not need.
Every stored value comes from the same instructions on the same values as on
one wave. With one window per workgroup, the window index no longer adds
gl_SubgroupID (it was 0 for the only valid wave).

compile_rr2.py uses two waves (D4R_RR2_TAIL_WAVES=1 for one): 120 -> 96
vector registers, 12 -> 16 waves per SIMD; RX 9070 XT, 720p -> 1440p:
dec0_1 565 -> 474 us. Four waves, splitting each pixel's stores, is slower
(529 us). Every image byte-identical on the replay inputs (also robustness2,
no robust access and the split gather build).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 22:03:34 +13:00
itchyfeetleech 69a4f6ae2f engine/rr: cheaper bounds checks and conversion guards in the translated passes
- ptx2rr.py fetch0 (the translation's bounds-checked texel fetch): one
  unsigned comparison per axis instead of two signed ones; a negative
  coordinate is above every size. enc0's gather fetches 102 times: 231
  instructions fewer, scalar spills 200 -> 176.
- The output passes (post, down, downp, pooled) also go through
  spv_exact_convert.py now and take ptx2rr.py --exact-convert's single f32
  guard on binary16 conversions, as the hybrid stages do. Their ISA already
  uses v_fma_mix*_f16 for plain conversions, so the check is the whole
  instruction mix: only v_and_or_b32 / v_or / v_perm / shifts go (post -16
  VALU, down -9, downp -73); every v_fma_f32 / v_mul_f32 / v_add_f32 /
  conversion / v_exp / v_log count is unchanged.

Every image byte-identical on the replay inputs (also robustness2 and the
split gather build). RX 9070 XT, 720p -> 1440p: enc0 745 -> 738 us, output
673 -> 665 us.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 21:47:38 +13:00
itchyfeetleech e46c589c68 engine/rr: hardware FP8 saturation for the f32 conversions
RADV lowers saturatedConvertEXT to FP8 as a NaN-preserving clamp per element
(v_minimummaximum_f32) followed by v_cvt_pk_fp8_f32, because the hardware's
FP16_OVFL saturation turns infinities into NaN instead of +-448. Run over all
2^32 f32 inputs on an RX 9070 XT, FP16_OVFL gives the clamp's result for
every finite value and NaN; only +-Inf differ.

The f32 values the native stages convert are never infinite (sums of FP8
products and binary16 biases, far below 3.4e38). Writing the conversion as
"an infinity becomes NaN, then saturate" (FINITE in rr_exact.glsl) makes it
the pattern nir_opt_algebraic turns into f2e4m3fn_satfn (vkd3d-proton's,
Mesa >= 25.2, the same release that added the FP8 conversions): the
hardware conversion with FP16_OVFL and no clamp. RR_SAT_CLAMP=1 builds the
old form.

168 - 232 fewer instructions per network stage (enc0 960 -> 128 clamps).
Every image byte-identical on the replay inputs (also robustness2). RX 9070
XT, 720p -> 1440p: frame 3.92 -> 3.79 ms, every network stage faster
(dec0_1 607 -> 576 us, enc0 752 -> 727, enc1 262 -> 247).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 21:20:01 +13:00
itchyfeetleech 2fa4561bc1 engine/rr: lighter binary16 conversion guards in the hybrid stages
ptx2rr.py guards every cvt.rn.f16.f32 with two opaque zeros: on the f32 bits
(against the fused multiply-convert, v_fma_mix*_f16) and on the binary16
bits (against RADV folding float(float16_t(x))). The hybrid stages' modules
(enc0, dec0_1) also go through spv_exact_convert.py, whose NoContraction on
every conversion already prevents the fold, so with --exact-convert only the
f32 OR is emitted.

Checked in RADV's ISA: the same v_cvt_f16_f32 / v_cvt_f32_f16 counts as
before and no v_fma_mix*_f16 (dropping the f32 OR as well brings
v_fma_mixlo_f16 back, so that one stays). Every image byte-identical on the
replay inputs, the split gather build (D4R_RR2_SPLIT=1) as well. 224 / 153
fewer instructions; RX 9070 XT, 720p -> 1440p: enc0 747 -> 729 us, dec0_1
598 -> 592 us.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 20:46:32 +13:00
itchyfeetleech 49439afb81 engine/rr: preset F in less shared memory (encoder 0 in one shader, enc1, dec0_1)
ACO sizes a shader's registers to the waves its shared memory leaves room
for, so stages that declare less shared memory also get fewer registers and
more waves. Three stages hold buffers whose lifetimes do not overlap:

- Encoder 0 at D4R_RR_RELAXED=3: with the one-wave block the gather no
  longer needs a pass of its own. The generated shader runs gather, block
  and tail in one 32-thread group and hands the features over in shared
  memory that the block's XT and KVS reuse (rr2_w.comp RR2_XF_SHARED). No
  gather pass, no record in the arena (36 MB of it lay past the arena at
  720p). D4R_RR2_SPLIT=1 keeps the pass.
- enc1: the merge input (GT) is in registers before XT or KVS is written:
  17 -> 9 KiB, 240 -> 144 vector registers, 6 -> 10 waves per SIMD.
- dec0_1: r and the MLP sum for the tail are written after the block is done
  with XT and KVS, as binary16 pairs: 14 -> 8 KiB, 192 -> 120 registers,
  8 -> 12 waves per SIMD.

Also D4R_ENGINE_RR_STAGE_STAMPS=1: a timestamp after every network stage and
output group, printed per stage by d4r-rr.

Every image of six temporal frames byte-identical to the previous model
(1280x720 -> 1440p, 1114x626 -> 1080p, 853x480 -> 1440p, robustness2). RX
9070 XT, RADV, 720p -> 1440p: frame 4.09 -> 4.00 ms (enc0 798 -> 772 us,
enc1 286 -> 266, dec0_1 660 -> 623). Other stages' SPIR-V is unchanged.

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 20:34:24 +13:00
itchyfeetleech 60da7b9019 vulkan: run the engine on the game's device, without staging
Native Vulkan NGX callers now get d4r's own network for presets K, M and L,
recorded straight into the caller's command buffer on the game's VkDevice with
the same GameUpscaler/GameUpscalerM adapters the D3D12 path uses through
vkd3d-proton. No CPU staging, CUDA evaluation or host wait. D4R_VULKAN_ENGINE=0
keeps the staging path, and anything the engine does not cover still goes there
with a logged reason.

- Inputs are sampled through the caller's views (SHADER_READ_ONLY_OPTIMAL, or
  GENERAL with D4R_VULKAN_INPUT_GENERAL=1); depth/stencil views get a depth-only
  view. The output must be an RGBA16F (or RGB10A2 with an RGB10 model) storage
  image the engine writes directly, since the resource contract gives STORAGE
  usage only.
- Every recording ends with an event; engine objects of a released feature are
  destroyed once all recorded events fired, or at shutdown.
- GetFeatureDeviceExtensionRequirements reports VK_KHR_cooperative_matrix and
  VK_EXT_shader_float8 where available. It applies d4r.ini first: games ask
  before NGX init, and engine_requested() would otherwise cache "off".
- The engine accepts R16G16_SNORM motion vectors (Enshrouded).
- Engine model loaders accept a native device (VulkanInterop::native).
- Harness: D4R_HARNESS_ENGINE_DEVICE=1 (1.3 device with the engine's
  features), D4R_HARNESS_ALLOW_BLACK=1 (staging K's black reset frame).

Harness vs the staging path (RX 9070, GE-Proton11-7, 640x360 -> 1280x720):
K 99.6 % bit-identical, rest within one half-float ulp; M 52.7 dB, L 54.3 dB
PSNR; dynamic, recreate and discard pass for K and M. In game, Enshrouded runs
preset M on this path (docs/vulkan.md, SUPPORTED_GAMES.md).

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 14:45:41 +13:00
ayo 0e851b7f90 release: prepare 0.1.5 with RDNA2 kernels and in-game DLSS/FSR switching
- Add native preset K kernels for gfx1030–gfx1036 using software matrix
  products, with fast and accuracy builds and RDNA2 motion kernels.
- Follow OptiScaler's DLSS and RR preset overrides, sanitize unsupported
  values and select the preset matching the feature's quality mode.
- Enable FSR 4 under NVIDIA GPU spoofing by exposing real AMD PCI IDs
  to OptiScaler and FidelityFX modules; bundle AMD's loader and upscaler.
- Keep K on native kernels in engine auto mode unless HIP is selected,
  and fall back to CUDA for M/L when shader_float8 is unavailable.
- Add native-engine K GPU timings, preset recreation harness controls
  and regression coverage; fix the input-wait test's glibc name clash.
- Update configuration, installer guidance and release documentation,
  simplify the README and move gameplay videos to docs/videos.md.

RDNA2 kernels remain untested on RDNA2 hardware; software matrix
arithmetic was validated against WMMA on RDNA3.
2026-10-17 16:05:09 -04:00
ayo 34e6b0fe71 rr: add FSR Ray Regeneration, exact RDNA3 inference and motion experiments
Add an experimental FSR Ray Regeneration backend for D3D12 RR requests:
- Convert depth, motion, normals and albedos into AMD denoiser inputs.
- Split and recompose diffuse/specular signals, then run Super Sampling.
- Add stability/detail controls, demodulation guards and halo limiting.
- Handle AMD provider discovery under NVIDIA GPU spoofing.
- Add runtime probes, synthetic reference scenes and per-dispatch profiling.

Add exact gfx11 DLSS-RR inference using f16 cooperative matrices:
- Reproduce FP8 conversions, accumulation and camera reprojection arithmetic.
- Widen model weights and avoid requiring VK_EXT_shader_float8.
- Extend model building, replay tools and descriptor metadata.

Expand motion preprocessing and experimental AW2 native-motion support:
- Add tiled LDS dilation, padded allocation handling and thin-only sky fill.
- Order dilation with input waits and NGX on the selected stream.
- Add jitter-aligned native composition, hybrid routing and wire history repair.
- Add prepared masks, sparse repair and independent motion/repair controls.

Add optional fused R11G11B10 input/output conversion, sampled stage timings
and profile summarization. Extend configuration, packaging, documentation,
FidelityFX headers and CPU/GPU regression coverage.
2026-10-16 04:55:25 -04:00
woutermans 17c1828f97 engine/rr, vulkan: honour PTX alignment for 32-bit and shared vector accesses 2026-10-15 02:49:16 +02:00
woutermans cf96e58bd9 engine/rr: bind the game colour and the exposure values for preset F's downp stage
downp reads the game colour at argument 80 (the render rectangle, where it composites the particle layer) and
scales its output by 1 / the exposure at buffer 72; left null/zeroed every output pixel was NaN - Cyberpunk 2077,
which passes ColorBeforeParticles, showed black.
2026-10-14 21:11:42 +02:00
woutermans d9e4627e60 release: install.sh accepts any y/n answer, and a non-terminal install builds the engine models
ask() takes the default on Enter or EOF, ignores whitespace, accepts any answer starting with y or n and
re-asks otherwise. Without a terminal (or with --yes) DO_ENGINE defaults to building the models.

RELEASE_NOTES.md: the install.sh engine setup, [Engine] Enabled = auto, and the FSR Ray Regeneration backend
that this build does not carry.
2026-10-14 20:50:35 +02:00
woutermans 747ac8538d release: install.sh asks what to install, caches the NVIDIA DLLs and the engine models, and [Engine] defaults to auto
Verified copies go to ~/.cache/d4r: the next game's install copies them instead of downloading or building
again. Enabled = auto (the new default) turns the engine on when d4r/engine holds a model; the shim probes the
nine model folders. build-engine-models.sh gains --exact (NVIDIA's roundings) and --find-proton.
2026-10-14 20:25:06 +02:00
woutermans d1559ca5fa release: install.sh downloads DLSS RR 310.9.1 and builds the engine's models
A fresh extract plus bash install.sh builds all 9 models in about a minute, and they run in the shim.
Engine-vs-CUDA path: K 67 dB, M/L 45-50 dB, RR D/E/F 43/46/37 dB.
2026-10-14 20:10:17 +02:00
woutermans 11a3511120 engine: Super Resolution models from a one-frame weight dump
The dumps are one harness frame per preset (11/13/12) with D4R_CUDA_REPLAY_DUMP_DIR set. The K/M/L models
built from them are byte-identical to k-def/m-def/l-def.
2026-10-14 20:09:22 +02:00
woutermans 5f520a186d engine/rr: build the Ray Reconstruction models from nvngx_dlssd.dll alone, without a capture
Models built from nvngx_dlssd.dll 310.9.1 are byte-identical to the capture-built ones (every file, plus
d4r-rr's outputs on capF, capF4K, capF900, capFXparticles and capDT). 310.9.1 contains 310.7's D/E kernels
and weights.
2026-10-14 20:09:08 +02:00
woutermans e3606cedf9 shim: defer CUDA feature creation for every engine feature and start the RR model at CreateFeature, waiting for it instead of compiling a CUDA fallback 2026-10-14 19:49:14 +02:00
woutermans 51afe5ea39 engine: fix preset F frame index at byte 1032 to f & 7u
NGX stores the frame index modulo 8 (0-7 repeating), not the raw frame
count. The raw count matched only the first eight frames and made every
later frame differ.
2026-10-14 19:42:39 +02:00
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
ayo 162bbd77c8 shim: adaptive marker polling, opt-in GPU-side input wait/completion and RGBA8 output
- [Interop] MarkerPollUs = auto is the new default: the input-marker wait learns when
  each feature's inputs become ready, sleeps in <=200 us chunks, yields near the
  prediction and backs off when late (d4r_input_wait.h); a number keeps fixed pacing
  (0 yields). d4r_config.py no longer exports 200 when the key is unset

- D4R_SHIM_GPU_INPUT_WAIT=1 (experimental, split VRAM, age 0): NGX's null stream
  waits on a coherent Vulkan word imported into HIP (d4rStreamWaitValue32Eq) instead
  of CPU polling. The original readback marker stays the authority; a CPU watchdog
  releases discarded lists and timeouts. Denoiser, verify, capture and dilation
  frames keep the CPU wait

- D4R_SHIM_GPU_COMPLETION=1 (needs GPU_INPUT_WAIT): the bridge writes a completion
  word right after the native output kernel (d4rNgxWriteValue32), and the game's queue
  polls it with a bounded compute wait (kernels/support/vulkan_wait.comp), replacing
  the split list's CPU timeline signal. frame_retired writes the same word on every
  failure path

- D4R_SHIM_NATIVE_COMPLETION=1: a null-stream callback in the bridge signals the
  split timeline (d4rNgxTimeline*); D4rFrameCompletion::can_signal only releases the
  oldest outstanding frame

- RGBA8 output: tex_common.h adds an 'R4' redirect tag, so K's native output kernel
  stores packed RGBA8 directly (D4R_SHIM_OUTPUT_RGBA8=1, checked against the kernel
  manifest by d4rOutputKernelRgba8). Host packing handles RGBA8 too.
  patches/vkd3d-proton/0004 (VKD3D_D4R_LINEAR_RGBA8=WxH) makes such outputs linear and
  exportable so the result can be written in place

- D4R_SHIM_FUSE_RGBA8_INPUT=1: one Vulkan pass converts and copies RGBA8 color input
  (vulkan_read.comp mode 5 matches the blit's toward-zero halves, plus a row stride)

- profiling: thread CPU time from the Unix bridge (Wine's GetThreadTimes ticks at
  10 ms); game-queue D3D12 timestamps (vk_input/vk_dlss_wait/vk_output/vk_total);
  gpu_eval split into input_gpu_wait and dlss_gpu; D4R_PROFILE_GPU_EVENTS,
  _TIMESTAMPS, _LOG and _CPU_TIME=0 switch off individual parts for bisection

- bridge: D4R_NGX_STREAM_PRIORITY=high|normal runs NGX on a priority stream
  (off by default; no gain measured)

- release_feature: a feature already marked retiring by a failed drain can still
  be released normally

- harness: RGBA8 with motion and jitter scenes (re-quantized each frame),
  D4R_HARNESS_RGBA8_RAMP, D4R_HARNESS_THRASH_MB to evict L2/Infinity Cache before
  each evaluation

- kernels/native: in_hoist.py and split_output_paths.py (opt-in scheduling
  experiments); scripts/build_k_ldr_output.sh builds K's LDR output with an
  optional D4R_LDR_OUTPUT_MAX_VGPR cap

- tests: test_input_wait.py and test_latency_optimizations.py; MarkerPollUs and
  frame-completion tests updated
2026-10-13 22:30:41 -04:00
woutermans f0b55a850a engine: this GPU's own arithmetic and shared weight tiles as the builders' defaults
- layer_m.comp builds relaxed by default (M_RELAXED: f32 accumulators, paired FP8
  conversions, no activation table) with two windows per wave sharing weight tiles
  (WPW=2); M_EXACT=1 keeps NVIDIA's roundings, bit-identical to the CUDA path
- layer.comp reuses weight tiles across its waves (TPW=2 in the 32-channel layers)
- engine/rr/rr2_w.comp: one wave per window for preset F's dec0_0 and enc0, used by
  compile_rr2.py at the new default D4R_RR_RELAXED=3 (build_native_model.sh too);
  D4R_RR_FUSE=1 matches the reference's fused multiply-adds, ptx2rr.py --snorm16
- [Engine] Network defaults to vulkan; the HIP network is opt-in
2026-10-14 00:52:41 +02:00
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
woutermans 065fe2c2a1 Engine: drop the layer-loop memory flush in preset M
The ten network layer dispatches only need dispatch serialization, not a
global memoryBarrier flush: each layer's cross-block traffic goes through
its own buffers, already published by the preceding stage's flush. An
empty compute->compute pipeline barrier keeps the ordering and removes
the flush cost: whole-frame median 2515.0 -> 2431.7 us at Balanced
(1488x837 -> 2560x1440), 3818.9 -> 3773.3 at Quality, 1892.6 -> 1810.6
at Performance, byte-identical to the captured launches (chain test
10/10 layers 100.0%, frame test inf dB, 8- and 16-frame outputs match).

Also documents this session's discarded probes (intra-frame barrier
relaxation, colSum subgroup shuffle, downsample BLOCK_Y 32, expansion
EO stride 24, cross-frame pipelining analysis, the NO_ROUND 7.0%
rounding-free ceiling) and adds the round_half.glsl include guard.
2026-10-12 23:40:57 +02:00
ayo d5c8be25c8 ASI add-on for existing OptiScaler installs
Add an optional D4R_INSTALL_MODE=asi release (d4r-<version>-asi.zip)
holding only d4r.asi and a companion d4r/ folder, to drop into an
existing OptiScaler plugin directory.

- Shim: export InitializeASI. It identifies OptiScaler by its PE export
  name to find the active INI, then launches d4r-setup.exe out of
  process (no GPU or network work under the loader lock). The .asi
  build loads its runtime and config from the companion d4r/ folder.

- tools/d4r_asi_setup.cpp: native helper. It waits for the game to
  exit, installs the D3D12 wrapper and the private d4r12core.dll beside
  the EXE, and merges the NGX route into the loader's INI (including
  GE-Proton's prefix-local INI) with backups. Unchanged launches do
  nothing. It skips Proton helpers and background utilities and runs
  setup for one plugin install at a time.

- scripts/private_d3d12_core.py: points vkd3d's d3d12.dll forwarder at
  d4r12core.dll without moving code addresses.

- install.sh runs from inside the ASI's d4r/ as well as the proxy
  release root. d4r-check.sh checks ASI installs (loader, plugin path,
  LoadAsiPlugins, HookOriginalNvngxOnly, Wine drive paths).

- Packaging, build script, docs, ASI_README.txt and tests.
2026-10-12 17:36:01 -04:00
Woutermans 85175e0b27 Merge pull request #17 from woutermans/rr/cuda-ray-reconstruction
[RR] Add working CUDA Ray Reconstruction support
2026-10-12 19:51:31 +02:00
woutermans db24805160 rr: Cyberpunk's extra inputs (SSS guide, colour before particles) and hardware depth
- GameFrameR/RFrameImages: DLSSD.ScreenSpaceSubsurfaceScatteringGuide (arg 264)
  and DLSSD.ColorBeforeParticles (output stage, texture 416 with its own history
  pair 384/392, zero-initialised on first use); guides array 6 -> 8
- DLSS.Use.HW.Depth: byte 980 and output byte 443 bit 24 (Cyberpunk passes the
  nonlinear depth buffer); replay's 'inverted' field becomes a bitmask
- shim: bind the two new resources, read the HW-depth flag, and accept every
  preset hint except E's 5 by mapping it to D's diamond_wallaby kernels
  (harness captures show 0x80000002/0x80000005 run preset D)
- harness: D4R_HARNESS_RR_EXTRA=particles,sss and D4R_HARNESS_RR_HW_DEPTH=1
- ptx2rr: out-of-range integer fetches return zero, matching ZLUDA's behaviour
  (the reference the stages are checked against)
- engine/rr/README: the capture findings behind the offsets
2026-10-12 13:06:59 +02:00
woutermans 50fa65d87b 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
2026-10-12 12:47:53 +02:00
woutermans 30472ae195 Fix the rebase onto the native engine: Vulkan frontend staging, RR gating, lazy RR debug stages
The native Vulkan frontend stages packed rows and refuses Ray Reconstruction, whose guides it
does not stage. make_ptx loads the denoiser's per-layer debug stages only when one is
requested, so builds without the denoiser's PTX corpus (preset L's texture kernels) work.
2026-10-12 12:47:53 +02:00
woutermans 417abc64b5 zluda: renumber the RR patches to 0009-0013
0008 was already taken by 0008-native-post-phase-kernels.patch, so the five
patches that follow it in the build order shift up by one: ray-reconstruction
PTX support, native-FP8 conversion, packed FP8 operands, shadow-store
coalescing and signed FP8 conversion. The patches themselves are unchanged;
only their numbers and the references in README.md, docs/building.md,
docs/native-kernels.md, docs/rr-engine.md, packaging/SOURCES.txt and
scripts/build_release_glibc241.sh move.
2026-10-12 12:47:53 +02:00
woutermans b9d61015dc packaging: version the RR work 0.1.4 2026-10-12 12:47:53 +02:00
woutermans 0fe3a28cb7 kernels/rr: staged-image Swin layer engine prototypes
Per-layer staged-image engine for the eleven translated denoiser Swin
layers: descriptors (verified offsets per phase), cut recipes that splice an
isolated native phase into NVIDIA's PTX, wave templates, epilogue notes and
GPU probes. The isolated phase-8 splice produces a byte-identical final
4K frame but is slower than the translated body (14.90 vs 6.59 ms) - a
correctness experiment, not a shipping improvement; docs/rr-engine.md
records the design and the next direct-converter win (B operands built
from transposed accumulators).
2026-10-12 12:47:53 +02:00
woutermans 798e25611c zluda: coalesce pair shadow stores, signed native FP8 conversion
Patch 0011 merges a complete accumulator-register pair overwrite into one
shadow-layout conversion (reads, rewritten shadow accesses and block
boundaries prevent it; partial writes keep read/modify/write), and requests
eight waves per SIMD for gfx12 native-FP8 functions. Patch 0012 keeps signs
in the native conversion: finite overflow clamped, NaNs mapped to signed
infinity, post-conversion sign masks gone; plain and ReLU remain
exhaustive-table identical to the software codec.

Eighty-frame harness at 720p->4K, excluding the first twenty: control
24.764 ms, coalescing 23.340 ms, plus eight waves 20.135 ms, plus signed
conversion 16.234 ms (15.144 ms with the harness idle gap removed); all
eighty saved frames byte-identical to the matching control. Twelve- and
ten-wave budgets regress (25.56/24.61 ms); fast math changes the image
(18.92 ms) and is not selected.
2026-10-12 12:47:53 +02:00
woutermans 406c0286c0 zluda: gfx12 native FP8 requantization and packed operand gathers
Patch 0009 lowers half-to-e4m3 (plain and ReLU) through hardware packed FP8
conversion on gfx12 with explicit saturation and sign restoration; patch 0010
keeps native FP8 operands packed through the NVIDIA-to-WMMA lane gathers.
Same-runtime software control 33.90 ms vs native 26.28/26.65 ms; packed
gathers bring the twenty-frame run to 25.33 ms, all frames byte-identical.
2026-10-12 12:47:53 +02:00
woutermans 2e81a6bbfa docs: the denoiser layers' measured launch shape and parameter sizes 2026-10-12 12:47:53 +02:00