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

+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";