shim/harness: capture and replay a frame's Ray Reconstruction guides exactly

A replay only means something if the denoiser gets the bytes the game gave it.
The capture now records, for every bound guide, the caller's own DXGI format and
texel size next to the plane (they differ from the CUDA array's for every
converted format, and B8G8R8A8_UNORM is four bytes to D3D12 and sixteen to the
float array), the names the caller registered each surface under as well as the
names the denoiser reads it as, the exposure texture's own geometry, and the
feature's creation state - dimensions, quality, create flags, output-subrect
setting and the effective render preset. A frame whose staging is not readable
aborts its whole capture instead of writing a descriptor without its plane.

The harness reads all of it back: it sizes and creates the feature from the
capture, creates each guide in the recorded format at the recorded size, uploads
the recorded bytes, applies the recorded scalars, subrect origins, inversion
flags and camera matrices, binds the exact source names and clears the ones the
frame stopped registering. A missing guide descriptor, a missing or mis-sized
plane, a truncated parameter line, a claimed exposure with no geometry, or a
capture taken before the creation state was recorded is refused rather than
filled in with a default, because a replay that invents any of it still renders
a plausible image that matches nothing the game produced.
This commit is contained in:
woutermans committed 2026-10-12 12:47:53 +02:00
1 parent 74ce9d8f9a
commit 0dc2397e2b
3 files changed
+1170 -53

No files matched your search

