rr: add FSR Ray Regeneration, exact RDNA3 inference and motion experiments

Add an experimental FSR Ray Regeneration backend for D3D12 RR requests:
- Convert depth, motion, normals and albedos into AMD denoiser inputs.
- Split and recompose diffuse/specular signals, then run Super Sampling.
- Add stability/detail controls, demodulation guards and halo limiting.
- Handle AMD provider discovery under NVIDIA GPU spoofing.
- Add runtime probes, synthetic reference scenes and per-dispatch profiling.

Add exact gfx11 DLSS-RR inference using f16 cooperative matrices:
- Reproduce FP8 conversions, accumulation and camera reprojection arithmetic.
- Widen model weights and avoid requiring VK_EXT_shader_float8.
- Extend model building, replay tools and descriptor metadata.

Expand motion preprocessing and experimental AW2 native-motion support:
- Add tiled LDS dilation, padded allocation handling and thin-only sky fill.
- Order dilation with input waits and NGX on the selected stream.
- Add jitter-aligned native composition, hybrid routing and wire history repair.
- Add prepared masks, sparse repair and independent motion/repair controls.

Add optional fused R11G11B10 input/output conversion, sampled stage timings
and profile summarization. Extend configuration, packaging, documentation,
FidelityFX headers and CPU/GPU regression coverage.
This commit is contained in:
ayo committed 2026-10-16 04:55:25 -04:00
1 parent 17c1828f97
commit 34e6b0fe71
83 files changed
+11300 -195

No files matched your search

