engine: Super Resolution presets E and F, the DLSS 3 CNN, on RDNA2

Presets E (5) and F (6) run on d4r's own Vulkan engine without cooperative
matrices: the input and reconstruction kernels translated from the user's DLL
(engine/compile_sr_e.py, rr/ptx2rr.py), the network engine/sr/e_net.comp with
packed v_pk_fma_f16 accumulation (engine/cnn_sr/mma_f16.glsl, the default
head; --head-accumulation f32 keeps the f32 head). Runtime runtime_e.inc
(EModel/EEngine), game adapter game_e.cpp, replay d4r-e; the shim serves them
from engine\e and engine\f (D4R_ENGINE_MODEL_E_DIR / _F_DIR), LDR or HDR, any
render size, not with AutoExposure. build-engine-models.sh builds both.

Translator fixes that apply to every translated kernel: .relu on f16/f16x2
fma/add/mul was ignored; add.rz was always taken for the roundf idiom (lost
the log1pf correction term); formatted image stores expand at the call site
(SPIR-V image type fix); f16 mma.sync m16n8k8/k16, .const loads, shared
16-bit stores, aligned 32-bit arena words (D4R_WORD_ARENA), unified arena.

Steam Deck gfx1033: 2.55 ms at 640x360 -> 1280x720, 6.20 ms at 1080p
output; Rounded Quality sizes take mid_3x3 (reconstruction 2.44 -> 1.23 ms).
Ray Reconstruction CNN presets A-C (compile_rr3.py, the packed RR flavor)
stay out of this commit; engine/README.md already documents their tuning.
This commit is contained in:
woutermans committed 2026-10-18 20:39:26 +02:00
1 parent 0e851b7f90
commit b9254f7f96
25 files changed
+2406 -36

No files matched your search

