From e14b4cb54c1fbd7092616ee2eba7575be44603fd Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 1 Oct 2026 16:13:07 +0300 Subject: [PATCH] Run native Windows DLSS K and M through an explicit OptiScaler source backend --- CMakeLists.txt | 4 + README.md | 6 +- docs/windows-rdna4-port.md | 41 ++- .../0001-windows-external-d4r-backend.patch | 328 ++++++++++++++++++ patches/optiscaler/README.md | 40 +++ scripts/windows/build-optiscaler-windows.ps1 | 75 ++++ scripts/windows/test-windows-rdna4.ps1 | 6 + tools/windows/d3d12_dlss_probe.cpp | 11 +- tools/windows/debug_launcher.cpp | 130 +++++++ 9 files changed, 633 insertions(+), 8 deletions(-) create mode 100644 patches/optiscaler/0001-windows-external-d4r-backend.patch create mode 100644 patches/optiscaler/README.md create mode 100644 scripts/windows/build-optiscaler-windows.ps1 create mode 100644 tools/windows/debug_launcher.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 35649ee..32b2dea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,10 @@ endif() install(TARGETS d4r_nvapi_compat RUNTIME DESTINATION nvapi-compat) target_link_libraries(d4r_d3d12_hip_interop_probe PRIVATE d3d12 dxgi dxguid) target_link_libraries(d4r_d3d12_dlss_probe PRIVATE d3d12 dxgi dxguid) +add_executable(d4r_debug_launcher tools/windows/debug_launcher.cpp) +target_include_directories(d4r_debug_launcher PRIVATE tools/windows) +target_link_libraries(d4r_debug_launcher PRIVATE version dbghelp) +install(TARGETS d4r_debug_launcher RUNTIME DESTINATION bin) target_link_libraries(d4r_d3d12_command_probe PRIVATE d3d12 dxgi dxguid) target_link_libraries(d4r_d3d12_pixel_probe PRIVATE d3d12 dxgi dxguid) add_dependencies(d4r_d3d12_pixel_probe d4r_pixel_code_object) diff --git a/README.md b/README.md index 544fdb2..758f5cd 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,10 @@ submission, and returns output in the same frame without CPU image copies. RGBA16/32F, R11G11B10, RGBA/BGRA8, depth, exposure and motion formats have independent D3D12 typed SRV/UAV conversion tests. Four-frame K/M runs with packed output match the FP16 baseline exactly after resource-format quantisation. -The build incorporates upstream RDNA4 commit `dbef4b2`. OptiScaler/game -integration and performance profiling remain in progress; no game support is +The build incorporates upstream RDNA4 commit `dbef4b2`. Patched OptiScaler now +passes standalone K/M native-network tests with exact RGB agreement against +the direct-d4r baseline; see [the source patch](patches/optiscaler/README.md). +Game integration and performance profiling remain in progress; no game support is claimed yet. See [build instructions and hardware results](docs/windows-rdna4-port.md). See [supported games](SUPPORTED_GAMES.md) for the tested games and DLSS models. diff --git a/docs/windows-rdna4-port.md b/docs/windows-rdna4-port.md index f0b862c..0c381de 100644 --- a/docs/windows-rdna4-port.md +++ b/docs/windows-rdna4-port.md @@ -3,7 +3,8 @@ Branch: `windows-rdna4`. Target: Windows 11 x64, RX 9070 XT, **gfx1201**. Priority: correct K, correct M, native Windows, same-frame output, then speed. This is a work log for an implementation in progress. Standalone DLSS K/M -works on the real Windows GPU; OptiScaler/game support remains unverified. +works on the real Windows GPU, including the patched OptiScaler frontend. +An actual game session remains unverified. ## Milestone and gates @@ -36,7 +37,7 @@ Do not integrate NGX until the integer PTX workload is stable on the real GPU. | M6: K layers, full transformer and image validation | PASS: 11 real-weight layers, 44 native launches, four D3D12 frames bit-exact against native CUDA, 81.85–92.91 dB versus translated K | | M7: M FP16-equivalent baseline and full transformer | PASS: all blocks of 40 native launches exactly match NumPy; targeted independent PTX regression matches; four D3D12 frames bit-exact against corrected translated M | | M8: standalone Windows D3D12 NGX harness | PASS for ordinary recorded EvaluateFeature and explicit boundaries: K/M, four frames each, no shim CPU image copies; both bit-exact against explicit boundary baselines | -| M9: OptiScaler integration, profiling, installation | Pending | +| M9: OptiScaler integration, profiling, installation | Patched frontend K/M PASS; game, profiling and final package pending | ## Public NGX identity and OptiScaler gate (2026-10-01) @@ -70,13 +71,43 @@ A local OptiScaler source fork starts from `45a2001303ddff632e279f77aef85ceede5832cb`. Its explicit `[DLSS] AllowExternalBackend=true` option requires an absolute NvngxPath with the `d4r_WindowsBackendVersion` ABI-1 marker; it does not spoof HIP architecture. -The marker is GPU-free and safe during discovery. The pending patch also lets -the external backend load its own NVAPI provider through the original loader. -This source integration has not yet passed the frontend harness. +The marker is GPU-free and safe during discovery. The patch lets the external +backend load its own NVAPI provider through the original loader and preserves +the exact locally supplied CUDA NGX core path from frontend redirection. +Physical adapter capability is queried outside DLL startup instead of using +NVIDIA architecture as a proxy. GetFeatureRequirements spoofing is bypassed. +Explicit external DLSS selection returns errors instead of silently using FSR. Microsoft Build Tools 17.14 / MSVC 14.44.35207 and SDK 10.0.26100 were installed from the signature-verified official bootstrapper without restarting Windows. No game files have been modified. +The OptiScaler fork commit is `33bbac2` on `windows-rdna4-d4r`. Its reproducible +source patch and build instructions are in [patches/optiscaler](../patches/optiscaler/README.md). +`build-optiscaler-windows.ps1` produces a DLL, PDB, GPL license and dependency +metadata from pinned source/submodules; it disables upstream's unrelated +packaging post-build commands. The public D3D12 probe now supplies and submits +a real open creation command list, as required by OptiScaler's API. + +`optiscaler-k-final.zip` and `optiscaler-m-final.zip` pass four complete +OptiScaler -> Windows d4r -> CUDA NGX -> ZLUDA/HIP evaluations on RX 9070 XT. +They contain 44 K and 40 M native transformer launches, respectively. Every +RGB value exactly matches the independently quantised direct-d4r baseline; +max absolute/relative error is 0. No FSR evaluation, NaN/Inf, shim CPU image copy +or previous-frame output is observed. The invalid-core regression intentionally +passes the shim as the CUDA core: it fails with an explicit frontend +`refusing FSR fallback` message and zero FSR evaluations, as required. + +The loader crash was localized with a PDB to Util::IsSubpath calling +filesystem::relative/weakly_canonical from LdrLoadDll interception. The patched +check is lexical, case-insensitive and handles empty relative paths without +filesystem I/O. The separate `d4r_debug_launcher.exe`, enabled by +`-CaptureExceptions`, captures minidumps, first/second-chance exception codes, +module load/unload addresses and raw stack slots outside the hooked process. +It propagates the child's exit code and terminates its child on runner timeout +via a process lifetime job. `optiscaler-m-source-debug2.zip` contains the +successful dump capture and `optiscaler-source-stack-symbols.log` the source +attribution. The ordinary in-process crash filter remains available too. + ## Initial PSO and enhanced texture layouts (2026-10-01) The backend now captures the initial PSO supplied to public CreateCommandList, diff --git a/patches/optiscaler/0001-windows-external-d4r-backend.patch b/patches/optiscaler/0001-windows-external-d4r-backend.patch new file mode 100644 index 0000000..0b3bac3 --- /dev/null +++ b/patches/optiscaler/0001-windows-external-d4r-backend.patch @@ -0,0 +1,328 @@ +From 33bbac250ac7e2ca4eff67e8817ada9dad578208 Mon Sep 17 00:00:00 2001 +From: Codex +Date: Thu, 1 Oct 2026 16:10:08 +0300 +Subject: [PATCH] Support explicitly selected Windows d4r DLSS backend on + physical AMD adapters + +--- + OptiScaler/Config.cpp | 3 ++ + OptiScaler/Config.h | 1 + + OptiScaler/OptiScaler.vcxproj | 4 +-- + OptiScaler/State.h | 1 + + OptiScaler/Util.cpp | 12 +++++-- + OptiScaler/dllmain.cpp | 33 +++++++++++++++++-- + OptiScaler/hooks/LibraryLoad_Hooks.cpp | 17 ++++++++++ + OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp | 17 ++++++++++ + OptiScaler/misc/IdentifyGpu.cpp | 20 +++++++++++ + OptiScaler/proxies/NVNGX_Proxy.h | 5 ++- + OptiScaler/upscalers/FeatureProvider_Dx12.cpp | 10 ++++++ + 11 files changed, 115 insertions(+), 8 deletions(-) + +diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp +index d8c62d3..f444387 100644 +--- a/OptiScaler/Config.cpp ++++ b/OptiScaler/Config.cpp +@@ -323,6 +323,7 @@ bool Config::Reload(std::filesystem::path iniPath) + { + // Don't enable again if set false because of no nvngx found + DLSSEnabled.set_from_config(readBool("DLSS", "Enabled")); ++ DLSSAllowExternalBackend.set_from_config(readBool("DLSS", "AllowExternalBackend")); + UseGenericAppIdWithDlss.set_from_config(readBool("DLSS", "UseGenericAppIdWithDlss")); + + RenderPresetOverride.set_from_config(readBool("DLSS", "RenderPresetOverride")); +@@ -1168,6 +1169,8 @@ bool Config::SaveIni() + // DLSS + { + ini.SetValue("DLSS", "Enabled", GetBoolValue(Instance()->DLSSEnabled.value_for_config()).c_str()); ++ ini.SetValue("DLSS", "AllowExternalBackend", ++ GetBoolValue(Instance()->DLSSAllowExternalBackend.value_for_config()).c_str()); + ini.SetValue("DLSS", "RenderPresetOverride", + GetBoolValue(Instance()->RenderPresetOverride.value_for_config()).c_str()); + ini.SetValue("DLSS", "RenderPresetForAll", +diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h +index 4ab62a4..0caf8c8 100644 +--- a/OptiScaler/Config.h ++++ b/OptiScaler/Config.h +@@ -275,6 +275,7 @@ class Config + + // DLSS + CustomOptional DLSSEnabled { true }; ++ CustomOptional DLSSAllowExternalBackend { false }; + CustomOptional RenderPresetOverride { false }; + CustomOptional RenderPresetForAll { 0 }; + CustomOptional RenderPresetDLAA { 0 }; +diff --git a/OptiScaler/OptiScaler.vcxproj b/OptiScaler/OptiScaler.vcxproj +index 24ca80b..dfbfc97 100644 +--- a/OptiScaler/OptiScaler.vcxproj ++++ b/OptiScaler/OptiScaler.vcxproj +@@ -236,7 +236,7 @@ + Windows + true + true +- false ++ true + false + winhttp.lib;WindowsApp.lib;Dbghelp.lib;dxgi.lib;d3d11.lib;d3d12.lib;vulkan-1.lib;dxguid.lib;freetype.lib;ffx_fsr2_api_x64.lib;ffx_fsr2_api_dx11_x64.lib;ffx_fsr2_api_dx12_x64.lib;ffx_fsr2_api_vk_x64.lib;ffx_fsr2_212_api_dx12_x64.lib;ffx_fsr2_212_api_vk_x64.lib;ffx_fsr2_212_api_x64.lib;d3dcompiler.lib;ffx_backend_dx11_x64.lib;ffx_fsr3_x64.lib;ffx_frameinterpolation_x64.lib;ffx_fsr3upscaler_x64.lib;ffx_opticalflow_x64.lib;%(AdditionalDependencies) + Source.def +@@ -873,4 +873,4 @@ copy NUL "$(SolutionDir)x64\Release\a\!! EXTRACT ALL FILES TO GAME FOLDER !!" /Y + + + +- +\ No newline at end of file ++ +diff --git a/OptiScaler/State.h b/OptiScaler/State.h +index 42164de..84f8263 100644 +--- a/OptiScaler/State.h ++++ b/OptiScaler/State.h +@@ -265,6 +265,7 @@ class State + int dlssgDetectedInterpolationCount = 0; + + // DLSS ++ bool dlssExternalBackend = false; + bool dlssPresetsOverriddenExternally = false; + bool dlssPresetsOverridenByOpti = false; + uint32_t dlssRenderPresetExternal = 0; +diff --git a/OptiScaler/Util.cpp b/OptiScaler/Util.cpp +index f7ac6fa..7367a91 100644 +--- a/OptiScaler/Util.cpp ++++ b/OptiScaler/Util.cpp +@@ -486,8 +486,16 @@ bool Util::GetFileVersion(std::wstring dllPath, version_t* fileVersionOut, versi + + bool Util::IsSubpath(const std::filesystem::path& path, const std::filesystem::path& base) + { +- auto rel = std::filesystem::relative(path, base); +- auto first = *rel.begin(); ++ // Called by LdrLoadDll hooks: relative()/weakly_canonical() perform file ++ // queries, can re-enter the hooks, and throw when an optional directory or ++ // a bare DLL search name cannot be resolved. This is a lexical routing ++ // check, not a filesystem access control decision. ++ auto normalizedPath = std::filesystem::path(ToLower(path.lexically_normal().wstring())); ++ auto normalizedBase = std::filesystem::path(ToLower(base.lexically_normal().wstring())); ++ auto rel = normalizedPath.lexically_relative(normalizedBase); ++ if (rel.empty()) ++ return false; ++ const auto first = *rel.begin(); + return first != "." && first != ".."; + } + +diff --git a/OptiScaler/dllmain.cpp b/OptiScaler/dllmain.cpp +index cba14b1..b4b786e 100644 +--- a/OptiScaler/dllmain.cpp ++++ b/OptiScaler/dllmain.cpp +@@ -948,7 +948,7 @@ static void CheckWorkingMode() + + // NVAPI + // Doesn't seem to like GetModuleHandle for some reason, so call our load to make sure +- if (GetDllNameWModule(&nvapiNamesW) != nullptr) ++ if (!State::Instance().dlssExternalBackend && GetDllNameWModule(&nvapiNamesW) != nullptr) + { + // This hooks nvapi as well when possible + auto nvapi64 = LibraryLoadHooks::LoadNvApi(); +@@ -1958,7 +1958,33 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv + return nvidiaDetected; + }; + +- bool possibleNvidia = isNvidiaViaNvapi(); ++ // Opt in only to a configured, versioned Windows d4r backend. The ++ // marker performs no HIP/NGX initialization under DLL_PROCESS_ATTACH. ++ // GPU support is still checked by its public NGX requirements API. ++ if (Config::Instance()->DLSSEnabled.value_or_default() && ++ Config::Instance()->DLSSAllowExternalBackend.value_or_default() && ++ Config::Instance()->NvngxPath.has_value()) ++ { ++ auto backendPath = std::filesystem::path(Config::Instance()->NvngxPath.value()) / L"_nvngx.dll"; ++ if (backendPath.is_absolute()) ++ { ++ auto module = NtdllProxy::LoadLibraryExW_Ldr(backendPath.c_str(), nullptr, ++ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); ++ if (module) ++ { ++ using BackendVersion = unsigned (*)(); ++ auto version = reinterpret_cast( ++ KernelBaseProxy::GetProcAddress_()(module, "d4r_WindowsBackendVersion")); ++ State::Instance().dlssExternalBackend = version && version() == 1; ++ NtdllProxy::FreeLibrary_Ldr(module); ++ } ++ } ++ if (State::Instance().dlssExternalBackend) ++ spdlog::info("Validated external Windows d4r DLSS backend; retaining physical GPU identity"); ++ else ++ spdlog::error("AllowExternalBackend requires an absolute NvngxPath containing Windows d4r ABI 1"); ++ } ++ bool possibleNvidia = !State::Instance().dlssExternalBackend && isNvidiaViaNvapi(); + + spdlog::info(""); + spdlog::info("Check for DLSS files"); +@@ -1992,7 +2018,8 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv + State::Instance().NVNGX_DLSSG_Path = Util::FindFilePath(exePath, "nvngx_dlssg.dll"); + + // Not 100% accurate for Nvidia cards without DLSS +- if (Config::Instance()->DLSSEnabled.value_or_default() && possibleNvidia) ++ if (Config::Instance()->DLSSEnabled.value_or_default() && ++ (possibleNvidia || State::Instance().dlssExternalBackend)) + { + if (State::Instance().NVNGX_DLSS_Path.has_value()) + { +diff --git a/OptiScaler/hooks/LibraryLoad_Hooks.cpp b/OptiScaler/hooks/LibraryLoad_Hooks.cpp +index 90bdb89..108ea5b 100644 +--- a/OptiScaler/hooks/LibraryLoad_Hooks.cpp ++++ b/OptiScaler/hooks/LibraryLoad_Hooks.cpp +@@ -46,6 +46,18 @@ HMODULE LibraryLoadHooks::LoadLibraryCheckW(std::wstring libName, LPCWSTR lpLibF + { + auto libNameA = wstring_to_string(libName); + ++ // The external D3D12 shim itself uses a locally supplied CUDA NGX core. ++ // Keep that exact absolute path distinct from game-facing NGX interception. ++ if (State::Instance().dlssExternalBackend) ++ { ++ wchar_t corePath[32768] {}; ++ const auto length = GetEnvironmentVariableW(L"D4R_NGX_CORE", corePath, 32768); ++ if (length && length < 32768 && ++ Util::ToLower(std::filesystem::path(libName).lexically_normal().wstring()) == ++ Util::ToLower(std::filesystem::path(corePath).lexically_normal().wstring())) ++ return nullptr; ++ } ++ + #ifdef LOG_LIB_OPERATIONS + LOG_TRACE("{}", libNameA); + #endif +@@ -132,6 +144,11 @@ HMODULE LibraryLoadHooks::LoadLibraryCheckW(std::wstring libName, LPCWSTR lpLibF + // NvApi64.dll + if (CheckDllNameW(&libName, &nvapiNamesW)) + { ++ // An explicit external CUDA backend owns its NVAPI topology provider. ++ // Returning OptiScaler's fake-NVAPI would replace that provider and can ++ // recurse into DXGI while the NGX loader holds the loader lock. ++ if (State::Instance().dlssExternalBackend) ++ return nullptr; // Continue through the original loader. + LOG_INFO("{} call!", libNameA); + + return LibraryLoadHooks::LoadNvApi(); +diff --git a/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp b/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp +index 68e1b42..89c9ba8 100644 +--- a/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp ++++ b/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp +@@ -687,6 +687,7 @@ static NVSDK_NGX_Result TryCreateOptiFeature(ID3D12GraphicsCommandList* InCmdLis + } + + // Assign handle ++ const bool allocatedHandle = *OutHandle == nullptr; + if (*OutHandle == nullptr) + *OutHandle = new NVSDK_NGX_Handle { handleId }; + else +@@ -705,6 +706,16 @@ static NVSDK_NGX_Result TryCreateOptiFeature(ID3D12GraphicsCommandList* InCmdLis + } + else + { ++ if (state.dlssExternalBackend && upscalerBackend == Upscaler::DLSS) ++ { ++ LOG_ERROR("External DLSS initialization failed; refusing FSR fallback"); ++ if (shouldRestoreSigs) ++ D3D12Hooks::RestoreRoot(InCmdList); ++ D3D12Hooks::SetRootSignatureTracking(true); ++ Dx12Contexts.erase(handleId); ++ if (allocatedHandle) { delete *OutHandle; *OutHandle = nullptr; } ++ return NVSDK_NGX_Result_Fail; ++ } + LOG_ERROR("Feature '{}' initialization failed falling back to FSR 2.1.2", UpscalerDisplayName(upscalerBackend)); + state.newBackend = Upscaler::FSR21; + state.changeBackend[handleId] = true; +@@ -1027,6 +1038,12 @@ static NVSDK_NGX_Result TryEvaluateOptiFeature(ID3D12GraphicsCommandList* InCmdL + // Fallback to FSR 2.1.2 if feature failed to initialize and user didn't explicitly request it + if (!feature->IsInited() && cfg.Dx12Upscaler.value_or_default() != Upscaler::FSR21) + { ++ if (state.dlssExternalBackend && cfg.Dx12Upscaler.value_or_default() == Upscaler::DLSS) ++ { ++ LOG_ERROR("External DLSS feature is not initialized; refusing FSR fallback"); ++ D3D12Hooks::SetRootSignatureTracking(true); ++ return NVSDK_NGX_Result_Fail; ++ } + LOG_WARN("Feature '{}' failed to initialize. Falling back to FSR 2.1.2", feature->Name()); + ImGui::InsertNotification({ ImGuiToastType::Warning, 10000, "Falling back to FSR 2.1.2" }); + +diff --git a/OptiScaler/misc/IdentifyGpu.cpp b/OptiScaler/misc/IdentifyGpu.cpp +index 3877f9f..6c05904 100644 +--- a/OptiScaler/misc/IdentifyGpu.cpp ++++ b/OptiScaler/misc/IdentifyGpu.cpp +@@ -8,6 +8,7 @@ + #include + #include "nvapi/NvApiTypes.h" + #include ++#include + + using Microsoft::WRL::ComPtr; + +@@ -187,6 +188,25 @@ std::vector IdentifyGpu::checkGpuInfo() + + for (auto& gpuInfo : localCachedInfo) + { ++ if (State::Instance().dlssExternalBackend && !gpuInfo.softwareAdapter) ++ { ++ // Query the configured D3D12 backend outside DLL_PROCESS_ATTACH. ++ // Preserve vendor/device/architecture data obtained from DXGI. ++ auto requirements = NVNGXProxy::D3D12_GetFeatureRequirements(); ++ ComPtr physicalAdapter; ++ if (requirements && SUCCEEDED(factory->EnumAdapterByLuid(gpuInfo.luid, IID_PPV_ARGS(&physicalAdapter)))) ++ { ++ NVSDK_NGX_FeatureDiscoveryInfo discovery {}; ++ discovery.SDKVersion = NVSDK_NGX_Version_API; ++ discovery.FeatureID = NVSDK_NGX_Feature_SuperSampling; ++ NVSDK_NGX_FeatureRequirement supported {}; ++ const auto result = requirements(physicalAdapter.Get(), &discovery, &supported); ++ gpuInfo.dlssCapable = result == NVSDK_NGX_Result_Success && ++ supported.FeatureSupported == NVSDK_NGX_FeatureSupportResult_Supported; ++ LOG_INFO("External D3D12 DLSS backend: {} support={} result={:X}", ++ gpuInfo.name, gpuInfo.dlssCapable, (unsigned) result); ++ } ++ } + if (gpuInfo.vendorId == VendorId::Nvidia) + { + queryNvapi(gpuInfo); +diff --git a/OptiScaler/proxies/NVNGX_Proxy.h b/OptiScaler/proxies/NVNGX_Proxy.h +index f6c08e5..7892009 100644 +--- a/OptiScaler/proxies/NVNGX_Proxy.h ++++ b/OptiScaler/proxies/NVNGX_Proxy.h +@@ -500,7 +500,10 @@ class NVNGXProxy + { + LOG_INFO("Getting nvngx method addresses"); + +- HookNgxApi(_module.dll); ++ // The external bridge reports physical adapter support itself. ++ // Do not overwrite its unsupported result with NVIDIA spoofing. ++ if (!State::Instance().dlssExternalBackend) ++ HookNgxApi(_module.dll); + + _module.D3D11_Init = + (PFN_D3D11_Init) KernelBaseProxy::GetProcAddress_()(_module.dll, "NVSDK_NGX_D3D11_Init"); +diff --git a/OptiScaler/upscalers/FeatureProvider_Dx12.cpp b/OptiScaler/upscalers/FeatureProvider_Dx12.cpp +index d738498..1191b29 100644 +--- a/OptiScaler/upscalers/FeatureProvider_Dx12.cpp ++++ b/OptiScaler/upscalers/FeatureProvider_Dx12.cpp +@@ -42,6 +42,11 @@ bool FeatureProvider_Dx12::GetFeature(Upscaler upscaler, UINT handleId, NVSDK_NG + break; + + case Upscaler::DLSS: ++ if (state.dlssExternalBackend && (!primaryGpu.dlssCapable || !state.NVNGX_DLSS_Path.has_value())) ++ { ++ LOG_ERROR("Configured external D3D12 DLSS backend is unavailable; refusing FSR fallback"); ++ return false; ++ } + if (primaryGpu.dlssCapable && state.NVNGX_DLSS_Path.has_value()) + { + *feature = std::make_unique(handleId, parameters); +@@ -77,6 +82,11 @@ bool FeatureProvider_Dx12::GetFeature(Upscaler upscaler, UINT handleId, NVSDK_NG + + if (!loaded) + { ++ if (state.dlssExternalBackend && upscaler == Upscaler::DLSS) ++ { ++ LOG_ERROR("External D3D12 DLSS module failed to load; refusing FSR fallback"); ++ return false; ++ } + // Fail after the constructor + ImGui::InsertNotification({ ImGuiToastType::Warning, 10000, "Falling back to FSR 2.1.2" }); + *feature = std::make_unique(handleId, parameters); +-- +2.55.0.windows.5 + diff --git a/patches/optiscaler/README.md b/patches/optiscaler/README.md new file mode 100644 index 0000000..a78aa54 --- /dev/null +++ b/patches/optiscaler/README.md @@ -0,0 +1,40 @@ +# Explicit Windows d4r backend + +Base: OptiScaler `45a2001303ddff632e279f77aef85ceede5832cb` (2026-09-30). +The modifications to OptiScaler are GPL-3.0, matching its upstream license. +No NVIDIA DLL is supplied or patched. + +`0001` introduces `[DLSS] AllowExternalBackend=true`, default false. Set +`[Libraries] NvngxPath` to the absolute directory containing the Windows d4r +shim as `_nvngx.dll`. Discovery requires its GPU-free ABI-1 marker. Physical +adapter support is queried through the bridge's D3D12 requirements API after +DLL startup; vendor, LUID and HIP architecture are retained. + +The configured `D4R_NGX_CORE` absolute path bypasses game-facing NGX DLL +redirection. The external backend loads its own NVAPI provider. An explicitly +selected external DLSS backend reports initialization/load errors instead of +silently switching to FSR. The patch fixes the upstream loader's filesystem +exception by making IsSubpath a lexical check, and retains a PDB for diagnosis. + +Build with Microsoft Build Tools 2022, v143 and Windows SDK 10.0.26100: + +```powershell +.\scripts\windows\build-optiscaler-windows.ps1 +``` + +The script clones the pinned source if absent, checks/applies the patch, +initializes pinned submodules, invokes MSBuild and writes the DLL, PDB, license +and build metadata into `dist/optiscaler-windows-d4r`. Existing source is not +reset. The upstream packaging post-build event is disabled for this build. +The official bootstrapper is https://aka.ms/vs/17/release/vs_buildtools.exe; +install the `Microsoft.VisualStudio.Workload.VCTools`, +`Microsoft.VisualStudio.Component.VC.Tools.x86.x64` and +`Microsoft.VisualStudio.Component.Windows11SDK.26100` components. The audited +local build uses MSVC 14.44.35207 / Build Tools 17.14. + +For standalone verification add +`-OptiScalerDll /OptiScaler.dll -CaptureExceptions` +to the documented K/M runner. Its private INI selects DLSS, disables frame +generation and keeps RestoreComputeSignature, RestoreGraphicSignature and +ExtendedStateRestore false. Use `-RequireNativeNetwork`; finite output alone +cannot distinguish DLSS from an unwanted FSR replacement. diff --git a/scripts/windows/build-optiscaler-windows.ps1 b/scripts/windows/build-optiscaler-windows.ps1 new file mode 100644 index 0000000..20bc250 --- /dev/null +++ b/scripts/windows/build-optiscaler-windows.ps1 @@ -0,0 +1,75 @@ +[CmdletBinding()] +param( + [string]$SourceRoot, + [string]$BuildToolsRoot, + [string]$InstallRoot, + [int]$Jobs = 6 +) +$ErrorActionPreference = 'Stop' +$repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) +if (!$SourceRoot) { $SourceRoot = Join-Path $repo 'external/OptiScaler' } +if (!$InstallRoot) { $InstallRoot = Join-Path $repo 'dist/optiscaler-windows-d4r' } +$SourceRoot = [IO.Path]::GetFullPath($SourceRoot) +$InstallRoot = [IO.Path]::GetFullPath($InstallRoot) +$base = '45a2001303ddff632e279f77aef85ceede5832cb' +$patch = Join-Path $repo 'patches/optiscaler/0001-windows-external-d4r-backend.patch' +$previousPath = $env:PATH +try { + # Git for Windows' submodule helper needs its POSIX utility directory. + $gitExe = (Get-Command git -ErrorAction Stop).Source + $gitInstall = [IO.Path]::GetFullPath((Join-Path (Split-Path $gitExe) '..')) + $gitUtilities = Join-Path $gitInstall 'usr/bin' + if (Test-Path -LiteralPath $gitUtilities) { $env:PATH = "$gitUtilities;$env:PATH" } + if (!(Test-Path -LiteralPath (Join-Path $SourceRoot '.git'))) { + & git clone --filter=blob:none --no-checkout https://github.com/optiscaler/OptiScaler.git $SourceRoot + if ($LASTEXITCODE) { throw 'OptiScaler clone failed.' } + & git -C $SourceRoot checkout --detach $base + if ($LASTEXITCODE) { throw 'Pinned OptiScaler checkout failed.' } + } + & git -C $SourceRoot merge-base --is-ancestor $base HEAD + if ($LASTEXITCODE) { throw "OptiScaler must be based on audited commit $base" } + $ErrorActionPreference = 'Continue' + & git -C $SourceRoot apply --reverse --check $patch 2>$null + $alreadyApplied = $LASTEXITCODE -eq 0 + $ErrorActionPreference = 'Stop' + if (!$alreadyApplied) { + & git -C $SourceRoot apply --check $patch + if ($LASTEXITCODE) { throw 'External backend patch conflicts with the existing source; no files changed.' } + & git -C $SourceRoot apply $patch + if ($LASTEXITCODE) { throw 'External backend patch failed.' } + } + & git -C $SourceRoot submodule update --init --depth 1 + if ($LASTEXITCODE) { throw 'Pinned OptiScaler submodules failed.' } + if (!$BuildToolsRoot -and (Test-Path -LiteralPath (Join-Path $repo '.tools/vs2022/MSBuild/Current/Bin/MSBuild.exe'))) { + $BuildToolsRoot = Join-Path $repo '.tools/vs2022' + } + if (!$BuildToolsRoot) { + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio/Installer/vswhere.exe' + if (Test-Path -LiteralPath $vswhere) { + $BuildToolsRoot = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + } + } + if (!$BuildToolsRoot) { throw 'MSVC v143 Build Tools and Windows SDK 10.0.26100 are required; see patches/optiscaler/README.md.' } + $msbuild = Join-Path $BuildToolsRoot 'MSBuild/Current/Bin/MSBuild.exe' + if (!(Test-Path -LiteralPath $msbuild)) { throw "MSBuild missing: $msbuild" } + New-Item -ItemType Directory -Force $InstallRoot | Out-Null + $log = Join-Path $InstallRoot 'build.log' + $ErrorActionPreference = 'Continue' + & $msbuild (Join-Path $SourceRoot 'OptiScaler.sln') "/m:$Jobs" /p:Configuration=Release /p:Platform=x64 ` + /p:PostBuildEventUseInBuild=false /verbosity:quiet "/flp:logfile=$log;verbosity=normal" + $ErrorActionPreference = 'Stop' + if ($LASTEXITCODE) { throw "OptiScaler build failed ($LASTEXITCODE); see $log" } + foreach ($file in @('OptiScaler.dll', 'OptiScaler.pdb')) { + Copy-Item -LiteralPath (Join-Path $SourceRoot "x64/Release/$file") -Destination $InstallRoot -Force + } + Copy-Item -LiteralPath (Join-Path $SourceRoot 'LICENSE') -Destination (Join-Path $InstallRoot 'GPL-3.0.txt') -Force + Copy-Item -LiteralPath $patch -Destination $InstallRoot -Force + $metadata = @{base=$base; sourceCommit=(& git -C $SourceRoot rev-parse HEAD).Trim(); + sourceDirty=[bool](& git -C $SourceRoot status --porcelain); backendAbi=1; + buildTools=$BuildToolsRoot; msbuildVersion=(& $msbuild -version -nologo | Select-Object -Last 1); + patchSha256=(Get-FileHash -LiteralPath $patch -Algorithm SHA256).Hash; + dllSha256=(Get-FileHash -LiteralPath (Join-Path $InstallRoot 'OptiScaler.dll') -Algorithm SHA256).Hash; + submodules=@(& git -C $SourceRoot submodule status)} + $metadata | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $InstallRoot 'build-info.json') -Encoding UTF8 + Write-Host "Built OptiScaler with external Windows d4r backend: $InstallRoot" +} finally { $env:PATH = $previousPath } diff --git a/scripts/windows/test-windows-rdna4.ps1 b/scripts/windows/test-windows-rdna4.ps1 index 55d4873..d949758 100644 --- a/scripts/windows/test-windows-rdna4.ps1 +++ b/scripts/windows/test-windows-rdna4.ps1 @@ -15,6 +15,7 @@ param( [switch]$Trace, [switch]$RequireNativeNetwork, [switch]$CommandListBackend, + [switch]$CaptureExceptions, [ValidateSet('baseline','packed','unorm')][string]$PixelProfile = 'baseline', [ValidateSet('legacy','enhanced')][string]$BarrierMode = 'legacy', [int]$Iterations = 32, @@ -71,6 +72,11 @@ function Quote-Argument([string]$Value) { } function Invoke-Probe([string]$Name, [string]$Exe, [string[]]$Arguments) { if (!(Test-Path -LiteralPath $Exe)) { throw "Executable missing: $Exe" } + if ($CaptureExceptions) { + $Arguments = @('--output-directory', $OutputDirectory, '--', $Exe) + $Arguments + $Exe = Join-Path $PackageRoot 'bin/d4r_debug_launcher.exe' + if (!(Test-Path -LiteralPath $Exe)) { throw "Debugger launcher missing: $Exe" } + } $stdout = Join-Path $OutputDirectory "$Name.stdout.log" $stderr = Join-Path $OutputDirectory "$Name.stderr.log" $argumentLine = ($Arguments | ForEach-Object { Quote-Argument $_ }) -join ' ' diff --git a/tools/windows/d3d12_dlss_probe.cpp b/tools/windows/d3d12_dlss_probe.cpp index 4aacd3b..add50bd 100644 --- a/tools/windows/d3d12_dlss_probe.cpp +++ b/tools/windows/d3d12_dlss_probe.cpp @@ -193,7 +193,16 @@ int main(int argc, char** argv) { for (const char* name : {"DLSS.Hint.Render.Preset.DLAA", "DLSS.Hint.Render.Preset.Quality", "DLSS.Hint.Render.Preset.Balanced", "DLSS.Hint.Render.Preset.Performance", "DLSS.Hint.Render.Preset.UltraPerformance", "DLSS.Hint.Render.Preset.UltraQuality"}) d4r_ngx_set_uint(parameters, name, args.preset); void* handle = nullptr; - check(shim.symbol("NVSDK_NGX_D3D12_CreateFeature")(nullptr, 1, parameters, &handle), "D3D12 CreateFeature"); + // Public NGX and OptiScaler require an open creation command list. + // Submit it independently from the pending synthetic input producer. + ComPtr creationAllocator; + ComPtr creationList; + dx(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(creationAllocator.GetAddressOf())), "Create feature allocator"); + dx(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, creationAllocator.Get(), nullptr, + IID_PPV_ARGS(creationList.GetAddressOf())), "Create feature command list"); + check(shim.symbol("NVSDK_NGX_D3D12_CreateFeature")(creationList.Get(), 1, parameters, &handle), "D3D12 CreateFeature"); + dx(creationList->Close(), "Close feature creation list"); + ID3D12CommandList* creationBatch[] = {creationList.Get()}; queue->ExecuteCommandLists(1, creationBatch); drain(); auto release = shim.symbol("NVSDK_NGX_D3D12_ReleaseFeature"); struct FeatureCleanup { Release fn; void* h; ~FeatureCleanup() { (void)fn(h); } } featureCleanup{release, handle}; static const char* names[] = {"Color", "Depth", "MotionVectors", "Output", "ExposureTexture"}; diff --git a/tools/windows/debug_launcher.cpp b/tools/windows/debug_launcher.cpp new file mode 100644 index 0000000..a9fed60 --- /dev/null +++ b/tools/windows/debug_launcher.cpp @@ -0,0 +1,130 @@ +// Out-of-process crash capture: unaffected by the frontend's loader hooks. +#include "diagnostic.h" +#include +#include + +namespace { +std::wstring quote(const std::wstring& value) { + std::wstring result = L"\""; unsigned slashes = 0; + for (wchar_t c : value) { + if (c == L'\\') { ++slashes; continue; } + result.append(slashes * (c == L'\"' ? 2 : 1), L'\\'); slashes = 0; + if (c == L'\"') result += L'\\'; + result += c; + } + result.append(slashes * 2, L'\\'); return result + L'\"'; +} +std::string file_path(HANDLE file) { + wchar_t path[32768]{}; + const auto length = GetFinalPathNameByHandleW(file, path, 32768, FILE_NAME_NORMALIZED); + return length && length < 32768 ? d4r::diag::utf8(path) : "unknown"; +} +struct Module { std::string path; uint32_t size; }; +uint32_t module_size(HANDLE process, uintptr_t base) { + IMAGE_DOS_HEADER dos{}; IMAGE_NT_HEADERS64 nt{}; SIZE_T read = 0; + if (!ReadProcessMemory(process, reinterpret_cast(base), &dos, sizeof(dos), &read) || + dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0 || dos.e_lfanew > 65536 || + !ReadProcessMemory(process, reinterpret_cast(base + dos.e_lfanew), &nt, sizeof(nt), &read) || + nt.Signature != IMAGE_NT_SIGNATURE) return 0; + return nt.OptionalHeader.SizeOfImage; +} +} +int main(int argc, char** argv) { + try { + if (argc < 5 || std::strcmp(argv[1], "--output-directory") || std::strcmp(argv[3], "--")) + throw std::runtime_error("Usage: d4r_debug_launcher --output-directory PATH -- EXE [arguments]"); + const std::filesystem::path directory(d4r::diag::wide(argv[2])); + std::filesystem::create_directories(directory); + std::ofstream log(directory / L"debugger.log", std::ios::app); + const auto executable = d4r::diag::wide(argv[4]); + std::wstring command; + for (int i = 4; i < argc; ++i) { if (i > 4) command += L' '; command += quote(d4r::diag::wide(argv[i])); } + STARTUPINFOW startup{sizeof(startup)}; PROCESS_INFORMATION child{}; + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + startup.hStdError = GetStdHandle(STD_ERROR_HANDLE); + HANDLE job = CreateJobObjectW(nullptr, nullptr); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!job || !SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) + throw std::runtime_error("Cannot create diagnostic process lifetime job"); + if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, + DEBUG_ONLY_THIS_PROCESS | CREATE_NO_WINDOW, nullptr, nullptr, &startup, &child)) + throw std::runtime_error("CreateProcess debugger error=" + std::to_string(GetLastError())); + if (!AssignProcessToJobObject(job, child.hProcess)) { + TerminateProcess(child.hProcess, 1); CloseHandle(job); + throw std::runtime_error("AssignProcessToJobObject debugger failed"); + } + std::map modules; + bool initialBreakpoint = true, dumped = false; DWORD exitCode = 1; + auto add_module = [&](void* base, HANDLE file) { + const auto address = reinterpret_cast(base); + modules[address] = {file_path(file), module_size(child.hProcess, address)}; + log << "LOAD base=0x" << std::hex << address << " size=0x" << modules[address].size << " path=" << modules[address].path << std::endl; + if (file && file != INVALID_HANDLE_VALUE) CloseHandle(file); + }; + auto address_info = [&](uintptr_t address) { + auto it = modules.upper_bound(address); + if (it != modules.begin()) { + --it; + if (address - it->first < it->second.size) + log << " module=" << it->second.path << "+0x" << std::hex << address - it->first; + } + }; + DEBUG_EVENT event{}; + while (WaitForDebugEvent(&event, INFINITE)) { + DWORD disposition = DBG_CONTINUE; + if (event.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) { + add_module(event.u.CreateProcessInfo.lpBaseOfImage, event.u.CreateProcessInfo.hFile); + CloseHandle(event.u.CreateProcessInfo.hProcess); CloseHandle(event.u.CreateProcessInfo.hThread); + } else if (event.dwDebugEventCode == CREATE_THREAD_DEBUG_EVENT) CloseHandle(event.u.CreateThread.hThread); + else if (event.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT) add_module(event.u.LoadDll.lpBaseOfDll, event.u.LoadDll.hFile); + else if (event.dwDebugEventCode == UNLOAD_DLL_DEBUG_EVENT) { + const auto base = reinterpret_cast(event.u.UnloadDll.lpBaseOfDll); + log << "UNLOAD base=0x" << std::hex << base << std::endl; modules.erase(base); + } else if (event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) { + const auto& exception = event.u.Exception; + const DWORD code = exception.ExceptionRecord.ExceptionCode; + log << "EXCEPTION code=0x" << std::hex << code << " firstChance=" << exception.dwFirstChance + << " address=0x" << reinterpret_cast(exception.ExceptionRecord.ExceptionAddress) << std::endl; + disposition = DBG_EXCEPTION_NOT_HANDLED; + if (initialBreakpoint && code == EXCEPTION_BREAKPOINT) { initialBreakpoint = false; disposition = DBG_CONTINUE; } + if (!dumped && (!exception.dwFirstChance || code == EXCEPTION_ACCESS_VIOLATION || code == EXCEPTION_ILLEGAL_INSTRUCTION || code == EXCEPTION_STACK_OVERFLOW || code == EXCEPTION_IN_PAGE_ERROR)) { + dumped = true; + log << "FAULT code=0x" << std::hex << code << " address=0x" << reinterpret_cast(exception.ExceptionRecord.ExceptionAddress) + << " thread=" << std::dec << event.dwThreadId << " firstChance=" << exception.dwFirstChance; + address_info(reinterpret_cast(exception.ExceptionRecord.ExceptionAddress)); log << std::endl; + HANDLE thread = OpenThread(THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, event.dwThreadId); + CONTEXT context{}; context.ContextFlags = CONTEXT_ALL; + if (thread && GetThreadContext(thread, &context)) { + log << "CONTEXT rip=0x" << std::hex << context.Rip << " rsp=0x" << context.Rsp << " rcx=0x" << context.Rcx << " rdx=0x" << context.Rdx << std::endl; + EXCEPTION_RECORD record = exception.ExceptionRecord; + EXCEPTION_POINTERS pointers{&record, &context}; + MINIDUMP_EXCEPTION_INFORMATION info{event.dwThreadId, &pointers, FALSE}; + const auto path = directory / (L"crash-" + std::to_wstring(child.dwProcessId) + L".dmp"); + HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); + if (file != INVALID_HANDLE_VALUE) { + const BOOL ok = MiniDumpWriteDump(child.hProcess, child.dwProcessId, file, + static_cast(MiniDumpNormal | MiniDumpWithThreadInfo), &info, nullptr, nullptr); + log << "MINIDUMP success=" << ok << " error=" << (ok ? 0 : GetLastError()) << std::endl; CloseHandle(file); + } + uintptr_t stack[256]{}; SIZE_T read = 0; + if (ReadProcessMemory(child.hProcess, reinterpret_cast(context.Rsp), stack, sizeof(stack), &read)) + for (unsigned i = 0; i < read / sizeof(uintptr_t); ++i) { + log << "STACK slot=" << std::dec << i << " address=0x" << std::hex << stack[i]; address_info(stack[i]); log << '\n'; + } + } + if (thread) CloseHandle(thread); log.flush(); + } + } else if (event.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) { + exitCode = event.u.ExitProcess.dwExitCode; + log << "EXIT code=0x" << std::hex << exitCode << std::endl; + ContinueDebugEvent(event.dwProcessId, event.dwThreadId, disposition); break; + } + ContinueDebugEvent(event.dwProcessId, event.dwThreadId, disposition); + } + CloseHandle(child.hThread); CloseHandle(child.hProcess); CloseHandle(job); + return static_cast(exitCode); + } catch (const std::exception& error) { std::fprintf(stderr, "DEBUG_LAUNCH_FAILURE %s\n", error.what()); return 1; } +}