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

+9 -2
View File
@@ -46,7 +46,7 @@ if (!$manifest) { Bad 'd4r\package.json is missing or damaged (extract the d4r Z
# --- files --------------------------------------------------------------------------------------
$required = @('dxgi.dll', 'OptiScaler.ini', 'd4r\_nvngx.dll', 'd4r\d4r.ini', 'd4r\dll-pins.txt',
'd4r\hip\bin\amdhip64_7.dll', 'd4r\zluda\nvcuda.dll', 'd4r\zluda\nvapi64.dll', 'd4r\nvapi\backend\nvapi64.dll')
'd4r\hip\bin\amdhip64_7.dll', 'd4r\zluda\nvcuda.dll', 'd4r\nvapi\backend\nvapi64.dll')
foreach ($file in $required) {
if (Test-Path -LiteralPath (Join-Path $game $file) -PathType Leaf) { Ok $file }
else { Bad "$file (extract the d4r ZIP again over the game folder)" }
@@ -156,6 +156,10 @@ else {
if ($missing.Count) { Bad "GPU $($device.name) ($a): this release has no complete build for it (missing: $($missing -join ', '))" }
else {
Ok "GPU $($device.name) ($a): build present, $native native kernels"
# Each nvapi64.dll names the GPU target it was built for; a copy for another GPU is refused at startup.
$nvapiText = [Text.Encoding]::GetEncoding(28591).GetString([IO.File]::ReadAllBytes((Join-Path $d4r "nvapi\$a\nvapi64.dll")))
$nvapiNames = @([regex]::Matches($nvapiText, 'D4R_NVAPI_TARGET=(gfx[0-9a-f]+)\0') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
if ($nvapiNames.Count -ne 1 -or $nvapiNames[0] -ne $a) { Bad "d4r\nvapi\$a\nvapi64.dll is built for '$($nvapiNames -join ', ')' instead of $a (files from different GPUs are mixed; extract the d4r ZIP again)" }
if (!$arch) { $arch = $a }
}
if ($a -eq 'gfx1101') { Note 'RX 7700 XT has passed native Windows synthetic checks and limited game-frame tests; broad game validation remains incomplete' }
@@ -164,6 +168,9 @@ else {
}
if (@($inventory.devices | Where-Object { $_.wavefrontSize -eq 32 }).Count -gt 1) { Note 'several GPUs found; d4r uses the one the game renders on' }
}
if (Test-Path -LiteralPath (Join-Path $d4r 'zluda\nvapi64.dll')) {
Note 'd4r\zluda\nvapi64.dll is left over from an older d4r release (one GPU''s build shared by all). d4r ignores it, but delete it'
}
if ($arch -and $registry.Count) {
$info = @($registry | Where-Object { $_.architecture -eq $arch }) | Select-Object -First 1
$bad = 0
@@ -198,7 +205,7 @@ $iniPath = Join-Path $d4r 'd4r.ini'
$model = 'K'
if (Test-Path -LiteralPath $iniPath) {
$section = ''; $iniProblems = 0
$known = @{ 'dlss.model' = 1; 'dlss.showwatermark' = 1; 'interop.asyncinterop' = 1; 'interop.batchinputcopies' = 1; 'debug.validateoutput' = 1; 'debug.log' = 1; 'paths.cachedir' = 1; 'paths.nativedir' = 1; 'kernels.nativefp8' = 1; 'rayreconstruction.backend' = 1; 'rayreconstruction.modeldir' = 1 }
$known = @{ 'dlss.model' = 1; 'dlss.showwatermark' = 1; 'interop.asyncinterop' = 1; 'interop.batchinputcopies' = 1; 'debug.validateoutput' = 1; 'debug.log' = 1; 'paths.cachedir' = 1; 'paths.nativedir' = 1; 'kernels.nativefp8' = 1; 'rayreconstruction.backend' = 1; 'rayreconstruction.modeldir' = 1; 'rayreconstruction.fsrdenoiser' = 1; 'rayreconstruction.fsrstabilitybias' = 1; 'rayreconstruction.fsrdetail' = 1 }
$boolean = @('dlss.showwatermark', 'interop.asyncinterop', 'interop.batchinputcopies', 'debug.validateoutput', 'kernels.nativefp8')
foreach ($raw in (Read-TextFile $iniPath).TrimStart([char]0xFEFF) -split "`n") {
$line = $raw.Trim()
+9
View File
@@ -36,3 +36,12 @@ function Assert-D4RCodeObjectTarget([string]$Path, [string]$Target) {
$expected=$info.elfMachine
if ($header[48] -ne $expected) { throw "Code object does not target ${Target}: $Path (ELF machine=$($header[48]))" }
}
# The compatibility NVAPI names the GPU target it was built for (nvapi_trace.cpp); a copy for another GPU, or one
# built before the marker existed, must never be packaged: it would refuse the GPU it is installed for.
function Assert-D4RNvapiTarget([string]$Path, [string]$Target) {
[void](Get-D4RGpuTargetInfo $Target)
$text=[Text.Encoding]::GetEncoding(28591).GetString([IO.File]::ReadAllBytes($Path))
$found=@([regex]::Matches($text,'D4R_NVAPI_TARGET=(gfx[0-9a-f]+)\0') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
if ($found.Count -ne 1) { throw "NVAPI does not name exactly one GPU target (rebuild d4r_nvapi_compat): $Path" }
if ($found[0] -ne $Target) { throw "NVAPI is built for $($found[0]), not ${Target}: $Path" }
}
+1
View File
@@ -44,6 +44,7 @@ function Stage([string]$source, [string]$relative) {
Stage (Join-Path $OptiScalerRoot 'OptiScaler.dll') 'OptiScaler.dll'
Stage (Join-Path $DiagnosticRoot 'bin/d4r_nvngx.dll') 'd4r/_nvngx.dll'
Stage (Join-Path $DiagnosticRoot "bin/pixel_convert_${GpuArch}.hsaco") "d4r/pixel_convert_${GpuArch}.hsaco"
Assert-D4RNvapiTarget (Join-Path $DiagnosticRoot 'nvapi-compat/nvapi64.dll') $GpuArch
Stage (Join-Path $DiagnosticRoot 'nvapi-compat/nvapi64.dll') 'd4r/nvapi/nvapi64.dll'
Stage (Join-Path $DiagnosticRoot 'nvapi-compat/nvapi64.dll') 'd4r/zluda/nvapi64.dll'
Stage (Join-Path $DiagnosticRoot 'bin/d4r_debug_launcher.exe') 'd4r/d4r_debug_launcher.exe'
@@ -33,7 +33,7 @@ foreach ($gpu in Get-D4RGpuTargets) {
$relative=$file.path.Replace('\','/')
if ((Get-FileHash -LiteralPath (Join-Path $source $file.path) -Algorithm SHA256).Hash -ne $file.sha256) { throw "Input file changed: $source/$relative" }
if ($relative.EndsWith('.hsaco')) { Assert-D4RCodeObjectTarget (Join-Path $source $file.path) $arch }
if ($relative -eq 'gpu-target.json' -or $relative -eq 'd4r/_nvngx.dll' -or $relative -eq 'd4r/d4r_gpu_inventory.exe' -or $relative -eq 'd4r/d4r_debug_launcher.exe' -or $relative -eq 'd4r/nvapi/nvapi64.dll' -or
if ($relative -eq 'gpu-target.json' -or $relative -eq 'd4r/_nvngx.dll' -or $relative -eq 'd4r/d4r_gpu_inventory.exe' -or $relative -eq 'd4r/d4r_debug_launcher.exe' -or $relative -eq 'd4r/nvapi/nvapi64.dll' -or $relative -eq 'd4r/zluda/nvapi64.dll' -or
$relative.StartsWith('d4r/native/') -or $relative -like 'd4r/pixel_convert_*.hsaco') {
$entry=Stage (Join-Path $source $file.path) "files/targets/$arch/$relative"
$targetFiles+=@{path=$relative; sha256=$entry.sha256; version=$entry.version}
+5 -1
View File
@@ -64,7 +64,10 @@ foreach ($gpu in $selected) {
# One GPU-query tool serves every target: it reports each device's real architecture.
if ($relative -eq 'd4r/d4r_gpu_inventory.exe') { if (!$inventoryStaged) { Stage $path 'd4r/d4r_gpu_inventory.exe'; $inventoryStaged=$true } }
elseif ($relative -eq 'd4r/_nvngx.dll') { Stage $path "d4r/_nvngx_$arch.dll" }
elseif ($relative -eq 'd4r/nvapi/nvapi64.dll') { Stage $path "d4r/nvapi/$arch/nvapi64.dll" }
elseif ($relative -eq 'd4r/nvapi/nvapi64.dll') { Assert-D4RNvapiTarget $path $arch; Stage $path "d4r/nvapi/$arch/nvapi64.dll" }
# The copy beside nvcuda.dll is one GPU's build: it is never shared. Each target's own build ships in
# d4r/nvapi/<arch>/ (above); the old shared copy made every GPU load the first target's NVAPI.
elseif ($relative -eq 'd4r/zluda/nvapi64.dll') { }
elseif ($relative -like 'd4r/pixel_convert_*.hsaco') { Stage $path $relative }
elseif ($relative.StartsWith('d4r/native/')) { Stage $path ("d4r/native/$arch/" + $relative.Substring('d4r/native/'.Length)) }
elseif ($relative -eq 'd4r/nvapi/backend/nvapi64.dll' -or $relative -eq 'OptiScaler.dll' -or $relative.StartsWith('licenses/') -or
@@ -76,6 +79,7 @@ foreach ($gpu in $selected) {
# Installer-only files (launcher, PowerShell installer, docs) are not part of a drag-in install.
}
if ($objects -ne 17) { throw "Expected 17 code objects for $arch" }
if (!(Test-Path -LiteralPath (Join-Path $PackageRoot "d4r/nvapi/$arch/nvapi64.dll") -PathType Leaf)) { throw "No d4r/nvapi/nvapi64.dll in the $arch input; the release needs one NVAPI build per target." }
$commonStaged=$true
$targets[$arch]=@{binarySourceCommit=$metadata.d4rCommit; hardwareValidation=$metadata.hardwareValidation;
nativeFp8M=[bool]$metadata.nativeFp8M; tokenLanesM=[bool]$metadata.tokenLanesM}
+7
View File
@@ -91,6 +91,13 @@ $other=@(Get-ChildItem -LiteralPath (Join-Path $game 'd4r/native/gfx1100') -Filt
Copy-Item -LiteralPath $other -Destination (Join-Path $game 'd4r/native/gfx1101/kernel3.hsaco') -Force
Expect 'code-object-for-another-gpu' (Run-Check $game $inv) 1 @('MISSING\s+kernel3\.hsaco is not built for gfx1101','differs from the release')
$game=New-Install 'gpu-wrong-nvapi'
Copy-Item -LiteralPath (Join-Path $game 'd4r/nvapi/gfx1100/nvapi64.dll') -Destination (Join-Path $game 'd4r/nvapi/gfx1151/nvapi64.dll') -Force
Expect 'nvapi-for-another-gpu' (Run-Check $game (Write-Inventory 'd' 'gfx1151' 'Radeon 8060S')) 1 @('MISSING\s+d4r\\nvapi\\gfx1151\\nvapi64\.dll is built for ''gfx1100'' instead of gfx1151')
$game=New-Install 'stale-shared-nvapi'
Copy-Item -LiteralPath (Join-Path $game 'd4r/nvapi/gfx1101/nvapi64.dll') -Destination (New-Item -ItemType Directory -Force (Join-Path $game 'd4r/zluda')).FullName -Force
Expect 'stale-shared-nvapi-noted' (Run-Check $game (Write-Inventory 'e' 'gfx1151' 'Radeon 8060S')) 0 @('note\s+d4r\\zluda\\nvapi64\.dll is left over from an older d4r release','ok\s+GPU Radeon 8060S \(gfx1151\): build present')
# 6. What the last game start said, and the bug-report ZIP.
$game=New-Install 'log-failure'
Set-Content -LiteralPath (Join-Path $game 'd4r/d4r_nvngx.log') "d4r: DLSS disabled: Wrong nvngx_dlss.dll: you have version 1.2.3.4" -Encoding ASCII
+12 -1
View File
@@ -46,10 +46,21 @@ function Build([string]$Name,[string[]]$Arguments) {
$results=@()
# 1. d4r.ini validation and DLL pin decisions (the shim's own header).
# The compatibility NVAPI is compiled once per registered target, exactly as the release builds it, so the
# probe can check the target each binary names.
$builtNvapi=Join-Path $OutputDirectory 'built-nvapi'
$noTarget=@($common | Where-Object { $_ -notlike '-DD4R_TARGET_ARCH=*' })
foreach ($target in $targets) {
$arch=$target.architecture
$dll=Join-Path $builtNvapi "$arch/nvapi64.dll"
New-Item -ItemType Directory -Force (Split-Path $dll) | Out-Null
Build "nvapi-$arch" ($noTarget+@("-DD4R_TARGET_ARCH=`"$arch`"",'-DD4R_NVAPI_COMPAT','-shared',(Join-Path $repo 'tools/windows/nvapi_trace.cpp'),'-o',$dll,'-lversion','-ldbghelp'))
}
$probe=Join-Path $OutputDirectory 'player_config_probe.exe'
Build 'player-config' ($common+@('-municode',(Join-Path $repo 'tools/windows/player_config_probe.cpp'),'-o',$probe,'-lversion','-ldbghelp','-lbcrypt'))
if (Invoke-Native (Join-Path $OutputDirectory 'player-config.log') $probe @((Join-Path $OutputDirectory 'pins-fixture'))) { throw "player_config_probe failed; see $(Join-Path $OutputDirectory 'player-config.log')" }
if (Invoke-Native (Join-Path $OutputDirectory 'player-config.log') $probe @((Join-Path $OutputDirectory 'pins-fixture'),$builtNvapi)) { throw "player_config_probe failed; see $(Join-Path $OutputDirectory 'player-config.log')" }
$results+=@{case='d4r-ini-and-dll-pins'; passed=$true}
$results+=@{case='architecture-selection-all-targets'; passed=$true; targets=$targets.Count; nvapiBuilds=$targets.Count}
# 2. Loader: GPU-free marker, LUID-based target selection, forwarding, and a
# clear failure when the folder has no build for the detected GPU.
@@ -16,17 +16,24 @@ function Write-CodeObject([string]$Path,[int]$Machine) {
New-Item -ItemType Directory -Force (Split-Path $Path) | Out-Null
[IO.File]::WriteAllBytes($Path,$header)
}
# Stand-in compatibility NVAPI: names its build target the way the real DLL does (nvapi_trace.cpp).
function Write-Nvapi([string]$Path,[string]$Target) {
New-Item -ItemType Directory -Force (Split-Path $Path) | Out-Null
[IO.File]::WriteAllBytes($Path,[Text.Encoding]::ASCII.GetBytes("MZ nvapi fixture D4R_NVAPI_TARGET=$Target")+[byte]0)
}
function New-Inputs([string]$Prefix,[scriptblock]$Mutate) {
foreach ($target in $targets) {
$arch=$target.architecture; $root="$Prefix-$arch"
$text=@{'OptiScaler.dll'='optiscaler'; 'd4r/_nvngx.dll'="shim $arch"; 'd4r/nvapi/nvapi64.dll'="nvapi $arch";
'd4r/nvapi/backend/nvapi64.dll'='raw backend nvapi'; 'd4r/hip/bin/amdhip64_7.dll'='hip'; 'd4r/zluda/nvcuda.dll'='zluda'; 'd4r/zluda/nvapi64.dll'='compat nvapi'; 'licenses/d4r.txt'='license';
$text=@{'OptiScaler.dll'='optiscaler'; 'd4r/_nvngx.dll'="shim $arch"; 'd4r/nvapi/backend/nvapi64.dll'='raw backend nvapi'; 'd4r/hip/bin/amdhip64_7.dll'='hip'; 'd4r/zluda/nvcuda.dll'='zluda'; 'licenses/d4r.txt'='license';
'd4r/d4r_gpu_inventory.exe'='inventory'; 'd4r/d4r_debug_launcher.exe'='launcher'; 'windows-game.ps1'='installer';
'd4r/native/d4r-kernels.txt'='manifest'}
foreach ($entry in $text.GetEnumerator()) {
$path=Join-Path $root $entry.Key; New-Item -ItemType Directory -Force (Split-Path $path) | Out-Null
Set-Content -LiteralPath $path $entry.Value -Encoding ASCII
}
# As the game package stages it: the target's own build, in both places.
Write-Nvapi (Join-Path $root 'd4r/nvapi/nvapi64.dll') $arch
Write-Nvapi (Join-Path $root 'd4r/zluda/nvapi64.dll') $arch
$objects=@("d4r/pixel_convert_$arch.hsaco")+@(1..16 | ForEach-Object { "d4r/native/kernel$_.hsaco" })
foreach ($object in $objects) { Write-CodeObject (Join-Path $root $object) $target.elfMachine }
if ($Mutate) { & $Mutate $root $arch }
@@ -62,16 +69,23 @@ if ((Get-Content -LiteralPath (Join-Path $release 'd4r/_nvngx.dll') -Raw).Trim()
foreach ($target in $targets) {
$arch=$target.architecture
if ((Get-Content -LiteralPath (Join-Path $release "d4r/_nvngx_$arch.dll") -Raw).Trim() -ne "shim $arch") { throw "Wrong shim for $arch" }
if ((Get-Content -LiteralPath (Join-Path $release "d4r/nvapi/$arch/nvapi64.dll") -Raw).Trim() -ne "nvapi $arch") { throw "Wrong NVAPI for $arch" }
Assert-D4RNvapiTarget (Join-Path $release "d4r/nvapi/$arch/nvapi64.dll") $arch
foreach ($other in $targets) {
if ($other.architecture -ne $arch) {
$mismatch=$false
try { Assert-D4RNvapiTarget (Join-Path $release "d4r/nvapi/$arch/nvapi64.dll") $other.architecture } catch { $mismatch=$_.Exception.Message -match "built for $arch" }
if (!$mismatch) { throw "NVAPI for $arch passed as $($other.architecture)" }
}
}
Assert-D4RCodeObjectTarget (Join-Path $release "d4r/pixel_convert_$arch.hsaco") $arch
$native=@(Get-ChildItem -LiteralPath (Join-Path $release "d4r/native/$arch") -Filter '*.hsaco')
if ($native.Count -ne 16 -or !(Test-Path -LiteralPath (Join-Path $release "d4r/native/$arch/d4r-kernels.txt"))) { throw "Native folder for $arch is incomplete" }
foreach ($object in $native) { Assert-D4RCodeObjectTarget $object.FullName $arch }
}
if ((Get-Content -LiteralPath (Join-Path $release 'd4r/nvapi/backend/nvapi64.dll') -Raw).Trim() -ne 'raw backend nvapi') { throw 'The raw ZLUDA NVAPI backend was not staged separately.' }
if ((Get-Content -LiteralPath (Join-Path $release 'd4r/zluda/nvapi64.dll') -Raw).Trim() -ne 'compat nvapi') { throw 'The compatibility NVAPI is not colocated with nvcuda.dll.' }
if (Test-Path -LiteralPath (Join-Path $release 'd4r/zluda/nvapi64.dll')) { throw 'A shared NVAPI beside nvcuda.dll would be loaded for every GPU; it must not ship.' }
if (@(Get-ChildItem -LiteralPath $release -Recurse -Filter 'd4r_gpu_inventory.exe').Count -ne 1) { throw 'The GPU query tool must ship exactly once.' }
foreach ($excluded in @('d4r/d4r_debug_launcher.exe','windows-game.ps1','d4r/native/kernel1.hsaco','d4r/nvapi/nvapi64.dll')) {
foreach ($excluded in @('d4r/d4r_debug_launcher.exe','windows-game.ps1','d4r/native/kernel1.hsaco','d4r/nvapi/nvapi64.dll','d4r/zluda/nvapi64.dll')) {
if (Test-Path -LiteralPath (Join-Path $release $excluded)) { throw "Installer-only or untargeted file shipped: $excluded" }
}
$manifest=Get-Content -LiteralPath (Join-Path $release 'd4r/package.json') -Raw | ConvertFrom-Json
@@ -141,6 +155,29 @@ try { Invoke-Package $prefix (Join-Path $OutputDirectory 'release-mixed') } catc
if (!$rejected) { throw 'A gfx1100 code object in the gfx1101 input was not rejected.' }
$results+=@{case='mixed-target-rejected'; passed=$true}
# 2b. NVAPI selection inputs: an NVAPI built for another GPU, one without a target marker (built before the
# marker existed) and a missing one are rejected; the d4r 0.1.5 input shape (the gfx1101 copy shared beside
# nvcuda.dll in every input) packages without that copy, and every target still gets its own build.
$cases=[ordered]@{
'wrong-target'=@{mutate={ param($root,$arch) if ($arch -eq 'gfx1151') { Write-Nvapi (Join-Path $root 'd4r/nvapi/nvapi64.dll') 'gfx1101' } }; expect='built for gfx1101, not gfx1151'}
'no-marker'=@{mutate={ param($root,$arch) if ($arch -eq 'gfx1102') { Set-Content -LiteralPath (Join-Path $root 'd4r/nvapi/nvapi64.dll') 'legacy nvapi' -Encoding ASCII } }; expect='does not name exactly one GPU target'}
'missing'=@{mutate={ param($root,$arch) if ($arch -eq 'gfx1200') { Remove-Item -LiteralPath (Join-Path $root 'd4r/nvapi/nvapi64.dll') } }; expect='No d4r/nvapi/nvapi64.dll in the gfx1200 input'}
}
foreach ($case in $cases.GetEnumerator()) {
$prefix=Join-Path $OutputDirectory "inputs/nvapi-$($case.Key)/game"
New-Inputs $prefix $case.Value.mutate
$rejected=$false
try { Invoke-Package $prefix (Join-Path $OutputDirectory "release-nvapi-$($case.Key)") } catch { $rejected=$_.Exception.Message -like "*$($case.Value.expect)*" }
if (!$rejected) { throw "NVAPI case '$($case.Key)' was not rejected with '$($case.Value.expect)'." }
}
$prefix=Join-Path $OutputDirectory 'inputs/shared-nvapi/game'
New-Inputs $prefix { param($root,$arch) Write-Nvapi (Join-Path $root 'd4r/zluda/nvapi64.dll') 'gfx1101' }
$sharedRelease=Join-Path $OutputDirectory 'release-shared-nvapi'
Invoke-Package $prefix $sharedRelease
if (Test-Path -LiteralPath (Join-Path $sharedRelease 'd4r/zluda/nvapi64.dll')) { throw 'The shared gfx1101 NVAPI shipped.' }
foreach ($target in $targets) { Assert-D4RNvapiTarget (Join-Path $sharedRelease "d4r/nvapi/$($target.architecture)/nvapi64.dll") $target.architecture }
$results+=@{case='nvapi-per-target'; passed=$true; targets=$targets.Count}
# 3. Shared runtime files must be identical across targets.
$prefix=Join-Path $OutputDirectory 'inputs/runtime/game'
New-Inputs $prefix { param($root,$arch) if ($arch -eq 'gfx1200') { Set-Content -LiteralPath (Join-Path $root 'd4r/zluda/nvcuda.dll') 'other zluda' -Encoding ASCII } }