+279
View File
@@ -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;
}
}
File diff suppressed because it is too large. Load diff
+777
View File
@@ -0,0 +1,777 @@
// Diagnostic per-dispatch GPU timing of AMD's denoiser (D4R_FSR_RR_DISPATCH_PROFILE=1, off by default).
//
// While ffxDispatch records into the game's list, the list's Dispatch/ExecuteIndirect entries (vkd3d-proton's
// shared command-list vtable) are wrapped to put a timestamp before every dispatch; the device's compute PSO
// creation is wrapped to tag each pipeline with vkd3d's shader hash (FNV-1 64 of the DXBC blob, the name
// VKD3D_SHADER_DUMP_PATH and VKD3D_SHADER_OVERRIDE use). Stamp i..i+1 is the time from the end of the work
// before dispatch i to the end of dispatch i (bottom-of-pipe), which is dispatch i's cost when the denoiser
// separates its passes with barriers. Wrappers forward unchanged outside the recording thread's window.
#pragma once
#include <d3d12.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <map>
#include <mutex>
#include <unordered_map>
#include <vector>
namespace fsr_rr_dp
{
constexpr UINT kSlots = 4;
constexpr UINT kStampsPerSlot = 2048;
using DispatchFn = void(STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, UINT, UINT);
using ExecuteIndirectFn = void(STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, ID3D12CommandSignature*, UINT,
ID3D12Resource*, UINT64, ID3D12Resource*, UINT64);
using ResourceBarrierFn = void(STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, const D3D12_RESOURCE_BARRIER*);
using SetRootCbvFn = void(STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, D3D12_GPU_VIRTUAL_ADDRESS);
using SetPipelineStateFn = void(STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, ID3D12PipelineState*);
using CreateComputeFn = HRESULT(STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_COMPUTE_PIPELINE_STATE_DESC*, REFIID,
void**);
using CreateStreamFn = HRESULT(STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_PIPELINE_STATE_STREAM_DESC*, REFIID,
void**);
struct Record
{
uint64_t hash;
UINT x, y, z;
bool indirect;
uint32_t barriers, uavBarriers; // recorded by the denoiser between the previous dispatch and this one
};
struct Stat
{
uint64_t hash = 0;
UINT x = 0, y = 0, z = 0;
bool indirect = false;
uint32_t barriers = 0, uavBarriers = 0;
double sum = 0;
uint32_t count = 0;
};
struct State
{
std::mutex mutex;
std::unordered_map<const void*, uint64_t> psoHash;
DispatchFn dispatch = nullptr;
ExecuteIndirectFn executeIndirect = nullptr;
SetPipelineStateFn setPipelineState = nullptr;
ResourceBarrierFn resourceBarrier = nullptr;
SetRootCbvFn setRootCbv = nullptr;
uint32_t barriersSinceDispatch = 0, uavBarriersSinceDispatch = 0;
CreateComputeFn createCompute = nullptr;
CreateStreamFn createStream = nullptr;
bool listHooked = false, deviceHooked = false, failed = false;
ID3D12QueryHeap* heap = nullptr;
ID3D12Resource* readback = nullptr;
volatile uint64_t* values = nullptr;
double ticksPerMs = 0;
std::vector<Record> records[kSlots];
UINT used[kSlots] = {};
std::map<UINT, Stat> stats; // by dispatch index within the frame
uint32_t frames = 0;
uint64_t lastHash = 0;
};
inline State& state()
{
static State s;
return s;
}
thread_local ID3D12GraphicsCommandList* t_list = nullptr;
thread_local UINT t_slot = 0;
thread_local uint64_t t_pso = 0;
thread_local D3D12_GPU_VIRTUAL_ADDRESS t_cbv = 0;
inline void snapshot_after_dispatch(ID3D12GraphicsCommandList* list);
inline uint64_t fnv1(const void* data, size_t size)
{
uint64_t h = 0xcbf29ce484222325ull;
const auto* p = static_cast<const uint8_t*>(data);
for (size_t i = 0; i < size; ++i)
h = (h * 0x100000001b3ull) ^ p[i];
return h;
}
inline bool patch(void** slot, void* hook, void** original)
{
if (*slot == hook)
return true;
DWORD old = 0;
if (!VirtualProtect(slot, sizeof(void*), PAGE_READWRITE, &old))
return false;
*original = *slot;
*slot = hook;
VirtualProtect(slot, sizeof(void*), old, &old);
return true;
}
inline void stamp(ID3D12GraphicsCommandList* list, uint64_t hash, UINT x, UINT y, UINT z, bool indirect)
{
State& s = state();
UINT& used = s.used[t_slot];
if (used + 1 >= kStampsPerSlot)
return;
list->EndQuery(s.heap, D3D12_QUERY_TYPE_TIMESTAMP, t_slot * kStampsPerSlot + used);
s.records[t_slot].push_back({hash, x, y, z, indirect, s.barriersSinceDispatch, s.uavBarriersSinceDispatch});
s.barriersSinceDispatch = s.uavBarriersSinceDispatch = 0;
++used;
}
static void STDMETHODCALLTYPE hook_set_pipeline_state(ID3D12GraphicsCommandList* list, ID3D12PipelineState* pso)
{
State& s = state();
if (t_list == list)
{
std::lock_guard<std::mutex> lock(s.mutex);
auto found = s.psoHash.find(pso);
t_pso = found != s.psoHash.end() ? found->second : 0;
}
s.setPipelineState(list, pso);
}
inline bool serialize()
{
static const bool on = env_uint("D4R_FSR_RR_DP_SERIALIZE", 0) != 0;
return on;
}
static void STDMETHODCALLTYPE hook_resource_barrier(ID3D12GraphicsCommandList* list, UINT count,
const D3D12_RESOURCE_BARRIER* barriers)
{
State& s = state();
if (t_list == list)
for (UINT i = 0; i < count; ++i)
{
++s.barriersSinceDispatch;
if (barriers[i].Type == D3D12_RESOURCE_BARRIER_TYPE_UAV)
++s.uavBarriersSinceDispatch;
}
s.resourceBarrier(list, count, barriers);
}
// Diagnostic: a full UAV barrier before every dispatch of the denoiser (tests for missing barriers).
inline void serialize_barrier(ID3D12GraphicsCommandList* list)
{
if (!serialize())
return;
D3D12_RESOURCE_BARRIER uav = {};
uav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
state().resourceBarrier(list, 1, &uav);
}
static void STDMETHODCALLTYPE hook_set_root_cbv(ID3D12GraphicsCommandList* list, UINT index,
D3D12_GPU_VIRTUAL_ADDRESS va)
{
if (t_list == list)
t_cbv = va;
state().setRootCbv(list, index, va);
}
static void STDMETHODCALLTYPE hook_dispatch(ID3D12GraphicsCommandList* list, UINT x, UINT y, UINT z)
{
State& s = state();
if (t_list == list)
{
serialize_barrier(list);
stamp(list, t_pso, x, y, z, false);
}
s.dispatch(list, x, y, z);
if (t_list == list)
snapshot_after_dispatch(list);
}
static void STDMETHODCALLTYPE hook_execute_indirect(ID3D12GraphicsCommandList* list, ID3D12CommandSignature* sig,
UINT maxCount, ID3D12Resource* args, UINT64 argsOffset,
ID3D12Resource* count, UINT64 countOffset)
{
State& s = state();
if (t_list == list)
{
serialize_barrier(list);
stamp(list, t_pso, maxCount, 0, 0, true);
}
s.executeIndirect(list, sig, maxCount, args, argsOffset, count, countOffset);
if (t_list == list)
snapshot_after_dispatch(list);
}
static HRESULT STDMETHODCALLTYPE hook_create_compute(ID3D12Device* device, const D3D12_COMPUTE_PIPELINE_STATE_DESC* desc,
REFIID riid, void** pso)
{
State& s = state();
const HRESULT hr = s.createCompute(device, desc, riid, pso);
if (SUCCEEDED(hr) && pso != nullptr && *pso != nullptr && desc != nullptr && desc->CS.pShaderBytecode != nullptr)
{
const uint64_t hash = fnv1(desc->CS.pShaderBytecode, desc->CS.BytecodeLength);
std::lock_guard<std::mutex> lock(s.mutex);
s.psoHash[*pso] = hash;
}
return hr;
}
// The CS bytecode of a pipeline state stream (D3DX12-style subobjects, each aligned to a pointer).
inline const D3D12_SHADER_BYTECODE* stream_cs(const D3D12_PIPELINE_STATE_STREAM_DESC* desc)
{
auto* p = static_cast<const uint8_t*>(desc->pPipelineStateSubobjectStream);
const uint8_t* end = p + desc->SizeInBytes;
while (p + sizeof(UINT) <= end)
{
const UINT type = *reinterpret_cast<const UINT*>(p);
size_t size;
switch (type)
{
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_ROOT_SIGNATURE: size = 16; break;
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CS:
return reinterpret_cast<const D3D12_SHADER_BYTECODE*>(p + 8);
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_VS:
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_PS:
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_DS:
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_HS:
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_GS:
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_CACHED_PSO: size = 24; break;
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_NODE_MASK:
case D3D12_PIPELINE_STATE_SUBOBJECT_TYPE_FLAGS: size = 8; break;
default: return nullptr;
}
p += size;
}
return nullptr;
}
static HRESULT STDMETHODCALLTYPE hook_create_stream(ID3D12Device* device, const D3D12_PIPELINE_STATE_STREAM_DESC* desc,
REFIID riid, void** pso)
{
State& s = state();
const HRESULT hr = s.createStream(device, desc, riid, pso);
const D3D12_SHADER_BYTECODE* cs = desc != nullptr ? stream_cs(desc) : nullptr;
if (SUCCEEDED(hr) && pso != nullptr && *pso != nullptr && cs != nullptr && cs->pShaderBytecode != nullptr)
{
const uint64_t hash = fnv1(cs->pShaderBytecode, cs->BytecodeLength);
std::lock_guard<std::mutex> lock(s.mutex);
s.psoHash[*pso] = hash;
}
return hr;
}
// D4R_FSR_RR_DP_SNAPSHOT=k1,k2,...: on frame D4R_FSR_RR_DP_SNAPSHOT_FRAME (default 1), copy every UAV buffer the
// denoiser created after its dispatch k (0-based), to <D4R_FSR_RR_DUMP_DIR>/snap_<k>_<buffer>.raw at teardown.
using CreateCommittedFn = HRESULT(STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_PROPERTIES*, D3D12_HEAP_FLAGS,
const D3D12_RESOURCE_DESC*, D3D12_RESOURCE_STATES,
const D3D12_CLEAR_VALUE*, REFIID, void**);
using CreatePlacedFn = HRESULT(STDMETHODCALLTYPE*)(ID3D12Device*, ID3D12Heap*, UINT64, const D3D12_RESOURCE_DESC*,
D3D12_RESOURCE_STATES, const D3D12_CLEAR_VALUE*, REFIID, void**);
using CreateCommitted1Fn = HRESULT(STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_PROPERTIES*, D3D12_HEAP_FLAGS,
const D3D12_RESOURCE_DESC*, D3D12_RESOURCE_STATES,
const D3D12_CLEAR_VALUE*, ID3D12ProtectedResourceSession*,
REFIID, void**);
struct Created
{
ID3D12Resource* resource;
D3D12_RESOURCE_DESC desc;
const char* how;
};
inline std::vector<Created>& created()
{
static std::vector<Created> c;
return c;
}
thread_local bool t_creating = false;
inline CreateCommittedFn& orig_committed() { static CreateCommittedFn f = nullptr; return f; }
inline CreatePlacedFn& orig_placed() { static CreatePlacedFn f = nullptr; return f; }
inline CreateCommitted1Fn& orig_committed1() { static CreateCommitted1Fn f = nullptr; return f; }
inline void note_created(HRESULT hr, const D3D12_RESOURCE_DESC* desc, void** out, const char* how)
{
if (!t_creating || FAILED(hr) || out == nullptr || *out == nullptr || desc == nullptr)
return;
// Only ID3D12Resource-compatible pointers are recorded; every resource interface starts with ID3D12Resource.
created().push_back({static_cast<ID3D12Resource*>(*out), *desc, how});
logf("D4R_FSR_RR_DP resource %zu (%s): dim %d %llux%ux%u format %d flags 0x%x", created().size() - 1, how,
desc->Dimension, static_cast<unsigned long long>(desc->Width), desc->Height, desc->DepthOrArraySize,
desc->Format, desc->Flags);
}
static HRESULT STDMETHODCALLTYPE hook_committed(ID3D12Device* d, const D3D12_HEAP_PROPERTIES* hp, D3D12_HEAP_FLAGS hf,
const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES st,
const D3D12_CLEAR_VALUE* cv, REFIID riid, void** out)
{
const HRESULT hr = orig_committed()(d, hp, hf, desc, st, cv, riid, out);
note_created(hr, desc, out, "committed");
return hr;
}
static HRESULT STDMETHODCALLTYPE hook_placed(ID3D12Device* d, ID3D12Heap* heap, UINT64 offset,
const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES st,
const D3D12_CLEAR_VALUE* cv, REFIID riid, void** out)
{
const HRESULT hr = orig_placed()(d, heap, offset, desc, st, cv, riid, out);
note_created(hr, desc, out, "placed");
return hr;
}
static HRESULT STDMETHODCALLTYPE hook_committed1(ID3D12Device* d, const D3D12_HEAP_PROPERTIES* hp, D3D12_HEAP_FLAGS hf,
const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES st,
const D3D12_CLEAR_VALUE* cv, ID3D12ProtectedResourceSession* ps,
REFIID riid, void** out)
{
const HRESULT hr = orig_committed1()(d, hp, hf, desc, st, cv, ps, riid, out);
note_created(hr, desc, out, "committed1");
return hr;
}
struct Snapshot
{
UINT dispatch;
std::vector<std::pair<size_t, ID3D12Resource*>> copies; // created() index, readback buffer
};
inline std::vector<Snapshot>& snapshots()
{
static std::vector<Snapshot> s;
return s;
}
inline const std::vector<UINT>& snapshot_points()
{
static const std::vector<UINT> points = [] {
std::vector<UINT> p;
const std::string list = env_string("D4R_FSR_RR_DP_SNAPSHOT");
size_t at = 0;
while (at < list.size())
{
const size_t comma = list.find(',', at);
p.push_back(static_cast<UINT>(std::stoul(list.substr(at, comma - at))));
at = comma == std::string::npos ? list.size() : comma + 1;
}
return p;
}();
return points;
}
thread_local uint32_t t_frame = 0;
thread_local UINT t_dispatchIndex = 0;
thread_local ID3D12Device* t_device = nullptr;
inline bool enabled()
{
static const bool on = env_uint("D4R_FSR_RR_DISPATCH_PROFILE", 0) != 0;
return on;
}
// Before the denoiser's context is created: tag the PSOs it builds.
inline void hook_device(ID3D12Device* device)
{
State& s = state();
if (!enabled() || s.deviceHooked)
return;
void** vtable = *reinterpret_cast<void***>(device);
ID3D12Device2* device2 = nullptr;
bool ok = patch(&vtable[11], reinterpret_cast<void*>(&hook_create_compute), reinterpret_cast<void**>(&s.createCompute));
if (SUCCEEDED(device->QueryInterface(__uuidof(ID3D12Device2), reinterpret_cast<void**>(&device2))))
{
void** vtable2 = *reinterpret_cast<void***>(device2);
ok = ok && patch(&vtable2[47], reinterpret_cast<void*>(&hook_create_stream), reinterpret_cast<void**>(&s.createStream));
device2->Release();
}
if (!snapshot_points().empty())
{
ID3D12Device4* device4 = nullptr;
ok = ok && patch(&vtable[27], reinterpret_cast<void*>(&hook_committed), reinterpret_cast<void**>(&orig_committed())) &&
patch(&vtable[29], reinterpret_cast<void*>(&hook_placed), reinterpret_cast<void**>(&orig_placed()));
if (SUCCEEDED(device->QueryInterface(__uuidof(ID3D12Device4), reinterpret_cast<void**>(&device4))))
{
void** vtable4 = *reinterpret_cast<void***>(device4);
ok = ok && patch(&vtable4[53], reinterpret_cast<void*>(&hook_committed1),
reinterpret_cast<void**>(&orig_committed1()));
device4->Release();
}
}
s.deviceHooked = ok;
logf("D4R_FSR_RR_DISPATCH_PROFILE: PSO creation hooks %s", ok ? "installed" : "FAILED");
}
inline bool ensure(ID3D12Device* device, ID3D12GraphicsCommandList* list)
{
State& s = state();
if (s.failed)
return false;
if (!s.listHooked)
{
void** vtable = *reinterpret_cast<void***>(list);
const bool ok =
patch(&vtable[14], reinterpret_cast<void*>(&hook_dispatch), reinterpret_cast<void**>(&s.dispatch)) &&
patch(&vtable[25], reinterpret_cast<void*>(&hook_set_pipeline_state),
reinterpret_cast<void**>(&s.setPipelineState)) &&
patch(&vtable[59], reinterpret_cast<void*>(&hook_execute_indirect),
reinterpret_cast<void**>(&s.executeIndirect)) &&
patch(&vtable[26], reinterpret_cast<void*>(&hook_resource_barrier),
reinterpret_cast<void**>(&s.resourceBarrier)) &&
patch(&vtable[37], reinterpret_cast<void*>(&hook_set_root_cbv), reinterpret_cast<void**>(&s.setRootCbv));
if (!ok)
{
s.failed = true;
logf("D4R_FSR_RR_DISPATCH_PROFILE: command-list hooks FAILED");
return false;
}
s.listHooked = true;
}
if (s.heap == nullptr)
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ID3D12CommandQueue* queue = nullptr;
UINT64 frequency = 0;
if (SUCCEEDED(device->CreateCommandQueue(&queueDesc, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue))))
{
queue->GetTimestampFrequency(&frequency);
queue->Release();
}
D3D12_QUERY_HEAP_DESC heapDesc = {};
heapDesc.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP;
heapDesc.Count = kSlots * kStampsPerSlot;
D3D12_HEAP_PROPERTIES heap = {};
heap.Type = D3D12_HEAP_TYPE_READBACK;
D3D12_RESOURCE_DESC desc = {};
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
desc.Width = 8ull * kSlots * kStampsPerSlot;
desc.Height = 1;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.SampleDesc.Count = 1;
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
void* mapped = nullptr;
if (frequency == 0 ||
FAILED(device->CreateQueryHeap(&heapDesc, __uuidof(ID3D12QueryHeap), reinterpret_cast<void**>(&s.heap))) ||
FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, __uuidof(ID3D12Resource),
reinterpret_cast<void**>(&s.readback))) ||
FAILED(s.readback->Map(0, nullptr, &mapped)))
{
s.failed = true;
logf("D4R_FSR_RR_DISPATCH_PROFILE: query heap FAILED");
return false;
}
s.values = static_cast<volatile uint64_t*>(mapped);
for (UINT i = 0; i < kSlots * kStampsPerSlot; ++i)
s.values[i] = 0;
s.ticksPerMs = static_cast<double>(frequency) / 1000.0;
}
return true;
}
inline void log_stats()
{
State& s = state();
double total = 0;
for (const auto& [index, stat] : s.stats)
total += stat.sum / std::max(1u, stat.count);
logf("D4R_FSR_RR_DISPATCH_PROFILE: %u frames, %zu dispatches/frame, %.3f ms GPU/frame (mean)", s.frames,
s.stats.size(), total);
for (const auto& [index, stat] : s.stats)
logf("D4R_FSR_RR_DISPATCH %4u %016llx %s %5u %5u %5u %.4f ms (n=%u) barriers %u uav %u", index,
static_cast<unsigned long long>(stat.hash), stat.indirect ? "ind" : "dir", stat.x, stat.y, stat.z,
stat.sum / std::max(1u, stat.count), stat.count, stat.barriers, stat.uavBarriers);
std::map<uint64_t, std::pair<double, uint32_t>> byHash;
for (const auto& [index, stat] : s.stats)
{
auto& entry = byHash[stat.hash];
entry.first += stat.sum / std::max(1u, stat.count);
++entry.second;
}
std::vector<std::pair<double, uint64_t>> ranked;
for (const auto& [hash, entry] : byHash)
ranked.push_back({entry.first, hash});
std::sort(ranked.rbegin(), ranked.rend());
for (const auto& [ms, hash] : ranked)
logf("D4R_FSR_RR_SHADER %016llx %.4f ms/frame over %u dispatches (%.1f%%)", static_cast<unsigned long long>(hash),
ms, byHash[hash].second, total > 0 ? 100.0 * ms / total : 0.0);
}
// Around the denoiser's ffxDispatch. frame selects a slot; the slot recorded kSlots frames ago has retired.
inline void begin(ID3D12Device* device, ID3D12GraphicsCommandList* list, uint32_t frame)
{
if (!enabled() || !ensure(device, list))
return;
State& s = state();
const UINT slot = frame % kSlots;
volatile uint64_t* values = s.values + slot * kStampsPerSlot;
const UINT used = s.used[slot];
if (used > 1 && values[0] != 0 && values[used - 1] >= values[0])
{
// Frames 1-5 carry the cold start; the stats keep warm frames only.
if (frame > 5 + kSlots)
{
for (UINT i = 0; i + 1 < used; ++i)
{
Stat& stat = s.stats[i];
const Record& r = s.records[slot][i];
stat.hash = r.hash, stat.x = r.x, stat.y = r.y, stat.z = r.z, stat.indirect = r.indirect;
stat.barriers = r.barriers, stat.uavBarriers = r.uavBarriers;
stat.sum += static_cast<double>(values[i + 1] - values[i]) / s.ticksPerMs;
++stat.count;
}
if (++s.frames % profile_log_every() == 0)
log_stats();
}
}
for (UINT i = 0; i < kStampsPerSlot; ++i)
values[i] = 0;
s.records[slot].clear();
s.used[slot] = 0;
t_slot = slot;
t_pso = 0;
t_list = list;
t_frame = frame;
t_dispatchIndex = 0;
t_device = device;
s.barriersSinceDispatch = s.uavBarriersSinceDispatch = 0;
stamp(list, 0, 0, 0, 0, false); // the start
}
inline void end(ID3D12GraphicsCommandList* list)
{
State& s = state();
if (t_list != list)
return;
t_list = nullptr;
stamp(list, 0, 0, 0, 0, false); // after the last dispatch
list->ResolveQueryData(s.heap, D3D12_QUERY_TYPE_TIMESTAMP, t_slot * kStampsPerSlot, s.used[t_slot], s.readback,
8ull * t_slot * kStampsPerSlot);
}
// D4R_FSR_RR_DUMP_DIR=<Windows dir>: also copy the denoiser's two outputs of every frame (RGBA16F, rows packed)
// to <dir>/denoised_<frame>_<diffuse|specular>.raw, for checking replacement shaders against the original.
// The copies are written once the frame has retired (4 frames later, or at teardown).
struct Dump
{
uint32_t frame = 0;
ID3D12Resource* buffer = nullptr;
UINT count = 0;
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint[8] = {};
UINT rows[8] = {};
UINT64 rowBytes[8] = {};
const char* names[8] = {};
bool pending = false;
};
inline std::vector<Dump>& dumps(int set)
{
static std::vector<Dump> d[2] = {std::vector<Dump>(8), std::vector<Dump>(8)};
return d[set];
}
inline void dump_write(Dump& d)
{
if (!d.pending)
return;
d.pending = false;
static const std::string dir = env_string("D4R_FSR_RR_DUMP_DIR");
void* mapped = nullptr;
if (FAILED(d.buffer->Map(0, nullptr, &mapped)))
return;
for (UINT i = 0; i < d.count; ++i)
{
char name[512];
std::snprintf(name, sizeof(name), "%s\\%s_%03u.raw", dir.c_str(), d.names[i], d.frame);
FILE* f = std::fopen(name, "wb");
if (f == nullptr)
continue;
const auto* base = static_cast<const uint8_t*>(mapped) + d.footprint[i].Offset;
for (UINT row = 0; row < d.rows[i]; ++row)
std::fwrite(base + static_cast<size_t>(row) * d.footprint[i].Footprint.RowPitch, 1, d.rowBytes[i], f);
std::fclose(f);
}
d.buffer->Unmap(0, nullptr);
}
// set 0/1: independent rings (e.g. the denoiser's inputs and its outputs). Textures are in `state` and return to it.
inline void dump_record(int set, ID3D12Device* device, ID3D12GraphicsCommandList* list, uint32_t frame, UINT count,
ID3D12Resource* const* textures, const char* const* names, const D3D12_RESOURCE_STATES state)
{
static const bool on = !env_string("D4R_FSR_RR_DUMP_DIR").empty();
if (!on || count > 8)
return;
Dump& d = dumps(set)[frame % dumps(set).size()];
dump_write(d); // the frame that used this slot has retired
if (d.buffer == nullptr)
{
UINT64 offset = 0;
for (UINT i = 0; i < count; ++i)
{
D3D12_RESOURCE_DESC desc = textures[i]->GetDesc();
UINT64 total = 0;
device->GetCopyableFootprints(&desc, 0, 1, offset, &d.footprint[i], &d.rows[i], &d.rowBytes[i], &total);
offset = (offset + total + 65535) & ~65535ull;
}
D3D12_HEAP_PROPERTIES heap = {};
heap.Type = D3D12_HEAP_TYPE_READBACK;
D3D12_RESOURCE_DESC buffer = {};
buffer.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
buffer.Width = offset;
buffer.Height = 1;
buffer.DepthOrArraySize = 1;
buffer.MipLevels = 1;
buffer.SampleDesc.Count = 1;
buffer.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &buffer, D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&d.buffer))))
{
d.buffer = nullptr;
return;
}
}
d.count = count;
D3D12_RESOURCE_BARRIER barriers[8] = {};
for (UINT i = 0; i < count; ++i)
{
d.names[i] = names[i];
barriers[i].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barriers[i].Transition.pResource = textures[i];
barriers[i].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barriers[i].Transition.StateBefore = state;
barriers[i].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
}
list->ResourceBarrier(count, barriers);
for (UINT i = 0; i < count; ++i)
{
D3D12_TEXTURE_COPY_LOCATION dst = {}, src = {};
dst.pResource = d.buffer;
dst.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
dst.PlacedFootprint = d.footprint[i];
src.pResource = textures[i];
src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
}
for (UINT i = 0; i < count; ++i)
std::swap(barriers[i].Transition.StateBefore, barriers[i].Transition.StateAfter);
list->ResourceBarrier(count, barriers);
d.frame = frame;
d.pending = true;
}
inline void snapshot_after_dispatch(ID3D12GraphicsCommandList* list)
{
const UINT index = ++t_dispatchIndex; // 1-based: matches the D4R_FSR_RR_DISPATCH table (row 0 = start stamp)
static const uint32_t frame = env_uint("D4R_FSR_RR_DP_SNAPSHOT_FRAME", 1);
const auto& points = snapshot_points();
if (t_frame != frame || std::find(points.begin(), points.end(), index) == points.end())
return;
D3D12_RESOURCE_BARRIER uav = {};
uav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
state().resourceBarrier(list, 1, &uav);
Snapshot snap{index, {}};
// the dispatch's root CBV, read through the CPU mapping of whichever recorded buffer holds it
for (size_t i = 0; i < created().size() && t_cbv != 0; ++i)
{
const Created& c = created()[i];
if (c.desc.Dimension != D3D12_RESOURCE_DIMENSION_BUFFER)
continue;
const D3D12_GPU_VIRTUAL_ADDRESS va = c.resource->GetGPUVirtualAddress();
if (t_cbv < va || t_cbv + 1376 > va + c.desc.Width)
continue;
void* mapped = nullptr;
D3D12_RANGE range = {static_cast<SIZE_T>(t_cbv - va), static_cast<SIZE_T>(t_cbv - va + 1376)};
if (SUCCEEDED(c.resource->Map(0, &range, &mapped)))
{
static const std::string dir = env_string("D4R_FSR_RR_DUMP_DIR");
char name[512];
std::snprintf(name, sizeof(name), "%s\\snap_%03u_cbv.raw", dir.c_str(), index);
if (FILE* f = std::fopen(name, "wb"))
{
std::fwrite(static_cast<const uint8_t*>(mapped) + (t_cbv - va), 1, 1376, f);
std::fclose(f);
}
D3D12_RANGE none = {0, 0};
c.resource->Unmap(0, &none);
logf("D4R_FSR_RR_DP_SNAPSHOT: dispatch %u CBV in resource %zu at +%llu", index, i,
static_cast<unsigned long long>(t_cbv - va));
}
break;
}
// CPU-visible buffers (upload copies, e.g. the weights): read through their mapping
for (size_t i = 0; i < created().size(); ++i)
{
const Created& c = created()[i];
D3D12_HEAP_PROPERTIES props = {};
D3D12_HEAP_FLAGS flags = {};
if (c.desc.Dimension != D3D12_RESOURCE_DIMENSION_BUFFER || FAILED(c.resource->GetHeapProperties(&props, &flags)) ||
props.Type != D3D12_HEAP_TYPE_UPLOAD)
continue;
void* mapped = nullptr;
if (SUCCEEDED(c.resource->Map(0, nullptr, &mapped)))
{
static const std::string dir = env_string("D4R_FSR_RR_DUMP_DIR");
char name[512];
std::snprintf(name, sizeof(name), "%s\\snap_%03u_upload%02zu.raw", dir.c_str(), index, i);
if (FILE* f = std::fopen(name, "wb"))
{
std::fwrite(mapped, 1, c.desc.Width, f);
std::fclose(f);
}
D3D12_RANGE none = {0, 0};
c.resource->Unmap(0, &none);
}
}
if (t_cbv != 0)
logf("D4R_FSR_RR_DP_SNAPSHOT: dispatch %u root CBV va 0x%llx", index, static_cast<unsigned long long>(t_cbv));
for (size_t i = 0; i < created().size(); ++i)
{
const Created& c = created()[i];
if (c.desc.Dimension != D3D12_RESOURCE_DIMENSION_BUFFER ||
(c.desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS) == 0)
continue;
D3D12_HEAP_PROPERTIES heap = {};
heap.Type = D3D12_HEAP_TYPE_READBACK;
D3D12_RESOURCE_DESC buffer = c.desc;
buffer.Flags = D3D12_RESOURCE_FLAG_NONE;
ID3D12Resource* readback = nullptr;
if (FAILED(t_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &buffer, D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&readback))))
continue;
D3D12_RESOURCE_BARRIER t = {};
t.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
t.Transition.pResource = c.resource;
t.Transition.StateBefore = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
t.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
state().resourceBarrier(list, 1, &t);
list->CopyBufferRegion(readback, 0, c.resource, 0, c.desc.Width);
std::swap(t.Transition.StateBefore, t.Transition.StateAfter);
state().resourceBarrier(list, 1, &t);
snap.copies.push_back({i, readback});
}
snapshots().push_back(std::move(snap));
}
inline void snapshot_flush()
{
static const std::string dir = env_string("D4R_FSR_RR_DUMP_DIR");
for (Snapshot& snap : snapshots())
{
for (auto& [index, readback] : snap.copies)
{
void* mapped = nullptr;
if (SUCCEEDED(readback->Map(0, nullptr, &mapped)))
{
char name[512];
std::snprintf(name, sizeof(name), "%s\\snap_%03u_res%02zu.raw", dir.c_str(), snap.dispatch, index);
if (FILE* f = std::fopen(name, "wb"))
{
std::fwrite(mapped, 1, created()[index].desc.Width, f);
std::fclose(f);
}
readback->Unmap(0, nullptr);
}
readback->Release();
}
logf("D4R_FSR_RR_DP_SNAPSHOT: dispatch %u written (%zu buffers)", snap.dispatch, snap.copies.size());
}
snapshots().clear();
}
inline void dump_flush()
{
snapshot_flush();
for (int set = 0; set < 2; ++set)
for (Dump& d : dumps(set))
dump_write(d);
}
} // namespace fsr_rr_dp
+172
View File
@@ -0,0 +1,172 @@
// AMD's FSR Ray Regeneration runtime in a process that reports an NVIDIA GPU to the game.
//
// d4r's DLSS games run with WINE_HIDE_AMD_GPU (PROTON_FORCE_NVAPI): Vulkan, and so DXGI and
// vkd3d-proton, report vendor 0x10de, which is what lets the game offer DLSS at all. vkd3d-proton
// loads Proton's amdxc64.dll - the shim standing in for AMD's D3D12 driver-extension DLL, which
// forwards to vkd3d-proton's own extensions - only when Vulkan reports vendor 0x1002
// (d3d12_device_init_vendor_hacks), so under the spoof it never does. AMD's runtimes look that module
// up by name, and without it the unmodified denoiser 1.2.0 DLL enumerates no provider for the same
// RX 7700 XT it serves without the spoof (tools/fsr_rr_probe.cpp, 2026-10-13).
//
// For a GPU that sysfs says is AMD's, the module is loaded here exactly as vkd3d-proton itself would
// have. The game's view of the GPU is not changed, and nothing of AMD's is patched. The real PCI ids
// are read from sysfs (Z:\sys\bus\pci\devices\...) at the address Vulkan reports for vkd3d-proton's
// physical device, which the vendor-id spoof does not touch: nothing is assumed without that evidence.
#pragma once
#include <windows.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <vulkan/vulkan_core.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include "d4r_vkd3d_interop.h"
struct D4rPciIdentity
{
uint32_t vendor = 0, device = 0;
bool known = false;
};
// The identity the D3D12 device's own DXGI adapter reports (spoofed or not).
inline D4rPciIdentity d4r_dxgi_identity(ID3D12Device* device)
{
D4rPciIdentity identity;
LUID luid;
device->GetAdapterLuid(&luid);
IDXGIFactory4* factory = nullptr;
if (FAILED(CreateDXGIFactory2(0, __uuidof(IDXGIFactory4), reinterpret_cast<void**>(&factory))))
return identity;
IDXGIAdapter1* adapter = nullptr;
if (SUCCEEDED(factory->EnumAdapterByLuid(luid, __uuidof(IDXGIAdapter1), reinterpret_cast<void**>(&adapter))))
{
DXGI_ADAPTER_DESC1 desc = {};
if (SUCCEEDED(adapter->GetDesc1(&desc)))
identity = {desc.VendorId, desc.DeviceId, true};
adapter->Release();
}
factory->Release();
return identity;
}
inline bool d4r_read_hex_file(const char* path, uint32_t& value)
{
FILE* file = std::fopen(path, "rb");
if (file == nullptr)
return false;
char text[32] = {};
const size_t length = std::fread(text, 1, sizeof(text) - 1, file);
std::fclose(file);
if (length == 0)
return false;
char* end = nullptr;
const unsigned long parsed = std::strtoul(text, &end, 16);
if (end == text)
return false;
value = static_cast<uint32_t>(parsed);
return true;
}
// The real PCI identity of the GPU behind a vkd3d-proton device: Vulkan's PCI bus address, then that
// device's sysfs vendor/device files.
inline D4rPciIdentity d4r_true_identity(ID3D12Device* device, std::string& detail)
{
D4rPciIdentity identity;
ID3D12DXVKInteropDevice1* interop = nullptr;
if (FAILED(device->QueryInterface(__uuidof(ID3D12DXVKInteropDevice1), reinterpret_cast<void**>(&interop))))
{
detail = "not a vkd3d-proton device";
return identity;
}
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice physical = VK_NULL_HANDLE;
VkDevice vkDevice = VK_NULL_HANDLE;
const HRESULT handles = interop->GetVulkanHandles(&instance, &physical, &vkDevice);
interop->Release();
HMODULE vulkan = GetModuleHandleA("vulkan-1.dll");
if (vulkan == nullptr)
vulkan = LoadLibraryA("vulkan-1.dll");
auto instanceProc = vulkan != nullptr ? reinterpret_cast<PFN_vkGetInstanceProcAddr>(
reinterpret_cast<void*>(GetProcAddress(vulkan, "vkGetInstanceProcAddr")))
: nullptr;
auto properties2 = instanceProc != nullptr && SUCCEEDED(handles)
? reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
instanceProc(instance, "vkGetPhysicalDeviceProperties2"))
: nullptr;
if (properties2 == nullptr)
{
detail = "no Vulkan physical-device query";
return identity;
}
VkPhysicalDevicePCIBusInfoPropertiesEXT bus = {};
bus.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PCI_BUS_INFO_PROPERTIES_EXT;
VkPhysicalDeviceDriverProperties driver = {};
driver.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES;
driver.pNext = &bus;
VkPhysicalDeviceProperties2 props = {};
props.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
props.pNext = &driver;
properties2(physical, &props);
if (bus.pciBus == 0 && bus.pciDevice == 0 && bus.pciDomain == 0 && bus.pciFunction == 0)
{
detail = "Vulkan reports no PCI bus address";
return identity;
}
char base[96];
std::snprintf(base, sizeof(base), "Z:\\sys\\bus\\pci\\devices\\%04x:%02x:%02x.%x\\", bus.pciDomain, bus.pciBus,
bus.pciDevice, bus.pciFunction);
const std::string vendorPath = std::string(base) + "vendor", devicePath = std::string(base) + "device";
if (!d4r_read_hex_file(vendorPath.c_str(), identity.vendor) || !d4r_read_hex_file(devicePath.c_str(), identity.device))
{
detail = "cannot read " + vendorPath;
return identity;
}
identity.known = true;
char text[256];
std::snprintf(text, sizeof(text), "PCI %04x:%02x:%02x.%x is %04x:%04x (Vulkan reports %04x:%04x, driver %s)",
bus.pciDomain, bus.pciBus, bus.pciDevice, bus.pciFunction, identity.vendor, identity.device,
props.properties.vendorID, props.properties.deviceID, driver.driverName);
detail = text;
return identity;
}
// Makes Proton's amdxc64.dll available to AMD's runtime when the GPU really is AMD's. Returns false
// only when it should be there and could not be loaded; `detail` says what was found either way.
inline bool d4r_fsr_rr_driver_extension(ID3D12Device* device, std::string& detail)
{
const D4rPciIdentity reported = d4r_dxgi_identity(device);
std::string pci;
const D4rPciIdentity real = d4r_true_identity(device, pci);
char prefix[512];
std::snprintf(prefix, sizeof(prefix), "DXGI reports %04x:%04x; %s", reported.vendor, reported.device,
real.known ? pci.c_str() : ("real PCI identity unknown (" + pci + ")").c_str());
detail = prefix;
if (!real.known || real.vendor != 0x1002)
{
detail += "; not an AMD GPU by sysfs: amdxc64.dll left alone";
return true;
}
if (GetModuleHandleA("winevulkan.dll") == nullptr)
{
// A native Windows driver's amdxc64.dll is AMD's own and is not loaded from here.
detail += "; not running on winevulkan: amdxc64.dll left alone";
return true;
}
HMODULE extension = GetModuleHandleA("amdxc64.dll");
const bool already = extension != nullptr;
if (extension == nullptr)
extension = LoadLibraryA("amdxc64.dll");
if (extension == nullptr)
{
detail += "; amdxc64.dll (Proton's AMD driver-extension shim) is not available";
return false;
}
char path[MAX_PATH] = {};
GetModuleFileNameA(extension, path, MAX_PATH);
detail += std::string(already ? "; amdxc64.dll already loaded (" : "; loaded amdxc64.dll (") + path + ")";
return true;
}
+239
View File
@@ -0,0 +1,239 @@
// The FSR Ray Regeneration backend's own two compute passes (kernels/support/fsr_rr_prepare.comp and
// fsr_rr_compose.comp), recorded into the game's D3D12 command list through vkd3d-proton's Vulkan
// interop. Descriptors are pushed (VK_KHR_push_descriptor, which vkd3d-proton requires), so nothing
// here outlives the command buffer that recorded it except the pipelines.
#pragma once
#include <vulkan/vulkan_core.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "d4r_vulkan_staging_spv.h"
struct D4rFsrRrPrepareConstants
{
float clipToView[16];
float viewToPreviousView[16];
float renderSize[4];
float motion[4];
uint32_t colorDepthBase[4];
uint32_t motionBaseFlags[4];
float depthParams[4];
float detail[4];
};
static_assert(sizeof(D4rFsrRrPrepareConstants) == 224, "fsr_rr_prepare.comp push constants");
enum D4rFsrRrPrepareFlags : uint32_t
{
kFsrRrHardwareDepth = 1u,
kFsrRrRoughnessPacked = 2u,
kFsrRrSpecularHit = 4u,
kFsrRrSpecularHitInW = 8u,
kFsrRrDiffuseHit = 16u,
kFsrRrDiffuseHitInW = 32u,
kFsrRrNormalsUnorm = 64u,
kFsrRrBeforeParticles = 128u,
kFsrRrInvalidNormalPassthrough = 256u,
};
// One image a pass reads or writes: the view and the layout the image is in while the pass runs.
struct D4rFsrRrImage
{
VkImageView view = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_GENERAL;
};
struct D4rFsrRrPasses
{
static constexpr uint32_t kPrepareInputs = 10, kPrepareOutputs = 8, kComposeImages = 8;
VkDevice device = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
VkDescriptorSetLayout prepareSet = VK_NULL_HANDLE, composeSet = VK_NULL_HANDLE;
VkPipelineLayout prepareLayout = VK_NULL_HANDLE, composeLayout = VK_NULL_HANDLE;
VkPipeline preparePipeline = VK_NULL_HANDLE, composePipeline = VK_NULL_HANDLE;
PFN_vkCreateSampler createSampler = nullptr;
PFN_vkDestroySampler destroySampler = nullptr;
PFN_vkCreateDescriptorSetLayout createSetLayout = nullptr;
PFN_vkDestroyDescriptorSetLayout destroySetLayout = nullptr;
PFN_vkCreatePipelineLayout createLayout = nullptr;
PFN_vkDestroyPipelineLayout destroyLayout = nullptr;
PFN_vkCreateShaderModule createShader = nullptr;
PFN_vkDestroyShaderModule destroyShader = nullptr;
PFN_vkCreateComputePipelines createPipelines = nullptr;
PFN_vkDestroyPipeline destroyPipeline = nullptr;
PFN_vkCmdBindPipeline bindPipeline = nullptr;
PFN_vkCmdPushConstants push = nullptr;
PFN_vkCmdPushDescriptorSetKHR pushDescriptors = nullptr;
PFN_vkCmdDispatch dispatch = nullptr;
PFN_vkCmdPipelineBarrier barrier = nullptr;
PFN_vkCreateImageView createView = nullptr;
PFN_vkDestroyImageView destroyView = nullptr;
bool pipeline(const uint32_t* code, size_t bytes, VkPipelineLayout layout, VkPipeline& result)
{
VkShaderModuleCreateInfo shader = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
shader.codeSize = bytes;
shader.pCode = code;
VkShaderModule module = VK_NULL_HANDLE;
if (createShader(device, &shader, nullptr, &module) != VK_SUCCESS)
return false;
VkComputePipelineCreateInfo info = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
info.stage.module = module;
info.stage.pName = "main";
info.layout = layout;
const VkResult status = createPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &result);
destroyShader(device, module, nullptr);
return status == VK_SUCCESS;
}
bool set_layout(const VkDescriptorType* types, uint32_t count, uint32_t pushBytes, VkDescriptorSetLayout& set,
VkPipelineLayout& layout)
{
VkDescriptorSetLayoutBinding bindings[kPrepareInputs + kPrepareOutputs] = {};
for (uint32_t i = 0; i < count; ++i)
{
bindings[i].binding = i;
bindings[i].descriptorType = types[i];
bindings[i].descriptorCount = 1;
bindings[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
bindings[i].pImmutableSamplers = types[i] == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER ? &sampler : nullptr;
}
VkDescriptorSetLayoutCreateInfo info = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
info.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR;
info.bindingCount = count;
info.pBindings = bindings;
if (createSetLayout(device, &info, nullptr, &set) != VK_SUCCESS)
return false;
VkPushConstantRange range = {VK_SHADER_STAGE_COMPUTE_BIT, 0, pushBytes};
VkPipelineLayoutCreateInfo pipelineLayout = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipelineLayout.setLayoutCount = 1;
pipelineLayout.pSetLayouts = &set;
pipelineLayout.pushConstantRangeCount = 1;
pipelineLayout.pPushConstantRanges = &range;
return createLayout(device, &pipelineLayout, nullptr, &layout) == VK_SUCCESS;
}
// `maxPushConstants`: VkPhysicalDeviceLimits::maxPushConstantsSize (the prepare pass needs 224).
bool init(VkDevice d, PFN_vkGetDeviceProcAddr proc, uint32_t maxPushConstants)
{
device = d;
if (maxPushConstants < sizeof(D4rFsrRrPrepareConstants))
return false;
#define D4R_LOAD(member, name) \
member = reinterpret_cast<decltype(member)>(proc(d, "vk" #name)); \
if (!member) \
return false
D4R_LOAD(createSampler, CreateSampler);
D4R_LOAD(destroySampler, DestroySampler);
D4R_LOAD(createSetLayout, CreateDescriptorSetLayout);
D4R_LOAD(destroySetLayout, DestroyDescriptorSetLayout);
D4R_LOAD(createLayout, CreatePipelineLayout);
D4R_LOAD(destroyLayout, DestroyPipelineLayout);
D4R_LOAD(createShader, CreateShaderModule);
D4R_LOAD(destroyShader, DestroyShaderModule);
D4R_LOAD(createPipelines, CreateComputePipelines);
D4R_LOAD(destroyPipeline, DestroyPipeline);
D4R_LOAD(bindPipeline, CmdBindPipeline);
D4R_LOAD(push, CmdPushConstants);
D4R_LOAD(pushDescriptors, CmdPushDescriptorSetKHR);
D4R_LOAD(dispatch, CmdDispatch);
D4R_LOAD(barrier, CmdPipelineBarrier);
D4R_LOAD(createView, CreateImageView);
D4R_LOAD(destroyView, DestroyImageView);
#undef D4R_LOAD
VkSamplerCreateInfo nearest = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
nearest.magFilter = nearest.minFilter = VK_FILTER_NEAREST;
nearest.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
nearest.addressModeU = nearest.addressModeV = nearest.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (createSampler(d, &nearest, nullptr, &sampler) != VK_SUCCESS)
return false;
VkDescriptorType prepare[kPrepareInputs + kPrepareOutputs];
for (uint32_t i = 0; i < kPrepareInputs + kPrepareOutputs; ++i)
prepare[i] = i < kPrepareInputs ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
VkDescriptorType compose[kComposeImages];
for (VkDescriptorType& type : compose)
type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
return set_layout(prepare, kPrepareInputs + kPrepareOutputs, sizeof(D4rFsrRrPrepareConstants), prepareSet,
prepareLayout) &&
set_layout(compose, kComposeImages, 16, composeSet, composeLayout) &&
pipeline(d4r_vk_fsr_rr_prepare, sizeof(d4r_vk_fsr_rr_prepare), prepareLayout, preparePipeline) &&
pipeline(d4r_vk_fsr_rr_compose, sizeof(d4r_vk_fsr_rr_compose), composeLayout, composePipeline);
}
void destroy()
{
if (device == VK_NULL_HANDLE || destroyPipeline == nullptr)
return;
destroyPipeline(device, preparePipeline, nullptr);
destroyPipeline(device, composePipeline, nullptr);
destroyLayout(device, prepareLayout, nullptr);
destroyLayout(device, composeLayout, nullptr);
destroySetLayout(device, prepareSet, nullptr);
destroySetLayout(device, composeSet, nullptr);
destroySampler(device, sampler, nullptr);
*this = D4rFsrRrPasses{};
}
// Everything the D3D12 side wrote before is visible to the pass, and the pass's writes to what
// the D3D12 side records after.
void full_barrier(VkCommandBuffer cmd)
{
VkMemoryBarrier memory = {VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
barrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr,
0, nullptr);
}
void record(VkCommandBuffer cmd, VkPipeline pipeline, VkPipelineLayout layout, const D4rFsrRrImage* images,
uint32_t count, uint32_t sampledCount, const void* constants, uint32_t constantBytes, uint32_t width,
uint32_t height)
{
VkDescriptorImageInfo infos[kPrepareInputs + kPrepareOutputs] = {};
VkWriteDescriptorSet writes[kPrepareInputs + kPrepareOutputs] = {};
for (uint32_t i = 0; i < count; ++i)
{
infos[i].sampler = i < sampledCount ? sampler : VK_NULL_HANDLE;
infos[i].imageView = images[i].view;
infos[i].imageLayout = images[i].layout;
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[i].dstBinding = i;
writes[i].descriptorCount = 1;
writes[i].descriptorType =
i < sampledCount ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
writes[i].pImageInfo = &infos[i];
}
bindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
pushDescriptors(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, count, writes);
push(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, constantBytes, constants);
dispatch(cmd, (width + 7) / 8, (height + 7) / 8, 1);
}
// images: the 10 sampled inputs, then the 8 storage outputs (fsr_rr_prepare.comp's bindings)
void record_prepare(VkCommandBuffer cmd, const D4rFsrRrImage* images, const D4rFsrRrPrepareConstants& constants,
uint32_t width, uint32_t height)
{
full_barrier(cmd);
record(cmd, preparePipeline, prepareLayout, images, kPrepareInputs + kPrepareOutputs, kPrepareInputs,
&constants, sizeof(constants), width, height);
full_barrier(cmd);
}
// images: denoised diffuse, denoised specular, diffuse albedo, specular albedo, residual, composed, noisy diffuse,
// noisy specular. haloLimit: fsr_rr_compose.comp's limit (0: off)
void record_compose(VkCommandBuffer cmd, const D4rFsrRrImage* images, uint32_t width, uint32_t height,
float haloLimit = 0.0f)
{
uint32_t limitBits;
std::memcpy(&limitBits, &haloLimit, sizeof(limitBits));
const uint32_t constants[4] = {width, height, limitBits, 0};
full_barrier(cmd);
record(cmd, composePipeline, composeLayout, images, kComposeImages, 0, constants, sizeof(constants), width,
height);
full_barrier(cmd);
}
};
+7
View File
@@ -11,6 +11,13 @@ typedef struct D4rMotionDilationParams
uint32_t radius, reversed_depth;
} D4rMotionDilationParams;
/* Exact LDS specialization; allocation padding is outside this active grid. */
static inline int d4r_motion_dilation_lds_eligible(const D4rMotionDilationParams* p)
{
return p->motion_width == p->render_width && p->motion_height == p->render_height &&
p->jitter_x == 0 && p->jitter_y == 0 && p->radius >= 1 && p->radius <= 2;
}
/* Low-resolution velocity and depth share the render raster grid. Allocation
padding is not part of that grid; retain the original NGX flags and MV scale.
Display-resolution velocity still needs the jittered depth-grid mapping. */
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include <stdint.h>
typedef struct D4rNativeMotionParams
{
uint64_t depth,geometry,scene,matrix,output;
uint64_t depth_pitch,geometry_pitch,output_pitch;
uint32_t width,height,render_width,render_height;
} D4rNativeMotionParams;
#ifdef __cplusplus
static_assert(sizeof(D4rNativeMotionParams)==80);
#endif
/* Native AW2 hardware-depth contract: immutable input planes; disjoint output. */
static inline int d4r_native_motion_parameters_valid(const D4rNativeMotionParams* p)
{
uint64_t pointers[5],sizes[5],pitches[3],row;
unsigned int i;
if(!p || !p->width || !p->height || p->width>4096 || p->height>4096 ||
!p->render_width || !p->render_height || p->render_width>p->width || p->render_height>p->height) return 0;
row=(uint64_t)p->width*4;
pitches[0]=p->depth_pitch;pitches[1]=p->geometry_pitch;pitches[2]=p->output_pitch;
for(i=0;i<3;i++)
{
if(pitches[i]<row || pitches[i]%4 || (p->height>1 && pitches[i]>(UINT64_MAX-row)/(p->height-1))) return 0;
}
pointers[0]=p->depth;pointers[1]=p->geometry;pointers[2]=p->scene;pointers[3]=p->matrix;pointers[4]=p->output;
sizes[0]=(p->height-1)*p->depth_pitch+row;sizes[1]=(p->height-1)*p->geometry_pitch+row;
sizes[2]=71*16;sizes[3]=4*16;sizes[4]=(p->height-1)*p->output_pitch+row;
for(i=0;i<5;i++) if(!pointers[i] || pointers[i]%4 || pointers[i]>UINT64_MAX-sizes[i]) return 0;
for(i=0;i<4;i++)
if(pointers[4]<pointers[i]+sizes[i] && pointers[i]<pointers[4]+sizes[4]) return 0;
return 1;
}
// Additive thin-field ABI: depth coverage is jittered, output velocity grid is not.
typedef struct D4rNativeMotionThinParams
{
D4rNativeMotionParams base;
float jitter_x, jitter_y;
} D4rNativeMotionThinParams;
#ifdef __cplusplus
static_assert(sizeof(D4rNativeMotionThinParams)==88);
#endif
// Preserve the game's vectors outside thin native-silhouette footprints.
typedef struct D4rNativeMotionHybridParams
{
D4rNativeMotionThinParams native;
uint64_t game_motion,game_motion_pitch;
} D4rNativeMotionHybridParams;
#ifdef __cplusplus
static_assert(sizeof(D4rNativeMotionHybridParams)==104);
#endif
static inline int d4r_native_motion_hybrid_parameters_valid(const D4rNativeMotionHybridParams* p)
{
if(!p || !d4r_native_motion_parameters_valid(&p->native.base) || !p->game_motion || p->game_motion%4 ||
!(p->native.jitter_x>=-1 && p->native.jitter_x<=1 && p->native.jitter_y>=-1 && p->native.jitter_y<=1)) return 0;
const D4rNativeMotionParams* b=&p->native.base;
const uint64_t row=(uint64_t)b->render_width*4;
if(p->game_motion_pitch<row || p->game_motion_pitch%4 ||
(b->render_height>1 && p->game_motion_pitch>(UINT64_MAX-row)/(b->render_height-1))) return 0;
const uint64_t size=(b->render_height-1)*p->game_motion_pitch+row;
const uint64_t output_size=(b->height-1)*b->output_pitch+(uint64_t)b->width*4;
if(p->game_motion>UINT64_MAX-size || (p->game_motion<b->output+output_size && b->output<p->game_motion+size)) return 0;
return 1;
}
// Packed silhouette mask and per-pixel wire axes, prepared once for this frame.
typedef struct D4rNativeMotionPreparedParams
{
D4rNativeMotionThinParams native;
uint64_t thin_mask,wire_axes;
uint32_t wire_flags,reserved;
} D4rNativeMotionPreparedParams;
#ifdef __cplusplus
static_assert(sizeof(D4rNativeMotionPreparedParams)==112);
#endif
static inline int d4r_native_motion_prepared_parameters_valid(const D4rNativeMotionPreparedParams* p)
{
if(!p || !d4r_native_motion_parameters_valid(&p->native.base) || !p->thin_mask || p->thin_mask%4 ||
!p->wire_axes || p->wire_axes%4 || (p->wire_flags & ~3u) || p->reserved ||
!(p->native.jitter_x>=-1 && p->native.jitter_x<=1 && p->native.jitter_y>=-1 && p->native.jitter_y<=1)) return 0;
const D4rNativeMotionParams* b=&p->native.base;
const uint64_t mask_size=(uint64_t)((b->width+31)/32)*b->height*4,axes_size=(uint64_t)b->width*b->height;
if(p->thin_mask>UINT64_MAX-mask_size || p->wire_axes>UINT64_MAX-axes_size ||
(p->thin_mask<p->wire_axes+axes_size && p->wire_axes<p->thin_mask+mask_size)) return 0;
const uint64_t pointers[5]={b->depth,b->geometry,b->scene,b->matrix,b->output};
const uint64_t sizes[5]={(b->height-1)*b->depth_pitch+(uint64_t)b->width*4,
(b->height-1)*b->geometry_pitch+(uint64_t)b->width*4,1136,64,
(b->height-1)*b->output_pitch+(uint64_t)b->width*4};
for(unsigned i=0;i<5;i++) if((p->thin_mask<pointers[i]+sizes[i] && pointers[i]<p->thin_mask+mask_size) ||
(p->wire_axes<pointers[i]+sizes[i] && pointers[i]<p->wire_axes+axes_size)) return 0;
return 1;
}
+1488 -112
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -514,7 +514,7 @@ static void native_vk_run(Feature* feature, NativeVkFrame* frame)
const auto downloaded = ProfileClock::now();
if (!destination.cuda) std::memcpy(destination.mapped, feature->outputHost[0].bytes, feature->outputHost[0].size());
feature->completedFrames++;
if (frame->number <= 3 || frame->number % 120 == 0 || profile_enabled())
if (profile_enabled() ? profile_log_frame(frame->number) : (frame->number <= 3 || frame->number % 120 == 0))
logf("D4R_VULKAN_PROFILE frame=%u stage=%.3f upload=%.3f evaluate=%.3f download=%.3f encode=%.3f ms",
frame->number, profile_ms(stageStart, staged), profile_ms(staged, uploaded),
profile_ms(uploaded, evaluatedAt), profile_ms(evaluatedAt, downloaded),
@@ -539,7 +539,7 @@ static void native_vk_run(Feature* feature, NativeVkFrame* frame)
if (g_nativeVk.setEvent(g_nativeVk.device, frame->outputReady) != VK_SUCCESS)
frame->result = NGX_FAIL_PLATFORM_ERROR;
frame->done = true;
if (frame->number <= 3 || frame->number % 120 == 0 || profile_enabled())
if (profile_enabled() ? profile_log_frame(frame->number) : (frame->number <= 3 || frame->number % 120 == 0))
logf("D4R_VULKAN_WAIT frame=%u input_ready=%.3f signal=%.3f total=%.3f ms",
frame->number, profile_ms(waitStarted, inputsReadyAt),
profile_ms(signalStarted, ProfileClock::now()), profile_ms(waitStarted, ProfileClock::now()));
@@ -857,7 +857,7 @@ D4R_EXPORT NgxResult NVSDK_NGX_VULKAN_EvaluateFeature(VkCommandBuffer cmd, const
// write and all earlier commands which reference the staging objects.
g_nativeVk.cmdSetEvent(cmd, frame->consumed, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
owner.frames.push_back(frame);
if (frame->number <= 3 || frame->number % 120 == 0 || profile_enabled())
if (profile_enabled() ? profile_log_frame(frame->number) : (frame->number <= 3 || frame->number % 120 == 0))
logf("D4R_VULKAN_API frame=%u reclaim=%.3f allocate=%.3f record=%.3f total=%.3f ms",
frame->number, profile_ms(apiStarted, reapedAt), profile_ms(reapedAt, allocatedAt),
profile_ms(allocatedAt, ProfileClock::now()), profile_ms(apiStarted, ProfileClock::now()));
+109
View File
@@ -0,0 +1,109 @@
#pragma once
#include <stdint.h>
#define D4R_WIRE_HISTORY_CONSERVATIVE 1u
#define D4R_WIRE_HISTORY_SKY_ONLY 2u
typedef struct D4rWireHistoryParams
{
uint64_t current, color, native_depth, motion;
uint64_t previous, previous_age, previous_depth;
uint64_t output, output_age, output_depth, input_depth, output_history;
uint64_t color_pitch, depth_pitch, motion_pitch, output_pitch, input_depth_pitch;
uint32_t width, height, render_width, render_height, history_valid;
float jitter_x, jitter_y;
uint32_t reserved;
} D4rWireHistoryParams;
#ifdef __cplusplus
static_assert(sizeof(D4rWireHistoryParams)==168);
#endif
static inline int d4r_wire_history_parameters_valid(const D4rWireHistoryParams* p)
{
if(!p || !p->width || !p->height || p->width>4096 || p->height>4096 ||
p->render_width<12 || p->render_height<12 || p->render_width>p->width || p->render_height>p->height ||
p->history_valid>1 || !(p->jitter_x>=-1 && p->jitter_x<=1 && p->jitter_y>=-1 && p->jitter_y<=1) ||
!p->current || !p->color || !p->native_depth || !p->motion || !p->previous || !p->previous_age ||
!p->previous_depth || !p->output || !p->output_age || !p->output_depth || !p->input_depth || !p->output_history ||
p->color_pitch<(uint64_t)p->render_width*8 || p->input_depth_pitch<(uint64_t)p->render_width*4 || p->depth_pitch<(uint64_t)p->width*4 ||
p->motion_pitch<(uint64_t)p->width*4 || p->output_pitch<(uint64_t)p->width*8 ||
p->current==p->output || p->previous==p->output || p->previous_age==p->output_age ||
p->previous_depth==p->output_depth || p->output_history==p->output ||
p->output_history==p->current || p->output_history==p->previous) return 0;
return 1;
}
// Sparse publication keeps current immutable until the separate apply dispatch.
// Scratch RGB uses the same pitch; patch_mask has one byte per output pixel.
typedef struct D4rWireHistorySparseParams
{
D4rWireHistoryParams base;
uint64_t patch_mask;
} D4rWireHistorySparseParams;
#ifdef __cplusplus
static_assert(sizeof(D4rWireHistorySparseParams)==176);
#endif
static inline int d4r_wire_history_sparse_parameters_valid(const D4rWireHistorySparseParams* p)
{
uint64_t pointers[12],rows[12],pitches[12],heights[12],bytes,end;
unsigned i;
if(!p || !d4r_wire_history_parameters_valid(&p->base) || !p->patch_mask) return 0;
const D4rWireHistoryParams* q=&p->base;
bytes=(uint64_t)q->width*q->height;
if(p->patch_mask>UINT64_MAX-bytes) return 0;
end=p->patch_mask+bytes;
pointers[0]=q->current;pointers[1]=q->color;pointers[2]=q->native_depth;pointers[3]=q->motion;
pointers[4]=q->previous;pointers[5]=q->previous_age;pointers[6]=q->previous_depth;
pointers[7]=q->output;pointers[8]=q->output_age;pointers[9]=q->output_depth;
pointers[10]=q->input_depth;pointers[11]=q->output_history;
for(i=0;i<12;i++) { rows[i]=(uint64_t)q->width*8;pitches[i]=q->output_pitch;heights[i]=q->height; }
rows[1]=(uint64_t)q->render_width*8;pitches[1]=q->color_pitch;heights[1]=q->render_height;
rows[2]=(uint64_t)q->width*4;pitches[2]=q->depth_pitch;
rows[3]=(uint64_t)q->width*4;pitches[3]=q->motion_pitch;
rows[5]=rows[8]=q->width;pitches[5]=pitches[8]=q->width;
rows[6]=rows[9]=(uint64_t)q->width*4;pitches[6]=pitches[9]=(uint64_t)q->width*4;
rows[10]=(uint64_t)q->render_width*4;pitches[10]=q->input_depth_pitch;heights[10]=q->render_height;
for(i=0;i<12;i++)
{
if(heights[i]>1 && pitches[i]>(UINT64_MAX-rows[i])/(heights[i]-1)) return 0;
bytes=(heights[i]-1)*pitches[i]+rows[i];
if(pointers[i]>UINT64_MAX-bytes || (p->patch_mask<pointers[i]+bytes && pointers[i]<end)) return 0;
}
// current becomes the apply destination: every input and history output must
// be disjoint from it, as must the correction scratch allocation.
const uint64_t current_bytes=(q->height-1)*q->output_pitch+(uint64_t)q->width*8;
const uint64_t scratch_bytes=current_bytes;
for(i=1;i<12;i++)
{
bytes=(heights[i]-1)*pitches[i]+rows[i];
if(q->current<pointers[i]+bytes && pointers[i]<q->current+current_bytes) return 0;
if(i!=7 && q->output<pointers[i]+bytes && pointers[i]<q->output+scratch_bytes) return 0;
}
return 1;
}
// Prepared repair preserves rendered output and live history exactly. RGB/depth
// history entries with age 255 are unspecified and must never be read.
typedef struct D4rWireHistoryPreparedParams
{
D4rWireHistorySparseParams sparse;
uint64_t axes;
} D4rWireHistoryPreparedParams;
#ifdef __cplusplus
static_assert(sizeof(D4rWireHistoryPreparedParams)==184);
#endif
static inline int d4r_wire_history_prepared_parameters_valid(const D4rWireHistoryPreparedParams* p)
{
if(!p || !d4r_wire_history_sparse_parameters_valid(&p->sparse) || !p->axes || (p->sparse.base.reserved & ~3u)) return 0;
const D4rWireHistoryParams* b=&p->sparse.base;
const uint64_t size=(uint64_t)b->width*b->height;
if(p->axes>UINT64_MAX-size || (p->axes<p->sparse.patch_mask+size && p->sparse.patch_mask<p->axes+size)) return 0;
const uint64_t pointers[12]={b->current,b->color,b->native_depth,b->motion,b->previous,b->previous_age,
b->previous_depth,b->output,b->output_age,b->output_depth,b->input_depth,b->output_history};
const uint64_t sizes[12]={(b->height-1)*b->output_pitch+(uint64_t)b->width*8,
(b->render_height-1)*b->color_pitch+(uint64_t)b->render_width*8,
(b->height-1)*b->depth_pitch+(uint64_t)b->width*4,(b->height-1)*b->motion_pitch+(uint64_t)b->width*4,
(b->height-1)*b->output_pitch+(uint64_t)b->width*8,size,size*4,
(b->height-1)*b->output_pitch+(uint64_t)b->width*8,size,size*4,
(b->render_height-1)*b->input_depth_pitch+(uint64_t)b->render_width*4,
(b->height-1)*b->output_pitch+(uint64_t)b->width*8};
for(unsigned i=0;i<12;i++) if(p->axes<pointers[i]+sizes[i] && pointers[i]<p->axes+size) return 0;
return 1;
}
+695
View File
@@ -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 "d4r_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(&timestamps)));
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);
}
+515
View File
@@ -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
+733
View File
@@ -0,0 +1,733 @@
// Drives d4r_nvngx.dll the way a DLSS Ray Reconstruction game does, with the synthetic ray-traced
// scene of fsr_rr_scene.h, and measures the result against ground truth: the scene's noise-free image
// rendered directly at the output resolution. It exercises whichever Ray Reconstruction backend the
// shim is configured with ([RayReconstruction] Backend / D4R_RR_BACKEND), or Super Sampling alone on
// the same noisy colour (--feature 1) as the no-denoiser baseline.
//
// Per frame the game-side inputs are: the noisy colour (each signal remodulated by its albedo, summed),
// linear or hardware depth, render-resolution motion vectors in pixels (previous - current), world
// normals with the roughness in w (DLSS.Roughness.Mode 1), linear diffuse and specular albedos, the
// unjittered WorldToView/ViewToClip matrices, and Halton(2,3) jitter. The camera holds still, then
// pans, then cuts (Reset) to another view.
//
// It checks: capability and requirement answers, feature creation, every evaluation's result, output
// written at exactly the requested size, finite values, error against ground truth before and after
// the cut, release and shutdown. Timing: CPU time of each EvaluateFeature call and of submit + wait.
//
// usage: rr_backend_check.exe SHIM_DLL OUT_DIR [--render WxH] [--output WxH] [--frames N] [--feature 13|1]
// [--hw-depth] [--separate-roughness] [--specular-hit]
#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 "fsr_rr_scene.h"
using namespace fsr_scene;
using NgxResult = unsigned int;
constexpr NgxResult NGX_SUCCESS = 1;
struct NgxHandle
{
unsigned int Id;
};
extern "C"
{
void d4r_ngx_set_float(void* parameters, const char* name, float value);
void d4r_ngx_set_uint(void* parameters, const char* name, unsigned int value);
void d4r_ngx_set_int(void* parameters, const char* name, int value);
void d4r_ngx_set_d3d12_resource(void* parameters, const char* name, ID3D12Resource* value);
void d4r_ngx_set_void(void* parameters, const char* name, void* value);
NgxResult d4r_ngx_get_int(void* parameters, const char* name, int* value);
NgxResult d4r_ngx_get_void(void* parameters, const char* name, void** value);
}
using PFN_Init_Ext = NgxResult (*)(unsigned long long, const wchar_t*, ID3D12Device*, unsigned int, const void*);
using PFN_Parameters = NgxResult (*)(void**);
using PFN_CreateFeature = NgxResult (*)(ID3D12GraphicsCommandList*, unsigned int, void*, NgxHandle**);
using PFN_Evaluate = NgxResult (*)(ID3D12GraphicsCommandList*, const NgxHandle*, void*, void*);
using PFN_Release = NgxResult (*)(NgxHandle*);
using PFN_Shutdown = NgxResult (*)();
struct NgxApplicationIdentifier
{
unsigned int type;
union
{
unsigned long long applicationId;
struct
{
const char* id;
int engineType;
const char* engineVersion;
} project;
} value;
};
struct NgxFeatureDiscovery
{
unsigned int sdkVersion;
unsigned int feature;
NgxApplicationIdentifier identifier;
const wchar_t* dataPath;
const void* featureInfo;
};
struct NgxFeatureRequirement
{
unsigned int supported;
unsigned int minArchitecture;
char minOsVersion[255];
};
using PFN_Requirements = NgxResult (*)(void*, const NgxFeatureDiscovery*, NgxFeatureRequirement*);
namespace
{
ID3D12Device* g_device;
ID3D12CommandQueue* g_queue;
ID3D12CommandAllocator* g_allocator;
ID3D12GraphicsCommandList* g_list;
ID3D12Fence* g_fence;
HANDLE g_event;
UINT64 g_fenceValue;
FILE* g_json;
bool g_jsonFirst = true;
std::string format(const char* pattern, ...)
{
char buffer[4096];
va_list args;
va_start(args, pattern);
std::vsnprintf(buffer, sizeof(buffer), pattern, args);
va_end(args);
return buffer;
}
void json_field(const char* key, const std::string& value)
{
if (g_json)
std::fprintf(g_json, "%s\n \"%s\": %s", g_jsonFirst ? "" : ",", key, value.c_str());
g_jsonFirst = false;
}
bool g_failed = false;
void check(const char* name, bool ok, const std::string& detail)
{
std::printf("check=%s result=%s %s\n", name, ok ? "ok" : "FAIL", detail.c_str());
std::fflush(stdout);
std::string escaped;
for (char c : detail)
if (c == '"' || c == '\\')
escaped += '\\', escaped += c;
else if (static_cast<unsigned char>(c) >= 0x20)
escaped += c;
json_field((std::string("check_") + name).c_str(),
format("{\"ok\": %s, \"detail\": \"%s\"}", ok ? "true" : "false", escaped.c_str()));
g_failed |= !ok;
}
// Process VRAM from the DRM fdinfo of this process (Wine maps Z: to /), as the D3D12 harness does.
unsigned long long process_vram_kib()
{
std::vector<unsigned long long> clients;
unsigned long long total = 0;
for (int fd = 0; fd < 4096; ++fd)
{
char path[64];
std::snprintf(path, sizeof(path), "Z:\\proc\\self\\fdinfo\\%d", fd);
FILE* file = std::fopen(path, "r");
if (file == nullptr)
continue;
unsigned long long client = 0, vram = 0;
bool drm = false;
char line[256];
while (std::fgets(line, sizeof(line), file) != nullptr)
if (std::sscanf(line, "drm-client-id: %llu", &client) == 1)
drm = true;
else
std::sscanf(line, "drm-memory-vram: %llu", &vram);
std::fclose(file);
if (drm && std::find(clients.begin(), clients.end(), client) == clients.end())
{
clients.push_back(client);
total += vram;
}
}
return total;
}
ID3D12Resource* create_texture(UINT width, UINT height, DXGI_FORMAT format, D3D12_RESOURCE_FLAGS flags,
D3D12_RESOURCE_STATES state)
{
D3D12_RESOURCE_DESC desc = {};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = width;
desc.Height = height;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.Format = format;
desc.SampleDesc.Count = 1;
desc.Flags = flags;
D3D12_HEAP_PROPERTIES heap = {};
heap.Type = D3D12_HEAP_TYPE_DEFAULT;
ID3D12Resource* resource = nullptr;
if (FAILED(g_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, state, nullptr,
__uuidof(ID3D12Resource), reinterpret_cast<void**>(&resource))))
return nullptr;
return resource;
}
ID3D12Resource* create_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;
ID3D12Resource* buffer = nullptr;
const D3D12_RESOURCE_STATES state =
type == D3D12_HEAP_TYPE_UPLOAD ? D3D12_RESOURCE_STATE_GENERIC_READ : D3D12_RESOURCE_STATE_COPY_DEST;
if (FAILED(g_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, state, nullptr,
__uuidof(ID3D12Resource), reinterpret_cast<void**>(&buffer))) ||
FAILED(buffer->Map(0, nullptr, reinterpret_cast<void**>(mapped))))
return nullptr;
return buffer;
}
void transition(ID3D12Resource* resource, D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after)
{
if (before == after)
return;
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = resource;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.StateBefore = before;
barrier.Transition.StateAfter = after;
g_list->ResourceBarrier(1, &barrier);
}
bool submit_and_wait()
{
if (FAILED(g_list->Close()))
return false;
ID3D12CommandList* lists[] = {g_list};
g_queue->ExecuteCommandLists(1, lists);
g_queue->Signal(g_fence, ++g_fenceValue);
g_fence->SetEventOnCompletion(g_fenceValue, g_event);
const bool done = WaitForSingleObject(g_event, 30000) == WAIT_OBJECT_0;
g_allocator->Reset();
g_list->Reset(g_allocator, nullptr);
return done && SUCCEEDED(g_device->GetDeviceRemovedReason());
}
// Uploads tightly packed rows into a texture (left in `state`).
struct Uploader
{
ID3D12Resource* staging = nullptr;
uint8_t* mapped = nullptr;
UINT64 capacity = 0, offset = 0;
void upload(ID3D12Resource* texture, UINT width, UINT height, DXGI_FORMAT format, const void* data,
size_t texelBytes, D3D12_RESOURCE_STATES state)
{
D3D12_RESOURCE_DESC desc = {};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = width;
desc.Height = height;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.Format = format;
desc.SampleDesc.Count = 1;
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {};
UINT rows = 0;
UINT64 rowBytes = 0, total = 0;
g_device->GetCopyableFootprints(&desc, 0, 1, 0, &footprint, &rows, &rowBytes, &total);
offset = (offset + 511) & ~UINT64(511);
footprint.Offset = offset;
for (UINT row = 0; row < rows; ++row)
std::memcpy(mapped + offset + row * footprint.Footprint.RowPitch,
static_cast<const uint8_t*>(data) + row * texelBytes * width, texelBytes * width);
offset += total;
transition(texture, state, D3D12_RESOURCE_STATE_COPY_DEST);
D3D12_TEXTURE_COPY_LOCATION dst = {};
dst.pResource = texture;
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;
g_list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
transition(texture, D3D12_RESOURCE_STATE_COPY_DEST, state);
}
};
float halton(uint32_t index, uint32_t base)
{
float result = 0, fraction = 1;
while (index > 0)
{
fraction /= base;
result += fraction * (index % base);
index /= base;
}
return result;
}
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 v = std::max(0.0f, rgb[k]);
row[x * 3 + k] = static_cast<uint8_t>(std::lround(saturate(std::pow(v / (1 + v), 1 / 2.2f)) * 255));
}
}
std::fwrite(row.data(), 1, row.size(), file);
}
std::fclose(file);
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 3)
{
std::printf("usage: rr_backend_check.exe SHIM_DLL OUT_DIR [--render WxH] [--output WxH] [--frames N] "
"[--feature 13|1] [--hw-depth] [--separate-roughness] [--specular-hit] [--separate-eval-params] [--water] [--measure-all]\n");
return 2;
}
const std::string outDir = argv[2];
UINT rw = 960, rh = 540, ow = 1920, oh = 1080, frames = 48, featureId = 13;
bool hwDepth = false, separateRoughness = false, specularHit = false, separateEvalParams = false, measureAll = false;
for (int i = 3; i < argc; ++i)
{
const std::string arg = argv[i];
if (arg == "--render" && i + 1 < argc)
std::sscanf(argv[++i], "%ux%u", &rw, &rh);
else if (arg == "--output" && i + 1 < argc)
std::sscanf(argv[++i], "%ux%u", &ow, &oh);
else if (arg == "--frames" && i + 1 < argc)
frames = static_cast<UINT>(std::atoi(argv[++i]));
else if (arg == "--feature" && i + 1 < argc)
featureId = static_cast<UINT>(std::atoi(argv[++i]));
else if (arg == "--hw-depth")
hwDepth = true;
else if (arg == "--separate-roughness")
separateRoughness = true;
else if (arg == "--specular-hit")
specularHit = true;
else if (arg == "--separate-eval-params")
separateEvalParams = true;
else if (arg == "--water")
fsr_scene::g_water = true;
else if (arg == "--measure-all")
measureAll = true;
}
const bool rr = featureId == 13;
CreateDirectoryA(outDir.c_str(), nullptr);
g_json = std::fopen((outDir + "\\check.json").c_str(), "w");
if (g_json)
std::fputs("{", g_json);
json_field("feature", format("%u", featureId));
json_field("render_size", format("[%u, %u]", rw, rh));
json_field("output_size", format("[%u, %u]", ow, oh));
json_field("options", format("{\"hw_depth\": %s, \"separate_roughness\": %s, \"specular_hit\": %s, \"separate_eval_params\": %s}",
hwDepth ? "true" : "false", separateRoughness ? "true" : "false",
specularHit ? "true" : "false", separateEvalParams ? "true" : "false"));
auto finish = [&](int code) {
if (g_json)
{
std::fputs("\n}\n", g_json);
std::fclose(g_json);
}
std::printf("overall=%s\n", code == 0 ? "PASS" : "FAIL");
return code;
};
if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_12_0, __uuidof(ID3D12Device), reinterpret_cast<void**>(&g_device))))
{
check("device", false, "D3D12CreateDevice failed");
return finish(1);
}
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
g_device->CreateCommandQueue(&queueDesc, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&g_queue));
g_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, __uuidof(ID3D12CommandAllocator),
reinterpret_cast<void**>(&g_allocator));
g_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, g_allocator, nullptr,
__uuidof(ID3D12GraphicsCommandList), reinterpret_cast<void**>(&g_list));
g_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, __uuidof(ID3D12Fence), reinterpret_cast<void**>(&g_fence));
g_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
const unsigned long long vramStart = process_vram_kib();
HMODULE shim = LoadLibraryA(argv[1]);
if (shim == nullptr)
{
check("load", false, format("LoadLibrary(%s) failed: %lu", argv[1], GetLastError()));
return finish(1);
}
auto init = reinterpret_cast<PFN_Init_Ext>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_Init_Ext")));
auto capability = reinterpret_cast<PFN_Parameters>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_GetCapabilityParameters")));
auto allocate = reinterpret_cast<PFN_Parameters>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_AllocateParameters")));
auto createFeature = reinterpret_cast<PFN_CreateFeature>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_CreateFeature")));
auto evaluate = reinterpret_cast<PFN_Evaluate>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_EvaluateFeature")));
auto release = reinterpret_cast<PFN_Release>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_ReleaseFeature")));
auto shutdown = reinterpret_cast<PFN_Shutdown>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_Shutdown")));
auto requirements = reinterpret_cast<PFN_Requirements>(reinterpret_cast<void*>(GetProcAddress(shim, "NVSDK_NGX_D3D12_GetFeatureRequirements")));
if (!init || !capability || !allocate || !createFeature || !evaluate || !release || !shutdown || !requirements)
{
check("load", false, "missing NGX exports");
return finish(1);
}
// Discovery before Init, as games do.
wchar_t dataPath[MAX_PATH];
GetTempPathW(MAX_PATH, dataPath);
NgxFeatureDiscovery discovery = {};
discovery.sdkVersion = 0x15;
discovery.feature = featureId;
discovery.identifier.value.applicationId = 241534723ULL;
discovery.dataPath = dataPath;
NgxFeatureRequirement requirement = {};
IDXGIFactory4* factory = nullptr;
IDXGIAdapter* adapter = nullptr;
LUID luid;
g_device->GetAdapterLuid(&luid);
CreateDXGIFactory1(__uuidof(IDXGIFactory4), reinterpret_cast<void**>(&factory));
factory->EnumAdapterByLuid(luid, __uuidof(IDXGIAdapter), reinterpret_cast<void**>(&adapter));
const NgxResult discovered = requirements(adapter, &discovery, &requirement);
check("requirements_before_init", discovered == NGX_SUCCESS && requirement.supported == 0,
format("result 0x%08x, FeatureSupported %u", discovered, requirement.supported));
adapter->Release();
factory->Release();
const auto initStart = std::chrono::steady_clock::now();
const NgxResult initialized = init(241534723ULL, dataPath, g_device, 0x15, nullptr);
const double initMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - initStart).count();
check("init", initialized == NGX_SUCCESS, format("0x%08x in %.0f ms", initialized, initMs));
if (initialized != NGX_SUCCESS)
return finish(1);
void* capabilities = nullptr;
capability(&capabilities);
int available = -1;
void* optimal = nullptr;
const char* key = rr ? "SuperSamplingDenoising.Available" : "SuperSampling.Available";
d4r_ngx_get_int(capabilities, key, &available);
d4r_ngx_get_void(capabilities, rr ? "DLSSDOptimalSettingsCallback" : "DLSSOptimalSettingsCallback", &optimal);
check("capability", available == 1, format("%s=%d, optimal-settings callback %s", key, available, optimal ? "set" : "absent"));
// Inputs at render size, output at output size.
const auto srv = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
ID3D12Resource* color = create_texture(rw, rh, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* depth = create_texture(rw, rh, DXGI_FORMAT_R32_FLOAT, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* motion = create_texture(rw, rh, DXGI_FORMAT_R16G16_FLOAT, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* normals = create_texture(rw, rh, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* roughness = create_texture(rw, rh, DXGI_FORMAT_R16_FLOAT, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* diffuse = create_texture(rw, rh, DXGI_FORMAT_R8G8B8A8_UNORM, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* specular = create_texture(rw, rh, DXGI_FORMAT_R8G8B8A8_UNORM, D3D12_RESOURCE_FLAG_NONE, srv);
ID3D12Resource* hitDistance = create_texture(rw, rh, DXGI_FORMAT_R16_FLOAT, D3D12_RESOURCE_FLAG_NONE, srv);
// The output starts out holding a sentinel, so an unwritten texel is detectable.
ID3D12Resource* output = create_texture(ow, oh, DXGI_FORMAT_R16G16B16A16_FLOAT,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
Uploader uploader;
uploader.capacity = 64ull << 20;
uploader.staging = create_buffer(uploader.capacity, D3D12_HEAP_TYPE_UPLOAD, &uploader.mapped);
D3D12_RESOURCE_DESC outputDesc = {};
outputDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
outputDesc.Width = ow;
outputDesc.Height = oh;
outputDesc.DepthOrArraySize = 1;
outputDesc.MipLevels = 1;
outputDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
outputDesc.SampleDesc.Count = 1;
D3D12_PLACED_SUBRESOURCE_FOOTPRINT outputFootprint = {};
UINT outputRows = 0;
UINT64 outputRowBytes = 0, outputTotal = 0;
g_device->GetCopyableFootprints(&outputDesc, 0, 1, 0, &outputFootprint, &outputRows, &outputRowBytes, &outputTotal);
uint8_t* readMapped = nullptr;
ID3D12Resource* readback = create_buffer(outputTotal, D3D12_HEAP_TYPE_READBACK, &readMapped);
if (!color || !depth || !motion || !normals || !roughness || !diffuse || !specular || !hitDistance || !output ||
!uploader.staging || !readback)
{
check("resources", false, "texture creation");
return finish(1);
}
void* parameters = nullptr;
allocate(&parameters);
d4r_ngx_set_uint(parameters, "Width", rw);
d4r_ngx_set_uint(parameters, "Height", rh);
d4r_ngx_set_uint(parameters, "OutWidth", ow);
d4r_ngx_set_uint(parameters, "OutHeight", oh);
d4r_ngx_set_int(parameters, "PerfQualityValue", 2);
d4r_ngx_set_int(parameters, "DLSS.Feature.Create.Flags", 1 | 2); // HDR | MVLowRes
if (rr)
{
d4r_ngx_set_int(parameters, "DLSS.Denoise.Mode", 1);
d4r_ngx_set_int(parameters, "DLSS.Roughness.Mode", separateRoughness ? 0 : 1);
d4r_ngx_set_int(parameters, "DLSS.Use.HW.Depth", hwDepth ? 1 : 0);
}
NgxHandle* feature = nullptr;
const auto createStart = std::chrono::steady_clock::now();
const NgxResult created = createFeature(g_list, featureId, parameters, &feature);
const double createMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - createStart).count();
check("create", created == NGX_SUCCESS && feature != nullptr, format("0x%08x in %.0f ms", created, createMs));
json_field("create_ms", format("%.1f", createMs));
if (created != NGX_SUCCESS)
return finish(1);
// Keep the depth/roughness modes only on the creation object, as callers are allowed to do.
// Frame resources and camera settings below go onto a fresh object without those scalars.
if (separateEvalParams)
allocate(&parameters);
const M4 projection = projection_matrix(1.0f, static_cast<float>(rw) / rh, 0.1f, 1000.0f);
const uint32_t moveStart = frames / 3, cutFrame = frames * 2 / 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 scene, truth;
Camera previous = camera_at(0);
std::vector<double> callUs, totalUs;
std::string log = "[";
bool allEvaluated = true, sized = true, finite = true;
double errorBeforeCut = -1, errorFinal = -1, errorAtCut = -1;
std::vector<uint16_t> hit(static_cast<size_t>(rw) * rh), rough(static_cast<size_t>(rw) * rh);
std::vector<float> view(16), clip(16);
for (uint32_t f = 0; f < frames; ++f)
{
const Camera camera = camera_at(f);
const bool reset = f == 0 || f == cutFrame;
const uint32_t phase = f % 16 + 1;
const float jx = halton(phase, 2) - 0.5f, jy = halton(phase, 3) - 0.5f;
render(scene, rw, rh, camera, reset ? camera : previous, projection, f, jx, jy);
uploader.offset = 0;
uploader.upload(color, rw, rh, DXGI_FORMAT_R16G16B16A16_FLOAT, scene.color.data(), 8, srv);
if (hwDepth)
uploader.upload(depth, rw, rh, DXGI_FORMAT_R32_FLOAT, scene.hardwareDepth.data(), 4, srv);
else
{
std::vector<float> linear(scene.depth);
for (size_t i = 0; i < linear.size(); ++i)
linear[i] = scene.valid[i] ? linear[i] : 1.0e7f; // the sky: beyond any geometry
uploader.upload(depth, rw, rh, DXGI_FORMAT_R32_FLOAT, linear.data(), 4, srv);
}
uploader.upload(motion, rw, rh, DXGI_FORMAT_R16G16_FLOAT, scene.motionPixels.data(), 4, srv);
if (rr)
{
uploader.upload(normals, rw, rh, DXGI_FORMAT_R16G16B16A16_FLOAT, scene.normalsRoughness.data(), 8, srv);
uploader.upload(diffuse, rw, rh, DXGI_FORMAT_R8G8B8A8_UNORM, scene.diffuseAlbedoLinear.data(), 4, srv);
uploader.upload(specular, rw, rh, DXGI_FORMAT_R8G8B8A8_UNORM, scene.specularAlbedoLinear.data(), 4, srv);
if (separateRoughness)
{
for (size_t i = 0; i < rough.size(); ++i)
rough[i] = scene.normalsRoughness[i * 4 + 3];
uploader.upload(roughness, rw, rh, DXGI_FORMAT_R16_FLOAT, rough.data(), 2, srv);
}
if (specularHit)
{
for (size_t i = 0; i < hit.size(); ++i)
hit[i] = scene.noisy[kIndirectSpecular][i * 4 + 3];
uploader.upload(hitDistance, rw, rh, DXGI_FORMAT_R16_FLOAT, hit.data(), 2, srv);
}
}
d4r_ngx_set_d3d12_resource(parameters, "Color", color);
d4r_ngx_set_d3d12_resource(parameters, "Depth", depth);
d4r_ngx_set_d3d12_resource(parameters, "MotionVectors", motion);
d4r_ngx_set_d3d12_resource(parameters, "Output", output);
d4r_ngx_set_float(parameters, "Jitter.Offset.X", -jx); // DLSS: minus the sample offset
d4r_ngx_set_float(parameters, "Jitter.Offset.Y", -jy);
d4r_ngx_set_float(parameters, "MV.Scale.X", 1.0f);
d4r_ngx_set_float(parameters, "MV.Scale.Y", 1.0f);
d4r_ngx_set_int(parameters, "Reset", reset ? 1 : 0);
d4r_ngx_set_uint(parameters, "DLSS.Render.Subrect.Dimensions.Width", rw);
d4r_ngx_set_uint(parameters, "DLSS.Render.Subrect.Dimensions.Height", rh);
if (rr)
{
d4r_ngx_set_d3d12_resource(parameters, "GBuffer.Normals", normals);
d4r_ngx_set_d3d12_resource(parameters, "GBuffer.Roughness", separateRoughness ? roughness : nullptr);
d4r_ngx_set_d3d12_resource(parameters, "GBuffer.DiffuseAlbedo", diffuse);
d4r_ngx_set_d3d12_resource(parameters, "GBuffer.SpecularAlbedo", specular);
d4r_ngx_set_d3d12_resource(parameters, "DLSSD.SpecularHitDistance", specularHit ? hitDistance : nullptr);
const M4 viewMatrix = view_matrix(camera.eye, camera.target);
std::memcpy(view.data(), &viewMatrix, 64);
std::memcpy(clip.data(), &projection, 64);
d4r_ngx_set_void(parameters, "WorldToViewMatrix", view.data());
d4r_ngx_set_void(parameters, "ViewToClipMatrix", clip.data());
}
// Mark the output with a sentinel (NaN) the backend has to overwrite everywhere.
{
std::vector<uint16_t> sentinel(static_cast<size_t>(ow) * oh * 4, 0x7e00u);
uploader.upload(output, ow, oh, DXGI_FORMAT_R16G16B16A16_FLOAT, sentinel.data(), 8,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
}
const auto start = std::chrono::steady_clock::now();
const NgxResult evaluated = evaluate(g_list, feature, parameters, nullptr);
const auto recorded = std::chrono::steady_clock::now();
transition(output, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_SOURCE);
D3D12_TEXTURE_COPY_LOCATION dst = {};
dst.pResource = readback;
dst.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
dst.PlacedFootprint = outputFootprint;
D3D12_TEXTURE_COPY_LOCATION src = {};
src.pResource = output;
src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
g_list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
transition(output, D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
const bool submitted = submit_and_wait();
const auto end = std::chrono::steady_clock::now();
callUs.push_back(std::chrono::duration<double, std::micro>(recorded - start).count());
totalUs.push_back(std::chrono::duration<double, std::micro>(end - start).count());
if (evaluated != NGX_SUCCESS || !submitted)
{
allEvaluated = false;
std::printf("frame %u: EvaluateFeature 0x%08x, submit %s\n", f, evaluated, submitted ? "ok" : "FAILED");
if (!submitted)
break;
}
// Ground truth (the noise-free scene rendered at the output size from the same camera), on the frames
// that are reported and every 8th.
const bool measured = measureAll || f % 8 == 7 || f == 0 || f + 1 == moveStart || f + 1 == cutFrame || f == cutFrame ||
f + 1 == frames;
if (!measured)
{
previous = camera;
continue;
}
render(truth, ow, oh, camera, camera, projection_matrix(1.0f, static_cast<float>(ow) / oh, 0.1f, 1000.0f), f);
std::vector<V3> result(static_cast<size_t>(ow) * oh);
double error = 0, noisyError = 0, waterError = 0;
size_t unwritten = 0, nonFinite = 0, n = 0, waterN = 0;
for (UINT y = 0; y < oh; ++y)
for (UINT x = 0; x < ow; ++x)
{
const uint16_t* texel = reinterpret_cast<const uint16_t*>(readMapped + outputFootprint.Offset +
y * outputFootprint.Footprint.RowPitch) + x * 4;
const size_t i = static_cast<size_t>(y) * ow + x;
const V3 value{from_half(texel[0]), from_half(texel[1]), from_half(texel[2])};
if (texel[0] == 0x7e00u && texel[1] == 0x7e00u && texel[2] == 0x7e00u)
{
++unwritten;
continue;
}
if (!std::isfinite(value.x) || !std::isfinite(value.y) || !std::isfinite(value.z))
{
++nonFinite;
continue;
}
result[i] = value;
// Compared in a tonemapped space (x / (1 + x)), so highlights do not dominate.
const V3 t = truth.colorClean[i];
const float tv[3] = {t.x, t.y, t.z}, ov[3] = {value.x, value.y, value.z};
for (int c = 0; c < 3; ++c)
{
const double d = ov[c] / (1 + std::max(0.0f, ov[c])) - tv[c] / (1 + tv[c]);
error += d * d;
++n;
if (from_half(truth.normalsRoughness[i * 4 + 3]) < 0.06f)
{
waterError += d * d;
++waterN;
}
}
}
// The noisy input itself, nearest-upsampled: the error with no denoising and no reconstruction.
for (UINT y = 0; y < oh; ++y)
for (UINT x = 0; x < ow; ++x)
{
const size_t i = static_cast<size_t>(y) * ow + x;
const size_t s = static_cast<size_t>(y * rh / oh) * rw + x * rw / ow;
const V3 t = truth.colorClean[i];
const float tv[3] = {t.x, t.y, t.z};
for (int c = 0; c < 3; ++c)
{
const float v = std::max(0.0f, from_half(scene.color[s * 4 + c]));
const double d = v / (1 + v) - tv[c] / (1 + tv[c]);
noisyError += d * d;
}
}
const double rmse = n ? std::sqrt(error / n) : -1, noisyRmse = std::sqrt(noisyError / (3.0 * ow * oh));
sized = sized && unwritten == 0;
finite = finite && nonFinite == 0;
if (f + 1 == cutFrame)
errorBeforeCut = rmse;
if (f == cutFrame)
errorAtCut = rmse;
if (f + 1 == frames)
errorFinal = rmse;
const double psnr = rmse > 0 ? 20 * std::log10(1.0 / rmse) : 0;
const double waterRmse = waterN ? std::sqrt(waterError / waterN) : -1;
if (fsr_scene::g_water)
std::printf("frame %u: rmse %.5f water %.5f\n", f, rmse, waterRmse);
log += format("%s\n {\"frame\": %u, \"reset\": %s, \"result\": %u, \"rmse\": %.5f, \"psnr_db\": %.2f, \"water_rmse\": %.5f, "
"\"noisy_input_rmse\": %.5f, \"unwritten\": %zu, \"nonfinite\": %zu, \"call_us\": %.0f, "
"\"submit_wait_us\": %.0f}",
f ? "," : "", f, reset ? "true" : "false", evaluated, rmse, psnr, waterRmse, noisyRmse, unwritten, nonFinite,
callUs.back(), totalUs.back());
if (f == 0 || f + 1 == moveStart || f + 1 == cutFrame || f == cutFrame || f + 1 == frames)
{
write_ppm(format("%s\\frame%03u_output.ppm", outDir.c_str(), f), ow, oh, result);
write_ppm(format("%s\\frame%03u_truth.ppm", outDir.c_str(), f), ow, oh, truth.colorClean);
std::vector<V3> input(static_cast<size_t>(rw) * rh);
for (size_t i = 0; i < input.size(); ++i)
input[i] = {from_half(scene.color[i * 4]), from_half(scene.color[i * 4 + 1]), from_half(scene.color[i * 4 + 2])};
write_ppm(format("%s\\frame%03u_input.ppm", outDir.c_str(), f), rw, rh, input);
}
previous = camera;
}
json_field("frame_log", log + "\n ]");
const unsigned long long vramPeak = process_vram_kib();
std::vector<double> warmCall(callUs.begin() + std::min<size_t>(callUs.size(), 4), callUs.end());
std::vector<double> warmTotal(totalUs.begin() + std::min<size_t>(totalUs.size(), 4), totalUs.end());
std::sort(warmCall.begin(), warmCall.end());
std::sort(warmTotal.begin(), warmTotal.end());
const double medianCall = warmCall.empty() ? -1 : warmCall[warmCall.size() / 2];
const double medianTotal = warmTotal.empty() ? -1 : warmTotal[warmTotal.size() / 2];
json_field("timing_us", format("{\"evaluate_call_median\": %.0f, \"submit_wait_median\": %.0f, \"first_frame_submit_wait\": %.0f}",
medianCall, medianTotal, totalUs.empty() ? -1.0 : totalUs[0]));
json_field("vram_mib", format("{\"before_init\": %.1f, \"after_frames\": %.1f}", vramStart / 1024.0, vramPeak / 1024.0));
check("evaluations", allEvaluated, format("%u frames, EvaluateFeature call median %.0f us, call+submit+wait median %.0f us",
frames, medianCall, medianTotal));
check("output_size", sized, format("every texel of the %ux%u output written on every frame", ow, oh));
check("output_finite", finite, "no NaN/Inf");
json_field("rmse", format("{\"before_cut\": %.5f, \"at_cut\": %.5f, \"final\": %.5f}", errorBeforeCut, errorAtCut, errorFinal));
std::printf("tonemapped RMSE vs ground truth: before cut %.5f, at cut %.5f, final %.5f\n", errorBeforeCut, errorAtCut,
errorFinal);
const NgxResult released = release(feature);
// Let the shim's deferred cleanup see the list retire.
submit_and_wait();
const NgxResult shut = shutdown();
check("release", released == NGX_SUCCESS && shut == NGX_SUCCESS, format("release 0x%08x, shutdown 0x%08x", released, shut));
return finish(g_failed ? 1 : 0);
}
+133
View File
@@ -0,0 +1,133 @@
// Compare fused storage output with the original RGBA16F copy + R11G11B10 blit.
#include "d4r_vulkan_staging.h"
#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
static void check(VkResult r) { if (r != VK_SUCCESS) { std::fprintf(stderr,"Vulkan error %d\n",r); std::exit(1); } }
struct Image { VkImage image; VkImageView view; VkDeviceMemory memory; VkFormat format; bool depth; };
int main() {
constexpr uint32_t W=257, H=257;
constexpr size_t BYTES=W*H*16;
VkApplicationInfo app={VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion=VK_API_VERSION_1_1;
VkInstanceCreateInfo ic={VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};ic.pApplicationInfo=&app;
VkInstance instance;check(vkCreateInstance(&ic,nullptr,&instance));
uint32_t count=0;check(vkEnumeratePhysicalDevices(instance,&count,nullptr));
std::vector<VkPhysicalDevice> devices(count);check(vkEnumeratePhysicalDevices(instance,&count,devices.data()));
VkPhysicalDevice physical=VK_NULL_HANDLE;
for(auto d:devices){VkPhysicalDeviceProperties p;vkGetPhysicalDeviceProperties(d,&p);if(p.vendorID==0x1002){physical=d;std::printf("GPU: %s\n",p.deviceName);break;}}
assert(physical);
vkGetPhysicalDeviceQueueFamilyProperties(physical,&count,nullptr);std::vector<VkQueueFamilyProperties> families(count);
vkGetPhysicalDeviceQueueFamilyProperties(physical,&count,families.data());
uint32_t family=0;while(family<count && !(families[family].queueFlags&VK_QUEUE_COMPUTE_BIT))++family;assert(family<count);
float priority=1;VkDeviceQueueCreateInfo qi={VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};qi.queueFamilyIndex=family;qi.queueCount=1;qi.pQueuePriorities=&priority;
VkPhysicalDeviceFeatures supported, enabled={};vkGetPhysicalDeviceFeatures(physical,&supported);
enabled.shaderStorageImageExtendedFormats=supported.shaderStorageImageExtendedFormats;
VkDeviceCreateInfo dc={VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};dc.queueCreateInfoCount=1;dc.pQueueCreateInfos=&qi;dc.pEnabledFeatures=&enabled;
VkDevice device;check(vkCreateDevice(physical,&dc,nullptr,&device));VkQueue queue;vkGetDeviceQueue(device,family,0,&queue);
VkPhysicalDeviceMemoryProperties memory;vkGetPhysicalDeviceMemoryProperties(physical,&memory);
auto allocate=[&](VkMemoryRequirements req,VkMemoryPropertyFlags flags){
uint32_t type=0;while(type<memory.memoryTypeCount && (!(req.memoryTypeBits&(1u<<type)) || (memory.memoryTypes[type].propertyFlags&flags)!=flags))++type;assert(type<memory.memoryTypeCount);
VkMemoryAllocateInfo a={VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};a.allocationSize=req.size;a.memoryTypeIndex=type;VkDeviceMemory m;check(vkAllocateMemory(device,&a,nullptr,&m));return m;
};
auto image=[&](VkFormat format,bool output){
Image result={};result.format=format;result.depth=format==VK_FORMAT_D32_SFLOAT_S8_UINT;
VkImageCreateInfo info={VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};info.imageType=VK_IMAGE_TYPE_2D;info.format=format;info.extent={W,H,1};info.mipLevels=info.arrayLayers=1;info.samples=VK_SAMPLE_COUNT_1_BIT;info.tiling=VK_IMAGE_TILING_OPTIMAL;
// Transfers build the reference and read back packed R11G11B10 words.
info.usage=VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (output ? VK_IMAGE_USAGE_STORAGE_BIT : 0);
check(vkCreateImage(device,&info,nullptr,&result.image));VkMemoryRequirements req;vkGetImageMemoryRequirements(device,result.image,&req);
result.memory=allocate(req,0);check(vkBindImageMemory(device,result.image,result.memory,0));
VkImageViewCreateInfo view={VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};view.image=result.image;view.viewType=VK_IMAGE_VIEW_TYPE_2D;view.format=format;
view.subresourceRange={result.depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
check(vkCreateImageView(device,&view,nullptr,&result.view));return result;
};
auto destroyImage=[&](Image& i){vkDestroyImageView(device,i.view,nullptr);vkDestroyImage(device,i.image,nullptr);vkFreeMemory(device,i.memory,nullptr);};
VkBufferCreateInfo bi={VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};bi.size=BYTES;bi.usage=VK_BUFFER_USAGE_STORAGE_BUFFER_BIT|VK_BUFFER_USAGE_TRANSFER_SRC_BIT|VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VkBuffer buffers[2];VkDeviceMemory allocations[2];float* mapped[2];
for(int i=0;i<2;++i){check(vkCreateBuffer(device,&bi,nullptr,&buffers[i]));VkMemoryRequirements req;vkGetBufferMemoryRequirements(device,buffers[i],&req);allocations[i]=allocate(req,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);check(vkBindBufferMemory(device,buffers[i],allocations[i],0));check(vkMapMemory(device,allocations[i],0,VK_WHOLE_SIZE,0,reinterpret_cast<void**>(&mapped[i])));}
D4rVkStaging staging;assert(staging.init(device,vkGetDeviceProcAddr));
VkCommandPoolCreateInfo pi={VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};pi.queueFamilyIndex=family;VkCommandPool pool;check(vkCreateCommandPool(device,&pi,nullptr,&pool));
VkCommandBufferAllocateInfo ca={VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};ca.commandPool=pool;ca.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;ca.commandBufferCount=1;VkCommandBuffer cmd;check(vkAllocateCommandBuffers(device,&ca,&cmd));
auto begin=[&](){check(vkResetCommandPool(device,pool,0));VkCommandBufferBeginInfo b={VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};b.flags=VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;check(vkBeginCommandBuffer(cmd,&b));};
auto submit=[&](){VkMemoryBarrier host={VK_STRUCTURE_TYPE_MEMORY_BARRIER};host.srcAccessMask=VK_ACCESS_SHADER_WRITE_BIT|VK_ACCESS_TRANSFER_WRITE_BIT;host.dstAccessMask=VK_ACCESS_HOST_READ_BIT;vkCmdPipelineBarrier(cmd,VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT|VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_HOST_BIT,0,1,&host,0,nullptr,0,nullptr);check(vkEndCommandBuffer(cmd));VkSubmitInfo s={VK_STRUCTURE_TYPE_SUBMIT_INFO};s.commandBufferCount=1;s.pCommandBuffers=&cmd;check(vkQueueSubmit(queue,1,&s,VK_NULL_HANDLE));check(vkQueueWaitIdle(queue));};
auto barrier=[&](const Image& i,VkImageLayout from,VkImageLayout to,VkAccessFlags src,VkAccessFlags dst){
VkImageMemoryBarrier b={VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};b.image=i.image;b.oldLayout=from;b.newLayout=to;b.srcAccessMask=src;b.dstAccessMask=dst;b.srcQueueFamilyIndex=b.dstQueueFamilyIndex=VK_QUEUE_FAMILY_IGNORED;
b.subresourceRange={static_cast<VkImageAspectFlags>(i.depth ? VK_IMAGE_ASPECT_DEPTH_BIT|VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_COLOR_BIT),0,1,0,1};vkCmdPipelineBarrier(cmd,VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,0,0,nullptr,0,nullptr,1,&b);
};
auto source=image(VK_FORMAT_R16G16B16A16_SFLOAT,false);
auto reference=image(VK_FORMAT_B10G11R11_UFLOAT_PACK32,true);
auto candidate=image(VK_FORMAT_B10G11R11_UFLOAT_PACK32,true);
D4rVkStagingSet write;assert(staging.set(write,candidate.view,candidate.format,buffers[0],W*H*8,true));
auto* halves=reinterpret_cast<uint16_t*>(mapped[0]);
for(unsigned i=0;i<W*H;++i){halves[i*4]=i;halves[i*4+1]=i*73+31;halves[i*4+2]=i*193+57;halves[i*4+3]=0x3c00;}
std::memset(mapped[1],0xa5,BYTES);
begin();barrier(source,VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,0,VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy region={};region.imageSubresource={VK_IMAGE_ASPECT_COLOR_BIT,0,0,1};region.imageExtent={W,H,1};
vkCmdCopyBufferToImage(cmd,buffers[0],source.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&region);
barrier(source,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT);
barrier(reference,VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,0,VK_ACCESS_TRANSFER_WRITE_BIT);
VkImageBlit blit={};blit.srcSubresource=blit.dstSubresource=region.imageSubresource;
blit.srcOffsets[1]=blit.dstOffsets[1]={(int)W,(int)H,1};
vkCmdBlitImage(cmd,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,reference.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,VK_FILTER_NEAREST);
barrier(reference,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT);
vkCmdCopyImageToBuffer(cmd,reference.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,buffers[1],1,&region);
barrier(candidate,VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_GENERAL,0,VK_ACCESS_SHADER_WRITE_BIT);
staging.record(cmd,write,W,H);
barrier(candidate,VK_IMAGE_LAYOUT_GENERAL,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_ACCESS_SHADER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT);
region.bufferOffset=W*H*4;
vkCmdCopyImageToBuffer(cmd,candidate.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,buffers[1],1,&region);submit();
auto* words=reinterpret_cast<uint32_t*>(mapped[1]);unsigned mismatches=0,finite=0,nan=0;
for(unsigned i=0;i<W*H;++i)for(unsigned c=0;c<3;++c) {
const auto h=halves[i*4+c];const unsigned shift=c*11,bits=c==2 ? 10 : 11,mask=(1u<<bits)-1;
unsigned old=(words[i]>>shift)&mask,now=(words[W*H+i]>>shift)&mask;
if((h&0x7c00)==0x7c00 && (h&0x3ff)) {
++nan;const unsigned m=c==2 ? 5 : 6;
assert((old>>m)==31 && (now>>m)==31 && (old&((1u<<m)-1)) && (now&((1u<<m)-1)));
} else {++finite;if(old!=now){if(mismatches<8)std::printf("DIFF pixel=%u channel=%u half=%04x blit=%x compute=%x\n",i,c,h,old,now);++mismatches;}}
}
std::printf("R11 output blit vs compute: finite_or_inf_channels=%u mismatches=%u nan_channels=%u\n",finite,mismatches,nan);
assert(mismatches==0);
staging.free(write);destroyImage(source);destroyImage(reference);destroyImage(candidate);
// Input conversion: every unsigned R11/R10 channel encoding, padded rows.
auto input=image(VK_FORMAT_B10G11R11_UFLOAT_PACK32,false);
auto input_reference=image(VK_FORMAT_R16G16B16A16_SFLOAT,true);
D4rVkStagingSet read;assert(staging.set(read,input.view,input.format,buffers[0],BYTES,false));
auto* raw=reinterpret_cast<uint32_t*>(mapped[0]);
for(unsigned i=0;i<BYTES/4;++i)raw[i]=0xa5a5a5a5;
for(unsigned i=0;i<W*H;++i)raw[i]=(i&2047u)|((i*73u+31u)&2047u)<<11|((i*193u+57u)&1023u)<<22;
std::vector<uint32_t> initial(raw,raw+BYTES/4);
begin();barrier(input,VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,0,VK_ACCESS_TRANSFER_WRITE_BIT);
region.bufferOffset=0;
vkCmdCopyBufferToImage(cmd,buffers[0],input.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&region);
barrier(input,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT);
barrier(input_reference,VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,0,VK_ACCESS_TRANSFER_WRITE_BIT);
vkCmdBlitImage(cmd,input.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,input_reference.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,VK_FILTER_NEAREST);
barrier(input_reference,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT);
vkCmdCopyImageToBuffer(cmd,input_reference.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,buffers[1],1,&region);
barrier(input,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,VK_ACCESS_TRANSFER_READ_BIT,VK_ACCESS_SHADER_READ_BIT);
VkMemoryBarrier reuse={VK_STRUCTURE_TYPE_MEMORY_BARRIER};reuse.srcAccessMask=VK_ACCESS_TRANSFER_READ_BIT;reuse.dstAccessMask=VK_ACCESS_SHADER_WRITE_BIT;
vkCmdPipelineBarrier(cmd,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,0,1,&reuse,0,nullptr,0,nullptr);
constexpr unsigned stride=W+7;
staging.record(cmd,read,W,H,1,stride);submit();
const auto* actual=reinterpret_cast<const uint16_t*>(mapped[0]);
const auto* expected=reinterpret_cast<const uint16_t*>(mapped[1]);
unsigned input_bad=0,input_nan=0;
for(unsigned y=0;y<H;++y) {
for(unsigned x=0;x<W;++x)for(unsigned c=0;c<4;++c) {
const auto got=actual[(size_t(y)*stride+x)*4+c],want=expected[(size_t(y)*W+x)*4+c];
if((want&0x7c00)==0x7c00 && (want&0x3ff)) {
++input_nan;assert((got&0x7c00)==0x7c00 && (got&0x3ff));
} else if(got!=want){if(input_bad<8)std::printf("INPUT DIFF x=%u y=%u channel=%u blit=%04x compute=%04x\n",x,y,c,want,got);++input_bad;}
}
for(unsigned word=(y*stride+W)*2;word<(y+1)*stride*2;++word)assert(raw[word]==initial[word]);
}
for(unsigned word=H*stride*2;word<BYTES/4;++word)assert(raw[word]==initial[word]);
std::printf("R11 input blit vs compute: mismatches=%u nan_channels=%u padded_rows_unchanged=1\n",input_bad,input_nan);
assert(input_bad==0);
staging.free(read);destroyImage(input);destroyImage(input_reference);
staging.destroy();vkDestroyCommandPool(device,pool,nullptr);
for(int i=0;i<2;++i){vkUnmapMemory(device,allocations[i]);vkDestroyBuffer(device,buffers[i],nullptr);vkFreeMemory(device,allocations[i],nullptr);}
vkDestroyDevice(device,nullptr);vkDestroyInstance(instance,nullptr);std::puts("PASS: exhaustive half inputs, all RGB channels, partial workgroups");
}
+346 -3
View File
@@ -14,6 +14,8 @@
#include <time.h>
#include "d4r_native_selection.h"
#include "d4r_motion_dilation.h"
#include "d4r_native_motion.h"
#include "d4r_wire_history.h"
#ifdef D4R_MOTION_KERNELS
#include "d4r_motion_kernels.h"
#endif
@@ -983,9 +985,11 @@ static void* find_zluda_symbol(const char* name)
return cuda_library != NULL ? dlsym(cuda_library, name) : NULL;
}
/* d4r-owned, opt-in preprocessing. Runs on HIP's legacy/default stream, shared
with CUDA's null stream; the shim records and waits for completion before NGX. */
CUresult WINAPI d4rDilateMotionVectors(const D4rMotionDilationParams* parameters)
static CUstream d4r_ngx_stream(CUstream stream);
/* Preprocessing, input waits and NGX null-stream launches all use d4r_ngx_stream.
allocation_height == 0 retains the original ABI's active-height contract. */
static CUresult dilate_motion_vectors(const D4rMotionDilationParams* parameters, uint32_t allocation_height, int thin_sky)
{
if (parameters == NULL || parameters->depth == 0 || parameters->motion == 0 || parameters->output == 0 ||
parameters->radius < 1 || parameters->radius > 2 || parameters->render_width == 0 ||
@@ -1002,6 +1006,8 @@ CUresult WINAPI d4rDilateMotionVectors(const D4rMotionDilationParams* parameters
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void* module;
static void* function;
static void* lds_function;
static void* thin_function;
static HIP_LAUNCH launch;
static int initialized, result = CUDA_ERROR_NOT_SUPPORTED;
pthread_mutex_lock(&lock);
@@ -1018,7 +1024,11 @@ CUresult WINAPI d4rDilateMotionVectors(const D4rMotionDilationParams* parameters
launch = hip != NULL ? (HIP_LAUNCH)dlsym(hip, "hipModuleLaunchKernel") : NULL;
if (image != NULL && load != NULL && get != NULL && launch != NULL &&
load(&module, image) == 0 && get(&function, module, "d4r_motion_dilate") == 0)
{
result = CUDA_SUCCESS;
get(&lds_function, module, "d4r_motion_dilate_lds");
get(&thin_function, module, "d4r_motion_dilate_lds_thin");
}
tracef("motion-vector dilation: %s (%s)", result == 0 ? "GPU kernel ready" : "unavailable", architecture);
}
const int status = result;
@@ -1026,6 +1036,19 @@ CUresult WINAPI d4rDilateMotionVectors(const D4rMotionDilationParams* parameters
if (status != CUDA_SUCCESS)
return status;
D4rMotionDilationParams copy = *parameters;
const int lds = lds_function != NULL && d4r_motion_dilation_lds_eligible(&copy);
if (allocation_height != 0 && (!lds || allocation_height < copy.motion_height ||
copy.motion_pitch % 4 != 0 || copy.motion_pitch / 4 > 0x7fffffffu || allocation_height > 0x7fffffffu))
return CUDA_ERROR_INVALID_VALUE;
if (thin_sky && (!lds || thin_function == NULL))
return CUDA_ERROR_NOT_SUPPORTED;
if (lds)
{
uint32_t height = allocation_height != 0 ? allocation_height : copy.motion_height;
void* arguments[] = {&copy, &height};
return launch(thin_sky ? thin_function : lds_function, (unsigned)((copy.motion_pitch / 4 + 15) / 16), (height + 15) / 16,
1, 256, 1, 1, 0, d4r_ngx_stream(NULL), arguments, NULL);
}
void* arguments[] = {&copy};
const uint64_t pixels = (uint64_t)copy.motion_width * copy.motion_height;
return launch(function, (unsigned)((pixels + 127) / 128), 1, 1, 128, 1, 1, 0, d4r_ngx_stream(NULL), arguments, NULL);
@@ -1034,6 +1057,326 @@ CUresult WINAPI d4rDilateMotionVectors(const D4rMotionDilationParams* parameters
#endif
}
CUresult WINAPI d4rDilateMotionVectors(const D4rMotionDilationParams* parameters)
{
return dilate_motion_vectors(parameters, 0, 0);
}
/* Additive export: retain the 72-byte parameter ABI and preserve the full padded
allocation in the LDS launch, without a separate full-plane copy. */
CUresult WINAPI d4rDilateMotionVectorsPadded(const D4rMotionDilationParams* parameters, uint32_t allocation_height)
{
if (allocation_height == 0)
return CUDA_ERROR_INVALID_VALUE;
return dilate_motion_vectors(parameters, allocation_height, 0);
}
CUresult WINAPI d4rDilateMotionVectorsThin(const D4rMotionDilationParams* parameters, uint32_t allocation_height)
{
if (allocation_height == 0)
return CUDA_ERROR_INVALID_VALUE;
return dilate_motion_vectors(parameters, allocation_height, 1);
}
/* Additive native-MV operation. The recorded input marker must precede this
* preprocessing launch, and NGX follows it on the same effective stream. */
static CUresult d4r_compose_native_motion(const D4rNativeMotionParams* parameters, unsigned int thin,float jitter_x,float jitter_y)
{
if(!d4r_native_motion_parameters_valid(parameters)) return CUDA_ERROR_INVALID_VALUE;
#ifdef D4R_MOTION_KERNELS
typedef int(__attribute__((sysv_abi)) * HIP_LOAD)(void**,const void*);
typedef int(__attribute__((sysv_abi)) * HIP_FUNCTION)(void**,void*,const char*);
typedef int(__attribute__((sysv_abi)) * HIP_LAUNCH)(void*,unsigned,unsigned,unsigned,
unsigned,unsigned,unsigned,unsigned,void*,void**,void**);
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
static void *module,*function[3];
static HIP_LAUNCH launch;
static int initialized,result=CUDA_ERROR_NOT_SUPPORTED;
int status;
pthread_mutex_lock(&lock);
if(!initialized)
{
void* hip;
const void* image;
HIP_LOAD load;
HIP_FUNCTION get;
char architecture[32];
initialized=1;pthread_once(&load_once,load_zluda);
hip=dlopen("libamdhip64.so.7",RTLD_NOW|RTLD_LOCAL);
gpu_architecture(architecture,sizeof(architecture));image=d4r_motion_image(architecture);
load=hip ? (HIP_LOAD)dlsym(hip,"hipModuleLoadData") : NULL;
get=hip ? (HIP_FUNCTION)dlsym(hip,"hipModuleGetFunction") : NULL;
launch=hip ? (HIP_LAUNCH)dlsym(hip,"hipModuleLaunchKernel") : NULL;
if(image && load && get && launch && load(&module,image)==0 &&
get(&function[0],module,"d4r_native_motion_compose")==0 &&
get(&function[1],module,"d4r_native_motion_compose_thin")==0 &&
get(&function[2],module,"d4r_native_motion_compose_aligned")==0) result=CUDA_SUCCESS;
tracef("native motion composition: %s (%s)",result==0 ? "GPU kernel ready" : "unavailable",architecture);
}
status=result;pthread_mutex_unlock(&lock);
if(status==CUDA_SUCCESS)
{
D4rNativeMotionThinParams copy={*parameters,jitter_x,jitter_y};
void* arguments[]={&copy};
return launch(function[thin],(copy.base.width+15)/16,(copy.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
}
return status;
#else
return CUDA_ERROR_NOT_SUPPORTED;
#endif
}
CUresult WINAPI d4rComposeNativeMotion(const D4rNativeMotionParams* parameters)
{ return d4r_compose_native_motion(parameters,0,0,0); }
CUresult WINAPI d4rComposeNativeMotionThin(const D4rNativeMotionThinParams* parameters)
{
if(!parameters || !(parameters->jitter_x>=-1 && parameters->jitter_x<=1 && parameters->jitter_y>=-1 && parameters->jitter_y<=1)) return CUDA_ERROR_INVALID_VALUE;
return d4r_compose_native_motion(&parameters->base,1,parameters->jitter_x,parameters->jitter_y);
}
CUresult WINAPI d4rComposeNativeMotionAligned(const D4rNativeMotionThinParams* parameters)
{
if(!parameters || !(parameters->jitter_x>=-1 && parameters->jitter_x<=1 && parameters->jitter_y>=-1 && parameters->jitter_y<=1)) return CUDA_ERROR_INVALID_VALUE;
return d4r_compose_native_motion(&parameters->base,2,parameters->jitter_x,parameters->jitter_y);
}
CUresult WINAPI d4rStabilizeThinWires(const D4rWireHistoryParams* parameters)
{
if(!d4r_wire_history_parameters_valid(parameters)) return CUDA_ERROR_INVALID_VALUE;
#ifdef D4R_MOTION_KERNELS
typedef int(__attribute__((sysv_abi)) * HIP_LOAD)(void**,const void*);
typedef int(__attribute__((sysv_abi)) * HIP_FUNCTION)(void**,void*,const char*);
typedef int(__attribute__((sysv_abi)) * HIP_LAUNCH)(void*,unsigned,unsigned,unsigned,
unsigned,unsigned,unsigned,unsigned,void*,void**,void**);
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
static void *module,*function;
static HIP_LAUNCH launch;
static int initialized,result=CUDA_ERROR_NOT_SUPPORTED;
int status;
pthread_mutex_lock(&lock);
if(!initialized)
{
void* hip;
const void* image;
HIP_LOAD load;
HIP_FUNCTION get;
char architecture[32];
initialized=1;pthread_once(&load_once,load_zluda);
hip=dlopen("libamdhip64.so.7",RTLD_NOW|RTLD_LOCAL);
gpu_architecture(architecture,sizeof(architecture));image=d4r_motion_image(architecture);
load=hip ? (HIP_LOAD)dlsym(hip,"hipModuleLoadData") : NULL;
get=hip ? (HIP_FUNCTION)dlsym(hip,"hipModuleGetFunction") : NULL;
launch=hip ? (HIP_LAUNCH)dlsym(hip,"hipModuleLaunchKernel") : NULL;
if(image && load && get && launch && load(&module,image)==0 &&
get(&function,module,"d4r_wire_history")==0) result=CUDA_SUCCESS;
tracef("thin wire history: %s (%s)",result==0 ? "GPU kernel ready" : "unavailable",architecture);
}
status=result;pthread_mutex_unlock(&lock);
if(status==CUDA_SUCCESS)
{
D4rWireHistoryParams copy=*parameters;
void* arguments[]={&copy};
return launch(function,(copy.width+15)/16,(copy.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
}
return status;
#else
return CUDA_ERROR_NOT_SUPPORTED;
#endif
}
CUresult WINAPI d4rComposeNativeMotionHybrid(const D4rNativeMotionHybridParams* parameters)
{
if(!d4r_native_motion_hybrid_parameters_valid(parameters)) return CUDA_ERROR_INVALID_VALUE;
#ifdef D4R_MOTION_KERNELS
typedef int(__attribute__((sysv_abi)) * HIP_LOAD)(void**,const void*);
typedef int(__attribute__((sysv_abi)) * HIP_FUNCTION)(void**,void*,const char*);
typedef int(__attribute__((sysv_abi)) * HIP_LAUNCH)(void*,unsigned,unsigned,unsigned,
unsigned,unsigned,unsigned,unsigned,void*,void**,void**);
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
static void *module,*function;
static HIP_LAUNCH launch;
static int initialized,result=CUDA_ERROR_NOT_SUPPORTED;
int status;
pthread_mutex_lock(&lock);
if(!initialized)
{
void* hip;
const void* image;
HIP_LOAD load;
HIP_FUNCTION get;
char architecture[32];
initialized=1;pthread_once(&load_once,load_zluda);
hip=dlopen("libamdhip64.so.7",RTLD_NOW|RTLD_LOCAL);
gpu_architecture(architecture,sizeof(architecture));image=d4r_motion_image(architecture);
load=hip ? (HIP_LOAD)dlsym(hip,"hipModuleLoadData") : NULL;
get=hip ? (HIP_FUNCTION)dlsym(hip,"hipModuleGetFunction") : NULL;
launch=hip ? (HIP_LAUNCH)dlsym(hip,"hipModuleLaunchKernel") : NULL;
if(image && load && get && launch && load(&module,image)==0 &&
get(&function,module,"d4r_native_motion_compose_hybrid")==0) result=CUDA_SUCCESS;
tracef("hybrid native motion: %s (%s)",result==0 ? "GPU kernel ready" : "unavailable",architecture);
}
status=result;pthread_mutex_unlock(&lock);
if(status==CUDA_SUCCESS)
{
D4rNativeMotionHybridParams copy=*parameters;
void* arguments[]={&copy};
return launch(function,(copy.native.base.width+15)/16,(copy.native.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
}
return status;
#else
return CUDA_ERROR_NOT_SUPPORTED;
#endif
}
CUresult WINAPI d4rStabilizeThinWiresSparse(const D4rWireHistorySparseParams* parameters)
{
if(!d4r_wire_history_sparse_parameters_valid(parameters)) return CUDA_ERROR_INVALID_VALUE;
#ifdef D4R_MOTION_KERNELS
typedef int(__attribute__((sysv_abi)) * HIP_LOAD)(void**,const void*);
typedef int(__attribute__((sysv_abi)) * HIP_FUNCTION)(void**,void*,const char*);
typedef int(__attribute__((sysv_abi)) * HIP_LAUNCH)(void*,unsigned,unsigned,unsigned,
unsigned,unsigned,unsigned,unsigned,void*,void**,void**);
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
static void *module,*function,*apply;
static HIP_LAUNCH launch;
static int initialized,result=CUDA_ERROR_NOT_SUPPORTED;
int status;
pthread_mutex_lock(&lock);
if(!initialized)
{
void* hip;
const void* image;
HIP_LOAD load;
HIP_FUNCTION get;
char architecture[32];
initialized=1;pthread_once(&load_once,load_zluda);
hip=dlopen("libamdhip64.so.7",RTLD_NOW|RTLD_LOCAL);
gpu_architecture(architecture,sizeof(architecture));image=d4r_motion_image(architecture);
load=hip ? (HIP_LOAD)dlsym(hip,"hipModuleLoadData") : NULL;
get=hip ? (HIP_FUNCTION)dlsym(hip,"hipModuleGetFunction") : NULL;
launch=hip ? (HIP_LAUNCH)dlsym(hip,"hipModuleLaunchKernel") : NULL;
if(image && load && get && launch && load(&module,image)==0 &&
get(&function,module,"d4r_wire_history_sparse")==0 &&
get(&apply,module,"d4r_wire_history_apply")==0) result=CUDA_SUCCESS;
tracef("sparse thin wire history: %s (%s)",result==0 ? "GPU kernel ready" : "unavailable",architecture);
}
status=result;pthread_mutex_unlock(&lock);
if(status==CUDA_SUCCESS)
{
D4rWireHistorySparseParams copy=*parameters;
void* arguments[]={&copy};
status=launch(function,(copy.base.width+15)/16,(copy.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
if(status!=0) return status;
return launch(apply,(copy.base.width+15)/16,(copy.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
}
return status;
#else
return CUDA_ERROR_NOT_SUPPORTED;
#endif
}
CUresult WINAPI d4rComposeNativeMotionPrepared(const D4rNativeMotionPreparedParams* parameters)
{
if(!d4r_native_motion_prepared_parameters_valid(parameters)) return CUDA_ERROR_INVALID_VALUE;
#ifdef D4R_MOTION_KERNELS
typedef int(__attribute__((sysv_abi)) * HIP_LOAD)(void**,const void*);
typedef int(__attribute__((sysv_abi)) * HIP_FUNCTION)(void**,void*,const char*);
typedef int(__attribute__((sysv_abi)) * HIP_LAUNCH)(void*,unsigned,unsigned,unsigned,
unsigned,unsigned,unsigned,unsigned,void*,void**,void**);
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
static void *module,*function,*apply;
static HIP_LAUNCH launch;
static int initialized,result=CUDA_ERROR_NOT_SUPPORTED;
int status;
pthread_mutex_lock(&lock);
if(!initialized)
{
void* hip;
const void* image;
HIP_LOAD load;
HIP_FUNCTION get;
char architecture[32];
initialized=1;pthread_once(&load_once,load_zluda);
hip=dlopen("libamdhip64.so.7",RTLD_NOW|RTLD_LOCAL);
gpu_architecture(architecture,sizeof(architecture));image=d4r_motion_image(architecture);
load=hip ? (HIP_LOAD)dlsym(hip,"hipModuleLoadData") : NULL;
get=hip ? (HIP_FUNCTION)dlsym(hip,"hipModuleGetFunction") : NULL;
launch=hip ? (HIP_LAUNCH)dlsym(hip,"hipModuleLaunchKernel") : NULL;
if(image && load && get && launch && load(&module,image)==0 &&
get(&function,module,"d4r_native_motion_prepare")==0 &&
get(&apply,module,"d4r_native_motion_prepared")==0) result=CUDA_SUCCESS;
tracef("prepared native motion: %s (%s)",result==0 ? "GPU kernel ready" : "unavailable",architecture);
}
status=result;pthread_mutex_unlock(&lock);
if(status==CUDA_SUCCESS)
{
D4rNativeMotionPreparedParams copy=*parameters;
void* arguments[]={&copy};
status=launch(function,(copy.native.base.width+31)/32,(copy.native.base.height+31)/32,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
if(status!=0) return status;
return launch(apply,(copy.native.base.width+15)/16,(copy.native.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
}
return status;
#else
return CUDA_ERROR_NOT_SUPPORTED;
#endif
}
CUresult WINAPI d4rStabilizeThinWiresPrepared(const D4rWireHistoryPreparedParams* parameters)
{
if(!d4r_wire_history_prepared_parameters_valid(parameters)) return CUDA_ERROR_INVALID_VALUE;
#ifdef D4R_MOTION_KERNELS
typedef int(__attribute__((sysv_abi)) * HIP_LOAD)(void**,const void*);
typedef int(__attribute__((sysv_abi)) * HIP_FUNCTION)(void**,void*,const char*);
typedef int(__attribute__((sysv_abi)) * HIP_LAUNCH)(void*,unsigned,unsigned,unsigned,
unsigned,unsigned,unsigned,unsigned,void*,void**,void**);
static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
static void *module,*function,*apply;
static HIP_LAUNCH launch;
static int initialized,result=CUDA_ERROR_NOT_SUPPORTED;
int status;
pthread_mutex_lock(&lock);
if(!initialized)
{
void* hip;
const void* image;
HIP_LOAD load;
HIP_FUNCTION get;
char architecture[32];
initialized=1;pthread_once(&load_once,load_zluda);
hip=dlopen("libamdhip64.so.7",RTLD_NOW|RTLD_LOCAL);
gpu_architecture(architecture,sizeof(architecture));image=d4r_motion_image(architecture);
load=hip ? (HIP_LOAD)dlsym(hip,"hipModuleLoadData") : NULL;
get=hip ? (HIP_FUNCTION)dlsym(hip,"hipModuleGetFunction") : NULL;
launch=hip ? (HIP_LAUNCH)dlsym(hip,"hipModuleLaunchKernel") : NULL;
if(image && load && get && launch && load(&module,image)==0 &&
get(&function,module,"d4r_wire_history_preclassified")==0 &&
get(&apply,module,"d4r_wire_history_apply")==0) result=CUDA_SUCCESS;
tracef("prepared thin wire history: %s (%s)",result==0 ? "GPU kernel ready" : "unavailable",architecture);
}
status=result;pthread_mutex_unlock(&lock);
if(status==CUDA_SUCCESS)
{
D4rWireHistoryPreparedParams copy=*parameters;
void* arguments[]={&copy};
status=launch(function,(copy.sparse.base.width+15)/16,(copy.sparse.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
if(status!=0) return status;
return launch(apply,(copy.sparse.base.width+15)/16,(copy.sparse.base.height+15)/16,1,256,1,1,0,
d4r_ngx_stream(NULL),arguments,NULL);
}
return status;
#else
return CUDA_ERROR_NOT_SUPPORTED;
#endif
}
static void tracef(const char* format, ...)
{
char line[2048];
+16
View File
@@ -125,3 +125,19 @@
@ stdcall d4rNgxTimelineDestroy(ptr) d4rNgxTimelineDestroy
@ stdcall d4rNgxTimelineWait(ptr) d4rNgxTimelineWait
@ stdcall d4rDilateMotionVectors(ptr) d4rDilateMotionVectors
@ stdcall d4rDilateMotionVectorsPadded(ptr long) d4rDilateMotionVectorsPadded
@ stdcall d4rDilateMotionVectorsThin(ptr long) d4rDilateMotionVectorsThin
@ stdcall d4rComposeNativeMotion(ptr) d4rComposeNativeMotion
@ stdcall d4rComposeNativeMotionThin(ptr) d4rComposeNativeMotionThin
@ stdcall d4rStabilizeThinWires(ptr) d4rStabilizeThinWires
@ stdcall d4rStabilizeThinWiresSparse(ptr) d4rStabilizeThinWiresSparse
@ stdcall d4rComposeNativeMotionAligned(ptr) d4rComposeNativeMotionAligned
@ stdcall d4rComposeNativeMotionHybrid(ptr) d4rComposeNativeMotionHybrid
@ stdcall d4rComposeNativeMotionPrepared(ptr) d4rComposeNativeMotionPrepared
@ stdcall d4rStabilizeThinWiresPrepared(ptr) d4rStabilizeThinWiresPrepared