Commit Graph
6 Commits
Author SHA1 Message Date
itchyfeetleech c29076b38f engine/rr: run dec0_1's translated tail on two waves
dec0_1's translated output tail ran on wave 0 while the block's other three
waves had finished. It is a prologue (y = r + MLP * s2 and the shuffles that
give each lane its two pixels) followed by the same per-pixel code twice.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: itchyfeetleech <[email protected]>
2026-10-18 14:45:41 +13:00