network natively on HIP with Vulkan->HIP shared memory, extend K kernels to
gfx11 (RDNA3), and add dependency/packaging/test tooling around them.
Windows: FSR RR backend, native HIP K runner, gfx11 K kernels, tooling
FSR Ray Regeneration (experimental)
- Add fsr_rr_backend.{h,cpp}, fsr_rr_passes.h and the fsr_rr/ prepare and
compose compute shaders. They run AMD's FidelityFX denoiser
(amd_fidelityfx_denoiser_dx12.dll, SDK 2.3, not bundled) on a private
vkd3d-proton device.
- Add the amdxc64 shim (tools/windows/amdxc64_shim) and two vkd3d-proton
patches for libcxx includes. AMD's amdxc64.dll is never loaded.
- d4r_nvngx_windows: RrFeature now selects between the native DLSS RR
backend and the FSR backend (D4R_RR_BACKEND=fsr). After the denoiser it
runs an internal Super Sampling stage (feature 1) to upscale. If SR is
unavailable and output != render size, the frame fails; it is never
returned noisy.
- d4r.ini / player_config: new [RayReconstruction] FsrDenoiser,
FsrStabilityBias (auto or >= 0) and FsrDetail (0-1), with validation, and
matching D4R_FSR_* environment variables.
- Add the FSR RR fixture, output-check and ngx test scripts, and a shader
compile script.
Native HIP K runner (Windows)
- CMake: add d4r_engine_k_hip (d4r-k-hip) plus separate volk/engine copies,
because VK_USE_PLATFORM_WIN32_KHR changes volk's entry points.
- Share Vulkan memory with HIP via Win32 opaque handles on Windows
(VK_KHR_external_memory_win32) and via fds on Linux. Handle types are now
intptr_t, and the platform choice is in D4R_EXT_MEMORY_* macros.
- HipNetwork: add debugRepeatHashes and debugNodeMicros, exposed as
D4R_HIP_REPEAT_CHECK, D4R_HIP_REPEAT_ALL and D4R_HIP_NODE_PROFILE in
replay.cpp.
- replay.cpp: display-resolution-motion frames (FrameFlagDisplayMotion) no
longer require near2rnd == out2rnd. Both values must still be in (0, 1].
K kernels on gfx11
- Allow position-token and enc1 tiling on __GFX11__ as well as gfx12.
- Add an opt-in ds_bpermute cross-half exchange
(D4R_WINDOWS_GFX11_DS_EXCHANGE, enabled for K on Windows gfx11). M keeps
its LDS path.
- Fix a block-barrier race in the position layer. Without POS_VSHARE a fast
wave could overwrite its hb rows while a slower wave still read them for a
later head, giving rare wrong results on gfx1101 (enc0 outputs differing
between identical launches).
Packaging and install
- install.bat, d4r.ini, WINDOWS_README: FSR option, and
install_dependencies.py to install pinned NVIDIA dependencies with staged,
hash-verified replacement and rollback.
- Update the release package, local package refresh and test scripts.
- Add the InteropBatchInputCopies option (default false). Batching is faster
on some synthetic runs, but player temporal exactness is unresolved.
Tooling and tests
- K model: capture_k_weights, build_k_model, compare_k_outputs,
benchmark_k_hip, build_capture_runtime, test_k_model and the
zluda-k-output-capture patch. Captured weights and models are private
local assets and must not enter release archives.
- Synthetic probes: test_m_ngx, test_sr_dynamic,
prepare_rr_dynamic_fixture and the d3d12/rr_ngx probe updates.
- Add tests/windows/test_install_dependencies.py.
Docs
- Update the windows-fsr-rr, k-performance, local-package, rr-interop and
rr-model-cache docs. Add windows-parity-handoff.md and
windows-parity-continuation-prompt.md.
932 lines
67 KiB
C++
932 lines
67 KiB
C++
// FSR Ray Regeneration as the native Windows Ray Reconstruction backend (experimental). See fsr_rr_backend.h.
|
|
//
|
|
// Data path of one evaluation (every hand-off is CPU-synchronized; no vkd3d-proton patch is needed):
|
|
// game queue : guides -> shared packet (D3D12 copies, as the native DLSS backend), signal, CPU waits
|
|
// vkd3d queue: one command list on a private vkd3d-proton device:
|
|
// packet -> staging images, fsr_rr_prepare (Vulkan, recorded through vkd3d's interop),
|
|
// AMD ffxDispatch, fsr_rr_compose (Vulkan), composed image -> packet output; CPU waits
|
|
// game queue : packet output -> the game's output texture, signal, CPU waits
|
|
// The conversion and composition follow the Linux adapter (reference 34e6b0fe, tools/d4r_fsr_rr_backend.inl).
|
|
#include "fsr_rr_backend.h"
|
|
#include "../d4r_fsr_rr.h"
|
|
#include "../d4r_vkd3d_interop.h"
|
|
#include "fsr_rr_passes.h"
|
|
#include "d3d12_command_hooks.h"
|
|
#include <volk.h>
|
|
#include "rr_bridge_replay.h"
|
|
#include <vulkan/vulkan_win32.h>
|
|
#include <dxgi1_4.h>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <mutex>
|
|
#include <tuple>
|
|
#include <vector>
|
|
#include <string>
|
|
|
|
namespace d4r::win::rr {
|
|
namespace {
|
|
using Clock = std::chrono::steady_clock;
|
|
double since(Clock::time_point start) { return std::chrono::duration<double, std::milli>(Clock::now() - start).count(); }
|
|
bool log_enabled() { static const bool on = std::getenv("D4R_FSR_RR_LOG") != nullptr; return on; }
|
|
LONG WINAPI crash_report(EXCEPTION_POINTERS* info) {
|
|
const auto code = info->ExceptionRecord->ExceptionCode;
|
|
if (code != 0xC0000005 && code != 0xC000001D && code != 0xC0000094 && code != 0xC00000FD) return EXCEPTION_CONTINUE_SEARCH;
|
|
char name[MAX_PATH] = "?"; HMODULE module = nullptr;
|
|
const void* at = info->ExceptionRecord->ExceptionAddress;
|
|
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, static_cast<const char*>(at), &module))
|
|
GetModuleFileNameA(module, name, MAX_PATH);
|
|
std::fprintf(stderr, "FSR_RR crash: code=%08lx at %p (%s +0x%llx) info0=%llu info1=%p\n", code, at, name,
|
|
(unsigned long long)(static_cast<const char*>(at) - reinterpret_cast<const char*>(module)),
|
|
(unsigned long long)info->ExceptionRecord->ExceptionInformation[0], (void*)info->ExceptionRecord->ExceptionInformation[1]);
|
|
std::fflush(stderr);
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
// The denoiser finds amdxc64.dll with GetModuleHandle, so in a process whose game device already loaded AMD's real
|
|
// amdxc64.dll (it always has: AMD's D3D12 driver does), the name resolves to AMD's driver component, which crashes
|
|
// on a vkd3d device. The loaded copy of AMD's DLL gets its GetModuleHandleA/W imports redirected in memory so that
|
|
// the name resolves to d4r's shim; every other lookup is forwarded. The DLL on disk is never modified.
|
|
HMODULE g_shimModule = nullptr;
|
|
HMODULE WINAPI module_handle_a(LPCSTR name) {
|
|
if (name && g_shimModule && !_stricmp(name, "amdxc64.dll")) return g_shimModule;
|
|
return GetModuleHandleA(name);
|
|
}
|
|
HMODULE WINAPI module_handle_w(LPCWSTR name) {
|
|
if (name && g_shimModule && !_wcsicmp(name, L"amdxc64.dll")) return g_shimModule;
|
|
return GetModuleHandleW(name);
|
|
}
|
|
bool redirect_amdxc(HMODULE module, HMODULE shim) {
|
|
g_shimModule = shim;
|
|
auto* base = reinterpret_cast<unsigned char*>(module);
|
|
auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(base);
|
|
auto* nt = reinterpret_cast<IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
|
const auto& directory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
|
|
if (!directory.VirtualAddress) return false;
|
|
unsigned patched = 0;
|
|
for (auto* descriptor = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(base + directory.VirtualAddress); descriptor->Name; ++descriptor) {
|
|
if (_stricmp(reinterpret_cast<const char*>(base + descriptor->Name), "KERNEL32.dll") != 0) continue;
|
|
auto* lookup = reinterpret_cast<IMAGE_THUNK_DATA*>(base + (descriptor->OriginalFirstThunk ? descriptor->OriginalFirstThunk : descriptor->FirstThunk));
|
|
auto* address = reinterpret_cast<void**>(base + descriptor->FirstThunk);
|
|
for (; lookup->u1.AddressOfData; ++lookup, ++address) {
|
|
if (lookup->u1.Ordinal & IMAGE_ORDINAL_FLAG) continue;
|
|
const char* function = reinterpret_cast<IMAGE_IMPORT_BY_NAME*>(base + lookup->u1.AddressOfData)->Name;
|
|
void* replacement = !std::strcmp(function, "GetModuleHandleA") ? reinterpret_cast<void*>(&module_handle_a)
|
|
: !std::strcmp(function, "GetModuleHandleW") ? reinterpret_cast<void*>(&module_handle_w) : nullptr;
|
|
if (!replacement) continue;
|
|
DWORD old = 0;
|
|
if (!VirtualProtect(address, sizeof(void*), PAGE_READWRITE, &old)) return false;
|
|
*address = replacement; ++patched;
|
|
VirtualProtect(address, sizeof(void*), old, &old);
|
|
}
|
|
}
|
|
return patched != 0;
|
|
}
|
|
void trace(const char* what) { if (log_enabled()) { std::fprintf(stderr, "FSR_RR step: %s\n", what); std::fflush(stderr); } }
|
|
std::wstring widen(const char* text) {
|
|
if (!text || !*text) return {};
|
|
const int n = MultiByteToWideChar(CP_UTF8, 0, text, -1, nullptr, 0);
|
|
std::wstring out(size_t(n > 0 ? n - 1 : 0), L'\0');
|
|
if (n > 1) MultiByteToWideChar(CP_UTF8, 0, text, -1, out.data(), n);
|
|
return out;
|
|
}
|
|
std::wstring env_wide(const char* name) { return widen(std::getenv(name)); }
|
|
std::string narrow(const std::wstring& text) {
|
|
std::string out(size_t(WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, nullptr, 0, nullptr, nullptr)), '\0');
|
|
if (!out.empty()) { WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, out.data(), int(out.size()), nullptr, nullptr); out.pop_back(); }
|
|
return out;
|
|
}
|
|
// The vkd3d-proton core interface (include/vkd3d_core_interface.idl). The IDL declares no base interface, so the
|
|
// vtable starts at CreateDevice: slot 0, not 3.
|
|
const GUID CLSID_Core = {0xed53efad, 0xda21, 0x4d96, {0xa1, 0xbc, 0xe7, 0x34, 0xe0, 0x78, 0x87, 0x9c}};
|
|
const GUID IID_CoreInterface = {0x18da885c, 0xa7b5, 0x464e, {0xa1, 0x21, 0xcc, 0xb7, 0x5d, 0x4d, 0xfc, 0x31}};
|
|
struct CoreInterface {
|
|
struct Vtbl {
|
|
HRESULT(STDMETHODCALLTYPE* createDevice)(CoreInterface*, IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
|
|
};
|
|
const Vtbl* vtbl;
|
|
};
|
|
// Row-major, row-vector 4x4 helpers (the NGX/FFX convention), in double. From the Linux adapter.
|
|
bool invert(const float (&m)[16], double (&out)[16]) {
|
|
double a[16]; for (int i = 0; i < 16; ++i) a[i] = m[i];
|
|
double inv[16];
|
|
inv[0] = a[5] * a[10] * a[15] - a[5] * a[11] * a[14] - a[9] * a[6] * a[15] + a[9] * a[7] * a[14] + a[13] * a[6] * a[11] - a[13] * a[7] * a[10];
|
|
inv[4] = -a[4] * a[10] * a[15] + a[4] * a[11] * a[14] + a[8] * a[6] * a[15] - a[8] * a[7] * a[14] - a[12] * a[6] * a[11] + a[12] * a[7] * a[10];
|
|
inv[8] = a[4] * a[9] * a[15] - a[4] * a[11] * a[13] - a[8] * a[5] * a[15] + a[8] * a[7] * a[13] + a[12] * a[5] * a[11] - a[12] * a[7] * a[9];
|
|
inv[12] = -a[4] * a[9] * a[14] + a[4] * a[10] * a[13] + a[8] * a[5] * a[14] - a[8] * a[6] * a[13] - a[12] * a[5] * a[10] + a[12] * a[6] * a[9];
|
|
inv[1] = -a[1] * a[10] * a[15] + a[1] * a[11] * a[14] + a[9] * a[2] * a[15] - a[9] * a[3] * a[14] - a[13] * a[2] * a[11] + a[13] * a[3] * a[10];
|
|
inv[5] = a[0] * a[10] * a[15] - a[0] * a[11] * a[14] - a[8] * a[2] * a[15] + a[8] * a[3] * a[14] + a[12] * a[2] * a[11] - a[12] * a[3] * a[10];
|
|
inv[9] = -a[0] * a[9] * a[15] + a[0] * a[11] * a[13] + a[8] * a[1] * a[15] - a[8] * a[3] * a[13] - a[12] * a[1] * a[11] + a[12] * a[3] * a[9];
|
|
inv[13] = a[0] * a[9] * a[14] - a[0] * a[10] * a[13] - a[8] * a[1] * a[14] + a[8] * a[2] * a[13] + a[12] * a[1] * a[10] - a[12] * a[2] * a[9];
|
|
inv[2] = a[1] * a[6] * a[15] - a[1] * a[7] * a[14] - a[5] * a[2] * a[15] + a[5] * a[3] * a[14] + a[13] * a[2] * a[7] - a[13] * a[3] * a[6];
|
|
inv[6] = -a[0] * a[6] * a[15] + a[0] * a[7] * a[14] + a[4] * a[2] * a[15] - a[4] * a[3] * a[14] - a[12] * a[2] * a[7] + a[12] * a[3] * a[6];
|
|
inv[10] = a[0] * a[5] * a[15] - a[0] * a[7] * a[13] - a[4] * a[1] * a[15] + a[4] * a[3] * a[13] + a[12] * a[1] * a[7] - a[12] * a[3] * a[5];
|
|
inv[14] = -a[0] * a[5] * a[14] + a[0] * a[6] * a[13] + a[4] * a[1] * a[14] - a[4] * a[2] * a[13] - a[12] * a[1] * a[6] + a[12] * a[2] * a[5];
|
|
inv[3] = -a[1] * a[6] * a[11] + a[1] * a[7] * a[10] + a[5] * a[2] * a[11] - a[5] * a[3] * a[10] - a[9] * a[2] * a[7] + a[9] * a[3] * a[6];
|
|
inv[7] = a[0] * a[6] * a[11] - a[0] * a[7] * a[10] - a[4] * a[2] * a[11] + a[4] * a[3] * a[10] + a[8] * a[2] * a[7] - a[8] * a[3] * a[6];
|
|
inv[11] = -a[0] * a[5] * a[11] + a[0] * a[7] * a[9] + a[4] * a[1] * a[11] - a[4] * a[3] * a[9] - a[8] * a[1] * a[7] + a[8] * a[3] * a[5];
|
|
inv[15] = a[0] * a[5] * a[10] - a[0] * a[6] * a[9] - a[4] * a[1] * a[10] + a[4] * a[2] * a[9] + a[8] * a[1] * a[6] - a[8] * a[2] * a[5];
|
|
const double det = a[0] * inv[0] + a[1] * inv[4] + a[2] * inv[8] + a[3] * inv[12];
|
|
if (!std::isfinite(det) || std::fabs(det) < 1e-30) return false;
|
|
for (int i = 0; i < 16; ++i) { out[i] = inv[i] / det; if (!std::isfinite(out[i])) return false; }
|
|
return true;
|
|
}
|
|
void multiply(const double (&a)[16], const double (&b)[16], double (&out)[16]) {
|
|
for (int r = 0; r < 4; ++r) for (int c = 0; c < 4; ++c) {
|
|
double sum = 0; for (int k = 0; k < 4; ++k) sum += a[r * 4 + k] * b[k * 4 + c]; out[r * 4 + c] = sum;
|
|
}
|
|
}
|
|
bool unorm(VkFormat format) {
|
|
return format == VK_FORMAT_R8G8B8A8_UNORM || format == VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
|
}
|
|
size_t resource_index(size_t plane) { return plane < 7 ? plane : plane + 2; }
|
|
// Same sampled-input formats as the native backend: the texel values reach Vulkan unchanged.
|
|
rr_bridge::Plane input_format(DXGI_FORMAT format, size_t plane) {
|
|
if (plane == Snapshot::Motion) {
|
|
if (format == DXGI_FORMAT_R16G16_FLOAT) return {nullptr, VK_FORMAT_R16G16_SFLOAT, 4};
|
|
if (format == DXGI_FORMAT_R32G32_FLOAT) return {nullptr, VK_FORMAT_R32G32_SFLOAT, 8};
|
|
} else if (plane == Snapshot::Depth || plane == Snapshot::HitDistance || plane == Snapshot::Disocclusion || plane == Snapshot::Subsurface) {
|
|
if (plane == Snapshot::Depth && (format == DXGI_FORMAT_R32_TYPELESS || format == DXGI_FORMAT_D32_FLOAT ||
|
|
format == DXGI_FORMAT_R32G8X24_TYPELESS || format == DXGI_FORMAT_D32_FLOAT_S8X24_UINT || format == DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS))
|
|
return {nullptr, VK_FORMAT_R32_SFLOAT, 4};
|
|
if (format == DXGI_FORMAT_R32_FLOAT) return {nullptr, VK_FORMAT_R32_SFLOAT, 4};
|
|
if (format == DXGI_FORMAT_R16_FLOAT) return {nullptr, VK_FORMAT_R16_SFLOAT, 2};
|
|
if (plane != Snapshot::Depth && format == DXGI_FORMAT_R8_UNORM) return {nullptr, VK_FORMAT_R8_UNORM, 1};
|
|
} else {
|
|
if (format == DXGI_FORMAT_R16G16B16A16_FLOAT) return {nullptr, VK_FORMAT_R16G16B16A16_SFLOAT, 8};
|
|
if (format == DXGI_FORMAT_R32G32B32A32_FLOAT) return {nullptr, VK_FORMAT_R32G32B32A32_SFLOAT, 16};
|
|
if (format == DXGI_FORMAT_R8G8B8A8_UNORM) return {nullptr, VK_FORMAT_R8G8B8A8_UNORM, 4};
|
|
if (format == DXGI_FORMAT_R10G10B10A2_UNORM) return {nullptr, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4};
|
|
if (plane == Snapshot::Roughness) {
|
|
if (format == DXGI_FORMAT_R16_FLOAT) return {nullptr, VK_FORMAT_R16_SFLOAT, 2};
|
|
if (format == DXGI_FORMAT_R32_FLOAT) return {nullptr, VK_FORMAT_R32_SFLOAT, 4};
|
|
if (format == DXGI_FORMAT_R8_UNORM) return {nullptr, VK_FORMAT_R8_UNORM, 1};
|
|
}
|
|
}
|
|
throw std::runtime_error("Unsupported FSR RR sampled input format");
|
|
}
|
|
DXGI_FORMAT copy_format(ID3D12Device* device, ID3D12Resource* texture, size_t plane) {
|
|
const auto desc = resource_desc(texture);
|
|
if (plane != Snapshot::Depth) return desc.Format;
|
|
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{}; UINT rows = 0; UINT64 rowBytes = 0, bytes = 0;
|
|
device->GetCopyableFootprints(&desc, 0, 1, 0, &footprint, &rows, &rowBytes, &bytes);
|
|
const auto format = input_format(desc.Format, plane);
|
|
if (!bytes || bytes == UINT64_MAX || rows != desc.Height || rowBytes != desc.Width * format.bytes ||
|
|
footprint.Footprint.Width != desc.Width || footprint.Footprint.Height != desc.Height)
|
|
throw std::runtime_error("Unsupported FSR RR depth-plane footprint");
|
|
return footprint.Footprint.Format;
|
|
}
|
|
// The Vulkan entry points used on vkd3d-proton's VkDevice. Loaded from the loader by instance/device, never through
|
|
// volk's globals, which belong to the native backend's own devices.
|
|
struct VkApi {
|
|
PFN_vkGetDeviceProcAddr deviceProc = nullptr;
|
|
PFN_vkGetPhysicalDeviceMemoryProperties memoryProperties = nullptr;
|
|
PFN_vkGetPhysicalDeviceFormatProperties formatProperties = nullptr;
|
|
PFN_vkCreateBuffer createBuffer = nullptr; PFN_vkDestroyBuffer destroyBuffer = nullptr;
|
|
PFN_vkGetBufferMemoryRequirements bufferRequirements = nullptr;
|
|
PFN_vkAllocateMemory allocate = nullptr; PFN_vkFreeMemory free = nullptr;
|
|
PFN_vkBindBufferMemory bindBuffer = nullptr;
|
|
PFN_vkCreateImage createImage = nullptr; PFN_vkDestroyImage destroyImage = nullptr;
|
|
PFN_vkGetImageMemoryRequirements imageRequirements = nullptr; PFN_vkBindImageMemory bindImage = nullptr;
|
|
PFN_vkCmdCopyBufferToImage copyBufferToImage = nullptr; PFN_vkCmdCopyImageToBuffer copyImageToBuffer = nullptr;
|
|
PFN_vkCmdBlitImage blitImage = nullptr;
|
|
PFN_vkCmdPipelineBarrier barrier = nullptr;
|
|
PFN_vkGetMemoryWin32HandlePropertiesKHR handleProperties = nullptr;
|
|
PFN_vkDeviceWaitIdle waitIdle = nullptr;
|
|
};
|
|
struct StagingImage {
|
|
VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
};
|
|
struct Intermediate {
|
|
ComPtr<ID3D12Resource> resource; D3D12_RESOURCE_DESC desc{};
|
|
D3D12_RESOURCE_STATES state = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
|
|
VkImageView view = VK_NULL_HANDLE; VkImageLayout layout = VK_IMAGE_LAYOUT_GENERAL; VkImage image = VK_NULL_HANDLE;
|
|
};
|
|
enum Tex { LinearDepth, MotionT, NormalsT, DiffuseAlbedo, SpecularAlbedo, DiffuseT, SpecularT, DiffuseOut, SpecularOut, Residual, Composed, Widened, TexCount };
|
|
std::mutex hostMutex;
|
|
}
|
|
|
|
FsrConfig FsrConfig::from_environment() {
|
|
FsrConfig config;
|
|
config.vkd3dCore = env_wide("D4R_FSR_VKD3D_CORE");
|
|
config.denoiserDll = env_wide("D4R_FSR_RR_DLL");
|
|
config.amdxcShim = env_wide("D4R_FSR_AMDXC_SHIM");
|
|
config.cacheDir = env_wide("D4R_FSR_CACHE_DIR");
|
|
return config;
|
|
}
|
|
|
|
struct FsrBackend::Impl {
|
|
Creation creation;
|
|
rr_bridge::Layout layout;
|
|
ComPtr<ID3D12Device> d3d; // the game's device
|
|
ComPtr<ID3D12CommandQueue> queue;
|
|
ComPtr<ID3D12Resource> shared;
|
|
ComPtr<ID3D12Fence> fence;
|
|
ComPtr<ID3D12CommandAllocator> producerAllocator, consumerAllocator;
|
|
ComPtr<ID3D12GraphicsCommandList> producer, consumer;
|
|
Handle event;
|
|
uint64_t sequence = 0;
|
|
bool pending = false, failed = false;
|
|
// --- private vkd3d-proton side ---
|
|
HMODULE core = nullptr;
|
|
ComPtr<ID3D12Device> vdev;
|
|
ComPtr<ID3D12CommandQueue> vqueue;
|
|
ComPtr<ID3D12CommandAllocator> vallocator;
|
|
ComPtr<ID3D12GraphicsCommandList> vlist;
|
|
ComPtr<ID3D12Fence> vfence; Handle vevent; uint64_t vsequence = 0;
|
|
ComPtr<ID3D12DXVKInteropDevice1> interop;
|
|
VkInstance vkInstance = VK_NULL_HANDLE; VkPhysicalDevice vkPhysical = VK_NULL_HANDLE; VkDevice vk = VK_NULL_HANDLE;
|
|
VkQueue vkQueue = VK_NULL_HANDLE; uint32_t vkFamily = 0;
|
|
VkApi api;
|
|
D4rFsrRrPasses passes;
|
|
FsrRrRuntime runtime;
|
|
ffxContext context = nullptr; uint32_t signals = 0;
|
|
VkBuffer packetBuffer = VK_NULL_HANDLE; VkDeviceMemory packetMemory = VK_NULL_HANDLE;
|
|
std::array<StagingImage, 11> staging{};
|
|
std::array<Intermediate, TexCount> textures{};
|
|
float previousView[16] = {}; bool havePrevious = false;
|
|
uint32_t frameIndex = 0;
|
|
FsrConfig config;
|
|
HMODULE shimModule = nullptr;
|
|
FsrBackend::SuperSampling superSampling;
|
|
std::array<unsigned, 4> srRenderLimits{}; // min width/height, max width/height from the SR library
|
|
// D4R_FSR_RR_LOG: GPU timestamps of the vkd3d list (begin, after prepare, after denoise, end)
|
|
ComPtr<ID3D12QueryHeap> stamps; ComPtr<ID3D12Resource> stampReadback; UINT64 stampFrequency = 0;
|
|
void stamp(UINT index) { if (stamps) vlist->EndQuery(stamps.Get(), D3D12_QUERY_TYPE_TIMESTAMP, index); }
|
|
ComPtr<ID3D12Resource> denoised; // game-device copy of the denoised colour, the Super Sampling stage's input
|
|
D3D12_RESOURCE_STATES denoisedState = D3D12_RESOURCE_STATE_COMMON;
|
|
|
|
Impl(ID3D12Device* device, const Creation& c, const FsrConfig& cfg)
|
|
: creation(c), layout(c.width, c.height, c.outWidth, c.outHeight), d3d(device), config(cfg) {
|
|
// Reserve each guide's largest texel size once, as the native backend does, so format changes keep the packet.
|
|
VkDeviceSize offset = 0;
|
|
for (auto& plane : layout.planes) {
|
|
plane.offset = (offset + 511) & ~VkDeviceSize(511);
|
|
plane.pitch = (VkDeviceSize(layout.rw) * 16 + 255) & ~VkDeviceSize(255);
|
|
offset = plane.offset + plane.pitch * layout.rh;
|
|
}
|
|
layout.outputOffset = (offset + 511) & ~VkDeviceSize(511);
|
|
layout.outputPitch = (VkDeviceSize(layout.ow) * 16 + 255) & ~VkDeviceSize(255);
|
|
layout.bytes = layout.outputOffset + layout.outputPitch * layout.oh;
|
|
}
|
|
~Impl() {
|
|
if (vk && api.waitIdle) (void)api.waitIdle(vk);
|
|
if (vqueue && vfence) {
|
|
const uint64_t value = ++vsequence;
|
|
if (SUCCEEDED(vqueue->Signal(vfence.Get(), value)) && SUCCEEDED(vfence->SetEventOnCompletion(value, vevent.value)))
|
|
WaitForSingleObject(vevent.value, 10000);
|
|
}
|
|
if (context) runtime.destroyContext(&context, nullptr);
|
|
if (vk) {
|
|
for (auto& s : staging) destroy_staging(s);
|
|
for (auto& t : textures) if (t.view) passes.destroyView(vk, t.view, nullptr);
|
|
passes.destroy();
|
|
if (packetBuffer) api.destroyBuffer(vk, packetBuffer, nullptr);
|
|
if (packetMemory) api.free(vk, packetMemory, nullptr);
|
|
}
|
|
// vkd3d-proton's DLL stays loaded for the process: its device objects are released by their ComPtrs.
|
|
}
|
|
void destroy_staging(StagingImage& s) {
|
|
if (s.view) passes.destroyView(vk, s.view, nullptr);
|
|
if (s.image) api.destroyImage(vk, s.image, nullptr);
|
|
if (s.memory) api.free(vk, s.memory, nullptr);
|
|
s = StagingImage{};
|
|
}
|
|
uint32_t memory_type(uint32_t bits, VkMemoryPropertyFlags flags) {
|
|
VkPhysicalDeviceMemoryProperties properties{}; api.memoryProperties(vkPhysical, &properties);
|
|
for (uint32_t i = 0; i < properties.memoryTypeCount; ++i)
|
|
if ((bits & (1u << i)) && (properties.memoryTypes[i].propertyFlags & flags) == flags) return i;
|
|
throw std::runtime_error("No compatible FSR RR Vulkan memory type");
|
|
}
|
|
void check(VkResult r, const char* what) {
|
|
if (r != VK_SUCCESS) throw std::runtime_error(std::string(what) + " VkResult=" + std::to_string(r));
|
|
}
|
|
|
|
void initialize() {
|
|
if (!d3d) throw std::runtime_error("Null FSR RR D3D12 device");
|
|
if ((creation.flags & 3u) != 3u) throw std::runtime_error("FSR RR requires HDR and render-resolution motion vectors");
|
|
if (creation.flags & 4u) throw std::runtime_error("FSR RR does not convert jittered motion vectors");
|
|
if (creation.quality < 0 || creation.quality > 5) throw std::runtime_error("Unsupported RR quality mode");
|
|
if (config.vkd3dCore.empty() || config.denoiserDll.empty() || config.amdxcShim.empty())
|
|
throw std::runtime_error("FSR RR needs D4R_FSR_VKD3D_CORE, D4R_FSR_RR_DLL and D4R_FSR_AMDXC_SHIM");
|
|
// --- the game-device packet, fence and copy lists (as the native backend) ---
|
|
shared = make_buffer(d3d.Get(), layout.bytes, D3D12_HEAP_TYPE_DEFAULT, D3D12_HEAP_FLAG_SHARED, D3D12_RESOURCE_STATE_COMMON);
|
|
dx(d3d->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)), "FSR RR fence");
|
|
event.value = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
|
vevent.value = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
|
if (!event.value || !vevent.value) throw std::runtime_error("FSR RR event creation failed");
|
|
auto createList = [&](ID3D12Device* device, auto& allocator, auto& list) {
|
|
dx(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)), "FSR RR allocator");
|
|
dx(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(), nullptr, IID_PPV_ARGS(&list)), "FSR RR command list");
|
|
dx(list->Close(), "FSR RR initial close");
|
|
};
|
|
createList(d3d.Get(), producerAllocator, producer); createList(d3d.Get(), consumerAllocator, consumer);
|
|
Handle memoryHandle;
|
|
dx(d3d->CreateSharedHandle(shared.Get(), nullptr, GENERIC_ALL, nullptr, &memoryHandle.value), "FSR RR packet handle");
|
|
if (log_enabled()) AddVectoredExceptionHandler(1, crash_report);
|
|
trace("before start_vkd3d");
|
|
start_vkd3d();
|
|
trace("before import_packet");
|
|
import_packet(memoryHandle.value);
|
|
trace("before vkd3d list");
|
|
createList(vdev.Get(), vallocator, vlist);
|
|
trace("before create_intermediates");
|
|
create_intermediates();
|
|
trace("before create_context");
|
|
create_context();
|
|
}
|
|
|
|
void start_vkd3d() {
|
|
// vkd3d-proton's own environment: the dxil-spirv RDNA3 WMMA/FP8 emulation, and a cache for its shaders.
|
|
SetEnvironmentVariableA("DXIL_SPIRV_CONFIG", "wmma_rdna3_workaround");
|
|
if (!config.cacheDir.empty() && !std::getenv("VKD3D_SHADER_CACHE_PATH")) {
|
|
CreateDirectoryW(config.cacheDir.c_str(), nullptr);
|
|
SetEnvironmentVariableW(L"VKD3D_SHADER_CACHE_PATH", config.cacheDir.c_str());
|
|
}
|
|
// The denoiser consults amdxc64.dll only when it is already loaded: ours, never AMD's.
|
|
shimModule = LoadLibraryW(config.amdxcShim.c_str());
|
|
if (!shimModule) throw std::runtime_error("FSR RR amdxc64 shim could not be loaded");
|
|
core = LoadLibraryExW(config.vkd3dCore.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
|
|
if (!core) throw std::runtime_error("FSR RR vkd3d-proton core could not be loaded");
|
|
using GetInterface = HRESULT(WINAPI*)(REFCLSID, REFIID, void**);
|
|
auto getInterface = reinterpret_cast<GetInterface>(reinterpret_cast<void*>(GetProcAddress(core, "D3D12GetInterface")));
|
|
CoreInterface* iface = nullptr;
|
|
if (!getInterface || FAILED(getInterface(CLSID_Core, IID_CoreInterface, reinterpret_cast<void**>(&iface))) || !iface)
|
|
throw std::runtime_error("FSR RR vkd3d-proton core interface unavailable");
|
|
ComPtr<IDXGIFactory4> factory; dx(CreateDXGIFactory1(IID_PPV_ARGS(&factory)), "FSR RR DXGI factory");
|
|
ComPtr<IDXGIAdapter1> adapter; dx(factory->EnumAdapterByLuid(adapter_luid(d3d.Get()), IID_PPV_ARGS(&adapter)), "FSR RR adapter");
|
|
dx(iface->vtbl->createDevice(iface, adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&vdev)), "FSR RR vkd3d device");
|
|
trace("vkd3d device created");
|
|
D3D12_COMMAND_QUEUE_DESC queueDesc{}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
|
dx(vdev->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&vqueue)), "FSR RR vkd3d queue");
|
|
dx(vdev->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&vfence)), "FSR RR vkd3d fence");
|
|
if (log_enabled()) {
|
|
D3D12_QUERY_HEAP_DESC heap{}; heap.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP; heap.Count = 4;
|
|
if (SUCCEEDED(vdev->CreateQueryHeap(&heap, IID_PPV_ARGS(&stamps))) && SUCCEEDED(vqueue->GetTimestampFrequency(&stampFrequency)) && stampFrequency)
|
|
stampReadback = make_buffer(vdev.Get(), 4 * sizeof(UINT64), D3D12_HEAP_TYPE_READBACK, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST);
|
|
else stamps.Reset();
|
|
}
|
|
trace("vkd3d queue and fence created");
|
|
dx(vdev->QueryInterface(IID_PPV_ARGS(&interop)), "FSR RR vkd3d Vulkan interop");
|
|
dx(interop->GetVulkanHandles(&vkInstance, &vkPhysical, &vk), "FSR RR Vulkan handles");
|
|
dx(interop->GetVulkanQueueInfo(vqueue.Get(), &vkQueue, &vkFamily), "FSR RR Vulkan queue info");
|
|
trace("interop and handles ok");
|
|
HMODULE vulkan = GetModuleHandleA("vulkan-1.dll"); if (!vulkan) vulkan = LoadLibraryA("vulkan-1.dll");
|
|
auto instanceProc = vulkan ? reinterpret_cast<PFN_vkGetInstanceProcAddr>(reinterpret_cast<void*>(GetProcAddress(vulkan, "vkGetInstanceProcAddr"))) : nullptr;
|
|
if (!instanceProc) throw std::runtime_error("FSR RR Vulkan loader unavailable");
|
|
api.deviceProc = reinterpret_cast<PFN_vkGetDeviceProcAddr>(instanceProc(vkInstance, "vkGetDeviceProcAddr"));
|
|
api.memoryProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(instanceProc(vkInstance, "vkGetPhysicalDeviceMemoryProperties"));
|
|
api.formatProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceFormatProperties>(instanceProc(vkInstance, "vkGetPhysicalDeviceFormatProperties"));
|
|
auto physicalProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties>(instanceProc(vkInstance, "vkGetPhysicalDeviceProperties"));
|
|
if (!api.deviceProc || !api.memoryProperties || !api.formatProperties || !physicalProperties) throw std::runtime_error("FSR RR Vulkan entry points missing");
|
|
#define D4R_VK(member, name) api.member = reinterpret_cast<decltype(api.member)>(api.deviceProc(vk, "vk" #name)); \
|
|
if (!api.member) throw std::runtime_error("FSR RR Vulkan entry point missing: vk" #name)
|
|
D4R_VK(createBuffer, CreateBuffer); D4R_VK(destroyBuffer, DestroyBuffer); D4R_VK(bufferRequirements, GetBufferMemoryRequirements);
|
|
D4R_VK(allocate, AllocateMemory); D4R_VK(free, FreeMemory); D4R_VK(bindBuffer, BindBufferMemory);
|
|
D4R_VK(createImage, CreateImage); D4R_VK(destroyImage, DestroyImage); D4R_VK(imageRequirements, GetImageMemoryRequirements);
|
|
D4R_VK(bindImage, BindImageMemory); D4R_VK(copyBufferToImage, CmdCopyBufferToImage); D4R_VK(copyImageToBuffer, CmdCopyImageToBuffer);
|
|
D4R_VK(blitImage, CmdBlitImage);
|
|
D4R_VK(barrier, CmdPipelineBarrier); D4R_VK(handleProperties, GetMemoryWin32HandlePropertiesKHR); D4R_VK(waitIdle, DeviceWaitIdle);
|
|
#undef D4R_VK
|
|
trace("vulkan entry points loaded");
|
|
VkPhysicalDeviceProperties limits{}; physicalProperties(vkPhysical, &limits);
|
|
if (!passes.init(vk, api.deviceProc, limits.limits.maxPushConstantsSize))
|
|
throw std::runtime_error("FSR RR conversion passes could not be created");
|
|
trace("passes created");
|
|
if (!runtime.load(config.denoiserDll)) throw std::runtime_error(std::string("FSR RR denoiser DLL: ") + fsr_rr_stage_name(runtime.stage));
|
|
if (!redirect_amdxc(runtime.module, shimModule)) throw std::runtime_error("FSR RR could not redirect the denoiser's amdxc64 lookup");
|
|
trace("amdxc64 lookup redirected to the d4r shim");
|
|
trace("before message callback");
|
|
runtime.set_message_callback([](uint32_t type, const wchar_t* message) {
|
|
std::fprintf(stderr, "FSR RR runtime %s: %ls\n", type == FFX_API_MESSAGE_TYPE_WARNING ? "warning" : "error", message);
|
|
}, FFX_API_CONFIGURE_GLOBALDEBUG_LEVEL_WARNINGS);
|
|
trace("denoiser dll loaded");
|
|
trace("before query_providers");
|
|
if (!runtime.query_providers(vdev.Get()))
|
|
throw std::runtime_error(std::string("FSR RR provider: ") + fsr_rr_stage_name(runtime.stage));
|
|
trace("providers queried");
|
|
if (log_enabled()) std::fprintf(stderr, "FSR_RR provider \"%s\" on vkd3d-proton\n", runtime.providerName.c_str());
|
|
}
|
|
|
|
void import_packet(HANDLE resource) {
|
|
VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
|
|
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT;
|
|
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; bi.size = layout.bytes;
|
|
bi.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
|
check(api.createBuffer(vk, &bi, nullptr, &packetBuffer), "FSR RR packet buffer");
|
|
VkMemoryRequirements req{}; api.bufferRequirements(vk, packetBuffer, &req);
|
|
VkMemoryWin32HandlePropertiesKHR handleProperties{VK_STRUCTURE_TYPE_MEMORY_WIN32_HANDLE_PROPERTIES_KHR};
|
|
check(api.handleProperties(vk, VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT, resource, &handleProperties), "FSR RR packet memory types");
|
|
const uint32_t type = memory_type(req.memoryTypeBits & handleProperties.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
VkImportMemoryWin32HandleInfoKHR imported{VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR};
|
|
imported.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT; imported.handle = resource;
|
|
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, &imported};
|
|
dedicated.buffer = packetBuffer;
|
|
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &dedicated};
|
|
ai.allocationSize = req.size; ai.memoryTypeIndex = type;
|
|
check(api.allocate(vk, &ai, nullptr, &packetMemory), "FSR RR packet import");
|
|
check(api.bindBuffer(vk, packetBuffer, packetMemory, 0), "FSR RR packet bind");
|
|
}
|
|
|
|
void create_intermediate(int i) {
|
|
auto& t = textures[i];
|
|
t.desc = {}; t.desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
|
|
t.desc.Width = creation.width; t.desc.Height = creation.height; t.desc.DepthOrArraySize = 1; t.desc.MipLevels = 1;
|
|
t.desc.Format = i == LinearDepth ? DXGI_FORMAT_R32_FLOAT : i == Widened ? DXGI_FORMAT_R32G32B32A32_FLOAT : DXGI_FORMAT_R16G16B16A16_FLOAT;
|
|
t.desc.SampleDesc.Count = 1; t.desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
|
|
D3D12_HEAP_PROPERTIES heap{}; heap.Type = D3D12_HEAP_TYPE_DEFAULT;
|
|
dx(vdev->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &t.desc, t.state, nullptr, IID_PPV_ARGS(&t.resource)), "FSR RR intermediate texture");
|
|
UINT64 handle = 0, offset = 0; VkFormat format = VK_FORMAT_UNDEFINED;
|
|
dx(interop->GetVulkanResourceInfo1(t.resource.Get(), &handle, &offset, &format), "FSR RR intermediate Vulkan info");
|
|
if (!handle) throw std::runtime_error("FSR RR intermediate has no Vulkan image");
|
|
t.image = reinterpret_cast<VkImage>(handle);
|
|
interop->GetVulkanImageLayout(t.resource.Get(), D3D12_RESOURCE_STATE_UNORDERED_ACCESS, &t.layout);
|
|
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
info.image = t.image; info.viewType = VK_IMAGE_VIEW_TYPE_2D; info.format = format;
|
|
info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
check(passes.createView(vk, &info, nullptr, &t.view), "FSR RR intermediate view");
|
|
}
|
|
void create_intermediates() {
|
|
// The float output image is needed only for an FSR-only RGBA32F output.
|
|
for (int i = 0; i < Widened; ++i) create_intermediate(i);
|
|
}
|
|
|
|
// AMD's tweakable settings (FfxApiConfigureDenoiserKey 1-6). Defaults are logged; D4R_FSR_RR_KEY_<n>=<float>
|
|
// overrides key n (diagnostics) and D4R_FSR_RR_STABILITY_BIAS ([RayReconstruction] FsrStabilityBias) sets key 2.
|
|
// AMD's default stability bias of 1 weights history heavily, which blurs fine detail on some content.
|
|
void apply_denoiser_keys() {
|
|
static const char* const names[7] = {"", "cross bilateral normal strength", "stability bias", "max radiance",
|
|
"radiance clip std k", "gaussian kernel relaxation", "disocclusion threshold"};
|
|
std::string defaults;
|
|
for (uint64_t key = 1; key <= 6; ++key) {
|
|
float value = 0.0f;
|
|
ffxQueryDescDenoiserGetDefaultKeyValue query = {};
|
|
query.header.type = FFX_API_QUERY_DESC_TYPE_DENOISER_GET_DEFAULT_KEYVALUE;
|
|
query.key = key; query.count = 1; query.data = &value;
|
|
char text[96];
|
|
if (runtime.query(&context, &query.header) == FFX_API_RETURN_OK)
|
|
std::snprintf(text, sizeof(text), "%s%s %g", defaults.empty() ? "" : ", ", names[key], value);
|
|
else
|
|
std::snprintf(text, sizeof(text), "%s%s ?", defaults.empty() ? "" : ", ", names[key]);
|
|
defaults += text;
|
|
char variable[32];
|
|
std::snprintf(variable, sizeof(variable), "D4R_FSR_RR_KEY_%u", unsigned(key));
|
|
std::string override = std::getenv(variable) ? std::getenv(variable) : "";
|
|
if (override.empty() && key == 2 && std::getenv("D4R_FSR_RR_STABILITY_BIAS") && std::string(std::getenv("D4R_FSR_RR_STABILITY_BIAS")) != "auto")
|
|
override = std::getenv("D4R_FSR_RR_STABILITY_BIAS");
|
|
if (override.empty()) continue;
|
|
char* end = nullptr;
|
|
const float wanted = std::strtof(override.c_str(), &end);
|
|
if (end == override.c_str() || !std::isfinite(wanted)) {
|
|
std::fprintf(stderr, "FSR_RR: %s override '%s' is not a number; ignored\n", names[key], override.c_str());
|
|
continue;
|
|
}
|
|
ffxConfigureDescDenoiserKeyValue configure = {};
|
|
configure.header.type = FFX_API_CONFIGURE_DESC_TYPE_DENOISER_KEYVALUE;
|
|
configure.key = key; configure.count = 1; configure.data = &wanted;
|
|
const ffxReturnCode_t configured = runtime.configure(&context, &configure.header);
|
|
std::fprintf(stderr, "FSR_RR: %s set to %g (%s)\n", names[key], wanted, fsr_rr_return_name(configured));
|
|
}
|
|
if (log_enabled()) std::fprintf(stderr, "FSR_RR denoiser defaults: %s\n", defaults.c_str());
|
|
}
|
|
// [RayReconstruction] FsrDetail (D4R_FSR_RR_DETAIL, 0-1): the share of the colour that bypasses the denoiser through
|
|
// the residual; D4R_FSR_RR_DEMOD_FLOOR (default 0.1): the smallest albedo a signal is divided by; D4R_FSR_RR_HALO_LIMIT
|
|
// (default 1, 0 = off): caps each denoised lobe at that multiple of the brightest noisy input around the texel.
|
|
static float env_float(const char* name, float fallback, float low, float high) {
|
|
const char* text = std::getenv(name);
|
|
if (!text || !*text) return fallback;
|
|
char* end = nullptr; const float value = std::strtof(text, &end);
|
|
if (end == text || !std::isfinite(value) || value < low || value > high)
|
|
throw std::runtime_error(std::string(name) + " must be a number between " + std::to_string(low) + " and " + std::to_string(high));
|
|
return value;
|
|
}
|
|
|
|
void create_context() {
|
|
const auto start = Clock::now();
|
|
// Diffuse is a direct signal (the game's diffuse hit distance is not carried); specular is indirect only
|
|
// when the game passes a specular hit distance, which decides the context's signal set once.
|
|
signals = FFX_DENOISER_SIGNAL_DIRECT_DIFFUSE | FFX_DENOISER_SIGNAL_DIRECT_SPECULAR;
|
|
const ffxReturnCode_t created = fsr_rr_create_context(runtime, vdev.Get(), creation.width, creation.height, signals, 0, context);
|
|
if (created != FFX_API_RETURN_OK) throw std::runtime_error(std::string("FSR RR ffxCreateContext failed: ") + fsr_rr_return_name(created));
|
|
apply_denoiser_keys();
|
|
if (log_enabled()) std::fprintf(stderr, "FSR_RR context created in %.0f ms (%ux%u, signals 0x%x)\n", since(start), creation.width, creation.height, signals);
|
|
}
|
|
|
|
void validate_frame(const Snapshot& frame) {
|
|
if (failed) throw std::runtime_error("FSR RR backend is poisoned after failed submission");
|
|
if (frame.renderWidth < 64 || frame.renderHeight < 64 || frame.renderWidth > creation.width || frame.renderHeight > creation.height)
|
|
throw std::runtime_error("RR render extent must be at least 64 and within creation geometry");
|
|
// The exposure texture belongs to the Super Sampling stage (it receives it); the denoiser does not use it.
|
|
if (frame.resources[Snapshot::Exposure] && !superSampling) throw std::runtime_error("FSR RR exposure texture needs the Super Sampling stage");
|
|
// The old dynamic fixture shrank below NVIDIA's Quality minimum, triggering BAD00005 and a teardown fault.
|
|
// Query the SR library's actual range and reject unsupported sizes before recording GPU work.
|
|
if (superSampling && (frame.renderWidth < srRenderLimits[0] || frame.renderHeight < srRenderLimits[1] ||
|
|
frame.renderWidth > srRenderLimits[2] || frame.renderHeight > srRenderLimits[3]))
|
|
throw std::runtime_error("FSR RR render size is outside the Super Sampling dynamic render range");
|
|
if (frame.exposureScale != 1 || frame.depthType || frame.normalsType || frame.roughnessType || frame.diffuseType || frame.specularType)
|
|
throw std::runtime_error("FSR RR guide encoding or exposure scale requires conversion");
|
|
if (frame.resources[Snapshot::Disocclusion] || frame.resources[Snapshot::Subsurface])
|
|
throw std::runtime_error("FSR RR does not use the disocclusion or subsurface guides");
|
|
if (!superSampling && (frame.renderWidth != creation.outWidth || frame.renderHeight != creation.outHeight))
|
|
throw std::runtime_error("FSR RR upscaling needs a Super Sampling stage (none is installed) or output size == render size");
|
|
for (size_t i = 0; i < Snapshot::Count; ++i) {
|
|
if (i == Snapshot::Exposure) continue;
|
|
auto* resource = frame.resources[i].Get();
|
|
if (!resource && i >= Snapshot::HitDistance) continue;
|
|
if (!resource) throw std::runtime_error("Missing required FSR RR texture");
|
|
ComPtr<ID3D12Device> owner; dx(resource->GetDevice(IID_PPV_ARGS(&owner)), "FSR RR texture device");
|
|
if (owner.Get() != d3d.Get()) throw std::runtime_error("FSR RR texture belongs to another device");
|
|
const auto desc = resource_desc(resource);
|
|
if (i != Snapshot::Output) (void)input_format(desc.Format, i);
|
|
if (i == Snapshot::Depth) {
|
|
(void)copy_format(d3d.Get(), resource, i);
|
|
if ((desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) &&
|
|
(frame.origins[i][0] || frame.origins[i][1] || frame.renderWidth != desc.Width || frame.renderHeight != desc.Height))
|
|
throw std::runtime_error("FSR RR depth-stencil requires a full depth-plane rectangle");
|
|
}
|
|
const uint32_t width = i == Snapshot::Output ? layout.ow : frame.renderWidth;
|
|
const uint32_t height = i == Snapshot::Output ? layout.oh : frame.renderHeight;
|
|
if (desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D || (i == Snapshot::Output && !superSampling &&
|
|
desc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT && desc.Format != DXGI_FORMAT_R32G32B32A32_FLOAT) ||
|
|
desc.SampleDesc.Count != 1 || desc.DepthOrArraySize != 1 ||
|
|
frame.origins[i][0] > desc.Width || width > desc.Width - frame.origins[i][0] ||
|
|
frame.origins[i][1] > desc.Height || height > desc.Height - frame.origins[i][1])
|
|
throw std::runtime_error("Unsupported FSR RR texture format or subrect (FSR-only output must be RGBA16F or RGBA32F)");
|
|
if (i == Snapshot::Output && !superSampling && desc.Format == DXGI_FORMAT_R32G32B32A32_FLOAT) {
|
|
VkFormatProperties source{}, destination{};
|
|
api.formatProperties(vkPhysical, VK_FORMAT_R16G16B16A16_SFLOAT, &source);
|
|
api.formatProperties(vkPhysical, VK_FORMAT_R32G32B32A32_SFLOAT, &destination);
|
|
if (!(source.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) ||
|
|
!(destination.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT))
|
|
throw std::runtime_error("FSR RR output widening requires half-to-float blit support");
|
|
}
|
|
if (i == Snapshot::Output && !(desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS))
|
|
throw std::runtime_error("RR output lacks UAV capability");
|
|
}
|
|
for (size_t i = 0; i < Snapshot::Count; ++i)
|
|
if (i != Snapshot::Output && frame.resources[i] == frame.resources[Snapshot::Output]) throw std::runtime_error("RR input/output alias is unsupported");
|
|
}
|
|
|
|
void ensure_staging(size_t plane) {
|
|
auto& s = staging[plane]; const VkFormat wanted = layout.planes[plane].format;
|
|
if (s.image && s.format == wanted) return;
|
|
destroy_staging(s);
|
|
VkImageCreateInfo ci{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; ci.imageType = VK_IMAGE_TYPE_2D; ci.format = wanted;
|
|
ci.extent = {creation.width, creation.height, 1}; ci.mipLevels = ci.arrayLayers = 1; ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
ci.tiling = VK_IMAGE_TILING_OPTIMAL; ci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
check(api.createImage(vk, &ci, nullptr, &s.image), "FSR RR staging image");
|
|
VkMemoryRequirements req{}; api.imageRequirements(vk, s.image, &req);
|
|
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; ai.allocationSize = req.size;
|
|
ai.memoryTypeIndex = memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
check(api.allocate(vk, &ai, nullptr, &s.memory), "FSR RR staging memory");
|
|
check(api.bindImage(vk, s.image, s.memory, 0), "FSR RR staging bind");
|
|
VkImageViewCreateInfo vi{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; vi.image = s.image; vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
vi.format = wanted; vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
check(passes.createView(vk, &vi, nullptr, &s.view), "FSR RR staging view");
|
|
s.format = wanted;
|
|
}
|
|
|
|
void wait_vkd3d() {
|
|
const uint64_t value = ++vsequence;
|
|
dx(vqueue->Signal(vfence.Get(), value), "FSR RR vkd3d signal");
|
|
dx(vfence->SetEventOnCompletion(value, vevent.value), "FSR RR vkd3d completion event");
|
|
if (WaitForSingleObject(vevent.value, 60000) != WAIT_OBJECT_0) throw std::runtime_error("FSR RR vkd3d completion timeout");
|
|
if (vfence->GetCompletedValue() == UINT64_MAX) throw std::runtime_error("FSR RR vkd3d device removed");
|
|
}
|
|
void wait_game(ID3D12CommandQueue* q, uint64_t value) {
|
|
dx(q->Signal(fence.Get(), value), "FSR RR game signal");
|
|
dx(fence->SetEventOnCompletion(value, event.value), "FSR RR game completion event");
|
|
if (WaitForSingleObject(event.value, 10000) != WAIT_OBJECT_0) throw std::runtime_error("FSR RR D3D12 completion timeout");
|
|
if (fence->GetCompletedValue() == UINT64_MAX) throw std::runtime_error("FSR RR D3D12 device removed");
|
|
}
|
|
void validate(ID3D12CommandQueue* q, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access) {
|
|
if (failed) throw std::runtime_error("FSR RR backend is poisoned after failed submission");
|
|
if (!q) throw std::runtime_error("Null RR submission queue");
|
|
D3D12_COMMAND_QUEUE_DESC queueDesc{};
|
|
#if defined(__MINGW32__)
|
|
q->GetDesc(&queueDesc);
|
|
#else
|
|
queueDesc = q->GetDesc();
|
|
#endif
|
|
if (queueDesc.Type != D3D12_COMMAND_LIST_TYPE_DIRECT) throw std::runtime_error("RR requires a direct queue");
|
|
ComPtr<ID3D12Device> owner; dx(q->GetDevice(IID_PPV_ARGS(&owner)), "RR queue device");
|
|
if (owner.Get() != d3d.Get()) throw std::runtime_error("RR queue belongs to another device");
|
|
for (size_t i = 0; i < access.size(); ++i)
|
|
if (frame.resources[i] && (access[i].pending_split || access[i].inherited)) throw std::runtime_error("Unresolved RR texture state");
|
|
validate_frame(frame);
|
|
}
|
|
|
|
void evaluate(ID3D12CommandQueue* q, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access) {
|
|
validate(q, frame, access);
|
|
failed = true;
|
|
const auto begin = Clock::now();
|
|
layout.rw = frame.renderWidth; layout.rh = frame.renderHeight;
|
|
for (size_t i = 0; i < layout.planes.size(); ++i) {
|
|
const auto resource = resource_index(i);
|
|
layout.planes[i].enabled = frame.resources[resource] != nullptr;
|
|
if (!layout.planes[i].enabled) continue;
|
|
const auto format = input_format(resource_desc(frame.resources[resource].Get()).Format, resource);
|
|
layout.planes[i].format = format.format; layout.planes[i].bytes = format.bytes;
|
|
}
|
|
const bool floatOutput = !superSampling && resource_desc(frame.resources[Snapshot::Output].Get()).Format == DXGI_FORMAT_R32G32B32A32_FLOAT;
|
|
if (floatOutput && !textures[Widened].resource) create_intermediate(Widened);
|
|
layout.outputBytes = floatOutput ? 16 : 8;
|
|
layout.outputFormat = floatOutput ? VK_FORMAT_R32G32B32A32_SFLOAT : VK_FORMAT_R16G16B16A16_SFLOAT;
|
|
const auto outputFormat = floatOutput ? DXGI_FORMAT_R32G32B32A32_FLOAT : DXGI_FORMAT_R16G16B16A16_FLOAT;
|
|
commands::InternalScope internal;
|
|
// --- 1. the game's guides into the shared packet ---
|
|
dx(producerAllocator->Reset(), "FSR RR producer allocator reset"); dx(producer->Reset(producerAllocator.Get(), nullptr), "FSR RR producer reset");
|
|
transition(producer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST);
|
|
for (size_t i = 0; i < layout.planes.size(); ++i) {
|
|
const auto resource = resource_index(i);
|
|
auto* texture = frame.resources[resource].Get(); const auto& plane = layout.planes[i];
|
|
if (!plane.enabled) continue;
|
|
texture_copy_barrier(producer.Get(), texture, access[resource], false, false);
|
|
auto source = rr_bridge::D3D12Frames::texture(texture);
|
|
auto dest = rr_bridge::D3D12Frames::placed(shared.Get(), plane.offset, plane.pitch, copy_format(d3d.Get(), texture, resource), layout.rw, layout.rh);
|
|
const auto x = frame.origins[resource][0], y = frame.origins[resource][1];
|
|
D3D12_BOX box{x, y, 0, x + layout.rw, y + layout.rh, 1};
|
|
const bool depthStencil = (resource_desc(texture).Flags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) != 0;
|
|
producer->CopyTextureRegion(&dest, 0, 0, 0, &source, depthStencil ? nullptr : &box);
|
|
texture_copy_barrier(producer.Get(), texture, access[resource], false, true);
|
|
}
|
|
transition(producer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON);
|
|
dx(producer->Close(), "FSR RR producer close");
|
|
ID3D12CommandList* inputs[] = {producer.Get()}; q->ExecuteCommandLists(1, inputs);
|
|
pending = true; queue = q;
|
|
wait_game(q, ++sequence);
|
|
const double producedMs = since(begin);
|
|
// --- 2. conversion, denoising and composition on the vkd3d device ---
|
|
run_vkd3d(frame);
|
|
const double vkd3dMs = since(begin) - producedMs;
|
|
// --- 3. the packet's output into the game's output texture ---
|
|
dx(consumerAllocator->Reset(), "FSR RR consumer allocator reset"); dx(consumer->Reset(consumerAllocator.Get(), nullptr), "FSR RR consumer reset");
|
|
transition(consumer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
|
auto* output = frame.resources[Snapshot::Output].Get();
|
|
if (superSampling) {
|
|
// the denoised colour at render size goes to a game-device texture; the Super Sampling stage upscales it
|
|
if (!denoised) {
|
|
D3D12_HEAP_PROPERTIES heap{}; heap.Type = D3D12_HEAP_TYPE_DEFAULT;
|
|
D3D12_RESOURCE_DESC desc{}; desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; desc.Width = creation.width; desc.Height = creation.height;
|
|
desc.DepthOrArraySize = desc.MipLevels = 1; desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; desc.SampleDesc.Count = 1;
|
|
desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
|
|
dx(d3d->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, denoisedState, nullptr, IID_PPV_ARGS(&denoised)), "FSR RR denoised texture");
|
|
}
|
|
transition(consumer.Get(), denoised.Get(), denoisedState, D3D12_RESOURCE_STATE_COPY_DEST);
|
|
auto source = rr_bridge::D3D12Frames::placed(shared.Get(), layout.outputOffset, layout.outputPitch, outputFormat, layout.rw, layout.rh);
|
|
auto dest = rr_bridge::D3D12Frames::texture(denoised.Get());
|
|
consumer->CopyTextureRegion(&dest, 0, 0, 0, &source, nullptr);
|
|
denoisedState = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
|
|
transition(consumer.Get(), denoised.Get(), D3D12_RESOURCE_STATE_COPY_DEST, denoisedState);
|
|
} else {
|
|
texture_copy_barrier(consumer.Get(), output, access[Snapshot::Output], true, false);
|
|
auto source = rr_bridge::D3D12Frames::placed(shared.Get(), layout.outputOffset, layout.outputPitch, outputFormat, layout.ow, layout.oh);
|
|
auto dest = rr_bridge::D3D12Frames::texture(output);
|
|
consumer->CopyTextureRegion(&dest, frame.origins[Snapshot::Output][0], frame.origins[Snapshot::Output][1], 0, &source, nullptr);
|
|
texture_copy_barrier(consumer.Get(), output, access[Snapshot::Output], true, true);
|
|
}
|
|
transition(consumer.Get(), shared.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON);
|
|
dx(consumer->Close(), "FSR RR consumer close");
|
|
ID3D12CommandList* outputs[] = {consumer.Get()}; q->ExecuteCommandLists(1, outputs);
|
|
wait_game(q, ++sequence);
|
|
if (superSampling) {
|
|
// the stage records/submits its own work on `q`; the denoised texture is in NON_PIXEL_SHADER_RESOURCE
|
|
const auto upscaleStart = Clock::now();
|
|
superSampling(q, denoised.Get(), frame, access);
|
|
if (log_enabled()) std::fprintf(stderr, "FSR_RR super sampling submitted in %.2f ms\n", since(upscaleStart));
|
|
}
|
|
pending = false; failed = false; queue.Reset();
|
|
if (log_enabled())
|
|
std::fprintf(stderr, "FSR_RR frame %u: %ux%u produce %.2f ms, vkd3d (convert+denoise+compose) %.2f ms, total %.2f ms\n",
|
|
frameIndex, layout.rw, layout.rh, producedMs, vkd3dMs, since(begin));
|
|
++frameIndex;
|
|
}
|
|
|
|
void run_vkd3d(const Snapshot& frame) {
|
|
const auto& s = frame.settings;
|
|
const uint32_t rw = layout.rw, rh = layout.rh;
|
|
// which game planes exist decides the context's specular signal once
|
|
const bool specularHit = layout.planes[7].enabled;
|
|
const uint32_t wanted = FFX_DENOISER_SIGNAL_DIRECT_DIFFUSE |
|
|
(specularHit ? FFX_DENOISER_SIGNAL_INDIRECT_SPECULAR : FFX_DENOISER_SIGNAL_DIRECT_SPECULAR);
|
|
if (wanted != signals) {
|
|
if (frameIndex == 0) {
|
|
runtime.destroyContext(&context, nullptr); context = nullptr; signals = wanted;
|
|
const ffxReturnCode_t created = fsr_rr_create_context(runtime, vdev.Get(), creation.width, creation.height, signals, 0, context);
|
|
if (created != FFX_API_RETURN_OK) throw std::runtime_error(std::string("FSR RR ffxCreateContext failed: ") + fsr_rr_return_name(created));
|
|
apply_denoiser_keys();
|
|
} else throw std::runtime_error("FSR RR: the game's hit-distance guide changed after the denoiser was created (recreate the feature)");
|
|
}
|
|
for (size_t plane : {0, 1, 2, 3, 4, 5, 6, 7, 10}) if (layout.planes[plane].enabled) ensure_staging(plane);
|
|
double clipToView[16], viewToWorld[16];
|
|
if (!invert(s.viewToClip, clipToView) || !invert(s.worldToView, viewToWorld)) throw std::runtime_error("FSR RR camera matrix is not invertible");
|
|
const bool reset = s.reset || !havePrevious;
|
|
double previousView[16], viewToPrevious[16];
|
|
for (int i = 0; i < 16; ++i) previousView[i] = reset ? s.worldToView[i] : this->previousView[i];
|
|
multiply(viewToWorld, previousView, viewToPrevious);
|
|
D4rFsrRrPrepareConstants c = {};
|
|
for (int i = 0; i < 16; ++i) { c.clipToView[i] = float(clipToView[i]); c.viewToPreviousView[i] = float(viewToPrevious[i]); }
|
|
c.renderSize[0] = float(rw); c.renderSize[1] = float(rh); c.renderSize[2] = 1.0f / rw; c.renderSize[3] = 1.0f / rh;
|
|
c.motion[0] = s.motionScale[0] / rw; c.motion[1] = s.motionScale[1] / rh; c.motion[2] = 1.0f; c.motion[3] = 1.0f;
|
|
uint32_t flags = 0;
|
|
if (s.hardwareDepth) flags |= kFsrRrHardwareDepth;
|
|
if (s.roughnessPacked) flags |= kFsrRrRoughnessPacked;
|
|
if (specularHit) flags |= kFsrRrSpecularHit;
|
|
if (unorm(layout.planes[3].format)) flags |= kFsrRrNormalsUnorm;
|
|
if (layout.planes[10].enabled) flags |= kFsrRrBeforeParticles;
|
|
flags |= kFsrRrInvalidNormalPassthrough;
|
|
c.motionBaseFlags[2] = flags;
|
|
const float viewZSign = s.viewToClip[11] < 0.0f ? -1.0f : 1.0f;
|
|
c.depthParams[0] = viewZSign; c.depthParams[1] = s.depthInverted ? 0.0f : 1.0f; c.depthParams[2] = 1.0e6f;
|
|
c.detail[0] = env_float("D4R_FSR_RR_DETAIL", 0.0f, 0.0f, 1.0f);
|
|
c.detail[1] = std::max(env_float("D4R_FSR_RR_DEMOD_FLOOR", 0.1f, 0.0f, 1.0f), 1e-3f);
|
|
|
|
dx(vallocator->Reset(), "FSR RR vkd3d allocator reset"); dx(vlist->Reset(vallocator.Get(), nullptr), "FSR RR vkd3d list reset");
|
|
stamp(0);
|
|
auto setState = [&](int index, D3D12_RESOURCE_STATES state) {
|
|
auto& t = textures[index];
|
|
if (t.resource) { transition(vlist.Get(), t.resource.Get(), t.state, state); t.state = state; }
|
|
};
|
|
for (int i = 0; i < TexCount; ++i) setState(i, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
|
VkCommandBuffer command = VK_NULL_HANDLE;
|
|
dx(interop->BeginVkCommandBufferInterop(vlist.Get(), &command), "FSR RR Vulkan interop begin");
|
|
// the packet comes in from the game's device
|
|
VkBufferMemoryBarrier acquire{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; acquire.buffer = packetBuffer; acquire.size = VK_WHOLE_SIZE;
|
|
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL; acquire.dstQueueFamilyIndex = vkFamily;
|
|
acquire.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
api.barrier(command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &acquire, 0, nullptr);
|
|
for (size_t plane : {0, 1, 2, 3, 4, 5, 6, 7, 10}) {
|
|
if (!layout.planes[plane].enabled) continue;
|
|
const auto& st = staging[plane];
|
|
VkImageMemoryBarrier toCopy{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toCopy.image = st.image;
|
|
toCopy.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; toCopy.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
toCopy.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toCopy.srcQueueFamilyIndex = toCopy.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
toCopy.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
api.barrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toCopy);
|
|
VkBufferImageCopy region{}; region.bufferOffset = layout.planes[plane].offset;
|
|
region.bufferRowLength = uint32_t(layout.planes[plane].pitch / layout.planes[plane].bytes);
|
|
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; region.imageExtent = {rw, rh, 1};
|
|
api.copyBufferToImage(command, packetBuffer, st.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
|
VkImageMemoryBarrier toRead = toCopy; toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
api.barrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toRead);
|
|
}
|
|
auto sampled = [&](size_t plane) { return D4rFsrRrImage{staging[plane].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; };
|
|
const bool hasHit = layout.planes[7].enabled, hasParticles = layout.planes[10].enabled;
|
|
D4rFsrRrImage images[D4rFsrRrPasses::kPrepareInputs + D4rFsrRrPasses::kPrepareOutputs] = {
|
|
sampled(0), sampled(2), sampled(1), sampled(3), sampled(4), sampled(5), sampled(6),
|
|
hasHit ? sampled(7) : sampled(3), sampled(3), hasParticles ? sampled(10) : sampled(0)};
|
|
static const int targets[D4rFsrRrPasses::kPrepareOutputs] = {LinearDepth, MotionT, NormalsT, DiffuseAlbedo, SpecularAlbedo, DiffuseT, SpecularT, Residual};
|
|
for (uint32_t i = 0; i < D4rFsrRrPasses::kPrepareOutputs; ++i)
|
|
images[D4rFsrRrPasses::kPrepareInputs + i] = {textures[targets[i]].view, textures[targets[i]].layout};
|
|
passes.record_prepare(command, images, c, rw, rh);
|
|
dx(interop->EndVkCommandBufferInterop(vlist.Get()), "FSR RR Vulkan interop end");
|
|
stamp(1);
|
|
|
|
// --- AMD FSR Ray Regeneration (D3D12 on vkd3d-proton) ---
|
|
const D3D12_RESOURCE_STATES read = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
|
|
for (int index : {LinearDepth, MotionT, NormalsT, DiffuseAlbedo, SpecularAlbedo, DiffuseT, SpecularT}) setState(index, read);
|
|
auto ffx = [&](int index, uint32_t state) {
|
|
const auto& t = textures[index];
|
|
return fsr_rr_texture(t.resource.Get(), t.desc, fsr_rr_surface_format(t.desc.Format), state);
|
|
};
|
|
const uint32_t ffxRead = FFX_API_RESOURCE_STATE_PIXEL_COMPUTE_READ;
|
|
ffxDispatchDescDenoiser desc = {};
|
|
desc.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER;
|
|
desc.commandList = vlist.Get();
|
|
desc.linearDepth = ffx(LinearDepth, ffxRead); desc.motionVectors = ffx(MotionT, ffxRead); desc.normals = ffx(NormalsT, ffxRead);
|
|
desc.specularAlbedo = ffx(SpecularAlbedo, ffxRead); desc.diffuseAlbedo = ffx(DiffuseAlbedo, ffxRead);
|
|
desc.motionVectorScale = {1.0f, 1.0f, 1.0f};
|
|
desc.jitterOffsets = {s.jitter[0], s.jitter[1]};
|
|
const double position[3] = {viewToWorld[12], viewToWorld[13], viewToWorld[14]};
|
|
double previousPosition[3] = {position[0], position[1], position[2]};
|
|
if (!reset) {
|
|
double previousInverse[16];
|
|
if (invert(this->previousView, previousInverse)) previousPosition[0] = previousInverse[12], previousPosition[1] = previousInverse[13], previousPosition[2] = previousInverse[14];
|
|
}
|
|
desc.cameraPositionDelta = {float(previousPosition[0] - position[0]), float(previousPosition[1] - position[1]), float(previousPosition[2] - position[2])};
|
|
std::memcpy(&desc.view, s.worldToView, sizeof(desc.view)); std::memcpy(&desc.projection, s.viewToClip, sizeof(desc.projection));
|
|
desc.linearDepthBounds = {0.0f, 1.0e6f};
|
|
desc.renderSize = {rw, rh};
|
|
desc.frameIndex = frameIndex;
|
|
desc.flags = uint32_t(FFX_DENOISER_DISPATCH_NON_GAMMA_ALBEDO) | (reset ? uint32_t(FFX_DENOISER_DISPATCH_RESET) : 0u);
|
|
ffxDispatchDescDenoiserDirectDiffuse directDiffuse = {}; ffxDispatchDescDenoiserDirectSpecular directSpecular = {};
|
|
ffxDispatchDescDenoiserIndirectSpecular indirectSpecular = {};
|
|
directDiffuse.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_DIRECT_DIFFUSE;
|
|
FfxApiDenoiserSignal* diffuseSignal = &directDiffuse.signal;
|
|
FfxApiDenoiserSignal* specularSignal; ffxApiHeader* specularHeader;
|
|
if (specularHit) {
|
|
indirectSpecular.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_INDIRECT_SPECULAR; specularSignal = &indirectSpecular.signal; specularHeader = &indirectSpecular.header;
|
|
} else {
|
|
directSpecular.header.type = FFX_API_DISPATCH_DESC_TYPE_DENOISER_DIRECT_SPECULAR; specularSignal = &directSpecular.signal; specularHeader = &directSpecular.header;
|
|
}
|
|
diffuseSignal->input = ffx(DiffuseT, ffxRead); diffuseSignal->output = ffx(DiffuseOut, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
|
|
specularSignal->input = ffx(SpecularT, ffxRead); specularSignal->output = ffx(SpecularOut, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
|
|
desc.header.pNext = &directDiffuse.header; directDiffuse.header.pNext = specularHeader;
|
|
const ffxReturnCode_t dispatched = runtime.dispatch(&context, &desc.header);
|
|
if (dispatched != FFX_API_RETURN_OK) throw std::runtime_error(std::string("FSR RR ffxDispatch failed: ") + fsr_rr_return_name(dispatched));
|
|
D3D12_RESOURCE_BARRIER uav{}; uav.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV; vlist->ResourceBarrier(1, &uav);
|
|
stamp(2);
|
|
|
|
// --- composition, then the composed image into the packet's output region ---
|
|
for (int i = 0; i < TexCount; ++i) setState(i, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
|
|
const D4rFsrRrImage compose[D4rFsrRrPasses::kComposeImages] = {
|
|
{textures[DiffuseOut].view, textures[DiffuseOut].layout}, {textures[SpecularOut].view, textures[SpecularOut].layout},
|
|
{textures[DiffuseAlbedo].view, textures[DiffuseAlbedo].layout}, {textures[SpecularAlbedo].view, textures[SpecularAlbedo].layout},
|
|
{textures[Residual].view, textures[Residual].layout}, {textures[Composed].view, textures[Composed].layout},
|
|
{textures[DiffuseT].view, textures[DiffuseT].layout}, {textures[SpecularT].view, textures[SpecularT].layout}};
|
|
dx(interop->BeginVkCommandBufferInterop(vlist.Get(), &command), "FSR RR Vulkan interop begin (compose)");
|
|
passes.record_compose(command, compose, rw, rh, env_float("D4R_FSR_RR_HALO_LIMIT", 1.0f, 0.0f, 1000.0f));
|
|
int copiedImage = Composed;
|
|
if (layout.outputBytes == 16) {
|
|
// Preserve the denoiser/composition's half arithmetic; widen only the completed output on the GPU.
|
|
VkImageBlit region{}; region.srcSubresource = region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
region.srcOffsets[1] = region.dstOffsets[1] = {int32_t(rw), int32_t(rh), 1};
|
|
api.blitImage(command, textures[Composed].image, textures[Composed].layout,
|
|
textures[Widened].image, textures[Widened].layout, 1, ®ion, VK_FILTER_NEAREST);
|
|
passes.full_barrier(command);
|
|
copiedImage = Widened;
|
|
}
|
|
VkBufferImageCopy back{}; back.bufferOffset = layout.outputOffset; back.bufferRowLength = uint32_t(layout.outputPitch / layout.outputBytes);
|
|
back.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; back.imageExtent = {rw, rh, 1};
|
|
api.copyImageToBuffer(command, textures[copiedImage].image, textures[copiedImage].layout, packetBuffer, 1, &back);
|
|
VkBufferMemoryBarrier release = acquire; release.srcQueueFamilyIndex = vkFamily; release.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
|
|
release.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; release.dstAccessMask = 0;
|
|
api.barrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 1, &release, 0, nullptr);
|
|
dx(interop->EndVkCommandBufferInterop(vlist.Get()), "FSR RR Vulkan interop end (compose)");
|
|
// Diagnostic (D4R_FSR_RR_DUMP_DIR, first D4R_FSR_RR_DUMP_FRAMES frames, default 3): raw copies of the converted
|
|
// linear depth and motion textures, to check the DLSS -> FFX guide conversion numerically.
|
|
stamp(3);
|
|
if (stamps) vlist->ResolveQueryData(stamps.Get(), D3D12_QUERY_TYPE_TIMESTAMP, 0, 4, stampReadback.Get(), 0);
|
|
const char* dumpDir = std::getenv("D4R_FSR_RR_DUMP_DIR");
|
|
const unsigned dumpFrames = std::getenv("D4R_FSR_RR_DUMP_FRAMES") ? unsigned(std::atoi(std::getenv("D4R_FSR_RR_DUMP_FRAMES"))) : 3;
|
|
struct Dump { int index; const char* name; ComPtr<ID3D12Resource> buffer; D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint; UINT rows; UINT bytesPerTexel; };
|
|
std::vector<Dump> dumps;
|
|
if (dumpDir && frameIndex < dumpFrames) {
|
|
for (auto [index, name, texel] : {std::tuple<int, const char*, UINT>{LinearDepth, "linear-depth", 4}, {MotionT, "motion", 8}, {NormalsT, "normals", 8}, {Composed, "composed", 8}}) {
|
|
Dump d{index, name, {}, {}, 0, texel};
|
|
UINT64 rowBytes = 0, total = 0; auto desc = textures[index].desc;
|
|
vdev->GetCopyableFootprints(&desc, 0, 1, 0, &d.footprint, &d.rows, &rowBytes, &total);
|
|
d.buffer = make_buffer(vdev.Get(), total, D3D12_HEAP_TYPE_READBACK, D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_STATE_COPY_DEST);
|
|
transition(vlist.Get(), textures[index].resource.Get(), textures[index].state, D3D12_RESOURCE_STATE_COPY_SOURCE);
|
|
D3D12_TEXTURE_COPY_LOCATION destination{}; destination.pResource = d.buffer.Get();
|
|
destination.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; destination.PlacedFootprint = d.footprint;
|
|
D3D12_TEXTURE_COPY_LOCATION source{}; source.pResource = textures[index].resource.Get(); source.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
|
|
vlist->CopyTextureRegion(&destination, 0, 0, 0, &source, nullptr);
|
|
transition(vlist.Get(), textures[index].resource.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, textures[index].state);
|
|
dumps.push_back(std::move(d));
|
|
}
|
|
}
|
|
dx(vlist->Close(), "FSR RR vkd3d list close");
|
|
ID3D12CommandList* lists[] = {vlist.Get()}; vqueue->ExecuteCommandLists(1, lists);
|
|
wait_vkd3d();
|
|
if (stamps) {
|
|
void* mapped = nullptr; D3D12_RANGE range{0, 4 * sizeof(UINT64)};
|
|
if (SUCCEEDED(stampReadback->Map(0, &range, &mapped))) {
|
|
UINT64 t[4]; std::memcpy(t, mapped, sizeof(t)); stampReadback->Unmap(0, nullptr);
|
|
auto ms = [&](int a, int b) { return double(t[b] - t[a]) * 1000.0 / double(stampFrequency); };
|
|
std::fprintf(stderr, "FSR_RR gpu frame %u: prepare %.2f ms, denoise %.2f ms, compose+copy %.2f ms, total %.2f ms\n", frameIndex, ms(0, 1), ms(1, 2), ms(2, 3), ms(0, 3));
|
|
}
|
|
}
|
|
for (auto& d : dumps) {
|
|
void* mapped = nullptr; D3D12_RANGE range{0, size_t(d.footprint.Footprint.RowPitch) * d.rows};
|
|
dx(d.buffer->Map(0, &range, &mapped), "FSR RR dump map");
|
|
const std::string path = std::string(dumpDir) + "/frame" + std::to_string(frameIndex) + "-" + d.name + ".bin";
|
|
if (FILE* out = std::fopen(path.c_str(), "wb")) {
|
|
for (UINT row = 0; row < rw && row < d.rows; ++row) {} // (rows are written below; width = render width)
|
|
for (UINT row = 0; row < rh; ++row)
|
|
std::fwrite(static_cast<const char*>(mapped) + size_t(row) * d.footprint.Footprint.RowPitch, 1, size_t(rw) * d.bytesPerTexel, out);
|
|
std::fclose(out);
|
|
}
|
|
d.buffer->Unmap(0, nullptr);
|
|
}
|
|
std::memcpy(this->previousView, s.worldToView, sizeof(this->previousView));
|
|
havePrevious = true;
|
|
}
|
|
};
|
|
|
|
FsrBackend::FsrBackend(ID3D12Device* device, const Creation& creation, const FsrConfig& config) {
|
|
std::lock_guard<std::mutex> lock(hostMutex);
|
|
auto candidate = std::make_unique<Impl>(device, creation, config);
|
|
candidate->initialize(); impl = std::move(candidate);
|
|
}
|
|
FsrBackend::~FsrBackend() {
|
|
std::lock_guard<std::mutex> lock(hostMutex);
|
|
if (impl && impl->pending) {
|
|
std::fprintf(stderr, "D4R_FSR_RR_QUARANTINE incomplete GPU work; retaining backend and guide resources\n");
|
|
(void)impl.release();
|
|
} else impl.reset();
|
|
}
|
|
void FsrBackend::evaluate(ID3D12CommandQueue* queue, const Snapshot& frame, const std::array<ResourceAccess, Snapshot::Count>& access) {
|
|
std::lock_guard<std::mutex> lock(hostMutex); impl->evaluate(queue, frame, access);
|
|
}
|
|
void FsrBackend::set_super_sampling(SuperSampling stage, const std::array<unsigned, 4>& renderLimits) {
|
|
std::lock_guard<std::mutex> lock(hostMutex); impl->superSampling = std::move(stage); impl->srRenderLimits = renderLimits;
|
|
}
|
|
void FsrBackend::validate_frame(const Snapshot& frame) { std::lock_guard<std::mutex> lock(hostMutex); impl->validate_frame(frame); }
|
|
void FsrBackend::drain() {
|
|
std::lock_guard<std::mutex> lock(hostMutex);
|
|
if (impl->pending || impl->failed) throw std::runtime_error("FSR RR backend cannot retire failed GPU work");
|
|
}
|
|
}
|