From 7da2f9ac399404c23b2afe2dc26f245fca16fffb Mon Sep 17 00:00:00 2001 From: ayo <93222141+countervolts@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:19:37 -0400 Subject: [PATCH] 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//. 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. --- scripts/windows/d4r-check.ps1 | 11 +- scripts/windows/gpu-target.ps1 | 9 + scripts/windows/package-windows-game.ps1 | 1 + .../windows/package-windows-quick-test.ps1 | 2 +- scripts/windows/package-windows-release.ps1 | 6 +- scripts/windows/test-d4r-check.ps1 | 7 + scripts/windows/test-player-config.ps1 | 13 +- .../windows/test-windows-release-package.ps1 | 47 ++++- tools/windows/d4r_nvngx_loader.cpp | 26 ++- tools/windows/gpu_environment.h | 20 ++ tools/windows/gpu_selection.h | 121 +++++++++++ tools/windows/hip_api.h | 46 +++-- tools/windows/ngx_windows_runtime.h | 18 ++ tools/windows/nvapi_trace.cpp | 9 + tools/windows/player_config.h | 48 ++++- tools/windows/player_config_probe.cpp | 192 +++++++++++++++++- 16 files changed, 530 insertions(+), 46 deletions(-) create mode 100644 tools/windows/gpu_environment.h create mode 100644 tools/windows/gpu_selection.h diff --git a/scripts/windows/d4r-check.ps1 b/scripts/windows/d4r-check.ps1 index 476d2df..d870dbf 100644 --- a/scripts/windows/d4r-check.ps1 +++ b/scripts/windows/d4r-check.ps1 @@ -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() diff --git a/scripts/windows/gpu-target.ps1 b/scripts/windows/gpu-target.ps1 index 3656690..857736e 100644 --- a/scripts/windows/gpu-target.ps1 +++ b/scripts/windows/gpu-target.ps1 @@ -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" } +} diff --git a/scripts/windows/package-windows-game.ps1 b/scripts/windows/package-windows-game.ps1 index f673ca3..43ff3db 100644 --- a/scripts/windows/package-windows-game.ps1 +++ b/scripts/windows/package-windows-game.ps1 @@ -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' diff --git a/scripts/windows/package-windows-quick-test.ps1 b/scripts/windows/package-windows-quick-test.ps1 index f35befe..3ec6b0c 100644 --- a/scripts/windows/package-windows-quick-test.ps1 +++ b/scripts/windows/package-windows-quick-test.ps1 @@ -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} diff --git a/scripts/windows/package-windows-release.ps1 b/scripts/windows/package-windows-release.ps1 index d9ec606..e7219a1 100644 --- a/scripts/windows/package-windows-release.ps1 +++ b/scripts/windows/package-windows-release.ps1 @@ -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// (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} diff --git a/scripts/windows/test-d4r-check.ps1 b/scripts/windows/test-d4r-check.ps1 index c52a02e..790a054 100644 --- a/scripts/windows/test-d4r-check.ps1 +++ b/scripts/windows/test-d4r-check.ps1 @@ -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 diff --git a/scripts/windows/test-player-config.ps1 b/scripts/windows/test-player-config.ps1 index ab0773d..265c1ad 100644 --- a/scripts/windows/test-player-config.ps1 +++ b/scripts/windows/test-player-config.ps1 @@ -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. diff --git a/scripts/windows/test-windows-release-package.ps1 b/scripts/windows/test-windows-release-package.ps1 index 71fd90d..0fd2f48 100644 --- a/scripts/windows/test-windows-release-package.ps1 +++ b/scripts/windows/test-windows-release-package.ps1 @@ -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 } } diff --git a/tools/windows/d4r_nvngx_loader.cpp b/tools/windows/d4r_nvngx_loader.cpp index cd4630c..9b65c04 100644 --- a/tools/windows/d4r_nvngx_loader.cpp +++ b/tools/windows/d4r_nvngx_loader.cpp @@ -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 #include @@ -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 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. diff --git a/tools/windows/gpu_environment.h b/tools/windows/gpu_environment.h new file mode 100644 index 0000000..86fca2b --- /dev/null +++ b/tools/windows/gpu_environment.h @@ -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 +#include + +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)); +} +} diff --git a/tools/windows/gpu_selection.h b/tools/windows/gpu_selection.h new file mode 100644 index 0000000..b63d6ca --- /dev/null +++ b/tools/windows/gpu_selection.h @@ -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_.dll shim, the nvapi\\nvapi64.dll and the native kernels, +// and its LUID picks the device when several GPUs share an architecture. +#include "gpu_target.h" +#include +#include +#include +#include +#include + +namespace d4r::diag { +using AdapterLuid = std::array; // 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& 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(low >> (8 * i)); luid[4 + i] = static_cast(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& 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& 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& devices, const AdapterLuid* luid, + const std::function& 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; +} +} diff --git a/tools/windows/hip_api.h b/tools/windows/hip_api.h index 3911df8..8395069 100644 --- a/tools/windows/hip_api.h +++ b/tools/windows/hip_api.h @@ -1,6 +1,6 @@ #pragma once #include "diagnostic.h" -#include "gpu_target.h" +#include "gpu_selection.h" #include #include #include @@ -43,14 +43,16 @@ struct HipApi { if (verbose || result != hipSuccess) std::printf("HIP %s -> %d (%s)\n", call, static_cast(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 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 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(), diff --git a/tools/windows/ngx_windows_runtime.h b/tools/windows/ngx_windows_runtime.h index 4ba82e0..a3e3a4a 100644 --- a/tools/windows/ngx_windows_runtime.h +++ b/tools/windows/ngx_windows_runtime.h @@ -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("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::wide(env_path("D4R_NVAPI_DLL"))); + require_nvapi_target(*nvapi, env_path("D4R_NVAPI_DLL")); dlss = std::make_unique(diag::wide(env_path("D4R_DLSS_DLL"))); core = std::make_unique(diag::wide(env_path("D4R_NGX_CORE"))); allocate = core->symbol("NVSDK_NGX_CUDA_AllocateParameters"); diff --git a/tools/windows/nvapi_trace.cpp b/tools/windows/nvapi_trace.cpp index eb5763c..fc83962 100644 --- a/tools/windows/nvapi_trace.cpp +++ b/tools/windows/nvapi_trace.cpp @@ -1,4 +1,5 @@ #include "diagnostic.h" +#include "gpu_target.h" #if defined(D4R_NVAPI_COMPAT) #include "hip_api.h" #include @@ -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) diff --git a/tools/windows/player_config.h b/tools/windows/player_config.h index 0ef5ab2..f185fdf 100644 --- a/tools/windows/player_config.h +++ b/tools/windows/player_config.h @@ -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\\; that copy is the only acceptable one for . 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& 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((dir / relative).wstring()); }; const std::optional 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> values[] = { {L"D4R_RR_BACKEND", settings.rrBackend == "off" ? unset : std::optional(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((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(compatNvapi.wstring())}, + {L"D4R_GPU_ARCH", std::optional(diag::wide(arch))}, + {L"D4R_NVAPI_DLL", nvapi.error.empty() ? std::optional(nvapi.path.wstring()) : unset}, {L"D4R_NVAPI_BACKEND", std::optional(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(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"; diff --git a/tools/windows/player_config_probe.cpp b/tools/windows/player_config_probe.cpp index d3a7503..a2d02df 100644 --- a/tools/windows/player_config_probe.cpp +++ b/tools/windows/player_config_probe.cpp @@ -21,10 +21,194 @@ std::string error_of(const std::function& 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(low >> (8 * i)); luid[4 + i] = static_cast(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 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\\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 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 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 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 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\ names and only . + 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(reinterpret_cast(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 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());