Keep RR guides and separate alpha in VRAM

This commit is contained in:
woutermans committed 2026-10-12 12:47:53 +02:00
1 parent 7117c38674
commit 5461c161f3
10 files changed
+1697 -169

No files matched your search

+8 -2
View File
@@ -44,9 +44,15 @@ Feature 13 uses the NGX core's CUDA lifecycle, not the denoiser's native D3D12 b
Feature requirements may be queried before NGX initialization. The shim loads the providers without initializing NGX with a provisional application identity; when the core declines feature-13 discovery, it asks the CUDA-capable denoiser's D3D12 requirements export for the adapter's eligibility. This discovery-only call does not select the native D3D12 rendering backend. Initialized capability checks and feature creation still require the core's successful denoiser initialization.
The shim snapshots denoiser guide bindings, scalar settings, camera matrices, and subrect origins per submitted frame. Normals and roughness use the SDK's `GBuffer.Normals` and `GBuffer.Roughness` resource keys; diffuse and specular albedo bindings are translated to the CUDA denoiser's `DLSS.Input.*Albedo` keys. Guide readback and CUDA upload follow the main input marker, and the backing resources remain held until that frame retires. Uploads use per-slot host staging because direct ROCm image copies from Vulkan-mapped memory truncated guide rows on gfx1201. CUDA parameters point at stable texture-handle storage, not D3D12 resource pointers. Normals preserve the caller's component values rather than being renormalized during format conversion.
The shim snapshots denoiser guide bindings, scalar settings, camera matrices, and subrect origins per submitted frame. Normals and roughness use the SDK's `GBuffer.Normals` and `GBuffer.Roughness` keys; albedo bindings are translated to the CUDA denoiser's `DLSS.Input.*Albedo` keys. CUDA parameters point at stable texture-handle storage, not D3D12 resource pointers. Normals preserve the caller's component values rather than being renormalized during conversion.
Separate alpha output follows the same produced-frame slot as RGB, including its output resource and subrect. Requesting separate alpha cuts over through the existing drained host-staging path instead of publishing mismatched VRAM RGB and alpha results.
With VRAM interop, guides copy from Vulkan images into per-slot shared device buffers, then into the CUDA arrays sampled by the denoiser. Half, float, UNORM, and SNORM layouts copy verbatim where possible. BGRA and packed RGB formats are converted to RGBA32F by a Vulkan blit. Raw integer guides retain the host conversion because integer-to-float blits do not reproduce its semantics. CUDA import happens on the worker after allocation, outside the guide-binding lock. The input marker orders both routes, and resources remain held until their frame retires.
`D4R_SHIM_RR_VRAM_GUIDES=0` selects the host guide route for A/B checks without disabling RGB VRAM interop. `D4R_SHIM_VRAM_VERIFY=1` also stages the guides and compares the actual CUDA arrays against the CPU conversion, failing the frame on mismatches. Verification preserves pitch-linear input row alignment; diagnostics must not replace GPU geometry with a tightly packed host stride.
Separate alpha no longer forces RGB through host staging. R32F alpha copies from the denoiser's CUDA array into a shared result buffer and into the caller's subrect; half and UNORM destinations use a GPU blit. Raw integer destinations keep their existing host conversion, independently of the RGB route. Blit rounding is hardware-dependent and can differ from CPU conversion by one destination quantization step.
Split frames reserve the current frame's alpha slot before its producer runs. Previous-frame presentation matches alpha to RGB's produced frame and validates its format and subrect. Pixels outside the evaluated rectangle remain untouched. `D4R_SHIM_ALPHA_VERIFY=1` compares the array region with its shared buffer; combined with output capture, it also saves that region as `*.rgba16f.alpha.f32`.
## Why each piece exists
+45
View File
@@ -104,6 +104,25 @@ scripts/d4r_play.sh
In the game, pick DLSS as the upscaler (OptiScaler intercepts it). Every setting in the file can also be given as an environment variable, which takes precedence; `D4R_NO_CONFIG=1` ignores the file.
### Cyberpunk 2077 RR launch notes
The optimized RR route was exercised in a loaded Cyberpunk 2077 2.31 save on RX 9070 XT (gfx1201), with preset E, path tracing and Ultra Performance (640×360 → 1920×1080). The user also verified gameplay. This is a functional check, not a comparative FPS benchmark.
On the tested GE-Proton11-3 setup, Wine's builtin ICU aborted on `icuuc.dll.u_setMemoryFunctions_65`. Use the game's native ICU DLLs for that setup:
```sh
WINEDLLOVERRIDES='icuuc=n,b;icuin=n,b;icudt=n,b;d3d12=n,b;d3d12core=n,b' \
D4R_RR_ENABLE=1 D4R_RR_PRESET=5 \
D4R_SHIM_VRAM_INTEROP=1 D4R_SHIM_SPLIT_FRAME=1 D4R_SHIM_LINEAR_INPUTS=1 \
D4R_ZLUDA_WMMA=1 D4R_ZLUDA_WMMA_FP8=1 D4R_ZLUDA_WMMA_FP8_NATIVE=1 \
scripts/d4r_play.sh
```
Merge these overrides with any existing `WINEDLLOVERRIDES`; do not discard overrides needed by other mods. Keep the patched vkd3d DLLs current, including game-local copies as described above. Select DLSS, Ray Reconstruction and path tracing in the game. `D4R_SHIM_RR_VRAM_GUIDES` defaults to `1` with VRAM interop; `0` selects host guide staging for A/B diagnostics.
Leave `D4R_SHIM_VRAM_VERIFY`, `D4R_SHIM_ALPHA_VERIFY` and output capture disabled for normal play or benchmarking. The in-game diagnostic run enabled verification and reported zero differing bytes for all six supplied RR guides: normals, diffuse/specular albedo, specular hit distance, pre-particle color and the subsurface-scattering guide. Verification itself downloads GPU data.
## 7. Package a release
```sh
@@ -160,3 +179,29 @@ Run `bash scripts/build_release_glibc241.sh` inside the container. The script ch
- `D4R_HARNESS_RECREATE_OUTPUT=1` releases and recreates the game's output texture between completed frames; compare its copy and in-place sequences to catch imported-memory handle reuse.
- `D4R_HARNESS_RECREATE=4 D4R_HARNESS_VERIFY_RECREATION=1` checks repeated feature/quality recreation. `D4R_HARNESS_SKIP_RECORDED_INPUT=1` checks discarded command-list input readiness without injecting stale history.
- Native kernels have their own validation path; see [native-kernels.md](native-kernels.md).
### Ray Reconstruction VRAM comparison
On the validated gfx1201/native-FP8 runtime, build the harness and shim, then compare host and GPU guide transport:
```sh
scripts/build_d3d12_dlss_harness.sh
D4R_SHIM_SPLIT_FRAME=1 D4R_SHIM_LINEAR_INPUTS=1 \
D4R_SHIM_VRAM_VERIFY=1 D4R_SHIM_ALPHA_VERIFY=1 \
D4R_RR_ALPHA=1 D4R_RR_GUIDE_PROBE=1 \
D4R_HARNESS_TEMPORAL=1 \
D4R_HARNESS_SIZE='193 109 288 162' \
scripts/run_rr_guide_compare.sh /tmp/rr-compare \
rgba16f,rgba16f,rgba8unorm,rgba8unorm \
rgba8snorm,rgba16unorm,bgra8unorm,rgb10a2unorm \
rgba32f,r32f,rgb11b10float,rgba8unorm \
rgba16f,r32uint,bgra8unorm,rgba16f
```
The script stages the matching vkd3d DLLs, real DXVK-NVAPI, shim, and feature libraries together. It defaults to RR preset E and enables the required native-FP8 flags. Override `D4R_RUNTIME_DIR`, `D4R_PROTON_DIR`, or `D4R_VKD3D_DIR` for an isolated runtime. Neither feature discovery nor failed evaluation is bypassed.
Every saved RGB and alpha frame is compared. The reset guide probe requires an image response, not spatial confinement: transformer attention may legitimately change both halves. `D4R_HARNESS_RR_ALPHA_FORMAT` selects `r32f`, `r16f`, `r8unorm`, or `r16unorm`; alpha captures are always decoded float32, with format-appropriate outside-subrect sentinels.
Verification intentionally downloads GPU data. Unset `D4R_SHIM_VRAM_VERIFY` and `D4R_SHIM_ALPHA_VERIFY` for performance measurements and games. For same-frame conversion checks, combine alpha verification with `D4R_SHIM_OUTPUT_DUMP_DIR` and the existing dump frame selectors; compare each saved alpha region with the corresponding presented alpha capture, allowing one destination quantization step for blit rounding.
`python3 -m unittest discover -s tests -v` includes output-comparator regressions for channel swaps, nonfinite pixels, missing/truncated captures, alpha frame mismatches, and writes outside the output subrect.
+24 -1
View File
@@ -1,6 +1,6 @@
# Performance
Unless stated otherwise, the frame rates and kernel timings on this page were measured on an RX 7700 XT (gfx1101). Native FP8 changes both arithmetic and cost, so the gfx11 results cannot be projected to RDNA4; the one RDNA4 measurement is in [RDNA4](#rdna4). No RDNA4 game frame rate has been measured.
Unless stated otherwise, the frame rates and kernel timings on this page were measured on an RX 7700 XT (gfx1101). Native FP8 changes both arithmetic and cost, so the gfx11 results cannot be projected to RDNA4; the RDNA4 Super Resolution measurement is in [RDNA4](#rdna4). The Ray Reconstruction measurements and Cyberpunk verification use an RX 9070 XT (gfx1201); the RR timings are synthetic harness measurements, not RDNA4 game-FPS benchmarks.
## Method
@@ -142,3 +142,26 @@ DLSS 4.5 (L) on the same card and harness, with enc0 and dec0 as ZLUDA's compile
| **all kernels** | **5.83** | **4.06** | **5.93** | **4.34** | **8.63** | **5.69** | **5.99** | **4.14** |
L's post kernel (1.2–1.5 ms) is the largest kernel left; it has no matrix multiplies. The 1920×1080 runs were taken in one of the two clock states the driver's governor settles in at that load (about 13% apart), so compare within the pair, not across resolutions.
## Ray Reconstruction VRAM transport
Measured separately from the Super Resolution game benchmarks above: RX 9070 XT (gfx1201), GE-Proton11-3, CUDA-capable DLSS-Denoiser 310.7, RR preset E, native FP8 WMMA and native Swin encoders. The synthetic D3D12 harness rendered 640×360 into 1280×720 with separate R32F alpha. Split-frame presentation and pitch-linear inputs were enabled; verification/capture downloads were disabled.
Four back-to-back 40-frame runs used host guides, GPU guides, GPU guides, then host guides. The first five frames of each run were excluded; values below are warm-frame medians in milliseconds:
| Guide transport | Worker time, first/repeat | Frame interval, first/repeat |
|---|---:|---:|
| Host staging | 14.967 / 14.296 | 17.373 / 16.795 |
| Shared VRAM | 13.960 / 13.953 | 16.482 / 16.680 |
All four final RGBA captures were byte-identical. The GPU guide runs recorded no host staging readbacks. This is a small synthetic transport comparison, not a game-FPS claim; network execution and clocks contribute to run-to-run variation.
With the usual two RGBA16F and two RGBA8 guides, VRAM transport eliminates approximately 11.1 MB of guide payload crossing GPU↔CPU per frame at this render size (5.5 MB in each direction, excluding padding). Separate alpha also no longer forces RGB through host staging. Unsupported raw-integer guide/alpha formats retain their existing host conversion without disabling the other GPU routes.
Correctness checks additionally covered five guide-format sets at an odd 193-pixel input width: 35 final/per-frame RGBA files were byte-identical between host and GPU guide transport. Six-frame source/presentation captures preserved RGB bytes and alpha subrect sentinels. R32F alpha was exact; half/UNORM blits can differ from the CPU quantizer by one destination step.
An aligned 192×108→288×162 synthetic capture against native CUDA on RTX 4090 measured RGB PSNR 42.15 dB, mean absolute component error 0.00357, and maximum error 0.13623; neither image contained nonfinite RGB values. This spot check does not establish RTX image-quality parity or game-wide temporal correctness. Transport A/B comparisons on the same AMD backend were exact; cross-backend neural arithmetic remains a separate accuracy limitation.
The optimized route also rendered a loaded Cyberpunk 2077 2.31 save at 640×360→1920×1080 Ultra Performance with path tracing and RR preset E on gfx1201. The overlay identified DLSSD 310.7.0; the shim created CUDA feature 13 with split-frame VRAM interop. In-game verification found zero differing bytes for all six supplied guide arrays, and the user verified gameplay. This diagnostic run enabled GPU readbacks and does not establish an FPS improvement or a game-wide artifact/latency audit.
The extended diagnostic log also recorded prolonged split-frame waits and a GPU-marker timeout later in the run; their cause was not isolated. The successful rendering and guide comparisons are not evidence of stall-free operation.
+16 -3
View File
@@ -21,11 +21,18 @@
- Run the signed CUDA-capable 3.10.7 DLSS-Denoiser through the NGX core's feature-13 lifecycle.
Presets D and E select distinct network weights independently of the Super Resolution preset.
- Preserve per-frame guide resources, camera matrices, subrects, and stable CUDA texture handles.
Support normalized 8-bit albedo guides with the CUDA array-format ABI and stage full guide rows.
Support normalized 8-bit albedo guides with the CUDA array-format ABI and preserve full guide rows.
- Support pre-Init feature discovery through the denoiser's requirements export without initializing
NGX with a provisional application identity. Initialized capability checks remain authoritative.
- Keep separate alpha and RGB outputs paired, including padded output subrects; consume alpha
readback synchronously before CPU conversion.
- Keep separate alpha and RGB outputs paired, including padded output subrects and current-frame
split presentation. Separate alpha no longer forces the whole feature through host staging.
R32F alpha stays in VRAM; half/UNORM alpha uses GPU conversion with hardware-dependent rounding.
Raw integer alpha retains its existing host conversion without disabling RGB VRAM interop.
Converted alpha can differ from CPU quantization by one destination step.
Normals, roughness, albedo, and other compatible RR guides use shared VRAM buffers; BGRA and
packed RGB guides convert on the GPU. Raw integer guides retain their host conversion.
Add actual-CUDA-array verification, per-frame rendered RGB/alpha comparisons, and source-alpha
capture for checking conversion against the exact frame being presented.
- Bound R8_UINT alpha stores to one byte and balance guide/alpha references when evaluation
cannot place its frame marker. Publish cached denoiser capabilities as a coherent snapshot.
- Extend the ZLUDA patch stack for real denoiser PTX, LLVM lowering, half texture/store operations,
@@ -37,6 +44,12 @@
1920×1080 and 3840×2160 Ultra Performance on gfx1201. The actual overlay reported DLSSD 310.7.0,
and CUDA feature-13 evaluations completed. The observed ~53 FPS at 1080p and ~20 FPS at 4K are
snapshots, not comparative benchmarks. Newer DLSS 4.5 RR preset F is not implemented or validated.
- Rechecked the optimized VRAM guide path in a loaded Cyberpunk 2077 2.31 save with path tracing,
RR preset E and 1080p Ultra Performance on gfx1201; the user verified gameplay.
All six supplied guide arrays matched the host reference byte-for-byte in diagnostic checks.
The tested GE-Proton11-3 setup required native ICU overrides
(`icuuc=n,b;icuin=n,b;icudt=n,b`) to avoid Wine's unimplemented `u_setMemoryFunctions_65`.
Keep guide/alpha verification off for normal play; it intentionally adds GPU readbacks.
# d4r 0.1.3
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -euo pipefail
# Runs the D3D12 harness's Ray Reconstruction path twice with identical inputs -
# once with the shim's guides travelling on the GPU (D4R_SHIM_RR_VRAM_GUIDES=1,
# the default) and once with them staged through the host (=0) - and compares the
# rendered images, every saved frame and the alpha output.
#
# The two runs differ only in that one variable, so any difference in the image is
# the guide path's, not the denoiser's: same colour, depth, motion, exposure, jitter,
# sizes and frame count. The harness additionally checks the guides themselves when
# D4R_HARNESS_RR_GUIDE_PROBE=1 (a guide change must affect the rendered image;
# transformer attention may spread that response beyond the changed half).
#
# usage: run_rr_guide_compare.sh OUTPUT_DIR [GUIDE_FORMAT_SET ...]
# each set is a comma-separated D4R_HARNESS_GUIDE_FORMATS value, one token per
# plane (GBuffer.Normals, GBuffer.Roughness, GBuffer.DiffuseAlbedo,
# GBuffer.SpecularAlbedo). The default is one set, rgba16f,rgba16f,rgba8unorm,rgba8unorm.
#
# environment:
# D4R_PROTON_DIR Proton build to launch through (default: GE-Proton11-3)
# D4R_RUNTIME_DIR install with nvngx_dlssd.dll, _nvngx.dll, kernels (default ~/.local/share/d4r-dlss)
# D4R_VKD3D_DIR d4r-patched d3d12.dll/d3d12core.dll (default ~/.cache/d4r-vkd3d-d4r)
# D4R_ZLUDA_DIR ZLUDA build whose libcuda.so the Wine bridge loads (default ~/.cache/d4r-zluda-current)
# D4R_ROCM_DIR ROCm user-space runtime (default ~/.cache/d4r-rocm-runtime, else /opt/rocm)
# D4R_NVAPI_DIR directory holding the real DXVK-NVAPI nvapi64.dll (default: from D4R_PROTON_DIR)
# D4R_RR_PRESET denoiser preset, 4 = D, 5 = E (default: E)
# D4R_HARNESS_SIZE "IN_W IN_H OUT_W OUT_H" (default "640 360 1280 720")
# D4R_HARNESS_FRAMES frames per run (default 6)
# D4R_RR_ALPHA=1 also register DLSSD.Alpha / DLSSD.OutputAlpha
# D4R_HARNESS_RR_ALPHA_FORMAT r32f (default), r16f, r8unorm, or r16unorm
# D4R_RR_GUIDE_PROBE=1 also run the in-harness guide response probe
# D4R_HARNESS_SKIP_BUILD=0 build the harness and shim first (default: skip)
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT_DIR="$(realpath -m "${1:?usage: run_rr_guide_compare.sh OUTPUT_DIR [GUIDE_FORMAT_SET ...]}")"
shift
FORMAT_SETS=("$@")
if [[ ${#FORMAT_SETS[@]} -eq 0 ]]; then
FORMAT_SETS=("rgba16f,rgba16f,rgba8unorm,rgba8unorm")
fi
PROTON_DIR="${D4R_PROTON_DIR:-$HOME/.local/share/Steam/compatibilitytools.d/GE-Proton11-3}"
RUNTIME_DIR="${D4R_RUNTIME_DIR:-$HOME/.local/share/d4r-dlss}"
VKD3D_DIR="${D4R_VKD3D_DIR:-$HOME/.cache/d4r-vkd3d-d4r}"
ZLUDA_DIR="${D4R_ZLUDA_DIR:-$HOME/.cache/d4r-zluda-current}"
ROCM_DIR="${D4R_ROCM_DIR:-$HOME/.cache/d4r-rocm-runtime}"
[[ -d "$ROCM_DIR/lib" ]] || ROCM_DIR=/opt/rocm
NVAPI_DIR="${D4R_NVAPI_DIR:-$PROTON_DIR/files/lib/wine/nvapi/x86_64-windows}"
FRAMES="${D4R_HARNESS_FRAMES:-6}"
read -r IN_W IN_H OUT_W OUT_H <<< "${D4R_HARNESS_SIZE:-640 360 1280 720}"
APP_DIR="${D4R_RR_APP_DIR:-$OUTPUT_DIR/app}"
HARNESS_EXE="$ROOT/build/d3d12_dlss_harness.exe"
# Every input the run needs, named so a missing one is a message, not a mystery.
require() { [[ -f "$1" ]] || { printf 'Missing %s (%s)\n' "$1" "$2" >&2; exit 2; }; }
require "$HARNESS_EXE" 'run scripts/build_d3d12_dlss_harness.sh (it also builds the shim)'
require "$ROOT/build/d4r_nvngx.dll" 'run scripts/build_d4r_nvngx_shim.sh'
require "$RUNTIME_DIR/bin/nvngx_dlssd.dll" 'NVIDIA DLSS-Denoiser 3.10.7 (CUDA-capable build)'
require "$RUNTIME_DIR/ngx/_nvngx.dll" 'NVIDIA NGX core'
require "$VKD3D_DIR/d3d12.dll" 'd4r-patched vkd3d-proton (scripts/build_vkd3d_proton_d4r.sh)'
require "$VKD3D_DIR/d3d12core.dll" 'd4r-patched vkd3d-proton'
require "$NVAPI_DIR/nvapi64.dll" 'DXVK-NVAPI from a Proton build'
require "$PROTON_DIR/proton" 'a Proton build'
[[ -e "$ZLUDA_DIR/libcuda.so" ]] || { printf 'Missing %s/libcuda.so (ZLUDA)\n' "$ZLUDA_DIR" >&2; exit 2; }
[[ -d "$RUNTIME_DIR/kernels" ]] || { printf 'Missing %s/kernels (native kernels)\n' "$RUNTIME_DIR" >&2; exit 2; }
mkdir -p "$APP_DIR" "$OUTPUT_DIR"
# One directory for everything the NGX core and the shim resolve relative to each
# other: the core loads nvngx_dlssd.dll from beside the shim, and the harness loads
# the d4r-patched vkd3d from beside its own executable.
cp -f "$HARNESS_EXE" "$ROOT/build/d4r_nvngx.dll" \
"$RUNTIME_DIR/bin/nvngx_dlssd.dll" "$RUNTIME_DIR/bin/nvngx_dlss.dll" \
"$VKD3D_DIR/d3d12.dll" "$VKD3D_DIR/d3d12core.dll" "$NVAPI_DIR/nvapi64.dll" "$APP_DIR/"
export D4R_HARNESS_SKIP_BUILD="${D4R_HARNESS_SKIP_BUILD:-1}"
export D4R_HARNESS_APP_DIR="$APP_DIR"
export D4R_HARNESS_NGX_DLL="$APP_DIR/d4r_nvngx.dll"
export D4R_RUNTIME_DIR="$RUNTIME_DIR"
export D4R_ZLUDA_DIR="$ZLUDA_DIR"
export D4R_ROCM_DIR="$ROCM_DIR"
export D4R_ZLUDA_NATIVE_DIR="$RUNTIME_DIR/kernels"
export D4R_ZLUDA_IMPLICIT_MAX_BLOCK=256
export D4R_ZLUDA_WMMA="${D4R_ZLUDA_WMMA:-1}"
export D4R_ZLUDA_WMMA_FP8="${D4R_ZLUDA_WMMA_FP8:-1}"
export D4R_ZLUDA_WMMA_FP8_NATIVE=1
export D4R_ZLUDA_NATIVE_SWIN_ENCODERS=1
export D4R_SHIM_VRAM_INTEROP=1
export D4R_RR_ENABLE=1
export D4R_RR_PRESET="${D4R_RR_PRESET:-5}"
export D4R_HARNESS_FEATURE_ID=13
export D4R_HARNESS_SAVE_FRAMES=1
export WINEDLLOVERRIDES="d3d12=n,b;d3d12core=n,b"
run_one() { # run_one LABEL OUTPUT_RAW GUIDE_PATH
local label="$1" output="$2" guide_path="$3"
printf '\n== %s: guides on the %s path (%s)\n' "$label" "$guide_path" "$4" >&2
D4R_SHIM_RR_VRAM_GUIDES="$guide_path" \
"$ROOT/scripts/run_d3d12_dlss_harness_proton.sh" "$PROTON_DIR" "$output" \
"$FRAMES" "$IN_W" "$IN_H" "$OUT_W" "$OUT_H" 2>&1 | tee "$OUTPUT_DIR/$label.log"
grep -E 'Ray Reconstruction|GetFeatureRequirements|CreateFeature|output read back|correlation|alpha:|Guide probe' \
"$OUTPUT_DIR/$label.log" || true
}
status=0
for formats in "${FORMAT_SETS[@]}"; do
tag="${formats//,/_}"
printf '\n### guide formats: %s\n' "$formats"
export D4R_HARNESS_GUIDE_FORMATS="$formats"
if [[ "${D4R_RR_ALPHA:-0}" == 1 ]]; then export D4R_HARNESS_RR_ALPHA=1; else unset D4R_HARNESS_RR_ALPHA; fi
if [[ "${D4R_RR_GUIDE_PROBE:-0}" == 1 ]]; then
export D4R_HARNESS_RR_GUIDE_PHASE=1 D4R_HARNESS_RR_GUIDE_PROBE=1
fi
host_raw="$OUTPUT_DIR/$tag.host.raw"
gpu_raw="$OUTPUT_DIR/$tag.gpu.raw"
run_one "$tag.host" "$host_raw" 0 "$formats"
run_one "$tag.gpu" "$gpu_raw" 1 "$formats"
compare=(python3 "$ROOT/tools/compare_rr_output.py" --a "$host_raw" --b "$gpu_raw"
--width "$OUT_W" --height "$OUT_H" --frames "$FRAMES" --same-frames)
[[ "${D4R_RR_ALPHA:-0}" == 1 ]] && compare+=(--alpha --alpha-width $((OUT_W + 8)) --alpha-height $((OUT_H + 4))
--alpha-base 4 2)
if [[ "${D4R_HARNESS_RR_ALPHA_FORMAT:-r32f}" == r8unorm ||
"${D4R_HARNESS_RR_ALPHA_FORMAT:-r32f}" == r16unorm ]]; then
compare+=(--alpha-sentinel 0)
fi
printf '\n== comparing %s host guides against %s GPU guides\n' "$tag" "$tag"
"${compare[@]}" || status=1
if [[ "${D4R_RR_GUIDE_PROBE:-0}" == 1 ]]; then
# The probe is a per-run check, so each guide path has to pass it on its own.
for label in "$tag.host" "$tag.gpu"; do
if [[ ! -f "$OUTPUT_DIR/$label.raw.probe.txt" ]]; then
printf 'Guide response probe did not run or did not pass for %s\n' "$label" >&2
status=1
else
cat "$OUTPUT_DIR/$label.raw.probe.txt"
fi
done
fi
done
printf '\n%s\n' "$([[ $status -eq 0 ]] && echo 'All guide path comparisons passed.' || echo 'Guide path comparison FAILED.')"
exit $status
+89
View File
@@ -0,0 +1,89 @@
"""Rendered RGB, alpha subrect preservation, and per-frame output comparison."""
from pathlib import Path
import struct
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
COMPARE = ROOT / "tools" / "compare_rr_output.py"
class RayReconstructionOutputTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.a = Path(self.directory.name) / "host.raw"
self.b = Path(self.directory.name) / "gpu.raw"
def compare(self, width=1, height=1, *extra):
return subprocess.run(
["python3", str(COMPARE), "--a", str(self.a), "--b", str(self.b),
"--width", str(width), "--height", str(height), *extra],
capture_output=True, text=True)
def test_channel_swap_with_identical_luma_is_rejected(self):
self.a.write_bytes(struct.pack("<eeee", 0.75, 0.125, 0.25, 1.0))
self.b.write_bytes(struct.pack("<eeee", 0.25, 0.125, 0.75, 1.0))
self.assertNotEqual(self.compare().returncode, 0)
def test_matching_nonfinite_pixels_are_rejected(self):
for value in (float("nan"), float("inf")):
with self.subTest(value=value):
raw = struct.pack("<eeee", value, 0.25, 0.5, 1.0)
self.a.write_bytes(raw)
self.b.write_bytes(raw)
self.assertNotEqual(self.compare().returncode, 0)
def test_truncated_image_is_rejected(self):
self.a.write_bytes(struct.pack("<eeee", 0.25, 0.5, 0.75, 1.0))
self.b.write_bytes(self.a.read_bytes()[:-1])
self.assertNotEqual(self.compare().returncode, 0)
def alpha_fixture(self, frames=0):
raw = struct.pack("<eeee", 0.25, 0.5, 0.75, 1.0) * 2
values = [-2.0] * 12
values[5:7] = [0.25, 0.75]
alpha = struct.pack("<12f", *values)
for run in (self.a, self.b):
run.write_bytes(raw)
Path(str(run) + ".alpha.raw").write_bytes(alpha)
for frame in range(1, frames + 1):
Path(f"{run}.frame{frame}").write_bytes(raw)
Path(f"{run}.alpha{frame}").write_bytes(alpha)
return values
def compare_alpha(self, frames=0):
return self.compare(2, 1, "--alpha", "--alpha-width", "4", "--alpha-height", "3",
"--alpha-base", "1", "1", "--frames", str(frames), "--same-frames")
def test_alpha_subrect_corruption_is_rejected_even_when_both_runs_agree(self):
values = self.alpha_fixture()
self.assertEqual(self.compare_alpha().returncode, 0)
values[0] = 0.5
for run in (self.a, self.b):
Path(str(run) + ".alpha.raw").write_bytes(struct.pack("<12f", *values))
self.assertNotEqual(self.compare_alpha().returncode, 0)
def test_flat_unwritten_alpha_is_rejected_even_when_both_runs_agree(self):
values = self.alpha_fixture()
values[5:7] = [0.0, 0.0]
for run in (self.a, self.b):
Path(str(run) + ".alpha.raw").write_bytes(struct.pack("<12f", *values))
self.assertNotEqual(self.compare_alpha().returncode, 0)
def test_alpha_frame_mismatch_is_not_hidden_by_matching_final_output(self):
values = self.alpha_fixture(frames=2)
self.assertEqual(self.compare_alpha(frames=2).returncode, 0)
values[5:7] = [0.5, 0.5]
Path(f"{self.b}.alpha1").write_bytes(struct.pack("<12f", *values))
self.assertNotEqual(self.compare_alpha(frames=2).returncode, 0)
def test_missing_alpha_frame_is_rejected(self):
self.alpha_fixture(frames=2)
Path(f"{self.b}.alpha2").unlink()
self.assertNotEqual(self.compare_alpha(frames=2).returncode, 0)
if __name__ == "__main__":
unittest.main()
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Compares two d3d12_dlss_harness Ray Reconstruction runs.
The harness writes the presented image as a raw RGBA16F (or RGBA8) buffer, one
.frameN file per frame with D4R_HARNESS_SAVE_FRAMES=1 and, with the alpha
scenario, a .alpha.raw of the alpha texture. This tool compares two such runs -
in practice the same inputs with the shim's guide path on the GPU
(D4R_SHIM_RR_VRAM_GUIDES=1) and on the host (=0) - and says what differs:
* image layout: every file has to be exactly width * height * bytesPerTexel,
* rendered content: every RGB component's mean/max absolute difference, PSNR,
and nonfinite samples, over the final image and every saved frame,
* frame matching: frame N of one run can be required to match frame N of the
other; optional temporal thresholds are explicit caller expectations,
* alpha: the same comparison of the alpha buffers, plus the sentinel outside
the caller's output subrect, which the denoiser must not touch.
Exit status is 0 only when every requested check passes.
usage: compare_rr_output.py --a RUN_A --b RUN_B --width W --height H
[--frames N] [--alpha] [--alpha-base X Y]
[--min-psnr DB] [--max-mean DIFF] [--alpha-sentinel VALUE]
"""
import argparse
import math
import os
import struct
import sys
def load(path, width, height, bpp):
expected = width * height * bpp
with open(path, "rb") as handle:
data = handle.read()
if len(data) != expected:
raise ValueError(f"{path}: {len(data)} bytes, expected {expected} "
f"({width}x{height} at {bpp} bytes per texel)")
return data
def rgb_plane(path, width, height, bpp):
"""Compare every RGB component: luma alone can hide a red/blue swap."""
raw = load(path, width, height, bpp)
if bpp == 4:
return [raw[index * 4 + channel] / 255.0
for index in range(width * height) for channel in range(3)]
return [component for pixel in struct.iter_unpack("<eeee", raw) for component in pixel[:3]]
def compare_planes(first, second, pixels, mask=None):
"""Mean/max absolute component difference, PSNR and nonfinite sample count."""
total = 0.0
peak = 0.0
squared = 0.0
counted = 0
nonfinite = 0
for index in range(pixels):
if mask is not None and not mask(index):
continue
left, right = first[index], second[index]
if not (math.isfinite(left) and math.isfinite(right)):
nonfinite += 1
continue
delta = abs(left - right)
total += delta
squared += delta * delta
peak = max(peak, delta)
counted += 1
if counted == 0:
return {"mean": float("nan"), "max": float("nan"), "psnr": float("nan"),
"nonfinite": nonfinite, "pixels": 0}
mean = total / counted
mse = squared / counted
psnr = float("inf") if mse == 0.0 else 10.0 * math.log10(1.0 / mse)
return {"mean": mean, "max": peak, "psnr": psnr, "nonfinite": nonfinite, "pixels": counted}
def report(name, stats, failures, limits, require_same):
print(f" {name}: mean |delta| {stats['mean']:.6f} max {stats['max']:.6f} "
f"PSNR {stats['psnr']:.2f} dB nonfinite {stats['nonfinite']} "
f"pixels {stats['pixels']}")
if stats["pixels"] == 0:
failures.append(f"{name}: nothing to compare")
return
if require_same:
if stats["psnr"] < limits["min_psnr"] or stats["mean"] > limits["max_mean"]:
failures.append(f"{name}: PSNR {stats['psnr']:.2f} dB below {limits['min_psnr']}, "
f"mean {stats['mean']:.6f} above {limits['max_mean']}")
if stats["nonfinite"]:
failures.append(f"{name}: {stats['nonfinite']} nonfinite samples")
def alpha_checks(path, width, height, base_x, base_y, out_width, out_height, failures, sentinel):
"""Sentinel outside the subrect, finite values inside, coverage rising with x."""
values = struct.unpack(f"<{width * height}f", load(path, width, height, 4))
outside = 0
invalid = 0
columns = [0.0] * out_width
counts = [0] * out_width
for y in range(height):
for x in range(width):
value = values[y * width + x]
inside = (base_x <= x < base_x + out_width and base_y <= y < base_y + out_height)
if not inside:
if value != sentinel:
outside += 1
continue
if not math.isfinite(value) or value < -0.001 or value > 1.001:
invalid += 1
continue
local = x - base_x
if 0 <= local < out_width:
columns[local] += value
counts[local] += 1
means = [(columns[i] / counts[i]) if counts[i] else 0.0 for i in range(out_width)]
rising = sum(1 for i in range(1, out_width) if means[i] >= means[i - 1])
trend = rising / max(1, out_width - 1)
print(f" alpha {path}: outside-subrect writes {outside}, invalid {invalid}, "
f"rising-column fraction {trend:.3f}, first {means[0]:.4f} last {means[-1]:.4f}")
if outside:
failures.append(f"{path}: {outside} pixels outside the output subrect were overwritten")
if invalid:
failures.append(f"{path}: {invalid} alpha values outside [0, 1] or nonfinite")
if out_width > 1 and means[-1] - means[0] < 0.1:
failures.append(f"{path}: alpha output lost the input's left-to-right coverage ramp")
return trend
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--a", required=True, help="first run's output raw")
parser.add_argument("--b", required=True, help="second run's output raw")
parser.add_argument("--width", type=int, required=True)
parser.add_argument("--height", type=int, required=True)
parser.add_argument("--rgba8", action="store_true", help="inputs were 8-bit, not 16-bit float")
parser.add_argument("--frames", type=int, default=0,
help="compare this many .frameN files of both runs")
parser.add_argument("--alpha", action="store_true", help="also compare the .alpha.raw buffers")
parser.add_argument("--alpha-width", type=int, default=0)
parser.add_argument("--alpha-height", type=int, default=0)
parser.add_argument("--alpha-sentinel", type=float, default=-2.0,
help="expected value outside the alpha subrect (0 for UNORM destinations)")
parser.add_argument("--alpha-base", type=int, nargs=2, default=(0, 0),
metavar=("X", "Y"))
parser.add_argument("--min-psnr", type=float, default=45.0)
parser.add_argument("--max-mean", type=float, default=0.002)
parser.add_argument("--same-frames", action="store_true",
help="every saved frame of one run must match the other's")
parser.add_argument("--temporal-change", type=float, default=None,
help="required mean |delta| between consecutive saved frames")
parser.add_argument("--temporal-still", type=float, default=None,
help="upper bound on |delta| between consecutive saved frames of a run "
"whose inputs did not change")
args = parser.parse_args()
bpp = 4 if args.rgba8 else 8
limits = {"min_psnr": args.min_psnr, "max_mean": args.max_mean}
failures = []
print(f"Comparing {args.a} with {args.b} at {args.width}x{args.height} "
f"({4 if args.rgba8 else 8} bytes per texel)")
first = rgb_plane(args.a, args.width, args.height, bpp)
second = rgb_plane(args.b, args.width, args.height, bpp)
pixels = args.width * args.height * 3
report("final frame", compare_planes(first, second, pixels), failures, limits, True)
if args.frames:
for frame in range(1, args.frames + 1):
path_a = f"{args.a}.frame{frame}"
path_b = f"{args.b}.frame{frame}"
if not (os.path.exists(path_a) and os.path.exists(path_b)):
failures.append(f"frame {frame}: missing {path_a if not os.path.exists(path_a) else path_b}")
continue
plane_a = rgb_plane(path_a, args.width, args.height, bpp)
plane_b = rgb_plane(path_b, args.width, args.height, bpp)
report(f"frame {frame}", compare_planes(plane_a, plane_b, pixels), failures, limits,
args.same_frames)
for run, plane in (("a", first), ("b", second)):
if args.temporal_change is None and args.temporal_still is None:
break
previous = None
for frame in range(1, args.frames + 1):
path = f"{getattr(args, run)}.frame{frame}"
if not os.path.exists(path):
continue
current = rgb_plane(path, args.width, args.height, bpp)
if previous is not None:
stats = compare_planes(previous, current, pixels)
limit = args.temporal_change if args.temporal_change is not None else args.temporal_still
if args.temporal_change is not None and stats["mean"] < limit:
failures.append(f"run {run} frame {frame}: consecutive frames differ by only "
f"{stats['mean']:.6f}, the per-frame change did not reach the image")
if args.temporal_still is not None and stats["mean"] > limit:
failures.append(f"run {run} frame {frame}: consecutive frames of an unchanged "
f"input differ by {stats['mean']:.6f}")
print(f" run {run} frame {frame - 1} -> {frame}: mean |delta| {stats['mean']:.6f}")
previous = current
if args.alpha:
width = args.alpha_width or args.width + 8
height = args.alpha_height or args.height + 4
alpha_a_path, alpha_b_path = args.a + ".alpha.raw", args.b + ".alpha.raw"
missing = [path for path in (alpha_a_path, alpha_b_path) if not os.path.exists(path)]
if missing:
# An alpha run that wrote nothing is a failed comparison, not a reason to stop here.
failures.append("alpha buffers missing: " + ", ".join(missing))
else:
alpha_failures = []
trend_a = alpha_checks(alpha_a_path, width, height, args.alpha_base[0],
args.alpha_base[1], args.width, args.height, alpha_failures, args.alpha_sentinel)
trend_b = alpha_checks(alpha_b_path, width, height, args.alpha_base[0],
args.alpha_base[1], args.width, args.height, alpha_failures, args.alpha_sentinel)
alpha_a = struct.unpack(f"<{width * height}f", load(alpha_a_path, width, height, 4))
alpha_b = struct.unpack(f"<{width * height}f", load(alpha_b_path, width, height, 4))
stats = compare_planes(alpha_a, alpha_b, len(alpha_a))
report("alpha", stats, alpha_failures, limits, True)
if trend_a < 0.5 or trend_b < 0.5:
alpha_failures.append("alpha coverage does not follow the input's left-to-right ramp")
failures += alpha_failures
for frame in range(1, args.frames + 1):
paths = (f"{args.a}.alpha{frame}", f"{args.b}.alpha{frame}")
missing = [path for path in paths if not os.path.exists(path)]
if missing:
failures.append(f"alpha frame {frame}: missing " + ", ".join(missing))
continue
planes = [struct.unpack(f"<{width * height}f", load(path, width, height, 4))
for path in paths]
report(f"alpha frame {frame}", compare_planes(*planes, width * height),
failures, limits, args.same_frames)
if failures:
print("\nFAIL")
for failure in failures:
print(f" {failure}")
return 1
print("\nPASS")
return 0
if __name__ == "__main__":
sys.exit(main())
+430 -25
View File
@@ -343,6 +343,37 @@ static std::vector<uint8_t> read_back(ID3D12Resource* texture, D3D12_RESOURCE_ST
return result;
}
static UINT alpha_texel_bytes(DXGI_FORMAT format)
{
return format == DXGI_FORMAT_R32_FLOAT ? 4 : (format == DXGI_FORMAT_R8_UNORM ? 1 : 2);
}
// Saved alpha stays float32 regardless of the caller's texture format.
static std::vector<uint8_t> read_alpha(ID3D12Resource* texture, DXGI_FORMAT format, UINT width, UINT height)
{
const UINT texel = alpha_texel_bytes(format);
auto raw = read_back(texture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, width * texel);
if (format == DXGI_FORMAT_R32_FLOAT)
return raw;
const size_t pixels = static_cast<size_t>(width) * height;
raw.resize(pixels * sizeof(float));
for (size_t pixel = pixels; pixel-- > 0;)
{
float value;
if (texel == 1)
value = raw[pixel] / 255.0f;
else
{
uint16_t packed;
std::memcpy(&packed, raw.data() + pixel * texel, sizeof(packed));
value = format == DXGI_FORMAT_R16_FLOAT ? static_cast<float>(std::bit_cast<_Float16>(packed))
: packed / 65535.0f;
}
std::memcpy(raw.data() + pixel * sizeof(float), &value, sizeof(value));
}
return raw;
}
static void trace_stage(const char* stage)
{
const char* path = std::getenv("D4R_HARNESS_TRACE");
@@ -427,6 +458,240 @@ static float pole_scene(float x, float y, UINT outWidth, UINT outHeight)
return 0.46f + 0.03f * v;
}
// --- Ray Reconstruction guide inputs ------------------------------------------
//
// A denoiser guide only proves it reached the network when its content changes the
// result, and only proves its layout when a transposed, channel-swapped,
// half-a-row-off or previous frame's copy changes it differently again. The guides
// are therefore filled with deterministic nonuniform patterns that depend on the
// texel's position, its channel, the plane it belongs to and the frame's phase, and
// the harness can perturb exactly one of those per probe evaluation.
struct GuideFormat
{
DXGI_FORMAT format;
unsigned channels;
unsigned bytes; // per texel
const char* token;
};
// The formats a game plausibly passes, one per path the shim treats differently:
// verbatim GPU copies (rgba16f, rgba8unorm, rgba16unorm), Vulkan blits of packed
// or byte-swapped images (bgra8unorm, rgb10a2unorm, rgb11b10float) and the integer
// guides Vulkan cannot blit into floats, which stay on the host (r32uint, r32sint).
static bool guide_format(const char* token, GuideFormat& format)
{
static const GuideFormat table[] = {
{DXGI_FORMAT_R16G16B16A16_FLOAT, 4, 8, "rgba16f"},
{DXGI_FORMAT_R32G32B32A32_FLOAT, 4, 16, "rgba32f"},
{DXGI_FORMAT_R16G16B16A16_UNORM, 4, 8, "rgba16unorm"},
{DXGI_FORMAT_R8G8B8A8_UNORM, 4, 4, "rgba8unorm"},
{DXGI_FORMAT_R8G8B8A8_SNORM, 4, 4, "rgba8snorm"},
{DXGI_FORMAT_B8G8R8A8_UNORM, 4, 4, "bgra8unorm"},
{DXGI_FORMAT_R10G10B10A2_UNORM, 4, 4, "rgb10a2unorm"},
{DXGI_FORMAT_R11G11B10_FLOAT, 4, 4, "rgb11b10float"},
{DXGI_FORMAT_R16G16_FLOAT, 2, 4, "rg16f"},
{DXGI_FORMAT_R32_FLOAT, 1, 4, "r32f"},
{DXGI_FORMAT_R32_UINT, 1, 4, "r32uint"},
{DXGI_FORMAT_R32_SINT, 1, 4, "r32sint"},
};
for (const GuideFormat& entry : table)
if (std::strcmp(entry.token, token) == 0)
{
format = entry;
return true;
}
return false;
}
// One guide texel's value. Plane, channel and phase each shift it, and every plane
// ramps along a different axis with its own checker period, so a guide that arrives
// transposed, from the wrong channel, from another plane or from another frame is a
// visibly different image rather than an indistinguishable one.
static float guide_value(unsigned plane, unsigned channel, UINT x, UINT y, UINT width, UINT height,
unsigned phase)
{
if (channel == 3)
return 1.0f;
const float u = width > 1 ? static_cast<float>(x) / static_cast<float>(width - 1) : 0.0f;
const float v = height > 1 ? static_cast<float>(y) / static_cast<float>(height - 1) : 0.0f;
const float offset = 0.37f * static_cast<float>(plane) + 0.19f * static_cast<float>(channel) +
0.5f * static_cast<float>(phase);
const float ramp = (plane % 2u) != 0 ? v : u;
const float wave = 0.22f * std::sin(6.2831853f * (2.0f * u + 3.0f * v + offset));
const unsigned shift = plane % 4u;
const float checker = ((((x >> shift) ^ (y >> shift)) & 1u) != 0) ? 0.05f : -0.05f;
const float value = 0.45f + 0.25f * ramp + wave + checker;
if (plane == 0) // normals: keep a unit-length, position-dependent normal
{
const float nx = 2.0f * value - 1.0f, ny = 2.0f * (0.45f + 0.25f * v + wave + checker) - 1.0f;
const float nzSquared = std::max(0.0f, 1.0f - nx * nx - ny * ny);
if (channel == 2)
return std::sqrt(nzSquared);
return 0.5f * (nx + 1.0f);
}
return std::clamp(value, 0.0f, 1.0f);
}
// Packs one value into a small float field: 6 mantissa bits for R11G11B10_FLOAT's
// two 11-bit channels, 5 for its 10-bit channel. Values are clamped into range
// first, so everything this writes the format can represent.
static uint16_t pack_small_float(float value, unsigned mantissaBits)
{
if (!(value > 0.0f))
return 0;
constexpr int bias = 15;
const unsigned mantissaScale = 1u << mantissaBits;
const float limit = std::ldexp(1.0f, (1 << 5) - 2 - bias);
value = std::min(value, limit);
int exponent = 0;
std::frexp(value, &exponent);
--exponent; // floor(log2(value))
const int clamped = std::clamp(exponent + bias, 1, (1 << 5) - 2);
const float scaled = std::ldexp(value, static_cast<int>(mantissaBits) - clamped + bias);
const int mantissa = std::clamp(static_cast<int>(std::lround(scaled)) - static_cast<int>(mantissaScale), 0,
static_cast<int>(mantissaScale) - 1);
return static_cast<uint16_t>((static_cast<unsigned>(clamped) << mantissaBits) |
static_cast<unsigned>(mantissa));
}
static void encode_guide_texel(const GuideFormat& format, const float* values, uint8_t* destination)
{
switch (format.format)
{
case DXGI_FORMAT_R16G16B16A16_FLOAT:
case DXGI_FORMAT_R16G16_FLOAT:
for (unsigned channel = 0; channel < format.channels; ++channel)
{
const uint16_t packed = std::bit_cast<uint16_t>(static_cast<_Float16>(values[channel]));
std::memcpy(destination + channel * 2, &packed, sizeof(packed));
}
return;
case DXGI_FORMAT_R32G32B32A32_FLOAT:
case DXGI_FORMAT_R32_FLOAT:
for (unsigned channel = 0; channel < format.channels; ++channel)
std::memcpy(destination + channel * 4, &values[channel], sizeof(float));
return;
case DXGI_FORMAT_R16G16B16A16_UNORM:
for (unsigned channel = 0; channel < format.channels; ++channel)
{
const uint16_t packed =
static_cast<uint16_t>(std::lround(std::clamp(values[channel], 0.0f, 1.0f) * 65535.0f));
std::memcpy(destination + channel * 2, &packed, sizeof(packed));
}
return;
case DXGI_FORMAT_R8G8B8A8_SNORM:
for (unsigned channel = 0; channel < format.channels; ++channel)
destination[channel] = static_cast<uint8_t>(
static_cast<int8_t>(std::clamp(std::lround(values[channel] * 127.0f), -127L, 127L)));
return;
case DXGI_FORMAT_B8G8R8A8_UNORM:
// Stored B, G, R, A: a verbatim copy would hand the denoiser swapped channels.
for (unsigned channel = 0; channel < format.channels; ++channel)
destination[channel < 3 ? 2 - channel : 3] =
static_cast<uint8_t>(std::lround(std::clamp(values[channel], 0.0f, 1.0f) * 255.0f));
return;
case DXGI_FORMAT_R8G8B8A8_UNORM:
for (unsigned channel = 0; channel < format.channels; ++channel)
destination[channel] = static_cast<uint8_t>(std::lround(std::clamp(values[channel], 0.0f, 1.0f) * 255.0f));
return;
case DXGI_FORMAT_R10G10B10A2_UNORM: {
uint32_t packed = 0;
for (unsigned channel = 0; channel < format.channels; ++channel)
{
const unsigned bits = channel == 3 ? 2u : 10u;
const unsigned maximum = (1u << bits) - 1u;
const unsigned value =
static_cast<unsigned>(std::lround(std::clamp(values[channel], 0.0f, 1.0f) * maximum));
packed |= (value & maximum) << (channel * 10u);
}
std::memcpy(destination, &packed, sizeof(packed));
return;
}
case DXGI_FORMAT_R11G11B10_FLOAT: {
const uint32_t packed = static_cast<uint32_t>(pack_small_float(values[0], 6)) |
(static_cast<uint32_t>(pack_small_float(values[1], 6)) << 11u) |
(static_cast<uint32_t>(pack_small_float(values[2], 5)) << 22u);
std::memcpy(destination, &packed, sizeof(packed));
return;
}
case DXGI_FORMAT_R32_UINT: {
const uint32_t packed = static_cast<uint32_t>(std::lround(values[0] * 4096.0f));
std::memcpy(destination, &packed, sizeof(packed));
return;
}
case DXGI_FORMAT_R32_SINT: {
const int32_t packed =
static_cast<int32_t>(std::lround((values[0] - 0.5f) * 8192.0f)); // roughly [-4096, 4096]
std::memcpy(destination, &packed, sizeof(packed));
return;
}
default:
break;
}
std::memset(destination, 0, format.bytes);
}
// --- comparing rendered frames ------------------------------------------------
static float luma_of(const uint8_t* pixel, bool rgba8)
{
float sum = 0.0f;
for (int channel = 0; channel < 3; ++channel)
sum += rgba8 ? static_cast<float>(pixel[channel]) / 255.0f
: static_cast<float>(std::bit_cast<_Float16>(
*reinterpret_cast<const uint16_t*>(pixel + channel * 2)));
return std::clamp(sum / 3.0f, 0.0f, 1.0f);
}
// True once a guide plane has been uploaded, so later phases update rather than create.
static bool g_uploadedGuides = false;
static std::string join_guide_formats(const std::array<std::string, 4>& names)
{
std::string joined;
for (const std::string& name : names)
joined += joined.empty() ? name : ", " + name;
return joined;
}
// Writes each guide plane's nonuniform pattern for one phase. With `push` set the content
// also goes to the plane's texture: the first write creates it, later ones are copy updates,
// exactly as a game re-renders its G-buffer every frame. `leftBias` adds a constant to every
// texel of the left half only, which is what the guide response probe needs: a change the
// denoiser can only reproduce if that texel's guide value reached it, in that place.
template <size_t Planes>
static void fill_guide_planes(std::array<ID3D12Resource*, Planes>& planes,
const std::array<GuideFormat, Planes>& specs,
std::array<std::vector<uint8_t>, Planes>& data, UINT width, UINT height,
unsigned phase, bool push, D3D12_RESOURCE_STATES srv, float leftBias = 0.0f)
{
for (size_t input = 0; input < Planes; ++input)
{
const GuideFormat& spec = specs[input];
data[input].assign(static_cast<size_t>(width) * height * spec.bytes, 0);
for (UINT y = 0; y < height; ++y)
for (UINT x = 0; x < width; ++x)
{
float values[4] = {1.0f, 1.0f, 1.0f, 1.0f};
for (unsigned channel = 0; channel < spec.channels; ++channel)
values[channel] = std::clamp(
guide_value(static_cast<unsigned>(input), channel, x, y, width, height, phase) +
(leftBias != 0.0f && x < width / 2 ? leftBias : 0.0f),
channel == 3 ? 1.0f : 0.0f, 1.0f);
encode_guide_texel(spec, values,
data[input].data() +
(static_cast<size_t>(y) * width + x) * spec.bytes);
}
if (!push || planes[input] == nullptr)
continue;
if (g_uploadedGuides)
update_texture(planes[input], data[input].data(), width * spec.bytes, srv);
else
upload(planes[input], data[input].data(), width * spec.bytes, srv);
}
g_uploadedGuides = true;
}
// D4R_HARNESS_REPLAY_DIR=<dir> replays frames captured in a game by the shim
// (D4R_SHIM_INPUT_DUMP_DIR + D4R_SHIM_CAPTURE_COUNT): frame-NNNNNN-color.rgba16f,
// -depth.r32f, -motion.rg16f and -params.txt, starting at D4R_HARNESS_REPLAY_START.
@@ -654,39 +919,73 @@ int main(int argc, char** argv)
const char* featureSetting = std::getenv("D4R_HARNESS_FEATURE_ID");
const unsigned int featureId = featureSetting ? static_cast<unsigned int>(std::strtoul(featureSetting, nullptr, 0)) : 1;
const bool reconstruction = featureId == 13;
// The four G-buffer planes a game registers, one per parameter. D4R_HARNESS_GUIDE_FORMATS
// picks the format each is passed in, one token per plane in registration order
// (GBuffer.Normals, GBuffer.Roughness, GBuffer.DiffuseAlbedo, GBuffer.SpecularAlbedo),
// so a single run can cover the paths the shim treats differently. The default keeps
// the formats games actually pass for these planes.
const char* guideFormatSetting = std::getenv("D4R_HARNESS_GUIDE_FORMATS");
static const char* const kDefaultGuideFormats[] = {"rgba16f", "rgba16f", "rgba8unorm", "rgba8unorm"};
std::vector<std::string> guideTokens;
if (guideFormatSetting != nullptr && guideFormatSetting[0] != '\0')
{
std::string rest = guideFormatSetting;
while (!rest.empty())
{
const size_t comma = rest.find(',');
guideTokens.push_back(comma == std::string::npos ? rest : rest.substr(0, comma));
rest = comma == std::string::npos ? std::string() : rest.substr(comma + 1);
}
}
std::array<GuideFormat, 4> guideSpecs{};
std::array<std::string, 4> guideNames{};
for (size_t input = 0; input < guideSpecs.size(); ++input)
{
guideNames[input] = input < guideTokens.size() ? guideTokens[input] : kDefaultGuideFormats[input];
if (!guide_format(guideNames[input].c_str(), guideSpecs[input]))
{
std::fprintf(stderr, "Unknown guide format '%s' for guide %zu of 4\n",
guideNames[input].c_str(), input);
return 2;
}
}
// D4R_HARNESS_RR_GUIDE_PHASE=1 rewrites the guides with the next pattern phase on every
// frame, so a stale or one-frame-late guide upload shows up as an output whose guides
// belong to a different frame.
const bool guidePhase = std::getenv("D4R_HARNESS_RR_GUIDE_PHASE") != nullptr;
std::array<std::vector<uint8_t>, 4> guideData{};
std::array<ID3D12Resource*, 4> guides{};
if (reconstruction)
{
// Planar synthetic scene: world-space +Z normals, rough diffuse material.
// Distinct guides exercise denoising inputs rather than aliasing the noisy colour.
std::vector<uint16_t> guide(static_cast<size_t>(resourceWidth) * inHeight * 4);
const std::array<std::array<float, 4>, 4> values{{
{0.0f, 0.0f, 1.0f, 0.0f}, {0.5f, 0.0f, 0.0f, 0.0f},
{0.7f, 0.4f, 0.2f, 1.0f}, {0.04f, 0.04f, 0.04f, 1.0f}}};
for (size_t input = 0; input < guides.size(); ++input)
{
// Cyberpunk supplies UNORM8 albedos, while normals remain floating point.
const bool unorm = input >= 2;
guides[input] = create_texture(resourceWidth, inHeight,
unorm ? DXGI_FORMAT_R8G8B8A8_UNORM : DXGI_FORMAT_R16G16B16A16_FLOAT,
guides[input] = create_texture(resourceWidth, inHeight, guideSpecs[input].format,
D3D12_RESOURCE_FLAG_NONE);
if (!guides[input]) return 1;
for (size_t pixel = 0; pixel < guide.size() / 4; ++pixel)
for (size_t component = 0; component < 4; ++component)
if (unorm)
reinterpret_cast<uint8_t*>(guide.data())[pixel * 4 + component] =
static_cast<uint8_t>(std::lround(values[input][component] * 255.0f));
else
guide[pixel * 4 + component] =
std::bit_cast<uint16_t>(static_cast<_Float16>(values[input][component]));
upload(guides[input], guide.data(), resourceWidth * (unorm ? 4 : 8), srv);
}
fill_guide_planes(guides, guideSpecs, guideData, resourceWidth, inHeight, 0, true, srv);
std::printf("Ray Reconstruction guides: %s%s\n", join_guide_formats(guideNames).c_str(),
guidePhase ? " (rewritten every frame)" : "");
}
const bool alphaScenario = reconstruction && std::getenv("D4R_HARNESS_RR_ALPHA") != nullptr;
const UINT alphaBaseX = 4, alphaBaseY = 2;
const UINT alphaWidth = outWidth + 8, alphaHeight = outHeight + 4;
ID3D12Resource* alphaInput = nullptr;
ID3D12Resource* alphaOutput = nullptr;
DXGI_FORMAT alphaFormat = DXGI_FORMAT_R32_FLOAT;
if (const char* format = std::getenv("D4R_HARNESS_RR_ALPHA_FORMAT"))
{
if (std::strcmp(format, "r16f") == 0) alphaFormat = DXGI_FORMAT_R16_FLOAT;
else if (std::strcmp(format, "r8unorm") == 0) alphaFormat = DXGI_FORMAT_R8_UNORM;
else if (std::strcmp(format, "r16unorm") == 0) alphaFormat = DXGI_FORMAT_R16_UNORM;
else if (std::strcmp(format, "r32f") != 0)
{
std::fprintf(stderr, "Unknown RR alpha output format: %s\n", format);
return 1;
}
}
const bool alphaUnorm = alphaFormat == DXGI_FORMAT_R8_UNORM || alphaFormat == DXGI_FORMAT_R16_UNORM;
const float alphaSentinel = alphaUnorm ? 0.0f : -2.0f;
if (alphaScenario)
{
// A varying coverage guide and a padded destination expose dropped alpha writes and
@@ -697,12 +996,24 @@ int main(int argc, char** argv)
coverage[static_cast<size_t>(y) * resourceWidth + x] =
0.25f + 0.5f * static_cast<float>(x) / std::max(1u, resourceWidth - 1);
alphaInput = create_texture(resourceWidth, inHeight, DXGI_FORMAT_R32_FLOAT, D3D12_RESOURCE_FLAG_NONE);
alphaOutput = create_texture(alphaWidth, alphaHeight, DXGI_FORMAT_R32_FLOAT,
alphaOutput = create_texture(alphaWidth, alphaHeight, alphaFormat,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
if (!alphaInput || !alphaOutput) return 1;
upload(alphaInput, coverage.data(), resourceWidth * sizeof(float), srv);
std::vector<float> sentinel(static_cast<size_t>(alphaWidth) * alphaHeight, -2.0f);
upload(alphaOutput, sentinel.data(), alphaWidth * sizeof(float), D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
const UINT texel = alpha_texel_bytes(alphaFormat);
std::vector<uint8_t> sentinel(static_cast<size_t>(alphaWidth) * alphaHeight * texel, 0);
if (!alphaUnorm)
for (size_t pixel = 0; pixel < static_cast<size_t>(alphaWidth) * alphaHeight; ++pixel)
{
if (texel == 4)
std::memcpy(sentinel.data() + pixel * texel, &alphaSentinel, sizeof(alphaSentinel));
else
{
const uint16_t packed = std::bit_cast<uint16_t>(static_cast<_Float16>(alphaSentinel));
std::memcpy(sentinel.data() + pixel * texel, &packed, sizeof(packed));
}
}
upload(alphaOutput, sentinel.data(), alphaWidth * texel, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
}
ID3D12Resource* colorTexture = create_texture(resourceWidth, inHeight,
rgba8 ? DXGI_FORMAT_R8G8B8A8_UNORM : DXGI_FORMAT_R16G16B16A16_FLOAT, D3D12_RESOURCE_FLAG_NONE);
@@ -965,6 +1276,13 @@ int main(int argc, char** argv)
outWidth * (rgba8 || rgb10 ? 4 : 8), D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
d4r_ngx_set_d3d12_resource(parameters, "Output", outputTexture);
}
if (reconstruction && guidePhase && frame > 1)
{
// The guides this frame are a different pattern, so the output can only be right
// if this frame's own guides were the ones the denoiser read.
fill_guide_planes(guides, guideSpecs, guideData, resourceWidth, inHeight,
static_cast<unsigned>(frame), true, srv);
}
if (qualityScene)
{
// Halton(2,3) jitter in render pixels, [-0.5, 0.5); a render pixel
@@ -1228,6 +1546,18 @@ int main(int argc, char** argv)
std::fclose(frameFile);
}
}
// The alpha the denoiser produced for this frame, so a host and a GPU guide run can be
// compared frame by frame and not only on the last one.
if (saveFrames && alphaScenario)
{
const std::vector<uint8_t> alphaFrame = read_alpha(alphaOutput, alphaFormat, alphaWidth, alphaHeight);
const std::string alphaPath = std::string(argv[2]) + ".alpha" + std::to_string(frame);
if (FILE* alphaFile = std::fopen(alphaPath.c_str(), "wb"))
{
std::fwrite(alphaFrame.data(), 1, alphaFrame.size(), alphaFile);
std::fclose(alphaFile);
}
}
}
const std::vector<uint8_t> output = read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
@@ -1242,6 +1572,82 @@ int main(int argc, char** argv)
for (uint8_t value : output)
nonzero += value != 0;
std::printf("output read back: %zu of %zu bytes nonzero, written to %s\n", nonzero, output.size(), argv[2]);
// Two reset evaluations with identical colour and depth, perturbing only the
// left-half guides, check that guide changes affect the rendered scene.
// Transformer attention can spread the response beyond the changed half;
// exact plane identity and layout are checked separately by VRAM verification.
if (reconstruction && std::getenv("D4R_HARNESS_RR_GUIDE_PROBE") != nullptr)
{
auto evaluate_reset_once = [&]() {
d4r_ngx_set_int(parameters, "Reset", 1);
const NgxResult evaluated = evaluate(g_list, feature, parameters, nullptr);
submit_and_wait();
Sleep(frameWaitMs);
return evaluated;
};
fill_guide_planes(guides, guideSpecs, guideData, resourceWidth, inHeight, 0, true, srv);
if (evaluate_reset_once() != NGX_SUCCESS)
{
std::fprintf(stderr, "Guide probe: the baseline evaluation was refused\n");
return 1;
}
const std::vector<uint8_t> baseline =
read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outWidth * (rgba8 ? 4 : 8));
fill_guide_planes(guides, guideSpecs, guideData, resourceWidth, inHeight, 0, true, srv, 0.4f);
if (evaluate_reset_once() != NGX_SUCCESS)
{
std::fprintf(stderr, "Guide probe: the left-half evaluation was refused\n");
return 1;
}
const std::vector<uint8_t> shifted =
read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outWidth * (rgba8 ? 4 : 8));
const size_t texel = rgba8 ? 4u : 8u;
double left = 0.0, right = 0.0;
size_t leftCount = 0, rightCount = 0, nonfinite = 0;
for (UINT y = 0; y < outHeight; ++y)
for (UINT x = 0; x < outWidth; ++x)
{
const size_t pixel = (static_cast<size_t>(y) * outWidth + x) * texel;
const float first = luma_of(baseline.data() + pixel, rgba8);
const float second = luma_of(shifted.data() + pixel, rgba8);
nonfinite += !std::isfinite(first) || !std::isfinite(second);
const double delta = std::fabs(static_cast<double>(first) - static_cast<double>(second));
if (x < outWidth / 2)
{
left += delta;
++leftCount;
}
else
{
right += delta;
++rightCount;
}
}
const double leftMean = leftCount ? left / leftCount : 0.0;
const double rightMean = rightCount ? right / rightCount : 0.0;
auto report_probe = [&](FILE* report) {
std::fprintf(report, "Guide probe: left-half mean |delta| %.6f, right-half %.6f, ratio %.4f, "
"nonfinite %zu\n",
leftMean, rightMean, leftMean > 0.0 ? rightMean / leftMean : 0.0, nonfinite);
};
report_probe(stdout);
const std::string probePath = std::string(argv[2]) + ".probe.txt";
if (FILE* report = std::fopen(probePath.c_str(), "w"))
{
report_probe(report);
std::fclose(report);
}
// Require a visible response, not locality: transformer attention can
// legitimately change both halves even when only one guide half changes.
if (!(leftMean + rightMean > 0.002) || nonfinite != 0)
{
std::fprintf(stderr,
"Ray Reconstruction guide changes did not affect the rendered image: "
"left-half mean |delta| %.6f, right-half %.6f\n",
leftMean, rightMean);
return 1;
}
}
// The shim queues each evaluation on its own worker, so a failure inside the denoiser is
// invisible in this thread's EvaluateFeature return value: the frame is accepted and the
// output texture is simply never written. The image is the only observable. On the static
@@ -1314,8 +1720,7 @@ int main(int argc, char** argv)
}
if (alphaScenario)
{
const std::vector<uint8_t> raw =
read_back(alphaOutput, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, alphaWidth * sizeof(float));
const std::vector<uint8_t> raw = read_alpha(alphaOutput, alphaFormat, alphaWidth, alphaHeight);
const float* values = reinterpret_cast<const float*>(raw.data());
size_t invalid = 0, overwritten = 0, leftCount = 0, rightCount = 0;
double left = 0, right = 0;
@@ -1327,7 +1732,7 @@ int main(int argc, char** argv)
y >= alphaBaseY && y < alphaBaseY + outHeight;
if (!inside)
{
overwritten += value != -2.0f;
overwritten += value != alphaSentinel;
continue;
}
invalid += !std::isfinite(value) || value < -0.001f || value > 1.001f;
+689 -136
View File
File diff suppressed because it is too large. Load diff
+8 -2
View File
@@ -1466,7 +1466,6 @@ static const char* find_function_name(CUfunction function)
pthread_mutex_unlock(&instrumentation_lock);
return name;
}
/* Opt-in per-kernel GPU elapsed time. Event pairs are recorded on the same
stream as each launch, then read only after an existing context sync. This
adds profiling overhead and must never be used as the FPS baseline. */
@@ -2261,11 +2260,18 @@ CUresult WINAPI cuDeviceGetLuid(char* luid, unsigned int* device_node_mask, CUde
if (function == NULL)
return missing("cuDeviceGetLuid");
CUresult result = function(luid, device_node_mask, device);
if (result == CUDA_SUCCESS && luid != NULL && device_node_mask != NULL)
if ((result == CUDA_SUCCESS || result == CUDA_ERROR_NOT_SUPPORTED) &&
luid != NULL && device_node_mask != NULL)
{
unsigned int low = get_process_u32("D4R_CUDA_LUID_LOW", 0xffffffffu);
unsigned int high = get_process_u32("D4R_CUDA_LUID_HIGH", 0xffffffffu);
unsigned int node_mask = get_process_u32("D4R_CUDA_NODE_MASK", 0xffffffffu);
// Linux NVIDIA cannot report a Windows LUID. The shim supplies the actual
// D3D12 adapter mapping; only a complete mapping can replace NOT_SUPPORTED.
if (result == CUDA_ERROR_NOT_SUPPORTED &&
(low == 0xffffffffu || high == 0xffffffffu || node_mask == 0xffffffffu))
return result;
result = CUDA_SUCCESS;
if (low != 0xffffffffu)
memcpy(luid, &low, sizeof(low));
if (high != 0xffffffffu)