Fix Windows NVAPI loading the gfx1101 build on every other AMD GPU

0.1.5 shipped one d4r/zluda/nvapi64.dll (the gfx1101 build) for all
targets, and compat_nvapi_path() preferred it over d4r/nvapi/<arch>/.
Nothing on Windows ever set D4R_GPU_ARCH, so on any other GPU NVAPI
failed with "No selected gfx1101 device", NGX CUDA init failed, and
DLSS stayed unavailable (black frames, no OptiScaler upscaler section).

- Select the NVAPI per architecture; never fall back to a shared DLL
  in a multi-target folder, and report a missing build by name.

- The loader and runtime publish D4R_GPU_ARCH and D4R_GPU_LUID for the
  game's adapter, so NVAPI picks the right device with several GPUs.

- nvapi64.dll exports the target it was built for; the runtime refuses
  a mismatch, and the packager and d4r-check read the same marker.

- Release packaging no longer ships d4r/zluda/nvapi64.dll and rejects
  NVAPI builds for the wrong target, without a marker, or missing.

- Move target resolution, device matching and loader choice into
  gpu_selection.h; regression tests cover all 11 targets, bad or
  missing D4R_GPU_ARCH, mixed installs, multi-GPU and missing files.

- d4r-check: accept the FSR keys in the shipped d4r.ini.
This commit is contained in:
ayo committed 2026-10-18 12:19:37 -04:00
1 parent df9f2cd012
commit 7da2f9ac39
16 files changed
+530 -46

No files matched your search

