#!/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(" 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())