Advance native Windows RR integration, kernel tooling and local packaging
Implement native NGX feature-13 RR through a LUID-matched D3D12/Vulkan shared-memory and timeline-fence bridge. Add owned parameter snapshots, GPU resource lifetime handling, inherited/enhanced barriers, optional guides, sampled input formats and depth-plane support with stencil preservation. Separate RR initialization from the SR CUDA runtime and expose explicit backend/model selection through player configuration. Add synthetic bridge, snapshot and NGX lifecycle harnesses covering camera/history/reset, parameter mutation, delayed submission, early release, capability gating and unsupported-input rejection. Add exact gfx11 D/E model generation, verified caches, versioned local deployment with configuration backups, and isolated portable model tools with pinned Python, NumPy and glslang dependencies. Extend Windows K benchmarking and private native-cache selection. Fix LLVM 22 emitter/helper compatibility and preserve exact-output validation for local output-kernel optimization. Add native FSR RR provider diagnostics, local development package refresh tooling, installer/checker updates, FidelityFX headers and Windows parity documentation. Keep NVIDIA binaries, derived models and private optimized kernels out of distributable packages. Native RR evidence remains synthetic; game validation, FSR RR dispatch, installer integration and Linux performance parity remain incomplete.
This commit is contained in:
1 parent
3345fffcd3
commit
247fe1092d
60 files changed
+7168
-66
No files matched your search
@@ -0,0 +1,279 @@
|
||||
// AMD FSR Ray Regeneration (FidelityFX denoiser 1.2) runtime access, shared by the NGX shim's
|
||||
// FSR Ray Reconstruction backend and tools/fsr_rr_probe.cpp.
|
||||
//
|
||||
// The denoiser is AMD's signed amd_fidelityfx_denoiser_dx12.dll, loaded from an explicit path
|
||||
// (never from a search path, never patched) and driven through its five FFX API exports. It
|
||||
// records its compute work into the caller's D3D12 command list; nothing here involves CUDA,
|
||||
// ZLUDA or NVIDIA's denoiser. Struct layouts come from AMD's MIT-licensed headers in
|
||||
// third_party/fidelityfx (SDK 2.3.0 / FSR Ray Regeneration 1.2.0).
|
||||
//
|
||||
// Each stage reports separately (see FsrRrStage): a missing DLL, a missing export, a provider
|
||||
// query that returns no denoiser for this device, a failed context creation (where shader
|
||||
// pipelines are built) and a failed dispatch are different failures and are never folded together.
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d12.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../third_party/fidelityfx/api/include/ffx_api.h"
|
||||
#include "../third_party/fidelityfx/api/include/ffx_api_types.h"
|
||||
#include "../third_party/fidelityfx/denoisers/include/ffx_denoiser.h"
|
||||
|
||||
// ffx_api_dx12.h's backend descriptor, declared here because that header's inline helpers call
|
||||
// ID3D12Resource::GetDesc() by value, which does not compile with WIDL_EXPLICIT_AGGREGATE_RETURNS.
|
||||
#ifndef FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_DX12
|
||||
#define FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_DX12 FFX_API_MAKE_BACKEND_SUB_ID(FFX_API_BACKEND_ID_DX12, 0x02)
|
||||
struct ffxCreateBackendDX12Desc
|
||||
{
|
||||
ffxCreateContextDescHeader header;
|
||||
ID3D12Device* device;
|
||||
};
|
||||
#endif
|
||||
static_assert(sizeof(ffxCreateBackendDX12Desc) == 24, "ffx_api_dx12.h layout");
|
||||
// The 1.2 descriptors as the signed runtime reads them (x64, natural alignment, same as MSVC).
|
||||
static_assert(sizeof(FfxApiResource) == 48 && sizeof(FfxApiDenoiserSignal) == 104, "ffx_api_types.h layout");
|
||||
static_assert(sizeof(ffxCreateContextDescDenoiser) == 40, "ffx_denoiser.h 1.2 create layout");
|
||||
static_assert(sizeof(ffxDispatchDescDenoiser) == 448 && offsetof(ffxDispatchDescDenoiser, view) == 296 &&
|
||||
offsetof(ffxDispatchDescDenoiser, renderSize) == 432,
|
||||
"ffx_denoiser.h 1.2 dispatch layout");
|
||||
|
||||
enum class FsrRrStage
|
||||
{
|
||||
NotLoaded, // nothing tried yet
|
||||
DllMissing, // no file at the configured path
|
||||
DllLoadFailed, // LoadLibrary failed (dependencies, architecture)
|
||||
ExportsMissing, // not an FFX API DLL
|
||||
NoProvider, // the runtime returned no denoiser version for this device
|
||||
Ready, // a provider was enumerated for this device; contexts may still fail
|
||||
};
|
||||
|
||||
inline const char* fsr_rr_stage_name(FsrRrStage stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case FsrRrStage::NotLoaded: return "not loaded";
|
||||
case FsrRrStage::DllMissing: return "denoiser DLL missing";
|
||||
case FsrRrStage::DllLoadFailed: return "denoiser DLL failed to load";
|
||||
case FsrRrStage::ExportsMissing: return "denoiser DLL lacks the FFX API exports";
|
||||
case FsrRrStage::NoProvider: return "no FSR Ray Regeneration provider for this device";
|
||||
case FsrRrStage::Ready: return "provider available";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
inline const char* fsr_rr_return_name(ffxReturnCode_t code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case FFX_API_RETURN_OK: return "OK";
|
||||
case FFX_API_RETURN_ERROR: return "ERROR";
|
||||
case FFX_API_RETURN_ERROR_UNKNOWN_DESCTYPE: return "ERROR_UNKNOWN_DESCTYPE";
|
||||
case FFX_API_RETURN_ERROR_RUNTIME_ERROR: return "ERROR_RUNTIME_ERROR";
|
||||
case FFX_API_RETURN_NO_PROVIDER: return "NO_PROVIDER";
|
||||
case FFX_API_RETURN_ERROR_MEMORY: return "ERROR_MEMORY";
|
||||
case FFX_API_RETURN_ERROR_PARAMETER: return "ERROR_PARAMETER";
|
||||
case FFX_API_RETURN_PROVIDER_NO_SUPPORT_NEW_DESCTYPE: return "PROVIDER_NO_SUPPORT_NEW_DESCTYPE";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// The loaded runtime and the provider the device was offered. One per process: the DLL keeps
|
||||
// global state (its debug callback), so it is loaded once and never unloaded while contexts exist.
|
||||
struct FsrRrRuntime
|
||||
{
|
||||
HMODULE module = nullptr;
|
||||
std::wstring path; // as configured
|
||||
std::wstring mappedPath; // as the loader mapped it
|
||||
PfnFfxCreateContext createContext = nullptr;
|
||||
PfnFfxDestroyContext destroyContext = nullptr;
|
||||
PfnFfxConfigure configure = nullptr;
|
||||
PfnFfxQuery query = nullptr;
|
||||
PfnFfxDispatch dispatch = nullptr;
|
||||
FsrRrStage stage = FsrRrStage::NotLoaded;
|
||||
ffxReturnCode_t queryResult = FFX_API_RETURN_OK;
|
||||
std::vector<uint64_t> versionIds;
|
||||
std::vector<std::string> versionNames;
|
||||
uint64_t providerId = 0; // the provider contexts are created with (the first enumerated)
|
||||
std::string providerName;
|
||||
|
||||
// Loads `dll` with its own directory searched for dependencies. Does not query providers.
|
||||
bool load(const std::wstring& dll)
|
||||
{
|
||||
path = dll;
|
||||
if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
stage = FsrRrStage::DllMissing;
|
||||
return false;
|
||||
}
|
||||
module = LoadLibraryExW(dll.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
|
||||
if (module == nullptr)
|
||||
{
|
||||
stage = FsrRrStage::DllLoadFailed;
|
||||
return false;
|
||||
}
|
||||
wchar_t mapped[MAX_PATH] = {};
|
||||
GetModuleFileNameW(module, mapped, MAX_PATH);
|
||||
mappedPath = mapped;
|
||||
createContext = reinterpret_cast<PfnFfxCreateContext>(reinterpret_cast<void*>(GetProcAddress(module, "ffxCreateContext")));
|
||||
destroyContext = reinterpret_cast<PfnFfxDestroyContext>(reinterpret_cast<void*>(GetProcAddress(module, "ffxDestroyContext")));
|
||||
configure = reinterpret_cast<PfnFfxConfigure>(reinterpret_cast<void*>(GetProcAddress(module, "ffxConfigure")));
|
||||
query = reinterpret_cast<PfnFfxQuery>(reinterpret_cast<void*>(GetProcAddress(module, "ffxQuery")));
|
||||
dispatch = reinterpret_cast<PfnFfxDispatch>(reinterpret_cast<void*>(GetProcAddress(module, "ffxDispatch")));
|
||||
if (!createContext || !destroyContext || !configure || !query || !dispatch)
|
||||
{
|
||||
stage = FsrRrStage::ExportsMissing;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Routes the runtime's own error/warning messages to `callback` (may be called on any thread).
|
||||
ffxReturnCode_t set_message_callback(ffxApiMessage callback, uint32_t level)
|
||||
{
|
||||
ffxConfigureDescGlobalDebug desc = {};
|
||||
desc.header.type = FFX_API_CONFIGURE_DESC_TYPE_GLOBALDEBUG;
|
||||
desc.effectId = FFX_API_EFFECT_ID_DENOISER;
|
||||
desc.fpMessage = callback;
|
||||
desc.debugLevel = level;
|
||||
return configure(nullptr, &desc.header);
|
||||
}
|
||||
|
||||
// The two-call version enumeration AMD's sample uses, against the real device. Zero
|
||||
// versions is the "no provider for this device" outcome, distinct from a query error.
|
||||
bool query_providers(ID3D12Device* device)
|
||||
{
|
||||
versionIds.clear();
|
||||
versionNames.clear();
|
||||
uint64_t count = 0;
|
||||
ffxQueryDescGetVersions desc = {};
|
||||
desc.header.type = FFX_API_QUERY_DESC_TYPE_GET_VERSIONS;
|
||||
desc.createDescType = FFX_API_EFFECT_ID_DENOISER;
|
||||
desc.device = device;
|
||||
desc.outputCount = &count;
|
||||
queryResult = query(nullptr, &desc.header);
|
||||
if (queryResult != FFX_API_RETURN_OK || count == 0)
|
||||
{
|
||||
stage = FsrRrStage::NoProvider;
|
||||
return false;
|
||||
}
|
||||
versionIds.resize(count);
|
||||
std::vector<const char*> names(count, nullptr);
|
||||
desc.versionIds = versionIds.data();
|
||||
desc.versionNames = names.data();
|
||||
queryResult = query(nullptr, &desc.header);
|
||||
if (queryResult != FFX_API_RETURN_OK || count == 0)
|
||||
{
|
||||
stage = FsrRrStage::NoProvider;
|
||||
return false;
|
||||
}
|
||||
versionIds.resize(count);
|
||||
for (uint64_t index = 0; index < count; ++index)
|
||||
versionNames.emplace_back(names[index] != nullptr ? names[index] : "");
|
||||
providerId = versionIds[0];
|
||||
providerName = versionNames[0];
|
||||
stage = FsrRrStage::Ready;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// GPU memory the denoiser would allocate for this configuration (total, aliasable).
|
||||
inline ffxReturnCode_t fsr_rr_query_memory(FsrRrRuntime& runtime, ID3D12Device* device, uint32_t width,
|
||||
uint32_t height, uint32_t signals, uint32_t flags,
|
||||
FfxApiEffectMemoryUsage& usage)
|
||||
{
|
||||
ffxOverrideVersion version = {};
|
||||
version.header.type = FFX_API_DESC_TYPE_OVERRIDE_VERSION;
|
||||
version.versionId = runtime.providerId;
|
||||
ffxQueryDescDenoiserGetGPUMemoryUsage desc = {};
|
||||
desc.header.type = FFX_API_QUERY_DESC_TYPE_DENOISER_GPU_MEMORY_USAGE;
|
||||
desc.header.pNext = &version.header;
|
||||
desc.device = device;
|
||||
desc.maxRenderSize = {width, height};
|
||||
desc.signalFlags = signals;
|
||||
desc.flags = flags;
|
||||
desc.gpuMemoryUsage = &usage;
|
||||
return runtime.query(nullptr, &desc.header);
|
||||
}
|
||||
|
||||
// Creates one denoiser context on the enumerated provider. The descriptor chain only has to live
|
||||
// for the call: ffx_api.h requires pointers *inside* descriptors to outlive the context, and
|
||||
// none of these carry any.
|
||||
inline ffxReturnCode_t fsr_rr_create_context(FsrRrRuntime& runtime, ID3D12Device* device, uint32_t maxWidth,
|
||||
uint32_t maxHeight, uint32_t signals, uint32_t flags,
|
||||
ffxContext& context)
|
||||
{
|
||||
ffxCreateContextDescDenoiser desc = {};
|
||||
desc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_DENOISER;
|
||||
desc.version = FFX_DENOISER_VERSION;
|
||||
desc.maxRenderSize = {maxWidth, maxHeight};
|
||||
desc.signalFlags = signals;
|
||||
desc.flags = flags;
|
||||
ffxCreateBackendDX12Desc backend = {};
|
||||
backend.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_DX12;
|
||||
backend.device = device;
|
||||
ffxOverrideVersion version = {};
|
||||
version.header.type = FFX_API_DESC_TYPE_OVERRIDE_VERSION;
|
||||
version.versionId = runtime.providerId;
|
||||
desc.header.pNext = &backend.header;
|
||||
backend.header.pNext = &version.header;
|
||||
context = nullptr;
|
||||
return runtime.createContext(&context, &desc.header, nullptr);
|
||||
}
|
||||
|
||||
// Asks a live context which provider actually serves it (the runtime's own account, logged as
|
||||
// the "actual backend").
|
||||
inline std::string fsr_rr_context_provider(FsrRrRuntime& runtime, ffxContext& context, uint64_t* id = nullptr)
|
||||
{
|
||||
ffxQueryGetProviderVersion desc = {};
|
||||
desc.header.type = FFX_API_QUERY_DESC_TYPE_GET_PROVIDER_VERSION;
|
||||
if (runtime.query(&context, &desc.header) != FFX_API_RETURN_OK || desc.versionName == nullptr)
|
||||
return std::string();
|
||||
if (id != nullptr)
|
||||
*id = desc.versionId;
|
||||
return desc.versionName;
|
||||
}
|
||||
|
||||
// An FfxApiResource for a 2D texture whose description the caller already knows (the shim never
|
||||
// calls GetDesc() by value; see above).
|
||||
inline FfxApiResource fsr_rr_texture(ID3D12Resource* resource, const D3D12_RESOURCE_DESC& desc, uint32_t format,
|
||||
uint32_t state)
|
||||
{
|
||||
FfxApiResource result = {};
|
||||
result.resource = resource;
|
||||
result.state = state;
|
||||
if (resource == nullptr)
|
||||
return result;
|
||||
result.description.type = FFX_API_RESOURCE_TYPE_TEXTURE2D;
|
||||
result.description.format = format;
|
||||
result.description.width = static_cast<uint32_t>(desc.Width);
|
||||
result.description.height = desc.Height;
|
||||
result.description.depth = desc.DepthOrArraySize;
|
||||
result.description.mipCount = desc.MipLevels;
|
||||
result.description.usage = FFX_API_RESOURCE_USAGE_READ_ONLY;
|
||||
if (desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS)
|
||||
result.description.usage |= FFX_API_RESOURCE_USAGE_UAV;
|
||||
return result;
|
||||
}
|
||||
|
||||
// The FFX surface formats the adapter's own intermediate textures use.
|
||||
inline uint32_t fsr_rr_surface_format(DXGI_FORMAT format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case DXGI_FORMAT_R32_FLOAT: return FFX_API_SURFACE_FORMAT_R32_FLOAT;
|
||||
case DXGI_FORMAT_R16G16B16A16_FLOAT: return FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT;
|
||||
case DXGI_FORMAT_R32G32B32A32_FLOAT: return FFX_API_SURFACE_FORMAT_R32G32B32A32_FLOAT;
|
||||
case DXGI_FORMAT_R10G10B10A2_UNORM: return FFX_API_SURFACE_FORMAT_R10G10B10A2_UNORM;
|
||||
case DXGI_FORMAT_R8G8B8A8_UNORM: return FFX_API_SURFACE_FORMAT_R8G8B8A8_UNORM;
|
||||
case DXGI_FORMAT_R16_FLOAT: return FFX_API_SURFACE_FORMAT_R16_FLOAT;
|
||||
case DXGI_FORMAT_R8_UNORM: return FFX_API_SURFACE_FORMAT_R8_UNORM;
|
||||
default: return FFX_API_SURFACE_FORMAT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
// Standalone check that AMD's FSR Ray Regeneration runtime runs on this D3D12 device, separate from
|
||||
// any game and from the NGX shim. It:
|
||||
// 1. creates a D3D12 device and reports the adapter identity the runtime will see through DXGI,
|
||||
// 2. loads the given amd_fidelityfx_denoiser_dx12.dll (unmodified, by path) and enumerates its
|
||||
// denoiser providers against that device (FFX_API_QUERY_DESC_TYPE_GET_VERSIONS),
|
||||
// 3. creates a context (this is where the runtime builds its shader pipelines) and asks it which
|
||||
// provider it got,
|
||||
// 4. dispatches a synthetic ray-traced scene with per-frame white noise: a static camera, then a
|
||||
// moving one (with exact 2.5D motion vectors), then a camera cut with FFX_DENOISER_DISPATCH_RESET,
|
||||
// 5. reads every output back and compares input and output against the noise-free signals, so a
|
||||
// passthrough or a stale history cannot pass for denoising.
|
||||
// Every stage prints one "stage=... result=..." line, and OUT_DIR/probe.json collects the lot.
|
||||
//
|
||||
// usage: fsr_rr_probe.exe DENOISER_DLL OUT_DIR [--size WxH] [--frames N] [--signals HEX] [--query-only]
|
||||
// [--no-driver-extension]
|
||||
// --no-driver-extension: do not load Proton's amdxc64.dll for an AMD GPU hidden as NVIDIA
|
||||
// (see d4r_fsr_rr_identity.h); reproduces the "no provider" outcome
|
||||
// --signals: FfxApiDenoiserSignalFlags of the radiance signals to run (default 0x36: direct and
|
||||
// indirect diffuse and specular, AMD's four-signal configuration; 0x30 = indirect only)
|
||||
#define WIDL_EXPLICIT_AGGREGATE_RETURNS
|
||||
#include <windows.h>
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_4.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "d4r_fsr_rr.h"
|
||||
#include "windows/fsr_rr_identity.h"
|
||||
#include "fsr_rr_scene.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace fsr_scene;
|
||||
|
||||
FILE* g_json = nullptr;
|
||||
bool g_jsonFirst = true;
|
||||
|
||||
void json_raw(const char* format, ...)
|
||||
{
|
||||
if (g_json == nullptr)
|
||||
return;
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
std::vfprintf(g_json, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
// One top-level "key": value pair; value is preformatted JSON.
|
||||
void json_field(const char* key, const std::string& value)
|
||||
{
|
||||
json_raw("%s\n \"%s\": %s", g_jsonFirst ? "" : ",", key, value.c_str());
|
||||
g_jsonFirst = false;
|
||||
}
|
||||
|
||||
std::string quoted(const std::string& text)
|
||||
{
|
||||
std::string out = "\"";
|
||||
for (char c : text)
|
||||
{
|
||||
if (c == '"' || c == '\\')
|
||||
out += '\\';
|
||||
if (static_cast<unsigned char>(c) < 0x20)
|
||||
continue;
|
||||
out += c;
|
||||
}
|
||||
return out + "\"";
|
||||
}
|
||||
|
||||
std::string narrow(const wchar_t* text)
|
||||
{
|
||||
char buffer[1024] = {};
|
||||
WideCharToMultiByte(CP_UTF8, 0, text, -1, buffer, sizeof(buffer), nullptr, nullptr);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string format(const char* pattern, ...)
|
||||
{
|
||||
char buffer[2048];
|
||||
va_list args;
|
||||
va_start(args, pattern);
|
||||
std::vsnprintf(buffer, sizeof(buffer), pattern, args);
|
||||
va_end(args);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void stage(const char* name, bool ok, const std::string& detail)
|
||||
{
|
||||
std::printf("stage=%s result=%s %s\n", name, ok ? "ok" : "FAIL", detail.c_str());
|
||||
std::fflush(stdout);
|
||||
json_field((std::string("stage_") + name).c_str(),
|
||||
format("{\"ok\": %s, \"detail\": %s}", ok ? "true" : "false", quoted(detail).c_str()));
|
||||
}
|
||||
|
||||
std::vector<std::string> g_messages;
|
||||
|
||||
void ffx_message(uint32_t type, const wchar_t* message)
|
||||
{
|
||||
const std::string text = narrow(message);
|
||||
std::printf("ffx %s: %s\n", type == FFX_API_MESSAGE_TYPE_WARNING ? "warning" : "error", text.c_str());
|
||||
std::fflush(stdout);
|
||||
if (g_messages.size() < 64)
|
||||
g_messages.push_back(text);
|
||||
}
|
||||
|
||||
// --- D3D12 plumbing -------------------------------------------------------------------
|
||||
|
||||
#define CHECK_HR(expr) \
|
||||
do \
|
||||
{ \
|
||||
const HRESULT hr_ = (expr); \
|
||||
if (FAILED(hr_)) \
|
||||
{ \
|
||||
std::printf("%s failed: 0x%08lx (line %d)\n", #expr, static_cast<unsigned long>(hr_), __LINE__); \
|
||||
return false; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
struct Texture
|
||||
{
|
||||
ID3D12Resource* resource = nullptr;
|
||||
D3D12_RESOURCE_DESC desc = {};
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {};
|
||||
UINT rows = 0;
|
||||
UINT64 rowBytes = 0, total = 0;
|
||||
D3D12_RESOURCE_STATES state = D3D12_RESOURCE_STATE_COMMON;
|
||||
};
|
||||
|
||||
ID3D12Device* g_device = nullptr;
|
||||
|
||||
bool make_texture(Texture& texture, uint32_t width, uint32_t height, DXGI_FORMAT format, bool uav,
|
||||
D3D12_RESOURCE_STATES state)
|
||||
{
|
||||
texture.desc = {};
|
||||
texture.desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
|
||||
texture.desc.Width = width;
|
||||
texture.desc.Height = height;
|
||||
texture.desc.DepthOrArraySize = 1;
|
||||
texture.desc.MipLevels = 1;
|
||||
texture.desc.Format = format;
|
||||
texture.desc.SampleDesc.Count = 1;
|
||||
texture.desc.Flags = uav ? D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS : D3D12_RESOURCE_FLAG_NONE;
|
||||
D3D12_HEAP_PROPERTIES heap = {};
|
||||
heap.Type = D3D12_HEAP_TYPE_DEFAULT;
|
||||
CHECK_HR(g_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &texture.desc, state, nullptr,
|
||||
IID_PPV_ARGS(&texture.resource)));
|
||||
texture.state = state;
|
||||
g_device->GetCopyableFootprints(&texture.desc, 0, 1, 0, &texture.footprint, &texture.rows, &texture.rowBytes,
|
||||
&texture.total);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool make_buffer(ID3D12Resource*& buffer, UINT64 bytes, D3D12_HEAP_TYPE type, uint8_t*& mapped)
|
||||
{
|
||||
D3D12_RESOURCE_DESC desc = {};
|
||||
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
|
||||
desc.Width = bytes;
|
||||
desc.Height = 1;
|
||||
desc.DepthOrArraySize = 1;
|
||||
desc.MipLevels = 1;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
|
||||
D3D12_HEAP_PROPERTIES heap = {};
|
||||
heap.Type = type;
|
||||
const D3D12_RESOURCE_STATES state =
|
||||
type == D3D12_HEAP_TYPE_UPLOAD ? D3D12_RESOURCE_STATE_GENERIC_READ : D3D12_RESOURCE_STATE_COPY_DEST;
|
||||
CHECK_HR(g_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, state, nullptr,
|
||||
IID_PPV_ARGS(&buffer)));
|
||||
CHECK_HR(buffer->Map(0, nullptr, reinterpret_cast<void**>(&mapped)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void barrier(ID3D12GraphicsCommandList* list, Texture& texture, D3D12_RESOURCE_STATES after)
|
||||
{
|
||||
if (texture.state == after)
|
||||
return;
|
||||
D3D12_RESOURCE_BARRIER b = {};
|
||||
b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
b.Transition.pResource = texture.resource;
|
||||
b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
b.Transition.StateBefore = texture.state;
|
||||
b.Transition.StateAfter = after;
|
||||
list->ResourceBarrier(1, &b);
|
||||
texture.state = after;
|
||||
}
|
||||
|
||||
// Copies `texelBytes`-sized texels from `source` (tightly packed rows) into the upload buffer at
|
||||
// `offset` with the texture's footprint, and records the copy.
|
||||
void upload(ID3D12GraphicsCommandList* list, Texture& texture, ID3D12Resource* staging, uint8_t* mapped,
|
||||
UINT64& offset, const void* source, size_t texelBytes)
|
||||
{
|
||||
offset = (offset + D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1) & ~UINT64(D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1);
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = texture.footprint;
|
||||
footprint.Offset = offset;
|
||||
const size_t rowBytes = texelBytes * texture.desc.Width;
|
||||
for (UINT row = 0; row < texture.rows; ++row)
|
||||
std::memcpy(mapped + offset + row * footprint.Footprint.RowPitch,
|
||||
static_cast<const uint8_t*>(source) + row * rowBytes, rowBytes);
|
||||
offset += texture.total;
|
||||
barrier(list, texture, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
D3D12_TEXTURE_COPY_LOCATION dst = {};
|
||||
dst.pResource = texture.resource;
|
||||
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
|
||||
D3D12_TEXTURE_COPY_LOCATION src = {};
|
||||
src.pResource = staging;
|
||||
src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
|
||||
src.PlacedFootprint = footprint;
|
||||
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
|
||||
}
|
||||
|
||||
void readback(ID3D12GraphicsCommandList* list, Texture& texture, ID3D12Resource* buffer, UINT64 offset)
|
||||
{
|
||||
barrier(list, texture, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = texture.footprint;
|
||||
footprint.Offset = offset;
|
||||
D3D12_TEXTURE_COPY_LOCATION dst = {};
|
||||
dst.pResource = buffer;
|
||||
dst.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
|
||||
dst.PlacedFootprint = footprint;
|
||||
D3D12_TEXTURE_COPY_LOCATION src = {};
|
||||
src.pResource = texture.resource;
|
||||
src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
|
||||
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
|
||||
}
|
||||
|
||||
// --- metrics and captures -------------------------------------------------------------
|
||||
|
||||
struct Error
|
||||
{
|
||||
double input = 0, output = 0, outputVsPrevious = 0, change = 0;
|
||||
size_t nonFinite = 0;
|
||||
};
|
||||
|
||||
Error measure(const Frame& frame, const Frame* previous, const uint8_t* rows, UINT pitch, uint32_t width,
|
||||
uint32_t height, int signal)
|
||||
{
|
||||
Error e;
|
||||
size_t n = 0;
|
||||
for (uint32_t y = 2; y + 2 < height; ++y)
|
||||
for (uint32_t x = 2; x + 2 < width; ++x)
|
||||
{
|
||||
const size_t i = static_cast<size_t>(y) * width + x;
|
||||
const uint16_t* out = reinterpret_cast<const uint16_t*>(rows + static_cast<size_t>(y) * pitch) + x * 4;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
{
|
||||
const float o = from_half(out[c]);
|
||||
if (!std::isfinite(o))
|
||||
{
|
||||
++e.nonFinite;
|
||||
continue;
|
||||
}
|
||||
if (!frame.valid[i])
|
||||
continue;
|
||||
const float clean = frame.clean[signal][i * 3 + c];
|
||||
const float in = from_half(frame.noisy[signal][i * 4 + c]);
|
||||
e.input += (in - clean) * (in - clean);
|
||||
e.output += (o - clean) * (o - clean);
|
||||
e.change += std::fabs(o - in);
|
||||
if (previous != nullptr && previous->valid[i])
|
||||
{
|
||||
const float old = previous->clean[signal][i * 3 + c];
|
||||
e.outputVsPrevious += (o - old) * (o - old);
|
||||
}
|
||||
++n;
|
||||
}
|
||||
}
|
||||
if (n > 0)
|
||||
{
|
||||
e.input = std::sqrt(e.input / n);
|
||||
e.output = std::sqrt(e.output / n);
|
||||
e.outputVsPrevious = std::sqrt(e.outputVsPrevious / n);
|
||||
e.change /= n;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
// Tonemapped composite (diffuse signals x diffuse albedo + specular signals x specular albedo).
|
||||
void write_ppm(const std::string& path, uint32_t width, uint32_t height, const std::vector<V3>& radiance)
|
||||
{
|
||||
FILE* file = std::fopen(path.c_str(), "wb");
|
||||
if (file == nullptr)
|
||||
return;
|
||||
std::fprintf(file, "P6\n%u %u\n255\n", width, height);
|
||||
std::vector<uint8_t> row(width * 3);
|
||||
for (uint32_t y = 0; y < height; ++y)
|
||||
{
|
||||
for (uint32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const V3 c = radiance[static_cast<size_t>(y) * width + x];
|
||||
const float rgb[3] = {c.x, c.y, c.z};
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
const float mapped = std::pow(std::max(0.0f, rgb[k]) / (1.0f + std::max(0.0f, rgb[k])), 1.0f / 2.2f);
|
||||
row[x * 3 + k] = static_cast<uint8_t>(std::lround(saturate(mapped) * 255.0f));
|
||||
}
|
||||
}
|
||||
std::fwrite(row.data(), 1, row.size(), file);
|
||||
}
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
if (argc < 3)
|
||||
{
|
||||
std::printf("usage: fsr_rr_probe.exe DENOISER_DLL OUT_DIR [--size WxH] [--frames N] [--signals HEX] [--query-only]\n");
|
||||
return 2;
|
||||
}
|
||||
const std::wstring dllPath = argv[1];
|
||||
const std::string outDir = narrow(argv[2]);
|
||||
uint32_t width = 960, height = 540, frames = 48, signals = 0x36;
|
||||
bool queryOnly = false, driverExtension = true;
|
||||
for (int i = 3; i < argc; ++i)
|
||||
{
|
||||
const std::wstring arg = argv[i];
|
||||
if (arg == L"--size" && i + 1 < argc)
|
||||
std::swscanf(argv[++i], L"%ux%u", &width, &height);
|
||||
else if (arg == L"--frames" && i + 1 < argc)
|
||||
frames = static_cast<uint32_t>(std::wcstoul(argv[++i], nullptr, 0));
|
||||
else if (arg == L"--signals" && i + 1 < argc)
|
||||
signals = static_cast<uint32_t>(std::wcstoul(argv[++i], nullptr, 16));
|
||||
else if (arg == L"--query-only")
|
||||
queryOnly = true;
|
||||
else if (arg == L"--no-driver-extension")
|
||||
driverExtension = false;
|
||||
}
|
||||
signals &= 0x36;
|
||||
if (signals == 0 || frames < 12)
|
||||
{
|
||||
std::printf("need at least one radiance signal and 12 frames\n");
|
||||
return 2;
|
||||
}
|
||||
CreateDirectoryA(outDir.c_str(), nullptr);
|
||||
g_json = std::fopen((outDir + "\\probe.json").c_str(), "w");
|
||||
json_raw("{");
|
||||
json_field("denoiser_dll", quoted(narrow(dllPath.c_str())));
|
||||
json_field("render_size", format("[%u, %u]", width, height));
|
||||
json_field("signal_flags", format("%u", signals));
|
||||
json_field("frames", format("%u", frames));
|
||||
int exitCode = 1;
|
||||
auto finish = [&](int code) {
|
||||
json_raw("\n}\n");
|
||||
if (g_json)
|
||||
std::fclose(g_json);
|
||||
g_json = nullptr;
|
||||
return code;
|
||||
};
|
||||
|
||||
// 1. Device, and the adapter identity DXGI reports (what the runtime can see).
|
||||
IDXGIFactory4* factory = nullptr;
|
||||
IDXGIAdapter1* adapter = nullptr;
|
||||
if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))) || FAILED(factory->EnumAdapters1(0, &adapter)))
|
||||
{
|
||||
stage("device", false, "no DXGI adapter");
|
||||
return finish(1);
|
||||
}
|
||||
DXGI_ADAPTER_DESC1 adapterDesc = {};
|
||||
adapter->GetDesc1(&adapterDesc);
|
||||
if (FAILED(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&g_device))))
|
||||
{
|
||||
stage("device", false, "D3D12CreateDevice failed");
|
||||
return finish(1);
|
||||
}
|
||||
D3D12_FEATURE_DATA_SHADER_MODEL shaderModel = {D3D_SHADER_MODEL_6_6};
|
||||
g_device->CheckFeatureSupport(D3D12_FEATURE_SHADER_MODEL, &shaderModel, sizeof(shaderModel));
|
||||
stage("device", true,
|
||||
format("adapter=\"%s\" vendor=0x%04x device=0x%04x luid=%08lx:%08lx shader_model=0x%x",
|
||||
narrow(adapterDesc.Description).c_str(), adapterDesc.VendorId, adapterDesc.DeviceId,
|
||||
static_cast<unsigned long>(adapterDesc.AdapterLuid.HighPart),
|
||||
static_cast<unsigned long>(adapterDesc.AdapterLuid.LowPart),
|
||||
static_cast<unsigned>(shaderModel.HighestShaderModel)));
|
||||
json_field("adapter", format("{\"name\": %s, \"vendor\": %u, \"device\": %u}",
|
||||
quoted(narrow(adapterDesc.Description)).c_str(), adapterDesc.VendorId,
|
||||
adapterDesc.DeviceId));
|
||||
|
||||
// 2. Runtime and provider.
|
||||
FsrRrRuntime runtime;
|
||||
if (!runtime.load(dllPath))
|
||||
{
|
||||
stage("load", false, format("%s (error %lu)", fsr_rr_stage_name(runtime.stage), GetLastError()));
|
||||
return finish(1);
|
||||
}
|
||||
stage("load", true, "mapped=" + narrow(runtime.mappedPath.c_str()));
|
||||
if (driverExtension)
|
||||
{
|
||||
std::string detail;
|
||||
const bool loaded = d4r_fsr_rr_driver_extension(g_device, detail);
|
||||
stage("driver_extension", loaded, detail);
|
||||
}
|
||||
const ffxReturnCode_t debug = runtime.set_message_callback(&ffx_message, FFX_API_CONFIGURE_GLOBALDEBUG_LEVEL_VERBOSE);
|
||||
std::printf("ffx global debug callback: %s\n", fsr_rr_return_name(debug));
|
||||
const bool provider = runtime.query_providers(g_device);
|
||||
{
|
||||
std::string list = "[";
|
||||
for (size_t i = 0; i < runtime.versionIds.size(); ++i)
|
||||
list += format("%s{\"id\": \"0x%016llx\", \"name\": %s}", i ? ", " : "",
|
||||
static_cast<unsigned long long>(runtime.versionIds[i]),
|
||||
quoted(runtime.versionNames[i]).c_str());
|
||||
json_field("providers", list + "]");
|
||||
}
|
||||
stage("provider", provider,
|
||||
provider ? format("count=%zu selected=0x%016llx name=\"%s\"", runtime.versionIds.size(),
|
||||
static_cast<unsigned long long>(runtime.providerId), runtime.providerName.c_str())
|
||||
: format("%s (query %s)", fsr_rr_stage_name(runtime.stage), fsr_rr_return_name(runtime.queryResult)));
|
||||
if (!provider || queryOnly)
|
||||
return finish(provider ? 0 : 1);
|
||||
|
||||
FfxApiEffectMemoryUsage memory = {};
|
||||
const ffxReturnCode_t memoryResult = fsr_rr_query_memory(runtime, g_device, width, height, signals, 0, memory);
|
||||
json_field("gpu_memory", format("{\"result\": %u, \"total_mb\": %.2f, \"aliasable_mb\": %.2f}", memoryResult,
|
||||
memory.totalUsageInBytes / 1048576.0, memory.aliasableUsageInBytes / 1048576.0));
|
||||
std::printf("gpu memory query: %s total=%.2f MB aliasable=%.2f MB\n", fsr_rr_return_name(memoryResult),
|
||||
memory.totalUsageInBytes / 1048576.0, memory.aliasableUsageInBytes / 1048576.0);
|
||||
|
||||
// 3. Context (shader pipelines are created here: the slow and the most likely to fail part).
|
||||
ffxContext context = nullptr;
|
||||
const auto createStart = std::chrono::steady_clock::now();
|
||||
const ffxReturnCode_t created = fsr_rr_create_context(runtime, g_device, width, height, signals, 0, context);
|
||||
const double createMs =
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - createStart).count();
|
||||
json_field("context_create_ms", format("%.1f", createMs));
|
||||
if (created != FFX_API_RETURN_OK)
|
||||
{
|
||||
stage("context", false, format("ffxCreateContext -> %s after %.1f ms", fsr_rr_return_name(created), createMs));
|
||||
return finish(1);
|
||||
}
|
||||
uint64_t actualId = 0;
|
||||
const std::string actual = fsr_rr_context_provider(runtime, context, &actualId);
|
||||
stage("context", true, format("%.1f ms, provider in use 0x%016llx \"%s\"", createMs,
|
||||
static_cast<unsigned long long>(actualId), actual.c_str()));
|
||||
json_field("provider_in_use", quoted(actual));
|
||||
|
||||
// 4. Resources and frames.
|
||||
ID3D12CommandQueue* queue = nullptr;
|
||||
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
|
||||
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
||||
ID3D12CommandAllocator* allocator = nullptr;
|
||||
ID3D12GraphicsCommandList* list = nullptr;
|
||||
ID3D12Fence* fence = nullptr;
|
||||
if (FAILED(g_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&queue))) ||
|
||||
FAILED(g_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator))) ||
|
||||
FAILED(g_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator, nullptr, IID_PPV_ARGS(&list))) ||
|
||||
FAILED(g_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence))))
|
||||
{
|
||||
stage("dispatch", false, "command objects");
|
||||
return finish(1);
|
||||
}
|
||||
list->Close();
|
||||
HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
uint64_t fenceValue = 0;
|
||||
const D3D12_RESOURCE_STATES readState =
|
||||
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
|
||||
Texture depth, motion, normals, diffuseAlbedo, specularAlbedo, input[kSignalCount], output[kSignalCount];
|
||||
bool ok = make_texture(depth, width, height, DXGI_FORMAT_R32_FLOAT, false, readState) &&
|
||||
make_texture(motion, width, height, DXGI_FORMAT_R16G16B16A16_FLOAT, false, readState) &&
|
||||
make_texture(normals, width, height, DXGI_FORMAT_R10G10B10A2_UNORM, false, readState) &&
|
||||
make_texture(diffuseAlbedo, width, height, DXGI_FORMAT_R8G8B8A8_UNORM, false, readState) &&
|
||||
make_texture(specularAlbedo, width, height, DXGI_FORMAT_R8G8B8A8_UNORM, false, readState);
|
||||
for (int s = 0; s < kSignalCount && ok; ++s)
|
||||
ok = make_texture(input[s], width, height, DXGI_FORMAT_R16G16B16A16_FLOAT, false, readState) &&
|
||||
make_texture(output[s], width, height, DXGI_FORMAT_R16G16B16A16_FLOAT, true,
|
||||
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
ID3D12Resource *staging = nullptr, *readbackBuffer = nullptr, *timestampBuffer = nullptr;
|
||||
uint8_t *stagingMapped = nullptr, *readbackMapped = nullptr, *timestampMapped = nullptr;
|
||||
const UINT64 planeBytes = output[0].total;
|
||||
ok = ok && make_buffer(staging, 16 * (planeBytes + 65536), D3D12_HEAP_TYPE_UPLOAD, stagingMapped) &&
|
||||
make_buffer(readbackBuffer, kSignalCount * planeBytes, D3D12_HEAP_TYPE_READBACK, readbackMapped) &&
|
||||
make_buffer(timestampBuffer, 16, D3D12_HEAP_TYPE_READBACK, timestampMapped);
|
||||
ID3D12QueryHeap* timestamps = nullptr;
|
||||
D3D12_QUERY_HEAP_DESC queryDesc = {};
|
||||
queryDesc.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP;
|
||||
queryDesc.Count = 2;
|
||||
ok = ok && SUCCEEDED(g_device->CreateQueryHeap(&queryDesc, IID_PPV_ARGS(×tamps)));
|
||||
UINT64 ticksPerSecond = 0;
|
||||
queue->GetTimestampFrequency(&ticksPerSecond);
|
||||
if (!ok)
|
||||
{
|
||||
stage("dispatch", false, "resource creation");
|
||||
return finish(1);
|
||||
}
|
||||
|
||||
const float aspect = static_cast<float>(width) / height;
|
||||
const M4 projection = projection_matrix(1.0f, aspect, 0.1f, 1000.0f);
|
||||
const uint32_t cutFrame = frames * 2 / 3, moveStart = frames / 3;
|
||||
auto camera_at = [&](uint32_t f) -> Camera {
|
||||
if (f >= cutFrame)
|
||||
return {{3.5f, 1.6f, 1.5f}, {-0.5f, -0.6f, 7.5f}};
|
||||
const float t = f >= moveStart ? static_cast<float>(f - moveStart) : 0.0f;
|
||||
return {{-0.6f + 0.04f * t, 0.6f, -1.5f}, {0.0f + 0.02f * t, -0.1f, 6.5f}};
|
||||
};
|
||||
|
||||
Frame frameData, previousFrame;
|
||||
std::vector<double> gpuMs, cpuMs;
|
||||
std::string frameLog = "[";
|
||||
bool dispatchOk = true, converged = true, resetOk = true, finite = true, notPassthrough = true;
|
||||
Camera previousCamera = camera_at(0);
|
||||
for (uint32_t f = 0; f < frames; ++f)
|
||||
{
|
||||
const Camera camera = camera_at(f);
|
||||
const bool reset = f == 0 || f == cutFrame;
|
||||
std::swap(previousFrame, frameData);
|
||||
render(frameData, width, height, camera, reset ? camera : previousCamera, projection, f);
|
||||
|
||||
allocator->Reset();
|
||||
list->Reset(allocator, nullptr);
|
||||
UINT64 offset = 0;
|
||||
upload(list, depth, staging, stagingMapped, offset, frameData.depth.data(), 4);
|
||||
upload(list, motion, staging, stagingMapped, offset, frameData.motion.data(), 8);
|
||||
upload(list, normals, staging, stagingMapped, offset, frameData.normals.data(), 4);
|
||||
upload(list, diffuseAlbedo, staging, stagingMapped, offset, frameData.diffuseAlbedo.data(), 4);
|
||||
upload(list, specularAlbedo, staging, stagingMapped, offset, frameData.specularAlbedo.data(), 4);
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
if (signals & kSignalFlags[s])
|
||||
upload(list, input[s], staging, stagingMapped, offset, frameData.noisy[s].data(), 8);
|
||||
Texture* all[] = {&depth, &motion, &normals, &diffuseAlbedo, &specularAlbedo};
|
||||
for (Texture* t : all)
|
||||
barrier(list, *t, readState);
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
{
|
||||
barrier(list, input[s], readState);
|
||||
barrier(list, output[s], D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
}
|
||||
|
||||
ffxDispatchDescDenoiser desc = {};
|
||||
desc.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER;
|
||||
desc.commandList = list;
|
||||
const uint32_t readFfx = FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ;
|
||||
desc.linearDepth = fsr_rr_texture(depth.resource, depth.desc, FFX_API_SURFACE_FORMAT_R32_FLOAT, readFfx);
|
||||
desc.motionVectors = fsr_rr_texture(motion.resource, motion.desc, FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT, readFfx);
|
||||
desc.normals = fsr_rr_texture(normals.resource, normals.desc, FFX_API_SURFACE_FORMAT_R10G10B10A2_UNORM, readFfx);
|
||||
desc.specularAlbedo = fsr_rr_texture(specularAlbedo.resource, specularAlbedo.desc, FFX_API_SURFACE_FORMAT_R8G8B8A8_UNORM, readFfx);
|
||||
desc.diffuseAlbedo = fsr_rr_texture(diffuseAlbedo.resource, diffuseAlbedo.desc, FFX_API_SURFACE_FORMAT_R8G8B8A8_UNORM, readFfx);
|
||||
desc.motionVectorScale = {1.0f, 1.0f, 1.0f};
|
||||
desc.jitterOffsets = {0.0f, 0.0f};
|
||||
const V3 delta = previousCamera.eye - camera.eye;
|
||||
desc.cameraPositionDelta = reset ? FfxApiFloatCoords3D{0, 0, 0} : FfxApiFloatCoords3D{delta.x, delta.y, delta.z};
|
||||
const M4 view = view_matrix(camera.eye, camera.target);
|
||||
std::memcpy(&desc.view, &view, sizeof(desc.view));
|
||||
std::memcpy(&desc.projection, &projection, sizeof(desc.projection));
|
||||
desc.linearDepthBounds = {0.0f, 1000.0f};
|
||||
desc.renderSize = {width, height};
|
||||
desc.frameIndex = f;
|
||||
desc.flags = reset ? FFX_DENOISER_DISPATCH_RESET : 0;
|
||||
ffxDispatchDescDenoiserDirectDiffuse dd = {};
|
||||
ffxDispatchDescDenoiserDirectSpecular ds = {};
|
||||
ffxDispatchDescDenoiserIndirectDiffuse id = {};
|
||||
ffxDispatchDescDenoiserIndirectSpecular is = {};
|
||||
dd.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_DIRECT_DIFFUSE;
|
||||
ds.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_DIRECT_SPECULAR;
|
||||
id.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_INDIRECT_DIFFUSE;
|
||||
is.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_INDIRECT_SPECULAR;
|
||||
FfxApiDenoiserSignal* signalDescs[kSignalCount] = {&dd.signal, &ds.signal, &id.signal, &is.signal};
|
||||
ffxApiHeader* headers[kSignalCount] = {&dd.header, &ds.header, &id.header, &is.header};
|
||||
ffxApiHeader* tail = &desc.header;
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
{
|
||||
if (!(signals & kSignalFlags[s]))
|
||||
continue;
|
||||
signalDescs[s]->input = fsr_rr_texture(input[s].resource, input[s].desc, FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT, readFfx);
|
||||
signalDescs[s]->output = fsr_rr_texture(output[s].resource, output[s].desc, FFX_API_SURFACE_FORMAT_R16G16B16A16_FLOAT,
|
||||
FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
tail->pNext = headers[s];
|
||||
tail = headers[s];
|
||||
}
|
||||
list->EndQuery(timestamps, D3D12_QUERY_TYPE_TIMESTAMP, 0);
|
||||
const auto cpuStart = std::chrono::steady_clock::now();
|
||||
const ffxReturnCode_t dispatched = runtime.dispatch(&context, &desc.header);
|
||||
cpuMs.push_back(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - cpuStart).count());
|
||||
list->EndQuery(timestamps, D3D12_QUERY_TYPE_TIMESTAMP, 1);
|
||||
list->ResolveQueryData(timestamps, D3D12_QUERY_TYPE_TIMESTAMP, 0, 2, timestampBuffer, 0);
|
||||
if (dispatched != FFX_API_RETURN_OK)
|
||||
{
|
||||
stage("dispatch", false, format("frame %u: ffxDispatch -> %s", f, fsr_rr_return_name(dispatched)));
|
||||
dispatchOk = false;
|
||||
list->Close();
|
||||
break;
|
||||
}
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
if (signals & kSignalFlags[s])
|
||||
readback(list, output[s], readbackBuffer, s * planeBytes);
|
||||
if (FAILED(list->Close()))
|
||||
{
|
||||
stage("dispatch", false, format("frame %u: Close failed", f));
|
||||
dispatchOk = false;
|
||||
break;
|
||||
}
|
||||
ID3D12CommandList* lists[] = {list};
|
||||
queue->ExecuteCommandLists(1, lists);
|
||||
queue->Signal(fence, ++fenceValue);
|
||||
fence->SetEventOnCompletion(fenceValue, event);
|
||||
if (WaitForSingleObject(event, 20000) != WAIT_OBJECT_0)
|
||||
{
|
||||
stage("dispatch", false, format("frame %u: GPU did not finish within 20 s", f));
|
||||
dispatchOk = false;
|
||||
break;
|
||||
}
|
||||
const HRESULT removed = g_device->GetDeviceRemovedReason();
|
||||
if (FAILED(removed))
|
||||
{
|
||||
stage("dispatch", false, format("frame %u: device removed 0x%08lx", f, static_cast<unsigned long>(removed)));
|
||||
dispatchOk = false;
|
||||
break;
|
||||
}
|
||||
uint64_t stamps[2];
|
||||
std::memcpy(stamps, timestampMapped, sizeof(stamps));
|
||||
const double gpu = ticksPerSecond ? (stamps[1] - stamps[0]) * 1000.0 / ticksPerSecond : -1.0;
|
||||
gpuMs.push_back(gpu);
|
||||
|
||||
// 5. Compare against the clean signals.
|
||||
frameLog += format("%s\n {\"frame\": %u, \"reset\": %s, \"gpu_ms\": %.3f, \"cpu_ms\": %.3f, \"signals\": {",
|
||||
f ? "," : "", f, reset ? "true" : "false", gpu, cpuMs.back());
|
||||
bool firstSignal = true;
|
||||
std::vector<V3> composeOut(static_cast<size_t>(width) * height), composeIn(composeOut.size()),
|
||||
composeClean(composeOut.size());
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
{
|
||||
if (!(signals & kSignalFlags[s]))
|
||||
continue;
|
||||
const uint8_t* rows = readbackMapped + s * planeBytes;
|
||||
const Error e = measure(frameData, f == cutFrame ? &previousFrame : nullptr, rows,
|
||||
output[s].footprint.Footprint.RowPitch, width, height, s);
|
||||
frameLog += format("%s\"%s\": {\"rmse_in\": %.5f, \"rmse_out\": %.5f, \"mean_abs_change\": %.5f, "
|
||||
"\"rmse_out_vs_previous_scene\": %.5f, \"nonfinite\": %zu}",
|
||||
firstSignal ? "" : ", ", kSignalNames[s], e.input, e.output, e.change,
|
||||
e.outputVsPrevious, e.nonFinite);
|
||||
firstSignal = false;
|
||||
finite = finite && e.nonFinite == 0;
|
||||
notPassthrough = notPassthrough && (f < 2 || e.change > 1e-3 * (e.input + 1e-6));
|
||||
// Converged before the cut: at least 8 static or slowly moving frames into history.
|
||||
if (f + 1 == cutFrame && !(e.output < 0.5 * e.input))
|
||||
converged = false;
|
||||
if (f == cutFrame && !(e.output < e.outputVsPrevious))
|
||||
resetOk = false;
|
||||
if (f + 1 == frames && !(e.output < 0.6 * e.input))
|
||||
resetOk = false;
|
||||
const bool diffuse = s == kDirectDiffuse || s == kIndirectDiffuse;
|
||||
for (uint32_t y = 0; y < height; ++y)
|
||||
for (uint32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const size_t i = static_cast<size_t>(y) * width + x;
|
||||
const V3 albedo = diffuse ? frameData.diffuseLinear[i] : frameData.specularLinear[i];
|
||||
const uint16_t* o = reinterpret_cast<const uint16_t*>(rows + static_cast<size_t>(y) * output[s].footprint.Footprint.RowPitch) + x * 4;
|
||||
composeOut[i] = composeOut[i] + mul(V3{from_half(o[0]), from_half(o[1]), from_half(o[2])}, albedo);
|
||||
composeIn[i] = composeIn[i] + mul(V3{from_half(frameData.noisy[s][i * 4]), from_half(frameData.noisy[s][i * 4 + 1]),
|
||||
from_half(frameData.noisy[s][i * 4 + 2])}, albedo);
|
||||
composeClean[i] = composeClean[i] + mul(V3{frameData.clean[s][i * 3], frameData.clean[s][i * 3 + 1],
|
||||
frameData.clean[s][i * 3 + 2]}, albedo);
|
||||
}
|
||||
}
|
||||
frameLog += "}}";
|
||||
if (f == 0 || f + 1 == moveStart || f + 1 == cutFrame || f == cutFrame || f == cutFrame + 1 || f + 1 == frames)
|
||||
{
|
||||
write_ppm(format("%s\\frame%03u_noisy.ppm", outDir.c_str(), f), width, height, composeIn);
|
||||
write_ppm(format("%s\\frame%03u_denoised.ppm", outDir.c_str(), f), width, height, composeOut);
|
||||
write_ppm(format("%s\\frame%03u_clean.ppm", outDir.c_str(), f), width, height, composeClean);
|
||||
}
|
||||
previousCamera = camera;
|
||||
}
|
||||
json_field("frame_log", frameLog + "\n ]");
|
||||
if (dispatchOk)
|
||||
{
|
||||
std::vector<double> warm(gpuMs.begin() + std::min<size_t>(gpuMs.size(), 3), gpuMs.end());
|
||||
std::sort(warm.begin(), warm.end());
|
||||
const double median = warm.empty() ? -1 : warm[warm.size() / 2];
|
||||
std::vector<double> cpuWarm(cpuMs.begin() + std::min<size_t>(cpuMs.size(), 3), cpuMs.end());
|
||||
std::sort(cpuWarm.begin(), cpuWarm.end());
|
||||
stage("dispatch", true, format("%u frames, GPU median %.3f ms (first frame %.3f ms), CPU record median %.3f ms",
|
||||
frames, median, gpuMs.empty() ? -1.0 : gpuMs[0],
|
||||
cpuWarm.empty() ? -1.0 : cpuWarm[cpuWarm.size() / 2]));
|
||||
json_field("gpu_ms_median", format("%.4f", median));
|
||||
stage("output_finite", finite, finite ? "no NaN/Inf in any output" : "non-finite output values");
|
||||
stage("not_passthrough", notPassthrough, "outputs differ from the noisy inputs");
|
||||
stage("denoised", converged, format("before the cut (frame %u) every signal's RMSE vs clean is < 0.5x the input's",
|
||||
cutFrame - 1));
|
||||
stage("reset", resetOk, format("frame %u (reset) is closer to the new scene than the old one; final frame < 0.6x input",
|
||||
cutFrame));
|
||||
exitCode = finite && notPassthrough && converged && resetOk ? 0 : 1;
|
||||
}
|
||||
std::string messages = "[";
|
||||
for (size_t i = 0; i < g_messages.size(); ++i)
|
||||
messages += (i ? ", " : "") + quoted(g_messages[i]);
|
||||
json_field("ffx_messages", messages + "]");
|
||||
runtime.destroyContext(&context, nullptr);
|
||||
return finish(exitCode);
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
// The synthetic ray-traced scene shared by tools/fsr_rr_probe.cpp (AMD's denoiser alone) and
|
||||
// tools/rr_backend_check.cpp (a game's NGX Ray Reconstruction request through the shim): a camera over
|
||||
// a checkered floor, three spheres and a wall, with smooth noise-free lighting signals and one-sample
|
||||
// white-noise versions of them, every guide the denoisers read, and exact 2.5D motion. Because the
|
||||
// noise-free signals are known, outputs are compared against ground truth rather than another
|
||||
// denoiser.
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace fsr_scene
|
||||
{
|
||||
|
||||
// --- small math -----------------------------------------------------------------
|
||||
|
||||
struct V3
|
||||
{
|
||||
float x, y, z;
|
||||
};
|
||||
inline V3 operator+(V3 a, V3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
||||
inline V3 operator-(V3 a, V3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
inline V3 operator*(V3 a, float s) { return {a.x * s, a.y * s, a.z * s}; }
|
||||
inline V3 mul(V3 a, V3 b) { return {a.x * b.x, a.y * b.y, a.z * b.z}; }
|
||||
inline float dot(V3 a, V3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
inline V3 cross(V3 a, V3 b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; }
|
||||
inline V3 normalize(V3 a) { return a * (1.0f / std::sqrt(dot(a, a))); }
|
||||
inline float saturate(float v) { return std::min(1.0f, std::max(0.0f, v)); }
|
||||
|
||||
inline uint16_t to_half(float value)
|
||||
{
|
||||
uint32_t bits;
|
||||
std::memcpy(&bits, &value, 4);
|
||||
const uint32_t sign = (bits >> 16) & 0x8000u;
|
||||
int exponent = static_cast<int>((bits >> 23) & 0xff) - 127 + 15;
|
||||
uint32_t mantissa = bits & 0x7fffffu;
|
||||
if (((bits >> 23) & 0xff) == 0xff)
|
||||
return static_cast<uint16_t>(sign | 0x7c00u | (mantissa ? 0x200u : 0u));
|
||||
if (exponent >= 31)
|
||||
return static_cast<uint16_t>(sign | 0x7bffu); // clamp to the largest finite half
|
||||
if (exponent <= 0)
|
||||
{
|
||||
if (exponent < -10)
|
||||
return static_cast<uint16_t>(sign);
|
||||
mantissa |= 0x800000u;
|
||||
const uint32_t shift = static_cast<uint32_t>(14 - exponent);
|
||||
uint32_t half = mantissa >> shift;
|
||||
if ((mantissa >> (shift - 1)) & 1u)
|
||||
++half;
|
||||
return static_cast<uint16_t>(sign | half);
|
||||
}
|
||||
uint32_t half = sign | (static_cast<uint32_t>(exponent) << 10) | (mantissa >> 13);
|
||||
if (mantissa & 0x1000u)
|
||||
++half;
|
||||
return static_cast<uint16_t>(half);
|
||||
}
|
||||
|
||||
inline float from_half(uint16_t half)
|
||||
{
|
||||
const uint32_t sign = (half & 0x8000u) << 16;
|
||||
uint32_t exponent = (half >> 10) & 0x1fu;
|
||||
uint32_t mantissa = half & 0x3ffu;
|
||||
uint32_t bits;
|
||||
if (exponent == 0)
|
||||
{
|
||||
if (mantissa == 0)
|
||||
bits = sign;
|
||||
else
|
||||
{
|
||||
exponent = 127 - 15 + 1;
|
||||
while ((mantissa & 0x400u) == 0)
|
||||
{
|
||||
mantissa <<= 1;
|
||||
--exponent;
|
||||
}
|
||||
bits = sign | (exponent << 23) | ((mantissa & 0x3ffu) << 13);
|
||||
}
|
||||
}
|
||||
else if (exponent == 31)
|
||||
bits = sign | 0x7f800000u | (mantissa << 13);
|
||||
else
|
||||
bits = sign | ((exponent - 15 + 127) << 23) | (mantissa << 13);
|
||||
float value;
|
||||
std::memcpy(&value, &bits, 4);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Row-major, row-vector (v' = vM) 4x4, the convention ffx_denoiser.h asks for.
|
||||
struct M4
|
||||
{
|
||||
float m[4][4];
|
||||
};
|
||||
|
||||
inline M4 view_matrix(V3 eye, V3 target)
|
||||
{
|
||||
const V3 forward = normalize(target - eye);
|
||||
const V3 right = normalize(cross({0, 1, 0}, forward));
|
||||
const V3 up = cross(forward, right);
|
||||
M4 v = {};
|
||||
v.m[0][0] = right.x, v.m[0][1] = up.x, v.m[0][2] = forward.x;
|
||||
v.m[1][0] = right.y, v.m[1][1] = up.y, v.m[1][2] = forward.y;
|
||||
v.m[2][0] = right.z, v.m[2][1] = up.z, v.m[2][2] = forward.z;
|
||||
v.m[3][0] = -dot(eye, right), v.m[3][1] = -dot(eye, up), v.m[3][2] = -dot(eye, forward), v.m[3][3] = 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
inline M4 projection_matrix(float fovY, float aspect, float zNear, float zFar)
|
||||
{
|
||||
const float ys = 1.0f / std::tan(fovY * 0.5f);
|
||||
M4 p = {};
|
||||
p.m[0][0] = ys / aspect;
|
||||
p.m[1][1] = ys;
|
||||
p.m[2][2] = zFar / (zFar - zNear);
|
||||
p.m[2][3] = 1;
|
||||
p.m[3][2] = -zNear * zFar / (zFar - zNear);
|
||||
return p;
|
||||
}
|
||||
|
||||
inline void transform(const M4& m, V3 v, float out[4])
|
||||
{
|
||||
for (int c = 0; c < 4; ++c)
|
||||
out[c] = v.x * m.m[0][c] + v.y * m.m[1][c] + v.z * m.m[2][c] + m.m[3][c];
|
||||
}
|
||||
|
||||
// --- the synthetic scene ----------------------------------------------------------
|
||||
|
||||
struct Camera
|
||||
{
|
||||
V3 eye, target;
|
||||
};
|
||||
|
||||
struct Sphere
|
||||
{
|
||||
V3 center;
|
||||
float radius;
|
||||
V3 base;
|
||||
float roughness, metal;
|
||||
};
|
||||
|
||||
inline const Sphere kSpheres[] = {
|
||||
{{0.0f, 0.2f, 6.0f}, 1.2f, {0.8f, 0.3f, 0.2f}, 0.35f, 0.0f},
|
||||
{{2.4f, -0.3f, 7.5f}, 0.7f, {0.95f, 0.9f, 0.8f}, 0.15f, 1.0f},
|
||||
{{-2.2f, -0.4f, 5.0f}, 0.6f, {0.2f, 0.5f, 0.85f}, 0.6f, 0.0f},
|
||||
};
|
||||
inline constexpr float kGroundY = -1.0f;
|
||||
inline constexpr float kWallZ = 14.0f;
|
||||
// Optional near-mirror pond in the floor (rr_backend_check --water): the reflection is the traced scene, with
|
||||
// guides as Alan Wake 2 hands them for its water (flat diffuse albedo 0.5, specular albedo ~0.96, roughness ~0.047).
|
||||
inline bool g_water = false;
|
||||
inline constexpr float kWaterRoughness = 0.047f;
|
||||
inline constexpr float kWaterMinX = -4.0f, kWaterMaxX = 4.5f, kWaterMinZ = 0.5f, kWaterMaxZ = 4.8f;
|
||||
inline const V3 kLight = normalize({0.45f, 0.9f, -0.35f});
|
||||
|
||||
struct Hit
|
||||
{
|
||||
float t = 1e30f;
|
||||
V3 position{}, normal{}, base{};
|
||||
float roughness = 1, metal = 0;
|
||||
bool valid = false, water = false;
|
||||
};
|
||||
|
||||
inline bool hit_sphere(V3 origin, V3 direction, const Sphere& s, float& t)
|
||||
{
|
||||
const V3 oc = origin - s.center;
|
||||
const float b = dot(oc, direction);
|
||||
const float c = dot(oc, oc) - s.radius * s.radius;
|
||||
const float h = b * b - c;
|
||||
if (h < 0)
|
||||
return false;
|
||||
const float root = -b - std::sqrt(h);
|
||||
if (root <= 1e-3f)
|
||||
return false;
|
||||
t = root;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Hit trace(V3 origin, V3 direction)
|
||||
{
|
||||
Hit hit;
|
||||
float t;
|
||||
for (const Sphere& s : kSpheres)
|
||||
if (hit_sphere(origin, direction, s, t) && t < hit.t)
|
||||
{
|
||||
hit.t = t;
|
||||
hit.position = origin + direction * t;
|
||||
hit.normal = normalize(hit.position - s.center);
|
||||
hit.base = s.base;
|
||||
hit.roughness = s.roughness;
|
||||
hit.metal = s.metal;
|
||||
hit.valid = true;
|
||||
}
|
||||
if (direction.y < -1e-4f)
|
||||
{
|
||||
t = (kGroundY - origin.y) / direction.y;
|
||||
if (t > 1e-3f && t < hit.t)
|
||||
{
|
||||
hit.t = t;
|
||||
hit.position = origin + direction * t;
|
||||
hit.normal = {0, 1, 0};
|
||||
const int checker = (static_cast<int>(std::floor(hit.position.x)) + static_cast<int>(std::floor(hit.position.z))) & 1;
|
||||
hit.base = checker ? V3{0.75f, 0.75f, 0.72f} : V3{0.22f, 0.24f, 0.25f};
|
||||
hit.roughness = 0.55f;
|
||||
hit.metal = 0;
|
||||
hit.valid = true;
|
||||
const V3 q = hit.position;
|
||||
if (g_water && q.x > kWaterMinX && q.x < kWaterMaxX && q.z > kWaterMinZ && q.z < kWaterMaxZ)
|
||||
{
|
||||
// small world-space ripples, so the reflection has fine detail a denoiser can lose
|
||||
hit.normal = normalize({0.035f * std::sin(5.1f * q.x + 1.3f * q.z) + 0.02f * std::sin(11.7f * q.z),
|
||||
1.0f,
|
||||
0.035f * std::cos(4.3f * q.z - 0.7f * q.x) + 0.02f * std::cos(13.1f * q.x)});
|
||||
hit.base = {0.5f, 0.5f, 0.5f};
|
||||
hit.roughness = kWaterRoughness;
|
||||
hit.water = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (direction.z > 1e-4f)
|
||||
{
|
||||
t = (kWallZ - origin.z) / direction.z;
|
||||
if (t > 1e-3f && t < hit.t)
|
||||
{
|
||||
hit.t = t;
|
||||
hit.position = origin + direction * t;
|
||||
hit.normal = {0, 0, -1};
|
||||
hit.base = {0.55f, 0.5f, 0.45f};
|
||||
hit.roughness = 0.85f;
|
||||
hit.metal = 0;
|
||||
hit.valid = true;
|
||||
}
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
inline bool shadowed(V3 position, V3 normal)
|
||||
{
|
||||
float t;
|
||||
const V3 origin = position + normal * 2e-3f;
|
||||
for (const Sphere& s : kSpheres)
|
||||
if (hit_sphere(origin, kLight, s, t))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// AMD's ApproximateBRDF (denoising.md 5.5.2, after Ray Tracing Gems ch. 32).
|
||||
inline V3 approximate_brdf(V3 f0, float alpha, float nov)
|
||||
{
|
||||
nov = std::fabs(nov);
|
||||
const float x[4] = {1, nov, nov * nov, nov * nov * nov};
|
||||
const float y[4] = {1, alpha, alpha * alpha, alpha * alpha * alpha};
|
||||
const float m1[2][2] = {{0.99044f, -1.28514f}, {1.29678f, -0.755907f}};
|
||||
const float m2[3][3] = {{1.0f, 2.92338f, 59.4188f}, {20.3225f, -27.0302f, 222.592f}, {121.563f, 626.13f, 316.627f}};
|
||||
const float m3[2][2] = {{0.0365463f, 3.32707f}, {9.0632f, -9.04756f}};
|
||||
const float m4[3][3] = {{1.0f, 3.59685f, -1.36772f}, {9.04401f, -16.3174f, 9.22949f}, {5.56589f, 19.7886f, -20.2123f}};
|
||||
// HLSL mul(M, v) with M row-major: (M v)_i = sum_j M[i][j] v_j
|
||||
const float a0 = m1[0][0] * x[0] + m1[0][1] * x[1], a1 = m1[1][0] * x[0] + m1[1][1] * x[1];
|
||||
const float xw[3] = {x[0], x[1], x[3]}, yw[3] = {y[0], y[1], y[3]};
|
||||
float b[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
b[i] = m2[i][0] * xw[0] + m2[i][1] * xw[1] + m2[i][2] * xw[2];
|
||||
const float c0 = m3[0][0] * x[0] + m3[0][1] * x[1], c1 = m3[1][0] * x[0] + m3[1][1] * x[1];
|
||||
const float xz[3] = {x[0], x[2], x[3]};
|
||||
float d[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
d[i] = m4[i][0] * xz[0] + m4[i][1] * xz[1] + m4[i][2] * xz[2];
|
||||
float bias = (a0 * y[0] + a1 * y[1]) / (b[0] * yw[0] + b[1] * yw[1] + b[2] * yw[2]);
|
||||
const float scale = (c0 * y[0] + c1 * y[1]) / (d[0] * yw[0] + d[1] * yw[1] + d[2] * yw[2]);
|
||||
bias *= saturate(f0.y * 50);
|
||||
const float s = std::max(0.0f, scale), bb = std::max(0.0f, bias);
|
||||
return {f0.x * s + bb, f0.y * s + bb, f0.z * s + bb};
|
||||
}
|
||||
|
||||
inline V3 reflect_dir(V3 direction, V3 normal) { return direction - normal * (2 * dot(direction, normal)); }
|
||||
|
||||
inline V3 environment(V3 direction)
|
||||
{
|
||||
const float up = saturate(direction.y * 0.5f + 0.5f);
|
||||
return V3{0.35f, 0.45f, 0.6f} * (0.4f + 1.2f * up);
|
||||
}
|
||||
|
||||
inline uint32_t hash32(uint32_t x)
|
||||
{
|
||||
// lowbias32, as AMD's guide recommends for white noise
|
||||
x ^= x >> 16;
|
||||
x *= 0x7feb352du;
|
||||
x ^= x >> 15;
|
||||
x *= 0x846ca68bu;
|
||||
x ^= x >> 16;
|
||||
return x;
|
||||
}
|
||||
|
||||
inline float uniform(uint32_t pixel, uint32_t frame, uint32_t stream)
|
||||
{
|
||||
return (hash32(pixel * 0x9e3779b9u ^ hash32(frame * 4u + stream + 0x1234567u)) >> 8) * (1.0f / 16777216.0f);
|
||||
}
|
||||
|
||||
enum Signal
|
||||
{
|
||||
kDirectDiffuse,
|
||||
kDirectSpecular,
|
||||
kIndirectDiffuse,
|
||||
kIndirectSpecular,
|
||||
kSignalCount
|
||||
};
|
||||
// FfxApiDenoiserSignalFlags of each signal (DIRECT_DIFFUSE 2, DIRECT_SPECULAR 4, INDIRECT_DIFFUSE 16,
|
||||
// INDIRECT_SPECULAR 32)
|
||||
inline const uint32_t kSignalFlags[kSignalCount] = {1u << 1, 1u << 2, 1u << 4, 1u << 5};
|
||||
inline const char* const kSignalNames[kSignalCount] = {"direct_diffuse", "direct_specular", "indirect_diffuse",
|
||||
"indirect_specular"};
|
||||
|
||||
struct Frame
|
||||
{
|
||||
// FSR Ray Regeneration's own inputs (fsr_rr_probe)
|
||||
std::vector<float> depth; // signed linear (view z)
|
||||
std::vector<uint16_t> motion; // RGBA16F: prevUV - curUV, prev z - cur z
|
||||
std::vector<uint32_t> normals; // R10G10B10A2: oct normal, roughness, material 0
|
||||
std::vector<uint32_t> diffuseAlbedo, specularAlbedo; // RGBA8, sqrt encoded
|
||||
std::vector<float> clean[kSignalCount]; // RGB
|
||||
std::vector<uint16_t> noisy[kSignalCount]; // RGBA16F
|
||||
std::vector<uint8_t> valid;
|
||||
std::vector<V3> diffuseLinear, specularLinear;
|
||||
// What a DLSS Ray Reconstruction game hands NGX (rr_backend_check)
|
||||
std::vector<uint16_t> color; // RGBA16F: the noisy signals remodulated and summed, sky added
|
||||
std::vector<V3> colorClean; // the same from the noise-free signals: the ground truth
|
||||
std::vector<float> hardwareDepth; // D32: NDC z of the projection (1 for the sky)
|
||||
std::vector<uint16_t> motionPixels; // RG16F: (previous - current) position in render pixels
|
||||
std::vector<uint16_t> normalsRoughness; // RGBA16F: signed world normal, w = linear roughness
|
||||
std::vector<uint32_t> diffuseAlbedoLinear, specularAlbedoLinear; // RGBA8 UNORM, linear
|
||||
};
|
||||
|
||||
inline uint32_t pack_unorm8(V3 v)
|
||||
{
|
||||
auto q = [](float f) { return static_cast<uint32_t>(std::lround(saturate(f) * 255.0f)); };
|
||||
return q(v.x) | (q(v.y) << 8) | (q(v.z) << 16) | (255u << 24);
|
||||
}
|
||||
|
||||
inline uint32_t pack_normal(V3 n, float roughness)
|
||||
{
|
||||
// AMD's NormalToOctahedronUv (denoising.md 5.3.1)
|
||||
const float l1 = std::fabs(n.x) + std::fabs(n.y) + std::fabs(n.z);
|
||||
float x = n.x / l1, y = n.y / l1;
|
||||
if (n.z < 0)
|
||||
{
|
||||
const float ox = (1.0f - std::fabs(y)) * (x >= 0 ? 1.0f : -1.0f);
|
||||
const float oy = (1.0f - std::fabs(x)) * (y >= 0 ? 1.0f : -1.0f);
|
||||
x = ox, y = oy;
|
||||
}
|
||||
auto q10 = [](float f) { return static_cast<uint32_t>(std::lround(saturate(f) * 1023.0f)); };
|
||||
return q10(x * 0.5f + 0.5f) | (q10(y * 0.5f + 0.5f) << 10) | (q10(roughness) << 20) | (0u << 30);
|
||||
}
|
||||
|
||||
// Renders one frame. (jitterX, jitterY): the sample offset from each pixel's centre in pixels (DLSS's
|
||||
// Jitter.Offset is its negation). Motion is that of the sampled surface point.
|
||||
inline void render(Frame& frame, uint32_t width, uint32_t height, const Camera& camera, const Camera& previous,
|
||||
const M4& projection, uint32_t frameIndex, float jitterX = 0.0f, float jitterY = 0.0f)
|
||||
{
|
||||
const size_t count = static_cast<size_t>(width) * height;
|
||||
frame.depth.assign(count, 0);
|
||||
frame.motion.assign(count * 4, 0);
|
||||
frame.normals.assign(count, 0);
|
||||
frame.diffuseAlbedo.assign(count, 0);
|
||||
frame.specularAlbedo.assign(count, 0);
|
||||
frame.valid.assign(count, 0);
|
||||
frame.diffuseLinear.assign(count, V3{});
|
||||
frame.specularLinear.assign(count, V3{});
|
||||
frame.color.assign(count * 4, 0);
|
||||
frame.colorClean.assign(count, V3{});
|
||||
frame.hardwareDepth.assign(count, 1.0f);
|
||||
frame.motionPixels.assign(count * 2, 0);
|
||||
frame.normalsRoughness.assign(count * 4, 0);
|
||||
frame.diffuseAlbedoLinear.assign(count, 0);
|
||||
frame.specularAlbedoLinear.assign(count, 0);
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
{
|
||||
frame.clean[s].assign(count * 3, 0);
|
||||
frame.noisy[s].assign(count * 4, 0);
|
||||
}
|
||||
const M4 view = view_matrix(camera.eye, camera.target);
|
||||
const M4 previousView = view_matrix(previous.eye, previous.target);
|
||||
const V3 forward = normalize(camera.target - camera.eye);
|
||||
const V3 right = normalize(cross({0, 1, 0}, forward));
|
||||
const V3 up = cross(forward, right);
|
||||
const float tanX = 1.0f / projection.m[0][0], tanY = 1.0f / projection.m[1][1];
|
||||
for (uint32_t py = 0; py < height; ++py)
|
||||
for (uint32_t px = 0; px < width; ++px)
|
||||
{
|
||||
const size_t i = static_cast<size_t>(py) * width + px;
|
||||
const float u = (px + 0.5f + jitterX) / width, v = (py + 0.5f + jitterY) / height;
|
||||
const float ndcX = u * 2 - 1, ndcY = 1 - v * 2;
|
||||
const V3 direction = normalize(forward + right * (ndcX * tanX) + up * (ndcY * tanY));
|
||||
const Hit hit = trace(camera.eye, direction);
|
||||
const uint32_t pixelId = static_cast<uint32_t>(i);
|
||||
frame.color[i * 4 + 3] = to_half(1.0f);
|
||||
frame.normalsRoughness[i * 4 + 2] = to_half(-1.0f);
|
||||
frame.normalsRoughness[i * 4 + 3] = to_half(1.0f);
|
||||
if (!hit.valid)
|
||||
{
|
||||
// Sky: outside linearDepthBounds, so the runtime passes it through.
|
||||
frame.depth[i] = 5000.0f;
|
||||
frame.normals[i] = pack_normal({0, 0, -1}, 1);
|
||||
const V3 sky = environment(direction);
|
||||
frame.colorClean[i] = sky;
|
||||
frame.color[i * 4 + 0] = to_half(sky.x);
|
||||
frame.color[i * 4 + 1] = to_half(sky.y);
|
||||
frame.color[i * 4 + 2] = to_half(sky.z);
|
||||
continue;
|
||||
}
|
||||
frame.valid[i] = 1;
|
||||
float clip[4];
|
||||
transform(view, hit.position, clip);
|
||||
frame.depth[i] = clip[2];
|
||||
float projected[4];
|
||||
transform(projection, {clip[0], clip[1], clip[2]}, projected);
|
||||
frame.hardwareDepth[i] = projected[2] / projected[3];
|
||||
float prevView[4], prevClip[4];
|
||||
transform(previousView, hit.position, prevView);
|
||||
const M4& p = projection;
|
||||
const V3 pv = {prevView[0], prevView[1], prevView[2]};
|
||||
transform(p, pv, prevClip);
|
||||
const float prevU = (prevClip[0] / prevClip[3]) * 0.5f + 0.5f;
|
||||
const float prevV = 0.5f - (prevClip[1] / prevClip[3]) * 0.5f;
|
||||
frame.motion[i * 4 + 0] = to_half(prevU - u);
|
||||
frame.motion[i * 4 + 1] = to_half(prevV - v);
|
||||
frame.motion[i * 4 + 2] = to_half(prevView[2] - clip[2]);
|
||||
frame.motion[i * 4 + 3] = to_half(0);
|
||||
frame.motionPixels[i * 2 + 0] = to_half((prevU - u) * width);
|
||||
frame.motionPixels[i * 2 + 1] = to_half((prevV - v) * height);
|
||||
|
||||
const V3 diffuse = hit.base * (1.0f - hit.metal);
|
||||
const V3 f0 = V3{0.04f, 0.04f, 0.04f} * (1.0f - hit.metal) + hit.base * hit.metal;
|
||||
const V3 toEye = normalize(camera.eye - hit.position);
|
||||
const float nov = std::max(dot(hit.normal, toEye), 1e-3f);
|
||||
const V3 specular = hit.water ? V3{0.96f, 0.96f, 0.96f} : approximate_brdf(f0, hit.roughness * hit.roughness, nov);
|
||||
frame.diffuseLinear[i] = diffuse;
|
||||
frame.specularLinear[i] = specular;
|
||||
frame.diffuseAlbedo[i] = pack_unorm8({std::sqrt(diffuse.x), std::sqrt(diffuse.y), std::sqrt(diffuse.z)});
|
||||
frame.specularAlbedo[i] = pack_unorm8({std::sqrt(specular.x), std::sqrt(specular.y), std::sqrt(specular.z)});
|
||||
frame.diffuseAlbedoLinear[i] = pack_unorm8(diffuse);
|
||||
frame.specularAlbedoLinear[i] = pack_unorm8(specular);
|
||||
frame.normals[i] = pack_normal(hit.normal, hit.roughness);
|
||||
frame.normalsRoughness[i * 4 + 0] = to_half(hit.normal.x);
|
||||
frame.normalsRoughness[i * 4 + 1] = to_half(hit.normal.y);
|
||||
frame.normalsRoughness[i * 4 + 2] = to_half(hit.normal.z);
|
||||
frame.normalsRoughness[i * 4 + 3] = to_half(hit.roughness);
|
||||
|
||||
// Demodulated (albedo-free) clean signals, smooth in world space.
|
||||
const float visibility = shadowed(hit.position, hit.normal) ? 0.0f : 1.0f;
|
||||
const float ndl = std::max(dot(hit.normal, kLight), 0.0f);
|
||||
const V3 reflected = direction - hit.normal * (2 * dot(direction, hit.normal));
|
||||
const float rdl = std::max(dot(reflected, kLight), 0.0f);
|
||||
const float glossExponent = 2.0f / (hit.roughness * hit.roughness * hit.roughness * hit.roughness + 0.02f);
|
||||
V3 clean[kSignalCount];
|
||||
clean[kDirectDiffuse] = V3{1.0f, 0.95f, 0.85f} * (2.5f * ndl * visibility);
|
||||
clean[kDirectSpecular] = V3{1.0f, 0.95f, 0.85f} * std::min(8.0f, 3.0f * std::pow(rdl, glossExponent) * visibility * (ndl > 0));
|
||||
const float wobble = 0.12f * std::sin(1.7f * hit.position.x) * std::sin(1.3f * hit.position.z);
|
||||
clean[kIndirectDiffuse] = environment(hit.normal) * (0.6f + wobble);
|
||||
clean[kIndirectSpecular] = environment(reflected) * (0.8f + 0.2f * (1 - hit.roughness));
|
||||
if (hit.water)
|
||||
{
|
||||
// The pond's light is its reflection: the traced scene, lit noise-free. Its diffuse lobe is
|
||||
// nearly dark despite the 0.5 guide albedo.
|
||||
clean[kDirectDiffuse] = clean[kDirectDiffuse] * 0.01f;
|
||||
clean[kIndirectDiffuse] = clean[kIndirectDiffuse] * 0.01f;
|
||||
const Hit bounce = trace(hit.position + hit.normal * 2e-3f, reflected);
|
||||
if (bounce.valid && !bounce.water)
|
||||
{
|
||||
const float bounceVisibility = shadowed(bounce.position, bounce.normal) ? 0.0f : 1.0f;
|
||||
const float bounceNdl = std::max(dot(bounce.normal, kLight), 0.0f);
|
||||
const V3 light = V3{1.0f, 0.95f, 0.85f} * (2.5f * bounceNdl * bounceVisibility) +
|
||||
environment(bounce.normal) * 0.6f;
|
||||
clean[kIndirectSpecular] = mul(bounce.base * (1.0f - bounce.metal), light) +
|
||||
environment(reflect_dir(reflected, bounce.normal)) * (0.04f + 0.9f * bounce.metal);
|
||||
}
|
||||
else
|
||||
clean[kIndirectSpecular] = environment(reflected);
|
||||
}
|
||||
V3 combined{}, combinedClean{};
|
||||
for (int s = 0; s < kSignalCount; ++s)
|
||||
{
|
||||
// One-sample-per-pixel white noise: a mean-one exponential weight shared by RGB,
|
||||
// like a single path sample's throughput.
|
||||
float sample = -std::log(std::max(1e-6f, 1.0f - uniform(pixelId, frameIndex, s)));
|
||||
if (hit.water && s == kIndirectSpecular)
|
||||
sample = 1.0f + 0.35f * (sample - 1.0f); // a mirror's reflection ray is far less noisy
|
||||
frame.clean[s][i * 3 + 0] = clean[s].x;
|
||||
frame.clean[s][i * 3 + 1] = clean[s].y;
|
||||
frame.clean[s][i * 3 + 2] = clean[s].z;
|
||||
frame.noisy[s][i * 4 + 0] = to_half(clean[s].x * sample);
|
||||
frame.noisy[s][i * 4 + 1] = to_half(clean[s].y * sample);
|
||||
frame.noisy[s][i * 4 + 2] = to_half(clean[s].z * sample);
|
||||
// A: ray hit distance for the indirect signals, any non-negative value for direct.
|
||||
float alpha = 0.0f;
|
||||
if (s == kIndirectDiffuse)
|
||||
alpha = 0.5f + 3.0f * uniform(pixelId, frameIndex, 10 + s);
|
||||
else if (s == kIndirectSpecular)
|
||||
{
|
||||
const Hit bounce = trace(hit.position + hit.normal * 2e-3f, reflected);
|
||||
alpha = bounce.valid ? std::min(bounce.t, 100.0f) : 100.0f;
|
||||
}
|
||||
frame.noisy[s][i * 4 + 3] = to_half(alpha);
|
||||
const V3 albedo = s == kDirectDiffuse || s == kIndirectDiffuse ? diffuse : specular;
|
||||
combined = combined + mul(clean[s] * sample, albedo);
|
||||
combinedClean = combinedClean + mul(clean[s], albedo);
|
||||
}
|
||||
frame.colorClean[i] = combinedClean;
|
||||
frame.color[i * 4 + 0] = to_half(combined.x);
|
||||
frame.color[i * 4 + 1] = to_half(combined.y);
|
||||
frame.color[i * 4 + 2] = to_half(combined.z);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fsr_scene
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Compare K input batching using identical synthetic frames and alternating runs.
|
||||
|
||||
Uses an existing private runtime; never installs into a game. CPU interop timing
|
||||
includes synchronization and NGX execution, and is not a GPU network-only time.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
for key in ('probe', 'shim', 'runtime', 'core', 'dlss', 'output'):
|
||||
parser.add_argument('--' + key, type=lambda s: Path(s).resolve(), required=True)
|
||||
parser.add_argument('--resolution', default='1920x1080')
|
||||
parser.add_argument('--input-resolution', help='defaults to nominal Quality (two-thirds per axis)')
|
||||
parser.add_argument('--frames', type=int, default=16)
|
||||
parser.add_argument('--profile-stages', action='store_true',
|
||||
help='diagnostic CPU stage logging; adds overhead to timings')
|
||||
parser.add_argument('--candidate-native', type=lambda s: Path(s).resolve(),
|
||||
help='compare an experimental native directory instead of input batching')
|
||||
parser.add_argument('--profile-kernels', action='store_true',
|
||||
help='diagnostic deferred GPU events, allowing legacy-stream dependencies; timings are instrumented')
|
||||
args = parser.parse_args()
|
||||
if args.frames < 8 or args.frames > 120:
|
||||
parser.error('frames must be 8..120')
|
||||
if not re.fullmatch(r'[1-9][0-9]*x[1-9][0-9]*', args.resolution):
|
||||
parser.error('resolution must be WIDTHxHEIGHT')
|
||||
ow, oh = map(int, args.resolution.split('x'))
|
||||
if not args.input_resolution:
|
||||
args.input_resolution = f'{ow * 2 // 3}x{oh * 2 // 3}'
|
||||
if not re.fullmatch(r'[1-9][0-9]*x[1-9][0-9]*', args.input_resolution):
|
||||
parser.error('input-resolution must be WIDTHxHEIGHT')
|
||||
build_root = Path(__file__).resolve().parents[2] / 'build'
|
||||
if not args.output.is_relative_to(build_root.resolve()):
|
||||
parser.error('output must stay under the repository ignored build directory')
|
||||
args.output.mkdir(parents=True, exist_ok=False)
|
||||
report = {'passed': False, 'resolution': args.resolution, 'warmupFrames': 4,
|
||||
'inputResolution': args.input_resolution,
|
||||
'stageProfiling': args.profile_stages,
|
||||
'kernelProfiling': args.profile_kernels,
|
||||
'comparison': 'native kernels' if args.candidate_native else 'input batching',
|
||||
'timing': 'CPU interop including NGX and synchronization', 'runs': []}
|
||||
runtime = args.runtime
|
||||
native = runtime / 'native'
|
||||
if (native / 'gfx1101').is_dir():
|
||||
native = native / 'gfx1101'
|
||||
nvapi = runtime / 'nvapi/nvapi64.dll'
|
||||
if not nvapi.is_file():
|
||||
nvapi = runtime / 'nvapi/gfx1101/nvapi64.dll'
|
||||
report['artifacts'] = {str(p): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in (args.probe, args.shim, args.core, args.dlss)}
|
||||
report['nativeArtifacts'] = {p.name: hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in sorted(native.glob('*')) if p.is_file()}
|
||||
if args.candidate_native:
|
||||
report['candidateNativeArtifacts'] = {p.name: hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in sorted(args.candidate_native.glob('*')) if p.is_file()}
|
||||
env = os.environ.copy()
|
||||
# Keep profiling and arithmetic settings independent of the caller's shell.
|
||||
for key in list(env):
|
||||
if key.startswith('D4R_') or key in ('ZLUDA_LOG_DIR', 'ZLUDA_CACHE_DIR', 'ZLUDA_CUDA_LIB'):
|
||||
del env[key]
|
||||
env.update(HIP_PATH=str(runtime / 'hip'),
|
||||
ZLUDA_CUDA_LIB=str(runtime / 'zluda/nvcuda.dll'),
|
||||
D4R_NVAPI_BACKEND=str(runtime / 'nvapi/backend/nvapi64.dll'),
|
||||
D4R_FORMAT_MODULE=str(runtime / 'pixel_convert_gfx1101.hsaco'),
|
||||
D4R_ZLUDA_NATIVE_DIR=str(native),
|
||||
D4R_ZLUDA_WMMA='1', D4R_ZLUDA_WMMA_FP8='1',
|
||||
D4R_ZLUDA_WMMA_F16_REFERENCE='1', D4R_QUIET_API='1',
|
||||
ZLUDA_CACHE_DIR=str(Path(os.environ['LOCALAPPDATA']) / 'd4r/cache'),
|
||||
D4R_NGX_DATA_DIR=str(args.output / 'ngx-data'))
|
||||
env['PATH'] = str(runtime / 'hip/bin') + os.pathsep + str(runtime / 'zluda') + os.pathsep + env['PATH']
|
||||
if args.profile_stages:
|
||||
env['D4R_PROFILE_STAGES'] = '1'
|
||||
if args.profile_kernels:
|
||||
env.update(D4R_ZLUDA_PROFILE_DEFERRED='1', D4R_ZLUDA_PROFILE_ALLOW_LEGACY='1',
|
||||
D4R_ZLUDA_PROFILE_EVERY='1')
|
||||
try:
|
||||
reference = None
|
||||
for index, batched in enumerate((False, True, True, False)):
|
||||
label = ('native-candidate' if args.candidate_native else 'batch') if batched else 'control'
|
||||
output = args.output / f'{index}-{label}'
|
||||
output.mkdir()
|
||||
env['D4R_DIAG_DIR'] = str(output)
|
||||
if batched and not args.candidate_native:
|
||||
env['D4R_BATCH_INPUT_COPIES'] = '1'
|
||||
else:
|
||||
env.pop('D4R_BATCH_INPUT_COPIES', None)
|
||||
env['D4R_ZLUDA_NATIVE_DIR'] = str(args.candidate_native if batched and args.candidate_native else native)
|
||||
command = [str(args.probe), '--module', str(args.shim), '--hip-root', str(runtime / 'hip'),
|
||||
'--cuda-dll', str(runtime / 'zluda/nvcuda.dll'), '--ngx-core', str(args.core),
|
||||
'--dlss-dll', str(args.dlss), '--nvapi-dll', str(nvapi),
|
||||
'--ngx-abi', 'project', '--interop-mode', 'command-list', '--preset', '11',
|
||||
'--ngx-output-resolution', args.resolution,
|
||||
'--ngx-input-resolution', args.input_resolution, '--iterations', str(args.frames)]
|
||||
with (output / 'stdout.log').open('wb') as out, (output / 'stderr.log').open('wb') as err:
|
||||
result = subprocess.run(command, env=env, stdout=out, stderr=err, timeout=180)
|
||||
text = (output / 'stdout.log').read_text(errors='replace')
|
||||
if result.returncode or 'PASS D3D12_DLSS' not in text:
|
||||
raise RuntimeError(f'run {index} failed ({result.returncode}); inspect {output}')
|
||||
iw, ih = map(int, args.input_resolution.split('x'))
|
||||
expected_size = f'D3D12_NGX_DIMENSIONS input={iw}x{ih} output={ow}x{oh}'
|
||||
if expected_size not in text or f'D4R_INTEROP async=0 batch_inputs={int(batched and not args.candidate_native)}' not in text:
|
||||
raise RuntimeError(f'run {index}: requested dimensions/interop mode not applied')
|
||||
if len(re.findall(r'D3D12_OUTPUT preset=11 frame=\d+ finite=1 ', text)) != args.frames:
|
||||
raise RuntimeError(f'run {index}: missing finite-output checks')
|
||||
times = [float(x) for x in re.findall(r'D4R_FRAME .*?interop_ngx_ms=([0-9.]+)', text)]
|
||||
if len(times) != args.frames:
|
||||
raise RuntimeError(f'run {index}: incomplete frame timing')
|
||||
hashes = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in output.glob('output-*.rgba16f')}
|
||||
if len(hashes) != args.frames:
|
||||
raise RuntimeError(f'run {index}: incomplete output readback')
|
||||
if reference is None:
|
||||
reference = hashes
|
||||
elif hashes != reference:
|
||||
raise RuntimeError(f'run {index}: output differs from control')
|
||||
report['runs'].append({'batch': batched and not bool(args.candidate_native),
|
||||
'candidate': batched, 'allFrameMs': times,
|
||||
'medianMs': statistics.median(times[4:]), 'outputHashes': hashes})
|
||||
print(f'run={index} variant={label} warmed_cpu_interop_ms={statistics.median(times[4:]):.3f}', flush=True)
|
||||
for mode in (False, True):
|
||||
values = [t for row in report['runs'] if row['candidate'] == mode for t in row['allFrameMs'][4:]]
|
||||
key = ('candidateMedianMs' if args.candidate_native else 'batchMedianMs') if mode else 'controlMedianMs'
|
||||
report[key] = statistics.median(values)
|
||||
report['passed'] = True
|
||||
except Exception as error:
|
||||
report['error'] = str(error)
|
||||
print(str(error), flush=True)
|
||||
finally:
|
||||
(args.output / 'summary.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
return 0 if report['passed'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -14,6 +14,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from gpu_targets import TARGETS
|
||||
from llvm22_ir import compatible_ir
|
||||
|
||||
|
||||
def require_code_object_target(path, target):
|
||||
@@ -39,6 +40,8 @@ def main():
|
||||
parser.error('this private experimental build recipe supports K v2 input/output variants only')
|
||||
repo = pathlib.Path(__file__).resolve().parents[2]
|
||||
output = args.output_directory.resolve()
|
||||
if not output.is_relative_to((repo / 'build').resolve()):
|
||||
parser.error('private texture outputs must stay under the repository ignored build directory')
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
for existing in output.glob('*.hsaco'):
|
||||
require_code_object_target(existing, args.gpu_arch)
|
||||
@@ -46,8 +49,16 @@ def main():
|
||||
work.mkdir(exist_ok=True)
|
||||
hip = args.hip_root.resolve()
|
||||
zluda = args.zluda_root.resolve()
|
||||
tools = {name: hip / 'bin' / (name + '.exe') for name in ('clang++', 'llvm-dis', 'llvm-as')}
|
||||
tools = {}
|
||||
for name in ('clang++', 'llvm-dis', 'llvm-as'):
|
||||
candidates = [hip / folder / (name + '.exe') for folder in ('lib/llvm/bin', 'bin')]
|
||||
tools[name] = next((path for path in candidates if path.is_file()), candidates[0])
|
||||
emitter = zluda / 'd4r_emit.exe'
|
||||
# The emitter's LLVM reader must consume the helper bitcode. A newer HIP
|
||||
# compiler can emit textual IR, but its bitcode attributes may be unreadable.
|
||||
emitter_assembler = zluda / 'llvm-as.exe'
|
||||
if emitter_assembler.is_file():
|
||||
tools['llvm-as'] = emitter_assembler
|
||||
for path in [args.dlss_dll, emitter, *tools.values()]:
|
||||
if not path.is_file():
|
||||
parser.error(f'missing dependency: {path}')
|
||||
@@ -63,7 +74,8 @@ def main():
|
||||
|
||||
def run(label, command):
|
||||
with (work / (label + '.log')).open('wb') as log:
|
||||
result = subprocess.run([str(arg) for arg in command], env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
result = subprocess.run([str(arg) for arg in command], env=env, stdout=log,
|
||||
stderr=subprocess.STDOUT, timeout=600)
|
||||
if result.returncode:
|
||||
print((work / (label + '.log')).read_text(errors='replace')[-8000:], file=sys.stderr)
|
||||
raise RuntimeError(f'{label} failed ({result.returncode}); see {work}')
|
||||
@@ -89,11 +101,16 @@ def main():
|
||||
run('disassemble', [tools['llvm-dis'], raw, '-o', ir])
|
||||
lines = []
|
||||
for line in ir.read_text().splitlines():
|
||||
# Helpers are linked directly into the translated module. Their ThinLTO
|
||||
# index becomes stale when the unused HIP/LLVM globals below are removed.
|
||||
if re.match(r'^\^\d+\s*=', line):
|
||||
continue
|
||||
if re.search(r'@llvm.used|wchar_size|llvm.module.flags|__hip_cuid', line):
|
||||
continue
|
||||
line = line.replace('optnone', '').replace(f'"target-cpu"="{args.gpu_arch}"', '')
|
||||
lines.append(re.sub(r'"target-features"="[^"]+"', '', line))
|
||||
ir.write_text('\n'.join(lines) + '\n')
|
||||
ir.write_text(compatible_ir('\n'.join(lines) + '\n') if emitter_assembler.is_file()
|
||||
else '\n'.join(lines) + '\n')
|
||||
run('assemble', [tools['llvm-as'], ir, '-o', extra])
|
||||
env['D4R_ZLUDA_EXTRA_BC'] = str(extra)
|
||||
run('emit', [emitter, edited, work / 'emitted', args.gpu_arch])
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Build exact gfx11 RR D/E models natively on Windows, without Git Bash.
|
||||
|
||||
Derived weights/shaders remain under ignored build/. Nothing is installed into
|
||||
a game or bundled. Uses the same packers and compiler recipe as the Linux shell.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def sha(path):
|
||||
with path.open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--dll', required=True, type=lambda p: Path(p).resolve())
|
||||
parser.add_argument('--preset', choices=('D', 'E'), default='D')
|
||||
parser.add_argument('--output', required=True, type=lambda p: Path(p).resolve())
|
||||
parser.add_argument('--glslang', type=lambda p: Path(p).resolve(), default=repo / '.tools/glslang-16.6.0/bin/glslangValidator.exe')
|
||||
parser.add_argument('--reuse', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if not args.output.is_relative_to((repo / 'build').resolve()):
|
||||
parser.error('derived models must stay under ignored build/')
|
||||
if sha(args.dll) != '4bc7ea5fcb2f32cf86bc2cb072e8d2860914a0be511cbdcb2fc206c1d9d83b80':
|
||||
parser.error('requires pinned NVIDIA RR 310.9.1 DLL')
|
||||
if sha(args.glslang) != '9643709c02c83e26773f96b6a87f197ab2e84f78be0879ad8fb3619418b5d9ad':
|
||||
parser.error('requires pinned glslang 16.6.0 executable')
|
||||
env = os.environ.copy()
|
||||
for key in tuple(env):
|
||||
if key.startswith('D4R_RR_'):
|
||||
del env[key]
|
||||
env.update(D4R_RR_GFX11='1', D4R_RR_RELAXED='0', PYTHONPATH=str(repo / '.tools/python/vendor'))
|
||||
env['PATH'] = str(args.glslang.parent) + os.pathsep + env['PATH']
|
||||
numpy_version = subprocess.check_output([sys.executable, '-c', 'import numpy; print(numpy.__version__)'], env=env, text=True, timeout=30).strip()
|
||||
sources = {str(Path(__file__).relative_to(repo)).replace('\\', '/'): sha(Path(__file__))}
|
||||
for directory in ('engine', 'vulkan', 'kernels/tools'):
|
||||
for path in sorted((repo / directory).rglob('*')):
|
||||
if path.is_file() and path.suffix in ('.py', '.comp', '.glsl', '.h', '.inc', '.sh', '.txt'):
|
||||
sources[path.relative_to(repo).as_posix()] = sha(path)
|
||||
identity = {'schema': 1, 'preset': args.preset, 'architecture': 'gfx1101', 'arithmetic': 'exact-gfx11',
|
||||
'dll': sha(args.dll), 'glslang': sha(args.glslang), 'python': sha(Path(sys.executable)),
|
||||
'numpy': numpy_version, 'sources': sources}
|
||||
stamp = args.output / 'windows-native-python-cache.json'
|
||||
def products():
|
||||
return {p.name: sha(p) for p in sorted(args.output.iterdir()) if p.is_file() and p.name != stamp.name}
|
||||
if args.output.exists():
|
||||
if not args.reuse:
|
||||
parser.error('use a fresh output directory or --reuse')
|
||||
cached = json.loads(stamp.read_text())
|
||||
if cached['identity'] != identity or cached['products'] != products():
|
||||
raise RuntimeError('RR cache changed; use a fresh output directory')
|
||||
print('RR_NATIVE_CACHE_HIT preset=' + args.preset)
|
||||
return
|
||||
args.output.mkdir(parents=True)
|
||||
out = args.output
|
||||
work = out / 'native-work'
|
||||
work.mkdir()
|
||||
rr = repo / 'engine/rr'
|
||||
serial = 0
|
||||
def run(command, label):
|
||||
nonlocal serial
|
||||
serial += 1
|
||||
print(f'RR_BUILD step={serial} {label}', flush=True)
|
||||
log = work / f'{serial:02}-{label}.log'
|
||||
with log.open('wb') as stream:
|
||||
process = subprocess.Popen([str(p) for p in command], cwd=repo, env=env, stdout=stream, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
code = process.wait(timeout=600)
|
||||
except subprocess.TimeoutExpired:
|
||||
if os.name == 'nt':
|
||||
subprocess.run(['taskkill', '/PID', str(process.pid), '/T', '/F'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30)
|
||||
else:
|
||||
process.kill()
|
||||
process.wait(timeout=30)
|
||||
raise RuntimeError('RR build timed out; see ' + str(log))
|
||||
if code:
|
||||
raise RuntimeError(f'RR build failed ({code}); see {log}')
|
||||
return log.read_text(errors='replace').strip()
|
||||
def python(script, *arguments, label):
|
||||
return run([sys.executable, script, *arguments], label)
|
||||
python(repo / 'kernels/tools/extract_dlss_ptx.py', args.dll, work / 'ptx', label='extract')
|
||||
python(repo / 'engine/compile_rr.py', '--dll', args.dll, '--preset', args.preset, out,
|
||||
'--ptx-dir', work / 'ptx', '--network-bindings-only', label='bindings')
|
||||
python(rr / 'dll_weights.py', args.dll, work / 'dll', '--model', '0' if args.preset == 'D' else '1', label='weights')
|
||||
manifest = (out / 'rr.txt').read_text().replace(f'preset {4 if args.preset == "D" else 5}\n',
|
||||
f'preset {4 if args.preset == "D" else 5}\narithmetic gfx11\n')
|
||||
defines = ['-DRR_EXACT_CONVERT', '-DRR_GFX11'] + (['-DRR_PRESET_D'] if args.preset == 'D' else [])
|
||||
def native(stage, shader, include):
|
||||
nonlocal manifest
|
||||
weights = out / f'weights-{stage}m.bin'
|
||||
wide = int(python(rr / 'widen_weights.py', weights, label='widen-' + stage).splitlines()[-1])
|
||||
run([args.glslang, '--target-env', 'vulkan1.3', '-V', *defines, f'-DW_WIDE={wide}u',
|
||||
'-I' + str(include), '-I' + str(rr), '-o', out / f'{stage}_m.spv', shader], 'compile-' + stage)
|
||||
python(rr / 'spv_exact_convert.py', out / f'{stage}_m.spv', label='exact-' + stage)
|
||||
pattern = rf'^stage {stage} {stage}\.spv 32 weights-{stage}\.bin (.*)$'
|
||||
def replacement(match):
|
||||
tail = match[1].removesuffix(' noopt')
|
||||
return f'stage {stage} {stage}_m.spv 32 weights-{stage}m.bin {tail}'
|
||||
manifest, count = re.subn(pattern, replacement, manifest, flags=re.MULTILINE)
|
||||
if count != 1:
|
||||
raise RuntimeError('Missing/duplicate native stage: ' + stage)
|
||||
for stage in ('enc1', 'enc2', 'enc3', 'enc4', 'enc5', 'dec4', 'dec3', 'dec2', 'dec1'):
|
||||
directory = work / stage
|
||||
directory.mkdir()
|
||||
shutil.copyfile(work / 'dll' / f'{stage}.npz', directory / 'weights.npz')
|
||||
python(rr / 'swin_pack.py', stage, directory / 'weights.npz', out / f'weights-{stage}m.bin', directory / 'swin_stage.h', label='pack-' + stage)
|
||||
native(stage, rr / 'swin_m.comp', directory)
|
||||
for stage in ('dec0', 'enc0'):
|
||||
directory = work / stage
|
||||
directory.mkdir()
|
||||
shutil.copyfile(work / 'dll' / f'{stage}.npz', directory / 'weights.npz')
|
||||
if stage == 'dec0':
|
||||
python(rr / 'dec0_pack.py', directory / 'weights.npz', out / 'weights-dec0m.bin', directory / 'dec0_m_w.h', label='pack-dec0')
|
||||
native(stage, rr / 'dec0_m.comp', directory)
|
||||
else:
|
||||
base = 0 if args.preset == 'D' else 4096
|
||||
python(rr / 'enc0_pack.py', directory / 'weights.npz', directory / 'enc0_m.bin', directory / 'enc0_m_w.h', str(base), label='pack-enc0')
|
||||
noise = b'' if not base else (work / 'dll/noise.bin').read_bytes()
|
||||
if len(noise) != base:
|
||||
raise RuntimeError('Invalid E noise table')
|
||||
(out / 'weights-enc0m.bin').write_bytes(noise + (directory / 'enc0_m.bin').read_bytes())
|
||||
python(rr / 'gen_enc0_m.py', out / 'enc0.generated.comp', directory / 'enc0_m.comp', label='gather-enc0')
|
||||
native(stage, directory / 'enc0_m.comp', directory)
|
||||
metadata = json.loads((out / 'model.json').read_text())
|
||||
if metadata['stages']['hkpn'].get('shader') == 'generated from the DLL':
|
||||
manifest = manifest.replace('stage hkpn hkpn.spv 32 ', 'stage hkpn hkpn.spv 64 ')
|
||||
for stage in ('enc0', 'enc1', 'enc2', 'enc3', 'enc4', 'enc5', 'dec4', 'dec3', 'dec2', 'dec1', 'dec0'):
|
||||
path = out / f'weights-{stage}.bin'
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
(out / 'rr.txt').write_text(manifest, newline='\n')
|
||||
stages = [line.split() for line in manifest.splitlines() if line.startswith('stage ')]
|
||||
if len(stages) != 12 or 'arithmetic gfx11' not in manifest:
|
||||
raise RuntimeError('Incomplete native manifest')
|
||||
for fields in stages:
|
||||
if not (out / fields[2]).is_file() or not (out / fields[4]).is_file():
|
||||
raise RuntimeError('Missing stage product')
|
||||
textures = [] if fields[5] == '-' else list(map(int, fields[5].split(',')))
|
||||
surfaces = [] if fields[6] == '-' else [[int(s.split(':')[0]), s.split(':')[1]] for s in fields[6].split(',')]
|
||||
(out / fields[2]).with_suffix('.json').write_text(json.dumps({'textures': textures, 'surfaces': surfaces}) + '\n')
|
||||
stamp.write_text(json.dumps({'identity': identity, 'products': products()}, indent=2) + '\n')
|
||||
print('RR_NATIVE_MODEL_BUILT preset=' + args.preset + ' output=' + str(out))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -29,7 +29,7 @@ int main(int argc, char** argv) {
|
||||
for (const auto& pair : {std::make_pair("D4R_HIP_ROOT", args.hip_root),
|
||||
std::make_pair("D4R_NVCUDA_DLL", args.cuda_dll), std::make_pair("D4R_NVAPI_DLL", args.nvapi_dll),
|
||||
std::make_pair("D4R_NGX_CORE", args.ngx_core), std::make_pair("D4R_DLSS_DLL", args.dlss_dll)})
|
||||
if (_putenv_s(pair.first, pair.second.c_str())) throw std::runtime_error("Cannot set runtime environment");
|
||||
if (_putenv_s(pair.first, args.player_mode ? "" : pair.second.c_str())) throw std::runtime_error("Cannot set runtime environment");
|
||||
HipApi hip(args.hip_root); hipDeviceProp_t props{}; hip.select_architecture(args.device, props);
|
||||
ComPtr<IDXGIFactory4> factory; dx(CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf())), "CreateDXGIFactory1");
|
||||
ComPtr<IDXGIAdapter1> adapter;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Native Windows backend. The Linux/Wine implementation remains separate.
|
||||
#include "ngx_windows_runtime.h"
|
||||
#include "ngx_runtime_initialization.h"
|
||||
#include "player_config.h"
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
#include "rr_native_backend.h"
|
||||
#endif
|
||||
#include <dxgi.h>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
@@ -8,9 +12,102 @@
|
||||
namespace {
|
||||
std::mutex apiMutex;
|
||||
std::shared_ptr<d4r::win::Runtime> runtime;
|
||||
std::shared_ptr<d4r::win::RuntimeInitialization> initialization;
|
||||
bool deferredSr = false;
|
||||
void ensure_sr_runtime() {
|
||||
if (!runtime) {
|
||||
if (!initialization) throw std::runtime_error("NGX is not initialized");
|
||||
runtime = initialization->create();
|
||||
}
|
||||
}
|
||||
void initialize(ID3D12Device* device, unsigned long long app, const wchar_t* data, unsigned sdk,
|
||||
const d4r::ngx::FeatureCommonInfo* common, const d4r::ngx::ProjectIdentity* project = nullptr) {
|
||||
if (initialization) {
|
||||
if (initialization->device.Get() != device) throw std::runtime_error("NGX already initialized on another device");
|
||||
return;
|
||||
}
|
||||
auto candidate = std::make_shared<d4r::win::RuntimeInitialization>(device, app, data, sdk, common, project);
|
||||
bool native = false;
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
const char* backend = std::getenv("D4R_RR_BACKEND"); native = backend && std::string(backend) == "native";
|
||||
#endif
|
||||
// Default SR startup remains eager. Native RR owns its Vulkan execution
|
||||
// independently; SR initializes only when its capabilities/feature are used.
|
||||
if (!native) runtime = candidate->create();
|
||||
initialization = std::move(candidate); deferredSr = native;
|
||||
}
|
||||
struct Handle { unsigned Id; };
|
||||
unsigned nextId = 1;
|
||||
std::unordered_map<Handle*, std::shared_ptr<d4r::win::Feature>> features;
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
struct RrFeature {
|
||||
d4r::win::rr::Creation creation;
|
||||
d4r::win::rr::Backend backend;
|
||||
RrFeature(ID3D12Device* device, void* parameters, const std::string& directory)
|
||||
: creation(parameters), backend(device, creation, directory) {}
|
||||
};
|
||||
std::unordered_map<Handle*, std::shared_ptr<RrFeature>> rrFeatures;
|
||||
std::string rr_directory() {
|
||||
const char* backend = std::getenv("D4R_RR_BACKEND");
|
||||
if (!backend || std::string(backend) != "native")
|
||||
throw std::runtime_error("Native RR requires D4R_RR_BACKEND=native");
|
||||
return d4r::win::env_path("D4R_RR_MODEL_DIR");
|
||||
}
|
||||
bool rr_ready(ID3D12Device* device) {
|
||||
try {
|
||||
const auto directory = rr_directory();
|
||||
void* p = d4r_ngx_parameters_create();
|
||||
if (!p) return false;
|
||||
struct Cleanup { void* p; ~Cleanup() { d4r_ngx_parameters_destroy(p); } } cleanup{p};
|
||||
for (const char* name : {"Width", "Height", "OutWidth", "OutHeight"}) d4r_ngx_set_uint(p, name, 64);
|
||||
d4r_ngx_set_uint(p, "DLSS.Feature.Create.Flags", 3);
|
||||
d4r::win::rr::Creation creation(p);
|
||||
// Readiness checks the actual configured model's pipeline/weight load,
|
||||
// adapter features and packet/fence import. No synthetic frame runs.
|
||||
d4r::win::rr::Backend candidate(device, creation, directory);
|
||||
return true;
|
||||
} catch (const std::exception& error) {
|
||||
if (std::getenv("D4R_RR_BACKEND")) std::fprintf(stderr, "D4R_RR_UNAVAILABLE %s\n", error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
struct RrSnapshot {
|
||||
d4r::win::rr::Snapshot frame;
|
||||
std::array<d4r::win::ResourceAccess, d4r::win::rr::Snapshot::Count> access;
|
||||
std::array<bool, d4r::win::rr::Snapshot::Count> explicitState{};
|
||||
RrSnapshot(const d4r::win::rr::Creation& creation, void* p, ID3D12GraphicsCommandList* list)
|
||||
: frame(creation, p) {
|
||||
const char* names[] = {"D4R.Color.State", "D4R.Motion.State", "D4R.Depth.State", "D4R.Normals.State",
|
||||
"D4R.Roughness.State", "D4R.Diffuse.State", "D4R.Specular.State", "D4R.Output.State", "D4R.Exposure.State",
|
||||
"D4R.HitDistance.State", "D4R.Disocclusion.State", "D4R.Subsurface.State", "D4R.BeforeParticles.State"};
|
||||
for (size_t i = 0; i < access.size(); ++i) {
|
||||
if (!frame.resources[i]) {
|
||||
if (i < 8) throw std::runtime_error("Missing required NGX RR guide/output");
|
||||
continue;
|
||||
}
|
||||
const auto fallback = d4r::win::ngx_resource_access(p, names[i], i == 7 ?
|
||||
D3D12_RESOURCE_STATE_UNORDERED_ACCESS : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
|
||||
unsigned declared = 0;
|
||||
explicitState[i] = d4r_ngx_get_uint(p, names[i], &declared) == 1;
|
||||
access[i] = list ? d4r::win::commands::resource_access(list, frame.resources[i].Get(), fallback) :
|
||||
d4r::win::commands::submitted_resource_access(frame.resources[i].Get(), fallback);
|
||||
if (access[i].pending_split) throw std::runtime_error("NGX RR texture has an unfinished split barrier");
|
||||
}
|
||||
if (frame.settings.roughnessPacked) {
|
||||
access[4] = access[3]; explicitState[4] = explicitState[3];
|
||||
}
|
||||
}
|
||||
void resolve() {
|
||||
for (size_t i = 0; i < access.size(); ++i) if (access[i].inherited) {
|
||||
access[i] = d4r::win::commands::submitted_resource_access(frame.resources[i].Get(), access[i]);
|
||||
// Explicit caller state remains valid when no prior submitted list
|
||||
// supplied a tracked transition for this resource.
|
||||
if (access[i].inherited && explicitState[i]) access[i].inherited = false;
|
||||
if (access[i].pending_split || access[i].inherited) throw std::runtime_error("NGX RR inherited texture state unresolved");
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif
|
||||
std::unordered_map<void*, bool> parameters;
|
||||
constexpr unsigned failure = 0xBAD00002, invalid = 0xBAD00005;
|
||||
unsigned call(const std::function<unsigned()>& fn) noexcept {
|
||||
@@ -27,7 +124,7 @@ unsigned call(const std::function<unsigned()>& fn) noexcept {
|
||||
}
|
||||
}
|
||||
unsigned allocate(void** out, bool caps) {
|
||||
if (!runtime || !out) return invalid;
|
||||
if (!initialization || !out) return invalid;
|
||||
void* value = d4r_ngx_parameters_create();
|
||||
if (!value) return failure;
|
||||
*out = nullptr;
|
||||
@@ -36,6 +133,15 @@ unsigned allocate(void** out, bool caps) {
|
||||
~LocalParameters() { if (p) d4r_ngx_parameters_destroy(p); }
|
||||
} owned{value};
|
||||
if (caps) {
|
||||
try { ensure_sr_runtime(); }
|
||||
catch (const std::exception& error) {
|
||||
if (!deferredSr) throw;
|
||||
std::fprintf(stderr, "D4R_SR_UNAVAILABLE %s\n", error.what());
|
||||
d4r_ngx_set_int(value, "SuperSampling.Available", 0);
|
||||
d4r_ngx_set_int(value, "SuperSampling.NeedsUpdatedDriver", 0);
|
||||
d4r_ngx_set_int(value, "SuperSampling.FeatureInitResult", int(failure));
|
||||
}
|
||||
if (runtime) {
|
||||
std::lock_guard<std::mutex> lock(runtime->mutex);
|
||||
runtime->current(); void* official = nullptr;
|
||||
d4r::win::ngx_check(runtime->capabilities(&official), "CUDA capabilities");
|
||||
@@ -48,6 +154,14 @@ unsigned allocate(void** out, bool caps) {
|
||||
void* callback = nullptr;
|
||||
if (d4r_ngx_get_void(official, name, &callback) == 1) d4r_ngx_set_void(value, name, callback);
|
||||
}
|
||||
}
|
||||
bool rrAvailable = false;
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
rrAvailable = rr_ready(initialization->device.Get());
|
||||
#endif
|
||||
d4r_ngx_set_int(value, "SuperSamplingDenoising.Available", rrAvailable ? 1 : 0);
|
||||
d4r_ngx_set_int(value, "SuperSamplingDenoising.NeedsUpdatedDriver", 0);
|
||||
d4r_ngx_set_int(value, "SuperSamplingDenoising.FeatureInitResult", rrAvailable ? 1 : int(0xBAD00001u));
|
||||
}
|
||||
parameters.emplace(value, true); *out = value; owned.p = nullptr;
|
||||
return 1;
|
||||
@@ -61,7 +175,7 @@ API unsigned NVSDK_NGX_D3D12_Init_Ext(unsigned long long app, const wchar_t* dat
|
||||
std::lock_guard<std::mutex> lock(apiMutex);
|
||||
return call([&] {
|
||||
d4r::win::player::ensure_environment();
|
||||
if (!runtime) runtime = std::make_shared<d4r::win::Runtime>(device, app, data, sdk, static_cast<const d4r::ngx::FeatureCommonInfo*>(info));
|
||||
initialize(device, app, data, sdk, static_cast<const d4r::ngx::FeatureCommonInfo*>(info));
|
||||
if (const char* backend = std::getenv("D4R_D3D12_COMMAND_BACKEND"); backend && std::string(backend) == "1")
|
||||
d4r::win::commands::install(device);
|
||||
return 1u;
|
||||
@@ -76,7 +190,7 @@ API unsigned NVSDK_NGX_D3D12_Init_ProjectID(const char* project, int engine, con
|
||||
if (!project || !*project) return invalid;
|
||||
d4r::win::player::ensure_environment();
|
||||
d4r::ngx::ProjectIdentity identity{project, engine, version ? version : ""};
|
||||
if (!runtime) runtime = std::make_shared<d4r::win::Runtime>(device, 0, data, sdk, static_cast<const d4r::ngx::FeatureCommonInfo*>(info), &identity);
|
||||
initialize(device, 0, data, sdk, static_cast<const d4r::ngx::FeatureCommonInfo*>(info), &identity);
|
||||
if (const char* backend = std::getenv("D4R_D3D12_COMMAND_BACKEND"); backend && std::string(backend) == "1") d4r::win::commands::install(device);
|
||||
return 1u;
|
||||
});
|
||||
@@ -86,7 +200,19 @@ API unsigned NVSDK_NGX_D3D12_GetFeatureRequirements(IDXGIAdapter* adapter, const
|
||||
if (!adapter || !info || !requirements) return invalid;
|
||||
return call([&] {
|
||||
*requirements = {}; requirements->FeatureSupported = 16; // Unsupported feature.
|
||||
if (info->FeatureID != 1) return 1u;
|
||||
if (info->FeatureID != 1) {
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
if (info->FeatureID == 13) {
|
||||
d4r::win::player::ensure_environment();
|
||||
d4r::win::ComPtr<ID3D12Device> device;
|
||||
if (SUCCEEDED(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device))) && rr_ready(device.Get())) {
|
||||
requirements->FeatureSupported = 0;
|
||||
std::strcpy(requirements->MinOSVersion, "10.0.22000.0");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return 1u;
|
||||
}
|
||||
d4r::win::player::ensure_environment();
|
||||
DXGI_ADAPTER_DESC desc{}; d4r::win::dx(adapter->GetDesc(&desc), "NGX requirements adapter");
|
||||
// OptiScaler can override the DXGI vendor ID. The physical HIP
|
||||
@@ -119,7 +245,18 @@ API unsigned NVSDK_NGX_D3D12_GetScratchBufferSize(unsigned, const void*, size_t*
|
||||
API unsigned NVSDK_NGX_D3D12_CreateFeature(ID3D12GraphicsCommandList*, unsigned feature, void* p, Handle** out) {
|
||||
std::lock_guard<std::mutex> lock(apiMutex);
|
||||
return call([&] {
|
||||
if (!runtime || feature != 1 || !p || !out) return invalid;
|
||||
if (!initialization || !p || !out) return invalid;
|
||||
*out = nullptr;
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
if (feature == 13) {
|
||||
auto value = std::make_shared<RrFeature>(initialization->device.Get(), p, rr_directory());
|
||||
d4r::win::commands::install(initialization->device.Get());
|
||||
auto handle = std::make_unique<Handle>(); handle->Id = nextId++;
|
||||
rrFeatures.emplace(handle.get(), std::move(value)); *out = handle.release(); return 1u;
|
||||
}
|
||||
#endif
|
||||
if (feature != 1) return invalid;
|
||||
ensure_sr_runtime();
|
||||
d4r::win::player::apply_preset(p);
|
||||
auto value = std::make_shared<d4r::win::Feature>(runtime, p);
|
||||
auto handle = std::make_unique<Handle>(); handle->Id = nextId++;
|
||||
@@ -127,12 +264,37 @@ API unsigned NVSDK_NGX_D3D12_CreateFeature(ID3D12GraphicsCommandList*, unsigned
|
||||
});
|
||||
}
|
||||
API unsigned d4r_D3D12_EvaluateAtBoundary(ID3D12CommandQueue* queue, Handle* handle, void* p) {
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
std::shared_ptr<RrFeature> rr;
|
||||
{ std::lock_guard<std::mutex> lock(apiMutex);
|
||||
auto it = rrFeatures.find(handle); if (it != rrFeatures.end()) rr = it->second; }
|
||||
if (rr) return call([&] {
|
||||
RrSnapshot snapshot(rr->creation, p, nullptr); snapshot.resolve();
|
||||
rr->backend.evaluate(queue, snapshot.frame, snapshot.access); return 1u;
|
||||
});
|
||||
#endif
|
||||
std::shared_ptr<d4r::win::Feature> feature;
|
||||
{ std::lock_guard<std::mutex> lock(apiMutex);
|
||||
auto it = features.find(handle); if (it == features.end() || !p) return invalid; feature = it->second; }
|
||||
return call([&] { feature->evaluate_boundary(queue, p); return 1u; });
|
||||
}
|
||||
API unsigned NVSDK_NGX_D3D12_EvaluateFeature(ID3D12GraphicsCommandList* list, const Handle* handle, void* p, void*) {
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
std::shared_ptr<RrFeature> rr;
|
||||
{ std::lock_guard<std::mutex> lock(apiMutex);
|
||||
auto it = rrFeatures.find(const_cast<Handle*>(handle)); if (it != rrFeatures.end()) rr = it->second; }
|
||||
if (rr) return call([&] {
|
||||
if (!p || !list) return invalid;
|
||||
auto snapshot = std::make_shared<RrSnapshot>(rr->creation, p, list);
|
||||
// Reject unsupported contracts before splitting the logical command
|
||||
// list. Submission still rechecks resources and resolved access.
|
||||
rr->backend.validate_frame(snapshot->frame);
|
||||
d4r::win::commands::record_boundary(list, [rr, snapshot](ID3D12CommandQueue* queue) {
|
||||
snapshot->resolve(); rr->backend.evaluate(queue, snapshot->frame, snapshot->access);
|
||||
});
|
||||
return 1u;
|
||||
});
|
||||
#endif
|
||||
std::shared_ptr<d4r::win::Feature> feature;
|
||||
{ std::lock_guard<std::mutex> lock(apiMutex);
|
||||
auto it = features.find(const_cast<Handle*>(handle));
|
||||
@@ -194,6 +356,13 @@ API unsigned NVSDK_NGX_D3D12_EvaluateFeature_C(ID3D12GraphicsCommandList* list,
|
||||
return NVSDK_NGX_D3D12_EvaluateFeature(list, handle, p, callback);
|
||||
}
|
||||
API unsigned NVSDK_NGX_D3D12_ReleaseFeature(Handle* handle) {
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
std::shared_ptr<RrFeature> rr;
|
||||
{ std::lock_guard<std::mutex> lock(apiMutex);
|
||||
auto it = rrFeatures.find(handle);
|
||||
if (it != rrFeatures.end()) { rr = std::move(it->second); rrFeatures.erase(it); delete handle; } }
|
||||
if (rr) return call([&] { rr->backend.drain(); rr.reset(); return 1u; });
|
||||
#endif
|
||||
std::shared_ptr<d4r::win::Feature> feature;
|
||||
{ std::lock_guard<std::mutex> lock(apiMutex);
|
||||
auto it = features.find(handle); if (it == features.end()) return invalid;
|
||||
@@ -206,10 +375,15 @@ API unsigned NVSDK_NGX_D3D12_Shutdown() {
|
||||
std::lock_guard<std::mutex> lock(apiMutex);
|
||||
return call([&] {
|
||||
if (runtime && std::getenv("D4R_ASYNC_INTEROP")) runtime->drain_async_queue();
|
||||
#if defined(D4R_NATIVE_RR_BACKEND)
|
||||
for (auto& pair : rrFeatures) pair.second->backend.drain();
|
||||
for (auto& pair : rrFeatures) delete pair.first;
|
||||
rrFeatures.clear();
|
||||
#endif
|
||||
for (auto& pair : features) delete pair.first;
|
||||
features.clear();
|
||||
for (auto& pair : parameters) d4r_ngx_parameters_destroy(pair.first);
|
||||
parameters.clear(); runtime.reset(); return 1u;
|
||||
parameters.clear(); runtime.reset(); initialization.reset(); deferredSr = false; return 1u;
|
||||
});
|
||||
}
|
||||
API unsigned NVSDK_NGX_D3D12_Shutdown1(ID3D12Device*) { return NVSDK_NGX_D3D12_Shutdown(); }
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Verify and deploy a locally built RR model; optionally select it in d4r.ini.
|
||||
|
||||
Existing models are preserved. Configuration changes receive an external backup.
|
||||
No NVIDIA DLL, compiler, driver, package archive or system setting is changed.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def select_native(data, model):
|
||||
bom = data.startswith(b'\xef\xbb\xbf')
|
||||
text = data.decode('utf-8-sig')
|
||||
newline = '\r\n' if '\r\n' in text else '\n'
|
||||
result, in_rr, found, pending = [], False, False, set()
|
||||
values = {'backend': 'native', 'modeldir': model}
|
||||
def flush():
|
||||
if pending and result and not result[-1].endswith(('\n', '\r')):
|
||||
result.append(newline)
|
||||
for key in ('backend', 'modeldir'):
|
||||
if key in pending:
|
||||
result.append(('Backend' if key == 'backend' else 'ModelDir') + ' = ' + values[key] + newline)
|
||||
pending.clear()
|
||||
for line in text.splitlines(keepends=True):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('['):
|
||||
if in_rr:
|
||||
flush()
|
||||
in_rr = stripped.split(']', 1)[0][1:].strip().casefold() == 'rayreconstruction'
|
||||
if in_rr:
|
||||
found = True
|
||||
pending = set(values)
|
||||
if in_rr and '=' in line and not stripped.startswith((';', '#')):
|
||||
key = line.split('=', 1)[0].strip().casefold()
|
||||
if key in values:
|
||||
prefix, body = line.split('=', 1)
|
||||
ending = '\r\n' if body.endswith('\r\n') else '\n' if body.endswith('\n') else ''
|
||||
body = body.removesuffix(ending) if ending else body
|
||||
comment = re.search(r'\s[;#]', body)
|
||||
suffix = body[comment.start():] if comment else ''
|
||||
result.append(prefix + '= ' + values[key] + suffix + ending)
|
||||
pending.discard(key)
|
||||
continue
|
||||
result.append(line)
|
||||
if in_rr:
|
||||
flush()
|
||||
if not found:
|
||||
if result and not result[-1].endswith('\n'):
|
||||
result.append(newline)
|
||||
result.extend((newline, '[RayReconstruction]' + newline, 'Backend = native' + newline,
|
||||
'ModelDir = ' + model + newline))
|
||||
return (b'\xef\xbb\xbf' if bom else b'') + ''.join(result).encode('utf-8')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--model', required=True, type=lambda p: Path(p).resolve())
|
||||
parser.add_argument('--runtime', required=True, type=lambda p: Path(p).resolve())
|
||||
parser.add_argument('--enable', action='store_true')
|
||||
parser.add_argument('--backup-root', type=lambda p: Path(p).resolve(),
|
||||
default=Path(os.environ.get('LOCALAPPDATA', str(Path.home()))) / 'd4r/backups')
|
||||
args = parser.parse_args()
|
||||
config = args.runtime / 'd4r.ini'
|
||||
if not config.is_file():
|
||||
parser.error('--runtime must contain d4r.ini')
|
||||
if args.enable and args.backup_root.resolve().is_relative_to(args.runtime.parent):
|
||||
parser.error('configuration backups must stay outside the game/runtime parent directory')
|
||||
cache_data = (args.model / 'windows-native-python-cache.json').read_bytes()
|
||||
cache = json.loads(cache_data)
|
||||
identity = cache['identity']
|
||||
if identity.get('preset') not in ('D', 'E') or identity.get('architecture') != 'gfx1101' or identity.get('arithmetic') != 'exact-gfx11':
|
||||
raise RuntimeError('Unsupported local RR cache identity')
|
||||
products = cache['products']
|
||||
if not {'rr.txt', 'enc0.args', 'hkpn.args'}.issubset(products):
|
||||
raise RuntimeError('RR cache omits required manifest/templates')
|
||||
for name, expected in products.items():
|
||||
if Path(name).name != name or name in ('.', '..') or '\\' in name or ':' in name:
|
||||
raise RuntimeError('Invalid model product path: ' + name)
|
||||
if sha((args.model / name).read_bytes()) != expected:
|
||||
raise RuntimeError('Model product changed: ' + name)
|
||||
manifest = (args.model / 'rr.txt').read_text()
|
||||
stages = [line.split() for line in manifest.splitlines() if line.startswith('stage ')]
|
||||
expected_stages = {'enc0', 'enc1', 'enc2', 'enc3', 'enc4', 'enc5', 'dec4', 'dec3', 'dec2', 'dec1', 'dec0', 'hkpn'}
|
||||
if (not manifest.startswith('D4RR0001\n') or
|
||||
f'preset {4 if identity["preset"] == "D" else 5}' not in manifest.splitlines() or
|
||||
len(stages) != 12 or {fields[1] for fields in stages} != expected_stages or 'arithmetic gfx11' not in manifest):
|
||||
raise RuntimeError('Incomplete RR model manifest')
|
||||
for fields in stages:
|
||||
if fields[2] not in products or fields[4] not in products:
|
||||
raise RuntimeError('Stage product not covered by cache')
|
||||
name = 'rr-' + identity['preset'].lower() + '-' + sha(cache_data)[:12]
|
||||
destination = args.runtime / 'engine' / name
|
||||
if not destination.resolve().is_relative_to(args.runtime):
|
||||
raise RuntimeError('RR destination must stay within the selected runtime')
|
||||
reused = destination.exists()
|
||||
if not reused:
|
||||
destination.mkdir(parents=True)
|
||||
for product in products:
|
||||
shutil.copy2(args.model / product, destination / product)
|
||||
(destination / 'd4r-private-rr.json').write_text(json.dumps({'privateLocalModel': True, 'cacheSha256': sha(cache_data), 'products': products}, indent=2) + '\n')
|
||||
for product, expected in products.items():
|
||||
if sha((destination / product).read_bytes()) != expected:
|
||||
raise RuntimeError('Existing/deployed RR model differs; preserving it: ' + str(destination))
|
||||
backup = None
|
||||
if args.enable:
|
||||
original = config.read_bytes()
|
||||
updated = select_native(original, destination.relative_to(args.runtime).as_posix())
|
||||
if updated != original:
|
||||
args.backup_root.mkdir(parents=True, exist_ok=True)
|
||||
backup = args.backup_root / ('rr-config-' + uuid.uuid4().hex + '.ini')
|
||||
with backup.open('xb') as stream:
|
||||
stream.write(original)
|
||||
temporary = config.with_name('d4r.ini.' + uuid.uuid4().hex + '.tmp')
|
||||
with temporary.open('xb') as stream:
|
||||
stream.write(updated)
|
||||
if config.read_bytes() != original:
|
||||
temporary.unlink()
|
||||
raise RuntimeError('d4r.ini changed during deployment; preserved current configuration')
|
||||
os.replace(temporary, config)
|
||||
print(json.dumps({'deployed': str(destination), 'preset': identity['preset'], 'productsVerified': len(products),
|
||||
'reused': reused, 'enabled': args.enable, 'configBackup': str(backup) if backup else None,
|
||||
'privateLocalModel': True, 'published': False}, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -119,6 +119,7 @@ struct Args {
|
||||
std::string pixel_profile = "baseline";
|
||||
std::string barrier_mode = "legacy";
|
||||
bool early_indirect = false;
|
||||
bool player_mode = false;
|
||||
std::string ngx_core, dlss_dll;
|
||||
std::string nvapi_dll;
|
||||
std::string ngx_abi = "driver";
|
||||
@@ -160,6 +161,7 @@ struct Args {
|
||||
barrier_mode = value;
|
||||
}
|
||||
else if (key == "--early-indirect") early_indirect = value == "1";
|
||||
else if (key == "--player-mode") player_mode = value == "1";
|
||||
else if (key == "--ngx-core") ngx_core = value;
|
||||
else if (key == "--dlss-dll") dlss_dll = value;
|
||||
else if (key == "--nvapi-dll") nvapi_dll = value;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_4.h>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
// Native Windows uses the installed driver's normal D3D12 path. Do not import
|
||||
// Proton's amdxc64 shim or Linux PCI/sysfs discovery into this process.
|
||||
inline bool d4r_fsr_rr_driver_extension(ID3D12Device* device, std::string& detail)
|
||||
{
|
||||
LUID luid{};
|
||||
device->GetAdapterLuid(&luid);
|
||||
IDXGIFactory4* factory = nullptr;
|
||||
if (FAILED(CreateDXGIFactory2(0, IID_PPV_ARGS(&factory)))) {
|
||||
detail = "native Windows: DXGI factory query failed";
|
||||
return false;
|
||||
}
|
||||
IDXGIAdapter1* adapter = nullptr;
|
||||
const HRESULT result = factory->EnumAdapterByLuid(luid, IID_PPV_ARGS(&adapter));
|
||||
factory->Release();
|
||||
if (FAILED(result)) {
|
||||
detail = "native Windows: device adapter query failed";
|
||||
return false;
|
||||
}
|
||||
DXGI_ADAPTER_DESC1 desc{};
|
||||
const HRESULT described = adapter->GetDesc1(&desc);
|
||||
adapter->Release();
|
||||
if (FAILED(described)) {
|
||||
detail = "native Windows: adapter description failed";
|
||||
return false;
|
||||
}
|
||||
char text[160];
|
||||
std::snprintf(text, sizeof(text), "native Windows DXGI %04x:%04x; installed D3D12 driver path",
|
||||
desc.VendorId, desc.DeviceId);
|
||||
detail = text;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Adapt textual HIP helper IR to the pinned LLVM 22 reader without relaxing FP."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
|
||||
|
||||
def float_hex(match):
|
||||
bits = int(match[1], 16)
|
||||
sign, exponent, fraction = bits >> 31, (bits >> 23) & 255, bits & 0x7fffff
|
||||
if exponent:
|
||||
wide_exponent = 2047 if exponent == 255 else exponent + 896
|
||||
wide_fraction = fraction << 29
|
||||
elif fraction:
|
||||
leading = fraction.bit_length() - 1
|
||||
wide_exponent = leading - 149 + 1023
|
||||
wide_fraction = (fraction - (1 << leading)) << (52 - leading)
|
||||
else:
|
||||
wide_exponent = wide_fraction = 0
|
||||
# LLVM's legacy float spelling uses the exact widening's binary64 bits.
|
||||
# Integer operations preserve signed zero and signaling/quiet NaN payloads.
|
||||
return f'0x{((sign << 63) | (wide_exponent << 52) | wide_fraction):016X}'
|
||||
|
||||
|
||||
def compatible_ir(text):
|
||||
text = re.sub(r'\bf0x([0-9A-Fa-f]{16})\b', r'0x\1', text)
|
||||
text = re.sub(r'\bf0x([0-9A-Fa-f]{4})\b', r'0xH\1', text)
|
||||
text = re.sub(r'\bf0x([0-9A-Fa-f]{8})\b', float_hex, text)
|
||||
text = text.replace('denormal_fpenv(dynamic)', '"denormal-fp-math"="dynamic,dynamic"')
|
||||
# This newer optimization promise is absent in LLVM 22. Dropping a promise
|
||||
# weakens what the optimizer may assume; it does not change instructions.
|
||||
text = re.sub(r'\bnocreateundeforpoison\b', '', text)
|
||||
text = re.sub(r'\bmemory\([^)]*\btarget_mem\b[^)]*\)', '', text)
|
||||
text = re.sub(r'\b(uitofp|sitofp|fptoui|fptosi)\s+(?:(?:nnan|ninf|nsz|arcp|contract|afn|reassoc|fast)\s+)+',
|
||||
r'\1 ', text)
|
||||
# LLVM 24 permits short decimal floats that the older reader rejects.
|
||||
# Restrict conversion to unquoted literals whose nearest IR type is float/half;
|
||||
# double literals and string attributes retain their original spelling.
|
||||
result = []
|
||||
decimal = re.compile(r'(?<![\w.])[-+]?\d+\.\d+(?:[eE][-+]?\d+)?(?![\w.])')
|
||||
for line in text.splitlines():
|
||||
pieces = re.split(r'("(?:[^"\\]|\\.)*")', line)
|
||||
for index in range(0, len(pieces), 2):
|
||||
piece = pieces[index]
|
||||
def convert(match):
|
||||
types = re.findall(r'\b(float|double|half|bfloat|fp128|i\d+)\b', piece[:match.start()])
|
||||
if not types or types[-1] not in ('float', 'half'):
|
||||
return match[0]
|
||||
if types[-1] == 'half':
|
||||
bits = struct.unpack('>H', struct.pack('>e', float(match[0])))[0]
|
||||
return f'0xH{bits:04X}'
|
||||
bits = struct.unpack('>I', struct.pack('>f', float(match[0])))[0]
|
||||
return float_hex(re.match(r'(.*)', f'{bits:08X}'))
|
||||
pieces[index] = decimal.sub(convert, piece)
|
||||
result.append(''.join(pieces))
|
||||
return '\n'.join(result) + '\n'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('input', type=Path)
|
||||
parser.add_argument('output', type=Path)
|
||||
args = parser.parse_args()
|
||||
args.output.write_text(compatible_ir(args.input.read_text()))
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
struct ID3D12Resource;
|
||||
extern "C" {
|
||||
void d4r_ngx_set_ull(void*, const char*, unsigned long long);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include "ngx_windows_runtime.h"
|
||||
|
||||
namespace d4r::win {
|
||||
// Own the complete Init contract when native RR defers the independent SR
|
||||
// runtime. The caller may mutate or free its strings/path list after Init.
|
||||
struct RuntimeInitialization {
|
||||
ComPtr<ID3D12Device> device;
|
||||
unsigned long long application;
|
||||
unsigned sdk;
|
||||
std::wstring data;
|
||||
ngx::FeatureCommonInfo common{};
|
||||
std::vector<std::wstring> paths;
|
||||
std::vector<const wchar_t*> pathPointers;
|
||||
ngx::ProjectIdentity project;
|
||||
bool hasCommon = false, hasProject = false;
|
||||
RuntimeInitialization(ID3D12Device* d3d, unsigned long long app, const wchar_t* directory, unsigned version,
|
||||
const ngx::FeatureCommonInfo* info, const ngx::ProjectIdentity* identity)
|
||||
: device(d3d), application(app), sdk(version), data(directory && *directory ? directory : std::filesystem::current_path().wstring()) {
|
||||
if (!d3d) throw std::runtime_error("Null NGX initialization device");
|
||||
if (identity) { project = *identity; hasProject = true; }
|
||||
if (info) {
|
||||
if (info->PathListInfo.Length > 1024 || (info->PathListInfo.Length && !info->PathListInfo.Path))
|
||||
throw std::runtime_error("Invalid NGX initialization path list");
|
||||
hasCommon = true;
|
||||
// Runtime consumes only path and logging information. InternalData
|
||||
// is opaque and must not become a deferred borrowed pointer.
|
||||
if (sdk >= 0x14) common.LoggingInfo = info->LoggingInfo;
|
||||
for (unsigned i = 0; i < info->PathListInfo.Length; ++i)
|
||||
if (info->PathListInfo.Path[i]) paths.emplace_back(info->PathListInfo.Path[i]);
|
||||
for (const auto& path : paths) pathPointers.push_back(path.c_str());
|
||||
common.PathListInfo = {pathPointers.data(), unsigned(pathPointers.size())};
|
||||
}
|
||||
}
|
||||
RuntimeInitialization(const RuntimeInitialization&) = delete;
|
||||
RuntimeInitialization& operator=(const RuntimeInitialization&) = delete;
|
||||
std::shared_ptr<Runtime> create() const {
|
||||
return std::make_shared<Runtime>(device.Get(), application, data.c_str(), sdk,
|
||||
hasCommon ? &common : nullptr, hasProject ? &project : nullptr);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,9 @@ struct Settings {
|
||||
bool nativeFp8 = false;
|
||||
bool asyncInterop = false, validateOutput = false, verbose = false, showWatermark = false;
|
||||
std::wstring cacheDir;
|
||||
std::wstring nativeDir;
|
||||
std::string rrBackend = "off";
|
||||
std::wstring rrModelDir;
|
||||
};
|
||||
struct State {
|
||||
bool active = false;
|
||||
@@ -122,6 +125,17 @@ inline Settings read_settings(const std::vector<IniEntry>& ini, std::vector<std:
|
||||
notes.push_back("d4r.ini: [Debug] Log must be normal or verbose, not '" + entry.value + "'; using normal");
|
||||
} else if (name == "paths.cachedir") {
|
||||
if (!entry.value.empty() && lower(entry.value) != "auto") settings.cacheDir = diag::wide(entry.value);
|
||||
} else if (name == "paths.nativedir") {
|
||||
if (!entry.value.empty() && lower(entry.value) != "auto") settings.nativeDir = diag::wide(entry.value);
|
||||
} else if (name == "rayreconstruction.backend") {
|
||||
const auto value = lower(entry.value);
|
||||
if (value.empty() || value == "auto" || value == "off") settings.rrBackend = "off";
|
||||
else if (value == "native" || value == "fsr") {
|
||||
settings.rrBackend = value;
|
||||
if (value == "fsr") notes.push_back("d4r.ini: native Windows FSR RR is unavailable; RR requests will be rejected");
|
||||
} else { settings.rrBackend = "off"; notes.push_back("d4r.ini: [RayReconstruction] Backend must be off, native or fsr; using off"); }
|
||||
} else if (name == "rayreconstruction.modeldir") {
|
||||
settings.rrModelDir = !entry.value.empty() && lower(entry.value) != "auto" ? diag::wide(entry.value) : L"";
|
||||
} else notes.push_back("d4r.ini: [" + entry.section + "] " + entry.key + " is not a Windows setting; ignored");
|
||||
}
|
||||
return settings;
|
||||
@@ -192,14 +206,31 @@ inline void verify_dll(const std::filesystem::path& dir, const std::vector<Pin>&
|
||||
|
||||
// The installer used to check these before touching the game; a drag-in
|
||||
// install checks them at startup instead.
|
||||
inline std::filesystem::path native_code_object_dir(const std::filesystem::path& dir) {
|
||||
inline std::filesystem::path native_code_object_dir(const std::filesystem::path& dir, const Settings* settings = nullptr) {
|
||||
if (settings && !settings->nativeDir.empty()) {
|
||||
auto root = std::filesystem::path(settings->nativeDir);
|
||||
if (root.is_relative()) root = dir / root;
|
||||
const auto arch = root / diag::wide(diag::HipApi::target_arch());
|
||||
return std::filesystem::is_directory(arch) ? arch : root;
|
||||
}
|
||||
const auto arch = dir / L"native" / diag::wide(diag::HipApi::target_arch());
|
||||
return std::filesystem::is_directory(arch) ? arch : dir / L"native";
|
||||
}
|
||||
inline void verify_code_objects(const std::filesystem::path& dir) {
|
||||
inline void verify_code_objects(const std::filesystem::path& dir, const Settings* settings = nullptr) {
|
||||
const std::string arch = diag::HipApi::target_arch();
|
||||
std::vector<std::filesystem::path> objects{dir / (L"pixel_convert_" + diag::wide(arch) + L".hsaco")};
|
||||
const auto native = native_code_object_dir(dir);
|
||||
const auto native = native_code_object_dir(dir, settings);
|
||||
if (settings && !settings->nativeDir.empty() &&
|
||||
(!std::filesystem::is_directory(native) || !std::filesystem::is_regular_file(native / L"d4r-kernels.txt")))
|
||||
throw std::runtime_error("[Paths] NativeDir must contain the complete native kernel set and d4r-kernels.txt");
|
||||
if (settings && !settings->nativeDir.empty()) {
|
||||
for (const char* name : {"dltss_pwin_enc0_layer", "dltss_pwin_enc1_layer", "dltss_pwin_enc2_layer",
|
||||
"dltss_pwin_enc3_layer", "dltss_pwin_enc4_layer", "dltss_pwin_dec5_layer", "dltss_pwin_dec4_layer",
|
||||
"dltss_pwin_dec3_layer", "dltss_pwin_dec2_layer", "dltss_pwin_dec1_layer", "dltss_pwin_dec0_layer",
|
||||
"rrlite_enc1_4x4", "rrlite_enc2_4x4", "rrlite_enc3_tube_4x4", "rrlite_dec2_4x4", "rrlite_dec1_4x4"})
|
||||
if (!std::filesystem::is_regular_file(native / (std::string(name) + ".hsaco")))
|
||||
throw std::runtime_error(std::string("[Paths] NativeDir is incomplete; missing ") + name);
|
||||
}
|
||||
if (!std::filesystem::is_directory(native)) throw std::runtime_error("d4r\\native (or d4r\\native\\" + arch + ") is missing; extract the d4r ZIP again");
|
||||
for (const auto& entry : std::filesystem::directory_iterator(native))
|
||||
if (entry.path().extension() == L".hsaco") objects.push_back(entry.path());
|
||||
@@ -212,9 +243,9 @@ inline void verify_code_objects(const std::filesystem::path& dir) {
|
||||
}
|
||||
}
|
||||
|
||||
inline void verify_native_fp8_bundle(const std::filesystem::path& dir) {
|
||||
inline void verify_native_fp8_bundle(const std::filesystem::path& dir, const Settings* settings = nullptr) {
|
||||
const auto arch = diag::HipApi::target_arch();
|
||||
const auto marker = native_code_object_dir(dir) / L"d4r-native-fp8.txt";
|
||||
const auto marker = native_code_object_dir(dir, settings) / L"d4r-native-fp8.txt";
|
||||
if (!std::filesystem::is_regular_file(marker))
|
||||
throw std::runtime_error("[Kernels] NativeFp8=true needs a matching gfx12 token-lane M bundle; rebuild with -TokenLanes and restart.");
|
||||
const auto contents = read_text(marker);
|
||||
@@ -260,6 +291,10 @@ inline void redirect(FILE* crt, DWORD stream, const std::filesystem::path& path)
|
||||
SetStdHandle(stream, reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(crt))));
|
||||
}
|
||||
|
||||
inline std::filesystem::path rr_model_directory(const std::filesystem::path& dir, const Settings& settings) {
|
||||
const std::filesystem::path model = settings.rrModelDir.empty() ? std::filesystem::path(L"engine/rr-d") : std::filesystem::path(settings.rrModelDir);
|
||||
return std::filesystem::absolute(model.is_relative() ? dir / model : model).lexically_normal();
|
||||
}
|
||||
inline void apply(const std::filesystem::path& dir, const Settings& settings) {
|
||||
std::filesystem::path cache = settings.cacheDir;
|
||||
if (cache.empty()) {
|
||||
@@ -276,6 +311,8 @@ inline void apply(const std::filesystem::path& dir, const Settings& settings) {
|
||||
const auto compatNvapi = compat_nvapi_path(dir);
|
||||
// Same codegen and paths as windows-game.ps1; only diagnostics differ.
|
||||
const std::pair<const wchar_t*, std::optional<std::wstring>> values[] = {
|
||||
{L"D4R_RR_BACKEND", settings.rrBackend == "off" ? unset : std::optional<std::wstring>(diag::wide(settings.rrBackend))},
|
||||
{L"D4R_RR_MODEL_DIR", settings.rrBackend == "native" ? std::optional<std::wstring>(rr_model_directory(dir, settings).wstring()) : unset},
|
||||
{L"D4R_HIP_ROOT", p(L"hip")}, {L"HIP_PATH", p(L"hip")},
|
||||
{L"D4R_NVCUDA_DLL", p(L"zluda\\nvcuda.dll")}, {L"ZLUDA_CUDA_LIB", p(L"zluda\\nvcuda.dll")},
|
||||
{L"D4R_NVAPI_DLL", std::optional<std::wstring>(compatNvapi.wstring())},
|
||||
@@ -283,7 +320,7 @@ inline void apply(const std::filesystem::path& dir, const Settings& settings) {
|
||||
{L"D4R_NGX_CORE", p(L"ngx\\_nvngx.dll")}, {L"D4R_DLSS_DLL", p(L"dlss\\nvngx_dlss.dll")},
|
||||
{L"D4R_NGX_DATA_DIR", std::optional<std::wstring>(ngxData.wstring())},
|
||||
{L"D4R_FORMAT_MODULE", p(L"pixel_convert_" + diag::wide(diag::HipApi::target_arch()) + L".hsaco")},
|
||||
{L"D4R_ZLUDA_NATIVE_DIR", std::optional<std::wstring>(native_code_object_dir(dir).wstring())},{L"D4R_D3D12_COMMAND_BACKEND", on},
|
||||
{L"D4R_ZLUDA_NATIVE_DIR", std::optional<std::wstring>(native_code_object_dir(dir, &settings).wstring())},{L"D4R_D3D12_COMMAND_BACKEND", on},
|
||||
{L"D4R_ZLUDA_WMMA", on}, {L"D4R_ZLUDA_WMMA_FP8", on},
|
||||
{L"D4R_ZLUDA_WMMA_FP8_NATIVE", settings.nativeFp8 ? on : off},
|
||||
{L"D4R_ZLUDA_WMMA_F16_REFERENCE", on},
|
||||
@@ -328,8 +365,8 @@ inline void ensure_environment() {
|
||||
verify_dll(dir, pins, "dlss", L"dlss\\nvngx_dlss.dll", "nvngx_dlss.dll", current.notes);
|
||||
verify_dll(dir, pins, "ngx", L"ngx\\_nvngx.dll", "_nvngx.dll", current.notes);
|
||||
reject_translation_layer();
|
||||
if (settings.nativeFp8) verify_native_fp8_bundle(dir);
|
||||
verify_code_objects(dir);
|
||||
if (settings.nativeFp8) verify_native_fp8_bundle(dir, &settings);
|
||||
verify_code_objects(dir, &settings);
|
||||
apply(dir, settings);
|
||||
current.active = true; current.preset = settings.preset;
|
||||
const char* model = settings.preset == 5 ? "E" : settings.preset == 12 ? "L" : settings.preset == 13 ? "M" : "K";
|
||||
|
||||
@@ -60,6 +60,28 @@ int wmain(int argc, wchar_t** argv) {
|
||||
expect(!s.showWatermark && has_note(notes, "ShowWatermark must be true or false"), "invalid ShowWatermark falls back to hidden with a note");
|
||||
s = settings("[Paths]\nCacheDir = D:\\d4r-cache\n", notes);
|
||||
expect(s.cacheDir == L"D:\\d4r-cache" && notes.empty(), "CacheDir path");
|
||||
s = settings("[Paths]\nNativeDir = private-native\n", notes);
|
||||
expect(s.nativeDir == L"private-native" && notes.empty(), "NativeDir path");
|
||||
expect(player::native_code_object_dir(dir, &s) == dir / L"private-native", "relative NativeDir resolves beside d4r.ini");
|
||||
expect(!error_of([&] { player::verify_code_objects(dir, &s); }).empty(), "incomplete private native directory rejected");
|
||||
s.cacheDir = (dir / L"cache").wstring();
|
||||
player::apply(dir, s);
|
||||
expect(std::wstring(_wgetenv(L"D4R_ZLUDA_NATIVE_DIR")) == (dir / L"private-native").wstring(),
|
||||
"player environment selects configured private native directory");
|
||||
s = settings("[Paths]\nNativeDir = auto\n", notes);
|
||||
expect(s.nativeDir.empty() && notes.empty(), "NativeDir auto preserves packaged kernels");
|
||||
s = settings("[RayReconstruction]\nBackend = NATIVE\nModelDir = local-rr\n", notes);
|
||||
expect(s.rrBackend == "native" && s.rrModelDir == L"local-rr" && notes.empty(), "native RR settings parse independently of SR");
|
||||
expect(player::rr_model_directory(dir, s) == std::filesystem::absolute(dir / L"local-rr").lexically_normal(), "relative RR model resolves beside d4r.ini");
|
||||
s.cacheDir = (dir / L"cache").wstring(); player::apply(dir, s);
|
||||
expect(std::string(std::getenv("D4R_RR_BACKEND")) == "native" &&
|
||||
std::filesystem::path(_wgetenv(L"D4R_RR_MODEL_DIR")) == player::rr_model_directory(dir, s), "native RR environment selected without a launcher");
|
||||
s = settings("[RayReconstruction]\nBackend = fsr\n", notes);
|
||||
expect(s.rrBackend == "fsr" && has_note(notes, "FSR RR is unavailable"), "unavailable FSR selection remains explicit");
|
||||
s = settings("[RayReconstruction]\nBackend = typo\n", notes);
|
||||
expect(s.rrBackend == "off" && has_note(notes, "Backend must be"), "invalid RR backend stays off");
|
||||
s = settings("", notes); s.cacheDir = (dir / L"cache").wstring(); player::apply(dir, s);
|
||||
expect(!std::getenv("D4R_RR_BACKEND") && !std::getenv("D4R_RR_MODEL_DIR"), "default RR off clears stale selection");
|
||||
|
||||
std::filesystem::create_directories(dir / L"nvapi");
|
||||
{ std::ofstream(dir / L"nvapi" / L"nvapi64.dll", std::ios::binary) << "generic nvapi fixture"; }
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Prepare a private portable RR model-tool bundle from pinned dependencies.
|
||||
|
||||
Downloads/extracts tools locally. Does not install Python, use pip, modify PATH,
|
||||
change policy, build a model, or install into a game.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import uuid
|
||||
import zipfile
|
||||
|
||||
|
||||
DEPENDENCIES = {
|
||||
'python-3.12.10-embed-amd64.zip': (
|
||||
'https://www.python.org/ftp/python/3.12.10/python-3.12.10-embed-amd64.zip',
|
||||
'4acbed6dd1c744b0376e3b1cf57ce906f9dc9e95e68824584c8099a63025a3c3'),
|
||||
'numpy-2.4.6-cp312-cp312-win_amd64.whl': (
|
||||
'https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl',
|
||||
'd8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751'),
|
||||
'glslang-16.6.0-windows-x86_64-release.zip': (
|
||||
'https://github.com/KhronosGroup/glslang/releases/download/16.6.0/glslang-16.6.0-windows-x86_64-release.zip',
|
||||
'82bf434e69b9bb4829de7e2b4bc2c5e7a7861e53d66cf75e5cc70f5f694a8d9b'),
|
||||
'glslang-16.6.0-LICENSE.txt': (
|
||||
'https://raw.githubusercontent.com/KhronosGroup/glslang/16.6.0/LICENSE.txt',
|
||||
'17e70c676e1521ff3e4686f04a2053d93a7e28a33be8de7ec37ab0ff72feb677'),
|
||||
}
|
||||
|
||||
|
||||
def sha(path):
|
||||
with path.open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def extract(archive, destination):
|
||||
with zipfile.ZipFile(archive) as source:
|
||||
for entry in source.infolist():
|
||||
name = PurePosixPath(entry.filename)
|
||||
if name.is_absolute() or '..' in name.parts or '\\' in entry.filename or ':' in entry.filename:
|
||||
raise RuntimeError('Unsafe dependency archive path')
|
||||
if entry.is_dir():
|
||||
continue
|
||||
target = destination.joinpath(*name.parts)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with source.open(entry) as data, target.open('xb') as out:
|
||||
shutil.copyfileobj(data, out)
|
||||
|
||||
|
||||
def main():
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', required=True, type=lambda p: Path(p).resolve())
|
||||
parser.add_argument('--archive', type=lambda p: Path(p).resolve(), help='optional clean portable-tools ZIP, before any model build')
|
||||
parser.add_argument('--download-cache', type=lambda p: Path(p).resolve(), default=repo / '.tools/downloads')
|
||||
args = parser.parse_args()
|
||||
if args.output.exists() or not args.output.is_relative_to((repo / 'build').resolve()):
|
||||
parser.error('use a fresh output directory under ignored build/')
|
||||
if args.archive and (args.archive.exists() or not args.archive.is_relative_to((repo / 'build').resolve()) or args.archive.is_relative_to(args.output)):
|
||||
parser.error('use a fresh archive under build/, outside the bundle directory')
|
||||
args.download_cache.mkdir(parents=True, exist_ok=True)
|
||||
for name, (url, expected) in DEPENDENCIES.items():
|
||||
path = args.download_cache / name
|
||||
if not path.exists():
|
||||
print('RR_TOOLS_DOWNLOAD ' + name, flush=True)
|
||||
temporary = path.with_name(name + '.part-' + uuid.uuid4().hex)
|
||||
with urllib.request.urlopen(url, timeout=60) as response, temporary.open('xb') as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
if sha(temporary) != expected:
|
||||
raise RuntimeError('Dependency download hash mismatch: ' + name)
|
||||
temporary.rename(path)
|
||||
if sha(path) != expected:
|
||||
raise RuntimeError('Cached dependency changed: ' + name)
|
||||
python = args.output / 'python'
|
||||
recipe = args.output / 'recipe'
|
||||
extract(args.download_cache / 'python-3.12.10-embed-amd64.zip', python)
|
||||
extract(args.download_cache / 'numpy-2.4.6-cp312-cp312-win_amd64.whl', recipe / '.tools/python/vendor')
|
||||
glslang = recipe / '.tools/glslang-16.6.0'
|
||||
extract(args.download_cache / 'glslang-16.6.0-windows-x86_64-release.zip', glslang)
|
||||
shutil.copy2(glslang / 'bin/glslang.exe', glslang / 'bin/glslangValidator.exe')
|
||||
if sha(glslang / 'bin/glslangValidator.exe') != '9643709c02c83e26773f96b6a87f197ab2e84f78be0879ad8fb3619418b5d9ad':
|
||||
raise RuntimeError('Extracted glslang identity mismatch')
|
||||
files = subprocess.check_output(['git', 'ls-files', '-z', 'engine', 'vulkan', 'kernels/tools'], cwd=repo).decode().split('\0')
|
||||
suffixes = {'.py', '.comp', '.glsl', '.h', '.inc', '.sh'}
|
||||
sources = [repo / name for name in files if name and Path(name).suffix in suffixes]
|
||||
sources += [repo / 'engine/rr/native_kernels.txt', repo / 'tools/windows/build_rr_model.py', repo / 'tools/windows/deploy_rr_model.py', repo / 'LICENSE']
|
||||
for source in sources:
|
||||
target = recipe / source.relative_to(repo)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, target)
|
||||
# Keep isolated imports: no user site-packages or PYTHONPATH dependence.
|
||||
paths = ['python312.zip', '.', '../recipe', '../recipe/engine', '../recipe/engine/rr',
|
||||
'../recipe/kernels/tools', '../recipe/vulkan', '../recipe/tools/windows', '../recipe/.tools/python/vendor']
|
||||
(python / 'python312._pth').write_text('\n'.join(paths) + '\n')
|
||||
licenses = args.output / 'licenses'
|
||||
licenses.mkdir()
|
||||
shutil.copy2(args.download_cache / 'glslang-16.6.0-LICENSE.txt', licenses / 'glslang-LICENSE.txt')
|
||||
shutil.copy2(python / 'LICENSE.txt', licenses / 'Python-LICENSE.txt')
|
||||
for source in (recipe / '.tools/python/vendor/numpy-2.4.6.dist-info').glob('licenses/*'):
|
||||
if source.is_file():
|
||||
shutil.copy2(source, licenses / ('numpy-' + source.name))
|
||||
manifest = {'kind': 'local-rr-model-tools', 'dependencies': {name: {'url': url, 'sha256': digest}
|
||||
for name, (url, digest) in DEPENDENCIES.items()}, 'sourceWorkingTree': 'local modified; file hashes authoritative',
|
||||
'files': {path.relative_to(args.output).as_posix(): sha(path) for path in sorted(args.output.rglob('*')) if path.is_file()},
|
||||
'modelsBundled': False, 'nvidiaBinariesBundled': False}
|
||||
(args.output / 'rr-tools.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
if args.archive:
|
||||
args.archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Package only the recorded pristine tool inputs. Never sweep a used
|
||||
# bundle's model-build directory into a distributable archive.
|
||||
with zipfile.ZipFile(args.archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for name in (*manifest['files'], 'rr-tools.json'):
|
||||
archive.write(args.output / name, name)
|
||||
with zipfile.ZipFile(args.archive) as archive:
|
||||
for name, expected in manifest['files'].items():
|
||||
if hashlib.sha256(archive.read(name)).hexdigest() != expected:
|
||||
raise RuntimeError('Portable-tools ZIP hash mismatch: ' + name)
|
||||
print('RR_TOOLS_ARCHIVE sha256=' + sha(args.archive) + ' bytes=' + str(args.archive.stat().st_size))
|
||||
print('RR_TOOLS_READY ' + str(args.output))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Refresh the verified gfx1101 development ZIP. Never creates a public release."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
|
||||
def sha(path):
|
||||
with path.open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
for name in ('base-zip', 'shim', 'output', 'archive'):
|
||||
parser.add_argument('--' + name, required=True, type=lambda value: Path(value).resolve())
|
||||
args = parser.parse_args()
|
||||
repo = Path(__file__).resolve().parents[2]
|
||||
build = (repo / 'build').resolve()
|
||||
for path in (args.output, args.archive):
|
||||
if not path.is_relative_to(build) or path.exists():
|
||||
parser.error('use fresh output/archive paths under ignored build/')
|
||||
if args.archive.is_relative_to(args.output):
|
||||
parser.error('archive must be outside the package directory')
|
||||
base_hash = sha(args.base_zip)
|
||||
if base_hash != 'e93be288f4013616cc2229be0132b40395d5031d8733bfa63886ba75ce1389ca':
|
||||
parser.error('base must be the verified local v3 ZIP')
|
||||
with zipfile.ZipFile(args.base_zip) as base:
|
||||
manifest = json.loads(base.read('d4r/package.json').decode('utf-8-sig'))
|
||||
if not manifest.get('localDevelopment') or set(manifest['targets']) != {'gfx1101'}:
|
||||
raise RuntimeError('Unexpected base package identity')
|
||||
records = {entry['path']: entry for entry in manifest['files']}
|
||||
names = [entry.filename for entry in base.infolist() if not entry.is_dir()]
|
||||
if len(names) != len(set(names)) or set(names) != set(records) | {'d4r/package.json'}:
|
||||
raise RuntimeError('Base ZIP entries differ from manifest')
|
||||
for name in names:
|
||||
parts = PurePosixPath(name)
|
||||
if parts.is_absolute() or '..' in parts.parts or '\\' in name or ':' in name:
|
||||
raise RuntimeError('Unsafe ZIP path: ' + name)
|
||||
data = base.read(name)
|
||||
if name in records and hashlib.sha256(data).hexdigest().upper() != records[name]['sha256'].upper():
|
||||
raise RuntimeError('Base ZIP hash mismatch: ' + name)
|
||||
destination = args.output.joinpath(*parts.parts)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_bytes(data)
|
||||
overlays = {
|
||||
'd4r/_nvngx_gfx1101.dll': args.shim,
|
||||
'd4r/d4r.ini': repo / 'packaging/windows/d4r.ini',
|
||||
'WINDOWS_README.txt': repo / 'packaging/windows/WINDOWS_README.txt',
|
||||
'd4r/d4r-check.ps1': repo / 'scripts/windows/d4r-check.ps1',
|
||||
'install.bat': repo / 'packaging/windows/install.bat',
|
||||
'install-dlss.ps1': repo / 'scripts/windows/install-dlss.ps1',
|
||||
'd4r/dll-pins.txt': repo / 'tools/windows/dll-pins.txt',
|
||||
}
|
||||
for name, source in overlays.items():
|
||||
if name not in records:
|
||||
raise RuntimeError('Overlay is not part of the validated package layout: ' + name)
|
||||
shutil.copy2(source, args.output / name)
|
||||
records[name] = {'path': name, 'sha256': sha(source).upper(), 'version': None}
|
||||
prohibited = {'nvngx_dlss.dll', 'nvngx_dlssd.dll'}
|
||||
for path in args.output.rglob('*'):
|
||||
if path.is_file() and (path.name.lower() in prohibited or path.suffix.lower() in ('.spv', '.ptx', '.cubin', '.weights')):
|
||||
raise RuntimeError('Proprietary/model artifact cannot be bundled: ' + str(path))
|
||||
manifest.update(localDevelopment=True, packageWorkingTreeDirty=True, binarySourceCommit=None,
|
||||
packageCommit=subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo, text=True).strip(),
|
||||
files=list(records.values()),
|
||||
localRefresh={'baseZipSha256': base_hash, 'binaryProvenance': 'local modified build; hashes authoritative',
|
||||
'overlays': {name: sha(path) for name, path in overlays.items()},
|
||||
'rr': 'experimental synthetic validation; disabled by default; models generated separately'})
|
||||
manifest['targets']['gfx1101']['binarySourceCommit'] = None
|
||||
manifest['targets']['gfx1101']['hardwareValidation'] = 'SR prior limited evidence; native RR synthetic NGX lifecycle only; no game RR or performance parity'
|
||||
(args.output / 'd4r/package.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
args.archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(args.archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for path in sorted(args.output.rglob('*')):
|
||||
if path.is_file():
|
||||
archive.write(path, path.relative_to(args.output).as_posix())
|
||||
with zipfile.ZipFile(args.archive) as archive:
|
||||
for name, entry in records.items():
|
||||
if hashlib.sha256(archive.read(name)).hexdigest().upper() != entry['sha256'].upper():
|
||||
raise RuntimeError('Refreshed ZIP hash mismatch: ' + name)
|
||||
report = {'zip': str(args.archive), 'sha256': sha(args.archive), 'bytes': args.archive.stat().st_size,
|
||||
'allHashesVerified': True, 'privateOverrideBundled': False, 'rrModelBundled': False,
|
||||
'sourceCommitCreated': False, 'published': False, 'baseZipSha256': base_hash}
|
||||
args.archive.with_suffix('.validation.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,326 @@
|
||||
#pragma once
|
||||
#include <d3d12.h>
|
||||
#include <wrl/client.h>
|
||||
#include "../../engine/runtime.h"
|
||||
#include "binary16.h"
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Canonical-input replay through the native D3D12/Vulkan shared packet. The
|
||||
// packet is GPU memory at the API boundary; CPU access is fixture upload/QA only.
|
||||
namespace rr_bridge {
|
||||
inline void check(VkResult r, const char* operation) {
|
||||
if (r != VK_SUCCESS) throw std::runtime_error(std::string(operation) + " VkResult=" + std::to_string(r));
|
||||
}
|
||||
inline std::vector<char> read(const std::filesystem::path& path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) throw std::runtime_error("Cannot read RR fixture: " + path.string());
|
||||
std::vector<char> result(size_t(f.tellg())); f.seekg(0); f.read(result.data(), std::streamsize(result.size()));
|
||||
if (!f) throw std::runtime_error("Incomplete RR fixture read");
|
||||
return result;
|
||||
}
|
||||
struct Frame { uint32_t id; d4r::RFrameSettings settings; std::array<bool, 4> optional{}; };
|
||||
struct Plane {
|
||||
const char* name; VkFormat format; uint32_t bytes;
|
||||
VkDeviceSize offset = 0, pitch = 0;
|
||||
bool enabled = true;
|
||||
};
|
||||
struct Layout {
|
||||
uint32_t rw = 0, rh = 0, ow = 0, oh = 0;
|
||||
std::array<Plane, 11> planes{{
|
||||
{"color", VK_FORMAT_R16G16B16A16_SFLOAT, 8}, {"motion", VK_FORMAT_R16G16_SFLOAT, 4},
|
||||
{"depth", VK_FORMAT_R32_SFLOAT, 4}, {"normals", VK_FORMAT_R16G16B16A16_SFLOAT, 8},
|
||||
{"roughness", VK_FORMAT_R16G16B16A16_SFLOAT, 8}, {"diffuse", VK_FORMAT_R8G8B8A8_UNORM, 4},
|
||||
{"specular", VK_FORMAT_R8G8B8A8_UNORM, 4},
|
||||
{"hitdistance", VK_FORMAT_R32_SFLOAT, 4, 0, 0, false},
|
||||
{"disocclusion", VK_FORMAT_R16_SFLOAT, 2, 0, 0, false},
|
||||
{"sss", VK_FORMAT_R16_SFLOAT, 2, 0, 0, false},
|
||||
{"particles", VK_FORMAT_R16G16B16A16_SFLOAT, 8, 0, 0, false}}};
|
||||
VkDeviceSize bytes = 0, outputOffset = 0, outputPitch = 0;
|
||||
std::filesystem::path inputs, output, reference;
|
||||
std::vector<Frame> frames;
|
||||
// Runtime layout construction performs no fixture I/O. Both callers use
|
||||
// the same aligned packet contract for native texture transfers.
|
||||
Layout(uint32_t renderWidth, uint32_t renderHeight, uint32_t outputWidth, uint32_t outputHeight)
|
||||
: rw(renderWidth), rh(renderHeight), ow(outputWidth), oh(outputHeight) {
|
||||
if (!rw || !rh || !ow || !oh || rw > 16384 || rh > 16384 || ow > 16384 || oh > 16384)
|
||||
throw std::runtime_error("Invalid RR packet geometry");
|
||||
initializePacket();
|
||||
}
|
||||
void initializePacket() {
|
||||
bytes = 0;
|
||||
for (auto& plane : planes) {
|
||||
bytes = (bytes + 511) & ~VkDeviceSize(511); plane.offset = bytes;
|
||||
plane.pitch = (VkDeviceSize(rw) * plane.bytes + 255) & ~VkDeviceSize(255);
|
||||
bytes += plane.pitch * rh;
|
||||
}
|
||||
outputOffset = (bytes + 511) & ~VkDeviceSize(511);
|
||||
outputPitch = (VkDeviceSize(ow) * 8 + 255) & ~VkDeviceSize(255);
|
||||
bytes = outputOffset + outputPitch * oh;
|
||||
}
|
||||
Layout(const std::string& inputDir, const std::string& outputDir, const std::string& referenceDir)
|
||||
: inputs(inputDir), output(outputDir), reference(referenceDir) {
|
||||
std::ifstream f(inputs / "frames.txt"); std::string token;
|
||||
if (!(f >> token >> rw >> rh >> ow >> oh) || token != "size" || rw < 32 || rh < 32 ||
|
||||
rw > 1024 || rh > 1024 || !ow || !oh || ow > 2560 || oh > 1440)
|
||||
throw std::runtime_error("Invalid bounded RR replay geometry");
|
||||
while (f >> token) {
|
||||
Frame frame{}; int reset, mode;
|
||||
auto& s = frame.settings;
|
||||
if (token != "frame" || !(f >> frame.id >> s.jitter[0] >> s.jitter[1] >> s.motionScale[0] >>
|
||||
s.motionScale[1] >> s.preExposure >> reset >> mode)) throw std::runtime_error("Invalid RR frame header");
|
||||
for (auto& value : s.worldToView) if (!(f >> value)) throw std::runtime_error("Invalid RR view matrix");
|
||||
for (auto& value : s.viewToClip) if (!(f >> value)) throw std::runtime_error("Invalid RR projection matrix");
|
||||
s.reset = reset != 0; s.depthInverted = (mode & 1) != 0; s.hardwareDepth = (mode & 2) != 0;
|
||||
s.roughnessPacked = (mode & 4) != 0;
|
||||
if (!std::isfinite(s.preExposure) || s.preExposure <= 0 ||
|
||||
!std::isfinite(s.jitter[0]) || !std::isfinite(s.jitter[1]) ||
|
||||
!std::isfinite(s.motionScale[0]) || !std::isfinite(s.motionScale[1]))
|
||||
throw std::runtime_error("Nonfinite or invalid RR frame settings");
|
||||
for (float value : s.worldToView) if (!std::isfinite(value)) throw std::runtime_error("Nonfinite RR view matrix");
|
||||
for (float value : s.viewToClip) if (!std::isfinite(value)) throw std::runtime_error("Nonfinite RR projection matrix");
|
||||
frames.push_back(frame);
|
||||
if (frames.size() > 32) throw std::runtime_error("RR replay is limited to 32 frames");
|
||||
}
|
||||
if (frames.empty()) throw std::runtime_error("RR fixture has no frames");
|
||||
std::set<uint32_t> ids;
|
||||
for (auto& frame : frames) {
|
||||
if (!ids.insert(frame.id).second) throw std::runtime_error("Duplicate RR frame ID");
|
||||
for (size_t i = 7; i < planes.size(); ++i) {
|
||||
frame.optional[i - 7] = std::filesystem::exists(inputs / (std::to_string(frame.id) + "-" + planes[i].name + ".bin"));
|
||||
planes[i].enabled = planes[i].enabled || frame.optional[i - 7];
|
||||
}
|
||||
}
|
||||
initializePacket();
|
||||
std::filesystem::create_directories(output);
|
||||
for (const auto& frame : frames)
|
||||
if (std::filesystem::exists(output / (std::to_string(frame.id) + "-out.bin")))
|
||||
throw std::runtime_error("Use a fresh RR output directory");
|
||||
}
|
||||
void upload(void* destination, unsigned frame) const {
|
||||
std::memset(destination, 0, size_t(bytes));
|
||||
for (const auto& p : planes) {
|
||||
if (!p.enabled) continue;
|
||||
const auto index = size_t(&p - planes.data());
|
||||
if (index >= 7 && !frames.at(frame).optional[index - 7]) continue;
|
||||
auto data = read(inputs / (std::to_string(frames.at(frame).id) + "-" + p.name + ".bin"));
|
||||
if (data.size() != size_t(rw) * rh * p.bytes) throw std::runtime_error("RR input size mismatch");
|
||||
for (uint32_t y = 0; y < rh; ++y)
|
||||
std::memcpy(static_cast<char*>(destination) + p.offset + y * p.pitch,
|
||||
data.data() + size_t(y) * rw * p.bytes, size_t(rw) * p.bytes);
|
||||
}
|
||||
}
|
||||
void validate(const void* source, unsigned frame) const {
|
||||
std::vector<char> packed(size_t(ow) * oh * 8);
|
||||
for (uint32_t y = 0; y < oh; ++y)
|
||||
std::memcpy(packed.data() + size_t(y) * ow * 8,
|
||||
static_cast<const char*>(source) + outputOffset + y * outputPitch, size_t(ow) * 8);
|
||||
double low = std::numeric_limits<double>::infinity(), high = -low;
|
||||
for (size_t i = 0; i < packed.size() / 2; ++i) {
|
||||
uint16_t bits; std::memcpy(&bits, packed.data() + i * 2, 2);
|
||||
const double value = d4r::diag::binary16::half_value(bits);
|
||||
if (!std::isfinite(value)) throw std::runtime_error("RR output contains NaN/Inf");
|
||||
if (i % 4 != 3) { low = std::min(low, value); high = std::max(high, value); }
|
||||
}
|
||||
if (!(high - low > 1e-5)) throw std::runtime_error("RR output is uniform");
|
||||
const auto name = std::to_string(frames.at(frame).id) + "-out.bin";
|
||||
if (!reference.empty() && read(reference / name) != packed)
|
||||
throw std::runtime_error("Bridged RR output differs from standalone replay");
|
||||
std::ofstream file(output / name, std::ios::binary); file.write(packed.data(), std::streamsize(packed.size()));
|
||||
if (!file) throw std::runtime_error("RR output write failed");
|
||||
std::printf("RR_BRIDGED_FRAME id=%u finite=1 nonuniform=1 standalone_exact=%u output=%ux%u\n",
|
||||
frames[frame].id, !reference.empty(), ow, oh);
|
||||
}
|
||||
};
|
||||
struct D3D12Frames {
|
||||
const Layout& layout;
|
||||
std::array<Microsoft::WRL::ComPtr<ID3D12Resource>, 11> inputs;
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> output;
|
||||
static void checkD3D(HRESULT r) { if (FAILED(r)) throw std::runtime_error("RR D3D12 texture operation failed: " + std::to_string(r)); }
|
||||
static DXGI_FORMAT format(VkFormat f) {
|
||||
switch (f) {
|
||||
case VK_FORMAT_R16G16B16A16_SFLOAT: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
case VK_FORMAT_R16G16_SFLOAT: return DXGI_FORMAT_R16G16_FLOAT;
|
||||
case VK_FORMAT_R32_SFLOAT: return DXGI_FORMAT_R32_FLOAT;
|
||||
case VK_FORMAT_R8G8B8A8_UNORM: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
case VK_FORMAT_R32G32B32A32_SFLOAT: return DXGI_FORMAT_R32G32B32A32_FLOAT;
|
||||
case VK_FORMAT_R32G32_SFLOAT: return DXGI_FORMAT_R32G32_FLOAT;
|
||||
case VK_FORMAT_R16_SFLOAT: return DXGI_FORMAT_R16_FLOAT;
|
||||
case VK_FORMAT_R8_UNORM: return DXGI_FORMAT_R8_UNORM;
|
||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return DXGI_FORMAT_R10G10B10A2_UNORM;
|
||||
default: throw std::runtime_error("Unsupported canonical RR D3D12 format");
|
||||
}
|
||||
}
|
||||
static void barrier(ID3D12GraphicsCommandList* cb, ID3D12Resource* image, D3D12_RESOURCE_STATES from, D3D12_RESOURCE_STATES to) {
|
||||
D3D12_RESOURCE_BARRIER b{}; b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
b.Transition = {image, D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, from, to}; cb->ResourceBarrier(1, &b);
|
||||
}
|
||||
static D3D12_TEXTURE_COPY_LOCATION placed(ID3D12Resource* resource, VkDeviceSize offset, VkDeviceSize pitch,
|
||||
DXGI_FORMAT f, uint32_t width, uint32_t height) {
|
||||
D3D12_TEXTURE_COPY_LOCATION result{}; result.pResource = resource;
|
||||
result.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; result.PlacedFootprint.Offset = offset;
|
||||
result.PlacedFootprint.Footprint = {f, width, height, 1, UINT(pitch)}; return result;
|
||||
}
|
||||
static D3D12_TEXTURE_COPY_LOCATION texture(ID3D12Resource* resource) {
|
||||
D3D12_TEXTURE_COPY_LOCATION result{}; result.pResource = resource;
|
||||
result.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; return result;
|
||||
}
|
||||
D3D12Frames(const Layout& l, ID3D12Device* device, DXGI_FORMAT depthFormat = DXGI_FORMAT_UNKNOWN) : layout(l) {
|
||||
auto create = [&](DXGI_FORMAT f, uint32_t width, uint32_t height, bool isOutput, ID3D12Resource** resource) {
|
||||
D3D12_HEAP_PROPERTIES heap{}; heap.Type = D3D12_HEAP_TYPE_DEFAULT;
|
||||
D3D12_RESOURCE_DESC desc{}; desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; desc.Format = f;
|
||||
desc.Width = width; desc.Height = height; desc.DepthOrArraySize = desc.MipLevels = 1; desc.SampleDesc.Count = 1;
|
||||
if (isOutput) desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
|
||||
if (f == DXGI_FORMAT_D32_FLOAT || f == DXGI_FORMAT_R32G8X24_TYPELESS || f == DXGI_FORMAT_D32_FLOAT_S8X24_UINT)
|
||||
desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
|
||||
checkD3D(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc,
|
||||
isOutput ? D3D12_RESOURCE_STATE_UNORDERED_ACCESS : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE,
|
||||
nullptr, IID_PPV_ARGS(resource)));
|
||||
};
|
||||
for (size_t i = 0; i < inputs.size(); ++i) if (l.planes[i].enabled)
|
||||
create(i == 2 && depthFormat != DXGI_FORMAT_UNKNOWN ? depthFormat : format(l.planes[i].format), l.rw, l.rh, false, inputs[i].GetAddressOf());
|
||||
create(DXGI_FORMAT_R16G16B16A16_FLOAT, l.ow, l.oh, true, output.GetAddressOf());
|
||||
}
|
||||
void recordInputs(ID3D12GraphicsCommandList* cb, ID3D12Resource* upload, ID3D12Resource* shared, uint32_t optionalMask = 15) {
|
||||
for (size_t i = 0; i < inputs.size(); ++i) {
|
||||
if (!inputs[i]) continue;
|
||||
if (i >= 7 && !(optionalMask & (1u << (i - 7)))) continue;
|
||||
auto* input = inputs[i].Get(); const auto& plane = layout.planes[i];
|
||||
auto copyFormat = format(plane.format);
|
||||
if (i == 2) {
|
||||
Microsoft::WRL::ComPtr<ID3D12Device> device; checkD3D(input->GetDevice(IID_PPV_ARGS(&device)));
|
||||
D3D12_RESOURCE_DESC desc{};
|
||||
#if defined(__MINGW32__)
|
||||
input->GetDesc(&desc);
|
||||
#else
|
||||
desc = input->GetDesc();
|
||||
#endif
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{};
|
||||
device->GetCopyableFootprints(&desc, 0, 1, 0, &footprint, nullptr, nullptr, nullptr);
|
||||
copyFormat = footprint.Footprint.Format;
|
||||
}
|
||||
auto src = placed(upload, plane.offset, plane.pitch, copyFormat, layout.rw, layout.rh);
|
||||
auto image = texture(input);
|
||||
barrier(cb, input, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
cb->CopyTextureRegion(&image, 0, 0, 0, &src, nullptr);
|
||||
barrier(cb, input, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
||||
auto dst = placed(shared, plane.offset, plane.pitch, copyFormat, layout.rw, layout.rh);
|
||||
cb->CopyTextureRegion(&dst, 0, 0, 0, &image, nullptr);
|
||||
barrier(cb, input, D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
|
||||
}
|
||||
}
|
||||
void recordOutput(ID3D12GraphicsCommandList* cb, ID3D12Resource* shared, ID3D12Resource* readback) {
|
||||
auto src = placed(shared, layout.outputOffset, layout.outputPitch, DXGI_FORMAT_R16G16B16A16_FLOAT, layout.ow, layout.oh);
|
||||
auto image = texture(output.Get());
|
||||
barrier(cb, output.Get(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
cb->CopyTextureRegion(&image, 0, 0, 0, &src, nullptr);
|
||||
barrier(cb, output.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
||||
auto dst = placed(readback, layout.outputOffset, layout.outputPitch, DXGI_FORMAT_R16G16B16A16_FLOAT, layout.ow, layout.oh);
|
||||
cb->CopyTextureRegion(&dst, 0, 0, 0, &image, nullptr);
|
||||
barrier(cb, output.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
}
|
||||
};
|
||||
struct Image {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED; bool initialized = false;
|
||||
VkDevice device; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
|
||||
~Image() { if (view) vkDestroyImageView(device, view, nullptr); if (image) vkDestroyImage(device, image, nullptr); if (memory) vkFreeMemory(device, memory, nullptr); }
|
||||
};
|
||||
struct Replay {
|
||||
const Layout& layout;
|
||||
VkPhysicalDevice physical; VkDevice device;
|
||||
std::array<std::unique_ptr<Image>, 11> images;
|
||||
std::array<std::vector<std::unique_ptr<Image>>, 11> alternatives;
|
||||
std::unique_ptr<d4r::RModel> model;
|
||||
std::unique_ptr<d4r::REngine> engine;
|
||||
bool first = true;
|
||||
Replay(const Layout& l, VkPhysicalDevice physical, VkDevice device, const std::string& directory)
|
||||
: layout(l), physical(physical), device(device) {
|
||||
const auto data = read(std::filesystem::path(directory) / "rr.txt");
|
||||
const std::string manifest(data.begin(), data.end());
|
||||
if (manifest.rfind("D4RR0001", 0) != 0 || manifest.find("arithmetic gfx11") == std::string::npos)
|
||||
throw std::runtime_error("This canonical RR bridge gate requires a D/E gfx11 model");
|
||||
for (size_t i = 0; i < images.size(); ++i) if (l.planes[i].enabled) configure(i, l.planes[i].format);
|
||||
model = std::make_unique<d4r::RModel>(physical, device, directory);
|
||||
engine = std::make_unique<d4r::REngine>(*model, l.rw, l.rh, l.ow, l.oh);
|
||||
}
|
||||
// Cache views by format: REngine caches descriptors referencing old views,
|
||||
// so they must remain alive until the engine is destroyed.
|
||||
void configure(size_t i, VkFormat format) {
|
||||
if (images.at(i) && images[i]->format == format) return;
|
||||
for (auto& previous : alternatives[i]) if (previous->format == format) {
|
||||
images[i].swap(previous); return;
|
||||
}
|
||||
VkFormatProperties properties{}; vkGetPhysicalDeviceFormatProperties(physical, format, &properties);
|
||||
const auto required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
|
||||
if ((properties.optimalTilingFeatures & required) != required)
|
||||
throw std::runtime_error("RR input format lacks Vulkan sampled/transfer support");
|
||||
VkPhysicalDeviceMemoryProperties memory; vkGetPhysicalDeviceMemoryProperties(physical, &memory);
|
||||
auto image = std::make_unique<Image>(); image->device = device; image->format = format;
|
||||
VkImageCreateInfo ci{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; ci.imageType = VK_IMAGE_TYPE_2D;
|
||||
ci.format = format; ci.extent = {layout.rw, layout.rh, 1}; ci.mipLevels = ci.arrayLayers = 1;
|
||||
ci.samples = VK_SAMPLE_COUNT_1_BIT; ci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
check(vkCreateImage(device, &ci, nullptr, &image->image), "RR input image");
|
||||
VkMemoryRequirements req; vkGetImageMemoryRequirements(device, image->image, &req);
|
||||
uint32_t type = memory.memoryTypeCount;
|
||||
for (uint32_t n = 0; n < memory.memoryTypeCount; ++n)
|
||||
if ((req.memoryTypeBits & (1u << n)) && (memory.memoryTypes[n].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { type = n; break; }
|
||||
if (type == memory.memoryTypeCount) throw std::runtime_error("No RR image memory type");
|
||||
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; ai.allocationSize = req.size; ai.memoryTypeIndex = type;
|
||||
check(vkAllocateMemory(device, &ai, nullptr, &image->memory), "RR image memory");
|
||||
check(vkBindImageMemory(device, image->image, image->memory, 0), "Bind RR image");
|
||||
VkImageViewCreateInfo vi{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; vi.image = image->image;
|
||||
vi.viewType = VK_IMAGE_VIEW_TYPE_2D; vi.format = ci.format; vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
check(vkCreateImageView(device, &vi, nullptr, &image->view), "RR input view");
|
||||
if (images[i]) alternatives[i].push_back(std::move(images[i]));
|
||||
images[i] = std::move(image);
|
||||
}
|
||||
void record(VkCommandBuffer cb, VkBuffer shared, unsigned frame) {
|
||||
uint32_t mask = 0;
|
||||
for (unsigned i = 0; i < 4; ++i) if (layout.frames.at(frame).optional[i]) mask |= 1u << i;
|
||||
record(cb, shared, layout.frames.at(frame).settings, mask);
|
||||
}
|
||||
// Own settings at the NGX submission boundary; never retain game pointers.
|
||||
void record(VkCommandBuffer cb, VkBuffer shared, const d4r::RFrameSettings& settings, uint32_t optionalMask = 15) {
|
||||
for (size_t i = 0; i < images.size(); ++i) {
|
||||
if (!layout.planes[i].enabled) continue;
|
||||
if (i >= 7 && !(optionalMask & (1u << (i - 7)))) continue;
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; b.image = images[i]->image;
|
||||
b.oldLayout = !images[i]->initialized ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
b.srcAccessMask = !images[i]->initialized ? 0 : VK_ACCESS_SHADER_READ_BIT; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
const auto& plane = layout.planes[i];
|
||||
VkBufferImageCopy copy{}; copy.bufferOffset = plane.offset; copy.bufferRowLength = uint32_t(plane.pitch / plane.bytes);
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {layout.rw, layout.rh, 1};
|
||||
vkCmdCopyBufferToImage(cb, shared, images[i]->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
||||
images[i]->initialized = true;
|
||||
b.oldLayout = b.newLayout; b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
}
|
||||
d4r::RFrameImages inputs{}; inputs.color = images[0]->view; inputs.motion = images[1]->view; inputs.depth = images[2]->view;
|
||||
inputs.normals = images[3]->view; inputs.roughness = images[4]->view; inputs.diffuseAlbedo = images[5]->view; inputs.specularAlbedo = images[6]->view;
|
||||
VkImageView* optional[] = {&inputs.specularHitDistance, &inputs.disocclusionMask, &inputs.subsurfaceGuide, &inputs.colorBeforeParticles};
|
||||
for (size_t i = 7; i < images.size(); ++i) if (layout.planes[i].enabled && (optionalMask & (1u << (i - 7)))) *optional[i - 7] = images[i]->view;
|
||||
engine->setFrame(inputs, settings);
|
||||
if (first) { model->recordUpload(cb); engine->recordInitialize(cb); first = false; }
|
||||
engine->recordFrame(cb);
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; b.image = engine->outputImage();
|
||||
b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_GENERAL; b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; b.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; b.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
VkBufferImageCopy copy{}; copy.bufferOffset = layout.outputOffset; copy.bufferRowLength = uint32_t(layout.outputPitch / 8);
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {layout.ow, layout.oh, 1};
|
||||
vkCmdCopyImageToBuffer(cb, engine->outputImage(), VK_IMAGE_LAYOUT_GENERAL, shared, 1, ©);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Native D3D12/Vulkan prerequisite query for the RR engine adapter. No dispatch.
|
||||
#include <windows.h>
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_4.h>
|
||||
#include <volk.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
int main() {
|
||||
IDXGIFactory4* factory = nullptr;
|
||||
if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) return 1;
|
||||
IDXGIAdapter1* selected = nullptr;
|
||||
DXGI_ADAPTER_DESC1 adapter{};
|
||||
for (UINT i = 0;; ++i) {
|
||||
IDXGIAdapter1* next = nullptr;
|
||||
if (factory->EnumAdapters1(i, &next) == DXGI_ERROR_NOT_FOUND) break;
|
||||
if (!next) break;
|
||||
DXGI_ADAPTER_DESC1 desc{};
|
||||
next->GetDesc1(&desc);
|
||||
if (desc.VendorId == 0x1002 && !(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)) {
|
||||
selected = next; adapter = desc; break;
|
||||
}
|
||||
next->Release();
|
||||
}
|
||||
factory->Release();
|
||||
if (!selected) { std::puts("FAIL no native AMD D3D12 adapter"); return 1; }
|
||||
ID3D12Device* device = nullptr;
|
||||
const HRESULT created = D3D12CreateDevice(selected, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device));
|
||||
selected->Release();
|
||||
if (FAILED(created)) { std::puts("FAIL D3D12 device creation"); return 1; }
|
||||
device->Release();
|
||||
if (volkInitialize() != VK_SUCCESS) return 1;
|
||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion = VK_API_VERSION_1_3;
|
||||
VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &app;
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
if (vkCreateInstance(&info, nullptr, &instance) != VK_SUCCESS) return 1;
|
||||
volkLoadInstance(instance);
|
||||
uint32_t count = 0;
|
||||
vkEnumeratePhysicalDevices(instance, &count, nullptr);
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
vkEnumeratePhysicalDevices(instance, &count, devices.data());
|
||||
bool ready = false;
|
||||
for (auto physical : devices) {
|
||||
VkPhysicalDeviceIDProperties id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &id};
|
||||
vkGetPhysicalDeviceProperties2(physical, &properties);
|
||||
if (!id.deviceLUIDValid || std::memcmp(id.deviceLUID, &adapter.AdapterLuid, VK_LUID_SIZE)) continue;
|
||||
uint32_t extensionCount = 0;
|
||||
vkEnumerateDeviceExtensionProperties(physical, nullptr, &extensionCount, nullptr);
|
||||
std::vector<VkExtensionProperties> extensions(extensionCount);
|
||||
vkEnumerateDeviceExtensionProperties(physical, nullptr, &extensionCount, extensions.data());
|
||||
auto has = [&](const char* name) {
|
||||
return std::any_of(extensions.begin(), extensions.end(), [&](const auto& e) { return !std::strcmp(name, e.extensionName); });
|
||||
};
|
||||
VkPhysicalDeviceExternalBufferInfo buffer{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_BUFFER_INFO};
|
||||
buffer.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
buffer.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
|
||||
VkExternalBufferProperties memory{VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES};
|
||||
vkGetPhysicalDeviceExternalBufferProperties(physical, &buffer, &memory);
|
||||
VkPhysicalDeviceExternalSemaphoreInfo semaphore{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_SEMAPHORE_INFO};
|
||||
semaphore.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT;
|
||||
VkExternalSemaphoreProperties fence{VK_STRUCTURE_TYPE_EXTERNAL_SEMAPHORE_PROPERTIES};
|
||||
vkGetPhysicalDeviceExternalSemaphoreProperties(physical, &semaphore, &fence);
|
||||
const bool memoryImport = (memory.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0;
|
||||
const bool fenceImport = (fence.externalSemaphoreFeatures & VK_EXTERNAL_SEMAPHORE_FEATURE_IMPORTABLE_BIT) != 0;
|
||||
const bool memoryExtension = has("VK_KHR_external_memory_win32");
|
||||
const bool fenceExtension = has("VK_KHR_external_semaphore_win32");
|
||||
std::printf("RR_INTEROP device=%s luid_match=1 memory_win32=%u d3d12_buffer_import=%u dedicated_only=%u semaphore_win32=%u d3d12_fence_import=%u\n",
|
||||
properties.properties.deviceName, memoryExtension, memoryImport,
|
||||
unsigned((memory.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0),
|
||||
fenceExtension, fenceImport);
|
||||
ready = memoryExtension && memoryImport && fenceExtension && fenceImport;
|
||||
}
|
||||
vkDestroyInstance(instance, nullptr);
|
||||
std::puts(ready ? "PASS RR_INTEROP_CAPABILITIES import_or_dispatch_executed=0" : "FAIL RR_INTEROP_CAPABILITIES");
|
||||
return ready ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// D3D12 -> Vulkan -> D3D12 shared-buffer/fence gate for native RR integration.
|
||||
// Synthetic transfers and optional local RR model replay. No NGX, CUDA, or game.
|
||||
#define WIDL_EXPLICIT_AGGREGATE_RETURNS
|
||||
#include <windows.h>
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_4.h>
|
||||
#include <wrl/client.h>
|
||||
#include <volk.h>
|
||||
#include <vulkan/vulkan_win32.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "rr_bridge_replay.h"
|
||||
#include "rr_native_packet.h"
|
||||
using Microsoft::WRL::ComPtr;
|
||||
namespace {
|
||||
void dx(HRESULT value, const char* operation) {
|
||||
if (FAILED(value)) throw std::runtime_error(std::string(operation) + " HRESULT=" + std::to_string(value));
|
||||
}
|
||||
void vk(VkResult value, const char* operation) {
|
||||
if (value != VK_SUCCESS) throw std::runtime_error(std::string(operation) + " VkResult=" + std::to_string(value));
|
||||
}
|
||||
struct Handle { HANDLE value = nullptr; ~Handle() { if (value) CloseHandle(value); } };
|
||||
ComPtr<ID3D12Resource> buffer(ID3D12Device* device, UINT64 bytes, D3D12_HEAP_TYPE type,
|
||||
D3D12_HEAP_FLAGS flags, D3D12_RESOURCE_STATES state) {
|
||||
D3D12_HEAP_PROPERTIES heap{}; heap.Type = type;
|
||||
D3D12_RESOURCE_DESC desc{}; desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
|
||||
desc.Width = bytes; desc.Height = desc.DepthOrArraySize = desc.MipLevels = 1;
|
||||
desc.SampleDesc.Count = 1; desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
|
||||
if (type == D3D12_HEAP_TYPE_DEFAULT) desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
|
||||
ComPtr<ID3D12Resource> result;
|
||||
dx(device->CreateCommittedResource(&heap, flags, &desc, state, nullptr, IID_PPV_ARGS(&result)), "Create buffer");
|
||||
return result;
|
||||
}
|
||||
void transition(ID3D12GraphicsCommandList* list, ID3D12Resource* resource,
|
||||
D3D12_RESOURCE_STATES from, D3D12_RESOURCE_STATES to) {
|
||||
D3D12_RESOURCE_BARRIER b{}; b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
b.Transition = {resource, D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, from, to};
|
||||
list->ResourceBarrier(1, &b);
|
||||
}
|
||||
struct Vulkan {
|
||||
std::unique_ptr<rr_bridge::NativeDevice> native;
|
||||
std::unique_ptr<rr_bridge::NativePacket> packet;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkBuffer shared = VK_NULL_HANDLE, readback = VK_NULL_HANDLE;
|
||||
VkDeviceMemory host = VK_NULL_HANDLE;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory imageMemory = VK_NULL_HANDLE;
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
bool submitted = false;
|
||||
std::unique_ptr<rr_bridge::Replay> replay;
|
||||
~Vulkan() {
|
||||
// The outer process timeout also bounds a stalled driver call. Never
|
||||
// destroy resources that a failed/timed-out submission might still use.
|
||||
if (submitted) { (void)replay.release(); (void)packet.release(); (void)native.release(); return; }
|
||||
replay.reset();
|
||||
if (device) {
|
||||
if (fence) vkDestroyFence(device, fence, nullptr);
|
||||
if (pool) vkDestroyCommandPool(device, pool, nullptr);
|
||||
|
||||
|
||||
if (readback) vkDestroyBuffer(device, readback, nullptr);
|
||||
if (image) vkDestroyImage(device, image, nullptr);
|
||||
|
||||
if (host) vkFreeMemory(device, host, nullptr);
|
||||
if (imageMemory) vkFreeMemory(device, imageMemory, nullptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
int main(int argc, char** argv) try {
|
||||
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
||||
UINT64 bytes = 1u << 20;
|
||||
unsigned iterations = 8;
|
||||
uint32_t imageWidth = 0, imageHeight = 0;
|
||||
std::string rrModel, rrInputs, rrOutput, rrReference;
|
||||
for (int i = 1; i < argc; i += 2) {
|
||||
if (i + 1 == argc) throw std::runtime_error("Missing argument value");
|
||||
const std::string key = argv[i];
|
||||
size_t end = 0;
|
||||
const std::string text = argv[i + 1];
|
||||
if (key == "--rr-model") { rrModel = text; continue; }
|
||||
if (key == "--rr-inputs") { rrInputs = text; continue; }
|
||||
if (key == "--rr-output") { rrOutput = text; continue; }
|
||||
if (key == "--rr-reference") { rrReference = text; continue; }
|
||||
if (text.empty() || text[0] == '-') throw std::runtime_error("Expected positive argument");
|
||||
const auto value = std::stoull(text, &end);
|
||||
if (end != text.size()) throw std::runtime_error("Invalid numeric argument");
|
||||
if (key == "--bytes") bytes = value;
|
||||
else if (key == "--iterations" && value <= 32) iterations = unsigned(value);
|
||||
else if (key == "--image-width" && value <= 8192) imageWidth = uint32_t(value);
|
||||
else if (key == "--image-height" && value <= 8192) imageHeight = uint32_t(value);
|
||||
else throw std::runtime_error("Unknown or out-of-range argument");
|
||||
}
|
||||
std::unique_ptr<rr_bridge::Layout> rrLayout;
|
||||
if (!rrModel.empty()) {
|
||||
if (rrInputs.empty() || rrOutput.empty() || imageWidth || imageHeight) throw std::runtime_error("RR replay needs model/inputs/output and no image-clear mode");
|
||||
rrLayout = std::make_unique<rr_bridge::Layout>(rrInputs, rrOutput, rrReference);
|
||||
bytes = rrLayout->bytes; iterations = unsigned(rrLayout->frames.size());
|
||||
} else if (!rrInputs.empty() || !rrOutput.empty() || !rrReference.empty()) throw std::runtime_error("Missing RR model");
|
||||
if (!bytes || bytes % 4 || bytes > (64u << 20) || !iterations)
|
||||
throw std::runtime_error("Need a 4-byte-aligned buffer (4 bytes..64MiB) and 1..32 iterations");
|
||||
const UINT64 rowPitch = (UINT64(imageWidth) * 8 + 255) & ~UINT64(255);
|
||||
const bool imageMode = imageWidth && imageHeight;
|
||||
if (bool(imageWidth) != bool(imageHeight) || (imageMode && bytes != rowPitch * imageHeight))
|
||||
throw std::runtime_error("Image dimensions must match the 256-byte-padded RGBA16F buffer size");
|
||||
ComPtr<IDXGIFactory4> factory; dx(CreateDXGIFactory1(IID_PPV_ARGS(&factory)), "DXGI factory");
|
||||
ComPtr<IDXGIAdapter1> adapter;
|
||||
DXGI_ADAPTER_DESC1 description{};
|
||||
bool selected = false;
|
||||
for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) {
|
||||
dx(adapter->GetDesc1(&description), "DXGI adapter description");
|
||||
if (description.VendorId == 0x1002 && !(description.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)) { selected = true; break; }
|
||||
adapter.Reset();
|
||||
}
|
||||
if (!selected) throw std::runtime_error("No AMD D3D12 adapter");
|
||||
ComPtr<ID3D12Device> d3d; dx(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&d3d)), "D3D12 device");
|
||||
ComPtr<ID3D12CommandQueue> queue;
|
||||
D3D12_COMMAND_QUEUE_DESC q{}; q.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
||||
dx(d3d->CreateCommandQueue(&q, IID_PPV_ARGS(&queue)), "D3D12 queue");
|
||||
auto shared = buffer(d3d.Get(), bytes, D3D12_HEAP_TYPE_DEFAULT, D3D12_HEAP_FLAG_SHARED, D3D12_RESOURCE_STATE_COMMON);
|
||||
auto upload = buffer(d3d.Get(), bytes, D3D12_HEAP_TYPE_UPLOAD, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_GENERIC_READ);
|
||||
auto readback = buffer(d3d.Get(), bytes, D3D12_HEAP_TYPE_READBACK, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
std::unique_ptr<rr_bridge::D3D12Frames> d3dFrames;
|
||||
if (rrLayout) d3dFrames = std::make_unique<rr_bridge::D3D12Frames>(*rrLayout, d3d.Get());
|
||||
ComPtr<ID3D12Fence> d3dFence; dx(d3d->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&d3dFence)), "Shared D3D12 fence");
|
||||
Handle memoryHandle, fenceHandle, event;
|
||||
dx(d3d->CreateSharedHandle(shared.Get(), nullptr, GENERIC_ALL, nullptr, &memoryHandle.value), "Shared buffer handle");
|
||||
dx(d3d->CreateSharedHandle(d3dFence.Get(), nullptr, GENERIC_ALL, nullptr, &fenceHandle.value), "Shared fence handle");
|
||||
event.value = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
if (!event.value) throw std::runtime_error("Fence event creation failed");
|
||||
Vulkan v;
|
||||
v.native = std::make_unique<rr_bridge::NativeDevice>();
|
||||
v.native->initialize(description.AdapterLuid, bool(rrLayout));
|
||||
v.device = v.native->device;
|
||||
const auto physical = v.native->physical;
|
||||
const auto family = v.native->family;
|
||||
const auto vkQueue = v.native->queue;
|
||||
if (rrLayout) v.replay = std::make_unique<rr_bridge::Replay>(*rrLayout, physical, v.device, rrModel);
|
||||
v.packet = std::make_unique<rr_bridge::NativePacket>(*v.native);
|
||||
v.packet->initialize(bytes, memoryHandle.value, fenceHandle.value);
|
||||
v.shared = v.packet->buffer; v.semaphore = v.packet->semaphore;
|
||||
VkPhysicalDeviceMemoryProperties memory{}; vkGetPhysicalDeviceMemoryProperties(physical, &memory);
|
||||
auto memoryType = [&](uint32_t bits, VkMemoryPropertyFlags flags) {
|
||||
for (uint32_t i = 0; i < memory.memoryTypeCount; ++i)
|
||||
if ((bits & (1u << i)) && (memory.memoryTypes[i].propertyFlags & flags) == flags) return i;
|
||||
throw std::runtime_error("No compatible Vulkan memory type");
|
||||
};
|
||||
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bi.size = bytes;
|
||||
bi.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
VkMemoryRequirements req{};
|
||||
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
vk(vkCreateBuffer(v.device, &bi, nullptr, &v.readback), "Vulkan readback buffer");
|
||||
vkGetBufferMemoryRequirements(v.device, v.readback, &req);
|
||||
ai.pNext = nullptr; ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
vk(vkAllocateMemory(v.device, &ai, nullptr, &v.host), "Vulkan readback memory");
|
||||
vk(vkBindBufferMemory(v.device, v.readback, v.host, 0), "Bind readback");
|
||||
if (imageMode) {
|
||||
VkImageCreateInfo image{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
image.imageType = VK_IMAGE_TYPE_2D; image.format = VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
image.extent = {imageWidth, imageHeight, 1}; image.mipLevels = image.arrayLayers = 1;
|
||||
image.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
image.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
|
||||
vk(vkCreateImage(v.device, &image, nullptr, &v.image), "Canonical RR image");
|
||||
vkGetImageMemoryRequirements(v.device, v.image, &req);
|
||||
ai.allocationSize = req.size; ai.memoryTypeIndex = memoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
vk(vkAllocateMemory(v.device, &ai, nullptr, &v.imageMemory), "Canonical RR image memory");
|
||||
vk(vkBindImageMemory(v.device, v.image, v.imageMemory, 0), "Bind canonical image");
|
||||
}
|
||||
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; pci.queueFamilyIndex = family;
|
||||
vk(vkCreateCommandPool(v.device, &pci, nullptr, &v.pool), "Vulkan command pool");
|
||||
VkCommandBufferAllocateInfo cai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; cai.commandPool = v.pool; cai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cai.commandBufferCount = 1;
|
||||
VkCommandBuffer cb; vk(vkAllocateCommandBuffers(v.device, &cai, &cb), "Vulkan command buffer");
|
||||
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; vk(vkCreateFence(v.device, &fci, nullptr, &v.fence), "Vulkan completion fence");
|
||||
std::puts("RR_INTEROP_IMPORTED memory=dedicated_d3d12_resource semaphore=d3d12_timeline_fence luid_match=1");
|
||||
ComPtr<ID3D12CommandAllocator> producerAllocator, consumerAllocator;
|
||||
ComPtr<ID3D12GraphicsCommandList> producer, consumer;
|
||||
dx(d3d->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&producerAllocator)), "Producer allocator");
|
||||
dx(d3d->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&consumerAllocator)), "Consumer allocator");
|
||||
dx(d3d->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, producerAllocator.Get(), nullptr, IID_PPV_ARGS(&producer)), "Producer list");
|
||||
dx(d3d->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, consumerAllocator.Get(), nullptr, IID_PPV_ARGS(&consumer)), "Consumer list");
|
||||
dx(producer->Close(), "Initial producer close"); dx(consumer->Close(), "Initial consumer close");
|
||||
auto pattern = [](unsigned iteration, uint32_t word) { return (word * 2654435761u) ^ (0x13579bdfu + iteration * 97u); };
|
||||
for (unsigned iteration = 0; iteration < iterations; ++iteration) {
|
||||
uint32_t* input = nullptr; D3D12_RANGE noRead{};
|
||||
dx(upload->Map(0, &noRead, reinterpret_cast<void**>(&input)), "Upload map");
|
||||
if (rrLayout) rrLayout->upload(input, iteration);
|
||||
else for (uint32_t word = 0; word < bytes / 4; ++word) input[word] = pattern(iteration, word);
|
||||
upload->Unmap(0, nullptr);
|
||||
dx(producerAllocator->Reset(), "Producer allocator reset"); dx(producer->Reset(producerAllocator.Get(), nullptr), "Producer reset");
|
||||
transition(producer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
if (d3dFrames) d3dFrames->recordInputs(producer.Get(), upload.Get(), shared.Get());
|
||||
else producer->CopyBufferRegion(shared.Get(), 0, upload.Get(), 0, bytes);
|
||||
transition(producer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON);
|
||||
dx(producer->Close(), "Producer close"); ID3D12CommandList* p[] = {producer.Get()}; queue->ExecuteCommandLists(1, p);
|
||||
const uint64_t ready = iteration * 3 + 1, done = ready + 1, complete = ready + 2;
|
||||
dx(queue->Signal(d3dFence.Get(), ready), "D3D12 producer signal");
|
||||
vk(vkResetCommandPool(v.device, v.pool, 0), "Vulkan pool reset");
|
||||
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; vk(vkBeginCommandBuffer(cb, &begin), "Vulkan begin");
|
||||
VkBufferMemoryBarrier acquire{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; acquire.buffer = v.shared; acquire.size = VK_WHOLE_SIZE;
|
||||
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL; acquire.dstQueueFamilyIndex = family; acquire.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
if (rrLayout) acquire.dstAccessMask |= VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &acquire, 0, nullptr);
|
||||
VkBufferImageCopy imageRegion{};
|
||||
imageRegion.bufferRowLength = uint32_t(rowPitch / 8);
|
||||
imageRegion.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
imageRegion.imageExtent = {imageWidth, imageHeight, 1};
|
||||
auto imageBarrier = [&](VkImageLayout from, VkImageLayout to, VkAccessFlags source, VkAccessFlags destination) {
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
b.image = v.image; b.oldLayout = from; b.newLayout = to;
|
||||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; b.srcAccessMask = source; b.dstAccessMask = destination;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
};
|
||||
if (rrLayout) {
|
||||
v.replay->record(cb, v.shared, iteration);
|
||||
} else if (imageMode) {
|
||||
vkCmdFillBuffer(cb, v.readback, 0, bytes, 0xdeadbeefu);
|
||||
VkBufferMemoryBarrier fill{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; fill.buffer = v.readback; fill.size = VK_WHOLE_SIZE;
|
||||
fill.srcQueueFamilyIndex = fill.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
fill.srcAccessMask = fill.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &fill, 0, nullptr);
|
||||
imageBarrier(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
vkCmdCopyBufferToImage(cb, v.shared, v.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &imageRegion);
|
||||
imageBarrier(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
vkCmdCopyImageToBuffer(cb, v.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, v.readback, 1, &imageRegion);
|
||||
imageBarrier(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
VkClearColorValue clear{}; clear.float32[0] = iteration % 2 ? .5f : .25f;
|
||||
clear.float32[1] = .5f; clear.float32[2] = .75f; clear.float32[3] = 1.f;
|
||||
VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
vkCmdClearColorImage(cb, v.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range);
|
||||
imageBarrier(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
} else {
|
||||
VkBufferCopy region{0, 0, bytes}; vkCmdCopyBuffer(cb, v.shared, v.readback, 1, ®ion);
|
||||
}
|
||||
VkBufferMemoryBarrier afterRead{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; afterRead.buffer = v.shared; afterRead.size = VK_WHOLE_SIZE;
|
||||
afterRead.srcQueueFamilyIndex = afterRead.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
afterRead.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; afterRead.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
if (!rrLayout) vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &afterRead, 0, nullptr);
|
||||
const uint32_t output = 0xa5a50000u | iteration;
|
||||
if (!rrLayout) vkCmdFillBuffer(cb, v.shared, 0, bytes, output);
|
||||
if (imageMode) {
|
||||
afterRead.srcAccessMask = afterRead.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &afterRead, 0, nullptr);
|
||||
vkCmdCopyImageToBuffer(cb, v.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, v.shared, 1, &imageRegion);
|
||||
}
|
||||
VkBufferMemoryBarrier release = acquire; release.srcQueueFamilyIndex = family; release.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
|
||||
release.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; release.dstAccessMask = 0;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 1, &release, 0, nullptr);
|
||||
VkBufferMemoryBarrier hostRead{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; hostRead.buffer = v.readback; hostRead.size = VK_WHOLE_SIZE;
|
||||
hostRead.srcQueueFamilyIndex = hostRead.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
hostRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; hostRead.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &hostRead, 0, nullptr);
|
||||
vk(vkEndCommandBuffer(cb), "Vulkan end");
|
||||
VkTimelineSemaphoreSubmitInfo values{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO};
|
||||
values.waitSemaphoreValueCount = values.signalSemaphoreValueCount = 1; values.pWaitSemaphoreValues = &ready; values.pSignalSemaphoreValues = &done;
|
||||
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO, &values}; submit.waitSemaphoreCount = submit.signalSemaphoreCount = 1;
|
||||
submit.pWaitSemaphores = submit.pSignalSemaphores = &v.semaphore; submit.pWaitDstStageMask = &waitStage; submit.commandBufferCount = 1; submit.pCommandBuffers = &cb;
|
||||
vk(vkResetFences(v.device, 1, &v.fence), "Vulkan fence reset");
|
||||
vk(vkQueueSubmit(vkQueue, 1, &submit, v.fence), "Vulkan shared-buffer submission"); v.submitted = true;
|
||||
dx(queue->Wait(d3dFence.Get(), done), "D3D12 wait for Vulkan");
|
||||
dx(consumerAllocator->Reset(), "Consumer allocator reset"); dx(consumer->Reset(consumerAllocator.Get(), nullptr), "Consumer reset");
|
||||
transition(consumer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
||||
if (d3dFrames) d3dFrames->recordOutput(consumer.Get(), shared.Get(), readback.Get());
|
||||
else consumer->CopyBufferRegion(readback.Get(), 0, shared.Get(), 0, bytes);
|
||||
transition(consumer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON);
|
||||
dx(consumer->Close(), "Consumer close"); ID3D12CommandList* c[] = {consumer.Get()}; queue->ExecuteCommandLists(1, c);
|
||||
dx(queue->Signal(d3dFence.Get(), complete), "D3D12 consumer signal");
|
||||
dx(d3dFence->SetEventOnCompletion(complete, event.value), "Completion event");
|
||||
if (WaitForSingleObject(event.value, 5000) != WAIT_OBJECT_0) throw std::runtime_error("D3D12/Vulkan handoff timeout");
|
||||
vk(vkWaitForFences(v.device, 1, &v.fence, VK_TRUE, 5000000000ull), "Vulkan completion wait"); v.submitted = false;
|
||||
if (rrLayout) {
|
||||
void* data = nullptr; D3D12_RANGE range{0, SIZE_T(bytes)};
|
||||
dx(readback->Map(0, &range, &data), "RR D3D12 output map");
|
||||
try { rrLayout->validate(data, iteration); } catch (...) { D3D12_RANGE empty{}; readback->Unmap(0, &empty); throw; }
|
||||
D3D12_RANGE empty{}; readback->Unmap(0, &empty);
|
||||
continue;
|
||||
}
|
||||
uint32_t* first = nullptr; vk(vkMapMemory(v.device, v.host, 0, bytes, 0, reinterpret_cast<void**>(&first)), "Vulkan readback map");
|
||||
auto padding = [&](uint32_t word) { return imageMode && word % (rowPitch / 4) >= imageWidth * 2; };
|
||||
size_t mismatches = 0;
|
||||
for (uint32_t word = 0; word < bytes / 4; ++word)
|
||||
mismatches += first[word] != (padding(word) ? 0xdeadbeefu : pattern(iteration, word));
|
||||
vkUnmapMemory(v.device, v.host);
|
||||
uint32_t* second = nullptr; D3D12_RANGE readRange{0, SIZE_T(bytes)};
|
||||
dx(readback->Map(0, &readRange, reinterpret_cast<void**>(&second)), "D3D12 readback map");
|
||||
for (uint32_t word = 0; word < bytes / 4; ++word) {
|
||||
const uint32_t imageWord = word % 2 ? 0x3c003a00u : (iteration % 2 ? 0x38003800u : 0x38003400u);
|
||||
const uint32_t expected = imageMode && !padding(word) ? imageWord : output;
|
||||
mismatches += second[word] != expected;
|
||||
}
|
||||
D3D12_RANGE noWrite{}; readback->Unmap(0, &noWrite);
|
||||
if (mismatches) throw std::runtime_error("Shared-buffer contents differ: " + std::to_string(mismatches));
|
||||
std::printf("RR_INTEROP_ITERATION index=%u bytes=%llu exact=1 producer_value=%llu vulkan_value=%llu consumer_value=%llu\n",
|
||||
iteration, static_cast<unsigned long long>(bytes), static_cast<unsigned long long>(ready), static_cast<unsigned long long>(done), static_cast<unsigned long long>(complete));
|
||||
}
|
||||
if (rrLayout) std::printf("PASS RR_BRIDGED_INFERENCE frames=%u guides=7 d3d12_textures=1 gpu_fence_handoff=1 standalone_reference=%u\n", iterations, !rrReference.empty());
|
||||
else std::printf("PASS RR_INTEROP_TRANSFER iterations=%u d3d12_to_vulkan_exact=1 vulkan_to_d3d12_exact=1 gpu_fence_handoff=1 canonical_image=%u inference_executed=0\n", iterations, imageMode);
|
||||
return 0;
|
||||
} catch (const std::exception& error) { std::fprintf(stderr, "FAIL RR_INTEROP_TRANSFER %s\n", error.what()); return 1; }
|
||||
@@ -0,0 +1,291 @@
|
||||
#include <volk.h>
|
||||
#include "rr_native_backend.h"
|
||||
#include "rr_native_packet.h"
|
||||
#include "rr_bridge_replay.h"
|
||||
#include "d3d12_command_hooks.h"
|
||||
#include <mutex>
|
||||
|
||||
namespace d4r::win::rr {
|
||||
namespace {
|
||||
// The engine uses Volk's global dispatch. Serialize device creation, recording,
|
||||
// submission and destruction; reload the owning dispatch before each operation.
|
||||
std::mutex dispatchMutex;
|
||||
using Packet = rr_bridge::NativePacket;
|
||||
using Device = rr_bridge::NativeDevice;
|
||||
size_t resource_index(size_t plane) { return plane < 7 ? plane : plane + 2; }
|
||||
rr_bridge::Plane input_format(DXGI_FORMAT format, size_t plane) {
|
||||
// Match Linux's direct sampled-image route. These formats preserve the
|
||||
// stored texel values; no CPU conversion or reinterpretation is involved.
|
||||
if (plane == Snapshot::Motion) {
|
||||
if (format == DXGI_FORMAT_R16G16_FLOAT) return {nullptr, VK_FORMAT_R16G16_SFLOAT, 4};
|
||||
if (format == DXGI_FORMAT_R32G32_FLOAT) return {nullptr, VK_FORMAT_R32G32_SFLOAT, 8};
|
||||
} else if (plane == Snapshot::Depth || plane == Snapshot::HitDistance || plane == Snapshot::Disocclusion || plane == Snapshot::Subsurface) {
|
||||
if (plane == Snapshot::Depth && (format == DXGI_FORMAT_R32_TYPELESS || format == DXGI_FORMAT_D32_FLOAT ||
|
||||
format == DXGI_FORMAT_R32G8X24_TYPELESS || format == DXGI_FORMAT_D32_FLOAT_S8X24_UINT || format == DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS))
|
||||
return {nullptr, VK_FORMAT_R32_SFLOAT, 4};
|
||||
if (format == DXGI_FORMAT_R32_FLOAT) return {nullptr, VK_FORMAT_R32_SFLOAT, 4};
|
||||
if (format == DXGI_FORMAT_R16_FLOAT) return {nullptr, VK_FORMAT_R16_SFLOAT, 2};
|
||||
if (plane != Snapshot::Depth && format == DXGI_FORMAT_R8_UNORM) return {nullptr, VK_FORMAT_R8_UNORM, 1};
|
||||
} else {
|
||||
if (format == DXGI_FORMAT_R16G16B16A16_FLOAT) return {nullptr, VK_FORMAT_R16G16B16A16_SFLOAT, 8};
|
||||
if (format == DXGI_FORMAT_R32G32B32A32_FLOAT) return {nullptr, VK_FORMAT_R32G32B32A32_SFLOAT, 16};
|
||||
if (format == DXGI_FORMAT_R8G8B8A8_UNORM) return {nullptr, VK_FORMAT_R8G8B8A8_UNORM, 4};
|
||||
if (format == DXGI_FORMAT_R10G10B10A2_UNORM) return {nullptr, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4};
|
||||
if (plane == Snapshot::Roughness) {
|
||||
if (format == DXGI_FORMAT_R16_FLOAT) return {nullptr, VK_FORMAT_R16_SFLOAT, 2};
|
||||
if (format == DXGI_FORMAT_R32_FLOAT) return {nullptr, VK_FORMAT_R32_SFLOAT, 4};
|
||||
if (format == DXGI_FORMAT_R8_UNORM) return {nullptr, VK_FORMAT_R8_UNORM, 1};
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Unsupported native RR sampled input format");
|
||||
}
|
||||
DXGI_FORMAT copy_format(ID3D12Device* device, ID3D12Resource* texture, size_t plane) {
|
||||
const auto desc = resource_desc(texture);
|
||||
if (plane != Snapshot::Depth) return desc.Format;
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{}; UINT rows = 0; UINT64 rowBytes = 0, bytes = 0;
|
||||
device->GetCopyableFootprints(&desc, 0, 1, 0, &footprint, &rows, &rowBytes, &bytes);
|
||||
const auto format = input_format(desc.Format, plane);
|
||||
if (!bytes || bytes == UINT64_MAX || rows != desc.Height || rowBytes != desc.Width * format.bytes ||
|
||||
footprint.Footprint.Width != desc.Width || footprint.Footprint.Height != desc.Height)
|
||||
throw std::runtime_error("Unsupported RR depth-plane footprint");
|
||||
return footprint.Footprint.Format;
|
||||
}
|
||||
}
|
||||
struct Backend::Impl {
|
||||
Creation creation;
|
||||
rr_bridge::Layout layout;
|
||||
ComPtr<ID3D12Device> d3d;
|
||||
ComPtr<ID3D12CommandQueue> queue;
|
||||
ComPtr<ID3D12Resource> shared;
|
||||
ComPtr<ID3D12Fence> fence;
|
||||
ComPtr<ID3D12CommandAllocator> producerAllocator, consumerAllocator;
|
||||
ComPtr<ID3D12GraphicsCommandList> producer, consumer;
|
||||
Handle event;
|
||||
std::unique_ptr<Device> native;
|
||||
std::unique_ptr<Packet> packet;
|
||||
std::unique_ptr<rr_bridge::Replay> replay;
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
VkCommandBuffer command = VK_NULL_HANDLE;
|
||||
VkFence completion = VK_NULL_HANDLE;
|
||||
uint64_t sequence = 0;
|
||||
bool pending = false, failed = false;
|
||||
std::unique_ptr<Snapshot> retained;
|
||||
Impl(ID3D12Device* device, const Creation& c)
|
||||
: creation(c), layout(c.width, c.height, c.outWidth, c.outHeight), d3d(device) {
|
||||
// Reserve each guide's largest supported texel size once. Format
|
||||
// changes can then preserve the packet, model and temporal history.
|
||||
VkDeviceSize offset = 0;
|
||||
for (auto& plane : layout.planes) {
|
||||
plane.offset = (offset + 511) & ~VkDeviceSize(511);
|
||||
plane.pitch = (VkDeviceSize(layout.rw) * 16 + 255) & ~VkDeviceSize(255);
|
||||
offset = plane.offset + plane.pitch * layout.rh;
|
||||
}
|
||||
layout.outputOffset = (offset + 511) & ~VkDeviceSize(511);
|
||||
layout.bytes = layout.outputOffset + layout.outputPitch * layout.oh;
|
||||
}
|
||||
void dispatch() {
|
||||
if (native && native->instance) volkLoadInstance(native->instance);
|
||||
if (native && native->device) volkLoadDevice(native->device);
|
||||
}
|
||||
~Impl() {
|
||||
dispatch();
|
||||
replay.reset();
|
||||
if (native && native->device) {
|
||||
if (completion) vkDestroyFence(native->device, completion, nullptr);
|
||||
if (pool) vkDestroyCommandPool(native->device, pool, nullptr);
|
||||
}
|
||||
packet.reset(); native.reset();
|
||||
}
|
||||
void initialize(const std::string& directory) {
|
||||
if (!d3d) throw std::runtime_error("Null native RR D3D12 device");
|
||||
// Match the Linux RR adapter's contract: HDR and render-resolution,
|
||||
// unjittered motion. D/E already perform their own exposure reduction.
|
||||
// NVIDIA/DLSS include/nvsdk_ngx_defs.h defines these public flag bits.
|
||||
if ((creation.flags & 3u) != 3u)
|
||||
throw std::runtime_error("Native RR requires HDR and render-resolution motion vectors");
|
||||
if (creation.flags & ~(3u | 8u | 64u))
|
||||
throw std::runtime_error("Native RR creation flags require an unimplemented conversion");
|
||||
if (creation.quality < 0 || creation.quality > 5) throw std::runtime_error("Unsupported RR quality mode");
|
||||
native = std::make_unique<Device>(); native->initialize(adapter_luid(d3d.Get()), true);
|
||||
replay = std::make_unique<rr_bridge::Replay>(layout, native->physical, native->device, directory);
|
||||
const unsigned indices[] = {3, 2, 1, 4, 5, 0};
|
||||
const auto requested = creation.presets[indices[creation.quality]];
|
||||
if (requested && requested != replay->model->preset()) throw std::runtime_error("Configured RR model differs from requested preset");
|
||||
shared = make_buffer(d3d.Get(), layout.bytes, D3D12_HEAP_TYPE_DEFAULT, D3D12_HEAP_FLAG_SHARED, D3D12_RESOURCE_STATE_COMMON);
|
||||
dx(d3d->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&fence)), "RR shared fence");
|
||||
Handle memoryHandle, fenceHandle;
|
||||
dx(d3d->CreateSharedHandle(shared.Get(), nullptr, GENERIC_ALL, nullptr, &memoryHandle.value), "RR packet handle");
|
||||
dx(d3d->CreateSharedHandle(fence.Get(), nullptr, GENERIC_ALL, nullptr, &fenceHandle.value), "RR fence handle");
|
||||
packet = std::make_unique<Packet>(*native); packet->initialize(layout.bytes, memoryHandle.value, fenceHandle.value);
|
||||
event.value = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
if (!event.value) throw std::runtime_error("RR completion event creation failed");
|
||||
auto createList = [&](auto& allocator, auto& list) {
|
||||
dx(d3d->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)), "RR allocator");
|
||||
dx(d3d->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(), nullptr, IID_PPV_ARGS(&list)), "RR command list");
|
||||
dx(list->Close(), "RR initial close");
|
||||
};
|
||||
createList(producerAllocator, producer); createList(consumerAllocator, consumer);
|
||||
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; pci.queueFamilyIndex = native->family;
|
||||
Device::check(vkCreateCommandPool(native->device, &pci, nullptr, &pool), "RR command pool");
|
||||
VkCommandBufferAllocateInfo cai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
cai.commandPool = pool; cai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cai.commandBufferCount = 1;
|
||||
Device::check(vkAllocateCommandBuffers(native->device, &cai, &command), "RR command buffer");
|
||||
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
Device::check(vkCreateFence(native->device, &fci, nullptr, &completion), "RR Vulkan completion fence");
|
||||
}
|
||||
void validate(ID3D12CommandQueue* q, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access) {
|
||||
if (failed) throw std::runtime_error("Native RR backend is poisoned after failed submission");
|
||||
if (!q) throw std::runtime_error("Null RR submission queue");
|
||||
D3D12_COMMAND_QUEUE_DESC queueDesc{};
|
||||
#if defined(__MINGW32__)
|
||||
q->GetDesc(&queueDesc);
|
||||
#else
|
||||
queueDesc = q->GetDesc();
|
||||
#endif
|
||||
if (queueDesc.Type != D3D12_COMMAND_LIST_TYPE_DIRECT) throw std::runtime_error("RR requires a direct queue");
|
||||
ComPtr<ID3D12Device> owner; dx(q->GetDevice(IID_PPV_ARGS(&owner)), "RR queue device");
|
||||
if (owner.Get() != d3d.Get()) throw std::runtime_error("RR queue belongs to another device");
|
||||
for (size_t i = 0; i < access.size(); ++i)
|
||||
if (frame.resources[i] && (access[i].pending_split || access[i].inherited)) throw std::runtime_error("Unresolved RR texture state");
|
||||
validate_frame(frame);
|
||||
}
|
||||
void validate_frame(const Snapshot& frame) {
|
||||
if (failed) throw std::runtime_error("Native RR backend is poisoned after failed submission");
|
||||
if (frame.renderWidth != layout.rw || frame.renderHeight != layout.rh)
|
||||
throw std::runtime_error("Dynamic RR render extent requires engine reconfiguration");
|
||||
if (frame.resources[Snapshot::Exposure]) throw std::runtime_error("RR exposure texture is not implemented");
|
||||
if (frame.exposureScale != 1 || frame.depthType || frame.normalsType || frame.roughnessType || frame.diffuseType || frame.specularType)
|
||||
throw std::runtime_error("RR guide encoding or exposure scale requires conversion");
|
||||
for (size_t i = 0; i < Snapshot::Count; ++i) {
|
||||
if (i == Snapshot::Exposure) continue;
|
||||
auto* resource = frame.resources[i].Get();
|
||||
if (!resource && i >= Snapshot::HitDistance) continue;
|
||||
if (!resource) throw std::runtime_error("Missing required native RR texture");
|
||||
ComPtr<ID3D12Device> resourceOwner; dx(resource->GetDevice(IID_PPV_ARGS(&resourceOwner)), "RR texture device");
|
||||
if (resourceOwner.Get() != d3d.Get()) throw std::runtime_error("RR texture belongs to another device");
|
||||
const auto desc = resource_desc(resource);
|
||||
if (i != Snapshot::Output) (void)input_format(desc.Format, i);
|
||||
if (i == Snapshot::Depth) {
|
||||
(void)copy_format(d3d.Get(), resource, i);
|
||||
// D3D12 requires whole-subresource copies for depth-stencil.
|
||||
// The copied subresource is depth plane 0, never stencil.
|
||||
if ((desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) &&
|
||||
(frame.origins[i][0] || frame.origins[i][1] || layout.rw != desc.Width || layout.rh != desc.Height))
|
||||
throw std::runtime_error("RR depth-stencil requires a full depth-plane rectangle");
|
||||
}
|
||||
const uint32_t width = i == Snapshot::Output ? layout.ow : layout.rw;
|
||||
const uint32_t height = i == Snapshot::Output ? layout.oh : layout.rh;
|
||||
if (desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D || (i == Snapshot::Output && desc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT) || desc.SampleDesc.Count != 1 || desc.DepthOrArraySize != 1 ||
|
||||
frame.origins[i][0] > desc.Width || width > desc.Width - frame.origins[i][0] ||
|
||||
frame.origins[i][1] > desc.Height || height > desc.Height - frame.origins[i][1])
|
||||
throw std::runtime_error("Unsupported native RR texture format or subrect");
|
||||
if (i == Snapshot::Output && !(desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS))
|
||||
throw std::runtime_error("RR output lacks UAV capability");
|
||||
}
|
||||
for (size_t i = 0; i < Snapshot::Count; ++i)
|
||||
if (i != Snapshot::Output && frame.resources[i] == frame.resources[Snapshot::Output]) throw std::runtime_error("RR input/output alias is unsupported");
|
||||
}
|
||||
void evaluate(ID3D12CommandQueue* q, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access) {
|
||||
validate(q, frame, access); dispatch();
|
||||
for (size_t i = 0; i < layout.planes.size(); ++i) {
|
||||
const auto resource = resource_index(i);
|
||||
layout.planes[i].enabled = frame.resources[resource] != nullptr;
|
||||
if (!layout.planes[i].enabled) continue;
|
||||
const auto format = input_format(resource_desc(frame.resources[resource].Get()).Format, resource);
|
||||
replay->configure(i, format.format);
|
||||
layout.planes[i].format = format.format; layout.planes[i].bytes = format.bytes;
|
||||
}
|
||||
// Recording mutates engine upload/history bookkeeping. Any later error
|
||||
// requires recreation, even if no GPU submission has started yet.
|
||||
failed = true;
|
||||
commands::InternalScope internal;
|
||||
dx(producerAllocator->Reset(), "RR producer allocator reset"); dx(producer->Reset(producerAllocator.Get(), nullptr), "RR producer reset");
|
||||
transition(producer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
for (size_t i = 0; i < layout.planes.size(); ++i) {
|
||||
const auto resource = resource_index(i);
|
||||
auto* texture = frame.resources[resource].Get(); const auto& plane = layout.planes[i];
|
||||
if (!plane.enabled) continue;
|
||||
texture_copy_barrier(producer.Get(), texture, access[resource], false, false);
|
||||
auto source = rr_bridge::D3D12Frames::texture(texture);
|
||||
auto dest = rr_bridge::D3D12Frames::placed(shared.Get(), plane.offset, plane.pitch,
|
||||
copy_format(d3d.Get(), texture, resource), layout.rw, layout.rh);
|
||||
const auto x = frame.origins[resource][0], y = frame.origins[resource][1];
|
||||
D3D12_BOX box{x, y, 0, x + layout.rw, y + layout.rh, 1};
|
||||
const bool depthStencil = (resource_desc(texture).Flags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) != 0;
|
||||
producer->CopyTextureRegion(&dest, 0, 0, 0, &source, depthStencil ? nullptr : &box);
|
||||
texture_copy_barrier(producer.Get(), texture, access[resource], false, true);
|
||||
}
|
||||
transition(producer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON);
|
||||
dx(producer->Close(), "RR producer close");
|
||||
dx(consumerAllocator->Reset(), "RR consumer allocator reset"); dx(consumer->Reset(consumerAllocator.Get(), nullptr), "RR consumer reset");
|
||||
transition(consumer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
||||
auto* output = frame.resources[Snapshot::Output].Get();
|
||||
texture_copy_barrier(consumer.Get(), output, access[Snapshot::Output], true, false);
|
||||
auto source = rr_bridge::D3D12Frames::placed(shared.Get(), layout.outputOffset, layout.outputPitch,
|
||||
DXGI_FORMAT_R16G16B16A16_FLOAT, layout.ow, layout.oh);
|
||||
auto dest = rr_bridge::D3D12Frames::texture(output);
|
||||
consumer->CopyTextureRegion(&dest, frame.origins[Snapshot::Output][0], frame.origins[Snapshot::Output][1], 0, &source, nullptr);
|
||||
texture_copy_barrier(consumer.Get(), output, access[Snapshot::Output], true, true);
|
||||
transition(consumer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON);
|
||||
dx(consumer->Close(), "RR consumer close");
|
||||
Device::check(vkResetCommandPool(native->device, pool, 0), "RR Vulkan pool reset");
|
||||
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
Device::check(vkBeginCommandBuffer(command, &begin), "RR Vulkan begin");
|
||||
VkBufferMemoryBarrier acquire{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; acquire.buffer = packet->buffer; acquire.size = VK_WHOLE_SIZE;
|
||||
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL; acquire.dstQueueFamilyIndex = native->family;
|
||||
acquire.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &acquire, 0, nullptr);
|
||||
replay->record(command, packet->buffer, frame.settings);
|
||||
VkBufferMemoryBarrier release = acquire; release.srcQueueFamilyIndex = native->family; release.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
|
||||
release.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; release.dstAccessMask = 0;
|
||||
vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 1, &release, 0, nullptr);
|
||||
Device::check(vkEndCommandBuffer(command), "RR Vulkan end");
|
||||
const uint64_t ready = sequence + 1, done = sequence + 2, complete = sequence + 3;
|
||||
VkTimelineSemaphoreSubmitInfo values{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO};
|
||||
values.waitSemaphoreValueCount = values.signalSemaphoreValueCount = 1; values.pWaitSemaphoreValues = &ready; values.pSignalSemaphoreValues = &done;
|
||||
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO, &values}; submit.waitSemaphoreCount = submit.signalSemaphoreCount = 1;
|
||||
submit.pWaitSemaphores = submit.pSignalSemaphores = &packet->semaphore; submit.pWaitDstStageMask = &stage;
|
||||
submit.commandBufferCount = 1; submit.pCommandBuffers = &command;
|
||||
Device::check(vkResetFences(native->device, 1, &completion), "RR completion reset");
|
||||
retained = std::make_unique<Snapshot>(frame); queue = q; pending = true;
|
||||
try {
|
||||
ID3D12CommandList* inputs[] = {producer.Get()}; q->ExecuteCommandLists(1, inputs);
|
||||
dx(q->Signal(fence.Get(), ready), "RR input ready signal");
|
||||
Device::check(vkQueueSubmit(native->queue, 1, &submit, completion), "RR model submission");
|
||||
dx(q->Wait(fence.Get(), done), "RR output queue wait");
|
||||
ID3D12CommandList* outputs[] = {consumer.Get()}; q->ExecuteCommandLists(1, outputs);
|
||||
dx(q->Signal(fence.Get(), complete), "RR completion signal");
|
||||
dx(fence->SetEventOnCompletion(complete, event.value), "RR completion event");
|
||||
if (WaitForSingleObject(event.value, 5000) != WAIT_OBJECT_0) throw std::runtime_error("RR D3D12 completion timeout");
|
||||
Device::check(vkWaitForFences(native->device, 1, &completion, VK_TRUE, 5000000000ull), "RR Vulkan completion wait");
|
||||
if (fence->GetCompletedValue() == UINT64_MAX) throw std::runtime_error("RR D3D12 device removed");
|
||||
pending = false; failed = false; retained.reset(); queue.Reset(); sequence = complete;
|
||||
} catch (...) { failed = true; throw; }
|
||||
}
|
||||
};
|
||||
Backend::Backend(ID3D12Device* device, const Creation& creation, const std::string& directory) {
|
||||
std::lock_guard<std::mutex> lock(dispatchMutex);
|
||||
auto candidate = std::make_unique<Impl>(device, creation);
|
||||
candidate->initialize(directory); impl = std::move(candidate);
|
||||
}
|
||||
Backend::~Backend() {
|
||||
std::lock_guard<std::mutex> lock(dispatchMutex);
|
||||
if (impl && impl->pending) {
|
||||
std::fprintf(stderr, "D4R_RR_QUARANTINE incomplete GPU work; retaining backend and guide resources\n");
|
||||
(void)impl.release();
|
||||
} else impl.reset();
|
||||
}
|
||||
void Backend::evaluate(ID3D12CommandQueue* queue, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access) {
|
||||
std::lock_guard<std::mutex> lock(dispatchMutex);
|
||||
impl->evaluate(queue, frame, access);
|
||||
}
|
||||
void Backend::validate_frame(const Snapshot& frame) {
|
||||
std::lock_guard<std::mutex> lock(dispatchMutex);
|
||||
impl->validate_frame(frame);
|
||||
}
|
||||
void Backend::drain() {
|
||||
std::lock_guard<std::mutex> lock(dispatchMutex);
|
||||
if (impl->pending || impl->failed) throw std::runtime_error("RR backend cannot retire failed GPU work");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "d3d12_external.h"
|
||||
#include "rr_ngx_snapshot.h"
|
||||
#include <memory>
|
||||
|
||||
namespace d4r::win::rr {
|
||||
// Native RR execution on borrowed game resources. No fixture files, synthetic
|
||||
// guide creation, child process, NVIDIA runtime, or CPU tensor transfer.
|
||||
class Backend {
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
Backend(ID3D12Device* device, const Creation& creation, const std::string& modelDirectory);
|
||||
~Backend();
|
||||
Backend(const Backend&) = delete;
|
||||
Backend& operator=(const Backend&) = delete;
|
||||
void validate_frame(const Snapshot& frame);
|
||||
void evaluate(ID3D12CommandQueue* queue, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access);
|
||||
void drain();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <volk.h>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace rr_bridge {
|
||||
// Owns only Vulkan objects. The caller supplies the actual D3D12 adapter LUID;
|
||||
// production callers must obtain it from the borrowed game's device.
|
||||
struct NativeDevice {
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice physical = VK_NULL_HANDLE;
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
uint32_t family = 0;
|
||||
NativeDevice() = default;
|
||||
NativeDevice(const NativeDevice&) = delete;
|
||||
NativeDevice& operator=(const NativeDevice&) = delete;
|
||||
static void check(VkResult result, const char* operation) {
|
||||
if (result != VK_SUCCESS)
|
||||
throw std::runtime_error(std::string(operation) + " VkResult=" + std::to_string(result));
|
||||
}
|
||||
void initialize(const LUID& luid, bool inference) {
|
||||
if (instance) throw std::runtime_error("RR Vulkan device already initialized");
|
||||
check(volkInitialize(), "Vulkan loader");
|
||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion = VK_API_VERSION_1_3;
|
||||
VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ici.pApplicationInfo = &app;
|
||||
check(vkCreateInstance(&ici, nullptr, &instance), "Vulkan instance"); volkLoadInstance(instance);
|
||||
uint32_t count = 0;
|
||||
check(vkEnumeratePhysicalDevices(instance, &count, nullptr), "Vulkan device count");
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
check(vkEnumeratePhysicalDevices(instance, &count, devices.data()), "Vulkan devices");
|
||||
for (auto candidate : devices) {
|
||||
VkPhysicalDeviceIDProperties id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &id};
|
||||
vkGetPhysicalDeviceProperties2(candidate, &properties);
|
||||
if (id.deviceLUIDValid && !std::memcmp(id.deviceLUID, &luid, VK_LUID_SIZE)) physical = candidate;
|
||||
}
|
||||
if (!physical) throw std::runtime_error("D3D12/Vulkan adapter LUID mismatch");
|
||||
uint32_t countFamilies = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(physical, &countFamilies, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> properties(countFamilies);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(physical, &countFamilies, properties.data());
|
||||
family = countFamilies;
|
||||
for (uint32_t i = 0; i < countFamilies; ++i)
|
||||
if (properties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { family = i; break; }
|
||||
if (family == countFamilies) throw std::runtime_error("No Vulkan compute queue");
|
||||
VkPhysicalDeviceCooperativeMatrixFeaturesKHR cm{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COOPERATIVE_MATRIX_FEATURES_KHR};
|
||||
VkPhysicalDeviceVulkan13Features v13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, &cm};
|
||||
VkPhysicalDeviceVulkan12Features v12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, &v13};
|
||||
VkPhysicalDeviceVulkan11Features v11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, &v12};
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &v11};
|
||||
vkGetPhysicalDeviceFeatures2(physical, &features);
|
||||
if (!v12.timelineSemaphore) throw std::runtime_error("Timeline semaphore unsupported");
|
||||
if (inference && (!cm.cooperativeMatrix || !v12.shaderFloat16 || !v11.storageBuffer16BitAccess || !v13.subgroupSizeControl))
|
||||
throw std::runtime_error("Missing RR cooperative-matrix/fp16/subgroup feature");
|
||||
if (!inference) v13.pNext = nullptr;
|
||||
float priority = 1;
|
||||
VkDeviceQueueCreateInfo qi{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
qi.queueFamilyIndex = family; qi.queueCount = 1; qi.pQueuePriorities = &priority;
|
||||
std::vector<const char*> extensions{"VK_KHR_external_memory_win32", "VK_KHR_external_semaphore_win32"};
|
||||
if (inference) extensions.push_back(VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME);
|
||||
VkDeviceCreateInfo di{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &features};
|
||||
di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi;
|
||||
di.enabledExtensionCount = uint32_t(extensions.size()); di.ppEnabledExtensionNames = extensions.data();
|
||||
check(vkCreateDevice(physical, &di, nullptr, &device), "Vulkan device"); volkLoadDevice(device);
|
||||
vkGetDeviceQueue(device, family, 0, &queue);
|
||||
}
|
||||
// Callers must retire submitted work before destroying this owner. A failed
|
||||
// submission may instead quarantine the owner and its dependent resources.
|
||||
~NativeDevice() {
|
||||
if (device) vkDestroyDevice(device, nullptr);
|
||||
if (instance) vkDestroyInstance(instance, nullptr);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
#include "rr_native_device.h"
|
||||
#include <vulkan/vulkan_win32.h>
|
||||
|
||||
namespace rr_bridge {
|
||||
// Imports the caller's D3D12 shared packet and fence. Exported Win32 handles
|
||||
// remain caller-owned; Vulkan keeps its own references after successful import.
|
||||
struct NativePacket {
|
||||
NativeDevice& native;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
explicit NativePacket(NativeDevice& owner) : native(owner) {}
|
||||
NativePacket(const NativePacket&) = delete;
|
||||
NativePacket& operator=(const NativePacket&) = delete;
|
||||
void initialize(VkDeviceSize bytes, HANDLE resource, HANDLE fence) {
|
||||
if (buffer || !bytes || !resource || !fence)
|
||||
throw std::runtime_error("Invalid RR shared packet import");
|
||||
auto memoryProperties = reinterpret_cast<PFN_vkGetMemoryWin32HandlePropertiesKHR>(
|
||||
vkGetDeviceProcAddr(native.device, "vkGetMemoryWin32HandlePropertiesKHR"));
|
||||
auto importSemaphore = reinterpret_cast<PFN_vkImportSemaphoreWin32HandleKHR>(
|
||||
vkGetDeviceProcAddr(native.device, "vkImportSemaphoreWin32HandleKHR"));
|
||||
if (!memoryProperties || !importSemaphore) throw std::runtime_error("Missing Win32 import entry points");
|
||||
VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
|
||||
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
|
||||
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; bi.size = bytes;
|
||||
bi.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
NativeDevice::check(vkCreateBuffer(native.device, &bi, nullptr, &buffer), "External Vulkan buffer");
|
||||
VkMemoryRequirements req; vkGetBufferMemoryRequirements(native.device, buffer, &req);
|
||||
VkMemoryWin32HandlePropertiesKHR handleProperties{VK_STRUCTURE_TYPE_MEMORY_WIN32_HANDLE_PROPERTIES_KHR};
|
||||
NativeDevice::check(memoryProperties(native.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT,
|
||||
resource, &handleProperties), "D3D12 resource memory types");
|
||||
VkPhysicalDeviceMemoryProperties properties{};
|
||||
vkGetPhysicalDeviceMemoryProperties(native.physical, &properties);
|
||||
const uint32_t bits = req.memoryTypeBits & handleProperties.memoryTypeBits;
|
||||
uint32_t type = properties.memoryTypeCount;
|
||||
for (uint32_t i = 0; i < properties.memoryTypeCount; ++i)
|
||||
if ((bits & (1u << i)) && (properties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
|
||||
type = i; break;
|
||||
}
|
||||
if (type == properties.memoryTypeCount) throw std::runtime_error("No compatible RR packet memory type");
|
||||
VkImportMemoryWin32HandleInfoKHR imported{VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR};
|
||||
imported.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT; imported.handle = resource;
|
||||
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, &imported};
|
||||
dedicated.buffer = buffer;
|
||||
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &dedicated};
|
||||
ai.allocationSize = req.size; ai.memoryTypeIndex = type;
|
||||
NativeDevice::check(vkAllocateMemory(native.device, &ai, nullptr, &memory), "Dedicated D3D12 memory import");
|
||||
NativeDevice::check(vkBindBufferMemory(native.device, buffer, memory, 0), "Bind imported buffer");
|
||||
VkSemaphoreTypeCreateInfo timeline{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO};
|
||||
timeline.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
|
||||
VkSemaphoreCreateInfo sci{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &timeline};
|
||||
NativeDevice::check(vkCreateSemaphore(native.device, &sci, nullptr, &semaphore), "Vulkan timeline semaphore");
|
||||
VkImportSemaphoreWin32HandleInfoKHR si{VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_WIN32_HANDLE_INFO_KHR};
|
||||
si.semaphore = semaphore; si.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT; si.handle = fence;
|
||||
NativeDevice::check(importSemaphore(native.device, &si), "D3D12 fence import");
|
||||
}
|
||||
// The owner must retire GPU submissions before destroying an import.
|
||||
~NativePacket() {
|
||||
if (semaphore) vkDestroySemaphore(native.device, semaphore, nullptr);
|
||||
if (buffer) vkDestroyBuffer(native.device, buffer, nullptr);
|
||||
if (memory) vkFreeMemory(native.device, memory, nullptr);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
// Synthetic NGX feature-13 lifecycle and delayed-submission test. No game access.
|
||||
#define WIDL_EXPLICIT_AGGREGATE_RETURNS
|
||||
#include <volk.h>
|
||||
#include "d3d12_external.h"
|
||||
#include "rr_bridge_replay.h"
|
||||
#include "ngx_parameters.h"
|
||||
#include "ngx_public_abi.h"
|
||||
#include <dxgi1_4.h>
|
||||
|
||||
using namespace d4r::win;
|
||||
int main(int argc, char** argv) try {
|
||||
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
if (argc != 6 && argc != 7) throw std::runtime_error("Usage: rr_ngx_probe SHIM INPUTS REFERENCE OUTPUT MODEL [contracts]");
|
||||
const std::string mode = argc == 7 ? argv[6] : "";
|
||||
const bool contracts = mode.find("contracts") != std::string::npos;
|
||||
const bool inherited = mode.find("inherited") != std::string::npos;
|
||||
const bool enhanced = mode.find("enhanced") != std::string::npos;
|
||||
const bool split = mode.find("split") != std::string::npos;
|
||||
const bool unavailable = mode.find("unavailable") != std::string::npos;
|
||||
const bool withoutSr = mode.find("without-sr") != std::string::npos;
|
||||
const bool floatGuides = mode.find("float-guides") != std::string::npos;
|
||||
const bool switchGuides = mode.find("switch-guides") != std::string::npos;
|
||||
const bool autoExposure = mode.find("auto-exposure") != std::string::npos;
|
||||
DXGI_FORMAT depthFormat = DXGI_FORMAT_UNKNOWN;
|
||||
if (mode.find("depth-d32s8-typeless") != std::string::npos) depthFormat = DXGI_FORMAT_R32G8X24_TYPELESS;
|
||||
else if (mode.find("depth-d32s8") != std::string::npos) depthFormat = DXGI_FORMAT_D32_FLOAT_S8X24_UINT;
|
||||
else if (mode.find("depth-d32") != std::string::npos) depthFormat = DXGI_FORMAT_D32_FLOAT;
|
||||
else if (mode.find("depth-r32-typeless") != std::string::npos) depthFormat = DXGI_FORMAT_R32_TYPELESS;
|
||||
const bool stencil = depthFormat == DXGI_FORMAT_R32G8X24_TYPELESS || depthFormat == DXGI_FORMAT_D32_FLOAT_S8X24_UINT;
|
||||
if (enhanced && split) throw std::runtime_error("The split fixture currently tests legacy split barriers only");
|
||||
rr_bridge::Layout layout(argv[2], argv[4], argv[3]);
|
||||
const auto canonical = layout;
|
||||
if (floatGuides) {
|
||||
for (size_t i : {size_t(0), size_t(3)}) {
|
||||
layout.planes[i].format = VK_FORMAT_R32G32B32A32_SFLOAT; layout.planes[i].bytes = 16;
|
||||
}
|
||||
layout.planes[1].format = VK_FORMAT_R32G32_SFLOAT; layout.planes[1].bytes = 8;
|
||||
layout.planes[4].format = VK_FORMAT_R16_SFLOAT; layout.planes[4].bytes = 2;
|
||||
layout.initializePacket();
|
||||
}
|
||||
const auto floatLayout = layout;
|
||||
auto uploadFrame = [&](void* destination, unsigned index) {
|
||||
if (!floatGuides) { layout.upload(destination, index); return; }
|
||||
// CPU conversion belongs only to fixture initialization. The production
|
||||
// backend transfers and samples the actual D3D12 formats on the GPU.
|
||||
std::memset(destination, 0, size_t(layout.bytes));
|
||||
for (size_t i = 0; i < layout.planes.size(); ++i) {
|
||||
const auto& plane = layout.planes[i];
|
||||
if (!plane.enabled || (i >= 7 && !layout.frames[index].optional[i - 7])) continue;
|
||||
auto data = rr_bridge::read(layout.inputs / (std::to_string(layout.frames[index].id) + "-" + plane.name + ".bin"));
|
||||
if (data.size() != size_t(layout.rw) * layout.rh * canonical.planes[i].bytes)
|
||||
throw std::runtime_error("Invalid canonical input for float-guide fixture");
|
||||
for (uint32_t y = 0; y < layout.rh; ++y) for (uint32_t x = 0; x < layout.rw; ++x) {
|
||||
const auto* source = data.data() + (size_t(y) * layout.rw + x) * canonical.planes[i].bytes;
|
||||
auto* dest = static_cast<char*>(destination) + plane.offset + y * plane.pitch + x * plane.bytes;
|
||||
if ((i == 0 || i == 1 || i == 3) && plane.bytes != canonical.planes[i].bytes) {
|
||||
for (uint32_t c = 0; c < plane.bytes / 4; ++c) {
|
||||
uint16_t half; std::memcpy(&half, source + c * 2, 2);
|
||||
const float value = float(d4r::diag::binary16::half_value(half)); std::memcpy(dest + c * 4, &value, 4);
|
||||
}
|
||||
} else std::memcpy(dest, source, plane.bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
ComPtr<IDXGIFactory4> factory; dx(CreateDXGIFactory1(IID_PPV_ARGS(&factory)), "NGX RR factory");
|
||||
ComPtr<IDXGIAdapter1> adapter;
|
||||
for (UINT i = 0; ; ++i) {
|
||||
if (factory->EnumAdapters1(i, &adapter) == DXGI_ERROR_NOT_FOUND) throw std::runtime_error("No AMD adapter");
|
||||
DXGI_ADAPTER_DESC1 desc{}; dx(adapter->GetDesc1(&desc), "NGX RR adapter");
|
||||
if (desc.VendorId == 0x1002 && !(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)) break;
|
||||
adapter.Reset();
|
||||
}
|
||||
ComPtr<ID3D12Device> device; dx(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device)), "NGX RR device");
|
||||
if (enhanced) {
|
||||
D3D12_FEATURE_DATA_D3D12_OPTIONS12 support{};
|
||||
dx(device->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS12, &support, sizeof(support)), "RR enhanced barrier support");
|
||||
if (!support.EnhancedBarriersSupported) throw std::runtime_error("RR enhanced barriers unavailable");
|
||||
}
|
||||
auto enhancedAccess = [](bool output) {
|
||||
ResourceAccess access; access.enhanced = true;
|
||||
access.layout = output ? D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_UNORDERED_ACCESS : D3D12_BARRIER_LAYOUT_DIRECT_QUEUE_SHADER_RESOURCE;
|
||||
access.access = output ? D3D12_BARRIER_ACCESS_UNORDERED_ACCESS : D3D12_BARRIER_ACCESS_SHADER_RESOURCE;
|
||||
return access;
|
||||
};
|
||||
auto switchBarrierModel = [&](ID3D12GraphicsCommandList* list, ID3D12Resource* texture, bool output, bool toEnhanced) {
|
||||
// Microsoft requires COMMON at each legacy/enhanced API boundary.
|
||||
// https://microsoft.github.io/DirectX-Specs/d3d/D3D12EnhancedBarriers.html
|
||||
const auto legacy = output ? D3D12_RESOURCE_STATE_UNORDERED_ACCESS : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
|
||||
if (toEnhanced) transition(list, texture, legacy, D3D12_RESOURCE_STATE_COMMON);
|
||||
ComPtr<ID3D12GraphicsCommandList7> newer; dx(list->QueryInterface(IID_PPV_ARGS(&newer)), "RR enhanced fixture list");
|
||||
const auto access = enhancedAccess(output);
|
||||
D3D12_TEXTURE_BARRIER barrier{}; barrier.pResource = texture;
|
||||
barrier.LayoutBefore = toEnhanced ? D3D12_BARRIER_LAYOUT_COMMON : access.layout;
|
||||
barrier.LayoutAfter = toEnhanced ? access.layout : D3D12_BARRIER_LAYOUT_COMMON;
|
||||
barrier.AccessBefore = toEnhanced ? D3D12_BARRIER_ACCESS_COMMON : access.access;
|
||||
barrier.AccessAfter = toEnhanced ? access.access : D3D12_BARRIER_ACCESS_COMMON;
|
||||
barrier.SyncBefore = barrier.SyncAfter = D3D12_BARRIER_SYNC_ALL;
|
||||
D3D12_BARRIER_GROUP group{}; group.Type = D3D12_BARRIER_TYPE_TEXTURE; group.NumBarriers = 1; group.pTextureBarriers = &barrier;
|
||||
newer->Barrier(1, &group);
|
||||
if (!toEnhanced) transition(list, texture, D3D12_RESOURCE_STATE_COMMON, legacy);
|
||||
};
|
||||
ComPtr<ID3D12CommandQueue> queue; D3D12_COMMAND_QUEUE_DESC q{}; q.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
||||
dx(device->CreateCommandQueue(&q, IID_PPV_ARGS(&queue)), "NGX RR queue");
|
||||
d4r::diag::Library shim(d4r::diag::wide(std::filesystem::absolute(argv[1]).string()));
|
||||
auto init = shim.symbol<unsigned(*)(const char*, int, const char*, const wchar_t*, ID3D12Device*, unsigned, const void*)>("NVSDK_NGX_D3D12_Init_ProjectID");
|
||||
auto allocate = shim.symbol<unsigned(*)(void**)>("NVSDK_NGX_D3D12_AllocateParameters");
|
||||
auto destroy = shim.symbol<unsigned(*)(void*)>("NVSDK_NGX_D3D12_DestroyParameters");
|
||||
auto create = shim.symbol<unsigned(*)(ID3D12GraphicsCommandList*, unsigned, void*, void**)>("NVSDK_NGX_D3D12_CreateFeature");
|
||||
auto evaluate = shim.symbol<unsigned(*)(ID3D12GraphicsCommandList*, const void*, void*, void*)>("NVSDK_NGX_D3D12_EvaluateFeature");
|
||||
auto release = shim.symbol<unsigned(*)(void*)>("NVSDK_NGX_D3D12_ReleaseFeature");
|
||||
auto shutdown = shim.symbol<unsigned(*)()>("NVSDK_NGX_D3D12_Shutdown");
|
||||
auto check = [](unsigned r, const char* operation) { if (r != 1) throw std::runtime_error(std::string(operation) + " NGX=" + std::to_string(r)); };
|
||||
auto require = [](bool condition, const char* operation) { if (!condition) throw std::runtime_error(operation); };
|
||||
if (contracts || unavailable) {
|
||||
auto requirements = shim.symbol<unsigned(*)(IDXGIAdapter*, const d4r::ngx::FeatureDiscoveryInfo*, d4r::ngx::FeatureRequirement*)>("NVSDK_NGX_D3D12_GetFeatureRequirements");
|
||||
d4r::ngx::FeatureDiscoveryInfo discovery{}; discovery.FeatureID = 13;
|
||||
d4r::ngx::FeatureRequirement result{};
|
||||
check(requirements(adapter.Get(), &discovery, &result), "RR requirements");
|
||||
require((result.FeatureSupported == 0) != unavailable, "RR requirement readiness disagrees with configured backend");
|
||||
ComPtr<IDXGIAdapter> warp; dx(factory->EnumWarpAdapter(IID_PPV_ARGS(&warp)), "RR WARP adapter");
|
||||
check(requirements(warp.Get(), &discovery, &result), "RR WARP requirements");
|
||||
require(result.FeatureSupported != 0, "RR advertised on unsupported WARP adapter");
|
||||
discovery.FeatureID = 14;
|
||||
check(requirements(adapter.Get(), &discovery, &result), "Unknown feature requirements");
|
||||
require(result.FeatureSupported != 0, "Unknown NGX feature advertised");
|
||||
}
|
||||
std::string projectId = "24480451-f00d-face-1304-0308dabad187", projectVersion = "1.0";
|
||||
std::wstring featurePath = std::filesystem::absolute(argv[2]).wstring();
|
||||
const wchar_t* featurePaths[] = {featurePath.c_str()};
|
||||
d4r::ngx::FeatureCommonInfo common{}; common.PathListInfo = {featurePaths, 1};
|
||||
check(init(projectId.c_str(), 0, projectVersion.c_str(), L"", device.Get(), 0x15, &common), "RR Init");
|
||||
// Native RR defers SR startup. Its Init snapshot must survive caller-owned
|
||||
// project/path strings and path-list descriptors changing before SR use.
|
||||
projectId.assign("mutated-project"); projectVersion.assign("mutated-version");
|
||||
featurePath.assign(L"mutated-feature-path"); featurePaths[0] = nullptr;
|
||||
common.PathListInfo = {}; common.LoggingInfo.DisableOtherLoggingSinks = true;
|
||||
if (contracts || unavailable) {
|
||||
auto capabilities = shim.symbol<unsigned(*)(void**)>("NVSDK_NGX_D3D12_GetCapabilityParameters");
|
||||
void* caps = nullptr; check(capabilities(&caps), "RR capabilities");
|
||||
require(d4r::ngx::int_value(caps, "SuperSamplingDenoising.Available") == (unavailable ? 0 : 1) &&
|
||||
(d4r::ngx::int_value(caps, "SuperSamplingDenoising.FeatureInitResult") == 1) != unavailable, "RR capability readiness failed");
|
||||
require(d4r::ngx::int_value(caps, "SuperSampling.Available") == (withoutSr ? 0 : 1), "SR capability disagrees with dependency readiness");
|
||||
check(destroy(caps), "RR destroy capabilities");
|
||||
}
|
||||
void* p = nullptr; check(allocate(&p), "RR Allocate");
|
||||
d4r_ngx_set_uint(p, "Width", layout.rw); d4r_ngx_set_uint(p, "Height", layout.rh);
|
||||
d4r_ngx_set_uint(p, "OutWidth", layout.ow); d4r_ngx_set_uint(p, "OutHeight", layout.oh);
|
||||
d4r_ngx_set_int(p, "PerfQualityValue", 2);
|
||||
const unsigned creationFlags = 3u | (layout.frames.front().settings.depthInverted ? 8u : 0u) | (autoExposure ? 64u : 0u);
|
||||
d4r_ngx_set_uint(p, "DLSS.Feature.Create.Flags", creationFlags);
|
||||
std::printf("RR_NGX_CREATION flags=%u\n", creationFlags);
|
||||
if (unavailable) {
|
||||
void* rejected = reinterpret_cast<void*>(uintptr_t(1));
|
||||
require(create(nullptr, 13, p, &rejected) != 1 && !rejected, "Unavailable RR backend accepted creation");
|
||||
check(destroy(p), "Unavailable RR destroy parameters");
|
||||
check(shutdown(), "Unavailable RR shutdown");
|
||||
std::puts("PASS RR_NGX_UNAVAILABLE capabilities=0 requirements=unsupported creation=rejected sr_available=1");
|
||||
return 0;
|
||||
}
|
||||
if (contracts) {
|
||||
void* rejected = reinterpret_cast<void*>(uintptr_t(1));
|
||||
require(create(nullptr, 14, p, &rejected) != 1 && !rejected, "Unknown feature creation accepted");
|
||||
d4r_ngx_set_uint(p, "Width", 0); rejected = reinterpret_cast<void*>(uintptr_t(1));
|
||||
require(create(nullptr, 13, p, &rejected) != 1 && !rejected, "Invalid RR creation accepted");
|
||||
d4r_ngx_set_uint(p, "Width", layout.rw);
|
||||
for (unsigned flags : {0u, 1u, 2u, 7u, 131u}) {
|
||||
d4r_ngx_set_uint(p, "DLSS.Feature.Create.Flags", flags); rejected = reinterpret_cast<void*>(uintptr_t(1));
|
||||
require(create(nullptr, 13, p, &rejected) != 1 && !rejected, "Unsupported RR creation flags accepted");
|
||||
}
|
||||
d4r_ngx_set_uint(p, "DLSS.Feature.Create.Flags", creationFlags);
|
||||
}
|
||||
void* feature = nullptr; check(create(nullptr, 13, p, &feature), "RR CreateFeature");
|
||||
auto textures = std::make_unique<rr_bridge::D3D12Frames>(layout, device.Get(), depthFormat);
|
||||
ComPtr<ID3D12Resource> stencilUpload, stencilReadback;
|
||||
D3D12_PLACED_SUBRESOURCE_FOOTPRINT stencilFootprint{}; UINT64 stencilBytes = 0;
|
||||
if (stencil) {
|
||||
const auto desc = resource_desc(textures->inputs[2].Get()); UINT rows = 0; UINT64 rowBytes = 0;
|
||||
device->GetCopyableFootprints(&desc, 1, 1, 0, &stencilFootprint, &rows, &rowBytes, &stencilBytes);
|
||||
require(rows == layout.rh && rowBytes == layout.rw && stencilBytes && stencilBytes != UINT64_MAX, "Unexpected stencil-plane footprint");
|
||||
stencilUpload = make_buffer(device.Get(), stencilBytes, D3D12_HEAP_TYPE_UPLOAD, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_GENERIC_READ);
|
||||
stencilReadback = make_buffer(device.Get(), stencilBytes, D3D12_HEAP_TYPE_READBACK, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
}
|
||||
auto stencilTransition = [](ID3D12GraphicsCommandList* list, ID3D12Resource* image, D3D12_RESOURCE_STATES from, D3D12_RESOURCE_STATES to) {
|
||||
D3D12_RESOURCE_BARRIER barrier{}; barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
barrier.Transition = {image, 1, from, to}; list->ResourceBarrier(1, &barrier);
|
||||
};
|
||||
auto stencilCopy = [&](ID3D12GraphicsCommandList* list, ID3D12Resource* image, bool upload) {
|
||||
D3D12_TEXTURE_COPY_LOCATION buffer{}; buffer.pResource = upload ? stencilUpload.Get() : stencilReadback.Get();
|
||||
buffer.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; buffer.PlacedFootprint = stencilFootprint;
|
||||
auto texture = rr_bridge::D3D12Frames::texture(image); texture.SubresourceIndex = 1;
|
||||
const auto copy = upload ? D3D12_RESOURCE_STATE_COPY_DEST : D3D12_RESOURCE_STATE_COPY_SOURCE;
|
||||
stencilTransition(list, image, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, copy);
|
||||
list->CopyTextureRegion(upload ? &texture : &buffer, 0, 0, 0, upload ? &buffer : &texture, nullptr);
|
||||
stencilTransition(list, image, copy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
|
||||
};
|
||||
auto upload = make_buffer(device.Get(), layout.bytes, D3D12_HEAP_TYPE_UPLOAD, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_GENERIC_READ);
|
||||
auto scratch = make_buffer(device.Get(), layout.bytes, D3D12_HEAP_TYPE_DEFAULT, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
auto readback = make_buffer(device.Get(), layout.bytes, D3D12_HEAP_TYPE_READBACK, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST);
|
||||
ComPtr<ID3D12Fence> fence; dx(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)), "NGX RR fixture fence");
|
||||
Handle event; event.value = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
if (!event.value) throw std::runtime_error("NGX RR fixture event");
|
||||
for (unsigned index = 0; index < layout.frames.size(); ++index) {
|
||||
if (switchGuides) {
|
||||
layout = index % 2 ? floatLayout : canonical;
|
||||
textures = std::make_unique<rr_bridge::D3D12Frames>(layout, device.Get(), depthFormat);
|
||||
}
|
||||
void* mapped = nullptr; D3D12_RANGE noRead{};
|
||||
dx(upload->Map(0, &noRead, &mapped), "NGX RR upload map"); uploadFrame(mapped, index); upload->Unmap(0, nullptr);
|
||||
ComPtr<ID3D12CommandAllocator> allocator; ComPtr<ID3D12GraphicsCommandList> list;
|
||||
dx(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)), "NGX RR fixture allocator");
|
||||
dx(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(), nullptr, IID_PPV_ARGS(&list)), "NGX RR fixture list");
|
||||
if (stencil) {
|
||||
dx(stencilUpload->Map(0, &noRead, &mapped), "RR stencil upload map");
|
||||
std::memset(mapped, 0, size_t(stencilBytes));
|
||||
for (uint32_t y = 0; y < layout.rh; ++y) for (uint32_t x = 0; x < layout.rw; ++x)
|
||||
static_cast<uint8_t*>(mapped)[stencilFootprint.Offset + y * stencilFootprint.Footprint.RowPitch + x] = uint8_t(x * 13 + y * 17 + index * 29);
|
||||
stencilUpload->Unmap(0, nullptr); stencilCopy(list.Get(), textures->inputs[2].Get(), true);
|
||||
}
|
||||
uint32_t optionalMask = 0;
|
||||
for (unsigned i = 0; i < 4; ++i) if (layout.frames[index].optional[i]) optionalMask |= 1u << i;
|
||||
textures->recordInputs(list.Get(), upload.Get(), scratch.Get(), optionalMask);
|
||||
if (enhanced) {
|
||||
for (size_t i = 0; i < textures->inputs.size(); ++i)
|
||||
if (textures->inputs[i] && (i < 7 || (optionalMask & (1u << (i - 7)))))
|
||||
switchBarrierModel(list.Get(), textures->inputs[i].Get(), false, true);
|
||||
switchBarrierModel(list.Get(), textures->output.Get(), true, true);
|
||||
}
|
||||
ComPtr<ID3D12CommandAllocator> prefixAllocator;
|
||||
ComPtr<ID3D12GraphicsCommandList> prefix;
|
||||
if (inherited) {
|
||||
// The preceding list publishes every resource state, including an
|
||||
// output which has not otherwise been used by a fixture upload.
|
||||
if (!enhanced) {
|
||||
transition(list.Get(), textures->output.Get(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COMMON);
|
||||
transition(list.Get(), textures->output.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
}
|
||||
dx(list->Close(), "NGX RR prefix close"); prefix = list; prefixAllocator = allocator;
|
||||
list.Reset(); allocator.Reset();
|
||||
dx(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)), "NGX RR inherited allocator");
|
||||
dx(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(), nullptr, IID_PPV_ARGS(&list)), "NGX RR inherited list");
|
||||
}
|
||||
const char* names[] = {"Color", "MotionVectors", "Depth", "GBuffer.Normals", "GBuffer.Roughness", "DLSS.Input.DiffuseAlbedo", "DLSS.Input.SpecularAlbedo"};
|
||||
for (size_t i = 0; i < 7; ++i) d4r_ngx_set_d3d12_resource(p, names[i], textures->inputs[i].Get());
|
||||
d4r_ngx_set_d3d12_resource(p, "Output", textures->output.Get());
|
||||
const char* optionalNames[] = {"DLSSD.SpecularHitDistance", "DLSS.DisocclusionMask", "DLSSD.ScreenSpaceSubsurfaceScatteringGuide", "DLSSD.ColorBeforeParticles"};
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
d4r_ngx_set_d3d12_resource(p, optionalNames[i], layout.frames[index].optional[i] ? textures->inputs[i + 7].Get() : nullptr);
|
||||
auto settings = layout.frames[index].settings;
|
||||
d4r_ngx_set_float(p, "Jitter.Offset.X", settings.jitter[0]); d4r_ngx_set_float(p, "Jitter.Offset.Y", settings.jitter[1]);
|
||||
d4r_ngx_set_float(p, "MV.Scale.X", settings.motionScale[0]); d4r_ngx_set_float(p, "MV.Scale.Y", settings.motionScale[1]);
|
||||
d4r_ngx_set_float(p, "DLSS.Pre.Exposure", settings.preExposure);
|
||||
d4r_ngx_set_uint(p, "Reset", settings.reset); d4r_ngx_set_uint(p, "DLSS.Use.HW.Depth", settings.hardwareDepth);
|
||||
d4r_ngx_set_uint(p, "DLSS.Roughness.Mode", settings.roughnessPacked);
|
||||
d4r_ngx_set_void(p, "WorldToViewMatrix", settings.worldToView); d4r_ngx_set_void(p, "ViewToClipMatrix", settings.viewToClip);
|
||||
const char* states[] = {"D4R.Color.State", "D4R.Motion.State", "D4R.Depth.State", "D4R.Normals.State", "D4R.Roughness.State", "D4R.Diffuse.State", "D4R.Specular.State"};
|
||||
if (!inherited) {
|
||||
for (const auto* state : states) d4r_ngx_set_uint(p, state, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
|
||||
d4r_ngx_set_uint(p, "D4R.Output.State", D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
}
|
||||
if (contracts && index == 0) {
|
||||
auto rejects = [&](const char* name) { require(evaluate(list.Get(), feature, p, nullptr) != 1, name); };
|
||||
if (depthFormat == DXGI_FORMAT_D32_FLOAT || stencil) {
|
||||
d4r_ngx_set_uint(p, "DLSS.Input.Depth.Subrect.Base.X", 1); rejects("Cropped depth-stencil accepted");
|
||||
d4r_ngx_set_uint(p, "DLSS.Input.Depth.Subrect.Base.X", 0);
|
||||
std::puts("RR_NGX_DEPTH_SUBRECT cropped_rejected=1");
|
||||
}
|
||||
d4r_ngx_set_void(p, "ViewToClipMatrix", nullptr); rejects("Missing RR matrix accepted");
|
||||
d4r_ngx_set_void(p, "ViewToClipMatrix", settings.viewToClip);
|
||||
d4r_ngx_set_d3d12_resource(p, "DLSSD.SpecularHitDistance", textures->inputs[0].Get()); rejects("Invalid RR scalar guide format accepted");
|
||||
d4r_ngx_set_d3d12_resource(p, "DLSSD.SpecularHitDistance", layout.frames[index].optional[0] ? textures->inputs[7].Get() : nullptr);
|
||||
d4r_ngx_set_uint(p, "DLSS.Render.Subrect.Dimensions.Width", layout.rw + 1); rejects("Unsupported RR dynamic extent accepted");
|
||||
d4r_ngx_set_uint(p, "DLSS.Render.Subrect.Dimensions.Width", layout.rw);
|
||||
d4r_ngx_set_uint(p, "DLSS.Depth.Type", 1); rejects("Unsupported RR encoding accepted");
|
||||
d4r_ngx_set_uint(p, "DLSS.Depth.Type", 0);
|
||||
d4r_ngx_set_uint(p, "DLSS.Input.Color.Subrect.Base.X", layout.rw); rejects("Invalid RR subrect accepted");
|
||||
d4r_ngx_set_uint(p, "DLSS.Input.Color.Subrect.Base.X", 0);
|
||||
d4r_ngx_set_d3d12_resource(p, "Output", textures->inputs[0].Get()); rejects("Aliased or invalid RR output accepted");
|
||||
d4r_ngx_set_d3d12_resource(p, "Output", textures->output.Get());
|
||||
std::puts("RR_NGX_NEGATIVE rejected=13 command_list_preserved=1");
|
||||
}
|
||||
if (split && index == 0) {
|
||||
D3D12_RESOURCE_BARRIER barrier{}; barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
barrier.Transition = {textures->inputs[0].Get(), 0, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_SOURCE};
|
||||
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLY; list->ResourceBarrier(1, &barrier);
|
||||
require(evaluate(list.Get(), feature, p, nullptr) != 1, "NGX RR accepted unfinished split barrier");
|
||||
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_END_ONLY; list->ResourceBarrier(1, &barrier);
|
||||
transition(list.Get(), textures->inputs[0].Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
|
||||
std::puts("RR_NGX_SPLIT unfinished_rejected=1 paired_before_submit=1");
|
||||
}
|
||||
check(evaluate(list.Get(), feature, p, nullptr), "RR EvaluateFeature");
|
||||
// Both pointer payloads and parameter scalars change before submission.
|
||||
settings.worldToView[0] = 999; settings.viewToClip[5] = 999;
|
||||
d4r_ngx_set_float(p, "DLSS.Pre.Exposure", 999);
|
||||
d4r_ngx_set_d3d12_resource(p, "Color", nullptr);
|
||||
auto output = textures->output;
|
||||
if (enhanced) texture_copy_barrier(list.Get(), output.Get(), enhancedAccess(true), false, false);
|
||||
else transition(list.Get(), output.Get(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
||||
auto source = rr_bridge::D3D12Frames::texture(output.Get());
|
||||
auto dest = rr_bridge::D3D12Frames::placed(readback.Get(), layout.outputOffset, layout.outputPitch,
|
||||
DXGI_FORMAT_R16G16B16A16_FLOAT, layout.ow, layout.oh);
|
||||
list->CopyTextureRegion(&dest, 0, 0, 0, &source, nullptr);
|
||||
if (enhanced) {
|
||||
texture_copy_barrier(list.Get(), output.Get(), enhancedAccess(true), false, true);
|
||||
for (size_t i = 0; i < textures->inputs.size(); ++i)
|
||||
if (textures->inputs[i] && (i < 7 || (optionalMask & (1u << (i - 7)))))
|
||||
switchBarrierModel(list.Get(), textures->inputs[i].Get(), false, false);
|
||||
switchBarrierModel(list.Get(), output.Get(), true, false);
|
||||
} else transition(list.Get(), output.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
if (stencil) stencilCopy(list.Get(), textures->inputs[2].Get(), false);
|
||||
if (index + 1 == layout.frames.size()) {
|
||||
check(release(feature), "RR Release before submission"); feature = nullptr;
|
||||
check(destroy(p), "RR Destroy parameters before submission"); p = nullptr;
|
||||
textures.reset(); output.Reset();
|
||||
}
|
||||
dx(list->Close(), "NGX RR fixture close");
|
||||
ID3D12CommandList* lists[] = {prefix ? prefix.Get() : list.Get(), list.Get()};
|
||||
queue->ExecuteCommandLists(inherited ? 2 : 1, lists);
|
||||
dx(queue->Signal(fence.Get(), index + 1), "NGX RR fixture signal");
|
||||
dx(fence->SetEventOnCompletion(index + 1, event.value), "NGX RR fixture completion");
|
||||
if (WaitForSingleObject(event.value, 10000) != WAIT_OBJECT_0) throw std::runtime_error("NGX RR fixture timeout");
|
||||
D3D12_RANGE range{0, SIZE_T(layout.bytes)}; dx(readback->Map(0, &range, &mapped), "NGX RR readback map");
|
||||
try { layout.validate(mapped, index); } catch (...) { readback->Unmap(0, nullptr); throw; }
|
||||
readback->Unmap(0, nullptr);
|
||||
if (stencil) {
|
||||
D3D12_RANGE stencilRange{0, SIZE_T(stencilBytes)};
|
||||
dx(stencilReadback->Map(0, &stencilRange, &mapped), "RR stencil verification map");
|
||||
bool exact = true;
|
||||
for (uint32_t y = 0; y < layout.rh; ++y) for (uint32_t x = 0; x < layout.rw; ++x)
|
||||
exact &= static_cast<const uint8_t*>(mapped)[stencilFootprint.Offset + y * stencilFootprint.Footprint.RowPitch + x] == uint8_t(x * 13 + y * 17 + index * 29);
|
||||
stencilReadback->Unmap(0, nullptr); require(exact, "RR bridge changed stencil values");
|
||||
}
|
||||
std::printf("RR_NGX_FRAME index=%u snapshot_mutated=1 exact=1 optional_mask=%u hardware_depth=%u depth_inverted=%u\n",
|
||||
index, optionalMask, settings.hardwareDepth, settings.depthInverted);
|
||||
}
|
||||
check(shutdown(), "RR Shutdown");
|
||||
if (contracts) std::puts("PASS RR_NGX_CONTRACTS capabilities=1 requirements=1 warp_rejected=1 invalid_inputs_rejected=1");
|
||||
if (inherited) std::puts("PASS RR_NGX_INHERITED prefix_list=1 caller_states=0");
|
||||
if (enhanced) std::puts("PASS RR_NGX_ENHANCED tracked_access=1 restored=1");
|
||||
if (split) std::puts("PASS RR_NGX_SPLIT unfinished_rejected=1");
|
||||
if (withoutSr) std::puts("PASS RR_NGX_WITHOUT_SR rr_executed=1 sr_available=0");
|
||||
if (floatGuides) std::puts("PASS RR_NGX_FLOAT_GUIDES color_rgba32=1 motion_rg32=1 normals_rgba32=1 roughness_r16=1");
|
||||
if (switchGuides) std::puts("PASS RR_NGX_SWITCH_GUIDES alternate_each_frame=1 feature_recreated=0");
|
||||
if (autoExposure) std::puts("PASS RR_NGX_FLAGS hdr=1 render_motion=1 auto_exposure=1");
|
||||
if (depthFormat != DXGI_FORMAT_UNKNOWN) std::printf("PASS RR_NGX_DEPTH format=%u stencil_preserved=%u\n", unsigned(depthFormat), stencil);
|
||||
std::puts("PASS RR_NGX_LIFECYCLE feature=13 snapshot_mutation=1 release_before_submit=1 parameter_destroy_before_submit=1");
|
||||
return 0;
|
||||
} catch (const std::exception& error) { std::fprintf(stderr, "FAIL RR_NGX %s\n", error.what()); return 1; }
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include "ngx_parameters.h"
|
||||
#include "../../engine/runtime.h"
|
||||
#include <d3d12.h>
|
||||
#include <wrl/client.h>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace d4r::win::rr {
|
||||
inline unsigned number(void* parameters, const char* name, unsigned fallback = 0) {
|
||||
unsigned value = 0;
|
||||
if (d4r_ngx_get_uint(parameters, name, &value) == 1) return value;
|
||||
int signedValue = 0;
|
||||
if (d4r_ngx_get_int(parameters, name, &signedValue) == 1 && signedValue >= 0) return unsigned(signedValue);
|
||||
return fallback;
|
||||
}
|
||||
struct Creation {
|
||||
unsigned width = 0, height = 0, outWidth = 0, outHeight = 0;
|
||||
unsigned flags = 0, outputSubrects = 0, hardwareDepth = 0, roughnessMode = 0;
|
||||
unsigned depthType = 0, normalsType = 0, roughnessType = 0, diffuseType = 0, specularType = 0;
|
||||
int quality = 0;
|
||||
std::array<unsigned, 6> presets{};
|
||||
explicit Creation(void* parameters) {
|
||||
if (!parameters) throw std::runtime_error("Null RR creation parameters");
|
||||
width = number(parameters, "Width"); height = number(parameters, "Height");
|
||||
outWidth = number(parameters, "OutWidth"); outHeight = number(parameters, "OutHeight");
|
||||
if (!width || !height || !outWidth || !outHeight || width > 16384 || height > 16384 || outWidth > 16384 || outHeight > 16384)
|
||||
throw std::runtime_error("Invalid RR creation geometry");
|
||||
flags = number(parameters, "DLSS.Feature.Create.Flags");
|
||||
outputSubrects = number(parameters, "DLSS.Enable.Output.Subrects");
|
||||
hardwareDepth = number(parameters, "DLSS.Use.HW.Depth");
|
||||
roughnessMode = number(parameters, "DLSS.Roughness.Mode");
|
||||
depthType = number(parameters, "DLSS.Depth.Type"); normalsType = number(parameters, "DLSS.Normals.Type");
|
||||
roughnessType = number(parameters, "DLSS.Roughness.Type"); diffuseType = number(parameters, "DLSS.Diffuse.Type");
|
||||
specularType = number(parameters, "DLSS.Specular.Type");
|
||||
quality = ngx::int_value(parameters, "PerfQualityValue");
|
||||
const char* names[] = {"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"};
|
||||
for (size_t i = 0; i < presets.size(); ++i) presets[i] = number(parameters, names[i]);
|
||||
}
|
||||
};
|
||||
// All data that may outlive EvaluateFeature is owned here. No matrix pointer
|
||||
// or resource pointer depends on the caller's parameter object's lifetime.
|
||||
struct Snapshot {
|
||||
enum Plane : size_t { Color, Motion, Depth, Normals, Roughness, Diffuse, Specular, Output,
|
||||
Exposure, HitDistance, Disocclusion, Subsurface, BeforeParticles, Count };
|
||||
std::array<Microsoft::WRL::ComPtr<ID3D12Resource>, Count> resources;
|
||||
std::array<std::array<unsigned, 2>, Count> origins{};
|
||||
RFrameSettings settings;
|
||||
unsigned renderWidth = 0, renderHeight = 0;
|
||||
unsigned depthType = 0, normalsType = 0, roughnessType = 0, diffuseType = 0, specularType = 0;
|
||||
float exposureScale = 1;
|
||||
static ID3D12Resource* resource(void* parameters, const char* name, const char* alias = nullptr) {
|
||||
ID3D12Resource* value = nullptr;
|
||||
(void)d4r_ngx_get_d3d12_resource(parameters, name, &value);
|
||||
if (!value && alias) (void)d4r_ngx_get_d3d12_resource(parameters, alias, &value);
|
||||
return value;
|
||||
}
|
||||
Snapshot(const Creation& creation, void* parameters) {
|
||||
if (!parameters) throw std::runtime_error("Null RR frame parameters");
|
||||
const char* names[] = {"Color", "MotionVectors", "Depth", "GBuffer.Normals", "GBuffer.Roughness",
|
||||
"DLSS.Input.DiffuseAlbedo", "DLSS.Input.SpecularAlbedo", "Output", "ExposureTexture",
|
||||
"DLSSD.SpecularHitDistance", "DLSS.DisocclusionMask", "DLSSD.ScreenSpaceSubsurfaceScatteringGuide", "DLSSD.ColorBeforeParticles"};
|
||||
for (size_t i = 0; i < Count; ++i)
|
||||
resources[i] = resource(parameters, names[i], i == Diffuse ? "GBuffer.DiffuseAlbedo" : i == Specular ? "GBuffer.SpecularAlbedo" : nullptr);
|
||||
renderWidth = number(parameters, "DLSS.Render.Subrect.Dimensions.Width", creation.width);
|
||||
renderHeight = number(parameters, "DLSS.Render.Subrect.Dimensions.Height", creation.height);
|
||||
if (!renderWidth) renderWidth = creation.width;
|
||||
if (!renderHeight) renderHeight = creation.height;
|
||||
const char* prefixes[] = {"DLSS.Input.Color", "DLSS.Input.MV", "DLSS.Input.Depth", "DLSS.Input.Normals",
|
||||
"DLSS.Input.Roughness", "DLSS.Input.DiffuseAlbedo", "DLSS.Input.SpecularAlbedo", "DLSS.Output"};
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
origins[i][0] = number(parameters, (std::string(prefixes[i]) + ".Subrect.Base.X").c_str());
|
||||
origins[i][1] = number(parameters, (std::string(prefixes[i]) + ".Subrect.Base.Y").c_str());
|
||||
}
|
||||
const char* optionalPrefixes[] = {"DLSSD.SpecularHitDistance", "DLSS.DisocclusionMask",
|
||||
"DLSSD.ScreenSpaceSubsurfaceScatteringGuide", "DLSSD.ColorBeforeParticles"};
|
||||
for (size_t i = HitDistance; i < Count; ++i) {
|
||||
origins[i][0] = number(parameters, (std::string(optionalPrefixes[i - HitDistance]) + ".Subrect.Base.X").c_str());
|
||||
origins[i][1] = number(parameters, (std::string(optionalPrefixes[i - HitDistance]) + ".Subrect.Base.Y").c_str());
|
||||
}
|
||||
settings.jitter[0] = ngx::float_value(parameters, "Jitter.Offset.X");
|
||||
settings.jitter[1] = ngx::float_value(parameters, "Jitter.Offset.Y");
|
||||
settings.motionScale[0] = ngx::float_value(parameters, "MV.Scale.X", 1);
|
||||
settings.motionScale[1] = ngx::float_value(parameters, "MV.Scale.Y", 1);
|
||||
settings.preExposure = ngx::float_value(parameters, "DLSS.Pre.Exposure", 1);
|
||||
settings.frameTimeMs = ngx::float_value(parameters, "FrameTimeDeltaInMsec", 16.666667f);
|
||||
exposureScale = ngx::float_value(parameters, "DLSS.Exposure.Scale", 1);
|
||||
settings.reset = number(parameters, "Reset") != 0;
|
||||
settings.depthInverted = (creation.flags & 8) != 0;
|
||||
settings.hardwareDepth = number(parameters, "DLSS.Use.HW.Depth", creation.hardwareDepth) != 0;
|
||||
// D/E consume a bound roughness texture even in packed mode. Only the
|
||||
// absent-texture case uses normals.w; preset F has a different contract.
|
||||
settings.roughnessPacked = number(parameters, "DLSS.Roughness.Mode", creation.roughnessMode) == 1 && !resources[Roughness];
|
||||
if (settings.roughnessPacked) { resources[Roughness] = resources[Normals]; origins[Roughness] = origins[Normals]; }
|
||||
depthType = number(parameters, "DLSS.Depth.Type", creation.depthType);
|
||||
normalsType = number(parameters, "DLSS.Normals.Type", creation.normalsType);
|
||||
roughnessType = number(parameters, "DLSS.Roughness.Type", creation.roughnessType);
|
||||
diffuseType = number(parameters, "DLSS.Diffuse.Type", creation.diffuseType);
|
||||
specularType = number(parameters, "DLSS.Specular.Type", creation.specularType);
|
||||
const char* matrixNames[] = {"WorldToViewMatrix", "ViewToClipMatrix"};
|
||||
float* matrices[] = {settings.worldToView, settings.viewToClip};
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
void* source = nullptr;
|
||||
if (d4r_ngx_get_void(parameters, matrixNames[i], &source) != 1 || !source)
|
||||
throw std::runtime_error(std::string("Missing RR matrix: ") + matrixNames[i]);
|
||||
std::memcpy(matrices[i], source, sizeof(float) * 16);
|
||||
for (unsigned n = 0; n < 16; ++n)
|
||||
if (!std::isfinite(matrices[i][n])) throw std::runtime_error("Nonfinite RR matrix");
|
||||
}
|
||||
for (float value : {settings.jitter[0], settings.jitter[1], settings.motionScale[0], settings.motionScale[1],
|
||||
settings.preExposure, settings.frameTimeMs, exposureScale})
|
||||
if (!std::isfinite(value)) throw std::runtime_error("Nonfinite RR frame scalar");
|
||||
if (settings.preExposure <= 0 || exposureScale <= 0 || settings.frameTimeMs < 0)
|
||||
throw std::runtime_error("Invalid RR exposure or frame time");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// CPU-only checks of the NGX RR snapshot contract. No model or GPU dispatch.
|
||||
#include <volk.h>
|
||||
#include "rr_ngx_snapshot.h"
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
struct Parameters {
|
||||
void* value = d4r_ngx_parameters_create();
|
||||
Parameters() { if (!value) throw std::runtime_error("Parameter allocation failed"); }
|
||||
~Parameters() { if (value) d4r_ngx_parameters_destroy(value); }
|
||||
};
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
template<class F> void rejects(F operation, const char* message) {
|
||||
try { operation(); } catch (const std::runtime_error&) { return; }
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
int main() try {
|
||||
using namespace d4r::win::rr;
|
||||
Parameters parameters;
|
||||
void* p = parameters.value;
|
||||
// RR callers can publish dimensions as signed NGX integers.
|
||||
d4r_ngx_set_int(p, "Width", 64); d4r_ngx_set_uint(p, "Height", 48);
|
||||
d4r_ngx_set_uint(p, "OutWidth", 128); d4r_ngx_set_uint(p, "OutHeight", 96);
|
||||
d4r_ngx_set_int(p, "DLSS.Feature.Create.Flags", 8);
|
||||
d4r_ngx_set_int(p, "DLSS.Use.HW.Depth", 1);
|
||||
d4r_ngx_set_int(p, "DLSS.Roughness.Mode", 1);
|
||||
d4r_ngx_set_uint(p, "RayReconstruction.Hint.Render.Preset.Quality", 4);
|
||||
Creation creation(p);
|
||||
require(creation.width == 64 && creation.presets[1] == 4, "RR creation values lost");
|
||||
float view[16]{}, clip[16]{};
|
||||
for (unsigned i = 0; i < 16; ++i) { view[i] = float(i + 1); clip[i] = float(i + 32); }
|
||||
d4r_ngx_set_void(p, "WorldToViewMatrix", view); d4r_ngx_set_void(p, "ViewToClipMatrix", clip);
|
||||
d4r_ngx_set_float(p, "Jitter.Offset.X", .25f);
|
||||
d4r_ngx_set_float(p, "DLSS.Pre.Exposure", 2.f);
|
||||
d4r_ngx_set_uint(p, "DLSS.Input.Normals.Subrect.Base.X", 7);
|
||||
d4r_ngx_set_uint(p, "DLSSD.SpecularHitDistance.Subrect.Base.X", 5);
|
||||
d4r_ngx_set_uint(p, "DLSSD.ColorBeforeParticles.Subrect.Base.Y", 9);
|
||||
d4r_ngx_set_uint(p, "DLSS.Use.HW.Depth", 0);
|
||||
Snapshot frame(creation, p);
|
||||
view[0] = -91; clip[15] = -93;
|
||||
d4r_ngx_set_float(p, "DLSS.Pre.Exposure", 3.f);
|
||||
d4r_ngx_set_uint(p, "Width", 777);
|
||||
d4r_ngx_set_uint(p, "RayReconstruction.Hint.Render.Preset.Quality", 5);
|
||||
require(frame.settings.worldToView[0] == 1 && frame.settings.viewToClip[15] == 47, "RR matrices were not copied");
|
||||
require(frame.settings.preExposure == 2 && frame.settings.jitter[0] == .25f, "RR scalars were not copied");
|
||||
require(creation.width == 64 && creation.presets[1] == 4, "RR creation depends on mutable parameters");
|
||||
require(frame.settings.depthInverted && !frame.settings.hardwareDepth, "RR frame override lost");
|
||||
require(frame.renderWidth == 64 && frame.renderHeight == 48, "RR render dimensions lost creation fallback");
|
||||
require(frame.settings.roughnessPacked && frame.origins[Snapshot::Roughness][0] == 7, "RR packed roughness origin lost");
|
||||
require(frame.origins[Snapshot::HitDistance][0] == 5 && frame.origins[Snapshot::BeforeParticles][1] == 9,
|
||||
"RR optional guide origins lost");
|
||||
auto copied = frame;
|
||||
require(copied.settings.worldToView[0] == 1, "Copied RR snapshot lost matrix ownership");
|
||||
{
|
||||
Parameters frameParameters;
|
||||
d4r_ngx_set_void(frameParameters.value, "WorldToViewMatrix", view);
|
||||
d4r_ngx_set_void(frameParameters.value, "ViewToClipMatrix", clip);
|
||||
Snapshot baseline(creation, frameParameters.value);
|
||||
require(baseline.settings.hardwareDepth && baseline.settings.roughnessPacked,
|
||||
"RR frame settings lost creation baseline");
|
||||
require(baseline.settings.preExposure == 1 && baseline.settings.motionScale[0] == 1,
|
||||
"RR frame scalar defaults incorrect");
|
||||
}
|
||||
view[3] = std::numeric_limits<float>::infinity();
|
||||
rejects([&] { Snapshot bad(creation, p); }, "Nonfinite RR matrix accepted");
|
||||
view[3] = 4;
|
||||
d4r_ngx_set_float(p, "DLSS.Pre.Exposure", 0);
|
||||
rejects([&] { Snapshot bad(creation, p); }, "Zero RR pre-exposure accepted");
|
||||
d4r_ngx_set_float(p, "DLSS.Pre.Exposure", 1);
|
||||
d4r_ngx_set_float(p, "Jitter.Offset.Y", std::numeric_limits<float>::quiet_NaN());
|
||||
rejects([&] { Snapshot bad(creation, p); }, "Nonfinite RR jitter accepted");
|
||||
d4r_ngx_set_float(p, "Jitter.Offset.Y", 0);
|
||||
d4r_ngx_set_void(p, "ViewToClipMatrix", nullptr);
|
||||
rejects([&] { Snapshot bad(creation, p); }, "Missing RR matrix accepted");
|
||||
d4r_ngx_set_uint(p, "Width", 0);
|
||||
rejects([&] { Creation bad(p); }, "Zero RR creation width accepted");
|
||||
// The completed snapshot must still be valid after the parameter object dies.
|
||||
d4r_ngx_parameters_destroy(parameters.value); parameters.value = nullptr;
|
||||
require(copied.settings.viewToClip[15] == 47 && copied.settings.preExposure == 2, "RR snapshot retained parameter storage");
|
||||
std::puts("PASS RR_SNAPSHOT cpu_only=1 matrices_owned=1 scalars_owned=1 creation_fallback=1 invalid_rejected=1");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "FAIL RR_SNAPSHOT %s\n", error.what()); return 1;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Run the synthetic production NGX feature-13 harness in a bounded process."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--contracts', action='store_true', help='also check capability readiness and negative contracts')
|
||||
parser.add_argument('--inherited', action='store_true', help='inherit access from a preceding list in the same submission batch')
|
||||
parser.add_argument('--enhanced', action='store_true', help='exercise enhanced texture access and restoration')
|
||||
parser.add_argument('--split', action='store_true', help='reject a legacy split barrier, then complete the pair and evaluate')
|
||||
parser.add_argument('--unavailable', choices=('disabled', 'missing-model', 'fsr'), help='test unavailable RR configuration without inference')
|
||||
parser.add_argument('--without-sr', action='store_true', help='remove SR dependency configuration and require native RR execution')
|
||||
parser.add_argument('--float-guides', action='store_true', help='exercise equivalent float32 color/motion/normals and scalar roughness')
|
||||
parser.add_argument('--switch-guides', action='store_true', help='alternate canonical and float guide formats without recreating feature')
|
||||
parser.add_argument('--auto-exposure', action='store_true', help='also set the public NGX AutoExposure creation flag')
|
||||
parser.add_argument('--depth-format', choices=('r32', 'r32-typeless', 'd32', 'd32s8', 'd32s8-typeless'), default='r32', help='exercise native depth-plane copy format')
|
||||
parser.add_argument('--player-config', action='store_true', help='stage a private player folder and select RR only through d4r.ini')
|
||||
parser.add_argument('--use-existing-ini', action='store_true', help='preserve the selected runtime INI when staging player validation')
|
||||
for name in ('probe', 'shim', 'runtime', 'core', 'dlss', 'model', 'inputs', 'reference', 'output'):
|
||||
parser.add_argument('--' + name, required=True, type=lambda value: Path(value).resolve())
|
||||
args = parser.parse_args()
|
||||
if args.use_existing_ini and not args.player_config:
|
||||
parser.error('--use-existing-ini requires --player-config')
|
||||
if args.player_config and (args.without_sr or args.unavailable or not args.contracts):
|
||||
parser.error('--player-config requires --contracts, configured SR dependencies and native RR')
|
||||
if args.enhanced and args.split:
|
||||
parser.error('--split currently covers legacy barriers; use a separate --enhanced run')
|
||||
if args.without_sr and (args.unavailable or not args.contracts):
|
||||
parser.error('--without-sr requires --contracts and a configured native RR backend')
|
||||
if args.switch_guides and not args.float_guides:
|
||||
parser.error('--switch-guides requires --float-guides')
|
||||
build = Path(__file__).resolve().parents[2] / 'build'
|
||||
if not args.output.is_relative_to(build.resolve()):
|
||||
parser.error('output must stay under ignored build/')
|
||||
args.output.mkdir(parents=True, exist_ok=False)
|
||||
env = {key: value for key, value in os.environ.items()
|
||||
if not key.startswith('D4R_') and key not in ('ZLUDA_CUDA_LIB', 'ZLUDA_LOG_DIR', 'ZLUDA_CACHE_DIR')}
|
||||
runtime = args.runtime
|
||||
native = runtime / 'native'
|
||||
if (native / 'gfx1101').is_dir():
|
||||
native = native / 'gfx1101'
|
||||
env.update(HIP_PATH=str(runtime / 'hip'), D4R_HIP_ROOT=str(runtime / 'hip'), D4R_NVCUDA_DLL=str(runtime / 'zluda/nvcuda.dll'),
|
||||
ZLUDA_CUDA_LIB=str(runtime / 'zluda/nvcuda.dll'), D4R_DLSS_DLL=str(args.dlss),
|
||||
D4R_NGX_CORE=str(args.core), D4R_NVAPI_DLL=str(runtime / 'nvapi/nvapi64.dll'),
|
||||
D4R_NVAPI_BACKEND=str(runtime / 'nvapi/backend/nvapi64.dll'),
|
||||
D4R_FORMAT_MODULE=str(runtime / 'pixel_convert_gfx1101.hsaco'),
|
||||
D4R_ZLUDA_NATIVE_DIR=str(native), D4R_QUIET_API='1',
|
||||
D4R_ZLUDA_WMMA='1', D4R_ZLUDA_WMMA_F16_REFERENCE='1',
|
||||
D4R_NGX_DATA_DIR=str(args.output / 'ngx-data'), D4R_RR_BACKEND='native', D4R_RR_MODEL_DIR=str(args.model))
|
||||
if args.unavailable == 'disabled':
|
||||
env.pop('D4R_RR_BACKEND')
|
||||
elif args.unavailable == 'missing-model':
|
||||
missing = args.output / 'missing-model'
|
||||
env['D4R_RR_MODEL_DIR'] = str(missing)
|
||||
elif args.unavailable == 'fsr':
|
||||
env['D4R_RR_BACKEND'] = 'fsr'
|
||||
if args.without_sr:
|
||||
for key in ('HIP_PATH', 'D4R_HIP_ROOT', 'D4R_NVCUDA_DLL', 'ZLUDA_CUDA_LIB', 'D4R_DLSS_DLL',
|
||||
'D4R_NGX_CORE', 'D4R_NVAPI_DLL', 'D4R_NVAPI_BACKEND', 'D4R_FORMAT_MODULE', 'D4R_ZLUDA_NATIVE_DIR'):
|
||||
env.pop(key, None)
|
||||
else:
|
||||
env['PATH'] = str(runtime / 'hip/bin') + os.pathsep + str(runtime / 'zluda') + os.pathsep + env['PATH']
|
||||
command = [str(args.probe), str(args.shim), str(args.inputs), str(args.reference),
|
||||
str(args.output / 'frames'), str(args.model)]
|
||||
if args.player_config:
|
||||
staged = args.output / 'player'
|
||||
shutil.copytree(runtime, staged)
|
||||
shutil.copy2(args.shim, staged / 'd4r_nvngx.dll')
|
||||
for source, relative in ((args.core, 'ngx/_nvngx.dll'), (args.dlss, 'dlss/nvngx_dlss.dll')):
|
||||
target = staged / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, target)
|
||||
if not args.use_existing_ini:
|
||||
config = (Path(__file__).resolve().parents[2] / 'packaging/windows/d4r.ini').read_text()
|
||||
config = config.replace('Backend = off', 'Backend = native').replace('ModelDir = auto', 'ModelDir = ' + str(args.model))
|
||||
(staged / 'd4r.ini').write_text(config)
|
||||
env = {key: value for key, value in os.environ.items()
|
||||
if not key.startswith('D4R_') and key not in ('HIP_PATH', 'ZLUDA_CUDA_LIB', 'ZLUDA_LOG_DIR', 'ZLUDA_CACHE_DIR')}
|
||||
command[1] = str(staged / 'd4r_nvngx.dll')
|
||||
mode = '-'.join(name for name in ('contracts', 'inherited', 'enhanced', 'split', 'unavailable') if getattr(args, name))
|
||||
if args.without_sr:
|
||||
mode += '-without-sr'
|
||||
if args.float_guides:
|
||||
mode += '-float-guides'
|
||||
if args.switch_guides:
|
||||
mode += '-switch-guides'
|
||||
if args.auto_exposure:
|
||||
mode += '-auto-exposure'
|
||||
if args.depth_format != 'r32':
|
||||
mode += '-depth-' + args.depth_format
|
||||
if mode:
|
||||
command.append(mode)
|
||||
report = {'passed': False, 'gameAccessed': False, 'qualityReference': False,
|
||||
'artifacts': {str(path): hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
for path in (args.probe, args.shim, args.core, args.dlss)}}
|
||||
try:
|
||||
with (args.output / 'stdout.log').open('wb') as out, (args.output / 'stderr.log').open('wb') as err:
|
||||
result = subprocess.run(command, env=env, stdout=out, stderr=err, timeout=180)
|
||||
report['exitCode'] = result.returncode
|
||||
text = (args.output / 'stdout.log').read_text(errors='replace')
|
||||
if args.unavailable:
|
||||
if result.returncode or 'PASS RR_NGX_UNAVAILABLE' not in text:
|
||||
raise RuntimeError('Unavailable NGX RR checks failed; inspect ' + str(args.output))
|
||||
report.update(passed=True, unavailable=args.unavailable, rrAvailable=False, srAvailable=True,
|
||||
creationRejected=True, frames=0)
|
||||
print('PASS NGX RR unavailable=' + args.unavailable + ' sr_available=1')
|
||||
return
|
||||
if result.returncode or 'PASS RR_NGX_LIFECYCLE' not in text:
|
||||
raise RuntimeError('NGX RR harness failed; inspect ' + str(args.output))
|
||||
if args.contracts and 'PASS RR_NGX_CONTRACTS' not in text:
|
||||
raise RuntimeError('NGX RR contract checks incomplete')
|
||||
if args.inherited and 'PASS RR_NGX_INHERITED' not in text:
|
||||
raise RuntimeError('NGX RR inherited access checks incomplete')
|
||||
for name in ('enhanced', 'split'):
|
||||
if getattr(args, name) and 'PASS RR_NGX_' + name.upper() not in text:
|
||||
raise RuntimeError('NGX RR ' + name + ' access checks incomplete')
|
||||
if args.without_sr and ('PASS RR_NGX_WITHOUT_SR' not in text or 'D4R_RUNTIME platform=Windows' in text):
|
||||
raise RuntimeError('NGX RR did not prove execution without initialized SR runtime')
|
||||
if args.float_guides and 'PASS RR_NGX_FLOAT_GUIDES' not in text:
|
||||
raise RuntimeError('NGX RR alternate float guide checks incomplete')
|
||||
if args.switch_guides and 'PASS RR_NGX_SWITCH_GUIDES' not in text:
|
||||
raise RuntimeError('NGX RR temporal guide-format switching checks incomplete')
|
||||
if args.auto_exposure and 'PASS RR_NGX_FLAGS' not in text:
|
||||
raise RuntimeError('NGX RR creation flag checks incomplete')
|
||||
depth_values = {'r32-typeless': 39, 'd32': 40, 'd32s8': 20, 'd32s8-typeless': 19}
|
||||
if args.depth_format != 'r32':
|
||||
marker = f'PASS RR_NGX_DEPTH format={depth_values[args.depth_format]} stencil_preserved={int(args.depth_format.startswith("d32s8"))}'
|
||||
if marker not in text:
|
||||
raise RuntimeError('NGX RR depth-format/stencil checks incomplete')
|
||||
if args.contracts and args.depth_format.startswith('d32') and 'RR_NGX_DEPTH_SUBRECT cropped_rejected=1' not in text:
|
||||
raise RuntimeError('NGX RR cropped depth-stencil rejection missing')
|
||||
expected = [line.split()[1] + '-out.bin' for line in (args.inputs / 'frames.txt').read_text().splitlines()
|
||||
if line.startswith('frame ')]
|
||||
outputs = sorted(path.name for path in (args.output / 'frames').glob('*-out.bin'))
|
||||
if outputs != sorted(expected):
|
||||
raise RuntimeError('Incomplete NGX RR output set')
|
||||
for name in expected:
|
||||
if (args.output / 'frames' / name).read_bytes() != (args.reference / name).read_bytes():
|
||||
raise RuntimeError('NGX RR reference mismatch: ' + name)
|
||||
optional_names = ('hitdistance', 'disocclusion', 'sss', 'particles')
|
||||
optional_frames = {name: sum((args.inputs / (frame.removesuffix('-out.bin') + '-' + name + '.bin')).is_file()
|
||||
for frame in expected) for name in optional_names}
|
||||
masks = [sum(1 << index for index, name in enumerate(optional_names)
|
||||
if (args.inputs / (frame.removesuffix('-out.bin') + '-' + name + '.bin')).is_file())
|
||||
for frame in expected]
|
||||
frame_lines = [line.split() for line in (args.inputs / 'frames.txt').read_text().splitlines() if line.startswith('frame ')]
|
||||
for index, mask in enumerate(masks):
|
||||
depth_mode = int(frame_lines[index][8])
|
||||
marker = (f'RR_NGX_FRAME index={index} snapshot_mutated=1 exact=1 optional_mask={mask}'
|
||||
f' hardware_depth={int(bool(depth_mode & 2))} depth_inverted={int(bool(depth_mode & 1))}')
|
||||
if marker not in text:
|
||||
raise RuntimeError('Incomplete NGX optional-guide/depth setting checks')
|
||||
report.update(passed=True, frames=len(expected), standaloneExact=True, contracts=args.contracts, inherited=args.inherited,
|
||||
enhanced=args.enhanced, split=args.split,
|
||||
withoutSr=args.without_sr,
|
||||
floatGuides=args.float_guides,
|
||||
switchGuides=args.switch_guides,
|
||||
creationFlags=int(next(line.split('flags=')[1] for line in text.splitlines()
|
||||
if line.startswith('RR_NGX_CREATION flags='))),
|
||||
optionalGuideFrames=optional_frames, optionalMasks=masks,
|
||||
depthFormat=args.depth_format, stencilPreserved=args.depth_format.startswith('d32s8'),
|
||||
hardwareDepthFrames=sum(bool(int(line[8]) & 2) for line in frame_lines),
|
||||
playerConfig=args.player_config,
|
||||
preservedPlayerIni=args.use_existing_ini,
|
||||
invertedDepthFrames=sum(bool(int(line[8]) & 1) for line in frame_lines),
|
||||
snapshotMutation=True, releaseBeforeSubmit=True, parameterDestroyBeforeSubmit=True)
|
||||
print('PASS NGX RR frames=' + str(len(expected)) + ' standalone_exact=1')
|
||||
finally:
|
||||
(args.output / 'summary.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user