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

+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)