+427
View File
@@ -0,0 +1,427 @@
"""Ray Reconstruction capture and replay: the exact guides travel, or the replay fails.
The shim's input dump records the guides a game bound, in the formats the game passed them,
and the harness replay feeds those back instead of its own synthetic planes. Both are checked
here against the real source, because the failure they exist to prevent is a plausible image
produced from fewer guides than the game rendered.
"""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def extract(source, start_marker, end_marker):
start = source.index(start_marker)
return source[start:source.index(end_marker, start)]
def build_and_run(body, name, with_directory=False):
with tempfile.TemporaryDirectory() as temporary:
runner = Path(temporary) / name
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-x', 'c++', '-', '-o', str(runner)], input=body,
text=True, capture_output=True, check=True)
# The harness joins its frame prefix with a backslash, which is one file name on this
# platform; the capture directory therefore holds files named "capture\frame-NNNNNN-...".
capture = Path(temporary) / 'capture'
capture.mkdir()
command = [str(runner), str(capture)] if with_directory else [str(runner)]
subprocess.run(command, check=True, timeout=10)
class AuxCaptureTests(unittest.TestCase):
"""dump_aux_guides must record the caller's bytes and the descriptor that reads them."""
def test_capture_records_every_guide_in_the_callers_format(self):
shim = (ROOT / 'tools/d4r_nvngx_shim.cpp').read_text()
tables = extract(shim, 'struct AuxScalar', 'struct FrameParams')
# The forward declaration in that slice has no definition here and nothing uses it.
tables = tables.replace(
'static unsigned int get_uint_or(void* parameters, const char* name, unsigned int fallback);\n', '')
capture = extract(shim, 'static void dump_aux_guides', '// Worker. Points the denoiser')
prelude = r'''
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <cassert>
using UINT = unsigned;
using DXGI_FORMAT = unsigned;
constexpr int MAX_PATH = 260;
constexpr int kSlots = 4;
enum class AuxLayout { Verbatim, Converted };
struct Staging
{
uint8_t* mapped = nullptr;
struct { size_t Offset; struct { size_t RowPitch; } Footprint; } layout = {};
};
struct AuxSurface
{
Staging staging[kSlots];
uint32_t texelBytes = 0;
UINT width = 0, height = 0;
DXGI_FORMAT format = 0;
AuxLayout how = AuxLayout::Verbatim;
};
struct AuxBinding
{
AuxSurface* surface = nullptr;
std::vector<const char*> names;
std::vector<const char*> sources;
};
struct InputSlot { std::vector<AuxBinding> auxBindings; };
struct Feature { InputSlot inputs[kSlots]; };
''' + tables + r'''
struct FrameParams
{
int auxScalar[sizeof(kAuxScalars) / sizeof(kAuxScalars[0])] = {};
unsigned int auxSubrect[sizeof(kAuxSubrectNames) / sizeof(kAuxSubrectNames[0])][2] = {};
float auxMatrix[sizeof(kAuxMatrices) / sizeof(kAuxMatrices[0])][16] = {};
bool auxMatrixSet[sizeof(kAuxMatrices) / sizeof(kAuxMatrices[0])] = {};
};
static void logf(const char*, ...) {}
static std::string join_aux_names(const std::vector<const char*>& names)
{
std::string joined;
for (const char* name : names)
joined += joined.empty() ? name : std::string(", ") + name;
return joined;
}
static bool input_dump_directory(uint32_t, char (&directory)[MAX_PATH])
{
std::snprintf(directory, MAX_PATH, "capture");
return true;
}
static uint8_t* capture_pinned(const char*, size_t) { return nullptr; }
struct Written { std::string path; std::vector<uint8_t> bytes; };
static std::vector<Written> g_written;
static void write_capture_file(uint32_t, const char*, const char* path, const void* data, size_t bytes)
{
const uint8_t* first = static_cast<const uint8_t*>(data);
g_written.push_back({path, std::vector<uint8_t>(first, first + bytes)});
}
'''
program = prelude + capture + r'''
static const char* text_of(const std::string& path)
{
for (const Written& written : g_written)
if (written.path == path)
return reinterpret_cast<const char*>(written.bytes.data());
return nullptr;
}
int main()
{
Feature feature;
InputSlot& slot = feature.inputs[0];
// A 3x2 BGRA8 guide: the shim converts this to float32 on upload, so the capture must hold
// the caller's four bytes per texel, not the array's sixteen.
constexpr size_t rowPitch = 3 * 4 + 8; // padded, as a readback heap is
std::vector<uint8_t> bgra(rowPitch * 2), depth(3 * 2 * 4);
for (size_t i = 0; i < bgra.size(); ++i) bgra[i] = static_cast<uint8_t>(i + 1);
for (size_t i = 0; i < depth.size(); ++i) depth[i] = static_cast<uint8_t>(200 - i);
AuxSurface surfaces[2];
surfaces[0].staging[0].mapped = bgra.data();
surfaces[0].staging[0].layout.Offset = 0;
surfaces[0].staging[0].layout.Footprint.RowPitch = rowPitch;
surfaces[0].texelBytes = 4;
surfaces[0].width = 3;
surfaces[0].height = 2;
surfaces[0].format = 87; // DXGI_FORMAT_B8G8R8A8_UNORM
surfaces[0].how = AuxLayout::Converted;
surfaces[1].staging[0].mapped = depth.data();
surfaces[1].staging[0].layout.Offset = 0;
surfaces[1].staging[0].layout.Footprint.RowPitch = 3 * 4;
surfaces[1].texelBytes = 4;
surfaces[1].width = 3;
surfaces[1].height = 2;
surfaces[1].format = 87;
surfaces[1].how = AuxLayout::Converted;
slot.auxBindings.push_back({&surfaces[0], {"GBuffer.Normals"}, {"GBuffer.Normals"}});
slot.auxBindings.push_back({&surfaces[1], {"DLSS.Input.DiffuseAlbedo"}, {"GBuffer.DiffuseAlbedo",
"DLSS.Input.DiffuseAlbedo"}});
FrameParams params;
params.auxScalar[0] = 1;
params.auxSubrect[0][0] = 5;
params.auxSubrect[0][1] = 7;
params.auxMatrixSet[0] = true;
params.auxMatrix[0][0] = 1.5f;
params.auxMatrix[0][5] = -2.25f;
dump_aux_guides(feature, 0, 42, params);
// Two planes and one descriptor, named for the frame.
assert(g_written.size() == 3);
const std::string plane0 = g_written[0].path, plane1 = g_written[1].path;
assert(plane0.find("frame-000042-guide-00") != std::string::npos);
assert(plane1.find("frame-000042-guide-01") != std::string::npos);
assert(g_written[2].path.find("frame-000042-guides.txt") != std::string::npos);
// Tightly packed caller bytes: the padded staging row must not leak into the capture.
assert(g_written[0].bytes.size() == 3 * 2 * 4);
for (UINT row = 0; row < 2; ++row)
for (UINT x = 0; x < 3; ++x)
assert(g_written[0].bytes[(row * 3 + x) * 4] == bgra[row * rowPitch + x * 4]);
// The descriptor names both spellings, the format, the geometry, the caller's texel size and
// whether the shim converts it.
const char* text = text_of(g_written[2].path);
assert(text != nullptr);
assert(std::string(text).find("guides 2\n") == 0);
assert(std::string(text).find("guide 1 GBuffer.DiffuseAlbedo,DLSS.Input.DiffuseAlbedo "
"DLSS.Input.DiffuseAlbedo 87 3 2 4 1") != std::string::npos);
assert(std::string(text).find("scalar DLSS.Denoise.Mode 1") != std::string::npos);
assert(std::string(text).find("subrect DLSS.Input.DiffuseAlbedo.Subrect.Base.X 5") != std::string::npos);
assert(std::string(text).find("subrect DLSS.Input.DiffuseAlbedo.Subrect.Base.Y 7") != std::string::npos);
assert(std::string(text).find("matrix WorldToViewMatrix 1 1.5") != std::string::npos);
assert(std::string(text).find("matrix ViewToClipMatrix 0") != std::string::npos);
// A guide whose bytes cannot be read fails the whole capture: a descriptor without its plane
// is an image made of fewer guides than the game gave, and nothing downstream would show it.
g_written.clear();
surfaces[1].staging[0].mapped = nullptr;
dump_aux_guides(feature, 0, 43, params);
assert(g_written.empty());
}
'''
build_and_run(program, 'rr-guide-capture')
class ReplayGuideLoadTests(unittest.TestCase):
"""A Ray Reconstruction replay must load the recorded guides, or refuse the run."""
def test_missing_or_malformed_guides_fail_instead_of_defaulting(self):
harness = (ROOT / 'tools/d3d12_dlss_harness.cpp').read_text()
# The guide loader, the file reader it needs and the name helpers; the frame-level
# parameter loader is covered by ReplayParamsTests instead.
replay = (extract(harness, 'struct ReplayGuide', 'static bool read_replay_params') +
extract(harness, 'static std::vector<std::string> split_names', 'static float halton'))
program = r'''
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include <cassert>
using DXGI_FORMAT = unsigned;
constexpr DXGI_FORMAT DXGI_FORMAT_UNKNOWN = 0;
struct ID3D12Resource;
constexpr int MAX_PATH = 260;
''' + replay + r'''
// join_names comes along with the loader and nothing here calls it.
[[maybe_unused]] static std::string unused_join(const std::vector<std::string>& names) { return join_names(names); }
static void write_bytes(const std::string& path, const std::vector<uint8_t>& bytes)
{
FILE* file = std::fopen(path.c_str(), "wb");
assert(file != nullptr);
assert(std::fwrite(bytes.data(), 1, bytes.size(), file) == bytes.size());
std::fclose(file);
}
static void write_text(const std::string& path, const std::string& text)
{
write_bytes(path, std::vector<uint8_t>(text.begin(), text.end()));
}
int main(int, char** argv)
{
const std::string directory = argv[1];
ReplayFrame frame;
// No descriptor at all: an RR replay without the game's guides is refused, not invented.
assert(!load_replay_guides(directory.c_str(), 1, frame));
// A descriptor whose guide plane is missing is refused too.
write_text(directory + "\\frame-000002-guides.txt",
"guides 1\nguide 0 GBuffer.Normals GBuffer.Normals 6 2 2 4 0\n");
assert(!load_replay_guides(directory.c_str(), 2, frame));
// The plane present, the count disagreeing with the file: refused rather than partly read.
write_bytes(directory + "\\frame-000002-guide-00.bin", std::vector<uint8_t>(2 * 2 * 4, 7));
write_text(directory + "\\frame-000002-guides.txt",
"guides 2\nguide 0 GBuffer.Normals GBuffer.Normals 6 2 2 4 0\n");
assert(!load_replay_guides(directory.c_str(), 2, frame));
// A complete descriptor: the guides, the settings, both Subrect axes and the matrices.
write_text(directory + "\\frame-000002-guides.txt",
"guides 1\n"
"guide 0 GBuffer.DiffuseAlbedo,DLSS.Input.DiffuseAlbedo DLSS.Input.DiffuseAlbedo 87 2 2 4 1\n"
"scalar DLSS.Denoise.Mode 1\n"
"subrect DLSS.Input.Normals.Subrect.Base.X 4\n"
"subrect DLSS.Input.Normals.Subrect.Base.Y 9\n"
"matrix WorldToViewMatrix 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n"
"matrix ViewToClipMatrix 0\n");
write_bytes(directory + "\\frame-000002-guide-00.bin", std::vector<uint8_t>(2 * 2 * 4, 3));
assert(load_replay_guides(directory.c_str(), 2, frame));
assert(frame.guides.size() == 1);
assert(frame.guides[0].sources.size() == 2);
assert(frame.guides[0].sources[0] == "GBuffer.DiffuseAlbedo");
assert(frame.guides[0].sources[1] == "DLSS.Input.DiffuseAlbedo");
assert(frame.guides[0].publish[0] == "DLSS.Input.DiffuseAlbedo");
assert(frame.guides[0].format == 87u && frame.guides[0].width == 2 && frame.guides[0].height == 2);
assert(frame.guides[0].texelBytes == 4 && frame.guides[0].converted);
assert(frame.guides[0].bytes.size() == 2 * 2 * 4 && frame.guides[0].bytes[0] == 3);
assert(frame.scalars.size() == 1 && frame.scalars[0].first == "DLSS.Denoise.Mode" &&
frame.scalars[0].second == 1);
assert(frame.subrects.size() == 2 && frame.subrects[0].second == 4 && frame.subrects[1].second == 9);
assert(frame.subrects[1].first == "DLSS.Input.Normals.Subrect.Base.Y");
assert(frame.matrices.size() == 2 && frame.matrixPresent.size() == 2);
assert(frame.matrices[0].second[0] == 1.0f && frame.matrices[0].second[15] == 16.0f);
assert(frame.matrixPresent[0].second);
// A matrix the game did not register stays absent, so the replay clears it rather than
// leaving the previous frame's camera in place.
assert(!frame.matrixPresent[1].second);
}
'''
with tempfile.TemporaryDirectory() as temporary:
# The harness joins the frame prefix with a backslash; on this platform that is one
# file name inside the directory, which is what the capture names are.
capture = Path(temporary) / 'capture'
capture.mkdir()
runner = Path(temporary) / 'rr-replay-load'
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-x', 'c++', '-', '-o', str(runner)], input=program,
text=True, capture_output=True, check=True)
subprocess.run([str(runner), str(capture)], check=True, timeout=10)
class ReplayParamsTests(unittest.TestCase):
"""The exposure plane, indicator axes, Output.Subrect origin and creation state.
Each is something the replay must take from the capture rather than invent: a substituted
exposure value, a default origin or a guessed quality mode is a comparison against a reference
the game never asked for.
"""
def build(self, body, name):
harness = (ROOT / 'tools/d3d12_dlss_harness.cpp').read_text()
source = (extract(harness, 'struct ReplayGuide', 'struct ReplayTexture') +
extract(harness, 'struct ReplayFrame', 'static bool read_file') +
extract(harness, 'static bool read_file', '// Splits a comma-separated name list'))
prelude = r'''
#include <array>
#include <bit>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include <cassert>
using DXGI_FORMAT = unsigned;
constexpr DXGI_FORMAT DXGI_FORMAT_UNKNOWN = 0;
struct ID3D12Resource;
constexpr int MAX_PATH = 260;
'''
common = r'''
static void write_bytes(const std::string& path, const std::vector<uint8_t>& bytes)
{
FILE* file = std::fopen(path.c_str(), "wb");
assert(file != nullptr);
assert(std::fwrite(bytes.data(), 1, bytes.size(), file) == bytes.size());
std::fclose(file);
}
static void write_text(const std::string& path, const std::string& text)
{
write_bytes(path, std::vector<uint8_t>(text.begin(), text.end()));
}
static void write_planes(const std::string& directory, uint32_t frame, unsigned width, unsigned height)
{
char path[MAX_PATH];
std::snprintf(path, sizeof(path), "%s\\frame-%06u-", directory.c_str(), frame);
write_bytes(std::string(path) + "color.rgba16f", std::vector<uint8_t>(width * height * 8, 1));
write_bytes(std::string(path) + "depth.r32f", std::vector<uint8_t>(width * height * 4, 2));
write_bytes(std::string(path) + "motion.rg16f", std::vector<uint8_t>(width * height * 4, 3));
}
'''
build_and_run(prelude + source + common + body, name, with_directory=True)
def test_exposure_geometry_and_creation_state_come_from_the_capture(self):
self.build(r'''
int main(int, char** argv)
{
const std::string directory = argv[1];
ReplayFrame frame;
// A 1x1 exposure scalar: the plane is four bytes, not the harness's own 1.0.
write_planes(directory, 1, 4, 2);
write_bytes(directory + "\\frame-000001-exposure.r32f", std::vector<uint8_t>{0, 0, 0x80, 0x3f});
write_text(directory + "\\frame-000001-params.txt",
"has_exposure 1\n"
"exposure_size 1 1\n"
"invert 1 1\n"
"output_base 7 9\n"
"create 1280 720 2560 1440 2 73 1 5\n");
assert(load_replay_frame(directory.c_str(), 1, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, frame));
assert(frame.hasExposure == 1);
assert(frame.exposureWidth == 1 && frame.exposureHeight == 1);
assert(frame.exposure.size() == 4);
uint32_t exposureBits = 0;
std::memcpy(&exposureBits, frame.exposure.data(), sizeof(exposureBits));
assert(exposureBits == 0x3f800000u); // 1.0f, the value the capture wrote
assert(frame.invertX == 1 && frame.invertY == 1);
assert(frame.outputBaseX == 7 && frame.outputBaseY == 9);
assert(frame.creation.recorded);
assert(frame.creation.width == 1280 && frame.creation.height == 720);
assert(frame.creation.outWidth == 2560 && frame.creation.outHeight == 1440);
assert(frame.creation.quality == 2 && frame.creation.flags == 73);
assert(frame.creation.outputSubrects == 1 && frame.creation.preset == 5);
// A per-pixel exposure buffer, in its own geometry.
ReplayFrame wide;
write_planes(directory, 2, 4, 2);
write_bytes(directory + "\\frame-000002-exposure.r32f", std::vector<uint8_t>(4 * 2 * 4, 9));
write_text(directory + "\\frame-000002-params.txt",
"has_exposure 1\nexposure_size 4 2\ncreate 1280 720 2560 1440 2 73 1 5\n");
assert(load_replay_frame(directory.c_str(), 2, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, wide));
assert(wide.exposureWidth == 4 && wide.exposureHeight == 2 && wide.exposure.size() == 4 * 2 * 4);
// No exposure texture bound: nothing is read, and none is invented.
ReplayFrame none;
write_planes(directory, 3, 4, 2);
write_text(directory + "\\frame-000003-params.txt",
"has_exposure 0\nexposure_size 0 0\ncreate 1280 720 2560 1440 2 73 0 5\n");
assert(load_replay_frame(directory.c_str(), 3, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, none));
assert(none.exposure.empty() && none.creation.outputSubrects == 0);
}
''', 'rr-replay-params')
def test_unreplayable_exposure_and_truncated_params_fail(self):
self.build(r'''
int main(int, char** argv)
{
const std::string directory = argv[1];
ReplayFrame frame;
// A claimed exposure texture whose geometry the capture never recorded: the replay would have
// to invent both the size and the contents.
write_planes(directory, 1, 4, 2);
write_bytes(directory + "\\frame-000001-exposure.r32f", std::vector<uint8_t>(4, 0));
write_text(directory + "\\frame-000001-params.txt", "has_exposure 1\nexposure_size 0 0\n");
assert(!load_replay_frame(directory.c_str(), 1, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, frame));
// Geometry recorded but the plane absent, then present at the wrong size.
write_planes(directory, 2, 4, 2);
write_text(directory + "\\frame-000002-params.txt", "has_exposure 1\nexposure_size 4 4\n");
assert(!load_replay_frame(directory.c_str(), 2, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, frame));
write_bytes(directory + "\\frame-000002-exposure.r32f", std::vector<uint8_t>(4 * 4 * 2, 0));
assert(!load_replay_frame(directory.c_str(), 2, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, frame));
// A truncated parameter is refused rather than left at its default.
write_planes(directory, 3, 4, 2);
write_text(directory + "\\frame-000003-params.txt", "has_exposure 0\noutput_base 4\n");
assert(!load_replay_frame(directory.c_str(), 3, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, frame));
// A capture with no creation state is reported as such, so the run can refuse to guess.
ReplayFrame legacy;
write_planes(directory, 4, 4, 2);
write_text(directory + "\\frame-000004-params.txt", "has_exposure 0\n");
assert(load_replay_frame(directory.c_str(), 4, 4 * 2 * 8, 4 * 2 * 4, 4 * 2 * 4, legacy));
assert(!legacy.creation.recorded);
}
''', 'rr-replay-params-fail')
if __name__ == '__main__':
unittest.main()
+527 -29
View File
@@ -20,6 +20,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#include <atomic>
@@ -695,6 +696,34 @@ static void fill_guide_planes(std::array<ID3D12Resource*, Planes>& planes,
// 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.
// A Ray Reconstruction capture adds -guides.txt, -guide-NN.bin and the exposure plane:
// the denoiser's own inputs, recorded in the format the game passed them, so the replay
// runs the same conversions over the same bytes the game rendered.
struct ReplayGuide
{
std::vector<std::string> sources; // parameter names the caller registered it under
std::vector<std::string> publish; // names the denoiser reads it as
DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN;
unsigned width = 0, height = 0, texelBytes = 0;
bool converted = false; // the shim unpacks this format into floats
std::vector<uint8_t> bytes;
};
// A captured guide's texture, kept across frames so a game that reuses one G-buffer surface
// is replayed as the same resource rather than a fresh one per frame.
struct ReplayTexture
{
ID3D12Resource* resource = nullptr;
DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN;
unsigned width = 0, height = 0;
bool uploaded = false;
};
// Room for the camera matrices a capture records. Fixed, because the parameter object holds
// the pointer until a later frame replaces it: storage that moved would dangle the denoiser's
// read. A capture with more matrices than this is refused rather than truncated.
constexpr size_t kReplayMatrices = 8;
struct ReplayFrame
{
std::vector<uint8_t> color, depth, motion;
@@ -703,6 +732,32 @@ struct ReplayFrame
int reset = 0, hasExposure = 0;
unsigned renderWidth = 0, renderHeight = 0;
unsigned colorBaseX = 0, colorBaseY = 0, depthBaseX = 0, depthBaseY = 0, mvBaseX = 0, mvBaseY = 0;
// Ray Reconstruction: the guides the game bound this frame and the settings, origins and
// camera matrices it gave the denoiser with them. matrixPresent says which of those
// matrices the caller actually registered, so an absent one is cleared rather than
// silently kept from an earlier frame.
std::vector<ReplayGuide> guides;
std::vector<std::pair<std::string, int>> scalars;
std::vector<std::pair<std::string, unsigned>> subrects;
std::vector<std::pair<std::string, std::array<float, 16>>> matrices;
std::vector<std::pair<std::string, bool>> matrixPresent;
// The exposure texture the game actually bound: its own geometry and the captured plane.
// A capture that says a frame had an exposure texture but carries no plane for it cannot be
// replayed exactly, so this is a missing plane, not a value to invent.
unsigned exposureWidth = 0, exposureHeight = 0;
std::vector<uint8_t> exposure;
int invertX = 0, invertY = 0;
unsigned outputBaseX = 0, outputBaseY = 0;
// How the game created this feature. Recorded per frame because it is part of what the
// replay has to reproduce: a different model, resolution, quality mode, create flag set,
// Output.Subrect setting or render preset is a different NVIDIA reference.
struct Creation
{
unsigned width = 0, height = 0, outWidth = 0, outHeight = 0;
int quality = 0, flags = 0, outputSubrects = 0;
unsigned preset = 0;
bool recorded = false;
} creation;
};
static bool read_file(const std::string& path, std::vector<uint8_t>& bytes, size_t expected)
@@ -717,12 +772,79 @@ static bool read_file(const std::string& path, std::vector<uint8_t>& bytes, size
return got == expected && !extra;
}
// Reads one frame's params.txt. Returns false when a parameter the replay needs was truncated,
// so a partial read never leaves a plausible-looking default in place.
static bool read_replay_params(FILE* file, uint32_t frame, ReplayFrame& out)
{
char key[64];
while (std::fscanf(file, "%63s", key) == 1)
{
const std::string name = key;
int read = 0, expected = 0;
if (name == "jitter")
read = std::fscanf(file, "%f %f", &out.jitterX, &out.jitterY), expected = 2;
else if (name == "mv_scale")
read = std::fscanf(file, "%f %f", &out.mvScaleX, &out.mvScaleY), expected = 2;
else if (name == "sharpness") read = std::fscanf(file, "%f", &out.sharpness), expected = 1;
else if (name == "pre_exposure") read = std::fscanf(file, "%f", &out.preExposure), expected = 1;
else if (name == "exposure_scale") read = std::fscanf(file, "%f", &out.exposureScale), expected = 1;
else if (name == "frame_time") read = std::fscanf(file, "%f", &out.frameTime), expected = 1;
else if (name == "reset") read = std::fscanf(file, "%d", &out.reset), expected = 1;
else if (name == "invert") read = std::fscanf(file, "%d %d", &out.invertX, &out.invertY), expected = 2;
else if (name == "has_exposure") read = std::fscanf(file, "%d", &out.hasExposure), expected = 1;
else if (name == "render")
read = std::fscanf(file, "%u %u", &out.renderWidth, &out.renderHeight), expected = 2;
else if (name == "color_base")
read = std::fscanf(file, "%u %u", &out.colorBaseX, &out.colorBaseY), expected = 2;
else if (name == "depth_base")
read = std::fscanf(file, "%u %u", &out.depthBaseX, &out.depthBaseY), expected = 2;
else if (name == "mv_base") read = std::fscanf(file, "%u %u", &out.mvBaseX, &out.mvBaseY), expected = 2;
else if (name == "output_base")
read = std::fscanf(file, "%u %u", &out.outputBaseX, &out.outputBaseY), expected = 2;
else if (name == "exposure_size")
read = std::fscanf(file, "%u %u", &out.exposureWidth, &out.exposureHeight), expected = 2;
else if (name == "create")
{
read = std::fscanf(file, "%u %u %u %u %d %d %d %u", &out.creation.width, &out.creation.height,
&out.creation.outWidth, &out.creation.outHeight, &out.creation.quality,
&out.creation.flags, &out.creation.outputSubrects, &out.creation.preset);
expected = 8;
if (read == expected)
out.creation.recorded = true;
}
else
{
std::fscanf(file, "%*[^\n]");
continue;
}
// Every value the line promised, not merely some of them: a half-read line would leave
// the rest at its default, which is exactly the invented value this refuses.
if (read != expected)
{
std::fprintf(stderr, "replay frame %u: the capture's '%s' line is truncated (%d of %d values)\n",
frame, name.c_str(), read, expected);
return false;
}
}
return true;
}
static bool load_replay_frame(const char* directory, uint32_t frame, size_t colorBytes, size_t depthBytes,
size_t motionBytes, ReplayFrame& out)
{
char prefix[MAX_PATH];
std::snprintf(prefix, sizeof(prefix), "%s\\frame-%06u-", directory, frame);
const std::string base = prefix;
FILE* file = std::fopen((base + "params.txt").c_str(), "r");
if (file == nullptr)
{
std::fprintf(stderr, "replay frame %u: no params\n", frame);
return false;
}
const bool params = read_replay_params(file, frame, out);
std::fclose(file);
if (!params)
return false;
if (!read_file(base + "color.rgba16f", out.color, colorBytes) ||
!read_file(base + "depth.r32f", out.depth, depthBytes) ||
!read_file(base + "motion.rg16f", out.motion, motionBytes))
@@ -730,31 +852,181 @@ static bool load_replay_frame(const char* directory, uint32_t frame, size_t colo
std::fprintf(stderr, "replay frame %u: missing or mis-sized plane under %s\n", frame, prefix);
return false;
}
FILE* file = std::fopen((base + "params.txt").c_str(), "r");
// The exposure plane, in the geometry the capture recorded: the game may have passed a 1x1
// scalar or a per-pixel buffer, and only its own size says which. Geometry missing under a
// claimed exposure texture is refused, not filled in with the harness's 1.0.
if (out.hasExposure != 0)
{
if (out.exposureWidth == 0 || out.exposureHeight == 0)
{
std::fprintf(stderr, "replay frame %u: the capture claims an exposure texture but records no "
"geometry for it\n",
frame);
return false;
}
const size_t bytes = static_cast<size_t>(out.exposureWidth) * out.exposureHeight * sizeof(float);
if (!read_file(base + "exposure.r32f", out.exposure, bytes))
{
std::fprintf(stderr, "replay frame %u: the exposure plane (%ux%u) is missing or mis-sized: %s\n",
frame, out.exposureWidth, out.exposureHeight, (base + "exposure.r32f").c_str());
return false;
}
}
return true;
}
// Splits a comma-separated name list, as the capture writes it.
static std::vector<std::string> split_names(const std::string& joined)
{
std::vector<std::string> names;
size_t start = 0;
while (start <= joined.size())
{
const size_t comma = joined.find(',', start);
names.push_back(joined.substr(start, comma == std::string::npos ? std::string::npos : comma - start));
if (comma == std::string::npos)
break;
start = comma + 1;
}
return names;
}
static std::string join_names(const std::vector<std::string>& names)
{
std::string joined;
for (const std::string& name : names)
joined += joined.empty() ? name : ", " + name;
return joined;
}
// Reads one frame's guides.txt and its guide planes. A Ray Reconstruction replay with no
// guide descriptor is refused: without the game's own guides the denoiser would be handed a
// different scene than the game rendered, and the comparison against the game's own image
// would be meaningless while still producing a plausible picture.
static bool load_replay_guides(const char* directory, uint32_t frame, ReplayFrame& out)
{
char prefix[MAX_PATH];
std::snprintf(prefix, sizeof(prefix), "%s\\frame-%06u-", directory, frame);
const std::string base = prefix;
FILE* file = std::fopen((base + "guides.txt").c_str(), "r");
if (file == nullptr)
{
std::fprintf(stderr, "replay frame %u: no params\n", frame);
std::fprintf(stderr, "replay frame %u: no guides descriptor (%sguides.txt); a Ray Reconstruction replay "
"requires the guides the game bound\n",
frame, prefix);
return false;
}
bool declared = false;
unsigned guides = 0, parsed = 0;
char key[64];
while (std::fscanf(file, "%63s", key) == 1)
{
const std::string name = key;
if (name == "jitter") std::fscanf(file, "%f %f", &out.jitterX, &out.jitterY);
else if (name == "mv_scale") std::fscanf(file, "%f %f", &out.mvScaleX, &out.mvScaleY);
else if (name == "sharpness") std::fscanf(file, "%f", &out.sharpness);
else if (name == "pre_exposure") std::fscanf(file, "%f", &out.preExposure);
else if (name == "exposure_scale") std::fscanf(file, "%f", &out.exposureScale);
else if (name == "frame_time") std::fscanf(file, "%f", &out.frameTime);
else if (name == "reset") std::fscanf(file, "%d", &out.reset);
else if (name == "has_exposure") std::fscanf(file, "%d", &out.hasExposure);
else if (name == "render") std::fscanf(file, "%u %u", &out.renderWidth, &out.renderHeight);
else if (name == "color_base") std::fscanf(file, "%u %u", &out.colorBaseX, &out.colorBaseY);
else if (name == "depth_base") std::fscanf(file, "%u %u", &out.depthBaseX, &out.depthBaseY);
else if (name == "mv_base") std::fscanf(file, "%u %u", &out.mvBaseX, &out.mvBaseY);
else std::fscanf(file, "%*[^\n]");
if (name == "guides")
{
if (std::fscanf(file, "%u", &guides) != 1)
break;
declared = true;
parsed = 0;
out.guides.clear();
out.guides.resize(guides);
continue;
}
if (name == "guide")
{
unsigned index = 0, format = 0, width = 0, height = 0, texelBytes = 0, converted = 0;
char sources[512], publish[512];
if (!declared || std::fscanf(file, "%u %511s %511s %u %u %u %u %u", &index, sources, publish, &format,
&width, &height, &texelBytes, &converted) != 8 ||
index >= out.guides.size() || texelBytes == 0)
{
std::fprintf(stderr, "replay frame %u: malformed guide descriptor in %sguides.txt\n", frame, prefix);
std::fclose(file);
return false;
}
++parsed;
ReplayGuide& guide = out.guides[index];
guide.sources = split_names(sources);
guide.publish = split_names(publish);
guide.format = static_cast<DXGI_FORMAT>(format);
guide.width = width;
guide.height = height;
guide.texelBytes = texelBytes;
guide.converted = converted != 0;
char path[MAX_PATH];
std::snprintf(path, sizeof(path), "%s\\frame-%06u-guide-%02u.bin", directory, frame, index);
if (!read_file(path, guide.bytes, static_cast<size_t>(texelBytes) * width * height))
{
std::fprintf(stderr, "replay frame %u: guide %u (%s) is missing or mis-sized: %s\n", frame, index,
publish, path);
std::fclose(file);
return false;
}
continue;
}
if (name == "scalar")
{
char parameter[96];
int value = 0;
if (std::fscanf(file, "%95s %d", parameter, &value) != 2)
break;
out.scalars.emplace_back(parameter, value);
continue;
}
if (name == "subrect")
{
char parameter[128];
unsigned value = 0;
if (std::fscanf(file, "%127s %u", parameter, &value) != 2)
break;
out.subrects.emplace_back(parameter, value);
continue;
}
if (name == "matrix")
{
char parameter[128];
int present = 0;
if (std::fscanf(file, "%127s %d", parameter, &present) != 2)
break;
std::array<float, 16> values{};
if (present != 0)
{
int read = 0;
for (; read < 16; ++read)
if (std::fscanf(file, "%f", &values[static_cast<size_t>(read)]) != 1)
break;
if (read != 16)
break;
}
// A matrix the caller did not register this frame is recorded as absent, so the
// replay clears it rather than leaving the previous frame's camera in place.
out.matrices.emplace_back(parameter, present != 0 ? values : std::array<float, 16>{});
out.matrixPresent.emplace_back(parameter, present != 0);
continue;
}
std::fscanf(file, "%*[^\n]");
}
std::fclose(file);
if (!declared)
{
std::fprintf(stderr, "replay frame %u: %sguides.txt lists no guides\n", frame, prefix);
return false;
}
for (size_t index = 0; index < out.guides.size(); ++index)
if (out.guides[index].bytes.size() !=
static_cast<size_t>(out.guides[index].texelBytes) * out.guides[index].width * out.guides[index].height)
{
std::fprintf(stderr, "replay frame %u: guide %zu has no recorded plane\n", frame, index);
return false;
}
// A descriptor that lists fewer guides than it declares is refused rather than partly read:
// the missing ones would otherwise be fed to the denoiser as nothing at all.
if (parsed != guides)
{
std::fprintf(stderr, "replay frame %u: %sguides.txt declares %u guides but %u were read\n", frame, prefix,
guides, parsed);
return false;
}
return true;
}
@@ -778,12 +1050,57 @@ int main(int argc, char** argv)
return 2;
}
const int frames = argc > 3 ? std::atoi(argv[3]) : 6;
const UINT inWidth = argc > 7 ? static_cast<UINT>(std::atoi(argv[4])) : 640;
const UINT inHeight = argc > 7 ? static_cast<UINT>(std::atoi(argv[5])) : 360;
const UINT outWidth = argc > 7 ? static_cast<UINT>(std::atoi(argv[6])) : 1280;
const UINT outHeight = argc > 7 ? static_cast<UINT>(std::atoi(argv[7])) : 720;
const char* replayDir = std::getenv("D4R_HARNESS_REPLAY_DIR");
const uint32_t replayStart = std::getenv("D4R_HARNESS_REPLAY_START") != nullptr
? static_cast<uint32_t>(std::atoi(std::getenv("D4R_HARNESS_REPLAY_START"))) : 1;
// A replay creates the feature the game created: same render and output dimensions, quality
// mode, create flag set, Output.Subrect setting and render preset. These are read before any
// texture is sized or any parameter allocated, so the reference the replay compares against
// is the one the game asked for rather than the harness's own defaults. A capture without
// that state is refused rather than replayed against guessed settings.
ReplayFrame::Creation replayCreation;
if (replayDir != nullptr)
{
ReplayFrame probe;
char paramsPath[MAX_PATH];
std::snprintf(paramsPath, sizeof(paramsPath), "%s\\frame-%06u-params.txt", replayDir, replayStart);
FILE* paramsFile = std::fopen(paramsPath, "r");
if (paramsFile == nullptr)
{
std::fprintf(stderr, "replay frame %u: no params at %s\n", replayStart, paramsPath);
return 1;
}
const bool parsed = read_replay_params(paramsFile, replayStart, probe);
std::fclose(paramsFile);
if (!parsed)
return 1;
if (!probe.creation.recorded)
{
std::fprintf(stderr, "replay frame %u: the capture records no creation state (%s), so the feature "
"would have to be recreated from guessed settings\n",
replayStart, paramsPath);
return 1;
}
replayCreation = probe.creation;
}
// The capture's own dimensions win: its planes are exactly this size, so anything else would
// upload a truncated or padded image rather than the game's.
UINT inWidth = argc > 7 ? static_cast<UINT>(std::atoi(argv[4])) : 640;
UINT inHeight = argc > 7 ? static_cast<UINT>(std::atoi(argv[5])) : 360;
UINT outWidth = argc > 7 ? static_cast<UINT>(std::atoi(argv[6])) : 1280;
UINT outHeight = argc > 7 ? static_cast<UINT>(std::atoi(argv[7])) : 720;
if (replayDir != nullptr)
{
inWidth = replayCreation.width;
inHeight = replayCreation.height;
outWidth = replayCreation.outWidth;
outHeight = replayCreation.outHeight;
std::printf("replay feature: %ux%u -> %ux%u quality=%d flags=0x%x output_subrects=%d preset=%u\n",
inWidth, inHeight, outWidth, outHeight, replayCreation.quality, replayCreation.flags,
replayCreation.outputSubrects, replayCreation.preset);
}
const char* resourceWidthSetting = std::getenv("D4R_HARNESS_RESOURCE_WIDTH");
const UINT resourceWidth = resourceWidthSetting != nullptr
UINT resourceWidth = resourceWidthSetting != nullptr
? static_cast<UINT>(std::atoi(resourceWidthSetting)) : inWidth;
if (resourceWidth < inWidth || resourceWidth > inWidth + 256)
{
@@ -800,7 +1117,6 @@ int main(int argc, char** argv)
const bool qualityScene = std::getenv("D4R_HARNESS_QUALITY_SCENE") != nullptr;
const bool poleScene = std::getenv("D4R_HARNESS_POLE_SCENE") != nullptr;
const bool exposureRGBA32 = std::getenv("D4R_HARNESS_EXPOSURE_RGBA32") != nullptr;
const char* replayDir = std::getenv("D4R_HARNESS_REPLAY_DIR");
const bool rgba8 = std::getenv("D4R_HARNESS_RGBA8") != nullptr;
// D4R_HARNESS_OUTPUT_RGB10A2=1: an R10G10B10A2_UNORM output texture (inputs unchanged), as some games pass
const bool rgb10 = std::getenv("D4R_HARNESS_OUTPUT_RGB10A2") != nullptr;
@@ -814,8 +1130,6 @@ int main(int argc, char** argv)
std::fprintf(stderr, "RGBA8 format probe requires the static synthetic scene\n");
return 2;
}
const uint32_t replayStart = std::getenv("D4R_HARNESS_REPLAY_START") != nullptr
? static_cast<uint32_t>(std::atoi(std::getenv("D4R_HARNESS_REPLAY_START"))) : 1;
const bool temporal = std::getenv("D4R_HARNESS_TEMPORAL") != nullptr || motionScene || jitterScene || qualityScene ||
replayDir != nullptr;
const bool forceReset = std::getenv("D4R_HARNESS_FORCE_RESET") != nullptr;
@@ -955,7 +1269,9 @@ int main(int argc, char** argv)
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)
// A replay feeds the denoiser the guides the game itself bound, so no synthetic plane is
// created here: a pattern-filled guide would stand in for a scene the game never rendered.
if (reconstruction && replayDir == nullptr)
{
for (size_t input = 0; input < guides.size(); ++input)
{
@@ -1162,6 +1478,28 @@ int main(int argc, char** argv)
d4r_ngx_set_uint(parameters, (std::string("RayReconstruction.Hint.Render.Preset.") + mode).c_str(),
static_cast<unsigned int>(std::strtoul(preset, nullptr, 0)));
}
// A replay overrides the environment's guesses with what the game actually created: the
// quality mode, the create flags (HDR, depth inversion, auto exposure), whether Output.Subrect
// origins are read at all, and the render preset. Each of these selects a different model or
// a different reference, so substituting a default here would compare against the wrong one.
if (replayDir != nullptr)
{
d4r_ngx_set_int(parameters, "PerfQualityValue", replayCreation.quality);
d4r_ngx_set_int(parameters, "DLSS.Feature.Create.Flags", replayCreation.flags);
d4r_ngx_set_int(parameters, "DLSS.Enable.Output.Subrects", replayCreation.outputSubrects);
static const char* const kRayReconstructionPresets[6] = {
"RayReconstruction.Hint.Render.Preset.DLAA", "RayReconstruction.Hint.Render.Preset.Quality",
"RayReconstruction.Hint.Render.Preset.Balanced", "RayReconstruction.Hint.Render.Preset.Performance",
"RayReconstruction.Hint.Render.Preset.UltraPerformance",
"RayReconstruction.Hint.Render.Preset.UltraQuality"};
static const char* const kSuperSamplingPresets[6] = {
"DLSS.Hint.Render.Preset.DLAA", "DLSS.Hint.Render.Preset.Quality", "DLSS.Hint.Render.Preset.Balanced",
"DLSS.Hint.Render.Preset.Performance", "DLSS.Hint.Render.Preset.UltraPerformance",
"DLSS.Hint.Render.Preset.UltraQuality"};
const char* const* const presetNames = reconstruction ? kRayReconstructionPresets : kSuperSamplingPresets;
for (int index = 0; index < 6; ++index)
d4r_ngx_set_uint(parameters, presetNames[index], replayCreation.preset);
}
NgxHandle* feature = nullptr;
if (std::getenv("D4R_HARNESS_LIFETIME_TEST") != nullptr && !test_external_lifetime())
return 1;
@@ -1175,7 +1513,9 @@ int main(int argc, char** argv)
d4r_ngx_set_d3d12_resource(parameters, "MotionVectors", motionTexture);
d4r_ngx_set_d3d12_resource(parameters, "Output", outputTexture);
d4r_ngx_set_d3d12_resource(parameters, "ExposureTexture", exposureTexture);
if (reconstruction)
// A replay binds each guide under the names the capture recorded, per frame; the
// synthetic planes and the alpha scenario's own guide belong to the static probe only.
if (reconstruction && replayDir == nullptr)
{
// The SDK's caller-facing names (nvsdk_ngx_defs.h: NVSDK_NGX_Parameter_GBuffer_Normals,
// _Roughness, _DiffuseAlbedo, _SpecularAlbedo), which is what a game registers. The
@@ -1185,7 +1525,9 @@ int main(int argc, char** argv)
for (size_t input = 0; input < guides.size(); ++input)
d4r_ngx_set_d3d12_resource(parameters, names[input], guides[input]);
}
if (alphaScenario)
// DLSSD.Alpha is a guide the capture records in its own right, so the synthetic alpha
// scenario would double-bind that name; it belongs to the static probe only.
if (alphaScenario && replayDir == nullptr)
{
d4r_ngx_set_d3d12_resource(parameters, "DLSSD.Alpha", alphaInput);
d4r_ngx_set_d3d12_resource(parameters, "DLSSD.OutputAlpha", alphaOutput);
@@ -1260,6 +1602,20 @@ int main(int argc, char** argv)
lifecycle_report("SKIP input: discarded recording before submitting the next frame");
}
// Ray Reconstruction replay state. The textures follow the capture's guide identity so a
// game that reuses a surface replays it as the same resource; the names remember what was
// bound last frame, so a guide the game dropped is cleared rather than left in place; the
// matrices outlive the per-frame ReplayFrame, because the parameter object holds their
// pointer until a later frame replaces it.
std::map<std::string, ReplayTexture> replayTextures;
std::vector<std::string> replayGuideNames;
// The exposure texture the capture recorded, at the geometry it recorded: a 1x1 scalar and a
// per-pixel buffer are different textures, and the game chose which.
ID3D12Resource* replayExposureTexture = nullptr;
UINT replayExposureWidth = 0, replayExposureHeight = 0;
bool replayExposureUploaded = false;
std::array<std::array<float, 16>, kReplayMatrices> replayMatrices{};
for (int frame = 1; frame <= frames; ++frame)
{
// Recreate the game's output between completed frames to exercise imported-memory handle reuse.
@@ -1276,7 +1632,9 @@ 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)
// A replay's guides come from the capture; rewriting them with a pattern would
// replace the game's own data with an invented one.
if (reconstruction && replayDir == nullptr && 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.
@@ -1406,8 +1764,148 @@ int main(int argc, char** argv)
d4r_ngx_set_uint(parameters, "DLSS.Input.MV.Subrect.Base.X", replay.mvBaseX);
d4r_ngx_set_uint(parameters, "DLSS.Input.MV.Subrect.Base.Y", replay.mvBaseY);
d4r_ngx_set_int(parameters, "Reset", frame == 1 || replay.reset != 0 ? 1 : 0);
// Games with AutoExposure pass no exposure texture; the synthetic scenes pass 1.0.
d4r_ngx_set_d3d12_resource(parameters, "ExposureTexture", replay.hasExposure ? exposureTexture : nullptr);
// The indicator axes and the Output.Subrect origin, as the game set them: the origin is
// only read at all when the feature was created with DLSS.Enable.Output.Subrects, and
// both are set every frame rather than left at whatever the previous frame wrote.
d4r_ngx_set_int(parameters, "DLSS.Indicator.Invert.X.Axis", replay.invertX);
d4r_ngx_set_int(parameters, "DLSS.Indicator.Invert.Y.Axis", replay.invertY);
d4r_ngx_set_uint(parameters, "DLSS.Output.Subrect.Base.X", replay.outputBaseX);
d4r_ngx_set_uint(parameters, "DLSS.Output.Subrect.Base.Y", replay.outputBaseY);
// The exposure texture the game actually bound this frame, with its own geometry and
// its own contents. A capture claiming one but carrying no plane for it was already
// refused above, so no substituted 1.0 can reach the denoiser here.
if (replay.hasExposure != 0)
{
if (replayExposureTexture == nullptr || replayExposureWidth != replay.exposureWidth ||
replayExposureHeight != replay.exposureHeight)
{
if (replayExposureTexture != nullptr)
{
replayExposureTexture->Release();
replayExposureTexture = nullptr;
}
replayExposureTexture = create_texture(replay.exposureWidth, replay.exposureHeight,
DXGI_FORMAT_R32_FLOAT, D3D12_RESOURCE_FLAG_NONE);
if (replayExposureTexture == nullptr)
{
std::fprintf(stderr, "replay frame %u: the %ux%u exposure texture could not be created\n",
replayStart + static_cast<uint32_t>(frame - 1), replay.exposureWidth,
replay.exposureHeight);
return 1;
}
replayExposureWidth = replay.exposureWidth;
replayExposureHeight = replay.exposureHeight;
replayExposureUploaded = false;
}
if (!replayExposureUploaded)
{
upload(replayExposureTexture, replay.exposure.data(), replayExposureWidth * 4, srv);
replayExposureUploaded = true;
}
else
{
update_texture(replayExposureTexture, replay.exposure.data(), replayExposureWidth * 4, srv);
}
d4r_ngx_set_d3d12_resource(parameters, "ExposureTexture", replayExposureTexture);
}
else
{
// A game with AutoExposure binds no exposure texture; nothing is left bound.
d4r_ngx_set_d3d12_resource(parameters, "ExposureTexture", nullptr);
}
// Ray Reconstruction: the guides the game bound this frame, in its own formats, plus
// the settings, origins and camera matrices it gave them with. A capture without
// them is refused rather than filled with synthetic planes - a denoiser handed
// invented guides would produce a plausible image that matches nothing the game
// rendered, which is the one outcome this replay exists to rule out.
if (reconstruction)
{
const uint32_t replayFrame = replayStart + static_cast<uint32_t>(frame - 1);
if (!load_replay_guides(replayDir, replayFrame, replay))
return 1;
std::vector<std::string> bound;
for (size_t guide = 0; guide < replay.guides.size(); ++guide)
{
const ReplayGuide& captured = replay.guides[guide];
if (captured.sources.empty() || captured.publish.empty() || captured.texelBytes == 0 ||
captured.width == 0 || captured.height == 0)
{
std::fprintf(stderr, "replay frame %u: guide %zu has an incomplete descriptor\n",
replayFrame, guide);
return 1;
}
// Keyed on the first name the caller registered it under, so the texture the
// game kept across frames is created once and only its contents are replaced.
ReplayTexture& texture = replayTextures[captured.sources.front()];
if (texture.resource == nullptr || texture.format != captured.format ||
texture.width != captured.width || texture.height != captured.height)
{
if (texture.resource != nullptr)
{
texture.resource->Release();
texture = {};
}
texture.resource = create_texture(captured.width, captured.height, captured.format,
D3D12_RESOURCE_FLAG_NONE);
if (texture.resource == nullptr)
{
std::fprintf(stderr, "replay frame %u: the %ux%u fmt=0x%x guide could not be created\n",
replayFrame, captured.width, captured.height,
static_cast<unsigned>(captured.format));
return 1;
}
texture.format = captured.format;
texture.width = captured.width;
texture.height = captured.height;
texture.uploaded = false;
}
const UINT rowBytes = captured.width * captured.texelBytes;
if (!texture.uploaded)
{
upload(texture.resource, captured.bytes.data(), rowBytes, srv);
texture.uploaded = true;
}
else
{
update_texture(texture.resource, captured.bytes.data(), rowBytes, srv);
}
for (const std::string& source : captured.sources)
{
d4r_ngx_set_d3d12_resource(parameters, source.c_str(), texture.resource);
bound.push_back(source);
}
if (frame == 1)
std::printf("replay guide %zu: %ux%u fmt=0x%x, %s as %s\n", guide, captured.width,
captured.height, static_cast<unsigned>(captured.format),
join_names(captured.sources).c_str(), join_names(captured.publish).c_str());
}
// A guide the game stopped registering is cleared: the parameter object outlives
// the frame, so leaving its texture bound would hand the denoiser a guide the game
// had already dropped, exactly as the shim clears the names it published before.
for (const std::string& name : replayGuideNames)
if (std::find(bound.begin(), bound.end(), name) == bound.end())
d4r_ngx_set_d3d12_resource(parameters, name.c_str(), nullptr);
replayGuideNames = bound;
for (const auto& [name, value] : replay.scalars)
d4r_ngx_set_int(parameters, name.c_str(), value);
for (const auto& [name, value] : replay.subrects)
d4r_ngx_set_uint(parameters, name.c_str(), value);
if (replay.matrices.size() != replay.matrixPresent.size() || replay.matrices.size() > kReplayMatrices)
{
std::fprintf(stderr, "replay frame %u: %zu camera matrices recorded, which this replay cannot hold\n",
replayFrame, replay.matrices.size());
return 1;
}
// Copied into storage that lives for the whole run, because the parameter object
// keeps the pointer until the next frame replaces it.
for (size_t index = 0; index < replay.matrices.size(); ++index)
{
replayMatrices[index] = replay.matrices[index].second;
d4r_ngx_set_void(parameters, replay.matrices[index].first.c_str(),
replay.matrixPresent[index].second ? replayMatrices[index].data() : nullptr);
}
}
}
else if (temporal)
d4r_ngx_set_int(parameters, "Reset", forceReset || frame == 1 ? 1 : 0);
+216 -24
View File
@@ -1913,7 +1913,13 @@ enum class AuxLayout
Converted // the bytes must be unpacked into floats first (see convert_aux_row)
};
// The CUDA array a denoiser input uses.
// The texel size an array of `arrayFormat` and `channels` occupies per texel.
static size_t aux_texel_bytes(uint32_t arrayFormat, uint32_t channels);
// The CUDA array a denoiser input uses, plus the texel size the caller's own resource
// occupies. The two differ for every converted format (B8G8R8A8_UNORM is four bytes to
// D3D12 and sixteen to the float array), and a capture records the caller's bytes, so a
// replay runs them through the same conversion again.
//
// Only formats that are byte-for-byte correct go down the verbatim path, and that is a
// shorter list than it looks. A CUDA array stores element_size * channels bytes per texel
@@ -1929,7 +1935,9 @@ enum class AuxLayout
//
// Everything that is not in this table is converted to a float array instead, which is
// correct for all of those cases at the cost of a per-row unpack.
static bool aux_array_layout(DXGI_FORMAT format, uint32_t& arrayFormat, uint32_t& channels, AuxLayout& how)
static bool aux_array_layout(DXGI_FORMAT format, uint32_t& arrayFormat, uint32_t& channels, AuxLayout& how,
uint32_t& texelBytes)
{
struct Entry
{
@@ -1938,7 +1946,8 @@ static bool aux_array_layout(DXGI_FORMAT format, uint32_t& arrayFormat, uint32_t
uint32_t channels;
};
// UNORM and SNORM map onto signed/unsigned integer arrays of the same width: sampling
// normalizes them, which is exactly how a D3D12 shader reads the resource.
// normalizes them, which is exactly how a D3D12 shader reads the resource. Their texel
// size is the array's own, so it is derived rather than listed twice.
static const Entry entries[] = {
{DXGI_FORMAT_R8_UNORM, CUDA_FORMAT_UNSIGNED_INT8, 1},
{DXGI_FORMAT_R8_SNORM, CUDA_FORMAT_SIGNED_INT8, 1},
@@ -1965,28 +1974,31 @@ static bool aux_array_layout(DXGI_FORMAT format, uint32_t& arrayFormat, uint32_t
arrayFormat = entry.arrayFormat;
channels = entry.channels;
how = AuxLayout::Verbatim;
texelBytes = static_cast<uint32_t>(aux_texel_bytes(entry.arrayFormat, entry.channels));
return true;
}
// Converted formats all end up as float arrays, so a shader read of the denoiser's
// texture returns the same numbers a D3D12 read of the caller's resource would.
// texture returns the same numbers a D3D12 read of the caller's resource would. Their
// texel size is the caller's own, which is what a capture records.
struct Converted
{
DXGI_FORMAT format;
uint32_t channels;
uint32_t texelBytes;
};
static const Converted converted[] = {
{DXGI_FORMAT_R8_UINT, 1},
{DXGI_FORMAT_R8G8_UINT, 2},
{DXGI_FORMAT_R8G8B8A8_UINT, 4},
{DXGI_FORMAT_R16_UINT, 1},
{DXGI_FORMAT_R32_UINT, 1},
{DXGI_FORMAT_R32_SINT, 1},
{DXGI_FORMAT_R32G32_UINT, 2},
{DXGI_FORMAT_R32G32_SINT, 2},
{DXGI_FORMAT_R32G32B32A32_UINT, 4},
{DXGI_FORMAT_B8G8R8A8_UNORM, 4},
{DXGI_FORMAT_R10G10B10A2_UNORM, 4},
{DXGI_FORMAT_R11G11B10_FLOAT, 4},
{DXGI_FORMAT_R8_UINT, 1, 1},
{DXGI_FORMAT_R8G8_UINT, 2, 2},
{DXGI_FORMAT_R8G8B8A8_UINT, 4, 4},
{DXGI_FORMAT_R16_UINT, 1, 2},
{DXGI_FORMAT_R32_UINT, 1, 4},
{DXGI_FORMAT_R32_SINT, 1, 4},
{DXGI_FORMAT_R32G32_UINT, 2, 8},
{DXGI_FORMAT_R32G32_SINT, 2, 8},
{DXGI_FORMAT_R32G32B32A32_UINT, 4, 16},
{DXGI_FORMAT_B8G8R8A8_UNORM, 4, 4},
{DXGI_FORMAT_R10G10B10A2_UNORM, 4, 4},
{DXGI_FORMAT_R11G11B10_FLOAT, 4, 4},
};
for (const Converted& entry : converted)
if (entry.format == format)
@@ -1994,6 +2006,7 @@ static bool aux_array_layout(DXGI_FORMAT format, uint32_t& arrayFormat, uint32_t
arrayFormat = CUDA_FORMAT_FLOAT;
channels = entry.channels;
how = AuxLayout::Converted;
texelBytes = entry.texelBytes;
return true;
}
return false;
@@ -2246,6 +2259,17 @@ struct FrameParams
unsigned int colorBaseX = 0, colorBaseY = 0, depthBaseX = 0, depthBaseY = 0;
unsigned int mvBaseX = 0, mvBaseY = 0, outputBaseX = 0, outputBaseY = 0;
bool hasExposure = false;
// The exposure texture's own geometry. A game may pass a per-pixel exposure buffer rather
// than the 1x1 scalar, and a capture that recorded a plane without its size could not say
// which it was.
unsigned int exposureWidth = 0, exposureHeight = 0;
// How this feature was created, copied off it so a capture records what the game asked for
// rather than what a replay would otherwise have to guess: the model, the render and output
// dimensions, the quality mode, the HDR/depth/exposure create flags, whether Output.Subrect
// origins are read, and the render preset in force.
unsigned int createWidth = 0, createHeight = 0, createOutWidth = 0, createOutHeight = 0;
int createQuality = 0, createFlags = 0, createSubrects = 0;
unsigned int createPreset = 0;
bool vram = false; // inputs and output stay in VRAM (VRAM interop)
bool split = false; // this frame presents its own result (split frames)
unsigned motionDilation = 0; // actually applied, worker only
@@ -2320,6 +2344,7 @@ struct AuxSurface
UINT width = 0, height = 0;
uint32_t arrayFormat = 0, channels = 0;
AuxLayout how = AuxLayout::Verbatim; // whether the bytes reach the array as they are
uint32_t texelBytes = 0; // the caller's own texel size, for a capture
uint32_t lastSeen = 0; // frame the caller last registered this resource
unsigned int holds = 0; // queued frames still naming this surface
bool dropping = false; // no longer registered; freed once holds reaches zero
@@ -2338,7 +2363,8 @@ struct AuxSurface
struct AuxBinding
{
AuxSurface* surface = nullptr;
std::vector<const char*> names;
std::vector<const char*> names; // what the denoiser reads it as
std::vector<const char*> sources; // what the caller registered it under
uint32_t frame = 0;
};
@@ -2435,6 +2461,9 @@ struct Feature
std::array<unsigned int, 6> creationPresets{};
bool cudaCreationDeferred = false;
unsigned int preset = 0;
// DLSS.Enable.Output.Subrects as the caller set it at creation: it decides whether the
// Output.Subrect origins are read at all, so a replay must create the feature the same way.
int outputSubrects = 0;
CudaEvent profileStart = nullptr, profileEnd = nullptr;
CudaEvent outputReadyEvent = nullptr;
// output wait pacing (D4R_SHIM_OUTPUT_POLL_US): smoothed time from the event record to its completion, kept
@@ -4092,20 +4121,26 @@ static void dump_input_planes(const HostPlane (&planes)[4], uint32_t frame, cons
if (written > 0 && written < MAX_PATH)
write_capture_file(frame, names[index], path, bytes, host.size());
}
// The evaluation parameters, so the harness can replay the sequence.
// The evaluation parameters, so the harness can replay the sequence. The exposure plane's
// own geometry and the feature's creation state are recorded here too: a capture that kept a
// plane without its size, or a sequence without the model it ran under, would leave the
// replay guessing at both.
char path[MAX_PATH];
const int written = std::snprintf(path, MAX_PATH, "%s\\frame-%06u-params.txt", directory, frame);
if (written <= 0 || written >= MAX_PATH)
return;
char text[1024];
char text[1536];
const int length = std::snprintf(
text, sizeof(text),
"jitter %.9g %.9g\nmv_scale %.9g %.9g\nsharpness %.9g\npre_exposure %.9g\n"
"exposure_scale %.9g\nframe_time %.9g\nreset %d\ninvert %d %d\nrender %u %u\n"
"color_base %u %u\ndepth_base %u %u\nmv_base %u %u\noutput_base %u %u\nhas_exposure %d\n",
"color_base %u %u\ndepth_base %u %u\nmv_base %u %u\noutput_base %u %u\nhas_exposure %d\n"
"exposure_size %u %u\ncreate %u %u %u %u %d %d %d %u\n",
p.jitterX, p.jitterY, p.mvScaleX, p.mvScaleY, p.sharpness, p.preExposure, p.exposureScale, p.frameTime,
p.reset, p.invertX, p.invertY, p.renderWidth, p.renderHeight, p.colorBaseX, p.colorBaseY, p.depthBaseX,
p.depthBaseY, p.mvBaseX, p.mvBaseY, p.outputBaseX, p.outputBaseY, p.hasExposure ? 1 : 0);
p.depthBaseY, p.mvBaseX, p.mvBaseY, p.outputBaseX, p.outputBaseY, p.hasExposure ? 1 : 0, p.exposureWidth,
p.exposureHeight, p.createWidth, p.createHeight, p.createOutWidth, p.createOutHeight, p.createQuality,
p.createFlags, p.createSubrects, p.createPreset);
if (length > 0)
write_capture_file(frame, "params", path, text, static_cast<size_t>(length));
}
@@ -5050,6 +5085,11 @@ static bool gather_aux_inputs(Feature& feature, ID3D12GraphicsCommandList* list,
{
InputSlot& slot = feature.inputs[slotIndex];
std::lock_guard<std::mutex> lock(feature.auxMutex);
// A frame the input dump selected records this frame's guides too, so a replay of an RR
// sequence feeds the denoiser the game's own guides rather than the harness's synthetic
// ones - without which the replayed image cannot be compared with the game's.
char captureDirectory[MAX_PATH];
const bool capture = input_dump_directory(frame, captureDirectory);
std::vector<AuxBinding> bindings;
for (const AuxInput& input : kAuxInputs)
{
@@ -5093,15 +5133,20 @@ static bool gather_aux_inputs(Feature& feature, ID3D12GraphicsCommandList* list,
bound = bound || strcmp(name, input.publish) == 0;
if (!bound)
binding->names.push_back(input.publish);
bool sourceBound = false;
for (const char* name : binding->sources)
sourceBound = sourceBound || strcmp(name, input.source) == 0;
if (!sourceBound)
binding->sources.push_back(input.source);
if (!firstBinding)
continue; // aliases share one captured plane and one upload
D3D12_RESOURCE_DESC desc;
resource->GetDesc(&desc);
uint32_t arrayFormat = 0, channels = 0;
uint32_t arrayFormat = 0, channels = 0, texelBytes = 0;
AuxLayout how = AuxLayout::Verbatim;
if (desc.DepthOrArraySize != 1 || desc.Width > 0xFFFFFFFFull ||
!aux_array_layout(desc.Format, arrayFormat, channels, how))
!aux_array_layout(desc.Format, arrayFormat, channels, how, texelBytes))
{
logf("Ray Reconstruction: denoiser input %s is %llux%u array=%u fmt=0x%x, which has no single-plane "
"CUDA layout; this frame's evaluation is refused rather than run without it",
@@ -5112,6 +5157,7 @@ static bool gather_aux_inputs(Feature& feature, ID3D12GraphicsCommandList* list,
surface->arrayFormat = arrayFormat;
surface->channels = channels;
surface->how = how;
surface->texelBytes = texelBytes;
surface->width = static_cast<UINT>(desc.Width);
surface->height = static_cast<UINT>(desc.Height);
VramCopy gpuCopy;
@@ -5121,7 +5167,10 @@ static bool gather_aux_inputs(Feature& feature, ID3D12GraphicsCommandList* list,
if (!record_aux_vram(list, *surface, slotIndex, gpuCopy, inputState))
return false;
}
if (!surface->gpu[slotIndex] || env_uint("D4R_SHIM_VRAM_VERIFY", 0) != 0)
// A captured frame reads its guides back even with VRAM interop on: the capture
// records the caller's own bytes in the caller's own format, which is what a replay
// has to feed back for the same conversion to run over it.
if (!surface->gpu[slotIndex] || env_uint("D4R_SHIM_VRAM_VERIFY", 0) != 0 || capture)
{
if (!ensure_staging(surface->staging[slotIndex], resource, D3D12_HEAP_TYPE_READBACK))
return false;
@@ -5292,6 +5341,125 @@ static bool upload_aux_inputs(Feature& feature, int slotIndex, uint32_t frame)
return true;
}
// Worker. Writes this frame's Ray Reconstruction guides next to the canonical input planes:
// the caller's own bytes in the caller's own format, the descriptor each was read with, and
// the per-frame settings, subrect origins and camera matrices the denoiser was given. A replay
// rebuilds the textures from those descriptors and uploads the recorded bytes, so the same
// conversion runs over the same data and the replayed image is comparable with the game's -
// which synthetic guides can never be.
//
// A guide whose bytes are not readable fails the whole capture: a descriptor without its
// plane, or a plane the replay silently skipped, is an image the denoiser would produce from
// fewer guides than the game gave it, and nothing downstream could tell.
static void dump_aux_guides(Feature& feature, int slotIndex, uint32_t frame, const FrameParams& p)
{
char directory[MAX_PATH];
if (!input_dump_directory(frame, directory))
return;
const InputSlot& slot = feature.inputs[slotIndex];
for (const AuxBinding& binding : slot.auxBindings)
if (binding.surface->staging[slotIndex].mapped == nullptr)
{
logf("frame %u: Ray Reconstruction guide %s is not readable, so this frame's capture is "
"incomplete and is not written",
frame, join_aux_names(binding.names).c_str());
return;
}
std::string text = "guides " + std::to_string(slot.auxBindings.size()) + "\n";
char path[MAX_PATH];
for (size_t index = 0; index < slot.auxBindings.size(); ++index)
{
const AuxBinding& binding = slot.auxBindings[index];
const AuxSurface& surface = *binding.surface;
// Both spellings are recorded: the names the caller registered the texture under, and
// the names the denoiser reads it as, which for the two albedos differ.
std::string sources, names;
for (const char* name : binding.sources)
sources += sources.empty() ? name : std::string(",") + name;
for (const char* name : binding.names)
names += names.empty() ? name : std::string(",") + name;
const int written =
std::snprintf(path, MAX_PATH, "%s\\frame-%06u-guide-%02zu.bin", directory, frame, index);
if (written <= 0 || written >= MAX_PATH)
{
logf("frame %u: the path for Ray Reconstruction guide %s does not fit; this frame's capture is "
"incomplete and is not written",
frame, join_aux_names(binding.names).c_str());
return;
}
const Staging& staging = surface.staging[slotIndex];
const size_t rowBytes = static_cast<size_t>(surface.texelBytes) * surface.width;
const size_t bytes = rowBytes * surface.height;
uint8_t* const pinned = capture_pinned(path, bytes);
std::vector<uint8_t> fallback;
if (pinned == nullptr)
fallback.resize(bytes);
uint8_t* const destination = pinned != nullptr ? pinned : fallback.data();
for (UINT row = 0; row < surface.height; ++row)
std::memcpy(destination + static_cast<size_t>(row) * rowBytes,
staging.mapped + staging.layout.Offset +
static_cast<size_t>(row) * staging.layout.Footprint.RowPitch,
rowBytes);
if (pinned == nullptr)
write_capture_file(frame, "Ray Reconstruction guide", path, destination, bytes);
char line[512];
const int length = std::snprintf(line, sizeof(line),
"guide %zu %s %s %u %u %u %u %d\n", index, sources.c_str(), names.c_str(),
static_cast<unsigned>(surface.format), surface.width, surface.height,
surface.texelBytes, surface.how == AuxLayout::Verbatim ? 0 : 1);
if (length <= 0)
return;
text += line;
}
// The scalar settings, the Subrect origins and the camera matrices, under the very
// parameter names the denoiser reads: a replay sets them by name, so a table that drifted
// here and there could not misroute them.
for (size_t index = 0; index < sizeof(kAuxScalars) / sizeof(kAuxScalars[0]); ++index)
{
char line[256];
const int length = std::snprintf(line, sizeof(line), "scalar %s %d\n", kAuxScalars[index].name,
p.auxScalar[index]);
if (length <= 0)
return;
text += line;
}
for (size_t index = 0; index < sizeof(kAuxSubrectNames) / sizeof(kAuxSubrectNames[0]); ++index)
{
for (int axis = 0; axis < 2; ++axis)
{
char line[256];
const int length = std::snprintf(line, sizeof(line), "subrect %s.Subrect.Base.%c %u\n",
kAuxSubrectNames[index], axis == 0 ? 'X' : 'Y',
p.auxSubrect[index][axis]);
if (length <= 0)
return;
text += line;
}
}
for (size_t index = 0; index < sizeof(kAuxMatrices) / sizeof(kAuxMatrices[0]); ++index)
{
char line[1024];
int length = std::snprintf(line, sizeof(line), "matrix %s %d", kAuxMatrices[index],
p.auxMatrixSet[index] ? 1 : 0);
if (length <= 0)
return;
for (size_t element = 0; element < 16 && p.auxMatrixSet[index]; ++element)
{
const int written = std::snprintf(line + length, sizeof(line) - static_cast<size_t>(length), " %.9g",
static_cast<double>(p.auxMatrix[index][element]));
if (written <= 0 || static_cast<size_t>(written) >= sizeof(line) - static_cast<size_t>(length))
return;
length += written;
}
text += line;
text += "\n";
}
const int written = std::snprintf(path, MAX_PATH, "%s\\frame-%06u-guides.txt", directory, frame);
if (written <= 0 || written >= MAX_PATH)
return;
write_capture_file(frame, "Ray Reconstruction guide parameters", path, text.data(), text.size());
}
// Worker. Points the denoiser's parameters at this frame's texture objects and forwards the
// settings and camera matrices the caller registered for this frame. Every name this file
// ever published is cleared first: the parameter object outlives the frame, so a name left
@@ -6141,6 +6309,8 @@ static void run_evaluation(Feature* feature, int slotIndex, uint32_t frame, Fram
{
std::lock_guard<std::mutex> lock(feature->auxMutex);
auxUploaded = upload_aux_inputs(*feature, slotIndex, frame);
if (auxUploaded)
dump_aux_guides(*feature, slotIndex, frame, params);
}
if (!auxUploaded)
{
@@ -7093,6 +7263,7 @@ static NgxResult create_feature(unsigned int featureId, void* parameters, NgxHan
feature->quality = quality;
feature->flags = flags;
const int subrects = get_int_or(parameters, "DLSS.Enable.Output.Subrects", 0);
feature->outputSubrects = subrects;
// The render preset is chosen by two independent knobs. D4R_DLSS_PRESET forces the
// Super Sampling model (NVSDK_NGX_DLSS_Hint_Render_Preset, e.g. 5 = E) and touches only
// Super Sampling features; D4R_RR_PRESET forces the Ray Reconstruction model
@@ -8393,12 +8564,21 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
outputDesc.Width, outputDesc.Height, outputDesc.Format, outputSupported ? "" : " (unsupported)");
return NGX_FAIL_UNSUPPORTED_FORMAT;
}
unsigned int exposureWidth = 0, exposureHeight = 0;
if (exposure != nullptr)
{
D3D12_RESOURCE_DESC exposureDesc;
exposure->GetDesc(&exposureDesc);
// Recorded before the format test: a capture that kept the plane but not its geometry
// could not say whether the game passed a scalar or a per-pixel exposure buffer.
exposureWidth = static_cast<unsigned int>(exposureDesc.Width);
exposureHeight = static_cast<unsigned int>(exposureDesc.Height);
if (!supported_input(Plane::Exposure, exposureDesc.Format))
{
exposure = nullptr;
exposureWidth = 0;
exposureHeight = 0;
}
}
FrameParams p;
@@ -8429,6 +8609,18 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
p.outputBaseX = get_uint_or(parameters, "DLSS.Output.Subrect.Base.X", 0);
p.outputBaseY = get_uint_or(parameters, "DLSS.Output.Subrect.Base.Y", 0);
p.hasExposure = exposure != nullptr;
p.exposureWidth = exposureWidth;
p.exposureHeight = exposureHeight;
// Creation state, so a capture records the model and geometry the game asked for rather
// than what a replay would otherwise have to guess.
p.createWidth = feature->width;
p.createHeight = feature->height;
p.createOutWidth = feature->outWidth;
p.createOutHeight = feature->outHeight;
p.createQuality = feature->quality;
p.createFlags = feature->flags;
p.createSubrects = feature->outputSubrects;
p.createPreset = feature->preset;
// Ray Reconstruction: the denoiser's own per-frame settings and camera matrices are read
// here, on the game thread, while the caller's parameter object is certainly alive, and