+15 -11
View File
@@ -6,6 +6,7 @@
// Keeping every file in one folder preserves NGX's search for
// nvngx_dlss.dll beside the calling module.
#include "d3d12_external.h"
#include "gpu_environment.h"
#include "ngx_public_abi.h"
#include <dxgi.h>
#include <fstream>
@@ -54,22 +55,25 @@ HMODULE select(const LUID* luid) {
d4r::diag::HipApi hip((dir / L"hip").string());
if (hip.hipInit(0) != hipSuccess) throw std::runtime_error("the AMD HIP runtime found no usable GPU; update the AMD graphics driver");
int count = 0; (void)hip.hipGetDeviceCount(&count);
std::string found, detected;
std::vector<d4r::diag::GpuCandidate> devices;
for (int i = 0; i < count; ++i) {
hipDeviceProp_t props{};
if (hip.hipGetDeviceProperties(&props, i) != hipSuccess) continue;
const auto arch = d4r::diag::gpu_architecture(props.gcnArchName);
detected += (detected.empty() ? "" : ", ") + arch + " (" + props.name + ")";
if (luid && std::memcmp(props.luid, luid, sizeof(*luid))) continue;
if (!luid && !std::filesystem::exists(dir / (L"_nvngx_" + d4r::diag::wide(arch) + L".dll"))) continue;
if (!found.empty() && found != arch) throw std::runtime_error("several GPUs with different architectures; start the game once DLSS selects one");
found = arch;
d4r::diag::GpuCandidate device{props.name, d4r::diag::gpu_architecture(props.gcnArchName), {}};
std::memcpy(device.luid.data(), props.luid, device.luid.size());
devices.push_back(device);
}
if (found.empty()) throw std::runtime_error("no supported AMD GPU for this game's adapter. Detected: " +
(detected.empty() ? std::string("none") : detected) + ". This release supports: " + supported_list(dir) + ".");
d4r::diag::AdapterLuid wanted{};
if (luid) std::memcpy(wanted.data(), luid, wanted.size());
const auto choice = d4r::diag::choose_loader_architecture(devices, luid ? &wanted : nullptr,
[&](const std::string& arch) { return std::filesystem::exists(dir / (L"_nvngx_" + d4r::diag::wide(arch) + L".dll")); },
supported_list(dir));
if (!choice.error.empty()) throw std::runtime_error(choice.error);
const auto& found = choice.architecture;
const auto path = dir / (L"_nvngx_" + d4r::diag::wide(found) + L".dll");
if (!std::filesystem::exists(path))
throw std::runtime_error("this release has no build for your GPU (" + found + "). It supports: " + supported_list(dir) + ".");
// The shim, NVAPI and the NGX core start from the environment: state the adapter's real architecture (and,
// once the game's adapter is known, its LUID) before the first of them loads, replacing any stale value.
d4r::diag::publish_selected_gpu(found, luid ? &wanted : nullptr);
shim = LoadLibraryExW(path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
if (!shim) {
selectionFailed = true; // A broken install will not fix itself during this session.
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Publishes the GPU the game renders on to the rest of the process. The NVAPI
// compatibility DLL, the NGX core's CUDA initialization and any later shim
// start from the environment, so the loader and the shim state the adapter's
// real architecture and LUID before they load them.
#include "gpu_selection.h"
#include <windows.h>
#include <cstdlib>
namespace d4r::diag {
// Updates the OS block (other DLLs, ZLUDA) and the CRT copy that getenv reads.
inline void set_process_variable(const char* name, const std::string& value) {
SetEnvironmentVariableA(name, value.c_str());
_putenv_s(name, value.c_str());
}
inline void publish_selected_gpu(const std::string& architecture, const AdapterLuid* luid = nullptr) {
set_process_variable("D4R_GPU_ARCH", architecture);
if (luid) set_process_variable("D4R_GPU_LUID", format_luid(*luid));
}
}
+121
View File
@@ -0,0 +1,121 @@
#pragma once
// GPU-architecture selection rules shared by the loader, the NGX shim and the
// NVAPI compatibility DLL. Pure logic: no HIP, no Windows headers, so the same
// code is unit-tested without a GPU (see player_config_probe.cpp).
//
// One physical adapter decides everything: its HIP architecture picks the
// _nvngx_<arch>.dll shim, the nvapi\<arch>\nvapi64.dll and the native kernels,
// and its LUID picks the device when several GPUs share an architecture.
#include "gpu_target.h"
#include <array>
#include <cstdio>
#include <functional>
#include <string>
#include <vector>
namespace d4r::diag {
using AdapterLuid = std::array<unsigned char, 8>; // Memory layout of LUID: LowPart, then HighPart.
struct GpuCandidate { std::string name, architecture; AdapterLuid luid{}; };
inline std::string supported_gpu_list() {
std::string list;
for (const auto& target : gpu_targets) list += (list.empty() ? "" : ", ") + std::string(target.architecture);
return list;
}
inline std::string describe_gpus(const std::vector<GpuCandidate>& devices) {
std::string text;
for (const auto& device : devices) text += (text.empty() ? "" : ", ") + device.architecture + " (" + device.name + ")";
return text.empty() ? "none" : text;
}
// "HHHHHHHH:LLLLLLLL", the form the logs already use for adapter LUIDs.
inline std::string format_luid(const AdapterLuid& luid) {
unsigned low = 0, high = 0;
for (int i = 0; i < 4; ++i) { low |= unsigned(luid[i]) << (8 * i); high |= unsigned(luid[4 + i]) << (8 * i); }
char text[32]; std::snprintf(text, sizeof(text), "%08x:%08x", high, low);
return text;
}
inline bool parse_luid(const char* text, AdapterLuid& luid) {
const std::string value = text ? text : "";
if (value.size() != 17 || value[8] != ':' || value.find_first_not_of("0123456789abcdefABCDEF:") != std::string::npos ||
value.find(':', 9) != std::string::npos) return false;
const unsigned high = std::stoul(value.substr(0, 8), nullptr, 16), low = std::stoul(value.substr(9), nullptr, 16);
for (int i = 0; i < 4; ++i) { luid[i] = static_cast<unsigned char>(low >> (8 * i)); luid[4 + i] = static_cast<unsigned char>(high >> (8 * i)); }
return true;
}
// D4R_GPU_ARCH, when valid, names the architecture this process runs for; the
// build's own target applies when it is absent. A set but unsupported value is
// reported and ignored: it must not silently pick a different GPU target.
struct TargetResolution { std::string architecture, source, warning; };
inline TargetResolution resolve_target_arch(const char* configured, const char* built_in) {
TargetResolution result{built_in, "build", {}};
if (!configured || !*configured) return result;
const std::string arch = gpu_architecture(configured); // "gfx1101:xnack-" -> "gfx1101"
if (supported_gpu_target(arch)) { result.architecture = arch; result.source = "D4R_GPU_ARCH"; return result; }
result.warning = std::string("D4R_GPU_ARCH='") + configured + "' is not a supported target (supported: " +
supported_gpu_list() + "); using the build's target " + built_in;
return result;
}
inline int find_gpu_device(const std::vector<GpuCandidate>& devices, const std::string& target, int requested = -1,
const AdapterLuid* luid = nullptr) {
for (size_t i = 0; i < devices.size(); ++i)
if (devices[i].architecture == target && (requested < 0 || requested == int(i)) && (!luid || devices[i].luid == *luid))
return int(i);
return -1;
}
// Why no device matched, in terms a player can act on: unsupported hardware,
// a file built for another GPU, or an adapter HIP cannot see.
inline std::string no_device_message(const std::vector<GpuCandidate>& devices, const std::string& target,
const AdapterLuid* luid = nullptr) {
const std::string prefix = "No selected " + target + " device for this adapter. ";
if (devices.empty()) return prefix + "The AMD HIP runtime reports no GPU; update the AMD graphics driver.";
const GpuCandidate* actual = nullptr;
if (luid) {
for (const auto& device : devices) if (device.luid == *luid) actual = &device;
if (!actual) return prefix + "No HIP device has the game's adapter LUID " + format_luid(*luid) + ". HIP sees: " + describe_gpus(devices) + ".";
} else {
for (const auto& device : devices) if (supported_gpu_target(device.architecture)) { actual = &device; break; }
}
if (!actual) return prefix + "Detected: " + describe_gpus(devices) + ", none supported by d4r (supported: " + supported_gpu_list() + ").";
if (!supported_gpu_target(actual->architecture))
return prefix + "The GPU is " + actual->architecture + " (" + actual->name + "), which d4r does not support (supported: " + supported_gpu_list() + ").";
return prefix + "The GPU is " + actual->architecture + " (" + actual->name + ") but this file is built for " + target +
". A file for another GPU was loaded (nvapi\\" + actual->architecture + "\\nvapi64.dll, _nvngx_" + actual->architecture +
".dll, or a stale D4R_GPU_ARCH); extract the d4r ZIP again.";
}
// An NVAPI (or any per-target file) that names another target than the selected GPU's is a mixed install.
inline std::string nvapi_target_error(const std::string& built, const std::string& wanted, const std::string& path) {
if (built == wanted) return {};
return "nvapi64.dll (" + path + ") is built for " + (built.empty() ? std::string("an unknown target") : built) +
" but the selected GPU is " + wanted + "; use d4r\\nvapi\\" + wanted + "\\nvapi64.dll and extract the d4r ZIP again if files are mixed up";
}
// Loader: the HIP architecture of the game's adapter (matched by LUID), or,
// before any adapter is known, the only architecture that has a build.
struct LoaderChoice { std::string architecture, error; };
inline LoaderChoice choose_loader_architecture(const std::vector<GpuCandidate>& devices, const AdapterLuid* luid,
const std::function<bool(const std::string&)>& has_build, const std::string& available) {
LoaderChoice choice;
for (const auto& device : devices) {
if (luid && device.luid != *luid) continue;
if (!luid && !has_build(device.architecture)) continue;
if (!choice.architecture.empty() && choice.architecture != device.architecture)
return {{}, "several GPUs with different architectures; start the game once DLSS selects one"};
choice.architecture = device.architecture;
}
if (choice.architecture.empty())
choice.error = "no supported AMD GPU for this game's adapter. Detected: " + describe_gpus(devices) +
". This release supports: " + available + ".";
else if (!supported_gpu_target(choice.architecture))
choice.error = "this GPU (" + choice.architecture + ") is not supported by d4r. This release supports: " + available + ".";
else if (!has_build(choice.architecture))
choice.error = "this release has no build for your GPU (" + choice.architecture + "). It supports: " + available + ".";
if (!choice.error.empty()) choice.architecture.clear();
return choice;
}
}
+30 -16
View File
@@ -1,6 +1,6 @@
#pragma once
#include "diagnostic.h"
#include "gpu_target.h"
#include "gpu_selection.h"
#include <hip/hip_runtime_api.h>
#include <cstdlib>
#include <string>
@@ -43,14 +43,16 @@ struct HipApi {
if (verbose || result != hipSuccess) std::printf("HIP %s -> %d (%s)\n", call, static_cast<int>(result), hipGetErrorName(result));
if (result != hipSuccess) throw std::runtime_error(std::string(call) + ": " + hipGetErrorString(result));
}
// The package's backend sets D4R_GPU_ARCH to the GPU type of the game's adapter before it loads this DLL, so one
// build serves every supported type (a compile-time-only target locked RDNA3 / gfx1201 cards out: "No selected
// gfx1200 device"). Unknown or missing values fall back to the build's own target.
// The architecture this process runs for: D4R_GPU_ARCH when it names a supported target, otherwise the build's
// own D4R_TARGET_ARCH. The loader and the shim publish the adapter's real architecture there before the
// NVAPI/NGX components start (publish_selected_gpu in gpu_environment.h), so every component agrees. A value that
// is set but unsupported is reported and ignored; select_architecture still refuses any device that is not of
// the architecture in use.
static const char* target_arch() {
static const std::string arch = [] {
const char* e = std::getenv("D4R_GPU_ARCH");
const std::string v = e ? gpu_architecture(e) : std::string(); // "gfx1101:xnack-" -> "gfx1101"
return supported_gpu_target(v) ? v : std::string(D4R_TARGET_ARCH);
const auto resolved = resolve_target_arch(std::getenv("D4R_GPU_ARCH"), D4R_TARGET_ARCH);
if (!resolved.warning.empty()) std::fprintf(stderr, "d4r: %s\n", resolved.warning.c_str());
return resolved.architecture;
}();
return arch.c_str();
}
@@ -59,6 +61,14 @@ struct HipApi {
const char* e = std::getenv("D4R_HIP_DEVICE");
return e && *e ? std::atoi(e) : -1;
}
// The adapter the loader or shim selected (D4R_GPU_LUID), for components that start before any D3D12 device is
// at hand. Absent means "any device of the target architecture"; a malformed value is an error, never ignored.
static bool configured_luid(AdapterLuid& luid) {
const char* text = std::getenv("D4R_GPU_LUID");
if (!text || !*text) return false;
if (!parse_luid(text, luid)) throw std::runtime_error(std::string("D4R_GPU_LUID='") + text + "' is not HHHHHHHH:LLLLLLLL");
return true;
}
int select_architecture(int requested, hipDeviceProp_t& selected, const void* required_luid = nullptr,
bool allow_no_match = false) const {
// Architecture spoofing invalidates a gfx12 correctness test.
@@ -71,24 +81,28 @@ struct HipApi {
std::printf("HIP runtime=%d driver_api=%d headers=%d.%d.%d sizeof(properties)=%zu\n",
version, driver, HIP_VERSION_MAJOR, HIP_VERSION_MINOR, HIP_VERSION_PATCH, sizeof(hipDeviceProp_t));
check(hipGetDeviceCount(&count), "hipGetDeviceCount");
int found = -1;
std::vector<GpuCandidate> devices;
requested = requested_device(requested);
AdapterLuid wanted{};
const AdapterLuid* luid = nullptr;
if (required_luid) { std::memcpy(wanted.data(), required_luid, wanted.size()); luid = &wanted; }
else if (configured_luid(wanted)) luid = &wanted;
std::vector<hipDeviceProp_t> properties;
for (int i = 0; i < count; ++i) {
hipDeviceProp_t props{};
check(hipGetDeviceProperties(&props, i), "hipGetDevicePropertiesR0600");
std::printf("GPU ordinal=%d name=%s gcnArchName=%s wave=%d vram=%zu pci=%04x:%02x:%02x\n",
i, props.name, props.gcnArchName, props.warpSize, props.totalGlobalMem,
props.pciDomainID, props.pciBusID, props.pciDeviceID);
const std::string arch = props.gcnArchName;
if (gpu_architecture(arch) == target_arch() && (requested < 0 || requested == i) && found < 0 &&
(!required_luid || std::memcmp(props.luid, required_luid, sizeof(props.luid)) == 0)) {
found = i;
selected = props;
}
GpuCandidate candidate{props.name, gpu_architecture(props.gcnArchName), {}};
std::memcpy(candidate.luid.data(), props.luid, candidate.luid.size());
devices.push_back(candidate);
properties.push_back(props);
}
const int found = find_gpu_device(devices, target_arch(), requested, luid);
if (found < 0 && allow_no_match) return -1; // Capability enumeration can include Intel/WARP adapters.
if (found < 0) throw std::runtime_error(std::string("No selected ") + target_arch() +
" device for this adapter; use the package matching the actual HIP architecture (see GPU log)");
if (found < 0) throw std::runtime_error(no_device_message(devices, target_arch(), luid));
selected = properties[found];
require_gpu_target(selected.gcnArchName, target_arch());
check(hipSetDevice(found), "hipSetDevice");
std::printf("SELECTED HIP ordinal=%d architecture=%s validation=%s\n", found, target_arch(),
+18
View File
@@ -1,5 +1,6 @@
#pragma once
#include "d3d12_external.h"
#include "gpu_environment.h"
#include "d3d12_boundary_timing.h"
#include "pixel_conversion.h"
#include "cuda_image_api.h"
@@ -23,6 +24,19 @@ inline std::string env_path(const char* name) {
if (!std::filesystem::path(diag::wide(value)).is_absolute()) throw std::runtime_error(std::string(name) + " must be absolute");
return value;
}
// The NVAPI compatibility DLL names the GPU target it was built for. A file for another GPU (a mixed install, or
// a shared copy from an older release) is refused here with the fix spelled out; older builds without the export
// are accepted and still validate the device themselves.
inline void require_nvapi_target(const diag::Library& nvapi, const std::string& path) {
using Target = const char*(__cdecl*)();
Target target = nullptr;
try { target = nvapi.symbol<Target>("d4r_NvapiTargetArchitecture"); }
catch (const std::exception&) { std::printf("D4R_NVAPI target=unknown (no d4r_NvapiTargetArchitecture export) path=%s\n", path.c_str()); return; }
const std::string built = target ? target() : "";
const std::string wanted = diag::HipApi::target_arch();
std::printf("D4R_NVAPI target=%s wanted=%s path=%s\n", built.c_str(), wanted.c_str(), path.c_str());
if (const auto error = diag::nvapi_target_error(built, wanted, path); !error.empty()) throw std::runtime_error(error);
}
inline void ngx_check(unsigned result, const char* operation) {
if (result != 1 || !std::getenv("D4R_QUIET_API")) std::printf("D4R_NGX %s result=0x%08x\n", operation, result);
if (result != 1) throw std::runtime_error(std::string(operation) + " NGX=" + std::to_string(result));
@@ -105,6 +119,9 @@ struct Runtime {
hip.select_architecture(-1, props, &luid);
hip.verbose = cuda.verbose = !std::getenv("D4R_QUIET_API");
if (std::memcmp(&luid, props.luid, sizeof(luid))) throw std::runtime_error("D3D12/HIP LUID mismatch");
// NVAPI and the NGX core start from the environment: tell them which adapter and architecture this is.
diag::AdapterLuid selectedLuid{}; std::memcpy(selectedLuid.data(), props.luid, selectedLuid.size());
diag::publish_selected_gpu(diag::HipApi::target_arch(), &selectedLuid);
cuda.check(cuda.cuInit(0), "cuInit(Windows runtime)");
int count = 0; cuda.check(cuda.cuDeviceGetCount(&count), "cuDeviceGetCount");
for (int i = 0; i < count; ++i) {
@@ -124,6 +141,7 @@ struct Runtime {
// NGX's loader can probe CUDA during DLL initialization: establish
// the correct primary context before loading either NVIDIA DLL.
nvapi = std::make_unique<diag::Library>(diag::wide(env_path("D4R_NVAPI_DLL")));
require_nvapi_target(*nvapi, env_path("D4R_NVAPI_DLL"));
dlss = std::make_unique<diag::Library>(diag::wide(env_path("D4R_DLSS_DLL")));
core = std::make_unique<diag::Library>(diag::wide(env_path("D4R_NGX_CORE")));
allocate = core->symbol<Allocate>("NVSDK_NGX_CUDA_AllocateParameters");
+9
View File
@@ -1,4 +1,5 @@
#include "diagnostic.h"
#include "gpu_target.h"
#if defined(D4R_NVAPI_COMPAT)
#include "hip_api.h"
#include <memory>
@@ -83,6 +84,14 @@ int __cdecl logical_info(void* logical, LogicalGpuData* info)
}
#endif
// The GPU target this DLL was built for. The marker string is also searched for by the packaging scripts, so a
// release cannot ship (and a shim cannot load) an NVAPI built for another GPU.
static const char target_marker[] = "D4R_NVAPI_TARGET=" D4R_TARGET_ARCH;
extern "C" __declspec(dllexport) const char* __cdecl d4r_NvapiTargetArchitecture()
{
return target_marker + sizeof("D4R_NVAPI_TARGET=") - 1;
}
// Trace build forwards responses unchanged. The compatibility build adds the
// public topology APIs above; other queries still go to upstream ZLUDA NVAPI.
extern "C" __declspec(dllexport) void* __cdecl nvapi_QueryInterface(unsigned id)
+41 -7
View File
@@ -272,11 +272,39 @@ inline void verify_native_fp8_bundle(const std::filesystem::path& dir, const Set
contents.find("tokenLanes=true\n") == std::string::npos)
throw std::runtime_error("[Kernels] NativeFp8=true needs matching gfx12 token-lane M kernels; rebuild the package with -TokenLanes and restart.");
}
inline std::filesystem::path compat_nvapi_path(const std::filesystem::path& dir) {
const auto colocated = dir / L"zluda" / L"nvapi64.dll";
if (std::filesystem::is_regular_file(colocated)) return colocated;
const auto arch = dir / L"nvapi" / diag::wide(diag::HipApi::target_arch()) / L"nvapi64.dll";
return std::filesystem::is_regular_file(arch) ? arch : dir / L"nvapi" / L"nvapi64.dll";
// The compatibility NVAPI is built for one GPU target, so it is never shared between targets. A release carries
// one per target in nvapi\<arch>\; that copy is the only acceptable one for <arch>. A single-target layout (quick
// test, game package) has just nvapi\nvapi64.dll or the copy beside nvcuda.dll, and the shim then checks the
// target the file names. In a multi-target folder a missing copy is an error: falling back to a shared DLL is
// how a gfx1101 NVAPI once served a gfx1151 GPU ("No selected gfx1101 device").
struct NvapiChoice { std::filesystem::path path; std::string error; };
inline NvapiChoice select_compat_nvapi(const std::filesystem::path& dir, const std::string& arch) {
const auto file = [&](const std::string& target) { return dir / L"nvapi" / diag::wide(target) / L"nvapi64.dll"; };
if (std::filesystem::is_regular_file(file(arch))) return {file(arch), {}};
std::string others;
for (const auto& target : diag::gpu_targets)
if (std::filesystem::is_regular_file(file(target.architecture)))
others += (others.empty() ? "" : ", ") + std::string(target.architecture);
if (!others.empty())
return {{}, "this release has no NVAPI build for " + arch + " (d4r\\nvapi\\" + arch + "\\nvapi64.dll is missing; it has " +
others + "); extract the d4r ZIP again"};
for (const auto& single : {dir / L"nvapi" / L"nvapi64.dll", dir / L"zluda" / L"nvapi64.dll"})
if (std::filesystem::is_regular_file(single)) return {single, {}};
return {{}, "nvapi64.dll is missing (expected d4r\\nvapi\\" + arch + "\\nvapi64.dll); extract the d4r ZIP again"};
}
inline std::filesystem::path compat_nvapi_path(const std::filesystem::path& dir, const std::string& arch = diag::HipApi::target_arch()) {
const auto choice = select_compat_nvapi(dir, arch);
if (!choice.error.empty()) throw std::runtime_error(choice.error);
return choice.path;
}
// A shared nvapi64.dll beside nvcuda.dll (d4r 0.1.5 shipped the gfx1101 build there for every GPU) is on PATH and
// could be loaded by name ahead of the right file; it is ignored here, so say so.
inline void note_stale_shared_nvapi(const std::filesystem::path& dir, std::vector<std::string>& notes) {
bool perTarget = false;
for (const auto& target : diag::gpu_targets)
perTarget = perTarget || std::filesystem::is_regular_file(dir / L"nvapi" / diag::wide(target.architecture) / L"nvapi64.dll");
if (perTarget && std::filesystem::is_regular_file(dir / L"zluda" / L"nvapi64.dll"))
notes.push_back("d4r\\zluda\\nvapi64.dll is left over from an older d4r release (one GPU's build shared by all); it is ignored. Delete it");
}
inline std::filesystem::path raw_nvapi_backend_path(const std::filesystem::path& dir) {
return dir / L"nvapi" / L"backend" / L"nvapi64.dll";
@@ -325,7 +353,10 @@ inline void apply(const std::filesystem::path& dir, const Settings& settings) {
const auto p = [&](const std::wstring& relative) { return std::optional<std::wstring>((dir / relative).wstring()); };
const std::optional<std::wstring> on = std::wstring(L"1"), off = std::wstring(L"0"), unset;
const wchar_t* path = _wgetenv(L"PATH");
const auto compatNvapi = compat_nvapi_path(dir);
const std::string arch = diag::HipApi::target_arch();
// Startup has already verified the file (verify_nvapi); a missing one leaves D4R_NVAPI_DLL unset so a stale
// value from an earlier run cannot point at a DLL for another GPU.
const auto nvapi = select_compat_nvapi(dir, arch);
// Same codegen and paths as windows-game.ps1; only diagnostics differ.
const std::pair<const wchar_t*, std::optional<std::wstring>> values[] = {
{L"D4R_RR_BACKEND", settings.rrBackend == "off" ? unset : std::optional<std::wstring>(diag::wide(settings.rrBackend))},
@@ -339,7 +370,8 @@ inline void apply(const std::filesystem::path& dir, const Settings& settings) {
{L"D4R_FSR_CACHE_DIR", settings.rrBackend == "fsr" ? std::optional<std::wstring>((cache / L"vkd3d").wstring()) : unset},
{L"D4R_HIP_ROOT", p(L"hip")}, {L"HIP_PATH", p(L"hip")},
{L"D4R_NVCUDA_DLL", p(L"zluda\\nvcuda.dll")}, {L"ZLUDA_CUDA_LIB", p(L"zluda\\nvcuda.dll")},
{L"D4R_NVAPI_DLL", std::optional<std::wstring>(compatNvapi.wstring())},
{L"D4R_GPU_ARCH", std::optional<std::wstring>(diag::wide(arch))},
{L"D4R_NVAPI_DLL", nvapi.error.empty() ? std::optional<std::wstring>(nvapi.path.wstring()) : unset},
{L"D4R_NVAPI_BACKEND", std::optional<std::wstring>(raw_nvapi_backend_path(dir).wstring())},
{L"D4R_NGX_CORE", p(L"ngx\\_nvngx.dll")}, {L"D4R_DLSS_DLL", p(L"dlss\\nvngx_dlss.dll")},
{L"D4R_NGX_DATA_DIR", std::optional<std::wstring>(ngxData.wstring())},
@@ -392,6 +424,8 @@ inline void ensure_environment() {
reject_translation_layer();
if (settings.nativeFp8) verify_native_fp8_bundle(dir, &settings);
verify_code_objects(dir, &settings);
compat_nvapi_path(dir); // Throws with the fix spelled out when this GPU has no NVAPI build.
note_stale_shared_nvapi(dir, current.notes);
apply(dir, settings);
current.active = true; current.preset = settings.preset;
const char* model = settings.preset == 5 ? "E" : settings.preset == 12 ? "L" : settings.preset == 13 ? "M" : "K";
+190 -2
View File
@@ -21,10 +21,194 @@ std::string error_of(const std::function<void()>& fn) {
try { fn(); } catch (const std::exception& error) { return error.what(); }
return {};
}
using d4r::diag::AdapterLuid;
using d4r::diag::GpuCandidate;
AdapterLuid luid_of(unsigned low, unsigned high = 0) {
AdapterLuid luid{};
for (int i = 0; i < 4; ++i) { luid[i] = static_cast<unsigned char>(low >> (8 * i)); luid[4 + i] = static_cast<unsigned char>(high >> (8 * i)); }
return luid;
}
void write_file(const std::filesystem::path& path, const std::string& text) {
std::filesystem::create_directories(path.parent_path());
std::ofstream(path, std::ios::binary) << text;
}
bool contains(const std::string& text, const std::string& part) { return text.find(part) != std::string::npos; }
// Architecture selection rules that decide which NVAPI, shim and kernels a GPU gets. Pure logic, so every
// registered target is covered here without the hardware; `builtNvapi` (optional) holds real nvapi64.dll
// builds, one per target, to check the compiled-in target marker the same way the shim and packager do.
void architecture_selection(const std::filesystem::path& root, const std::filesystem::path& builtNvapi) {
namespace diag = d4r::diag;
std::filesystem::remove_all(root);
std::vector<std::string> archs;
for (const auto& target : diag::gpu_targets) archs.push_back(target.architecture);
expect(archs.size() == 11, "the registry lists the 11 Windows GPU targets");
// D4R_GPU_ARCH: valid, suffixed, missing, empty and unsupported values.
for (const auto& arch : archs) {
auto r = diag::resolve_target_arch(arch.c_str(), "gfx1101");
expect(r.architecture == arch && r.source == "D4R_GPU_ARCH" && r.warning.empty(), "D4R_GPU_ARCH=" + arch + " selects " + arch);
r = diag::resolve_target_arch((arch + ":xnack-").c_str(), "gfx1201");
expect(r.architecture == arch, "feature-suffixed D4R_GPU_ARCH is normalized for " + arch);
r = diag::resolve_target_arch(nullptr, arch.c_str());
expect(r.architecture == arch && r.source == "build" && r.warning.empty(), "missing D4R_GPU_ARCH uses the build target " + arch);
r = diag::resolve_target_arch("", arch.c_str());
expect(r.architecture == arch && r.warning.empty(), "empty D4R_GPU_ARCH uses the build target " + arch);
}
for (const char* bad : {"gfx1030", "gfx9999", "GFX1101", "1101", "gfx1101 ", "rdna3"}) {
const auto r = diag::resolve_target_arch(bad, "gfx1151");
expect(r.architecture == "gfx1151" && r.source == "build" && contains(r.warning, std::string("'") + bad + "' is not a supported target"),
std::string("unsupported D4R_GPU_ARCH='") + bad + "' is reported and the build target is kept");
}
// LUID text used to hand the game's adapter to NVAPI.
for (const auto luid : {luid_of(0), luid_of(0x1a2b3c4d, 0), luid_of(0xdeadbeef, 0x7), luid_of(0xffffffff, 0xffffffff)}) {
AdapterLuid parsed{};
expect(diag::parse_luid(diag::format_luid(luid).c_str(), parsed) && parsed == luid, "LUID round trip " + diag::format_luid(luid));
}
AdapterLuid scratch{};
for (const char* bad : {"", "1234", "0000000000000000", "0000000:0000000a", "0000000g:00000001", "00000000:0000000100", "0x000000:00000001", "00000000:+0000001"})
expect(!diag::parse_luid(bad, scratch), std::string("malformed LUID '") + bad + "' is rejected");
// Release layout: nvapi\<arch>\nvapi64.dll for all targets plus the 0.1.5-style shared copy beside nvcuda.
const auto release = root / L"release";
for (const auto& arch : archs) write_file(release / L"nvapi" / diag::wide(arch) / L"nvapi64.dll", "nvapi " + arch);
write_file(release / L"zluda" / L"nvapi64.dll", "nvapi gfx1101 (shared)");
write_file(release / L"nvapi" / L"backend" / L"nvapi64.dll", "raw backend");
for (const auto& arch : archs) {
const auto choice = player::select_compat_nvapi(release, arch);
expect(choice.error.empty() && choice.path == release / L"nvapi" / diag::wide(arch) / L"nvapi64.dll" && player::read_text(choice.path) == "nvapi " + arch,
"NVAPI for " + arch + " is its own nvapi\\" + arch + " build, not the shared gfx1101 copy");
}
std::vector<std::string> notes;
player::note_stale_shared_nvapi(release, notes);
expect(has_note(notes, "left over from an older d4r release"), "a leftover shared NVAPI is called out");
// A missing per-target build is an error that names the fix; no other file may stand in for it.
for (const auto& arch : archs) {
const auto folder = root / (L"missing-" + diag::wide(arch));
std::filesystem::remove_all(folder);
std::filesystem::copy(release, folder, std::filesystem::copy_options::recursive);
std::filesystem::remove(folder / L"nvapi" / diag::wide(arch) / L"nvapi64.dll");
const auto choice = player::select_compat_nvapi(folder, arch);
expect(choice.path.empty() && contains(choice.error, "no NVAPI build for " + arch) && contains(choice.error, "nvapi\\" + arch + "\\nvapi64.dll"),
"missing nvapi\\" + arch + " fails with a clear message instead of using another DLL");
expect(!error_of([&] { player::compat_nvapi_path(folder, arch); }).empty(), "compat_nvapi_path(" + arch + ") throws when its build is missing");
std::filesystem::remove_all(folder);
}
// Single-target layouts (quick test, game package, development folders).
const auto single = root / L"single";
expect(contains(player::select_compat_nvapi(single, "gfx1151").error, "nvapi64.dll is missing"), "an empty folder has no NVAPI");
write_file(single / L"zluda" / L"nvapi64.dll", "single zluda");
expect(player::select_compat_nvapi(single, "gfx1151").path == single / L"zluda" / L"nvapi64.dll", "a single-target package may keep NVAPI beside nvcuda");
write_file(single / L"nvapi" / L"nvapi64.dll", "single nvapi");
expect(player::select_compat_nvapi(single, "gfx1151").path == single / L"nvapi" / L"nvapi64.dll", "nvapi\\nvapi64.dll outranks the copy beside nvcuda");
notes.clear(); player::note_stale_shared_nvapi(single, notes);
expect(notes.empty(), "single-target layouts are not flagged as stale");
// The environment the shim hands to ZLUDA, NVAPI and NGX.
{ // target_arch() is fixed per process; the per-target matrix above covers the other architectures.
const auto folder = root / L"apply";
for (const auto& other : archs) write_file(folder / L"nvapi" / diag::wide(other) / L"nvapi64.dll", "nvapi " + other);
auto s = settings("", notes); s.cacheDir = (folder / L"cache").wstring();
player::apply(folder, s);
const auto current = std::string(diag::HipApi::target_arch());
expect(_wgetenv(L"D4R_GPU_ARCH") && std::wstring(_wgetenv(L"D4R_GPU_ARCH")) == diag::wide(current), "player environment publishes D4R_GPU_ARCH=" + current);
expect(std::filesystem::path(_wgetenv(L"D4R_NVAPI_DLL")) == folder / L"nvapi" / diag::wide(current) / L"nvapi64.dll",
"player environment points D4R_NVAPI_DLL at the build for " + current);
std::filesystem::remove(folder / L"nvapi" / diag::wide(current) / L"nvapi64.dll");
player::apply(folder, s);
expect(!std::getenv("D4R_NVAPI_DLL"), "a missing build clears D4R_NVAPI_DLL instead of keeping a stale path");
}
// Several GPUs: iGPU + two dGPUs of one architecture + every registered target on one machine.
const std::vector<GpuCandidate> machine{{"AMD Radeon 780M", "gfx1103", luid_of(10)}, {"AMD Radeon RX 7700 XT", "gfx1101", luid_of(11)},
{"AMD Radeon RX 7700 XT #2", "gfx1101", luid_of(12)}, {"Radeon 8060S", "gfx1151", luid_of(13)}};
expect(diag::find_gpu_device(machine, "gfx1101") == 1, "without a LUID the first device of the architecture is used");
const auto second = luid_of(12), igpu = luid_of(10), unknown = luid_of(99);
expect(diag::find_gpu_device(machine, "gfx1101", -1, &second) == 2, "the LUID picks the second of two same-architecture GPUs");
expect(diag::find_gpu_device(machine, "gfx1103", -1, &igpu) == 0, "the iGPU is found by LUID");
expect(diag::find_gpu_device(machine, "gfx1101", -1, &igpu) == -1, "a LUID of another architecture does not match");
expect(diag::find_gpu_device(machine, "gfx1101", 2) == 2 && diag::find_gpu_device(machine, "gfx1101", 3) == -1, "an explicit ordinal must also be of the target architecture");
expect(diag::find_gpu_device(machine, "gfx1151", -1, &unknown) == -1, "an unknown adapter LUID matches nothing");
std::vector<GpuCandidate> everything;
for (size_t i = 0; i < archs.size(); ++i) everything.push_back({"GPU " + archs[i], archs[i], luid_of(100 + unsigned(i))});
for (size_t i = 0; i < archs.size(); ++i) {
const auto luid = luid_of(100 + unsigned(i));
bool exact = diag::find_gpu_device(everything, archs[i], -1, &luid) == int(i) && diag::find_gpu_device(everything, archs[i]) == int(i);
for (size_t j = 0; j < archs.size(); ++j) exact = exact && (j == i || diag::find_gpu_device(everything, archs[j], -1, &luid) == -1);
expect(exact, "among all 11 architectures " + archs[i] + " resolves only to its own adapter");
}
// Diagnostics for genuinely unsupported or mismatched GPUs (no crash, an actionable message).
auto message = diag::no_device_message(machine, "gfx1101", &unknown);
expect(contains(message, "No HIP device has the game's adapter LUID") && contains(message, "gfx1151 (Radeon 8060S)"), "unknown adapter: " + message);
const auto strix = luid_of(13);
message = diag::no_device_message(machine, "gfx1101", &strix);
expect(contains(message, "The GPU is gfx1151") && contains(message, "built for gfx1101") && contains(message, "extract the d4r ZIP again"),
"a gfx1101 file on a gfx1151 adapter is named as a mixed install: " + message);
const std::vector<GpuCandidate> rdna2{{"AMD Radeon RX 6800", "gfx1030", luid_of(5)}};
const auto six = luid_of(5);
message = diag::no_device_message(rdna2, "gfx1101", &six);
expect(contains(message, "gfx1030") && contains(message, "does not support") && contains(message, "gfx1154"), "unsupported RDNA2 GPU: " + message);
message = diag::no_device_message(rdna2, "gfx1201");
expect(contains(message, "none supported by d4r"), "no supported device at all: " + message);
expect(contains(diag::no_device_message({}, "gfx1100"), "reports no GPU"), "an empty HIP device list asks for a driver update");
// Loader: pick the shim for the game's adapter, never a different GPU's.
const auto has_all = [](const std::string& arch) { return diag::supported_gpu_target(arch); }; // A full release: one shim per target.
for (size_t i = 0; i < archs.size(); ++i) {
const auto luid = luid_of(100 + unsigned(i));
const auto choice = diag::choose_loader_architecture(everything, &luid, has_all, "all");
expect(choice.architecture == archs[i] && choice.error.empty(), "loader picks _nvngx_" + archs[i] + ".dll for that adapter");
}
auto choice = diag::choose_loader_architecture(machine, &igpu, has_all, "all");
expect(choice.architecture == "gfx1103", "loader follows the iGPU when the game renders on it");
choice = diag::choose_loader_architecture(machine, &second, has_all, "all");
expect(choice.architecture == "gfx1101", "loader follows the game's dGPU among several GPUs");
choice = diag::choose_loader_architecture(machine, nullptr, has_all, "all");
expect(choice.architecture.empty() && contains(choice.error, "several GPUs with different architectures"), "before an adapter is known, several architectures wait for Init");
choice = diag::choose_loader_architecture(machine, nullptr, [](const std::string& a) { return a == "gfx1151"; }, "gfx1151");
expect(choice.architecture == "gfx1151", "before an adapter is known, the only architecture with a build is used");
choice = diag::choose_loader_architecture(machine, &strix, [](const std::string& a) { return a == "gfx1101"; }, "gfx1101");
expect(choice.architecture.empty() && contains(choice.error, "no build for your GPU (gfx1151)") && contains(choice.error, "supports: gfx1101"), "loader: " + choice.error);
choice = diag::choose_loader_architecture(rdna2, &six, has_all, "all");
expect(choice.architecture.empty() && contains(choice.error, "gfx1030") && contains(choice.error, "not supported by d4r"), "loader: " + choice.error);
choice = diag::choose_loader_architecture(rdna2, nullptr, has_all, "all");
expect(choice.architecture.empty() && contains(choice.error, "no supported AMD GPU") && contains(choice.error, "gfx1030"), "loader: " + choice.error);
expect(diag::nvapi_target_error("gfx1151", "gfx1151", "p").empty() && contains(diag::nvapi_target_error("gfx1101", "gfx1151", "p"), "built for gfx1101 but the selected GPU is gfx1151"),
"an NVAPI built for another target is refused");
// Real nvapi64.dll builds, one per target: the file in nvapi\<arch> names <arch> and only <arch>.
if (builtNvapi.empty()) { std::printf("skip real NVAPI builds (no directory given)\n"); return; }
for (const auto& arch : archs) {
const auto layout = root / L"built";
const auto source = builtNvapi / diag::wide(arch) / L"nvapi64.dll";
std::filesystem::create_directories(layout / L"nvapi" / diag::wide(arch));
std::filesystem::copy_file(source, layout / L"nvapi" / diag::wide(arch) / L"nvapi64.dll", std::filesystem::copy_options::overwrite_existing);
}
for (const auto& arch : archs) {
const auto choice = player::select_compat_nvapi(root / L"built", arch);
expect(choice.error.empty(), "built layout has nvapi\\" + arch);
if (!choice.error.empty()) continue;
const HMODULE module = LoadLibraryExW(choice.path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
auto target = module ? reinterpret_cast<const char*(__cdecl*)()>(reinterpret_cast<void*>(GetProcAddress(module, "d4r_NvapiTargetArchitecture"))) : nullptr;
const std::string built = target ? target() : "";
expect(built == arch, "nvapi\\" + arch + "\\nvapi64.dll names " + arch + " (reports '" + built + "')");
const auto bytes = player::read_text(choice.path);
std::string others;
for (const auto& other : archs) if (other != arch && contains(bytes, "D4R_NVAPI_TARGET=" + other)) others += other + " ";
expect(contains(bytes, "D4R_NVAPI_TARGET=" + arch + '\0') && others.empty(), "nvapi\\" + arch + " carries only its own marker for the packager (" + others + ")");
expect(diag::nvapi_target_error(built, arch, "p").empty(), "the shim accepts nvapi\\" + arch + " for " + arch);
if (module) FreeLibrary(module);
}
}
}
int wmain(int argc, wchar_t** argv) {
if (argc != 2) { std::printf("usage: player_config_probe FIXTURE_DIR\n"); return 2; }
if (argc != 2 && argc != 3) { std::printf("usage: player_config_probe FIXTURE_DIR [BUILT_NVAPI_DIR]\n"); return 2; }
const std::filesystem::path dir(argv[1]);
std::vector<std::string> notes;
@@ -122,9 +306,13 @@ int wmain(int argc, wchar_t** argv) {
std::filesystem::create_directories(dir / L"zluda");
const auto colocatedNvapi = dir / L"zluda" / L"nvapi64.dll";
{ std::ofstream(colocatedNvapi, std::ios::binary) << "colocated compatibility NVAPI fixture"; }
expect(player::compat_nvapi_path(dir) == colocatedNvapi, "player mode reuses NVAPI colocated with nvcuda");
expect(player::compat_nvapi_path(dir) == archNvapi, "a shared NVAPI beside nvcuda never outranks the architecture-specific one");
std::filesystem::remove(colocatedNvapi);
std::filesystem::remove(archNvapi);
std::filesystem::remove(dir / L"nvapi" / L"nvapi64.dll");
architecture_selection(dir / L"architecture-fixture", argc > 2 ? std::filesystem::path(argv[2]) : std::filesystem::path());
std::filesystem::create_directories(dir / L"nvapi");
{ std::ofstream(dir / L"nvapi" / L"nvapi64.dll", std::ios::binary) << "generic nvapi fixture"; }
expect(player::raw_nvapi_backend_path(dir) == dir / L"nvapi" / L"backend" / L"nvapi64.dll",
"raw ZLUDA NVAPI backend has a separate explicit path");
const auto archNative = dir / L"native" / d4r::diag::wide(d4r::diag::HipApi::target_arch());