+10 -4
View File
@@ -25,9 +25,12 @@ GameExe =
; K | 4 | DLAA / Quality / Balanced | Default; faster alternative to M/L ; K | 4 | DLAA / Quality / Balanced | Default; faster alternative to M/L
; M | 4.5 | Performance | More demanding on RDNA3 ; M | 4.5 | Performance | More demanding on RDNA3
; L | 4.5 | Ultra Performance | Especially 4K; experimental ; L | 4.5 | Ultra Performance | Especially 4K; experimental
; E | 3 CNN | Quality / Balanced / Performance | Legacy faster fallback ; E | 3 CNN | Quality / Balanced / Performance | Legacy faster fallback; the engine's choice on RDNA2
; F | 3 CNN | DLAA / Ultra Performance | Deprecated; prefer K or L ; F | 3 CNN | DLAA / Ultra Performance | Deprecated; prefer K or L
; ;
; On the engine ([Engine]) E and F run from engine\e and engine\f (build-engine-models.sh) without cooperative
; matrices, so they are the models for RDNA2 GPUs (Steam Deck: about 5 ms at 640x360 -> 1280x720).
;
; Select the upscaling mode separately in game/OptiScaler. These are recommendations; ; Select the upscaling mode separately in game/OptiScaler. These are recommendations;
; other modes are allowed. L requires the L texture build. ; other modes are allowed. L requires the L texture build.
; Aliases: CNN = E, DLSS4 = K, DLSS4.5 = M. A/B/C/D/G/J use K in this DLL version. ; Aliases: CNN = E, DLSS4 = K, DLSS4.5 = M. A/B/C/D/G/J use K in this DLL version.
@@ -134,12 +137,15 @@ EvalSync = false
MarkerPollUs = auto MarkerPollUs = auto
[Engine] [Engine]
; Experimental. Runs DLSS 4 and 4.5 (models K, L and M) on d4r's own Vulkan inference engine instead of NVIDIA's ; Experimental. Runs DLSS 4 and 4.5 (models K, L and M) and DLSS 3's CNN (models E and F) on d4r's own Vulkan
; inference engine instead of NVIDIA's
; kernels through ZLUDA: the whole evaluation is recorded into the game's own command list, the game shows the ; kernels through ZLUDA: the whole evaluation is recorded into the game's own command list, the game shows the
; result of the frame it just rendered, and FrameAge and the [Interop] hand-off settings do not apply. ; result of the frame it just rendered, and FrameAge and the [Interop] hand-off settings do not apply.
; Needs VkD3DDir, a driver with VK_KHR_cooperative_matrix (Mesa RADV on RDNA3/RDNA4) and the model folders ; Needs VkD3DDir, a driver with VK_KHR_cooperative_matrix (Mesa RADV on RDNA3/RDNA4) and the model folders
; (RuntimeDir/engine/k, k-ldr, m, m-ldr, l, l-ldr; scripts/install_d4r_runtime.sh). A game or setting the engine ; (RuntimeDir/engine/k, k-ldr, m, m-ldr, l, l-ldr; scripts/install_d4r_runtime.sh). E and F (engine/e, engine/f;
; does not cover uses the normal path, with the reason in d4r_nvngx.log: other models than K, L and M, ; D4R_ENGINE_MODEL_E_DIR, D4R_ENGINE_MODEL_F_DIR) need no cooperative matrices: they run on RDNA2 (Steam Deck)
; too, from any render size and HDR or LDR, but not with AutoExposure. A game or setting the engine
; does not cover uses the normal path, with the reason in d4r_nvngx.log: other models than E, F, K, L and M,
; display-resolution motion vectors with L or M, or a K model built before the current engine/compile_k.py for display-resolution ; display-resolution motion vectors with L or M, or a K model built before the current engine/compile_k.py for display-resolution
; motion, non-inverted depth or exposure supplied by the game. Details and measurements: engine/README.md. ; motion, non-inverted depth or exposure supplied by the game. Details and measurements: engine/README.md.
; (D4R_ENGINE) ; (D4R_ENGINE)
+109
View File
@@ -1121,6 +1121,114 @@ Ultra Performance, the downsample weights and the network's summation order (M i
backend in the harness as well: 1706x960 -> 2560x1440 and 640x360 -> 1920x1080, where it measured 36-41 dB with the backend in the harness as well: 1706x960 -> 2560x1440 and 640x360 -> 1920x1080, where it measured 36-41 dB with the
native accuracy kernels' order; `compile_m.py` builds both presets' networks with `TEXL`). Details: `L_NOTES.md`. native accuracy kernels' order; `compile_m.py` builds both presets' networks with `TEXL`). Details: `L_NOTES.md`.
## Presets E and F: DLSS 3's CNN (runtime and shim path exist; experimental, the RDNA2 route)
Presets E (5) and F (6) are NGX 310.7's CNN upscaler, in the `nvngx_dlss.dll` the installer already fetches. Its
frame is an input kernel (`cuda_engine_input_kernel_rel_{ldr,hdr}_mvdiff_{mvhi,mvlo}`: the game's colour, depth and
motion, the previous output and motion to 16 channels per token of 4x4 output pixels, the warped history and this
frame's output-size motion), eleven `dltss_nchw8_*` network kernels (a U-Net over the token grid, the output padded
to 128 / 4, five levels) and a reconstruction kernel (`..._aniso_gaussian_dfn_{fastpath_3x3_128, mid_3x3_128,
3x3_256, fastpath_5x5_256, 5x5_256}`: the last decoder level, a 40-channel head per token and the anisotropic
Gaussian reconstruction of the output and the next history). E and F share the kernels; they differ in weights and
in two constants.
- The network is d4r's own `sr/e_net.comp` (five modes: the first encoder, the 3x3 + pool encoders, 1x1 convs, the
upsampling 1x1, the decoders' double 1x1), packed binary16 like the RR CNN's packed flavor (`rr/cnn_pk.glsl`): a
lane owns a pixel, the weights are wave-uniform scalar loads feeding `v_pk_fma_f16`, the input of a workgroup is
staged in LDS. No cooperative matrices, so it is the engine's route on RDNA2. It writes NGX's tensor layouts
(`[C/8][H][W][8]`, the full-size skips as 2x2 phases, 8-bit activations as binary16 bits 14..7), which the
translated reconstruction kernel reads.
- The input and reconstruction kernels are translated from the user's DLL (`rr/ptx2rr.py`) in every variant; the
runtime picks a variant for the ratio (5x5 above 2.5x, `fastpath_3x3` from 1.6x, `mid` when each render axis is
floor/ceil of two thirds of its output axis, the generic kernels otherwise) and rewrites every frame-dependent
field of their argument blocks (sizes, ratios,
jitter, reset, MV scale, the HDR and MVLowRes flags; harness captures at seven sizes and ratios, 2026-10-18).
- `compile_sr_e.py REPLAY NVNGX_DLSS_DLL OUT --preset E|F` builds a model from one frame's replay dump (the weights are
packed in the DLL; NGX uploads them unpacked), `runtime_e.inc` is `EModel` / `EEngine`, `game_e.cpp` the game
adapter, `replay_e.cpp` the replay (`d4r-e`). The shim runs presets 5 and 6 with `engine\e` / `engine\f`
(`D4R_ENGINE_MODEL_E_DIR`, `_F_DIR`), LDR or HDR, either motion resolution, any render size (a new size rebuilds
the engine); not with AutoExposure. `build-engine-models.sh` builds both.
With the f32 reconstruction head, accuracy against NGX through ZLUDA (three temporal frames): 63-68 dB at 640x360 -> 1280x720
(LDR and HDR, both motion resolutions, E and F), 1280x720 -> 1920x1080, 1001x563 -> 1501x845 (generic kernel),
427x240 -> 1280x720 (5x5), 742x418 -> 1280x720; DLAA 57-73 dB. Network tensors 45-60 dB each (binary16 accumulation).
The translation needed two `vulkan/ptx2glsl.py` fixes that apply to every translated kernel: `.relu` on f16 / f16x2
`fma`, `add` and `mul` was ignored, and `add.rz` was always taken for the `roundf` idiom (CUDA's `log1pf` starts with
an `add.rz`, which lost its second operand).
Steam Deck (gfx1033, RADV, best of 12), 640x360 -> 1280x720: 4.3 ms (input 0.78, network 1.86, reconstruction 1.68);
1280x720 -> 1920x1080: 10.1 ms. The network's ideal at the Deck's packed-f16 rate is 0.75 ms at 720p; the two
translated kernels are the larger share (the reconstruction is 5400 VALU instructions as translated, its emulated
mma about 0.3 ms of it), so native input and reconstruction kernels are the next step.
RDNA2 tuning (2026-10-18): the SR decoders now reuse the input's padded LDS rows for their hidden layer,
with a workgroup barrier before overwriting the input. This removes a second allocation and its bank conflicts.
RR's packed CNN uses smaller tiles on the stages limited by LDS occupancy; its decoder filters cache each bilinear
sample once per tile, retaining the original half-precision operation order. Rebuild the model directories with
`compile_sr_e.py` / `compile_rr3.py` to use these shaders; the existing runtime selects the packed RR flavor on RDNA2.
Steam Deck gfx1033, RADV, GPU fixed temporarily at its stock 1600 MHz, interleaved baseline/candidate runs,
best of 12 submissions per frame (four temporal frames for RR, three for SR), 640x360 -> 1280x720:
| Model | Before tuning, ms | After tuning, ms |
|---|---:|---:|
| SR E, complete frame | 3.13 | 3.09 |
| SR E, network | 1.24 | 1.20 |
| RR A, complete frame | 14.61 | 12.48 |
| RR A, input + network | 12.98 | 10.85 |
These are inference timings without output readback, with no other GPU workload; automatic clocks and games can
run slower. The changes preserve the packed path's arithmetic: all outputs and histories match byte for byte at
one submission per frame and in repeated runs. Additional checks cover RR's partially filled tiles (70x66 render),
and SR's HDR, output-resolution motion, odd sizes (1001x563 -> 1501x845), 1080p output and 5x5 reconstruction.
The packed convolutions still differ from NVIDIA's matrix accumulation as described above. The translated SR
input/reconstruction passes still take most of the frame, so these results do not establish hardware-optimal
performance. `tests/test_cnn_packed_gpu.py` checks decoder reuse and partial pixel sets against a CPU reference
without proprietary weights (`D4R_RUN_GPU_TESTS=1 D4R_EVK=/path/to/evk python3 -m unittest tests.test_cnn_packed_gpu`).
The reconstruction head now defaults to packed binary16 accumulation too (`D4R_MMA_PACKED` in
`cnn_sr/mma_f16.glsl`, enabled only for the SR model builder). Each lane computes two columns together with
`v_pk_fma_f16`, retaining the fragment layout and K order without expanding operands and accumulators to f32.
The translated stages also use aligned 32-bit arena accesses for packed words. This removes pairs of scalar
16-bit loads/stores; their binding and tensor layouts stay the same. Formatted image stores expand at the call
site, retaining the concrete SPIR-V image format instead of passing it through an unformatted function parameter.
All four input and five reconstruction variants pass `spirv-val`.
On the same Deck at a fixed stock 1600 MHz, best of 12 submissions per frame, the complete E pass with the
previous f32 head versus the new packed head (three-frame input sequence):
| Render -> output | f32 head, ms | Packed head, ms |
|---|---:|---:|
| 640x360 -> 1280x720, LDR | 3.09 | 2.55 |
| 640x360 -> 1280x720, HDR | 3.09 | 2.66 |
| 1001x563 -> 1501x845, generic reconstruction | 6.28 | 5.30 |
| 1280x720 -> 1920x1080 | 7.15 | 6.20 |
| 427x240 -> 1280x720, 5x5 reconstruction | 5.10 | 4.02 |
At 640x360 -> 1280x720 the reconstruction pass itself falls from about 1.35 to 0.80 ms. The packed head rounds
after each half FMA, so its image is **not byte-identical** to the previous head. At one submission per frame,
eight tested configurations (also HDR, both motion resolutions, balanced scaling and DLAA) give 64.9-92.6 dB
RGB PSNR against the f32-head engine over three temporal frames, with finite outputs throughout. These comparisons
measure the arithmetic change, not quality parity with NGX or a long gameplay sequence. `--head-accumulation f32`
retains the previous head arithmetic, with the word-access optimization; the default is `f16` for both E and F.
`tests/test_cnn_mma_gpu.py` checks both accumulation modes, fragment mapping and independent concurrent waves
against CPU references. Rebuild model shaders to pick up the new default; old model directories continue to run.
Rounded Quality dimensions also use the existing `mid_3x3_128` reconstruction kernel: each render axis must be
floor or ceil of two thirds of its output axis. The kernel still samples with the actual ratios and jitter.
This avoids the generic reconstruction fallback for 853x533 -> 1280x800 without changing the model arithmetic.
On the Deck at stock 1600 MHz, three interleaved before/after runs (120 temporal frames each, 30 warm-up frames
discarded, 270 measured samples per build) reduce median reconstruction from 2.44 to 1.23 ms and the complete
upscaling pass from 4.46 to 3.26 ms. This change only needs a runtime rebuild; existing model shaders work.
`tests/test_sr_reconstruction_gpu.py` compares automatic selection against forced generic reconstruction, including
mixed per-axis rounding, partial tiles, HDR/LDR, both motion resolutions, jitter, history and reset. On the Deck,
all 384 preset E test frames match bit for bit; an optional private preset F model enables the same checks for F.
```sh
python3 engine/compile_sr_e.py "$CAP/replay" nvngx_dlss.dll build/e-model --preset E
build/native-engine/d4r-e build/e-model INPUT_DIR OUT_DIR # D4R_ENGINE_E_PROFILE=1: microseconds per pass
```
## Files ## Files
| File | Role | | File | Role |
@@ -1142,6 +1250,7 @@ native accuracy kernels' order; `compile_m.py` builds both presets' networks wit
| `l_model.py`, `test_l.py`, `L_NOTES.md` | Preset L: weights of the input and expansion stages, their tests against a capture, the algorithms | | `l_model.py`, `test_l.py`, `L_NOTES.md` | Preset L: weights of the input and expansion stages, their tests against a capture, the algorithms |
| `enc0_features.glsl` | Presets M and L: the input stage's 32 per-pixel feature slots | | `enc0_features.glsl` | Presets M and L: the input stage's 32 per-pixel feature slots |
| `exposure_k.comp` | Automatic exposure: log-luma block sums, then the exposure state and the stages' exposure fields | | `exposure_k.comp` | Automatic exposure: log-luma block sums, then the exposure state and the stages' exposure fields |
| `sr/e_net.comp`, `compile_sr_e.py`, `runtime_e.inc`, `game_e.h`, `game_e.cpp`, `replay_e.cpp` | Presets E and F: the network shader, the model builder, the runtime, the game adapter and the replay (`d4r-e`) |
| `test_frame.py`, `test_model.py` | Temporal result checks and model corruption tests | | `test_frame.py`, `test_model.py` | Temporal result checks and model corruption tests |
| `test_game.cpp` | Queued direct-texture, rectangle, layout, conversion and precision regression checks | | `test_game.cpp` | Queued direct-texture, rectangle, layout, conversion and precision regression checks |
| `test_k.py`, `test_in*.py`, `test_out*.py` | Isolated shader diagnosis and reference comparisons | | `test_k.py`, `test_in*.py`, `test_out*.py` | Isolated shader diagnosis and reference comparisons |
+4 -2
View File
@@ -12,11 +12,13 @@ if [[ -f "$ROCM/include/hip/hip_runtime_api.h" ]]; then
HIP=(-DD4R_ENGINE_HIP "$ROOT/engine/hip_net.cpp" -I"$ROCM/include" -L"$ROCM/lib" -lamdhip64 "-Wl,-rpath,$ROCM/lib") HIP=(-DD4R_ENGINE_HIP "$ROOT/engine/hip_net.cpp" -I"$ROCM/include" -L"$ROCM/lib" -lamdhip64 "-Wl,-rpath,$ROCM/lib")
fi fi
"$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers -fPIC -shared \ "$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers -fPIC -shared \
"$ROOT/engine/runtime.cpp" "$ROOT/engine/game.cpp" "$ROOT/engine/game_m.cpp" "$ROOT/engine/game_rr.cpp" -lvulkan -o "$OUT/libd4r_engine.so" "$ROOT/engine/runtime.cpp" "$ROOT/engine/game.cpp" "$ROOT/engine/game_m.cpp" "$ROOT/engine/game_rr.cpp" "$ROOT/engine/game_e.cpp" -lvulkan -o "$OUT/libd4r_engine.so"
"$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \ "$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \
"$ROOT/engine/replay.cpp" "${HIP[@]}" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-k" "$ROOT/engine/replay.cpp" "${HIP[@]}" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-k"
"$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \ "$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \
"$ROOT/engine/replay_m.cpp" "${HIP[@]}" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-m" "$ROOT/engine/replay_m.cpp" "${HIP[@]}" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-m"
"$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \ "$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \
"$ROOT/engine/replay_rr.cpp" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-rr" "$ROOT/engine/replay_rr.cpp" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-rr"
printf 'Built %s, %s and %s\n' "$OUT/d4r-k" "$OUT/d4r-m" "$OUT/d4r-rr" "$CXX" -std=c++17 -O3 -Wall -Wextra -Wno-missing-field-initializers \
"$ROOT/engine/replay_e.cpp" -L"$OUT" -ld4r_engine -lvulkan '-Wl,-rpath,$ORIGIN' -o "$OUT/d4r-e"
printf 'Built %s, %s, %s and %s\n' "$OUT/d4r-k" "$OUT/d4r-m" "$OUT/d4r-rr" "$OUT/d4r-e"
+70
View File
@@ -0,0 +1,70 @@
// Extension-free f16 MMA for wave32. PTX m16n8k8 / m16n8k16 row.col fragments:
// A: (row=g+8*(word&1), k=2*t+8*(word/2)+half).
// B: (k=2*t+8*word+half, col=g). C/D: (row=g+8*word, col=2*t+half).
// g=lane/4, t=lane%4. Shuffle sources stay within the current subgroup;
// there is no workgroup scratch or cross-wave synchronization.
// Include engine/round_half.glsl first: RADV's packHalf2x16 truncates instead of rounding to nearest.
#ifndef D4R_MMA_F16_INCLUDED
#define D4R_MMA_F16_INCLUDED
#ifdef D4R_MMA_PACKED
// The SR reconstruction head uses the same packed-half accumulation as e_net.comp.
// Each lane owns two rows and two columns; one packed FMA updates a row's two columns.
// Keep the fragment mapping and K order, rounding after each FMA instead of each MMA.
f16vec4 mma_packed_step8(uint a0, uint a1, uint b0, f16vec4 acc)
{
const uint lane = gl_SubgroupInvocationID, row = lane >> 2u, col = (lane & 3u) * 2u;
[[unroll]] for (uint k = 0u; k < 4u; ++k) {
const f16vec2 x0 = unpackFloat2x16(subgroupShuffle(a0, row * 4u + k));
const f16vec2 x1 = unpackFloat2x16(subgroupShuffle(a1, row * 4u + k));
const f16vec2 y0 = unpackFloat2x16(subgroupShuffle(b0, col * 4u + k));
const f16vec2 y1 = unpackFloat2x16(subgroupShuffle(b0, (col + 1u) * 4u + k));
acc.xy = fma(f16vec2(x0.x), f16vec2(y0.x, y1.x), acc.xy);
acc.xy = fma(f16vec2(x0.y), f16vec2(y0.y, y1.y), acc.xy);
acc.zw = fma(f16vec2(x1.x), f16vec2(y0.x, y1.x), acc.zw);
acc.zw = fma(f16vec2(x1.y), f16vec2(y0.y, y1.y), acc.zw);
}
return acc;
}
void mma_f16_m16n8k8(uint a0, uint a1, uint b0, uint c0, uint c1, out uint d0, out uint d1)
{
const f16vec4 acc = mma_packed_step8(a0, a1, b0, f16vec4(unpackFloat2x16(c0), unpackFloat2x16(c1)));
d0 = packFloat2x16(acc.xy); d1 = packFloat2x16(acc.zw);
}
void mma_f16_m16n8k16(uint a0, uint a1, uint a2, uint a3, uint b0, uint b1,
uint c0, uint c1, out uint d0, out uint d1)
{
f16vec4 acc = mma_packed_step8(a0, a1, b0, f16vec4(unpackFloat2x16(c0), unpackFloat2x16(c1)));
acc = mma_packed_step8(a2, a3, b1, acc);
d0 = packFloat2x16(acc.xy); d1 = packFloat2x16(acc.zw);
}
#else
vec4 mma_f16_step8(uint a0, uint a1, uint b0, vec4 acc)
{
const uint lane = gl_SubgroupInvocationID, row = lane >> 2u, col = (lane & 3u) * 2u;
[[unroll]] for (uint k = 0u; k < 4u; ++k) {
const vec2 x0 = unpackHalf2x16(subgroupShuffle(a0, row * 4u + k));
const vec2 x1 = unpackHalf2x16(subgroupShuffle(a1, row * 4u + k));
const vec2 y0 = unpackHalf2x16(subgroupShuffle(b0, col * 4u + k));
const vec2 y1 = unpackHalf2x16(subgroupShuffle(b0, (col + 1u) * 4u + k));
acc.x = fma(x0.x, y0.x, acc.x); acc.x = fma(x0.y, y0.y, acc.x);
acc.y = fma(x0.x, y1.x, acc.y); acc.y = fma(x0.y, y1.y, acc.y);
acc.z = fma(x1.x, y0.x, acc.z); acc.z = fma(x1.y, y0.y, acc.z);
acc.w = fma(x1.x, y1.x, acc.w); acc.w = fma(x1.y, y1.y, acc.w);
}
return acc;
}
void mma_f16_m16n8k8(uint a0, uint a1, uint b0, uint c0, uint c1, out uint d0, out uint d1)
{
const vec4 acc = mma_f16_step8(a0, a1, b0, vec4(unpackHalf2x16(c0), unpackHalf2x16(c1)));
d0 = packHalf2x16(roundHalfValue(acc).xy); d1 = packHalf2x16(roundHalfValue(acc).zw);
}
void mma_f16_m16n8k16(uint a0, uint a1, uint a2, uint a3, uint b0, uint b1,
uint c0, uint c1, out uint d0, out uint d1)
{
vec4 acc = mma_f16_step8(a0, a1, b0, vec4(unpackHalf2x16(c0), unpackHalf2x16(c1)));
acc = mma_f16_step8(a2, a3, b1, acc);
d0 = packHalf2x16(roundHalfValue(acc).xy); d1 = packHalf2x16(roundHalfValue(acc).zw);
}
#endif // D4R_MMA_PACKED
#endif
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Compile an E/F CNN capture into Vulkan stages and a compact, alias-preserving tensor arena.
Usage: compile_e.py PTX_DIR CAPTURE_DIR OUTPUT_DIR [--preset E|F] [--hdr] [--frame N]
PTX and all model payloads are derived from the user's DLL and must not be redistributed.
"""
import argparse
import json
from pathlib import Path
import re
import struct
import subprocess
import sys
HERE = Path(__file__).resolve().parent
def manifest(path):
result = {'allocs': {}, 'pointers': [], 'textures': [], 'surfaces': []}
for line in path.read_text().splitlines():
p = line.split()
if not p:
continue
if p[0] == 'kernel':
result['name'] = p[1]
elif p[0] == 'launch':
result['launch'] = list(map(int, p[1:7]))
elif p[0] == 'alloc':
result['allocs'][int(p[1])] = (int(p[2], 16), int(p[3]))
elif p[0] == 'pointer':
result['pointers'].append(tuple(map(int, p[1:4])))
elif p[0] in ('texture', 'surface'):
result[p[0] + 's'].append(int(p[1]))
return result
def dump_shape(cap, stage, offset):
number = int(stage['directory'].name.split('-')[1])
prefix = f'launch-{number:03d}-{stage["name"]}-arg{offset:03d}-'
paths = sorted((cap / 'dump').glob(prefix + '*'))
for path in paths:
m = re.search(r'-(texture|surface)-(\d+)x(\d+)x(\d+)-fmt(\d+)\.bin$', path.name)
if m:
_, w, h, channels, dtype = m.groups()
channels, dtype = int(channels), int(dtype)
if channels not in (1, 2, 4) or dtype not in (16, 32):
raise ValueError(f'unsupported captured image: {path.name}')
fmt = {1: 'r', 2: 'rg', 4: 'rgba'}[channels] + str(dtype) + 'f'
return {'width': int(w), 'height': int(h), 'format': fmt}
return None
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('ptx_dir', type=Path)
ap.add_argument('capture_dir', type=Path)
ap.add_argument('output', type=Path)
ap.add_argument('--preset', choices=('E', 'F'), default='E')
ap.add_argument('--hdr', action='store_true')
ap.add_argument('--frame', type=int, default=1)
a = ap.parse_args()
stages = []
for d in sorted((a.capture_dir / 'replay').glob('replay-*')):
s = manifest(d / 'manifest.txt')
s['directory'] = d
stages.append(s)
starts = [i for i, s in enumerate(stages) if s['name'].startswith('cuda_engine_input_kernel_')]
if not 1 <= a.frame <= len(starts):
ap.error(f'capture contains {len(starts)} CNN frames')
first = starts[a.frame - 1]
end = starts[a.frame] if a.frame < len(starts) else len(stages)
stages = [s for s in stages[first:end] if s['name'].startswith(('cuda_engine_input_kernel_', 'dltss_nchw8_'))]
if len(stages) != 13 or 'aniso_gaussian_dfn' not in stages[-1]['name']:
raise ValueError('capture does not contain the complete CNN input/network/reconstruction graph')
if ('_ldr_' not in stages[0]['name']) != a.hdr:
raise ValueError('--hdr does not match the captured input kernel')
color = dump_shape(a.capture_dir, stages[0], 280)
output = dump_shape(a.capture_dir, stages[-1], 464)
if color is None or output is None:
raise ValueError('capture lacks the current color or final output image')
rw, rh, ow, oh = color['width'], color['height'], output['width'], output['height']
# Merge only overlapping intervals, never CUDA address-space holes. A nonzero arena
# prefix preserves null-pointer tests when a valid tensor was the first allocation.
intervals = sorted({v for s in stages for v in s['allocs'].values()})
regions = []
for base, size in intervals:
if regions and base < regions[-1]['end']:
regions[-1]['end'] = max(regions[-1]['end'], base + size)
else:
regions.append({'base': base, 'end': base + size})
arena_size = 256
for r in regions:
r['offset'] = arena_size
arena_size = (arena_size + r['end'] - r['base'] + 255) & ~255
r['mutable'] = False
def region_for(address):
return next(r for r in regions if r['base'] <= address < r['end'])
def arena_offset(address):
r = region_for(address)
return r['offset'] + address - r['base']
modules = {}
for p in sorted(a.ptx_dir.glob('*.ptx')):
text = p.read_text()
for name in re.findall(r'\.entry\s+(\w+)\s*\(', text):
if name not in modules or '.target sm_89' in text[:400]:
modules[name] = p
a.output.mkdir(parents=True, exist_ok=True)
cache = {}
lines = ['D4RE0001', f'size {rw} {rh} {ow} {oh}', f'preset {5 if a.preset == "E" else 6}', f'hdr {int(a.hdr)}',
f'buffer 0 {arena_size} arena-init.bin']
diagnostics = []
for index, s in enumerate(stages):
name, launch = s['name'], s['launch']
if name not in cache:
generated = a.output / (name + '.generated.comp')
command = [sys.executable, str(HERE / 'rr/ptx2rr.py'), str(modules[name]), name, str(generated),
'--block', ','.join(map(str, launch[3:6])), '--weights-arg', '999', '--unified-arena']
command += ['--buffer=' + str(off) for off, _, _ in s['pointers']]
for off in s['surfaces']:
shape = dump_shape(a.capture_dir, s, off)
if shape:
command += [f'--surface={off}={shape["format"]}']
subprocess.run(command, check=True)
spv = a.output / (name + '.spv')
subprocess.run(['glslangValidator', '-V', '--target-env', 'vulkan1.3', str(generated), '-o', str(spv)], check=True)
info = json.loads(generated.with_suffix('.json').read_text())
constants = generated.with_suffix('.globals.bin')
globals_name = name + '.globals.bin'
(a.output / globals_name).write_bytes(constants.read_bytes() if constants.exists() else bytes(16))
cache[name] = (info, globals_name)
info, globals_name = cache[name]
for off, allocation, sub in s['pointers']:
if off in info['written_buffers']:
base, _ = s['allocs'][allocation]
region_for(base + sub)['mutable'] = True
params = bytearray((s['directory'] / 'args.bin').read_bytes())
for off, allocation, sub in s['pointers']:
base, _ = s['allocs'][allocation]
struct.pack_into('<Q', params, off, arena_offset(base + sub))
parameter_name = f'stage-{index:02d}.args'
(a.output / parameter_name).write_bytes(params)
lines.append(f'stage {index} {name}.spv {parameter_name} {globals_name} ' + ' '.join(map(str, launch[:3])))
input_stage = index == 0
for off in info['textures']:
shape = dump_shape(a.capture_dir, s, off)
roles = ({280: 'color', 288: 'history_read', 304: 'motion', 312: 'depth', 320: 'motion_read',
344: 'exposure', 360: 'previous_exposure'} if input_stage else
{456: 'color', 464: 'output', 472: 'exposure'})
if shape is None:
role = 'zero'
elif off not in roles:
raise ValueError(f'unmapped input image {name} argument {off}')
else:
role = roles[off]
lines.append(f'texture {index} {off} {role}')
for off, fmt in info['surfaces']:
roles = ({328: 'motion_write', 384: 'output'} if input_stage else
{488: 'output', 496: 'history_write'})
role = roles.get(off, 'discard')
shape = dump_shape(a.capture_dir, s, off)
if shape:
fmt = shape['format']
lines.append(f'surface {index} {off} {role} {fmt}')
diagnostics.append({'name': name, 'params': parameter_name, 'grid': launch[:3], 'writtenArguments': info['written_buffers']})
initial = bytearray(arena_size)
copied = set()
for s in stages:
for allocation, (base, size) in s['allocs'].items():
r = region_for(base)
if r['mutable'] or base in copied:
continue
data = (s['directory'] / f'alloc-{allocation}.bin').read_bytes()
if len(data) != size:
raise ValueError('truncated captured allocation')
at = arena_offset(base)
initial[at:at+size] = data
copied.add(base)
(a.output / 'arena-init.bin').write_bytes(initial)
(a.output / 'e.txt').write_text('\n'.join(lines) + '\n')
(a.output / 'model.json').write_text(json.dumps({'format': 'D4RE0001', 'preset': a.preset,
'geometry': [rw, rh, ow, oh], 'arenaBytes': arena_size, 'regions': regions, 'stages': diagnostics}, indent=2) + '\n')
print(f'CNN {a.preset}: {len(stages)} stages, {arena_size} arena bytes, {rw}x{rh} -> {ow}x{oh}')
if __name__ == '__main__':
main()
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Build the engine's DLSS Super Resolution CNN model directory (presets E and F) from a one-frame dump.
usage: compile_sr_e.py REPLAY_DIR NVNGX_DLSS_DLL OUT_DIR --preset E|F [--ptx-dir DIR]
REPLAY_DIR: the replay dump (D4R_CUDA_REPLAY_DUMP_DIR) of one frame of preset E or F through NGX, any size, any
flags: its uploads hold the network's weights, which nvngx_dlss.dll keeps packed. The frame (NGX 310.7):
input kernel cuda_engine_input_kernel_rel_{ldr,hdr}_mvdiff_{mvhi,mvlo}: the game's textures (and the warped
history) to the network's 16 input channels per token (4 x 4 output pixels)
network eleven dltss_nchw8 kernels: d4r's own engine/sr/e_net.comp (packed binary16)
reconstruction dltss_nchw8_..._aniso_gaussian_dfn_{fastpath_3x3,mid_3x3,3x3_256,fastpath_5x5,5x5_256}: the last
decoder level, the 40-channel head and the reconstruction of the output and its history
The input and reconstruction kernels are translated from the DLL's PTX (engine/rr/ptx2rr.py) in every variant the
runtime selects from (engine/runtime_e.inc). The reconstruction head defaults to packed half FMAs for RDNA2,
like the rest of the network. --head-accumulation f32 retains the reference's per-MMA rounding; all translated
variants use aligned word accesses for their packed tensors. Writes:
e.txt the manifest
weights.bin the network's weights packed for e_net.comp, then the reconstruction kernels' four
head tensors as NGX uploads them (copied into the arena by the runtime)
<stage>.spv the network stages
input-<v>.spv / .globals.bin, final-<v>.spv / .globals.bin the translated kernels
input.args, final.args their argument blocks from the dump (the runtime rewrites every frame-dependent field)
Everything but d4r's shaders is derived from the user's DLL: it stays on their machine and is never packaged.
"""
import argparse
import glob
import json
import os
import re
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
import numpy as np
HERE = Path(__file__).resolve().parent
NET_SRC = HERE / 'sr' / 'e_net.comp'
# the network: name, the kernel's index in the frame's dltss launches, mode, defines (PS: pixel sets per lane, 2 on the
# large levels), the weights to pack
# (argument offset, layout, in, out) and the biases (argument offsets). Layouts: k3 = NGX's [K/8][tap][N][8],
# k1 = [K/8][N][8], oi = [N][K]; all packed to [tap][k][n] / [k][n] binary16.
NET = [
('enc0', 0, 'ENC0', dict(CIN=16, C=32), [(8, 'k3', 16, 32), (40, 'k1', 32, 64)], [88, 176]),
('enc1', 1, 'ENC3', dict(CIN=64, C=128, IN8=1), [(8, 'k3', 64, 128)], [56]),
('enc2', 2, 'ENC3', dict(CIN=128, C=128), [(8, 'k3', 128, 128)], [56]),
('enc3', 3, 'ENC3', dict(CIN=128, C=256), [(8, 'k3', 128, 256)], [56]),
('mid0', 4, 'CONV1', dict(CIN=256, C=512), [(8, 'k1', 256, 512)], [24]),
('mid1', 5, 'CONV1', dict(CIN=512, C=256), [(8, 'k1', 512, 256)], [24]),
('up4', 6, 'UP1', dict(CIN=256, C=256), [(16, 'k1', 256, 256)], [32]),
('red4', 7, 'CONV1', dict(CIN=256, C=128), [(8, 'k1', 256, 128)], [24]),
('dec3', 8, 'DEC2', dict(CIN=128, C=128, C2=128), [(16, 'k1', 128, 128), (240, 'oi', 128, 128)], [24, 248]),
('dec2', 9, 'DEC2', dict(CIN=128, C=128, C2=64, OUT8=1), [(16, 'k1', 128, 128), (240, 'oi', 128, 64)], [24, 248]),
('dec1', 10, 'DEC2', dict(CIN=64, C=64, C2=32, LOW8=1, SKIP8=1, OUT8=1), [(16, 'k1', 64, 64), (240, 'oi', 64, 32)],
[24, 248]),
]
# the reconstruction kernels' weight arguments (the last decoder level's two convs; NGX's layouts, kept as they are)
HEAD = [(16, 2048), (24, 64), (240, 2560), (248, 80)]
INPUT_VARIANTS = ['ldr_mvdiff_mvhi', 'ldr_mvdiff_mvlo', 'hdr_mvdiff_mvhi', 'hdr_mvdiff_mvlo']
FINAL_VARIANTS = ['fastpath_3x3_128', 'mid_3x3_128', '3x3_256', 'fastpath_5x5_256', '5x5_256']
INPUT_KERNEL = 'cuda_engine_input_kernel_rel_{}'
FINAL_KERNEL = 'dltss_nchw8_bilinear_upsample_conv_1x1_conv_1x1_aniso_gaussian_dfn_{}_032_032_048_032_e5m3_fp16_kernel'
# the translated kernels' argument contract: pointers (arena byte offsets), surfaces and their formats
INPUT_POINTERS, INPUT_SURFACES = [376], {328: 'rg16f', 368: 'rgba16f', 384: 'rgba16f'}
FINAL_POINTERS, FINAL_SURFACES = [0, 8, 16, 24, 240, 248, 256], {488: 'rgba16f', 496: 'rgba16f'}
class Launch:
def __init__(self, d):
self.dir = Path(d)
self.name = self.dir.name.split('-', 2)[2]
self.allocs, self.ptr = {}, {}
for line in (self.dir / 'manifest.txt').read_text().splitlines():
p = line.split()
if p and p[0] == 'alloc':
self.allocs[int(p[1])] = (int(p[2], 16), int(p[3]))
elif p and p[0] == 'pointer':
self.ptr[int(p[1])] = (int(p[2]), int(p[3]))
self.args = (self.dir / 'args.bin').read_bytes()
def arg(self, off, n):
a, d = self.ptr[off]
data = (self.dir / f'alloc-{a}.bin').read_bytes()
if d + n > len(data):
raise ValueError(f'{self.name}: argument {off} is shorter than {n} bytes')
return data[d:d + n]
def pack(raw, layout, cin, cout):
w = np.frombuffer(raw, np.float16)
if layout == 'k3':
return w.reshape(cin // 8, 9, cout, 8).transpose(1, 0, 3, 2).reshape(9 * cin, cout)
if layout == 'k1':
return w.reshape(cin // 8, cout, 8).transpose(0, 2, 1).reshape(cin, cout)
return w.reshape(cout, cin).T
def glsl(source, out, defines=()):
subprocess.run(['glslangValidator', '-V', '--target-env', 'vulkan1.3', *defines, str(source), '-o', str(out)],
check=True, stdout=subprocess.DEVNULL)
def find_module(ptx_dir, kernel):
found = [p for p in sorted(Path(ptx_dir).glob('*.ptx')) if f'.entry {kernel}(' in p.read_text(errors='replace')]
for p in found:
if '.target sm_89' in p.read_text(errors='replace')[:400]:
return p
if found:
return found[-1]
raise FileNotFoundError(f'no PTX module with the entry {kernel}')
def translate(ptx_dir, kernel, out, name, block, pointers, surfaces, packed=False):
comp = out / f'{name}.generated.comp'
cmd = [sys.executable, str(HERE / 'rr' / 'ptx2rr.py'), str(find_module(ptx_dir, kernel)), kernel, str(comp),
'--block', ','.join(map(str, block)), '--weights-arg', '999', '--unified-arena']
cmd += [f'--buffer={b}' for b in pointers] + [f'--surface={o}={f}' for o, f in surfaces.items()]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL)
glsl(comp, out / f'{name}.spv', ['-DD4R_WORD_ARENA', *(['-DD4R_MMA_PACKED'] if packed else [])])
g = comp.with_suffix('.globals.bin')
(out / f'{name}.globals.bin').write_bytes(g.read_bytes() if g.exists() else bytes(16))
info = json.loads(comp.with_suffix('.json').read_text())
for f in (comp, comp.with_suffix('.json'), g):
if f.exists():
f.unlink()
tex = ','.join(map(str, info['textures'])) or '-'
surf = ','.join(f'{o}:{f}' for o, f in info['surfaces']) or '-'
return f'{name}.spv {name}.globals.bin {tex} {surf}'
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('replay', type=Path)
ap.add_argument('dll')
ap.add_argument('output', type=Path)
ap.add_argument('--preset', choices=['E', 'F'], required=True, help='the preset the dump ran (the two differ in weights '
'and in two of the kernels\' constants)')
ap.add_argument('--ptx-dir')
ap.add_argument('--head-accumulation', choices=['f16', 'f32'], default='f16',
help='f16: packed half FMAs for the reconstruction head (RDNA2); f32: reference matrix accumulation')
a = ap.parse_args()
launches = [Launch(d) for d in sorted(glob.glob(str(a.replay / 'replay-*')))]
first = next((i for i, l in enumerate(launches) if l.name.startswith('cuda_engine_input_kernel_rel')), None)
if first is None:
raise SystemExit(f'{a.replay}: no preset E/F frame (cuda_engine_input_kernel_rel_*)')
net = [l for l in launches[first + 1:] if l.name.startswith('dltss_nchw8_')][:12]
if len(net) != 12 or 'aniso_gaussian' not in net[11].name:
raise SystemExit(f'{a.replay}: the frame does not hold the CNN network and its reconstruction')
out = a.output
out.mkdir(parents=True, exist_ok=True)
blob = bytearray()
def put(data):
at = len(blob)
blob.extend(data)
blob.extend(bytes(-len(blob) % 256))
return at
# the reconstruction's per-preset constant at 584 is k / (rx ry): k from the dump's own ratios
fin = net[11].args
ow0, oh0 = struct.unpack_from('<II', fin, 508)[0], struct.unpack_from('<I', fin, 504)[0]
rx0, ry0 = struct.unpack_from('<f', fin, 536)[0], struct.unpack_from('<f', fin, 528)[0]
k584 = struct.unpack_from('<f', fin, 584)[0] * rx0 * ry0
lines = ['D4RE0002', f'preset {5 if a.preset == "E" else 6}', f'kspread {k584:.9g}', 'weights weights.bin',
f'head_accumulation {a.head_accumulation}']
for name, index, mode, defines, ws, bs in NET:
l = net[index]
offs = []
for arg, layout, cin, cout in ws:
offs.append(put(np.ascontiguousarray(pack(l.arg(arg, cin * cout * (9 if layout == 'k3' else 1) * 2),
layout, cin, cout)).tobytes()))
cout_of = [ws[0][3]] + ([ws[1][3]] if len(ws) > 1 else [])
for arg, n in zip(bs, cout_of):
offs.append(put(l.arg(arg, n * 2)))
# word offsets in the order the parameter block takes them: w1 b1 w2 b2
wb = [offs[0] // 4, offs[len(ws)] // 4, (offs[1] // 4) if len(ws) > 1 else 0, (offs[len(ws) + 1] // 4) if len(bs) > 1 else 0]
glsl(NET_SRC, out / f'{name}.spv', [f'-DMODE_{mode}', *[f'-D{k}={v}' for k, v in defines.items()]])
lines.append(f'stage {name} {name}.spv ' + ' '.join(map(str, wb)) + f' {defines.get("PS", 1)}')
head = [put(net[11].arg(arg, n)) for arg, n in HEAD]
lines.append('head ' + ' '.join(f'{arg}:{at}:{n}' for (arg, n), at in zip(HEAD, head)))
(out / 'weights.bin').write_bytes(bytes(blob))
with tempfile.TemporaryDirectory() as tmp:
ptx = a.ptx_dir or tmp
if not a.ptx_dir:
subprocess.run([sys.executable, str(HERE.parent / 'kernels' / 'tools' / 'extract_dlss_ptx.py'), a.dll, tmp],
check=True, stdout=subprocess.DEVNULL)
for v in INPUT_VARIANTS:
lines.append(f'input {v} ' + translate(ptx, INPUT_KERNEL.format(v), out, f'input-{v}', (128, 1, 1),
INPUT_POINTERS, INPUT_SURFACES))
for v in FINAL_VARIANTS:
lines.append(f'final {v} ' + translate(ptx, FINAL_KERNEL.format(v), out, f'final-{v}', (256, 1, 1),
FINAL_POINTERS, FINAL_SURFACES, packed=a.head_accumulation == 'f16'))
# the argument blocks, pointers and texture handles cleared (the runtime fills them)
for name, l, size in (('input', launches[first], 408), ('final', net[11], 656)):
block = bytearray(l.args[:size].ljust(size, b'\0'))
for off in l.ptr:
block[off:off + 8] = bytes(8)
(out / f'{name}.args').write_bytes(bytes(block))
lines += ['template input input.args', 'template final final.args']
(out / 'e.txt').write_text('\n'.join(lines) + '\n')
print(f'Super Resolution CNN model in {out} ({len(blob)} weight bytes)')
if __name__ == '__main__':
main()
+55
View File
@@ -0,0 +1,55 @@
#include "game_e.h"
#include "game_internal.h"
namespace d4r {
struct GameUpscalerE::Impl {
EModel& model;
EEngine engine;
uint32_t width, height, outputWidth, outputHeight, motionWidth, motionHeight;
game_detail::CanonicalInputs fallback;
bool started = false;
Impl(EModel& m, VkPhysicalDevice p, VkDevice d, uint32_t ow, uint32_t oh, uint32_t rw, uint32_t rh, bool hdr, bool lowRes)
: model(m), engine(m, rw, rh, ow, oh, hdr, lowRes), width(rw), height(rh), outputWidth(ow), outputHeight(oh),
motionWidth(lowRes ? rw : ow), motionHeight(lowRes ? rh : oh), fallback(p, d, rw, rh, motionWidth, motionHeight) {}
};
GameUpscalerE::GameUpscalerE(EModel& model, VkPhysicalDevice physical, VkDevice device, uint32_t outputWidth, uint32_t outputHeight,
uint32_t renderWidth, uint32_t renderHeight, bool hdr, bool lowResMotion)
: impl(std::make_unique<Impl>(model, physical, device, outputWidth, outputHeight, renderWidth, renderHeight, hdr, lowResMotion)) {}
GameUpscalerE::~GameUpscalerE() = default;
void GameUpscalerE::record(VkCommandBuffer cb, const GameFrameE& frame) {
auto& g = *impl;
if (!gameColorFormat(frame.color.format) || !gameMotionFormat(frame.motion.format) ||
!gameDepthFormat(frame.depth.format, frame.depth.aspect) || !gameOutputFormat(frame.output.format))
throw std::runtime_error("unsupported game texture format");
const bool directColor = frame.color.view != VK_NULL_HANDLE && !frame.color.x && !frame.color.y;
const bool directMotion = frame.motion.view != VK_NULL_HANDLE && !frame.motion.x && !frame.motion.y;
const bool directDepth = frame.depth.view != VK_NULL_HANDLE && !frame.depth.x && !frame.depth.y;
const bool directOutput = game_detail::directOutput(frame.output, frame.color, frame.motion, frame.depth, false) &&
!frame.output.x && !frame.output.y;
if (!g.started) {
if (!g.model.uploadRecorded()) g.model.recordUpload(cb);
g.engine.recordInitialize(cb);
g.started = true;
}
g.fallback.record(cb, frame.color, frame.motion, frame.depth, g.width, g.height, g.motionWidth, g.motionHeight,
directColor, directMotion, directDepth);
EFrameSettings s;
s.jitter[0] = frame.jitter[0]; s.jitter[1] = frame.jitter[1];
s.motionScale[0] = frame.motionScale[0]; s.motionScale[1] = frame.motionScale[1];
s.reset = frame.reset;
FrameImages images{};
images.color = directColor ? frame.color.view : g.fallback.color.view;
images.motion = directMotion ? frame.motion.view : g.fallback.motion.view;
images.depth = directDepth ? frame.depth.view : g.fallback.depth.view;
images.colorLayout = directColor ? frame.color.layout : VK_IMAGE_LAYOUT_GENERAL;
images.motionLayout = directMotion ? frame.motion.layout : VK_IMAGE_LAYOUT_GENERAL;
images.depthLayout = directDepth ? frame.depth.layout : VK_IMAGE_LAYOUT_GENERAL;
images.exposure = frame.exposure.view;
images.exposureLayout = frame.exposure.layout;
g.engine.setFrame(images, s, directOutput ? FrameOutput{frame.output.view, frame.output.format} : FrameOutput{});
g.engine.recordFrame(cb);
if (!directOutput) game_detail::copyOutput(cb, g.engine.outputImage(), frame.output, g.outputWidth, g.outputHeight);
}
} // namespace d4r
+28
View File
@@ -0,0 +1,28 @@
// Adapter between a game's own textures and the preset E/F engine (DLSS 3's CNN, engine/runtime_e.inc): input
// conversion, the frame and the copy into the game's output texture, all recorded into the game's command buffer.
#pragma once
#include "game.h"
namespace d4r {
struct GameFrameE {
GameImage color, depth, motion, output; // as for GameUpscaler; compatible views at origin zero may be borrowed
GameImage exposure; // HDR: the game's 1x1 float exposure (a sampled view), else 1
float jitter[2] = {0, 0};
float motionScale[2] = {1, 1}; // NGX's MV scale
bool reset = false;
};
class GameUpscalerE {
public:
// The model must outlive this object. Render size fixed for its lifetime (a new size needs a new adapter).
// lowResMotion: motion at the render size (NGX's MVLowRes), otherwise at the output size.
GameUpscalerE(EModel& model, VkPhysicalDevice physical, VkDevice device, uint32_t outputWidth, uint32_t outputHeight,
uint32_t renderWidth, uint32_t renderHeight, bool hdr, bool lowResMotion);
~GameUpscalerE(); // only once every recorded frame has completed on the GPU
GameUpscalerE(const GameUpscalerE&) = delete;
GameUpscalerE& operator=(const GameUpscalerE&) = delete;
void record(VkCommandBuffer command, const GameFrameE& frame);
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace d4r
+196
View File
@@ -0,0 +1,196 @@
// Native Super Resolution preset E/F replay: runs EEngine (engine/runtime_e.inc) on a capture's game inputs, frame
// after frame, and writes the output (and with D4R_E_DUMP_TENSORS=1 the network's tensors) after each frame.
//
// usage: d4r-e MODEL_DIR INPUT_DIR OUT_DIR
// INPUT_DIR: frames.txt ("size RW RH OW OH HDR LOWRES MW MH", then per frame "frame F jx jy mvsx mvsy reset") and
// F-color.bin (RGBA16F, render size), F-motion.bin (RG16F, MW x MH),
// F-depth.bin (R32F, render size, with LOWRES). D4R_E_TIME_REPEAT=N: best of N GPU timings per frame.
#include "runtime.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <vector>
namespace {
void check(VkResult r, const char* operation) {
if (r != VK_SUCCESS) throw std::runtime_error(std::string(operation) + ": VkResult " + std::to_string(r));
}
#define CK(call) check((call), #call)
std::vector<char> read(const std::string& file) {
std::ifstream stream(file, std::ios::binary | std::ios::ate);
if (!stream) throw std::runtime_error("cannot read " + file);
std::vector<char> data(static_cast<size_t>(stream.tellg()));
stream.seekg(0);
stream.read(data.data(), static_cast<std::streamsize>(data.size()));
return data;
}
struct Ctx {
VkPhysicalDevice physical;
VkDevice device;
VkQueue queue;
VkCommandPool pool;
VkPhysicalDeviceMemoryProperties memory;
uint32_t type(uint32_t bits, VkMemoryPropertyFlags flags) const {
for (uint32_t i = 0; i < memory.memoryTypeCount; ++i)
if ((bits & (1u << i)) && (memory.memoryTypes[i].propertyFlags & flags) == flags) return i;
throw std::runtime_error("memory type");
}
template <class F> void submit(F&& record) {
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
ai.commandPool = pool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1;
VkCommandBuffer cb; CK(vkAllocateCommandBuffers(device, &ai, &cb));
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
CK(vkBeginCommandBuffer(cb, &bi)); record(cb); CK(vkEndCommandBuffer(cb));
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO}; si.commandBufferCount = 1; si.pCommandBuffers = &cb;
CK(vkQueueSubmit(queue, 1, &si, VK_NULL_HANDLE)); CK(vkQueueWaitIdle(queue));
vkFreeCommandBuffers(device, pool, 1, &cb);
}
};
struct HostBuffer {
VkBuffer buffer; VkDeviceMemory memory; void* mapped;
HostBuffer(Ctx& c, VkDeviceSize size) {
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bi.size = size;
bi.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
CK(vkCreateBuffer(c.device, &bi, nullptr, &buffer));
VkMemoryRequirements r; vkGetBufferMemoryRequirements(c.device, buffer, &r);
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; ai.allocationSize = r.size;
ai.memoryTypeIndex = c.type(r.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
CK(vkAllocateMemory(c.device, &ai, nullptr, &memory)); CK(vkBindBufferMemory(c.device, buffer, memory, 0));
CK(vkMapMemory(c.device, memory, 0, VK_WHOLE_SIZE, 0, &mapped));
}
};
void copyRegion(VkBufferImageCopy& r, uint32_t w, uint32_t h) {
r = {}; r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; r.imageExtent = {w, h, 1};
}
struct Input {
VkImage image; VkImageView view; VkFormat format;
Input(Ctx& c, VkFormat f, uint32_t w, uint32_t h) : format(f) {
VkImageCreateInfo ii{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
ii.imageType = VK_IMAGE_TYPE_2D; ii.format = f; ii.extent = {w, h, 1}; ii.mipLevels = ii.arrayLayers = 1;
ii.samples = VK_SAMPLE_COUNT_1_BIT; ii.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
CK(vkCreateImage(c.device, &ii, nullptr, &image));
VkMemoryRequirements r; vkGetImageMemoryRequirements(c.device, image, &r);
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; ai.allocationSize = r.size;
ai.memoryTypeIndex = c.type(r.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VkDeviceMemory m; CK(vkAllocateMemory(c.device, &ai, nullptr, &m)); CK(vkBindImageMemory(c.device, image, m, 0));
VkImageViewCreateInfo vi{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
vi.image = image; vi.viewType = VK_IMAGE_VIEW_TYPE_2D; vi.format = f; vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
CK(vkCreateImageView(c.device, &vi, nullptr, &view));
}
};
void barrier(VkCommandBuffer cb, VkImage image, VkImageLayout from, VkImageLayout to) {
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
b.image = image; b.oldLayout = from; b.newLayout = to;
b.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; b.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
}
} // namespace
int main(int argc, char** argv) try {
if (argc != 4) { std::cerr << "usage: d4r-e MODEL_DIR INPUT_DIR OUT_DIR\n"; return 2; }
const std::string modelDir = argv[1], inputDir = argv[2], outDir = argv[3];
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion = VK_API_VERSION_1_3;
VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ici.pApplicationInfo = &app;
VkInstance instance; CK(vkCreateInstance(&ici, nullptr, &instance));
uint32_t count = 8; VkPhysicalDevice devices[8]; vkEnumeratePhysicalDevices(instance, &count, devices);
Ctx c{};
c.physical = devices[0];
for (uint32_t i = 0; i < count; ++i) {
VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(devices[i], &p);
if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) c.physical = devices[i];
}
vkGetPhysicalDeviceMemoryProperties(c.physical, &c.memory);
VkPhysicalDeviceVulkan13Features v13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
VkPhysicalDeviceVulkan12Features v12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, &v13};
VkPhysicalDeviceVulkan11Features v11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, &v12};
VkPhysicalDeviceFeatures2 f2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &v11};
vkGetPhysicalDeviceFeatures2(c.physical, &f2);
uint32_t families = 8; VkQueueFamilyProperties qf[8]; vkGetPhysicalDeviceQueueFamilyProperties(c.physical, &families, qf);
uint32_t family = 0;
for (uint32_t i = 0; i < families; ++i) if (qf[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { family = i; break; }
float priority = 1;
VkDeviceQueueCreateInfo qi{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; qi.queueFamilyIndex = family; qi.queueCount = 1; qi.pQueuePriorities = &priority;
VkDeviceCreateInfo di{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &f2}; di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi;
CK(vkCreateDevice(c.physical, &di, nullptr, &c.device));
vkGetDeviceQueue(c.device, family, 0, &c.queue);
VkCommandPoolCreateInfo pi{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; pi.queueFamilyIndex = family;
pi.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
CK(vkCreateCommandPool(c.device, &pi, nullptr, &c.pool));
auto framesText = read(inputDir + "/frames.txt");
std::istringstream frames(std::string(framesText.begin(), framesText.end()));
std::string key;
uint32_t rw, rh, ow, oh, hdr, lowRes, mw, mh;
frames >> key >> rw >> rh >> ow >> oh >> hdr >> lowRes >> mw >> mh;
d4r::EModel model(c.physical, c.device, modelDir);
d4r::EEngine engine(model, rw, rh, ow, oh, hdr != 0, lowRes != 0);
Input color(c, VK_FORMAT_R16G16B16A16_SFLOAT, rw, rh), motion(c, VK_FORMAT_R16G16_SFLOAT, mw, mh), depth(c, VK_FORMAT_R32_SFLOAT, rw, rh);
HostBuffer staging(c, std::max<VkDeviceSize>(VkDeviceSize(ow) * oh * 8, 64u << 20));
VkQueryPoolCreateInfo qp{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
qp.queryType = VK_QUERY_TYPE_TIMESTAMP; qp.queryCount = 4;
VkQueryPool stamps; CK(vkCreateQueryPool(c.device, &qp, nullptr, &stamps));
VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(c.physical, &props);
const int repeat = std::getenv("D4R_E_TIME_REPEAT") ? std::atoi(std::getenv("D4R_E_TIME_REPEAT")) : 1;
bool first = true;
auto upload = [&](Input& in, const std::string& name, uint32_t f, uint32_t w, uint32_t h, uint32_t texel) {
auto data = read(inputDir + "/" + std::to_string(f) + "-" + name + ".bin");
if (data.size() != size_t(w) * h * texel) throw std::runtime_error("input size: " + name);
std::memcpy(staging.mapped, data.data(), data.size());
c.submit([&](VkCommandBuffer cb) {
barrier(cb, in.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
VkBufferImageCopy r; copyRegion(r, w, h);
vkCmdCopyBufferToImage(cb, staging.buffer, in.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &r);
barrier(cb, in.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL);
});
};
while (frames >> key) {
uint32_t f; int reset;
d4r::EFrameSettings s;
frames >> f >> s.jitter[0] >> s.jitter[1] >> s.motionScale[0] >> s.motionScale[1] >> reset;
s.reset = reset != 0;
upload(color, "color", f, rw, rh, 8);
upload(motion, "motion", f, mw, mh, 4);
if (lowRes) upload(depth, "depth", f, rw, rh, 4);
d4r::FrameImages images{};
images.color = color.view; images.motion = motion.view; images.depth = depth.view;
engine.setFrame(images, s);
if (first) c.submit([&](VkCommandBuffer cb) { model.recordUpload(cb); engine.recordInitialize(cb); first = false; });
double best = 1e30, in = 0, net = 0, fin = 0;
for (int r = 0; r < repeat; ++r) {
if (r) engine.setFrame(images, s);
c.submit([&](VkCommandBuffer cb) {
vkCmdResetQueryPool(cb, stamps, 0, 4);
engine.recordFrame(cb, stamps, 0);
});
uint64_t t[4];
CK(vkGetQueryPoolResults(c.device, stamps, 0, 4, sizeof(t), t, 8, VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT));
const double ms = props.limits.timestampPeriod * 1e-6;
if ((t[3] - t[0]) * ms < best) { best = (t[3] - t[0]) * ms; in = (t[1] - t[0]) * ms; net = (t[2] - t[1]) * ms; fin = (t[3] - t[2]) * ms; }
}
std::cout << "frame " << f << ": " << best << " ms (input " << in << ", network " << net << ", reconstruction " << fin << ")\n";
c.submit([&](VkCommandBuffer cb) {
VkBufferImageCopy r; copyRegion(r, ow, oh);
vkCmdCopyImageToBuffer(cb, engine.outputImage(), VK_IMAGE_LAYOUT_GENERAL, staging.buffer, 1, &r);
});
std::ofstream(outDir + "/" + std::to_string(f) + "-out.bin", std::ios::binary)
.write(static_cast<const char*>(staging.mapped), static_cast<std::streamsize>(size_t(ow) * oh * 8));
if (std::getenv("D4R_E_DUMP_TENSORS"))
for (const auto& d : engine.debugBuffers()) {
c.submit([&](VkCommandBuffer cb) {
VkBufferCopy r{d.offset, 0, d.bytes};
vkCmdCopyBuffer(cb, d.buffer, staging.buffer, 1, &r);
});
std::ofstream(outDir + "/" + std::to_string(f) + "-" + d.name + ".bin", std::ios::binary)
.write(static_cast<const char*>(staging.mapped), static_cast<std::streamsize>(d.bytes));
}
}
return 0;
} catch (const std::exception& e) {
std::cerr << "d4r-e: " << e.what() << "\n";
return 1;
}
+126 -20
View File
@@ -67,6 +67,14 @@ uint HU2_(u16vec2 u) { return uint(u.x) | (uint(u.y) << 16); }
uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); } uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); }
int s16(uint u) { return (int(u) << 16) >> 16; } int s16(uint u) { return (int(u) << 16) >> 16; }
float rcp_a(float x) { return 1.0 / x; } float rcp_a(float x) { return 1.0 / x; }
// add.rz.f32: the sum rounded toward zero (round to nearest, then one ulp back when that went away from zero; the
// error term is exact, TwoSum)
float add_rz(float a, float b) { precise float s = a + b; precise float bb = s - a; precise float e = (a - (s - bb)) + (b - bb);
return (e != 0.0 && ((e < 0.0) != (s < 0.0)) && s != 0.0) ? uintBitsToFloat(floatBitsToUint(s) - 1u) : s; }
float lg2_ftz(float x) { return log2(abs(x) < 1.17549435e-38 ? 0.0 : x); }
// PTX .relu: negative results become +0, NaN stays NaN
f16vec2 relu2(f16vec2 v) { return f16vec2(v.x < 0.0hf ? 0.0hf : v.x, v.y < 0.0hf ? 0.0hf : v.y); }
float16_t relu1(float16_t v) { return v < 0.0hf ? 0.0hf : v; }
vec4 fetch0(sampler2D t, ivec2 c) { const ivec2 n = textureSize(t, 0); return (c.x < 0 || c.y < 0 || c.x >= n.x || c.y >= n.y) ? vec4(0.0) : texelFetch(t, c, 0); } vec4 fetch0(sampler2D t, ivec2 c) { const ivec2 n = textureSize(t, 0); return (c.x < 0 || c.y < 0 || c.x >= n.x || c.y >= n.y) ? vec4(0.0) : texelFetch(t, c, 0); }
// x + copysign(0.5, x) rounded toward zero and then truncated: round half away from zero, exactly // x + copysign(0.5, x) rounded toward zero and then truncated: round half away from zero, exactly
float round_half_away(float x) { float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; } float round_half_away(float x) { float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; }
@@ -101,17 +109,27 @@ uint GW16(uint a) { CHECKW(a); return uint(wbuf.wh[a >> 1]); }
// a 32-bit access is 4-byte aligned (PTX ISA): one word of the same buffer's word view // a 32-bit access is 4-byte aligned (PTX ISA): one word of the same buffer's word view
uint GW32(uint a) { CHECKW(a + 2u); return wbuf32.ww[a >> 2]; } uint GW32(uint a) { CHECKW(a + 2u); return wbuf32.ww[a >> 2]; }
uint GA16(uint a) { CHECKR(a); return uint(abuf.ah[a >> 1]); } uint GA16(uint a) { CHECKR(a); return uint(abuf.ah[a >> 1]); }
uint GA32(uint a) { CHECKR(a + 2u); return uint(abuf.ah[a >> 1]) | (uint(abuf.ah[(a >> 1) + 1u]) << 16); } uint GA32(uint a) { CHECKR(a + 2u);
#ifdef D4R_WORD_ARENA
return abuf32.aw[a >> 2];
#else
return uint(abuf.ah[a >> 1]) | (uint(abuf.ah[(a >> 1) + 1u]) << 16);
#endif
}
void SA16(uint a, uint v) { CHECKS(a); abuf.ah[a >> 1] = uint16_t(v & 0xffffu); } void SA16(uint a, uint v) { CHECKS(a); abuf.ah[a >> 1] = uint16_t(v & 0xffffu); }
void SA32(uint a, uint v) { CHECKS(a + 2u); abuf.ah[a >> 1] = uint16_t(v & 0xffffu); abuf.ah[(a >> 1) + 1u] = uint16_t(v >> 16); } void SA32(uint a, uint v) { CHECKS(a + 2u);
#ifdef D4R_WORD_ARENA
abuf32.aw[a >> 2] = v;
#else
abuf.ah[a >> 1] = uint16_t(v & 0xffffu); abuf.ah[(a >> 1) + 1u] = uint16_t(v >> 16);
#endif
}
// Surface stores with .zero out-of-bounds semantics. // Surface stores with .zero out-of-bounds semantics.
void sust_p4(writeonly image2D s, int x, int y, vec4 v) // Expand stores at the call site: passing a formatted image to an unformatted image2D
{ // parameter makes glslang emit mismatched SPIR-V image types.
ivec2 n = imageSize(s); #define sust_p4(S, X, Y, V) { ivec2 sn_ = imageSize(S); ivec2 sc_ = ivec2(X, Y); if (uint(sc_.x) < uint(sn_.x) && uint(sc_.y) < uint(sn_.y)) imageStore(S, sc_, V); }
if (uint(x) < uint(n.x) && uint(y) < uint(n.y)) imageStore(s, ivec2(x, y), v); #define sust_p1(S, X, Y, V) sust_p4(S, X, Y, vec4(V, 0.0, 0.0, 0.0))
}
void sust_p1(writeonly image2D s, int x, int y, float v) { sust_p4(s, x, y, vec4(v, 0.0, 0.0, 0.0)); }
// Formatted stores to normalized surfaces, as the reference converts them (ZLUDA's surface_formatted_store_bits): NaN // Formatted stores to normalized surfaces, as the reference converts them (ZLUDA's surface_formatted_store_bits): NaN
// to zero, clamp, round half away from zero in f32. The quotient is what the image store then converts back exactly. // to zero, clamp, round half away from zero in f32. The quotient is what the image store then converts back exactly.
// UNORM 10:10:10:2 surfaces (the pooled histories) are RGBA32F images holding the quantised values, like ZLUDA's. // UNORM 10:10:10:2 surfaces (the pooled histories) are RGBA32F images holding the quantised values, like ZLUDA's.
@@ -189,14 +207,8 @@ vec4 q_unorm1010102_hw(vec4 v)
// was one binary16 ulp low in half its values, which the next frame's encoder 0 amplifies): OR-ing in a zero the // was one binary16 ulp low in half its values, which the next frame's encoder 0 amplifies): OR-ing in a zero the
// compiler cannot see (the dispatch's z count is 1) keeps the conversion's rounding. // compiler cannot see (the dispatch's z count is 1) keeps the conversion's rounding.
#define SUST_Z (gl_NumWorkGroups.z - 1u) #define SUST_Z (gl_NumWorkGroups.z - 1u)
void sust_b2(writeonly image2D s, int x, int y, uint a, uint b) #define sust_b2(S, X, Y, A, B) sust_p4(S, (X) >> 2, Y, vec4(float(H((A) | SUST_Z)), float(H((B) | SUST_Z)), 0.0, 0.0))
{ #define sust_b4(S, X, Y, A, B, C, D) sust_p4(S, (X) >> 3, Y, vec4(float(H((A) | SUST_Z)), float(H((B) | SUST_Z)), float(H((C) | SUST_Z)), float(H((D) | SUST_Z))))
sust_p4(s, x >> 2, y, vec4(float(H(a | SUST_Z)), float(H(b | SUST_Z)), 0.0, 0.0));
}
void sust_b4(writeonly image2D s, int x, int y, uint a, uint b, uint c, uint d)
{
sust_p4(s, x >> 3, y, vec4(float(H(a | SUST_Z)), float(H(b | SUST_Z)), float(H(c | SUST_Z)), float(H(d | SUST_Z))));
}
// shfl.sync.up/down within a 32-lane warp (ZLUDA's SHFL_SYNC_IMPL). in_bounds is the predicate output. // shfl.sync.up/down within a 32-lane warp (ZLUDA's SHFL_SYNC_IMPL). in_bounds is the predicate output.
uint shfl_dir(uint v, uint delta, uint opts, bool down, out bool in_bounds) uint shfl_dir(uint v, uint delta, uint opts, bool down, out bool in_bounds)
@@ -507,6 +519,8 @@ class RRT(base.T):
def __init__(self, ptx, kernel, block, weights_arg=40, buffers=()): def __init__(self, ptx, kernel, block, weights_arg=40, buffers=()):
self.weights_arg = weights_arg self.weights_arg = weights_arg
self.buffers = sorted(buffers) # argument offsets of further buffers, each its own binding self.buffers = sorted(buffers) # argument offsets of further buffers, each its own binding
self.written_buffers = set()
self.unified_arena = False
self.snorm16 = set() # texture arguments of --snorm16 self.snorm16 = set() # texture arguments of --snorm16
bx, by, bz = block bx, by, bz = block
if bx * by * bz % 32: if bx * by * bz % 32:
@@ -535,7 +549,7 @@ class RRT(base.T):
# module globals with an initialiser (the output filter's bicubic phase table): constants, laid out one after # module globals with an initialiser (the output filter's bicubic phase table): constants, laid out one after
# another in a blob that is the stage's weight buffer (binding 1); `mov.u64 %rd, NAME` is its offset there # another in a blob that is the stage's weight buffer (binding 1); `mov.u64 %rd, NAME` is its offset there
self.globals, self.globals_blob = {}, b'' self.globals, self.globals_blob = {}, b''
for gm in re.finditer(r'\.global\s+\.align\s+\d+\s+\.b8\s+(\w+)\[(\d+)\]\s*=\s*\{([^}]*)\}', ptx): for gm in re.finditer(r'\.(?:global|const)\s+\.align\s+\d+\s+\.b8\s+(\w+)\[(\d+)\]\s*=\s*\{([^}]*)\}', ptx):
self.globals[gm.group(1)] = len(self.globals_blob) self.globals[gm.group(1)] = len(self.globals_blob)
data = bytes(int(x) for x in gm.group(3).split(',')) data = bytes(int(x) for x in gm.group(3).split(','))
self.globals_blob += data + bytes(-len(data) % 16) self.globals_blob += data + bytes(-len(data) % 16)
@@ -629,6 +643,8 @@ class RRT(base.T):
def gorigin(self, reg): def gorigin(self, reg):
s = self.origin.get(reg, set()) s = self.origin.get(reg, set())
if self.unified_arena and s and 'w' not in s:
return 'a'
if len(s) != 1: if len(s) != 1:
raise base.Err(f'global pointer {reg} has origin {sorted(s)} (need exactly one of w/a)') raise base.Err(f'global pointer {reg} has origin {sorted(s)} (need exactly one of w/a)')
return next(iter(s)) return next(iter(s))
@@ -651,6 +667,16 @@ class RRT(base.T):
return f'uint({self.val(reg, "u64")}) + {off}u' if off >= 0 else f'uint({self.val(reg, "u64")}) - {-off}u' return f'uint({self.val(reg, "u64")}) + {off}u' if off >= 0 else f'uint({self.val(reg, "u64")}) - {-off}u'
def ld(self, o, a): def ld(self, o, a):
if 'const' in o:
m = re.fullmatch(r'\[\s*(\w+)\s*(?:\+\s*(\d+))?\s*\]', a[1].strip())
if not m or m.group(1) not in self.globals:
raise base.Err(f'unsupported constant address {a[1]}')
size, count = mem_shape(o)
at = self.globals[m.group(1)] + int(m.group(2) or 0)
dsts = braces(a[0]) if count > 1 else [a[0]]
for i, d in enumerate(dsts):
self.A(d, f'GW{size * 8}({at + i * size}u)', 'u16' if size == 2 else 'u32')
return
if 'global' not in o: if 'global' not in o:
return super().ld(o, a) return super().ld(o, a)
size, count = mem_shape(o) size, count = mem_shape(o)
@@ -661,12 +687,23 @@ class RRT(base.T):
self.A(d, f'{fn}{size * 8}({self.gbyte(reg, off + i * size)})', 'u16' if size == 2 else 'u32') self.A(d, f'{fn}{size * 8}({self.gbyte(reg, off + i * size)})', 'u16' if size == 2 else 'u32')
def st(self, o, a): def st(self, o, a):
if 'shared' in o and o[-1] in ('u16', 'b16', 'f16') and not {'v2', 'v4'} & set(o):
reg, off = self.addr(a[0])
value = self.val(a[1], 'u16')
self.E('{ uint at_ = ' + self.val(reg, 'u32') + f' + {off}u;')
self.E('uint shift_ = (at_ & 2u) * 8u;')
self.E('atomicAnd(sh[at_ >> 2], ~(0xffffu << shift_));')
self.E(f'atomicOr(sh[at_ >> 2], (uint({value}) & 0xffffu) << shift_); }}')
return
if 'global' not in o: if 'global' not in o:
return super().st(o, a) return super().st(o, a)
size, count = mem_shape(o) size, count = mem_shape(o)
reg, off = self.gaddr(a[0]) reg, off = self.gaddr(a[0])
if self.gorigin(reg) == 'w': if self.gorigin(reg) == 'w':
raise base.Err(f'store through the read-only weight pointer: {a}') raise base.Err(f'store through the read-only weight pointer: {a}')
for origin in self.origin.get(reg, ()):
if origin.startswith('x'):
self.written_buffers.add(int(origin[1:]))
fn = self.gfn(reg, True) fn = self.gfn(reg, True)
vals = braces(a[1]) if count > 1 else [a[1]] vals = braces(a[1]) if count > 1 else [a[1]]
for i, v in enumerate(vals): for i, v in enumerate(vals):
@@ -799,6 +836,20 @@ class RRT(base.T):
return return
if op.startswith('mma.sync.aligned.m16n8k32.row.col.f16.e4m3.e4m3.f16'): if op.startswith('mma.sync.aligned.m16n8k32.row.col.f16.e4m3.e4m3.f16'):
return self.mma(a) return self.mma(a)
if op in ('mma.sync.aligned.m16n8k8.row.col.f16.f16.f16.f16',
'mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16'):
d, x, b, c = (braces(t) for t in a)
depth = 8 if '.m16n8k8.' in op else 16
if (len(d), len(x), len(b), len(c)) != (2, depth // 4, depth // 8, 2):
raise base.Err(f'invalid fragments for {op}')
operands = ', '.join(self.val(r, 'u32') for r in x + b + c)
self.E('{ uint md0_, md1_;')
self.E(f'mma_f16_m16n8k{depth}({operands}, md0_, md1_);')
self.A(d[0], 'md0_', 'u32')
self.A(d[1], 'md1_', 'u32')
return self.E('}')
if op.startswith('mma.sync.'):
raise base.Err(f'unsupported matrix operation {op}')
if op == 'mma.pair.e4m3': if op == 'mma.pair.e4m3':
d, h, x, b, c, e, f = (braces(t) for t in a) d, h, x, b, c, e, f = (braces(t) for t in a)
args = ', '.join(self.val(r, 'u32') for r in x + b + c + e + f) args = ', '.join(self.val(r, 'u32') for r in x + b + c + e + f)
@@ -875,6 +926,51 @@ class RRT(base.T):
c = ('(isnan({0}) || isnan({1}))' if parts[1] == 'nan' else c = ('(isnan({0}) || isnan({1}))' if parts[1] == 'nan' else
'!(isnan({0}) || isnan({1}))' if parts[1] == 'num' else self.cmp(set(parts), True)) '!(isnan({0}) || isnan({1}))' if parts[1] == 'num' else self.cmp(set(parts), True))
return self.A(a[0], f'(({c.format(self.val(a[1], "f16"), self.val(a[2], "f16"))}) ? 1.0hf : 0.0hf)', 'f16') return self.A(a[0], f'(({c.format(self.val(a[1], "f16"), self.val(a[2], "f16"))}) ? 1.0hf : 0.0hf)', 'f16')
if op == 'popc.b32':
return self.A(a[0], f'uint(bitCount({self.val(a[1], "u32")}))', 'u32')
if op in ('dp2a.lo.u32.u32', 'dp2a.hi.u32.u32'):
x, y, c = (self.val(t, 'u32') for t in a[1:])
shift = 0 if parts[1] == 'lo' else 16
return self.A(a[0], f'({c} + ({x} & 0xffffu) * (({y} >> {shift}u) & 0xffu) + '
f'({x} >> 16u) * (({y} >> {shift + 8}u) & 0xffu))', 'u32')
if op.startswith('ldmatrix.sync.aligned.m8n8.'):
regs = braces(a[0])
count = 4 if 'x4' in parts else 2 if 'x2' in parts else 1
if len(regs) != count or parts[-1] != 'b16':
raise base.Err(f'unsupported matrix load {op}')
src, off = self.addr(a[1])
addr = f'({self.val(src, "u32")} + {off}u)'
self.E('{ uint lane_ = gl_SubgroupInvocationID;')
for i, dst in enumerate(regs):
if 'trans' in parts:
self.E(f'uint lo_{i} = subgroupShuffle({addr}, {8*i}u + 2u * (lane_ & 3u)) + (lane_ >> 2u) * 2u;')
self.E(f'uint hi_{i} = subgroupShuffle({addr}, {8*i + 1}u + 2u * (lane_ & 3u)) + (lane_ >> 2u) * 2u;')
self.A(dst, f'(SH16(lo_{i}) | (SH16(hi_{i}) << 16u))', 'u32')
else:
self.E(f'uint row_{i} = subgroupShuffle({addr}, (lane_ >> 2u) + {8*i}u);')
self.A(dst, f'SH32(row_{i} + (lane_ & 3u) * 4u)', 'u32')
return self.E('}')
if op.startswith('cp.async'):
if op == 'cp.async.commit_group' or op.startswith('cp.async.wait_group'):
return # Copies below execute synchronously in program order.
if not op.startswith(('cp.async.cg.shared.global', 'cp.async.ca.shared.global')):
raise base.Err(f'unsupported asynchronous copy {op}')
dst, doff = self.addr(a[0])
src, soff = self.gaddr(a[1])
size = int(a[2])
if size not in (4, 8, 16):
raise base.Err(f'unsupported asynchronous copy size {size}')
valid = self.val(a[3], 'u32') if len(a) > 3 else f'{size}u'
fn = self.gfn(src)
self.E('{ uint dst_ = ' + self.val(dst, 'u32') + f' + {doff}u;')
self.E(f'uint bytes_ = {valid};')
for i in range(size // 4):
self.E(f'uint word_{i} = 0u;')
self.E(f'if (bytes_ > {4*i}u) {{')
self.E(f'uint n_ = min(bytes_ - {4*i}u, 4u);')
self.E(f'word_{i} = {fn}32({self.gbyte(src, soff + 4*i)}) & (0xffffffffu >> ((4u - n_) * 8u)); }}')
self.E(f'sh[(dst_ + {4*i}u) >> 2] = word_{i};')
return self.E('}')
return super().op(op, a) return super().op(op, a)
# ------------------------------------------------ stage contract # ------------------------------------------------ stage contract
@@ -882,6 +978,7 @@ class RRT(base.T):
w('#version 460') w('#version 460')
w('#extension GL_EXT_shader_explicit_arithmetic_types : require') w('#extension GL_EXT_shader_explicit_arithmetic_types : require')
w('#extension GL_EXT_shader_16bit_storage : require') w('#extension GL_EXT_shader_16bit_storage : require')
w('#extension GL_EXT_control_flow_attributes : require')
w('#extension GL_KHR_shader_subgroup_basic : require') w('#extension GL_KHR_shader_subgroup_basic : require')
w('#extension GL_KHR_shader_subgroup_shuffle : require') w('#extension GL_KHR_shader_subgroup_shuffle : require')
w('#extension GL_KHR_shader_subgroup_ballot : require') w('#extension GL_KHR_shader_subgroup_ballot : require')
@@ -897,6 +994,9 @@ class RRT(base.T):
w('layout(set = 0, binding = 1, std430) readonly buffer WeightBuf { uint16_t wh[]; } wbuf;') w('layout(set = 0, binding = 1, std430) readonly buffer WeightBuf { uint16_t wh[]; } wbuf;')
w('layout(set = 0, binding = 1, std430) readonly buffer WeightBuf32 { uint ww[]; } wbuf32;') w('layout(set = 0, binding = 1, std430) readonly buffer WeightBuf32 { uint ww[]; } wbuf32;')
w('layout(set = 0, binding = 2, std430) buffer ArenaBuf { uint16_t ah[]; } abuf;') w('layout(set = 0, binding = 2, std430) buffer ArenaBuf { uint16_t ah[]; } abuf;')
w('#ifdef D4R_WORD_ARENA')
w('layout(set = 0, binding = 2, std430) buffer ArenaBuf32 { uint aw[]; } abuf32;')
w('#endif')
n = 3 n = 3
for off in sorted(self.textures): for off in sorted(self.textures):
w(f'layout(set = 0, binding = {n}) uniform sampler2D T{off};') w(f'layout(set = 0, binding = {n}) uniform sampler2D T{off};')
@@ -906,7 +1006,7 @@ class RRT(base.T):
q = {'rgba8_snorm': 'q_snorm8', 'rgba8': 'q_unorm8', 'rgba32f': 'q_unorm1010102', 'rgb10_a2': 'q_unorm1010102_hw'}.get(self.surfaces[off], '') q = {'rgba8_snorm': 'q_snorm8', 'rgba8': 'q_unorm8', 'rgba32f': 'q_unorm1010102', 'rgb10_a2': 'q_unorm1010102_hw'}.get(self.surfaces[off], '')
w(f'#define SUSTQ_{off}(v) {q}(v)') w(f'#define SUSTQ_{off}(v) {q}(v)')
n += 1 n += 1
for off in self.buffers: for off in (() if self.unified_arena else self.buffers):
w(f'layout(set = 0, binding = {n}, std430) buffer XBuf{off} {{ uint16_t h[]; }} xb{off};') w(f'layout(set = 0, binding = {n}, std430) buffer XBuf{off} {{ uint16_t h[]; }} xb{off};')
w(f'uint GX{off}_16(uint a) {{ return uint(xb{off}.h[a >> 1]); }}') w(f'uint GX{off}_16(uint a) {{ return uint(xb{off}.h[a >> 1]); }}')
w(f'uint GX{off}_32(uint a) {{ return uint(xb{off}.h[a >> 1]) | (uint(xb{off}.h[(a >> 1) + 1u]) << 16); }}') w(f'uint GX{off}_32(uint a) {{ return uint(xb{off}.h[a >> 1]) | (uint(xb{off}.h[(a >> 1) + 1u]) << 16); }}')
@@ -921,7 +1021,8 @@ class RRT(base.T):
w(f'shared uint mms[{384 * (self.bx * self.by * self.bz // 32)}]; // mma_e4m3_f16 scratch, per subgroup') w(f'shared uint mms[{384 * (self.bx * self.by * self.bz // 32)}]; // mma_e4m3_f16 scratch, per subgroup')
def runtime(self): def runtime(self):
return RR_RUNTIME return (RR_RUNTIME + (ROOT / 'engine' / 'round_half.glsl').read_text() +
(ROOT / 'engine' / 'cnn_sr' / 'mma_f16.glsl').read_text())
def main(): def main():
@@ -940,6 +1041,8 @@ def main():
ap.add_argument('--buffer', action='append', type=int, default=[], metavar='OFFSET', ap.add_argument('--buffer', action='append', type=int, default=[], metavar='OFFSET',
help='a pointer argument that is neither the weights nor the arena: its own storage buffer, bound ' help='a pointer argument that is neither the weights nor the arena: its own storage buffer, bound '
'after the surfaces in ascending argument offset (repeatable)') 'after the surfaces in ascending argument offset (repeatable)')
ap.add_argument('--unified-arena', action='store_true',
help='place all --buffer pointer arguments in binding 2; retain their write metadata')
ap.add_argument('--probe', action='append', default=[], metavar='LINE:%REG,...', ap.add_argument('--probe', action='append', default=[], metavar='LINE:%REG,...',
help='debugging: store the registers after the statement at that module line executes (last ' help='debugging: store the registers after the statement at that module line executes (last '
'execution) for workgroup --probe-block, into a debug buffer bound after the surfaces') 'execution) for workgroup --probe-block, into a debug buffer bound after the surfaces')
@@ -956,6 +1059,7 @@ def main():
def make(): def make():
t = RRT(src, ns.kernel, block, ns.weights_arg, ns.buffer) t = RRT(src, ns.kernel, block, ns.weights_arg, ns.buffer)
t.unified_arena = ns.unified_arena
t.snorm16 = set(ns.snorm16) t.snorm16 = set(ns.snorm16)
t.probes = probes t.probes = probes
t.probe_block = tuple(int(x) for x in ns.probe_block.split(',')) t.probe_block = tuple(int(x) for x in ns.probe_block.split(','))
@@ -971,7 +1075,9 @@ def main():
# the binding order (stage contract): textures, then surfaces, in ascending argument offset # the binding order (stage contract): textures, then surfaces, in ascending argument offset
Path(ns.out).with_suffix('.json').write_text(json.dumps( Path(ns.out).with_suffix('.json').write_text(json.dumps(
{'kernel': ns.kernel, 'block': block, 'textures': sorted(t.textures), {'kernel': ns.kernel, 'block': block, 'textures': sorted(t.textures),
'surfaces': [[off, t.surfaces[off]] for off in sorted(t.surfaces)], 'buffers': t.buffers, 'shared': t.shared_bytes, 'surfaces': [[off, t.surfaces[off]] for off in sorted(t.surfaces)],
'buffers': [] if t.unified_arena else t.buffers, 'shared': t.shared_bytes,
'written_buffers': sorted(t.written_buffers),
'probes': [[line, reg] for line in sorted(probes) for reg in probes[line]], 'oob': ns.oob_report}) + '\n') 'probes': [[line, reg] for line in sorted(probes) for reg in probes[line]], 'oob': ns.oob_report}) + '\n')
sys.stderr.write(f'block={block} textures={sorted(t.textures)} surfaces={t.surfaces} shared={t.shared_bytes}\n') sys.stderr.write(f'block={block} textures={sorted(t.textures)} surfaces={t.surfaces} shared={t.shared_bytes}\n')
+16
View File
@@ -36,9 +36,24 @@ struct Device {
VkDevice handle; VkDevice handle;
VkPhysicalDeviceMemoryProperties memory; VkPhysicalDeviceMemoryProperties memory;
VkPhysicalDeviceProperties properties; VkPhysicalDeviceProperties properties;
bool coopmat = false;
Device(VkPhysicalDevice physical, VkDevice device) : handle(device) { Device(VkPhysicalDevice physical, VkDevice device) : handle(device) {
vkGetPhysicalDeviceMemoryProperties(physical, &memory); vkGetPhysicalDeviceMemoryProperties(physical, &memory);
vkGetPhysicalDeviceProperties(physical, &properties); vkGetPhysicalDeviceProperties(physical, &properties);
#ifndef D4R_ENGINE_VK_TABLE
uint32_t count = 0;
VKCHECK(vkEnumerateDeviceExtensionProperties(physical, nullptr, &count, nullptr));
std::vector<VkExtensionProperties> extensions(count);
VKCHECK(vkEnumerateDeviceExtensionProperties(physical, nullptr, &count, extensions.data()));
coopmat = std::any_of(extensions.begin(), extensions.end(), [](const auto& extension) {
return std::strcmp(extension.extensionName, VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME) == 0;
});
if (const char* force = std::getenv("D4R_RR_TEST_NO_COOPMAT"))
if (std::strcmp(force, "1") == 0) coopmat = false;
#endif
// The shim borrows a device whose enabled feature chain is unavailable here.
// CNN stages use the extension-free flavor instead of assuming physical support
// means VK_KHR_cooperative_matrix was enabled by the application.
} }
VkDeviceMemory allocate(VkMemoryRequirements requirements, VkMemoryPropertyFlags flags) const { VkDeviceMemory allocate(VkMemoryRequirements requirements, VkMemoryPropertyFlags flags) const {
for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) { for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) {
@@ -804,4 +819,5 @@ VkDeviceSize Engine::networkHeadBytes(uint32_t outputWidth, uint32_t outputHeigh
VkDeviceSize Engine::activationBytes() const { return impl->activations; } VkDeviceSize Engine::activationBytes() const { return impl->activations; }
#include "runtime_m.inc" #include "runtime_m.inc"
#include "runtime_rr.inc" #include "runtime_rr.inc"
#include "runtime_e.inc"
} // namespace d4r } // namespace d4r
+47
View File
@@ -307,4 +307,51 @@ private:
struct Impl; struct Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
}; };
// ---------------------------------------------------------------- Super Resolution presets E and F (the DLSS 3 CNN)
// NGX 310.7's CNN upscaler: its input kernel and its reconstruction kernel translated from the user's DLL, the
// network between them d4r's own packed-f16 shaders (engine/sr/e_net.comp), so it runs on any GPU with binary16
// arithmetic, cooperative matrices not required (engine/runtime_e.inc, built by engine/compile_sr_e.py). Same
// conventions as the classes above. The device needs shaderFloat16, storageBuffer16BitAccess, shaderInt16,
// subgroupSizeControl and computeFullSubgroups with 32-wide subgroups.
struct EFrameSettings {
float jitter[2] = {0, 0}; // render pixels, as the game passes them to NGX
float motionScale[2] = {1, 1}; // NGX's MV scale: motion texels times this are the motion's pixels
bool reset = false;
};
class EModel {
public:
EModel(VkPhysicalDevice physical, VkDevice device, const std::string& directory, VkPipelineCache cache = VK_NULL_HANDLE);
~EModel();
EModel(const EModel&) = delete;
EModel& operator=(const EModel&) = delete;
void recordUpload(VkCommandBuffer command);
bool uploadRecorded() const;
private:
struct Impl;
std::unique_ptr<Impl> impl;
friend class EEngine;
};
class EEngine {
public:
// hdr: NGX's IsHDR (linear colour, the exposure textures); lowResMotion: MVLowRes (render-size motion vectors,
// in render pixels; otherwise output-size motion in output pixels). Render size at most the output size.
EEngine(EModel& model, uint32_t renderWidth, uint32_t renderHeight, uint32_t outputWidth, uint32_t outputHeight,
bool hdr, bool lowResMotion);
~EEngine();
EEngine(const EEngine&) = delete;
EEngine& operator=(const EEngine&) = delete;
void recordInitialize(VkCommandBuffer command);
// color (and depth with lowResMotion) at the render size, motion at the render or output size; images.exposure
// the game's 1x1 exposure (HDR; 1 when absent). A borrowed RGBA16F finalOutput (GENERAL) is written directly.
void setFrame(const FrameImages& images, const EFrameSettings& settings, FrameOutput finalOutput = {});
// With a timestamp query pool, four stamps are written from query `first`: start, after the input kernel, after
// the network and after the reconstruction.
void recordFrame(VkCommandBuffer command, VkQueryPool stamps = VK_NULL_HANDLE, uint32_t first = 0);
VkImage outputImage() const; // RGBA16F, GENERAL, when no finalOutput was bound
struct DebugBuffer { std::string name; VkBuffer buffer; VkDeviceSize offset, bytes; };
std::vector<DebugBuffer> debugBuffers() const; // the network's tensors in the arena (engine/replay_e.cpp)
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace d4r } // namespace d4r
+490
View File
@@ -0,0 +1,490 @@
// Super Resolution presets E and F (the CNN of nvngx_dlss.dll 310.7): the runtime around engine/sr/e_net.comp and
// the translated input and reconstruction kernels of a "D4RE0002" model (engine/compile_sr_e.py). Included by
// runtime.cpp after runtime_rr.inc (its helpers: Tex, bindRange, put32 / putF / put64).
//
// The frame: the input kernel (the game's colour, depth and motion, the previous output history and motion to the
// network's 16 channels per token, the warped history and this frame's output-size motion), the eleven network
// stages, the reconstruction kernel (the last decoder level, the head, the output and the next history). One buffer
// (the arena) holds every tensor; the translated kernels take its byte offsets in place of NGX's pointers.
//
// The two translated kernels' argument blocks are NGX's (harness captures at seven sizes and ratios, 2026-10-18):
// the dump's block as a template, every field that depends on the sizes, the frame or the flags rewritten here.
namespace {
constexpr unsigned ENet = 11;
constexpr const char* ENames[ENet] = {"enc0", "enc1", "enc2", "enc3", "mid0", "mid1", "up4", "red4", "dec3", "dec2", "dec1"};
// the network's tensors: name, channels, level (L0 = the token grid), 8-bit
struct ETensor { const char* name; uint32_t channels, level; bool e8; };
constexpr ETensor ETensors[] = {
{"A", 16, 0, false}, {"B", 32, 0, true}, {"S1", 64, 1, true}, {"P1", 64, 2, true}, {"K2", 128, 2, false},
{"P2", 128, 3, false}, {"K3", 128, 3, false}, {"P3", 128, 4, false}, {"K4", 256, 4, false}, {"P4", 256, 5, false},
{"Q5", 512, 5, false}, {"R5", 256, 5, false}, {"D4", 256, 4, false}, {"E4", 128, 4, false}, {"D3", 128, 3, false},
{"D2", 64, 2, true}, {"D1", 32, 1, true}};
constexpr unsigned ETensorCount = sizeof(ETensors) / sizeof(ETensors[0]);
// per stage: the level it runs at and its tensors (input, skip or second output, output, third output; -1 none)
struct EStageIO { uint32_t level; int in, skip, out, out2; };
constexpr EStageIO EStages[ENet] = {
{0, 0, 1, 2, 3}, {2, 3, 4, 5, -1}, {3, 5, 6, 7, -1}, {4, 7, 8, 9, -1}, {5, 9, -1, 10, -1}, {5, 10, -1, 11, -1},
{4, 11, 8, 12, -1}, {4, 12, -1, 13, -1}, {3, 13, 6, 14, -1}, {2, 14, 4, 15, -1}, {1, 15, 2, 16, -1}};
struct ETranslated {
std::string shader, globalsFile;
std::vector<uint32_t> textures;
std::vector<std::pair<uint32_t, std::string>> surfaces;
std::unique_ptr<Pipeline> pipeline;
Buffer globals;
std::vector<char> pending;
};
} // namespace
struct EModel::Impl {
Device device;
VkPipelineCache cache = VK_NULL_HANDLE;
std::string directory;
std::array<Pipeline, ENet> stages;
std::array<std::array<uint32_t, 4>, ENet> weightAt{}; // word offsets in weights.bin: w1 b1 w2 b2
std::array<uint32_t, ENet> pixelSets{}; // pixel sets per lane (e_net.comp PS)
struct Head { uint32_t arg; VkDeviceSize at, bytes; };
std::vector<Head> head;
Buffer weights;
std::vector<char> weightsPending;
std::map<std::string, std::unique_ptr<ETranslated>> input, final;
std::vector<uint8_t> inputTemplate, finalTemplate;
bool uploaded = false;
uint32_t preset = 5; // 5: E, 6: F
float kspread = 0.035f; // the reconstruction's constant at 584 times rx ry (E 0.035, F 0.1)
Impl(VkPhysicalDevice physical, VkDevice dev, const std::string& dir, VkPipelineCache c)
: device(physical, dev), cache(c), directory(dir) {
const auto text = read(dir + "/e.txt");
std::istringstream lines(std::string(text.begin(), text.end()));
std::string line;
if (!std::getline(lines, line) || line.rfind("D4RE0002", 0) != 0) throw std::runtime_error("not a preset E/F model (e.txt D4RE0002)");
const auto b = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
std::array<bool, ENet> seen{};
while (std::getline(lines, line)) {
std::istringstream f(line);
std::string key; f >> key;
if (key == "preset") f >> preset;
else if (key == "kspread") f >> kspread;
else if (key == "weights") {
std::string file; f >> file;
weightsPending = read(dir + "/" + file);
weights.create(device, weightsPending.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
} else if (key == "stage") {
std::string name, shader; f >> name >> shader;
unsigned s = 0;
while (s < ENet && name != ENames[s]) ++s;
if (s >= ENet || seen[s]) throw std::runtime_error("e.txt: invalid stage: " + line);
for (auto& w : weightAt[s]) f >> w;
if (!(f >> pixelSets[s])) pixelSets[s] = 1;
if (pixelSets[s] != 1 && pixelSets[s] != 2) throw std::runtime_error("e.txt: invalid stage: " + line);
stages[s].create(device.handle, dir + "/" + shader, {b, b, b}, 32, cache);
seen[s] = true;
} else if (key == "head") {
for (std::string item; f >> item;) {
Head h{};
if (std::sscanf(item.c_str(), "%u:%llu:%llu", &h.arg, reinterpret_cast<unsigned long long*>(&h.at),
reinterpret_cast<unsigned long long*>(&h.bytes)) != 3)
throw std::runtime_error("e.txt: invalid head record");
head.push_back(h);
}
} else if (key == "input" || key == "final") {
// "input|final VARIANT SHADER.spv GLOBALS TEXTURES SURFACES"
std::string variant, textureList, surfaceList;
auto entry = std::make_unique<ETranslated>();
ETranslated& k = *entry;
f >> variant >> k.shader >> k.globalsFile >> textureList >> surfaceList;
std::istringstream tl(textureList == "-" ? "" : textureList), sl(surfaceList == "-" ? "" : surfaceList);
for (std::string item; std::getline(tl, item, ',');) k.textures.push_back(static_cast<uint32_t>(std::stoul(item)));
for (std::string item; std::getline(sl, item, ',');) {
const auto colon = item.find(':');
k.surfaces.emplace_back(static_cast<uint32_t>(std::stoul(item.substr(0, colon))), item.substr(colon + 1));
}
(key == "input" ? input : final)[variant] = std::move(entry);
} else if (key == "template") {
std::string which, file; f >> which >> file;
auto bytes = read(dir + "/" + file);
(which == "input" ? inputTemplate : finalTemplate).assign(bytes.begin(), bytes.end());
}
}
for (unsigned s = 0; s < ENet; ++s)
if (!seen[s]) throw std::runtime_error("e.txt: incomplete preset E/F model");
if (!weights.handle || head.size() != 4 || inputTemplate.size() != 408 || finalTemplate.size() != 656 || input.empty() || final.empty())
throw std::runtime_error("e.txt: incomplete preset E/F model");
}
// a translated kernel, its pipeline built on first use (the engine needs one input and one reconstruction variant)
ETranslated& kernel(bool isInput, const std::string& variant) {
auto& table = isInput ? input : final;
auto found = table.find(variant);
if (found == table.end()) throw std::runtime_error("e.txt: no " + std::string(isInput ? "input" : "reconstruction") + " kernel " + variant);
auto& k = *found->second;
if (!k.pipeline) {
const auto b = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, t = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, s = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
std::vector<VkDescriptorType> types{b, b, b};
types.insert(types.end(), k.textures.size(), t);
types.insert(types.end(), k.surfaces.size(), s);
k.pipeline = std::make_unique<Pipeline>();
k.pipeline->create(device.handle, directory + "/" + k.shader, types, 32, cache);
k.pending = read(directory + "/" + k.globalsFile);
k.globals.create(device, k.pending.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
}
return k;
}
};
EModel::EModel(VkPhysicalDevice physical, VkDevice device, const std::string& directory, VkPipelineCache cache)
: impl(std::make_unique<Impl>(physical, device, directory, cache)) {}
EModel::~EModel() = default;
void EModel::recordUpload(VkCommandBuffer cb) {
auto& m = *impl;
if (m.uploaded) throw std::runtime_error("model upload already recorded");
recordContents(cb, m.weights, m.weightsPending);
memoryBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT);
m.uploaded = true;
}
bool EModel::uploadRecorded() const { return impl->uploaded; }
struct EEngine::Impl {
EModel::Impl& model;
const Device& d;
uint32_t rw, rh, ow, oh, pw, ph, h0, w0; // render, output, the output padded to 128, the token grid
bool hdr, lowRes;
ETranslated *input = nullptr, *final = nullptr;
std::string finalVariant;
uint32_t finalGrid[2] = {0, 0};
Buffer arena, params;
std::array<VkDeviceSize, ETensorCount> at{}, bytes{};
std::array<VkDeviceSize, 4> headAt{};
VkDeviceSize scratch = 0, arenaBytes = 0;
std::array<VkDeviceSize, ENet> paramAt{};
VkDeviceSize inAt = 0, finAt = 0, paramBytes = 0;
std::vector<uint8_t> paramStaging, inBlock, finBlock;
std::array<Tex, 2> history, motionHistory;
Tex warped, discard, zero, exposureOne, out;
bool outNew = false;
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
VkDescriptorPool fixedPool = VK_NULL_HANDLE;
std::array<VkDescriptorSet, ENet> netSets{};
struct Bindings {
FrameImages images{};
FrameOutput finalOutput{};
VkDescriptorPool pool = VK_NULL_HANDLE;
std::array<VkDescriptorSet, 2> input{}, final{};
};
std::vector<Bindings> bindings;
const Bindings* bound = nullptr;
VkQueryPool profile = VK_NULL_HANDLE; // D4R_ENGINE_E_PROFILE=1: microseconds per pass, printed a frame later
bool profiled = false;
float prevJitter[2] = {0, 0};
uint32_t frame = 0;
bool reset = true;
uint32_t lh(uint32_t l) const { return h0 >> l; }
uint32_t lw(uint32_t l) const { return w0 >> l; }
Impl(EModel::Impl& m, uint32_t renderWidth, uint32_t renderHeight, uint32_t outputWidth, uint32_t outputHeight, bool isHdr, bool lowResMotion)
: model(m), d(m.device), rw(renderWidth), rh(renderHeight), ow(outputWidth), oh(outputHeight), hdr(isHdr), lowRes(lowResMotion) {
if (rw < 32 || rh < 32 || rw > ow || rh > oh || ow > 16384 || oh > 16384)
throw std::runtime_error("unsupported preset E/F dimensions");
pw = (ow + 127) & ~127u; ph = (oh + 127) & ~127u; w0 = pw / 4; h0 = ph / 4;
// The mid kernel uses the actual scale/jitter and also covers the integer-rounded 2/3 render sizes.
// Requiring exact 3:2 dimensions sends e.g. 853x533 -> 1280x800 through the much larger generic kernel.
// Restrict this extension to floor/ceil rounding on each axis; retain the generic path at other ratios.
const auto qualityAxis = [](uint32_t render, uint32_t output) {
return render == (2 * output) / 3 || render == (2 * output + 2) / 3;
};
input = &model.kernel(true, std::string(hdr ? "hdr" : "ldr") + "_mvdiff_" + (lowRes ? "mvlo" : "mvhi"));
const double sx = double(ow) / rw, sy = double(oh) / rh, s = std::min(sx, sy);
if (s > 2.5) finalVariant = "fastpath_5x5_256";
else if (s >= 1.6) finalVariant = "fastpath_3x3_128";
else if (qualityAxis(rw, ow) && qualityAxis(rh, oh)) finalVariant = "mid_3x3_128";
else finalVariant = "3x3_256";
if (const char* v = std::getenv("D4R_ENGINE_E_FINAL")) finalVariant = v; // development: force a variant
final = &model.kernel(false, finalVariant);
const bool generic = finalVariant == "3x3_256" || finalVariant == "5x5_256";
finalGrid[0] = (ow + 63) / 64; finalGrid[1] = generic ? (oh + 63) / 64 : (oh + 31) / 32;
// the arena: the network's tensors, the head weights, a scratch word block (NGX's unused pointer 256)
VkDeviceSize a = 0;
auto region = [&](VkDeviceSize n) { const VkDeviceSize r = a; a += (n + 255) & ~VkDeviceSize(255); return r; };
for (unsigned t = 0; t < ETensorCount; ++t) {
const auto& e = ETensors[t];
bytes[t] = VkDeviceSize(e.channels) * lh(e.level) * lw(e.level) * (e.e8 ? 1 : 2);
at[t] = region(bytes[t]);
}
for (unsigned i = 0; i < 4; ++i) headAt[i] = region(model.head[i].bytes);
scratch = region(256);
arenaBytes = a;
const VkBufferUsageFlags storage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
arena.create(d, arenaBytes, storage);
auto place = [&](size_t n) { const VkDeviceSize r = paramBytes; paramBytes += (n + 255) & ~VkDeviceSize(255); return r; };
for (unsigned s = 0; s < ENet; ++s) paramAt[s] = place(64);
inAt = place(model.inputTemplate.size()); finAt = place(model.finalTemplate.size());
params.create(d, paramBytes, storage);
paramStaging.assign(static_cast<size_t>(paramBytes), 0);
for (unsigned s = 0; s < ENet; ++s) {
const auto& io = EStages[s];
uint32_t p[10] = {lh(io.level), lw(io.level), uint32_t(at[io.in]), io.skip >= 0 ? uint32_t(at[io.skip]) : 0u,
uint32_t(at[io.out]), io.out2 >= 0 ? uint32_t(at[io.out2]) : 0u,
model.weightAt[s][0], model.weightAt[s][1], model.weightAt[s][2], model.weightAt[s][3]};
std::memcpy(paramStaging.data() + paramAt[s], p, sizeof(p));
}
const VkFormat half4 = VK_FORMAT_R16G16B16A16_SFLOAT, half2 = VK_FORMAT_R16G16_SFLOAT;
for (int i = 0; i < 2; ++i) { history[i].create(d, ow, oh, half4); motionHistory[i].create(d, ow, oh, half2); }
warped.create(d, ow, oh, half4);
discard.create(d, 1, 1, half4);
zero.create(d, 1, 1, half4);
exposureOne.create(d, 1, 1, VK_FORMAT_R32_SFLOAT);
VkSamplerCreateInfo si{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; si.maxLod = 0;
VKCHECK(vkCreateSampler(d.handle, &si, nullptr, &nearest));
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
VKCHECK(vkCreateSampler(d.handle, &si, nullptr, &linear));
VkDescriptorPoolSize sizes[] = {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 * ENet}};
VkDescriptorPoolCreateInfo pi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pi.maxSets = ENet; pi.poolSizeCount = 1; pi.pPoolSizes = sizes;
VKCHECK(vkCreateDescriptorPool(d.handle, &pi, nullptr, &fixedPool));
for (unsigned s = 0; s < ENet; ++s) {
netSets[s] = descriptorSet(d.handle, fixedPool, model.stages[s].descriptors);
bindRange(d.handle, netSets[s], 0, params, paramAt[s]);
bindBuffer(d.handle, netSets[s], 1, model.weights);
bindBuffer(d.handle, netSets[s], 2, arena);
}
}
~Impl() {
if (profile) vkDestroyQueryPool(d.handle, profile, nullptr);
for (auto& b : bindings) vkDestroyDescriptorPool(d.handle, b.pool, nullptr);
if (fixedPool) vkDestroyDescriptorPool(d.handle, fixedPool, nullptr);
if (nearest) vkDestroySampler(d.handle, nearest, nullptr);
if (linear) vkDestroySampler(d.handle, linear, nullptr);
}
struct Resource { VkImageView view = VK_NULL_HANDLE; bool linear = false; VkImageLayout layout = VK_IMAGE_LAYOUT_GENERAL; };
// what a translated kernel's texture or surface argument is (NGX's wiring in the captures; null where NGX leaves
// it unbound). par: this frame's parity (history written this frame, the other one read)
Resource resolve(bool isInput, bool surface, uint32_t offset, uint32_t par, const FrameImages& in, VkImageView output) const {
const VkImageView exposure = hdr ? (in.exposure ? in.exposure : exposureOne.view) : VK_NULL_HANDLE;
const VkImageLayout exposureLayout = in.exposure ? in.exposureLayout : VK_IMAGE_LAYOUT_GENERAL;
if (isInput) {
if (!surface) switch (offset) {
case 280: return {in.color, true, in.colorLayout};
case 288: return {history[par ^ 1].view, true};
case 304: return {in.motion, false, in.motionLayout};
case 312: return {in.depth, false, in.depthLayout};
case 320: return {motionHistory[par ^ 1].view, true};
case 344: case 352: case 360: return {exposure, false, exposureLayout};
} else switch (offset) {
case 328: return {motionHistory[par].view};
case 368: return {discard.view};
case 384: return {warped.view};
}
} else {
if (!surface) switch (offset) {
case 456: return {in.color, true, in.colorLayout};
case 464: return {warped.view, false};
case 472: case 480: return {exposure, false, exposureLayout};
} else switch (offset) {
case 488: return {output};
case 496: return {history[par].view};
}
}
return {};
}
VkDescriptorSet translatedSet(Bindings& b, bool isInput, uint32_t par, VkImageView output) {
ETranslated& k = isInput ? *input : *final;
VkDescriptorSet set = descriptorSet(d.handle, b.pool, k.pipeline->descriptors);
bindRange(d.handle, set, 0, params, isInput ? inAt : finAt);
bindBuffer(d.handle, set, 1, k.globals);
bindBuffer(d.handle, set, 2, arena);
uint32_t binding = 3;
for (uint32_t offset : k.textures) {
const auto r = resolve(isInput, false, offset, par, b.images, output);
bindImage(d.handle, set, binding++, r.view ? r.view : zero.view, r.linear ? linear : nearest, r.view ? r.layout : VK_IMAGE_LAYOUT_GENERAL);
}
for (const auto& [offset, format] : k.surfaces) {
const auto r = resolve(isInput, true, offset, par, b.images, output);
bindImage(d.handle, set, binding++, r.view ? r.view : discard.view);
}
return set;
}
void setFrame(const FrameImages& images, const EFrameSettings& settings, FrameOutput finalOutput) {
if (!images.color || !images.motion || (lowRes && !images.depth))
throw std::runtime_error("preset E/F needs colour and motion (and depth with render-size motion)");
if (finalOutput.view && finalOutput.format != VK_FORMAT_R16G16B16A16_SFLOAT)
throw std::runtime_error("preset E/F writes RGBA16F output only");
const bool rst = settings.reset || frame == 0;
const float frw = float(rw), frh = float(rh), fow = float(ow), foh = float(oh);
const float rx = frw / fow, ry = frh / foh;
const float pj[2] = {rst ? 0.0f : prevJitter[0], rst ? 0.0f : prevJitter[1]};
auto& p = inBlock;
p = model.inputTemplate;
put32(p, 0, ow); put32(p, 4, oh); put32(p, 8, pw); put32(p, 12, ph); put32(p, 16, pw / 2); put32(p, 20, ph / 2);
put32(p, 24, ow); put32(p, 28, oh); putF(p, 32, 1.0f / fow); putF(p, 36, 1.0f / foh);
put32(p, 40, rw - 1); put32(p, 44, rh - 1);
put32(p, 48, (lowRes ? rw : ow) - 1); put32(p, 52, (lowRes ? rh : oh) - 1);
putF(p, 56, settings.motionScale[0] * (lowRes ? fow / frw : 1.0f)); putF(p, 60, settings.motionScale[1] * (lowRes ? foh / frh : 1.0f));
putF(p, 72, lowRes ? rx : 1.0f); putF(p, 76, lowRes ? ry : 1.0f);
putF(p, 80, settings.jitter[0]); putF(p, 84, settings.jitter[1]);
put32(p, 108, hdr ? 1u : 0u); put32(p, 112, rst ? 1u : 0u);
putF(p, 120, rx); putF(p, 124, ry); putF(p, 128, rx); putF(p, 132, ry);
put32(p, 136, rw); put32(p, 140, rh); putF(p, 144, rx); putF(p, 148, ry);
putF(p, 152, 1.0f / fow); putF(p, 156, 1.0f / foh); putF(p, 160, model.preset == 5 ? 500.0f / foh : 1.0f);
put32(p, 184, lowRes ? 1u : 0u); put32(p, 188, lowRes ? 1u : 0u);
put64(p, 376, at[0]);
auto& o = finBlock;
o = model.finalTemplate;
put32(o, 40, lh(1)); put32(o, 44, lw(1)); put32(o, 60, h0); put32(o, 64, w0);
put32(o, 88, lw(1) * 32); put32(o, 92, w0 * 32); put32(o, 96, w0 * 32);
put32(o, 264, h0); put32(o, 268, w0); put32(o, 284, h0); put32(o, 288, w0);
put32(o, 312, w0 * 32); put32(o, 316, w0 * 32); put32(o, 320, w0 * 40);
put32(o, 504, oh); put32(o, 508, ow); put32(o, 512, hdr ? 1u : 0u);
putF(o, 528, ry); putF(o, 532, 1.0f / ry); putF(o, 536, rx); putF(o, 540, 1.0f / rx);
putF(o, 544, -pj[0]); putF(o, 548, -pj[1]); putF(o, 552, -settings.jitter[0]); putF(o, 556, -settings.jitter[1]);
put32(o, 564, rh - 1); put32(o, 572, rw - 1);
putF(o, 584, model.kspread / (rx * ry));
put64(o, 0, at[16]); put64(o, 8, at[1]);
for (unsigned i = 0; i < 4; ++i) put64(o, model.head[i].arg, headAt[i]);
put64(o, 256, scratch);
if (!finalOutput.view && !out.handle) {
out.create(d, ow, oh, VK_FORMAT_R16G16B16A16_SFLOAT);
outNew = true;
}
const VkImageView output = finalOutput.view ? finalOutput.view : out.view;
// texture and surface handles: 1 where something is bound, 0 where NGX leaves the argument null
for (uint32_t offset : input->textures) put64(p, offset, resolve(true, false, offset, 0, images, output).view ? 1u : 0u);
for (const auto& [offset, format] : input->surfaces) put64(p, offset, offset == 368 ? 0u : 1u); // NGX leaves 368 null
for (uint32_t offset : final->textures) put64(o, offset, resolve(false, false, offset, 0, images, output).view ? 1u : 0u);
for (const auto& [offset, format] : final->surfaces) put64(o, offset, 1u);
auto same = [&](const Bindings& b) { return !std::memcmp(&b.images, &images, sizeof(images)) && b.finalOutput.view == finalOutput.view; };
auto found = std::find_if(bindings.begin(), bindings.end(), same);
if (found == bindings.end()) {
if (bindings.size() >= 16) throw std::runtime_error("too many distinct preset E/F input view sets");
Bindings b;
b.images = images; b.finalOutput = finalOutput;
const uint32_t tex = 2 * uint32_t(input->textures.size() + final->textures.size());
const uint32_t surf = 2 * uint32_t(input->surfaces.size() + final->surfaces.size());
VkDescriptorPoolSize sizes[] = {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 12}, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, tex},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, surf}};
VkDescriptorPoolCreateInfo pi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pi.maxSets = 4; pi.poolSizeCount = 3; pi.pPoolSizes = sizes;
VKCHECK(vkCreateDescriptorPool(d.handle, &pi, nullptr, &b.pool));
for (uint32_t par = 0; par < 2; ++par) {
b.input[par] = translatedSet(b, true, par, output);
b.final[par] = translatedSet(b, false, par, output);
}
bindings.push_back(std::move(b));
found = bindings.end() - 1;
}
bound = &*found;
if (const char* dump = std::getenv("D4R_ENGINE_E_DUMP")) { // development: the frame's two argument blocks
std::ofstream(std::string(dump) + "/" + std::to_string(frame + 1) + "-input.args", std::ios::binary)
.write(reinterpret_cast<const char*>(inBlock.data()), static_cast<std::streamsize>(inBlock.size()));
std::ofstream(std::string(dump) + "/" + std::to_string(frame + 1) + "-final.args", std::ios::binary)
.write(reinterpret_cast<const char*>(finBlock.data()), static_cast<std::streamsize>(finBlock.size()));
}
reset = rst;
prevJitter[0] = settings.jitter[0]; prevJitter[1] = settings.jitter[1];
}
void recordInitialize(VkCommandBuffer cb) {
VkClearColorValue clear{};
VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
std::vector<VkImage> images{warped.handle, discard.handle, zero.handle, exposureOne.handle};
for (int i = 0; i < 2; ++i) { images.push_back(history[i].handle); images.push_back(motionHistory[i].handle); }
for (VkImage image : images) {
imageBarrier(cb, image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
vkCmdClearColorImage(cb, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
}
VkClearColorValue one{}; one.float32[0] = 1.0f;
vkCmdClearColorImage(cb, exposureOne.handle, VK_IMAGE_LAYOUT_GENERAL, &one, 1, &range);
vkCmdFillBuffer(cb, arena.handle, 0, VK_WHOLE_SIZE, 0);
memoryBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT);
// the head weights into the arena (the reconstruction kernel reads them there)
for (unsigned i = 0; i < 4; ++i) {
VkBufferCopy region{model.head[i].at, headAt[i], model.head[i].bytes};
vkCmdCopyBuffer(cb, model.weights.handle, arena.handle, 1, &region);
}
for (const auto* k : {input, final})
if (!k->pending.empty()) recordContents(cb, k->globals, const_cast<ETranslated*>(k)->pending);
memoryBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
frame = 0; reset = true;
}
void recordFrame(VkCommandBuffer cb, VkQueryPool stamps, uint32_t first) {
if (!bound) throw std::runtime_error("EEngine::setFrame was not called");
const auto& b = *bound;
const uint32_t cur = frame & 1u;
auto stamp = [&](uint32_t n) { if (stamps) vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, stamps, first + n); };
auto step = [&] {
memoryBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
};
if (outNew) {
clearNewImage(cb, out.handle);
outNew = false;
}
constexpr uint32_t passes = ENet + 2;
if (!profile && std::getenv("D4R_ENGINE_E_PROFILE")) {
VkQueryPoolCreateInfo qi{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
qi.queryType = VK_QUERY_TYPE_TIMESTAMP; qi.queryCount = passes + 1;
VKCHECK(vkCreateQueryPool(d.handle, &qi, nullptr, &profile));
}
if (profile && profiled) {
uint64_t t[passes + 1];
if (vkGetQueryPoolResults(d.handle, profile, 0, passes + 1, sizeof(t), t, 8, VK_QUERY_RESULT_64_BIT) == VK_SUCCESS) {
double total = 0;
std::fprintf(stderr, "sr cnn profile (us):");
for (uint32_t i = 0; i < passes; ++i) {
const double us = (t[i + 1] - t[i]) * d.properties.limits.timestampPeriod * 1e-3;
total += us;
std::fprintf(stderr, " %s %.1f", i == 0 ? "input" : i == passes - 1 ? "final" : ENames[i - 1], us);
}
std::fprintf(stderr, " total %.1f\n", total);
}
}
uint32_t mark = 0;
auto pass = [&] { if (profile) vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, profile, mark++); };
if (profile) vkCmdResetQueryPool(cb, profile, 0, passes + 1);
stamp(0);
memoryBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
std::memcpy(paramStaging.data() + inAt, inBlock.data(), inBlock.size());
std::memcpy(paramStaging.data() + finAt, finBlock.data(), finBlock.size());
for (VkDeviceSize a = 0; a < paramBytes; a += 65536)
vkCmdUpdateBuffer(cb, params.handle, a, std::min<VkDeviceSize>(65536, paramBytes - a), paramStaging.data() + a);
memoryBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
pass();
dispatch(cb, *input->pipeline, b.input[cur], (ow + 15) / 16, (oh + 15) / 16);
step(); stamp(1); pass();
for (unsigned s = 0; s < ENet; ++s) {
const uint32_t l = EStages[s].level;
const uint32_t ps = model.pixelSets[s];
uint32_t gx, gy = 1;
if (s == 0) { gx = w0 / 16; gy = h0 / (8 * ps); }
else if (s <= 3) { gx = (lw(l) + 7) / 8; gy = (lh(l) + 4 * ps - 1) / (4 * ps); }
else gx = (lw(l) * lh(l) + 32 * ps - 1) / (32 * ps);
dispatch(cb, model.stages[s], netSets[s], gx, gy);
step(); pass();
}
stamp(2);
dispatch(cb, *final->pipeline, b.final[cur], finalGrid[0], finalGrid[1]);
memoryBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT);
stamp(3); pass();
profiled = profile != VK_NULL_HANDLE;
++frame;
}
};
EEngine::EEngine(EModel& model, uint32_t renderWidth, uint32_t renderHeight, uint32_t outputWidth, uint32_t outputHeight, bool hdr, bool lowResMotion)
: impl(std::make_unique<Impl>(*model.impl, renderWidth, renderHeight, outputWidth, outputHeight, hdr, lowResMotion)) {
if (!model.impl->uploaded) {} // the caller records the model's upload before the first frame
}
EEngine::~EEngine() = default;
void EEngine::recordInitialize(VkCommandBuffer cb) { impl->recordInitialize(cb); }
void EEngine::setFrame(const FrameImages& images, const EFrameSettings& settings, FrameOutput finalOutput) { impl->setFrame(images, settings, finalOutput); }
void EEngine::recordFrame(VkCommandBuffer cb, VkQueryPool stamps, uint32_t first) { impl->recordFrame(cb, stamps, first); }
VkImage EEngine::outputImage() const { return impl->out.handle; }
std::vector<EEngine::DebugBuffer> EEngine::debugBuffers() const {
std::vector<DebugBuffer> list;
for (unsigned t = 0; t < ETensorCount; ++t) list.push_back({ETensors[t].name, impl->arena.handle, impl->at[t], impl->bytes[t]});
return list;
}
+408
View File
@@ -0,0 +1,408 @@
#version 460
// The network of DLSS Super Resolution presets E and F (nvngx_dlss.dll 310.7: the "dltss_nchw8_*" kernels between
// NGX's input kernel and its reconstruction kernel) as d4r's own packed-f16 shaders, for any GPU with binary16
// arithmetic and no cooperative matrices required (RADV on RDNA2 included).
//
// The network is a U-Net over the token grid (L0: the output size rounded up to 128, divided by 4; level l is L0 / 2^l):
// enc0 conv 3x3 16 -> 32, ReLU -> skip0 (8-bit, L0); 2x2 max-pool, conv 1x1 32 -> 64, ReLU -> skip1 (8-bit, L1);
// its 2x2 max-pool -> pool1 (8-bit, L2)
// enc1 conv 3x3 64 -> 128 (8-bit in) -> skip2 (L2), pool -> pool2 (L3) enc2 128 -> 128 at L3, enc3 128 -> 256
// at L4 (both binary16): MODE_ENC3
// mid0 conv 1x1 256 -> 512, mid1 512 -> 256 at L5: MODE_CONV1
// up4 bilinear 2x of mid1 + skip4, conv 1x1 256 -> 256 (MODE_UP1); red4 conv 1x1 256 -> 128 (MODE_CONV1)
// dec3, dec2, dec1 bilinear 2x of the level below + the skip, conv 1x1 C -> M, ReLU, conv 1x1 M -> N, ReLU
// (MODE_DEC2); dec1's output and skip0 feed NGX's reconstruction kernel (translated, engine/compile_e.py), which
// runs the last decoder level and the head itself.
// Every conv is followed by its bias and a ReLU. Tensors are NVIDIA's: [C/8][H][W][8] ("nchw8") for the pooled and
// decoder outputs, and the full-size skips as the four 2x2 phases [y&1][x&1][C/8][H/2][W/2][8] ("sub"); 8-bit
// activations are binary16 bits 14..7 (round half up, as cnn_common.glsl's enc8).
//
// Arithmetic: a lane owns a pixel, a wave 32 pixels (a 4 x 8 block in Morton order, so 2x2 pools are quads of
// lanes) and 32 output channels; the weights are wave-uniform scalar loads feeding v_pk_fma_f16 (as
// engine/rr/cnn_pk.glsl). Binary16 accumulation: not bit-exact to NGX's f16 mma (about 45-80 dB per tensor).
//
// Bindings: 0 the parameter block (below), 1 the weights (binary16, [tap][k][n] / [k][n], n fastest), 2 the arena
// (the frame's tensors; every offset in bytes).
#extension GL_EXT_shader_explicit_arithmetic_types : require
#extension GL_EXT_shader_16bit_storage : require
#extension GL_EXT_control_flow_attributes : require
#extension GL_KHR_shader_subgroup_shuffle : require
#extension GL_KHR_shader_subgroup_basic : require
layout(std430, binding = 0) readonly buffer Params { uint p[]; };
layout(std430, binding = 1) readonly restrict buffer Weights { uint w[]; }; // binary16 pairs
layout(std430, binding = 1) readonly restrict buffer Weights4 { uvec4 w4[]; };
layout(std430, binding = 2) buffer Arena { uint a[]; };
layout(std430, binding = 2) buffer Arena4 { uvec4 a4[]; };
layout(std430, binding = 2) buffer Arena2 { uvec2 a2[]; };
// the parameter block (words): 0 H, 1 W (this stage's level), 2 input, 3 skip (MODE_UP1 / MODE_DEC2) or second
// output, 4 output, 5 third output, 6.. weights and biases (word offsets in binding 1): 6 w1, 7 b1, 8 w2, 9 b2
#define PH p[0]
#define PW p[1]
#ifndef CIN
#define CIN 32
#endif
#ifndef C
#define C 32 // the stage's (first) conv outputs
#endif
#ifndef C2
#define C2 32 // MODE_ENC0 / MODE_DEC2: the second conv's outputs
#endif
#define NQ 32 // a wave's output channels
#ifndef LOW8
#define LOW8 0
#endif
#ifndef SKIP8
#define SKIP8 0
#endif
#ifndef IN8
#define IN8 0
#endif
#ifndef OUT8
#define OUT8 0
#endif
#if defined(MODE_DEC2)
#define WAVES ((C > C2 ? C : C2) / NQ)
#elif defined(MODE_ENC0)
#define WAVES 4 // a 8 x 16 tile of L0; its pooled 4 x 8 block of L1 is the second conv's
#else
#define WAVES (C / NQ)
#endif
layout(local_size_x = 32 * WAVES) in;
// gl_SubgroupID, not the invocation index / 32: the compiler then knows a wave's index (and every weight address
// built from it) is uniform, and loads the weights with scalar loads
uint wave() { return gl_SubgroupID; }
uint lane() { return gl_LocalInvocationIndex & 31u; }
// a lane's pixel in its wave's 4 x 8 block: x = b0 + 2 b2 + 4 b4, y = b1 + 2 b3
uvec2 morton(uint l) { return uvec2((l & 1u) | ((l >> 1u) & 2u) | ((l >> 2u) & 4u), ((l >> 1u) & 1u) | ((l >> 2u) & 2u)); }
f16vec2 h2(uint u) { return unpackFloat2x16(u); }
uint u2(f16vec2 v) { return packFloat2x16(v); }
// 8-bit activations <-> binary16
f16vec4 dec8x4(uint u)
{
const uint lo = ((u << 7u) & 0x7f80u) | ((u << 15u) & 0x7f800000u);
const uint hi = ((u >> 9u) & 0x7f80u) | ((u >> 1u) & 0x7f800000u);
return f16vec4(h2(lo), h2(hi));
}
uint enc8x4(f16vec2 x, f16vec2 y)
{
const f16vec2 ex = h2(u2(x) & 0xfc00fc00u), ey = h2(u2(y) & 0xfc00fc00u);
const uint rx = u2(fma(ex, f16vec2(0.0625), x)), ry = u2(fma(ey, f16vec2(0.0625), y));
return ((rx >> 7u) & 0xffu) | ((rx >> 15u) & 0xff00u) | ((ry << 9u) & 0xff0000u) | ((ry << 1u) & 0xff000000u);
}
// eight channels (one nchw8 group g) of a pixel of a [CH/8][H][W][8] tensor at byte offset `at`
void load8(uint at, uint g, uint h, uint wd, uint y, uint x, bool in8, out f16vec2 v[4])
{
const uint e = (g * h + y) * wd + x;
if (in8) {
const uvec2 b = a2[at / 8u + e];
const f16vec4 lo = dec8x4(b.x), hi = dec8x4(b.y);
v[0] = lo.xy; v[1] = lo.zw; v[2] = hi.xy; v[3] = hi.zw;
} else {
const uvec4 b = a4[at / 16u + e];
v[0] = h2(b.x); v[1] = h2(b.y); v[2] = h2(b.z); v[3] = h2(b.w);
}
}
// the same from a 2x2-phase ("sub") tensor of a level of size h x wd
void loadSub(uint at, uint g, uint ch, uint h, uint wd, uint y, uint x, bool in8, out f16vec2 v[4])
{
const uint ph = (y & 1u) * 2u + (x & 1u);
load8(at + ph * (ch * (h / 2u) * (wd / 2u)) * (in8 ? 1u : 2u), g, h / 2u, wd / 2u, y >> 1u, x >> 1u, in8, v);
}
void store8(uint at, uint g, uint h, uint wd, uint y, uint x, bool out8, f16vec2 v[4])
{
const uint e = (g * h + y) * wd + x;
if (out8) a2[at / 8u + e] = uvec2(enc8x4(v[0], v[1]), enc8x4(v[2], v[3]));
else a4[at / 16u + e] = uvec4(u2(v[0]), u2(v[1]), u2(v[2]), u2(v[3]));
}
void storeSub(uint at, uint g, uint ch, uint h, uint wd, uint y, uint x, bool out8, f16vec2 v[4])
{
const uint ph = (y & 1u) * 2u + (x & 1u);
store8(at + ph * (ch * (h / 2u) * (wd / 2u)) * (out8 ? 1u : 2u), g, h / 2u, wd / 2u, y >> 1u, x >> 1u, out8, v);
}
#ifndef PS
#define PS 1 // pixel sets per lane: 2 reuses every scalar-loaded weight for two pixels
#endif
// acc[i][j] (pixel set i, output pair j of the wave's 32) += the eight channels v[i] (k = k0 .. k0 + 7) times the
// weight rows k0 .. k0 + 7 of a [k][N] matrix at word offset wrow (N / 2 words per row), the wave's columns from
// word column wcol
void fma8(inout f16vec2 acc[PS][16], f16vec2 v[PS][4], uint wrow, uint rowWords, uint wcol)
{
[[unroll]] for (uint kk = 0u; kk < 8u; ++kk) {
const uint r = wrow + kk * rowWords + wcol;
[[unroll]] for (uint j4 = 0u; j4 < 4u; ++j4) {
const uvec4 q = w4[r / 4u + j4];
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
const float16_t x = (kk & 1u) == 0u ? v[i][kk >> 1u].x : v[i][kk >> 1u].y;
acc[i][4u * j4] = fma(f16vec2(x), h2(q.x), acc[i][4u * j4]);
acc[i][4u * j4 + 1u] = fma(f16vec2(x), h2(q.y), acc[i][4u * j4 + 1u]);
acc[i][4u * j4 + 2u] = fma(f16vec2(x), h2(q.z), acc[i][4u * j4 + 2u]);
acc[i][4u * j4 + 3u] = fma(f16vec2(x), h2(q.w), acc[i][4u * j4 + 3u]);
}
}
}
}
void biasRelu(inout f16vec2 acc[PS][16], uint b, uint wcol)
{
[[unroll]] for (uint j4 = 0u; j4 < 4u; ++j4) {
const uvec4 q = w4[(b + wcol) / 4u + j4];
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
acc[i][4u * j4] = max(acc[i][4u * j4] + h2(q.x), f16vec2(0.0));
acc[i][4u * j4 + 1u] = max(acc[i][4u * j4 + 1u] + h2(q.y), f16vec2(0.0));
acc[i][4u * j4 + 2u] = max(acc[i][4u * j4 + 2u] + h2(q.z), f16vec2(0.0));
acc[i][4u * j4 + 3u] = max(acc[i][4u * j4 + 3u] + h2(q.w), f16vec2(0.0));
}
}
}
void zero(out f16vec2 acc[PS][16]) { [[unroll]] for (uint i = 0u; i < uint(PS); ++i) [[unroll]] for (uint j = 0u; j < 16u; ++j) acc[i][j] = f16vec2(0.0); }
// the 2x2 max over a lane quad (xor 1, 2: Morton order)
void pool4(inout f16vec2 acc[16])
{
[[unroll]] for (uint j = 0u; j < 16u; ++j) {
acc[j] = max(acc[j], h2(subgroupShuffleXor(u2(acc[j]), 1u)));
acc[j] = max(acc[j], h2(subgroupShuffleXor(u2(acc[j]), 2u)));
}
}
// the wave's 32 channels (groups gq .. gq + 3) of a pixel
void storeAll(uint at, uint gq, uint h, uint wd, uint y, uint x, bool out8, bool sub, uint ch, f16vec2 acc[16])
{
[[unroll]] for (uint g = 0u; g < 4u; ++g) {
f16vec2 v[4] = f16vec2[4](acc[4u * g], acc[4u * g + 1u], acc[4u * g + 2u], acc[4u * g + 3u]);
if (sub) storeSub(at, gq + g, ch, h, wd, y, x, out8, v); else store8(at, gq + g, h, wd, y, x, out8, v);
}
}
// the input of MODE_UP1 / MODE_DEC2 at (y, x), eight channels of group g: the level below (h/2 x wd/2) upsampled
// 2x bilinearly (half-pixel centres, edges clamped: 0.75 / 0.25 per axis) plus the skip
void upSkip(uint g, uint y, uint x, out f16vec2 v[4])
{
const uint h = PH, wd = PW, lh = h / 2u, lw = wd / 2u;
const uint iy = y >> 1u, ix = x >> 1u;
const uint ny = uint(clamp(int(iy) + ((y & 1u) == 0u ? -1 : 1), 0, int(lh) - 1));
const uint nx = uint(clamp(int(ix) + ((x & 1u) == 0u ? -1 : 1), 0, int(lw) - 1));
f16vec2 a0[4], a1[4], b0[4], b1[4], s[4];
load8(p[2], g, lh, lw, iy, ix, LOW8 != 0, a0); load8(p[2], g, lh, lw, iy, nx, LOW8 != 0, a1);
load8(p[2], g, lh, lw, ny, ix, LOW8 != 0, b0); load8(p[2], g, lh, lw, ny, nx, LOW8 != 0, b1);
loadSub(p[3], g, uint(CIN), h, wd, y, x, SKIP8 != 0, s);
[[unroll]] for (uint i = 0u; i < 4u; ++i) {
const f16vec2 r0 = fma(f16vec2(0.25), a1[i], f16vec2(0.75) * a0[i]);
const f16vec2 r1 = fma(f16vec2(0.25), b1[i], f16vec2(0.75) * b0[i]);
v[i] = fma(f16vec2(0.25), r1, f16vec2(0.75) * r0) + s[i];
}
}
#if defined(MODE_ENC0)
shared f16vec2 sh[32 * PS * 18]; // pooled pixels, padded to avoid LDS bank conflicts
#endif
// the convolution's input in LDS, loaded once by the workgroup (all its loads in flight, and its waves share it):
// the tile of pixels (with the 3x3 conv's one-pixel halo) x CIN channels, binary16, rows of CINP halves (CIN / 2 + 2
// words: 2 mod 4, so a 64-bit read's 16 lanes hit distinct bank pairs)
#if defined(MODE_DEC2) && CIN != C
#error "decoder LDS reuse requires CIN == C"
#endif
#define CINP (CIN + 4)
#if defined(MODE_ENC0)
#define TW 16 // four waves of 4 PS x 8 pixels
#define TH (8 * PS)
#elif defined(MODE_ENC3)
#define TW 8 // every wave the same 4 PS x 8 pixels
#define TH (4 * PS)
#endif
#ifdef TW
#define TILEPX ((TH + 2) * (TW + 2))
#else
#define TILEPX (32 * PS)
#endif
#if TILEPX * CINP * 2 > 65536
#error "CNN tile exceeds 64 KiB of shared memory; reduce PS"
#endif
shared f16vec4 tile[TILEPX * CINP / 4];
void tileStore(uint px, uint g, f16vec2 v[4])
{
const uint at = (px * uint(CINP) + g * 8u) / 4u;
tile[at] = f16vec4(v[0], v[1]); tile[at + 1u] = f16vec4(v[2], v[3]);
}
void tileLoad(uint px, uint g, out f16vec2 v[4])
{
const uint at = (px * uint(CINP) + g * 8u) / 4u;
const f16vec4 a = tile[at], b = tile[at + 1u];
v[0] = a.xy; v[1] = a.zw; v[2] = b.xy; v[3] = b.zw;
}
#ifdef TW
// the tile of the [CIN/8][H][W][8] input around the block at (y0, x0), zero outside
void loadTile(uint at, uint h, uint wd, int y0, int x0, bool in8)
{
for (uint i = gl_LocalInvocationIndex; i < uint(TILEPX * CIN / 8); i += gl_WorkGroupSize.x) {
const uint px = i / uint(CIN / 8), g = i % uint(CIN / 8);
const int y = y0 - 1 + int(px / uint(TW + 2)), x = x0 - 1 + int(px % uint(TW + 2));
f16vec2 v[4];
if (y >= 0 && y < int(h) && x >= 0 && x < int(wd)) load8(at, g, h, wd, uint(y), uint(x), in8, v);
else { v[0] = f16vec2(0.0); v[1] = f16vec2(0.0); v[2] = f16vec2(0.0); v[3] = f16vec2(0.0); }
tileStore(px, g, v);
}
barrier();
}
#endif
#ifdef TW
// the 3x3 conv (CIN -> C) of the tile's pixels (ly + 4 i, lx) (block coordinates), the wave's 32 outputs
void conv3x3(inout f16vec2 acc[PS][16], uint ly, uint lx, uint w1, uint wcol)
{
[[dont_unroll]] for (uint t = 0u; t < 9u; ++t) {
const uint px = (ly + t / 3u) * uint(TW + 2) + lx + t % 3u;
[[dont_unroll]] for (uint g = 0u; g < uint(CIN) / 8u; ++g) {
f16vec2 v[PS][4];
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) tileLoad(px + 4u * i * uint(TW + 2), g, v[i]);
fma8(acc, v, w1 + (t * uint(CIN) + g * 8u) * uint(C / 2), uint(C / 2), wcol);
}
}
}
#endif
// conv 1x1 over the tile's rows (32 PS pixels, set i the pixels 32 i + lane), K channels, the wave's 32 outputs
void conv1x1(inout f16vec2 acc[PS][16], uint lane, uint k, uint wrow, uint n, uint wcol)
{
[[dont_unroll]] for (uint g = 0u; g < k / 8u; ++g) {
f16vec2 v[PS][4];
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) tileLoad(32u * i + lane, g, v[i]);
fma8(acc, v, wrow + g * 8u * (n / 2u), n / 2u, wcol);
}
}
void main()
{
const uint wv = wave(), l = lane();
f16vec2 acc[PS][16];
zero(acc);
const uvec2 m = morton(l);
#if defined(MODE_ENC0)
// a 8 PS x 16 tile of L0: wave wv its 4 PS x 8 part, pixel set i the 4 x 8 block i of that
const uint wx = (wv & 1u) * 8u, wy = (wv >> 1u) * 4u * uint(PS);
const uint H0 = PH, W0 = PW;
loadTile(p[2], H0, W0, int(gl_WorkGroupID.y * uint(TH)), int(gl_WorkGroupID.x * 16u), false);
conv3x3(acc, wy + m.y, wx + m.x, p[6], 0u);
biasRelu(acc, p[7], 0u);
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
const uint x = gl_WorkGroupID.x * 16u + wx + m.x, y = gl_WorkGroupID.y * uint(TH) + wy + 4u * i + m.y;
storeAll(p[3], 0u, H0, W0, y, x, true, true, 32u, acc[i]); // skip0: 8-bit, phases
pool4(acc[i]);
// the pooled L1 pixel of every lane quad into the tile's 4 PS x 8 L1 block (4 x 8 Morton blocks again)
if ((l & 3u) == 0u) {
const uint lx = (wv & 1u) * 4u + (m.x >> 1u), ly = (wv >> 1u) * 2u * uint(PS) + 2u * i + (m.y >> 1u);
const uint b = ly >> 2u, yy = ly & 3u;
const uint q = b * 32u + ((lx & 1u) | ((yy & 1u) << 1u) | ((lx & 2u) << 1u) | ((yy & 2u) << 2u) | ((lx & 4u) << 2u));
[[unroll]] for (uint j = 0u; j < 16u; ++j) sh[q * 18u + j] = acc[i][j];
}
}
barrier();
// conv 1x1 32 -> 64 at L1: wave wv the outputs 32 (wv & 1) .. +31 of the L1 block wv >> 1 (PS of them)
if (wv < 2u * uint(PS)) {
const uint b = wv >> 1u, hf = wv & 1u;
const uint x1 = gl_WorkGroupID.x * 8u + m.x, y1 = gl_WorkGroupID.y * 4u * uint(PS) + 4u * b + m.y;
f16vec2 a1[1][16];
[[unroll]] for (uint j = 0u; j < 16u; ++j) a1[0][j] = f16vec2(0.0);
[[unroll]] for (uint g = 0u; g < 4u; ++g) {
const uint at = (b * 32u + l) * 18u + 4u * g;
f16vec2 v[1][4] = f16vec2[1][4](f16vec2[4](sh[at], sh[at + 1u], sh[at + 2u], sh[at + 3u]));
#if PS == 1
fma8(a1, v, p[8] + g * 8u * 32u, 32u, hf * 16u);
#else
// fma8 takes PS pixel sets: the second conv runs one set per wave
[[unroll]] for (uint kk = 0u; kk < 8u; ++kk) {
const uint r = p[8] + g * 8u * 32u + kk * 32u + hf * 16u;
const float16_t xv = (kk & 1u) == 0u ? v[0][kk >> 1u].x : v[0][kk >> 1u].y;
[[unroll]] for (uint j4 = 0u; j4 < 4u; ++j4) {
const uvec4 qv = w4[r / 4u + j4];
a1[0][4u * j4] = fma(f16vec2(xv), h2(qv.x), a1[0][4u * j4]);
a1[0][4u * j4 + 1u] = fma(f16vec2(xv), h2(qv.y), a1[0][4u * j4 + 1u]);
a1[0][4u * j4 + 2u] = fma(f16vec2(xv), h2(qv.z), a1[0][4u * j4 + 2u]);
a1[0][4u * j4 + 3u] = fma(f16vec2(xv), h2(qv.w), a1[0][4u * j4 + 3u]);
}
}
#endif
}
{
[[unroll]] for (uint j4 = 0u; j4 < 4u; ++j4) {
const uvec4 qv = w4[(p[9] + hf * 16u) / 4u + j4];
a1[0][4u * j4] = max(a1[0][4u * j4] + h2(qv.x), f16vec2(0.0));
a1[0][4u * j4 + 1u] = max(a1[0][4u * j4 + 1u] + h2(qv.y), f16vec2(0.0));
a1[0][4u * j4 + 2u] = max(a1[0][4u * j4 + 2u] + h2(qv.z), f16vec2(0.0));
a1[0][4u * j4 + 3u] = max(a1[0][4u * j4 + 3u] + h2(qv.w), f16vec2(0.0));
}
}
storeAll(p[4], hf * 4u, H0 / 2u, W0 / 2u, y1, x1, true, true, 64u, a1[0]); // skip1: 8-bit, phases
pool4(a1[0]);
if ((l & 3u) == 0u) storeAll(p[5], hf * 4u, H0 / 4u, W0 / 4u, y1 >> 1u, x1 >> 1u, true, false, 64u, a1[0]); // pool1
}
#elif defined(MODE_ENC3)
// a 4 PS x 8 block of the level, wave wv the outputs 32 wv .. +31; skip (binary16, phases) and its pool (nchw8)
loadTile(p[2], PH, PW, int(gl_WorkGroupID.y * uint(TH)), int(gl_WorkGroupID.x * 8u), IN8 != 0);
conv3x3(acc, m.y, m.x, p[6], wv * 16u);
biasRelu(acc, p[7], wv * 16u);
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
const uint x = gl_WorkGroupID.x * 8u + m.x, y = gl_WorkGroupID.y * uint(TH) + 4u * i + m.y;
const bool inside = x < PW && y < PH;
if (inside) storeAll(p[3], wv * 4u, PH, PW, y, x, false, true, uint(C), acc[i]);
pool4(acc[i]);
if (inside && (l & 3u) == 0u) storeAll(p[4], wv * 4u, PH / 2u, PW / 2u, y >> 1u, x >> 1u, false, false, uint(C), acc[i]);
}
#else
// a row-major run of 32 PS pixels (set i: 32 i + lane): its input staged in LDS once for all the waves
for (uint e = gl_LocalInvocationIndex; e < uint(32 * PS * CIN / 8); e += gl_WorkGroupSize.x) {
const uint q = gl_WorkGroupID.x * 32u * uint(PS) + e / uint(CIN / 8), g = e % uint(CIN / 8), qy = q / PW, qx = q % PW;
f16vec2 v[4];
if (qy >= PH) { v[0] = f16vec2(0.0); v[1] = f16vec2(0.0); v[2] = f16vec2(0.0); v[3] = f16vec2(0.0); }
#if defined(MODE_UP1) || defined(MODE_DEC2)
else upSkip(g, qy, qx, v);
#else
else load8(p[2], g, PH, PW, qy, qx, false, v);
#endif
tileStore(e / uint(CIN / 8), g, v);
}
barrier();
#if defined(MODE_DEC2)
// conv 1 (C outputs over C / 32 waves) into LDS, then conv 2 (C2 outputs over C2 / 32 waves)
if (wv < uint(C) / 32u) {
conv1x1(acc, l, uint(CIN), p[6], uint(C), wv * 16u);
biasRelu(acc, p[7], wv * 16u);
}
// All waves finish reading the input before the hidden layer reuses its padded LDS rows.
barrier();
if (wv < uint(C) / 32u) {
[[unroll]] for (uint i = 0u; i < uint(PS); ++i)
[[unroll]] for (uint g = 0u; g < 4u; ++g) {
f16vec2 v[4] = f16vec2[4](acc[i][4u * g], acc[i][4u * g + 1u], acc[i][4u * g + 2u], acc[i][4u * g + 3u]);
tileStore(32u * i + l, wv * 4u + g, v);
}
}
barrier();
if (wv < uint(C2) / 32u) {
zero(acc);
[[dont_unroll]] for (uint g = 0u; g < uint(C) / 8u; ++g) {
f16vec2 v[PS][4];
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
tileLoad(32u * i + l, g, v[i]);
}
fma8(acc, v, p[8] + g * 8u * uint(C2 / 2), uint(C2 / 2), wv * 16u);
}
biasRelu(acc, p[9], wv * 16u);
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
const uint px = gl_WorkGroupID.x * 32u * uint(PS) + 32u * i + l, y = px / PW, x = px % PW;
if (y < PH) storeAll(p[4], wv * 4u, PH, PW, y, x, OUT8 != 0, false, uint(C2), acc[i]);
}
}
#else
conv1x1(acc, l, uint(CIN), p[6], uint(C), wv * 16u);
biasRelu(acc, p[7], wv * 16u);
[[unroll]] for (uint i = 0u; i < uint(PS); ++i) {
const uint px = gl_WorkGroupID.x * 32u * uint(PS) + 32u * i + l, y = px / PW, x = px % PW;
if (y < PH) storeAll(p[4], wv * 4u, PH, PW, y, x, false, false, uint(C), acc[i]);
}
#endif
#endif
}
+5
View File
@@ -26,6 +26,10 @@ bool loadVulkan(PFN_vkGetInstanceProcAddr instanceProc, VkInstance instance, VkD
ok &= (vkTable.CmdPipelineBarrier = reinterpret_cast<PFN_vkCmdPipelineBarrier>(deviceProc(device, "vkCmdPipelineBarrier"))) != nullptr; ok &= (vkTable.CmdPipelineBarrier = reinterpret_cast<PFN_vkCmdPipelineBarrier>(deviceProc(device, "vkCmdPipelineBarrier"))) != nullptr;
ok &= (vkTable.CmdUpdateBuffer = reinterpret_cast<PFN_vkCmdUpdateBuffer>(deviceProc(device, "vkCmdUpdateBuffer"))) != nullptr; ok &= (vkTable.CmdUpdateBuffer = reinterpret_cast<PFN_vkCmdUpdateBuffer>(deviceProc(device, "vkCmdUpdateBuffer"))) != nullptr;
ok &= (vkTable.CmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(deviceProc(device, "vkCmdWriteTimestamp"))) != nullptr; ok &= (vkTable.CmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(deviceProc(device, "vkCmdWriteTimestamp"))) != nullptr;
ok &= (vkTable.CmdResetQueryPool = reinterpret_cast<PFN_vkCmdResetQueryPool>(deviceProc(device, "vkCmdResetQueryPool"))) != nullptr;
ok &= (vkTable.CreateQueryPool = reinterpret_cast<PFN_vkCreateQueryPool>(deviceProc(device, "vkCreateQueryPool"))) != nullptr;
ok &= (vkTable.DestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(deviceProc(device, "vkDestroyQueryPool"))) != nullptr;
ok &= (vkTable.GetQueryPoolResults = reinterpret_cast<PFN_vkGetQueryPoolResults>(deviceProc(device, "vkGetQueryPoolResults"))) != nullptr;
ok &= (vkTable.CreateBuffer = reinterpret_cast<PFN_vkCreateBuffer>(deviceProc(device, "vkCreateBuffer"))) != nullptr; ok &= (vkTable.CreateBuffer = reinterpret_cast<PFN_vkCreateBuffer>(deviceProc(device, "vkCreateBuffer"))) != nullptr;
ok &= (vkTable.CreateComputePipelines = reinterpret_cast<PFN_vkCreateComputePipelines>(deviceProc(device, "vkCreateComputePipelines"))) != nullptr; ok &= (vkTable.CreateComputePipelines = reinterpret_cast<PFN_vkCreateComputePipelines>(deviceProc(device, "vkCreateComputePipelines"))) != nullptr;
ok &= (vkTable.CreateDescriptorPool = reinterpret_cast<PFN_vkCreateDescriptorPool>(deviceProc(device, "vkCreateDescriptorPool"))) != nullptr; ok &= (vkTable.CreateDescriptorPool = reinterpret_cast<PFN_vkCreateDescriptorPool>(deviceProc(device, "vkCreateDescriptorPool"))) != nullptr;
@@ -46,6 +50,7 @@ bool loadVulkan(PFN_vkGetInstanceProcAddr instanceProc, VkInstance instance, VkD
ok &= (vkTable.DestroyShaderModule = reinterpret_cast<PFN_vkDestroyShaderModule>(deviceProc(device, "vkDestroyShaderModule"))) != nullptr; ok &= (vkTable.DestroyShaderModule = reinterpret_cast<PFN_vkDestroyShaderModule>(deviceProc(device, "vkDestroyShaderModule"))) != nullptr;
ok &= (vkTable.FreeMemory = reinterpret_cast<PFN_vkFreeMemory>(deviceProc(device, "vkFreeMemory"))) != nullptr; ok &= (vkTable.FreeMemory = reinterpret_cast<PFN_vkFreeMemory>(deviceProc(device, "vkFreeMemory"))) != nullptr;
ok &= (vkTable.GetBufferMemoryRequirements = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(deviceProc(device, "vkGetBufferMemoryRequirements"))) != nullptr; ok &= (vkTable.GetBufferMemoryRequirements = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(deviceProc(device, "vkGetBufferMemoryRequirements"))) != nullptr;
ok &= (vkTable.GetBufferDeviceAddress = reinterpret_cast<PFN_vkGetBufferDeviceAddress>(deviceProc(device, "vkGetBufferDeviceAddress"))) != nullptr;
ok &= (vkTable.GetImageMemoryRequirements = reinterpret_cast<PFN_vkGetImageMemoryRequirements>(deviceProc(device, "vkGetImageMemoryRequirements"))) != nullptr; ok &= (vkTable.GetImageMemoryRequirements = reinterpret_cast<PFN_vkGetImageMemoryRequirements>(deviceProc(device, "vkGetImageMemoryRequirements"))) != nullptr;
ok &= (vkTable.MapMemory = reinterpret_cast<PFN_vkMapMemory>(deviceProc(device, "vkMapMemory"))) != nullptr; ok &= (vkTable.MapMemory = reinterpret_cast<PFN_vkMapMemory>(deviceProc(device, "vkMapMemory"))) != nullptr;
ok &= (vkTable.UnmapMemory = reinterpret_cast<PFN_vkUnmapMemory>(deviceProc(device, "vkUnmapMemory"))) != nullptr; ok &= (vkTable.UnmapMemory = reinterpret_cast<PFN_vkUnmapMemory>(deviceProc(device, "vkUnmapMemory"))) != nullptr;
+10
View File
@@ -24,6 +24,10 @@ struct VkTable {
PFN_vkCmdPipelineBarrier CmdPipelineBarrier = nullptr; PFN_vkCmdPipelineBarrier CmdPipelineBarrier = nullptr;
PFN_vkCmdUpdateBuffer CmdUpdateBuffer = nullptr; PFN_vkCmdUpdateBuffer CmdUpdateBuffer = nullptr;
PFN_vkCmdWriteTimestamp CmdWriteTimestamp = nullptr; PFN_vkCmdWriteTimestamp CmdWriteTimestamp = nullptr;
PFN_vkCmdResetQueryPool CmdResetQueryPool = nullptr;
PFN_vkCreateQueryPool CreateQueryPool = nullptr;
PFN_vkDestroyQueryPool DestroyQueryPool = nullptr;
PFN_vkGetQueryPoolResults GetQueryPoolResults = nullptr;
PFN_vkCreateBuffer CreateBuffer = nullptr; PFN_vkCreateBuffer CreateBuffer = nullptr;
PFN_vkCreateComputePipelines CreateComputePipelines = nullptr; PFN_vkCreateComputePipelines CreateComputePipelines = nullptr;
PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr; PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr;
@@ -44,6 +48,7 @@ struct VkTable {
PFN_vkDestroyShaderModule DestroyShaderModule = nullptr; PFN_vkDestroyShaderModule DestroyShaderModule = nullptr;
PFN_vkFreeMemory FreeMemory = nullptr; PFN_vkFreeMemory FreeMemory = nullptr;
PFN_vkGetBufferMemoryRequirements GetBufferMemoryRequirements = nullptr; PFN_vkGetBufferMemoryRequirements GetBufferMemoryRequirements = nullptr;
PFN_vkGetBufferDeviceAddress GetBufferDeviceAddress = nullptr;
PFN_vkGetImageMemoryRequirements GetImageMemoryRequirements = nullptr; PFN_vkGetImageMemoryRequirements GetImageMemoryRequirements = nullptr;
PFN_vkMapMemory MapMemory = nullptr; PFN_vkMapMemory MapMemory = nullptr;
PFN_vkUnmapMemory UnmapMemory = nullptr; PFN_vkUnmapMemory UnmapMemory = nullptr;
@@ -72,6 +77,10 @@ bool loadVulkan(PFN_vkGetInstanceProcAddr instanceProc, VkInstance instance, VkD
#define vkCmdPipelineBarrier ::d4r::vkTable.CmdPipelineBarrier #define vkCmdPipelineBarrier ::d4r::vkTable.CmdPipelineBarrier
#define vkCmdUpdateBuffer ::d4r::vkTable.CmdUpdateBuffer #define vkCmdUpdateBuffer ::d4r::vkTable.CmdUpdateBuffer
#define vkCmdWriteTimestamp ::d4r::vkTable.CmdWriteTimestamp #define vkCmdWriteTimestamp ::d4r::vkTable.CmdWriteTimestamp
#define vkCmdResetQueryPool ::d4r::vkTable.CmdResetQueryPool
#define vkCreateQueryPool ::d4r::vkTable.CreateQueryPool
#define vkDestroyQueryPool ::d4r::vkTable.DestroyQueryPool
#define vkGetQueryPoolResults ::d4r::vkTable.GetQueryPoolResults
#define vkCreateBuffer ::d4r::vkTable.CreateBuffer #define vkCreateBuffer ::d4r::vkTable.CreateBuffer
#define vkCreateComputePipelines ::d4r::vkTable.CreateComputePipelines #define vkCreateComputePipelines ::d4r::vkTable.CreateComputePipelines
#define vkCreateDescriptorPool ::d4r::vkTable.CreateDescriptorPool #define vkCreateDescriptorPool ::d4r::vkTable.CreateDescriptorPool
@@ -92,6 +101,7 @@ bool loadVulkan(PFN_vkGetInstanceProcAddr instanceProc, VkInstance instance, VkD
#define vkDestroyShaderModule ::d4r::vkTable.DestroyShaderModule #define vkDestroyShaderModule ::d4r::vkTable.DestroyShaderModule
#define vkFreeMemory ::d4r::vkTable.FreeMemory #define vkFreeMemory ::d4r::vkTable.FreeMemory
#define vkGetBufferMemoryRequirements ::d4r::vkTable.GetBufferMemoryRequirements #define vkGetBufferMemoryRequirements ::d4r::vkTable.GetBufferMemoryRequirements
#define vkGetBufferDeviceAddress ::d4r::vkTable.GetBufferDeviceAddress
#define vkGetImageMemoryRequirements ::d4r::vkTable.GetImageMemoryRequirements #define vkGetImageMemoryRequirements ::d4r::vkTable.GetImageMemoryRequirements
#define vkMapMemory ::d4r::vkTable.MapMemory #define vkMapMemory ::d4r::vkTable.MapMemory
#define vkUnmapMemory ::d4r::vkTable.UnmapMemory #define vkUnmapMemory ::d4r::vkTable.UnmapMemory
+14 -4
View File
@@ -4,9 +4,8 @@
# #
# usage: bash d4r/tools/build-engine-models.sh [--rr-only | --sr-only] [--exact] [D4R_FOLDER] # usage: bash d4r/tools/build-engine-models.sh [--rr-only | --sr-only] [--exact] [D4R_FOLDER]
# bash d4r/tools/build-engine-models.sh --find-proton prints the GE-Proton it would use (or nothing) # bash d4r/tools/build-engine-models.sh --find-proton prints the GE-Proton it would use (or nothing)
#
# Ray Reconstruction engine/rr-d, rr-e, rr-f from d4r/nvngx_dlssd.dll 310.9.1 alone (about half a minute) # Ray Reconstruction engine/rr-d, rr-e, rr-f from d4r/nvngx_dlssd.dll 310.9.1 alone (about half a minute)
# Super Resolution engine/k, k-ldr, m, m-ldr, l, l-ldr # Super Resolution engine/k, k-ldr, m, m-ldr, l, l-ldr, e, f
# NVIDIA's DLSS keeps these weights packed in nvngx_dlss.dll; they are read where NGX unpacks # NVIDIA's DLSS keeps these weights packed in nvngx_dlss.dll; they are read where NGX unpacks
# them: the bundled D3D12 test app renders one frame per model through this d4r folder under # them: the bundled D3D12 test app renders one frame per model through this d4r folder under
# GE-Proton, and the uploads are saved (a minute or two each; the first run also compiles # GE-Proton, and the uploads are saved (a minute or two each; the first run also compiles
@@ -121,13 +120,13 @@ if (( DO_SR )); then
fail "d4r's d3d12.dll / d3d12core.dll not found next to $RUNTIME" fail "d4r's d3d12.dll / d3d12core.dll not found next to $RUNTIME"
fi fi
winpath() { printf 'Z:%s' "$(printf '%s' "$1" | tr '/' '\\')"; } winpath() { printf 'Z:%s' "$(printf '%s' "$1" | tr '/' '\\')"; }
dump() { # dump NAME PRESET: the uploads of one frame of preset PRESET in WORK/NAME/replay dump() { # dump NAME PRESET PATTERN [FLAGS]: the uploads of one frame of preset PRESET in WORK/NAME/replay
local w="$WORK/$1" local w="$WORK/$1"
mkdir -p "$w/replay" mkdir -p "$w/replay"
printf ' rendering one frame with model %s through d4r (GE-Proton %s)...\n' "$1" "$(basename "$PROTON")" printf ' rendering one frame with model %s through d4r (GE-Proton %s)...\n' "$1" "$(basename "$PROTON")"
(cd "$APP" && env STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_ROOT" STEAM_COMPAT_DATA_PATH="$WORK/prefix" \ (cd "$APP" && env STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_ROOT" STEAM_COMPAT_DATA_PATH="$WORK/prefix" \
PROTON_FORCE_NVAPI=1 DXVK_NVAPI_GPU_ARCH=AD100 WINEDLLOVERRIDES='d3d12=n,b;d3d12core=n,b;d4r12core=n,b' \ PROTON_FORCE_NVAPI=1 DXVK_NVAPI_GPU_ARCH=AD100 WINEDLLOVERRIDES='d3d12=n,b;d3d12core=n,b;d4r12core=n,b' \
D4R_ENGINE=0 D4R_DLSS_PRESET="$2" D4R_HARNESS_CREATE_FLAGS=0x4B D4R_SHIM_LOG="$(winpath "$w/shim.log")" \ D4R_ENGINE=0 D4R_DLSS_PRESET="$2" D4R_HARNESS_CREATE_FLAGS="${4:-0x4B}" D4R_SHIM_LOG="$(winpath "$w/shim.log")" \
D4R_CUDA_REPLAY_DUMP_DIR="$w/replay" D4R_CUDA_REPLAY_DUMP_LIMIT=60 \ D4R_CUDA_REPLAY_DUMP_DIR="$w/replay" D4R_CUDA_REPLAY_DUMP_LIMIT=60 \
"$PROTON/proton" run "$APP/d3d12_dlss_harness.exe" "$(winpath "$RUNTIME/nvngx.dll")" "$(winpath "$w/out.raw")" \ "$PROTON/proton" run "$APP/d3d12_dlss_harness.exe" "$(winpath "$RUNTIME/nvngx.dll")" "$(winpath "$w/out.raw")" \
1 640 360 1280 720) > "$w/run.log" 2>&1 || true 1 640 360 1280 720) > "$w/run.log" 2>&1 || true
@@ -157,5 +156,16 @@ if (( DO_SR )); then
done done
rm -rf -- "${WORK:?}/$1" rm -rf -- "${WORK:?}/$1"
done done
# presets E and F (DLSS 3's CNN): one frame each, LDR with render-size motion (the model serves every flag set)
echo 'Building the Super Resolution CNN models (E and F)...'
PTX="$WORK/ptx"
run ptx.log python3 "$KIT/kernels/tools/extract_dlss_ptx.py" "$DLL" "$PTX"
for c in "e 5 E" "f 6 F"; do
set -- $c
dump "$1" "$2" cuda_engine_input_kernel_rel 0xa
run "$1.log" python3 "$KIT/engine/compile_sr_e.py" "$WORK/$1/replay" "$DLL" "$WORK/$1-out" --preset "$3" --ptx-dir "$PTX"
place "$1" "$WORK/$1-out" $(ls "$WORK/$1-out")
rm -rf -- "${WORK:?}/$1"
done
fi fi
printf 'Engine models are in %s/engine.\n' "$RUNTIME" printf 'Engine models are in %s/engine.\n' "$RUNTIME"
+4 -1
View File
@@ -11,9 +11,12 @@
; K | 4 | DLAA / Quality / Balanced | Default; faster alternative to M/L ; K | 4 | DLAA / Quality / Balanced | Default; faster alternative to M/L
; M | 4.5 | Performance | More demanding on RDNA3 ; M | 4.5 | Performance | More demanding on RDNA3
; L | 4.5 | Ultra Performance | Especially 4K; experimental ; L | 4.5 | Ultra Performance | Especially 4K; experimental
; E | 3 CNN | Quality / Balanced / Performance | Legacy faster fallback ; E | 3 CNN | Quality / Balanced / Performance | Legacy faster fallback; the engine's choice on RDNA2
; F | 3 CNN | DLAA / Ultra Performance | Deprecated; prefer K or L ; F | 3 CNN | DLAA / Ultra Performance | Deprecated; prefer K or L
; ;
; On the engine ([Engine]) E and F run from engine\e and engine\f (build-engine-models.sh) without cooperative
; matrices, so they are the models for RDNA2 GPUs (Steam Deck: about 5 ms at 640x360 -> 1280x720).
;
; Select the upscaling mode separately in game/OptiScaler. These are recommendations; ; Select the upscaling mode separately in game/OptiScaler. These are recommendations;
; other modes are allowed. L requires the L texture build. ; other modes are allowed. L requires the L texture build.
; game = use the game/OptiScaler choice. Benchmarks: D4R_README.txt. ; game = use the game/OptiScaler choice. Benchmarks: D4R_README.txt.
+1 -1
View File
@@ -24,7 +24,7 @@ PARAM_OBJECT="$ROOT/build/d4r_ngx_param_msvc.obj"
# The engine (D4R_ENGINE) reaches Vulkan through a function table: MinGW has no Vulkan import library. # The engine (D4R_ENGINE) reaches Vulkan through a function table: MinGW has no Vulkan import library.
"$MINGW_CXX" -std=c++20 -O2 -Wall -Wextra -Wno-missing-field-initializers -DD4R_ENGINE_VK_TABLE -I"$ROOT/build/vulkan-include" -I"$ROOT/build/vulkan-staging" -shared -static -static-libgcc -static-libstdc++ \ "$MINGW_CXX" -std=c++20 -O2 -Wall -Wextra -Wno-missing-field-initializers -DD4R_ENGINE_VK_TABLE -I"$ROOT/build/vulkan-include" -I"$ROOT/build/vulkan-staging" -shared -static -static-libgcc -static-libstdc++ \
"$ROOT/tools/d4r_nvngx_shim.cpp" "$ROOT/tools/d4r_ngx_param_host.cpp" "$PARAM_OBJECT" \ "$ROOT/tools/d4r_nvngx_shim.cpp" "$ROOT/tools/d4r_ngx_param_host.cpp" "$PARAM_OBJECT" \
"$ROOT/engine/runtime.cpp" "$ROOT/engine/game.cpp" "$ROOT/engine/game_m.cpp" "$ROOT/engine/game_rr.cpp" "$ROOT/engine/vk_dispatch.cpp" \ "$ROOT/engine/runtime.cpp" "$ROOT/engine/game.cpp" "$ROOT/engine/game_m.cpp" "$ROOT/engine/game_rr.cpp" "$ROOT/engine/game_e.cpp" "$ROOT/engine/vk_dispatch.cpp" \
-ldxgi -lbcrypt -Wl,--entry,d4r_dll_entry -o "$ROOT/build/d4r_nvngx.dll" -ldxgi -lbcrypt -Wl,--entry,d4r_dll_entry -o "$ROOT/build/d4r_nvngx.dll"
printf 'Built %s\n' "$ROOT/build/d4r_nvngx.dll" printf 'Built %s\n' "$ROOT/build/d4r_nvngx.dll"
cp "$ROOT/build/d4r_nvngx.dll" "$ROOT/build/d4r.asi" cp "$ROOT/build/d4r_nvngx.dll" "$ROOT/build/d4r.asi"
+3 -1
View File
@@ -16,7 +16,7 @@ HARNESS="${D4R_HARNESS_EXE:-$ROOT/build/d3d12_dlss_harness.exe}"
[[ -f "$HARNESS" ]] || { echo "missing $HARNESS (scripts/build_d3d12_dlss_harness.sh)" >&2; exit 2; } [[ -f "$HARNESS" ]] || { echo "missing $HARNESS (scripts/build_d3d12_dlss_harness.sh)" >&2; exit 2; }
KIT="$D4R/tools/models" KIT="$D4R/tools/models"
rm -rf "$KIT" rm -rf "$KIT"
mkdir -p "$KIT/bin" "$KIT/engine/rr" "$KIT/kernels/tools" "$KIT/kernels/rr" "$KIT/vulkan" "$D4R/licenses" mkdir -p "$KIT/bin" "$KIT/engine/rr" "$KIT/engine/sr" "$KIT/engine/cnn_sr" "$KIT/kernels/tools" "$KIT/kernels/rr" "$KIT/vulkan" "$D4R/licenses"
copy() { # copy SRC_DIR DST_DIR PATTERN...: never tests copy() { # copy SRC_DIR DST_DIR PATTERN...: never tests
local src=$1 dst=$2 pat f local src=$1 dst=$2 pat f
shift 2 shift 2
@@ -30,6 +30,8 @@ copy() { # copy SRC_DIR DST_DIR PATTERN...: never tests
} }
copy engine "$KIT/engine" '*.py' '*.comp' '*.glsl' copy engine "$KIT/engine" '*.py' '*.comp' '*.glsl'
copy engine/rr "$KIT/engine/rr" '*.py' '*.comp' '*.glsl' '*.txt' '*.npy' '*.sh' copy engine/rr "$KIT/engine/rr" '*.py' '*.comp' '*.glsl' '*.txt' '*.npy' '*.sh'
copy engine/cnn_sr "$KIT/engine/cnn_sr" '*.glsl'
copy engine/sr "$KIT/engine/sr" '*.comp' '*.glsl'
copy kernels/tools "$KIT/kernels/tools" '*.py' copy kernels/tools "$KIT/kernels/tools" '*.py'
copy kernels/rr "$KIT/kernels/rr" '*.py' copy kernels/rr "$KIT/kernels/rr" '*.py'
copy vulkan "$KIT/vulkan" '*.py' # ptx2glsl.py: engine/rr/ptx2rr.py builds on it copy vulkan "$KIT/vulkan" '*.py' # ptx2glsl.py: engine/rr/ptx2rr.py builds on it
+93
View File
@@ -0,0 +1,93 @@
"""Opt-in Vulkan regression for CNN f16 fragments, including independent concurrent waves.
D4R_RUN_GPU_TESTS=1 D4R_EVK=/path/to/evk python3 -m unittest tests.test_cnn_mma_gpu
Build evk with: g++ -O2 engine/evk.cpp -lvulkan -o build/evk
"""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS') == '1', 'opt-in GPU test')
class CnnMatrixGpuTests(unittest.TestCase):
def test_fragment_mapping_accumulation_and_wave_isolation(self):
self.check_fragments(packed=False)
def test_packed_fragment_rounding_and_wave_isolation(self):
self.check_fragments(packed=True)
def check_fragments(self, packed):
import numpy as np
rng = np.random.default_rng(412)
fragments = np.empty((8, 32, 8), dtype='<u4')
expected = np.empty((8, 32, 4), dtype='<u4')
def packed(values):
return np.ascontiguousarray(values, dtype='<f2').view('<u4')[0]
for wave in range(8):
a = rng.normal(0, 0.7, (16, 16)).astype(np.float16)
b = rng.normal(0, 0.7, (16, 8)).astype(np.float16)
c = rng.normal(0, 0.7, (16, 8)).astype(np.float16)
sums = c.astype(np.float32)
results = []
for k in range(16):
if packed:
# A half FMA rounds the exact product plus accumulator once to half.
sums = (sums.astype(float) + a[:, k:k+1].astype(float) * b[k:k+1, :].astype(float)).astype(np.float16).astype(np.float32)
else:
# A product of two finite binary16 values is exact in binary32.
sums = (sums + a[:, k:k+1].astype(np.float32) * b[k:k+1, :].astype(np.float32)).astype(np.float32)
if k in (7, 15):
results.append(sums.astype(np.float16))
for lane in range(32):
g, t = lane // 4, lane % 4
fragments[wave, lane] = [
packed(a[g, 2*t:2*t+2]), packed(a[g+8, 2*t:2*t+2]),
packed(a[g, 2*t+8:2*t+10]), packed(a[g+8, 2*t+8:2*t+10]),
packed(b[2*t:2*t+2, g]), packed(b[2*t+8:2*t+10, g]),
packed(c[g, 2*t:2*t+2]), packed(c[g+8, 2*t:2*t+2]),
]
expected[wave, lane, 0:2] = [packed(results[0][g, 2*t:2*t+2]), packed(results[0][g+8, 2*t:2*t+2])]
expected[wave, lane, 2:4] = [packed(results[1][g, 2*t:2*t+2]), packed(results[1][g+8, 2*t:2*t+2])]
source = '''#version 460
#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_shader_explicit_arithmetic_types : require
#extension GL_EXT_control_flow_attributes : require
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_shuffle : require
#include "../round_half.glsl"
#include "mma_f16.glsl"
layout(local_size_x = 128) in;
layout(binding=0,std430) readonly buffer In { uint v[]; } src;
layout(binding=1,std430) writeonly buffer Out { uint v[]; } dst;
void main() {
uint i=gl_GlobalInvocationID.x, at=i*8u, outAt=i*4u;
uint d0,d1;
mma_f16_m16n8k8(src.v[at],src.v[at+1u],src.v[at+4u],src.v[at+6u],src.v[at+7u],d0,d1);
dst.v[outAt]=d0;dst.v[outAt+1u]=d1;
mma_f16_m16n8k16(src.v[at],src.v[at+1u],src.v[at+2u],src.v[at+3u],src.v[at+4u],src.v[at+5u],src.v[at+6u],src.v[at+7u],d0,d1);
dst.v[outAt+2u]=d0;dst.v[outAt+3u]=d1;
}
'''
with tempfile.TemporaryDirectory() as temporary:
d = Path(temporary)
shader, spv = d/'matrix.comp', d/'matrix.spv'
shader.write_text(source)
fragments.tofile(d/'in.bin')
subprocess.run(['glslangValidator', '-V', '--target-env', 'vulkan1.3',
*(['-DD4R_MMA_PACKED'] if packed else []), '-I'+str(ROOT/'engine/cnn_sr'), str(shader), '-o', str(spv)], check=True, capture_output=True)
subprocess.run([os.environ.get('D4R_EVK', str(ROOT/'build/evk')), str(spv), '2', '1', '1',
'in:0='+str(d/'in.bin'), 'out:1='+str(d/'out.bin')+':'+str(expected.nbytes)],
check=True, capture_output=True)
actual = np.fromfile(d/'out.bin', dtype='<u4').reshape(expected.shape)
np.testing.assert_array_equal(actual, expected)
if __name__ == '__main__':
unittest.main()
+52
View File
@@ -0,0 +1,52 @@
"""Compile translated surface stores with their real Vulkan image types."""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
@unittest.skipUnless(shutil.which('glslangValidator') and shutil.which('spirv-val'), 'SPIR-V tools required')
class CnnShaderTypeTests(unittest.TestCase):
def test_mixed_formatted_and_binary_surface_stores_validate(self):
# A function taking an unformatted image2D loses the concrete format type.
# Exercise both RG16F and RGBA16F, including the binary half store helper.
ptx = '''.version 8.0
.target sm_80
.address_size 64
.visible .entry surface_test(.param .align 8 .b8 args[32])
.maxntid 32, 1, 1
{
.reg .b64 %rd<4>;
.reg .b32 %r<4>;
.reg .f32 %f<2>;
ld.param.u64 %rd0, [args+0];
ld.param.u64 %rd1, [args+8];
mov.u32 %r0, 0;
mov.u32 %r1, 15360;
mov.f32 %f0, 0f3F800000;
sust.b.2d.v2.b16.zero [%rd0, {%r0, %r0}], {%r1, %r1};
sust.p.2d.v4.b32.zero [%rd1, {%r0, %r0}], {%f0, %f0, %f0, %f0};
ret;
}
'''
with tempfile.TemporaryDirectory() as temporary:
d = Path(temporary)
(d/'surface.ptx').write_text(ptx)
subprocess.run([sys.executable, str(ROOT/'engine/rr/ptx2rr.py'), str(d/'surface.ptx'), 'surface_test',
str(d/'surface.comp'), '--block', '32,1,1', '--weights-arg', '999',
'--surface=0=rg16f', '--surface=8=rgba16f'], check=True, capture_output=True)
for packed in (False, True):
with self.subTest(packed=packed):
subprocess.run(['glslangValidator', '-V', '--target-env', 'vulkan1.3',
*(['-DD4R_WORD_ARENA', '-DD4R_MMA_PACKED'] if packed else []),
str(d/'surface.comp'), '-o', str(d/'surface.spv')], check=True, capture_output=True)
subprocess.run(['spirv-val', '--target-env', 'vulkan1.3', str(d/'surface.spv')],
check=True, capture_output=True)
if __name__ == '__main__':
unittest.main()
+72
View File
@@ -0,0 +1,72 @@
"""Opt-in rounded Quality reconstruction regression against the generic kernel.
D4R_RUN_GPU_TESTS=1 D4R_SR_E_MODEL=/private/model/e \
D4R_SR_REPLAY=/path/to/d4r-e python3 -m unittest tests.test_sr_reconstruction_gpu
Optionally set D4R_SR_F_MODEL to exercise preset F too. No proprietary fixtures are needed.
"""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS') == '1', 'opt-in GPU test')
class SrReconstructionGpuTests(unittest.TestCase):
def test_rounded_quality_e(self):
self.check_model('D4R_SR_E_MODEL')
def test_rounded_quality_f(self):
self.check_model('D4R_SR_F_MODEL')
def check_model(self, variable):
import numpy as np
if not os.environ.get(variable):
self.skipTest(f'{variable} must name a private compiled model')
model = Path(os.environ[variable]).resolve()
replay = Path(os.environ.get('D4R_SR_REPLAY', ROOT / 'build/native-engine/d4r-e')).resolve()
env = dict(os.environ, D4R_E_TIME_REPEAT='1', ENABLE_VKBASALT='0')
# Test automatic selection, independent of development overrides in the shell.
env.pop('D4R_ENGINE_E_FINAL', None)
shapes = [(1280, 800), (129, 65), (128, 64), (127, 63)]
jitters = [(-.5, -.5), (.5, .5), (-.499, .499), (.499, -.499), (0, 0), (.25, -.25)]
for ow, oh in shapes:
# Independently round each render axis down/up; also exercise mixed rounding.
for upx, upy in [(0, 0), (1, 1), (0, 1), (1, 0)]:
rw, rh = (2 * ow + 2 * upx) // 3, (2 * oh + 2 * upy) // 3
for hdr, low in [(0, 1), (1, 1), (0, 0), (1, 0)]:
with self.subTest(size=(rw, rh, ow, oh), hdr=hdr, low_motion=low):
with tempfile.TemporaryDirectory() as temporary:
case = Path(temporary)
mw, mh = (rw, rh) if low else (ow, oh)
rng = np.random.default_rng(123)
lines = [f'size {rw} {rh} {ow} {oh} {hdr} {low} {mw} {mh}']
for frame, (jx, jy) in enumerate(jitters, 1):
color = rng.random((rh, rw, 4), dtype=np.float32) * (8 if hdr else 1)
color[..., 3] = 1
color.astype('<f2').tofile(case / f'{frame}-color.bin')
rng.uniform(-12, 12, (mh, mw, 2)).astype('<f2').tofile(case / f'{frame}-motion.bin')
rng.uniform(.01, 1, (rh, rw)).astype('<f4').tofile(case / f'{frame}-depth.bin')
lines.append(f'frame {frame} {jx} {jy} 1 1 {int(frame in (1, 6))}')
(case / 'frames.txt').write_text('\n'.join(lines) + '\n')
for name, override in [('reference', {'D4R_ENGINE_E_FINAL': '3x3_256'}), ('auto', {})]:
out = case / name
out.mkdir()
result = subprocess.run([str(replay), str(model), str(case), str(out)],
env=dict(env, **override), capture_output=True, text=True, timeout=90)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
for frame in range(1, len(jitters) + 1):
name = f'{frame}-out.bin'
reference = np.fromfile(case / 'reference' / name, '<f2')
actual = np.fromfile(case / 'auto' / name, '<f2')
self.assertEqual(actual.size, ow * oh * 4)
self.assertTrue(np.isfinite(reference).all())
self.assertTrue(np.isfinite(actual).all())
self.assertTrue(np.any(actual.reshape(-1, 4)[:, :3] != 0))
np.testing.assert_array_equal(actual.view("<u2"), reference.view("<u2"), err_msg=name)
if __name__ == '__main__':
unittest.main()
+179 -1
View File
@@ -104,6 +104,7 @@
#include "d4r_fsr_rr_passes.h" #include "d4r_fsr_rr_passes.h"
#include "../engine/game.h" #include "../engine/game.h"
#include "../engine/game_m.h" #include "../engine/game_m.h"
#include "../engine/game_e.h"
#include "../engine/game_rr.h" #include "../engine/game_rr.h"
#include "../engine/vk_dispatch.h" #include "../engine/vk_dispatch.h"
@@ -2926,6 +2927,8 @@ struct Feature
std::unique_ptr<d4r::GameUpscalerM> engineMDynamic; // Vulkan at changing sizes; original HIP resources stay alive std::unique_ptr<d4r::GameUpscalerM> engineMDynamic; // Vulkan at changing sizes; original HIP resources stay alive
unsigned int engineMLastWidth = 0, engineMLastHeight = 0; unsigned int engineMLastWidth = 0, engineMLastHeight = 0;
unsigned int engineMRenderWidth = 0, engineMRenderHeight = 0; // first evaluation's effective extent unsigned int engineMRenderWidth = 0, engineMRenderHeight = 0; // first evaluation's effective extent
std::unique_ptr<d4r::GameUpscalerE> engineE; // presets E/F (DLSS 3's CNN): fixed render size, rebuilt on a change
unsigned int engineERenderWidth = 0, engineERenderHeight = 0;
std::unique_ptr<d4r::GameDenoiser> engineR; // Ray Reconstruction (engine/runtime_rr.inc) std::unique_ptr<d4r::GameDenoiser> engineR; // Ray Reconstruction (engine/runtime_rr.inc)
ProfileClock::time_point engineRProfileAt{}; ProfileClock::time_point engineRProfileAt{};
// Preset M and preset K with the network on the native HIP layers ([Engine] Network): the buffers shared // Preset M and preset K with the network on the native HIP layers ([Engine] Network): the buffers shared
@@ -10271,6 +10274,179 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list,
return true; return true;
} }
// Presets E (5) and F (6), DLSS 3's CNN (engine/runtime_e.inc, built by engine/compile_sr_e.py from
// nvngx_dlss.dll 310.7.0): engine\e and engine\f next to this DLL, or D4R_ENGINE_MODEL_E_DIR / _F_DIR. One model
// serves LDR and HDR, both motion resolutions; its network runs without cooperative matrices (RDNA2 included).
struct EngineBackendE
{
std::once_flag once[2];
std::unique_ptr<d4r::EModel> models[2];
};
static EngineBackendE g_engineE;
static void init_engine_backend_e(int slot)
{
const char* const name = slot ? "F" : "E";
std::string directory = env_string(slot ? "D4R_ENGINE_MODEL_F_DIR" : "D4R_ENGINE_MODEL_E_DIR");
if (directory.empty())
{
char folder[MAX_PATH * 3] = {};
WideCharToMultiByte(CP_ACP, 0, (g_portable.dir + (slot ? L"\\engine\\f" : L"\\engine\\e")).c_str(), -1, folder,
sizeof(folder), nullptr, nullptr);
directory = folder;
}
if (g_vk.interop == nullptr || g_vk.lifetime == nullptr || g_vk.device == VK_NULL_HANDLE || d4r::vkTable.CreateBuffer == nullptr)
{
logf("engine backend (%s): Vulkan interop is unavailable", name);
return;
}
const std::string path = directory[0] == '/' ? "Z:" + directory : directory;
const auto start = std::chrono::steady_clock::now();
try
{
g_engineE.models[slot] = std::make_unique<d4r::EModel>(g_vk.physical, g_vk.device, path);
}
catch (const std::exception& error)
{
logf("engine backend (%s): cannot load the model from %s: %s", name, path.c_str(), error.what());
return;
}
logf("engine backend (%s): model %s loaded in %.0f ms", name, path.c_str(),
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count());
}
static bool engine_evaluate_e(Feature& feature, ID3D12GraphicsCommandList* list, ID3D12Resource* color,
ID3D12Resource* depth, ID3D12Resource* motion, ID3D12Resource* output,
ID3D12Resource* exposure, const FrameParams& p, uint32_t frame)
{
const int slot = feature.preset == 6 ? 1 : 0;
const char* const name = slot ? "F" : "E";
auto refuse = [&](const char* reason) {
feature.engineRefused = true;
logf("engine backend (%s): %s; feature %u uses the CUDA backend from frame %u", name, reason, feature.handle.Id, frame);
return false;
};
std::call_once(g_engineE.once[slot], [slot] { init_engine_backend_e(slot); });
d4r::EModel* const model = g_engineE.models[slot].get();
if (model == nullptr)
return refuse(slot ? "no preset F model (engine\\f)" : "no preset E model (engine\\e)");
// NVSDK_NGX_DLSS_Feature_Flags: IsHDR 1, MVLowRes 2, MVJittered 4, DepthInverted 8, AutoExposure 0x40.
const bool hdr = (feature.flags & 0x1) != 0, lowRes = (feature.flags & 0x2) != 0;
if ((feature.flags & 0x40) != 0)
return refuse("auto exposure is not implemented for preset E/F");
if (color == output || depth == output || motion == output)
return refuse("input/output texture aliasing is not supported");
if (p.renderWidth > feature.outWidth || p.renderHeight > feature.outHeight)
return refuse("the render size exceeds the output size");
d4r::GameFrameE game;
using Source = EngineFrameSource;
const auto inputState = static_cast<D3D12_RESOURCE_STATES>(env_uint("D4R_SHIM_INPUT_STATE", 0x40));
const auto depthState = static_cast<D3D12_RESOURCE_STATES>(env_uint("D4R_SHIM_DEPTH_STATE", 0x40));
const auto outputState = static_cast<D3D12_RESOURCE_STATES>(env_uint("D4R_SHIM_OUTPUT_STATE", 0x8));
const uint32_t mw = lowRes ? p.renderWidth : feature.outWidth, mh = lowRes ? p.renderHeight : feature.outHeight;
// HDR: the game's exposure texture is NGX's exposure (its copy kernel moves texel 0 into the 1x1 the kernels read)
const bool gameExposure = hdr && exposure != nullptr;
Source sources[5] = {
{color, &game.color, p.colorBaseX, p.colorBaseY, p.renderWidth, p.renderHeight, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE},
{depth, &game.depth, p.depthBaseX, p.depthBaseY, p.renderWidth, p.renderHeight, depthState, D3D12_RESOURCE_STATE_COPY_SOURCE},
{motion, &game.motion, p.mvBaseX, p.mvBaseY, mw, mh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE},
{output, &game.output, p.outputBaseX, p.outputBaseY, feature.outWidth, feature.outHeight, outputState,
D3D12_RESOURCE_STATE_COPY_DEST},
{exposure, &game.exposure, 0, 0, 1, 1, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE},
};
const size_t sourceCount = gameExposure ? 5 : 4;
for (size_t index = 0; index < sourceCount; ++index)
{
Source& source = sources[index];
UINT64 handle = 0, offset = 0;
D3D12_RESOURCE_DESC desc;
source.resource->GetDesc(&desc);
if (FAILED(g_vk.interop->GetVulkanResourceInfo1(source.resource, &handle, &offset, &source.image->format)) ||
handle == 0 || desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D || desc.SampleDesc.Count != 1 ||
source.x + source.width > desc.Width || source.y + source.height > desc.Height)
return refuse("a texture is not a single-sample 2D image covering its rectangle");
source.image->image = reinterpret_cast<VkImage>(handle);
source.image->x = source.x;
source.image->y = source.y;
prepare_engine_view(feature, source.resource, desc, *source.image, source.image == &game.output,
source.x == 0 && source.y == 0, source.copyState);
g_vk.interop->GetVulkanImageLayout(source.resource, source.copyState, &source.image->layout);
}
if (gameExposure && (game.exposure.view == VK_NULL_HANDLE || (game.exposure.format != VK_FORMAT_R32_SFLOAT &&
game.exposure.format != VK_FORMAT_R16_SFLOAT &&
game.exposure.format != VK_FORMAT_R32G32B32A32_SFLOAT &&
game.exposure.format != VK_FORMAT_R16G16B16A16_SFLOAT)))
return refuse("the exposure texture must be a float texture the engine samples directly");
if (game.depth.format == VK_FORMAT_D32_SFLOAT || game.depth.format == VK_FORMAT_D32_SFLOAT_S8_UINT)
game.depth.aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
if (!d4r::gameColorFormat(game.color.format) || !d4r::gameMotionFormat(game.motion.format) ||
!d4r::gameDepthFormat(game.depth.format, game.depth.aspect) || !d4r::gameOutputFormat(game.output.format))
{
logf("engine backend (%s): Vulkan formats colour %d, depth %d, motion %d, output %d", name, game.color.format,
game.depth.format, game.motion.format, game.output.format);
return refuse("a texture format is not implemented");
}
game.jitter[0] = p.jitterX;
game.jitter[1] = p.jitterY;
game.motionScale[0] = p.mvScaleX;
game.motionScale[1] = p.mvScaleY;
const bool resized = feature.engineE != nullptr &&
(p.renderWidth != feature.engineERenderWidth || p.renderHeight != feature.engineERenderHeight);
game.reset = p.reset != 0 || feature.engineE == nullptr || resized;
try
{
if (feature.engineE == nullptr || resized)
{
feature.engineE.reset();
feature.engineE = std::make_unique<d4r::GameUpscalerE>(*model, g_vk.physical, g_vk.device, feature.outWidth,
feature.outHeight, p.renderWidth, p.renderHeight, hdr, lowRes);
feature.engineERenderWidth = p.renderWidth;
feature.engineERenderHeight = p.renderHeight;
logf("engine backend (%s): feature %u runs preset %s in the game's command list (%ux%u -> %ux%u, %s, %s motion)",
name, feature.handle.Id, name, p.renderWidth, p.renderHeight, feature.outWidth, feature.outHeight,
hdr ? "HDR" : "LDR", lowRes ? "render-size" : "output-size");
}
}
catch (const std::exception& error)
{
logf("engine backend (%s): %s", name, error.what());
return refuse("the engine could not be created");
}
if (FAILED(g_vk.lifetime->RetainExternalResources(list, feature.resources)))
return refuse("the command list cannot retain the engine's resources");
for (size_t index = 0; index < sourceCount; ++index)
transition(list, sources[index].resource, sources[index].state, sources[index].copyState);
VkCommandBuffer command = VK_NULL_HANDLE;
bool recorded = SUCCEEDED(g_vk.interop->BeginVkCommandBufferInterop(list, &command));
const char* failure = recorded ? nullptr : "BeginVkCommandBufferInterop failed";
if (recorded)
{
try
{
feature.engineE->record(command, game);
}
catch (const std::exception& error)
{
logf("engine backend (%s): %s", name, error.what());
failure = "the frame could not be recorded";
recorded = false;
}
if (FAILED(g_vk.interop->EndVkCommandBufferInterop(list)) && recorded)
{
failure = "EndVkCommandBufferInterop failed";
recorded = false;
}
}
for (size_t index = 0; index < sourceCount; ++index)
transition(list, sources[index].resource, sources[index].copyState, sources[index].state);
if (!recorded)
return refuse(failure);
if (frame <= 3 || frame % 600 == 0)
logf("engine frame %u recorded, preset %s (jitter %.6f,%.6f, reset %d, mvScale %.3f,%.3f)", frame, name, p.jitterX,
p.jitterY, game.reset, p.mvScaleX, p.mvScaleY);
return true;
}
// Ray Reconstruction models (engine/compile_rr.py; preset F: engine/compile_rr2.py): one per preset, engine\rr-d, // Ray Reconstruction models (engine/compile_rr.py; preset F: engine/compile_rr2.py): one per preset, engine\rr-d,
// engine\rr-e and engine\rr-f next to this DLL, or D4R_ENGINE_MODEL_RR_D_DIR / _E_DIR / _F_DIR. Slots: 0 D, 1 E, 2 F. // engine\rr-e and engine\rr-f next to this DLL, or D4R_ENGINE_MODEL_RR_D_DIR / _E_DIR / _F_DIR. Slots: 0 D, 1 E, 2 F.
struct EngineBackendR struct EngineBackendR
@@ -10641,6 +10817,8 @@ static bool engine_evaluate(Feature& feature, ID3D12GraphicsCommandList* list, I
return engine_evaluate_rr(feature, list, color, depth, motion, output, parameters, p, frame); return engine_evaluate_rr(feature, list, color, depth, motion, output, parameters, p, frame);
if (feature.preset == 13 || feature.preset == 12) if (feature.preset == 13 || feature.preset == 12)
return engine_evaluate_m(feature, list, color, depth, motion, output, p, frame); return engine_evaluate_m(feature, list, color, depth, motion, output, p, frame);
if (feature.preset == 5 || feature.preset == 6)
return engine_evaluate_e(feature, list, color, depth, motion, output, exposure, p, frame);
if (feature.preset == 11 && env_string("D4R_ENGINE_K") == "0" && env_string("D4R_ENGINE_NETWORK") != "hip") if (feature.preset == 11 && env_string("D4R_ENGINE_K") == "0" && env_string("D4R_ENGINE_NETWORK") != "hip")
return refuse("[Engine] Enabled = auto keeps K on the native kernels (faster than the Vulkan network)"); return refuse("[Engine] Enabled = auto keeps K on the native kernels (faster than the Vulkan network)");
// Without the HDR flag NGX runs its LDR input and output kernels: a model directory of its own. // Without the HDR flag NGX runs its LDR input and output kernels: a model directory of its own.
@@ -10654,7 +10832,7 @@ static bool engine_evaluate(Feature& feature, ID3D12GraphicsCommandList* list, I
return refuse(ldr ? "no LDR model (engine\\k-ldr) for a game without the HDR flag" : "not available"); return refuse(ldr ? "no LDR model (engine\\k-ldr) for a game without the HDR flag" : "not available");
// NVSDK_NGX_DLSS_Feature_Flags: IsHDR 1, MVLowRes 2, MVJittered 4, DepthInverted 8. // NVSDK_NGX_DLSS_Feature_Flags: IsHDR 1, MVLowRes 2, MVJittered 4, DepthInverted 8.
if (feature.preset != 11) if (feature.preset != 11)
return refuse("only presets K, L and M are implemented"); return refuse("only presets E, F, K, L and M are implemented");
// MVLowRes (0x2) clear: vectors are at output resolution (engine revision D4RO0003). // MVLowRes (0x2) clear: vectors are at output resolution (engine revision D4RO0003).
const bool displayMotion = (feature.flags & 0x2) == 0; const bool displayMotion = (feature.flags & 0x2) == 0;
if (displayMotion && !model->hasDisplayMotion()) if (displayMotion && !model->hasDisplayMotion())
+18 -2
View File
@@ -13,7 +13,7 @@ import re
import sys import sys
REG_RE = re.compile(r'%([a-zA-Z]+)(\d+)$') REG_RE = re.compile(r'%([a-zA-Z]+)(\d+)$')
GT = {'f': 'float', 'r': 'uint', 'rd': 'uint64_t', 'rs': 'uint', 'p': 'bool'} GT = {'f': 'float', 'r': 'uint', 'rd': 'uint64_t', 'rs': 'uint', 'p': 'bool', 'fd': 'double'}
SREG = {'%tid.x': 'gl_LocalInvocationID.x', '%tid.y': 'gl_LocalInvocationID.y', '%tid.z': 'gl_LocalInvocationID.z', SREG = {'%tid.x': 'gl_LocalInvocationID.x', '%tid.y': 'gl_LocalInvocationID.y', '%tid.z': 'gl_LocalInvocationID.z',
'%ctaid.x': 'gl_WorkGroupID.x', '%ctaid.y': 'gl_WorkGroupID.y', '%ctaid.z': 'gl_WorkGroupID.z', '%ctaid.x': 'gl_WorkGroupID.x', '%ctaid.y': 'gl_WorkGroupID.y', '%ctaid.z': 'gl_WorkGroupID.z',
'%ntid.x': 'gl_WorkGroupSize.x', '%ntid.y': 'gl_WorkGroupSize.y', '%ntid.x': 'gl_WorkGroupSize.x', '%ntid.y': 'gl_WorkGroupSize.y',
@@ -406,8 +406,10 @@ class T:
A(dst, V(src, 'u32'), 'u32'); return A(dst, V(src, 'u32'), 'u32'); return
# ------------------------------------------------ f32 # ------------------------------------------------ f32
if ft == 'f32' and base in ('add', 'sub', 'mul'): if ft == 'f32' and base in ('add', 'sub', 'mul'):
if 'rz' in mods and base == 'add':
A(a[0], f'add_rz({V(a[1], "f32")}, {V(a[2], "f32")})', 'float'); return
if 'rz' in mods: if 'rz' in mods:
A(a[0], f'round_half_away({V(a[1], "f32")})', 'float'); return # the roundf idiom: + copysign(0.5), then trunc raise Err(f'unsupported rounding: {base}.rz')
A(a[0], f'{V(a[1], "f32")} {"+-*"[["add", "sub", "mul"].index(base)]} {V(a[2], "f32")}', 'float'); return A(a[0], f'{V(a[1], "f32")} {"+-*"[["add", "sub", "mul"].index(base)]} {V(a[2], "f32")}', 'float'); return
if ft == 'f32' and base == 'fma': if ft == 'f32' and base == 'fma':
A(a[0], f'fma({V(a[1], "f32")}, {V(a[2], "f32")}, {V(a[3], "f32")})', 'float'); return A(a[0], f'fma({V(a[1], "f32")}, {V(a[2], "f32")}, {V(a[3], "f32")})', 'float'); return
@@ -419,6 +421,8 @@ class T:
A(a[0], f'abs({V(a[1], "f32")})', 'float'); return A(a[0], f'abs({V(a[1], "f32")})', 'float'); return
if ft == 'f32' and base in ('rcp', 'ex2', 'lg2', 'rsqrt', 'sqrt'): if ft == 'f32' and base in ('rcp', 'ex2', 'lg2', 'rsqrt', 'sqrt'):
fn = {'rcp': 'rcp_a', 'ex2': 'exp2', 'lg2': 'log2', 'rsqrt': 'inversesqrt', 'sqrt': 'sqrt'}[base] fn = {'rcp': 'rcp_a', 'ex2': 'exp2', 'lg2': 'log2', 'rsqrt': 'inversesqrt', 'sqrt': 'sqrt'}[base]
if base == 'lg2' and 'ftz' in mods:
fn = 'lg2_ftz' # a denormal input is zero: -inf, where log2 returns about -127
A(a[0], f'{fn}({V(a[1], "f32")})', 'float'); return A(a[0], f'{fn}({V(a[1], "f32")})', 'float'); return
if ft == 'f32' and base == 'div': if ft == 'f32' and base == 'div':
A(a[0], f'{V(a[1], "f32")} * rcp_a({V(a[2], "f32")})', 'float'); return A(a[0], f'{V(a[1], "f32")} * rcp_a({V(a[2], "f32")})', 'float'); return
@@ -428,6 +432,8 @@ class T:
e = f'fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})' e = f'fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})'
if 'sat' in mods: if 'sat' in mods:
e = f'min(max({e}, f16vec2(0.0hf)), f16vec2(1.0hf))' e = f'min(max({e}, f16vec2(0.0hf)), f16vec2(1.0hf))'
if 'relu' in mods: # negative results clamp to +0 (NaN stays NaN)
e = f'relu2({e})'
A(a[0], e, 'f16x2'); return A(a[0], e, 'f16x2'); return
if ft == 'f16x2' and base == 'abs': if ft == 'f16x2' and base == 'abs':
A(a[0], f'abs({V(a[1], "f16x2")})', 'f16x2'); return A(a[0], f'abs({V(a[1], "f16x2")})', 'f16x2'); return
@@ -442,6 +448,8 @@ class T:
e = f'fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})' e = f'fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})'
if 'sat' in mods: if 'sat' in mods:
e = f'min(max({e}, 0.0hf), 1.0hf)' e = f'min(max({e}, 0.0hf), 1.0hf)'
if 'relu' in mods:
e = f'relu1({e})'
A(a[0], e, 'f16'); return A(a[0], e, 'f16'); return
if base == 'set' and 'f16x2' in mods and 'u32' in mods: if base == 'set' and 'f16x2' in mods and 'u32' in mods:
c = self.cmp(mods, True); x, y = V(a[1], 'f16x2'), V(a[2], 'f16x2') c = self.cmp(mods, True); x, y = V(a[1], 'f16x2'), V(a[2], 'f16x2')
@@ -901,6 +909,14 @@ uint HU2_(u16vec2 u) { return uint(u.x) | (uint(u.y) << 16); }
uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); } uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); }
int s16(uint u) { return (int(u) << 16) >> 16; } int s16(uint u) { return (int(u) << 16) >> 16; }
float rcp_a(float x) { return 1.0 / x; } float rcp_a(float x) { return 1.0 / x; }
// add.rz.f32: the sum rounded toward zero (round to nearest, then one ulp back when that went away from zero; the
// error term is exact, TwoSum)
float add_rz(float a, float b) { precise float s = a + b; precise float bb = s - a; precise float e = (a - (s - bb)) + (b - bb);
return (e != 0.0 && ((e < 0.0) != (s < 0.0)) && s != 0.0) ? uintBitsToFloat(floatBitsToUint(s) - 1u) : s; }
float lg2_ftz(float x) { return log2(abs(x) < 1.17549435e-38 ? 0.0 : x); }
// PTX .relu: negative results become +0, NaN stays NaN
f16vec2 relu2(f16vec2 v) { return f16vec2(v.x < 0.0hf ? 0.0hf : v.x, v.y < 0.0hf ? 0.0hf : v.y); }
float16_t relu1(float16_t v) { return v < 0.0hf ? 0.0hf : v; }
// x + copysign(0.5, x) rounded toward zero and then truncated: round half away from zero, exactly // x + copysign(0.5, x) rounded toward zero and then truncated: round half away from zero, exactly
float round_half_away(float x) { float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; } float round_half_away(float x) { float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; }
uint PARAM32(uint o) { return param.w[o >> 2]; } uint PARAM32(uint o) { return param.w[o >> 2]; }