diff --git a/CMakeLists.txt b/CMakeLists.txt index 55103e2..aeedccb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,12 +146,23 @@ add_dependencies(d4r_nvngx_windows d4r_pixel_code_object) set_target_properties(d4r_nvngx_windows PROPERTIES PREFIX "" OUTPUT_NAME "d4r_nvngx") target_compile_definitions(d4r_nvngx_windows PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX __HIP_PLATFORM_AMD__) target_include_directories(d4r_nvngx_windows PRIVATE "${D4R_HIP_ROOT}/include") -target_link_libraries(d4r_nvngx_windows PRIVATE version dbghelp d3d12 dxguid minhook) +target_link_libraries(d4r_nvngx_windows PRIVATE version dbghelp d3d12 dxguid minhook bcrypt) install(FILES "${D4R_MINHOOK_ROOT}/LICENSE.txt" DESTINATION licenses RENAME MinHook.txt) if(MINGW) target_link_options(d4r_nvngx_windows PRIVATE -static) endif() install(TARGETS d4r_nvngx_windows RUNTIME DESTINATION bin) +# Target-independent _nvngx.dll of drag-in releases: loads _nvngx_.dll +# for the game's actual HIP architecture. Its own D4R_TARGET_ARCH is unused. +add_library(d4r_nvngx_loader SHARED tools/windows/d4r_nvngx_loader.cpp) +set_target_properties(d4r_nvngx_loader PROPERTIES PREFIX "" OUTPUT_NAME "d4r_nvngx_loader") +target_compile_definitions(d4r_nvngx_loader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX __HIP_PLATFORM_AMD__) +target_include_directories(d4r_nvngx_loader PRIVATE "${D4R_HIP_ROOT}/include") +target_link_libraries(d4r_nvngx_loader PRIVATE version dbghelp) +if(MINGW) + target_link_options(d4r_nvngx_loader PRIVATE -static) +endif() +install(TARGETS d4r_nvngx_loader RUNTIME DESTINATION bin) if(MSVC) target_sources(d4r_ngx_cuda_init_probe PRIVATE tools/d4r_ngx_param_msvc.cpp tools/d4r_ngx_param_host.cpp) target_sources(d4r_d3d12_dlss_probe PRIVATE tools/d4r_ngx_param_msvc.cpp tools/d4r_ngx_param_host.cpp) diff --git a/packaging/windows/OptiScaler.ini b/packaging/windows/OptiScaler.ini new file mode 100644 index 0000000..7e9684f --- /dev/null +++ b/packaging/windows/OptiScaler.ini @@ -0,0 +1,27 @@ +; OptiScaler settings for the d4r Windows drag-in release. Values not listed keep OptiScaler's +; defaults. The DLSS model is chosen in d4r\d4r.ini, not here. +[Upscalers] +Dx12Upscaler=dlss +[FrameGen] +Enabled=false +FGInput=nofg +FGOutput=nofg +[DLSS] +Enabled=true +AllowExternalBackend=true +RenderPresetOverride=false +UseGenericAppIdWithDlss=false +[Libraries] +; Relative to this folder (needs d4r's OptiScaler with patch 0006). +NvngxPath=d4r +NvngxDlssPath=d4r +NvngxFeaturePath=d4r +[Hotfix] +RestoreComputeSignature=false +RestoreGraphicSignature=false +ExtendedStateRestore=false +[Log] +LogToFile=true +LogLevel=2 +[Menu] +DisableSplash=true diff --git a/packaging/windows/d4r.ini b/packaging/windows/d4r.ini new file mode 100644 index 0000000..25a38ea --- /dev/null +++ b/packaging/windows/d4r.ini @@ -0,0 +1,38 @@ +; d4r settings for this game (Windows). This file lives in the game's d4r folder and applies to +; this game only. Edit it with Notepad, save, and restart the game. Mistakes are reported in +; d4r\d4r_nvngx.log and by d4r-check.ps1; an invalid value falls back to the default shown here. +; +; Only settings that exist and are checked on Windows are listed. The Linux settings for other +; models, kernels and interop are not available here. + +[DLSS] +; DLSS model: +; +; Model | DLSS | Recommended modes | Notes +; ------+------+----------------------------+-------------------------------------- +; K | 4 | DLAA / Quality / Balanced | Default; faster +; M | 4.5 | Performance | More demanding +; +; Select the quality mode (Quality, Performance, ...) in the game or the OptiScaler menu (Insert). +; Models E and L are not available on Windows. +Model = K + +[Interop] +; Experimental: lets the game continue while DLSS runs on the GPU instead of waiting for it. +; Requires a game that uses one D3D12 command queue. Off is the tested setting. Whether it is +; faster depends on the game and has not been measured broadly; turn it off if the game crashes, +; stutters or shows a wrong image. +AsyncInterop = false + +[Debug] +; Check every DLSS output for invalid values (NaN/Inf) on the GPU. Costs some speed; use it only +; when reporting a problem. +ValidateOutput = false +; normal: errors and a summary in d4r\d4r_nvngx.log. verbose: every API call and kernel launch +; (large log; use it only when asked for a bug report). +Log = normal + +[Paths] +; Folder for the compiled DLSS kernels. auto = %LOCALAPPDATA%\d4r\cache. The first launch fills it +; and can take several minutes; later launches are quick. +CacheDir = auto diff --git a/patches/optiscaler/0006-relative-d4r-library-paths.patch b/patches/optiscaler/0006-relative-d4r-library-paths.patch new file mode 100644 index 0000000..f13526b --- /dev/null +++ b/patches/optiscaler/0006-relative-d4r-library-paths.patch @@ -0,0 +1,30 @@ +Subject: [PATCH] Resolve relative d4r library paths against the OptiScaler folder + +Lets a drag-in d4r install ship OptiScaler.ini with NvngxPath=d4r instead of +an absolute path that only an installer could know. Absolute paths behave +exactly as before. +--- +diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp +--- a/OptiScaler/Config.cpp ++++ b/OptiScaler/Config.cpp +@@ -787,9 +787,17 @@ + { + MainDllPath.set_from_config(readWString("Libraries", "OptiDllPath")); + +- NvngxPath.set_from_config(readWString("Libraries", "NvngxPath")); +- NVNGX_DLSS_Library.set_from_config(readWString("Libraries", "NvngxDlssPath")); +- DLSSFeaturePath.set_from_config(readWString("Libraries", "NvngxFeaturePath")); ++ // A relative library folder (d4r drag-in installs use "d4r") is resolved ++ // against this DLL's folder, as LogFileName is. Absolute paths are unchanged. ++ auto libraryPath = [](std::optional value) -> std::optional ++ { ++ if (!value.has_value() || value->empty() || std::filesystem::path(*value).is_absolute()) ++ return value; ++ return (Util::DllPath().parent_path() / *value).lexically_normal().wstring(); ++ }; ++ NvngxPath.set_from_config(libraryPath(readWString("Libraries", "NvngxPath"))); ++ NVNGX_DLSS_Library.set_from_config(libraryPath(readWString("Libraries", "NvngxDlssPath"))); ++ DLSSFeaturePath.set_from_config(libraryPath(readWString("Libraries", "NvngxFeaturePath"))); + NvapiDllPath.set_from_config(readWString("Libraries", "NvapiPath")); + + FfxDx12Path.set_from_config(readWString("Libraries", "FfxDx12Path")); diff --git a/scripts/windows/package-windows-release.ps1 b/scripts/windows/package-windows-release.ps1 new file mode 100644 index 0000000..9a2145d --- /dev/null +++ b/scripts/windows/package-windows-release.ps1 @@ -0,0 +1,98 @@ +# Builds the Windows drag-in release: extract it into the game folder, copy +# the two NVIDIA DLLs, edit d4r\d4r.ini. Like the Linux release, no installer +# or launcher is involved. Inputs are the per-target game packages that +# package-windows-game.ps1 produces (the same inputs as the quick test). +[CmdletBinding()] +param( + [string]$GamePackagePrefix, + [Parameter(Mandatory=$true)][string]$LoaderDll, + [Parameter(Mandatory=$true)][string]$PackageRoot, + [string]$ArchivePath +) +$ErrorActionPreference='Stop' +. (Join-Path $PSScriptRoot 'gpu-target.ps1') +$repo=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) +if (!$GamePackagePrefix) { $GamePackagePrefix=Join-Path $repo 'dist/windows-gpu-coverage-game' } +$PackageRoot=[IO.Path]::GetFullPath($PackageRoot) +if (Test-Path -LiteralPath $PackageRoot) { throw 'Use a new output directory to avoid mixing releases.' } +$git=Get-Command git -ErrorAction SilentlyContinue +$commit=if ($git) { (& git -C $repo rev-parse HEAD).Trim() } else { $null } +if ($ArchivePath -and (!$git -or [bool](& git -C $repo status --porcelain))) { throw 'A public ZIP needs git and committed sources.' } +New-Item -ItemType Directory -Force $PackageRoot | Out-Null +$files=[Collections.Generic.List[object]]::new() +function Stage([string]$Source,[string]$Relative) { + $destination=Join-Path $PackageRoot $Relative + if (Test-Path -LiteralPath $destination) { throw "Duplicate release path: $Relative" } + New-Item -ItemType Directory -Force (Split-Path $destination) | Out-Null + Copy-Item -LiteralPath $Source -Destination $destination + $item=Get-Item -LiteralPath $destination + $files.Add(@{path=$Relative.Replace('\','/'); sha256=(Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash; version=$item.VersionInfo.FileVersion}) +} +$targets=[ordered]@{}; $baseline=$null; $commonStaged=$false +foreach ($gpu in Get-D4RGpuTargets) { + $arch=$gpu.architecture; $source="$GamePackagePrefix-$arch" + $metadata=Get-Content -LiteralPath (Join-Path $source 'package.json') -Raw | ConvertFrom-Json + if ($metadata.architecture -ne $arch -or $metadata.d4rWorkingTreeDirty) { throw "Uncommitted or wrong-target input: $source" } + if (!$baseline) { $baseline=$metadata } + if ($metadata.dlssSha256 -ne $baseline.dlssSha256 -or $metadata.zludaBuild.sourceCommit -ne $baseline.zludaBuild.sourceCommit) { throw 'Input packages do not use the same validated dependencies.' } + $objects=0 + foreach ($file in $metadata.files) { + $relative=$file.path.Replace('\','/'); $path=Join-Path $source $file.path + if ((Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ne $file.sha256) { throw "Input file changed: $source/$relative" } + if ($relative.EndsWith('.hsaco')) { Assert-D4RCodeObjectTarget $path $arch; $objects++ } + # Per-target files get target-specific names so one folder serves every GPU. + if ($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 -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 'OptiScaler.dll' -or $relative.StartsWith('licenses/') -or + ($relative.StartsWith('d4r/hip/') -or $relative.StartsWith('d4r/zluda/'))) { + $public=if ($relative -eq 'OptiScaler.dll') { 'dxgi.dll' } elseif ($relative.StartsWith('licenses/')) { "d4r/$relative" } else { $relative } + if (!$commonStaged) { Stage $path $public } + elseif ((Get-FileHash -LiteralPath (Join-Path $PackageRoot $public) -Algorithm SHA256).Hash -ne $file.sha256) { throw "Shared runtime differs in $arch : $relative" } + } + # Installer-only files (launcher, inventory, PowerShell, docs) are not part of a drag-in install. + } + if ($objects -ne 17) { throw "Expected 17 code objects for $arch" } + $commonStaged=$true + $targets[$arch]=@{binarySourceCommit=$metadata.d4rCommit; hardwareValidation=$metadata.hardwareValidation} +} +$loader=(Get-Item -LiteralPath $LoaderDll).FullName +if ([IO.Path]::GetFileName($loader) -ne 'd4r_nvngx_loader.dll') { throw 'Pass the d4r_nvngx_loader.dll built by CMake.' } +Stage $loader 'd4r/_nvngx.dll' +Stage (Join-Path $repo 'packaging/windows/OptiScaler.ini') 'OptiScaler.ini' +Stage (Join-Path $repo 'packaging/windows/d4r.ini') 'd4r/d4r.ini' +Stage (Join-Path $repo 'tools/windows/dll-pins.txt') 'd4r/dll-pins.txt' +foreach ($optional in @(@{source='packaging/windows/WINDOWS_README.txt'; path='WINDOWS_README.txt'}, + @{source='scripts/windows/d4r-check.ps1'; path='d4r/d4r-check.ps1'})) { + $path=Join-Path $repo $optional.source + if (Test-Path -LiteralPath $path) { Stage $path $optional.path } +} +# Shows players where the NGX core goes; a ZIP cannot carry an empty folder. +$placeholder=Join-Path $PackageRoot 'd4r/ngx/PUT _nvngx.dll HERE.txt' +New-Item -ItemType Directory -Force (Split-Path $placeholder) | Out-Null +"Copy NVIDIA's _nvngx.dll into this folder. See WINDOWS_README.txt." | Set-Content -LiteralPath $placeholder -Encoding ASCII +$files.Add(@{path='d4r/ngx/PUT _nvngx.dll HERE.txt'; sha256=(Get-FileHash -LiteralPath $placeholder -Algorithm SHA256).Hash; version=$null}) +if (Get-ChildItem -LiteralPath $PackageRoot -Recurse -File | Where-Object { $_.Name -eq 'nvngx_dlss.dll' -or ($_.Name -eq '_nvngx.dll' -and $_.DirectoryName -ne (Join-Path $PackageRoot 'd4r')) }) { + throw 'Proprietary NVIDIA binaries must not be in the release.' +} +# Read by d4r-check.ps1: every shipped file's hash, so a damaged or mixed install is reported. +$manifest=[ordered]@{schema=1; kind='windows-drag-in'; packageCommit=$commit; binarySourceCommit=$baseline.d4rCommit; + dependencies=@{optiScalerSourceCommit=$baseline.optiScaler.sourceCommit; zludaSourceCommit=$baseline.zludaBuild.sourceCommit; hipRuntime=$baseline.hipRuntime}; + targets=$targets; files=$files} +$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $PackageRoot 'd4r/package.json') -Encoding UTF8 +if ($ArchivePath) { + $ArchivePath=[IO.Path]::GetFullPath($ArchivePath) + if ($ArchivePath.StartsWith($PackageRoot+'\',[StringComparison]::OrdinalIgnoreCase)) { throw 'Place the ZIP outside the output directory.' } + New-Item -ItemType Directory -Force (Split-Path $ArchivePath) | Out-Null + Add-Type -AssemblyName System.IO.Compression,System.IO.Compression.FileSystem + $stream=[IO.File]::Open($ArchivePath,[IO.FileMode]::Create,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None) + $archive=$null + try { + $archive=[IO.Compression.ZipArchive]::new($stream,[IO.Compression.ZipArchiveMode]::Create) + foreach ($relative in @($files.path)+@('d4r/package.json')) { + [void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive,(Join-Path $PackageRoot $relative),$relative,[IO.Compression.CompressionLevel]::Optimal) + } + } finally { if ($archive) { $archive.Dispose() }; $stream.Dispose() } +} +Write-Host "Windows drag-in release: $PackageRoot ($($targets.Count) GPU targets, no NVIDIA DLLs)" diff --git a/scripts/windows/test-player-config.ps1 b/scripts/windows/test-player-config.ps1 new file mode 100644 index 0000000..a01f640 --- /dev/null +++ b/scripts/windows/test-player-config.ps1 @@ -0,0 +1,88 @@ +# Drag-in player mode checks. Builds with the vendored llvm-mingw; needs no +# NVIDIA DLLs and launches no GPU kernels. The loader case enumerates HIP +# devices and is skipped (reported, not passed) without a supported AMD GPU. +[CmdletBinding()] +param( + [string]$CompilerRoot, [string]$HipRoot, [string]$OutputDirectory +) +$ErrorActionPreference='Stop' +$repo=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) +if (!$CompilerRoot) { $CompilerRoot=Join-Path $repo '.tools/llvm-mingw-20260922-ucrt-x86_64' } +if (!$HipRoot) { $HipRoot=Join-Path $repo '.tools/therock-10.2.0a20260929/_rocm_sdk_core' } +if (!$OutputDirectory) { $OutputDirectory=Join-Path $repo ('test-results/player-config/'+(Get-Date -Format 'yyyyMMdd-HHmmss-fff')) } +$OutputDirectory=[IO.Path]::GetFullPath($OutputDirectory) +New-Item -ItemType Directory -Force $OutputDirectory | Out-Null +$cxx=Join-Path $CompilerRoot 'bin/x86_64-w64-mingw32-clang++.exe' +if (!(Test-Path -LiteralPath $cxx)) { throw "llvm-mingw not found: $cxx" } +if (!(Test-Path -LiteralPath (Join-Path $HipRoot 'include/hip/hip_runtime_api.h'))) { throw "HIP headers not found under $HipRoot" } + +# Same generated target table as CMakeLists.txt. +$targets=(Get-Content -LiteralPath (Join-Path $repo 'tools/windows/gpu-targets.json') -Raw | ConvertFrom-Json).targets +$generated=Join-Path $OutputDirectory 'generated' +New-Item -ItemType Directory -Force $generated | Out-Null +$entries=($targets | ForEach-Object { " GpuTarget{`"$($_.architecture)`", $($_.elfMachine), $($_.isaMajor), `"$($_.family)`"}," }) -join "`n" +@('#pragma once','#include ','namespace d4r::diag {', + 'struct GpuTarget { const char* architecture; unsigned elf_machine; unsigned isa_major; const char* family; };', + "inline constexpr std::array gpu_targets = {{",$entries,'}};','}') | + Set-Content -LiteralPath (Join-Path $generated 'gpu_targets.generated.h') -Encoding ASCII +$common=@('-std=c++17','-O1','-static','-DWIN32_LEAN_AND_MEAN','-DNOMINMAX','-D__HIP_PLATFORM_AMD__','-DD4R_TARGET_ARCH="gfx1201"', + '-I',$generated,'-I',(Join-Path $repo 'tools/windows'),'-I',(Join-Path $repo 'tools'),'-I',(Join-Path $HipRoot 'include')) +# Windows PowerShell turns native stderr into terminating errors under Stop; +# judge native tools by exit code and keep their output in the log. +function Invoke-Native([string]$Log,[string]$Exe,[string[]]$Arguments) { + $ErrorActionPreference='Continue' + & $Exe @Arguments *> $Log + return $LASTEXITCODE +} +function Build([string]$Name,[string[]]$Arguments) { + $log=Join-Path $OutputDirectory "build-$Name.log" + # PowerShell 5 strips embedded quotes from native arguments; escape them. + if (Invoke-Native $log $cxx @($Arguments | ForEach-Object { $_.Replace('"','\"') })) { throw "Build failed: $Name; see $log" } +} +$results=@() + +# 1. d4r.ini validation and DLL pin decisions (the shim's own header). +$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')" } +$results+=@{case='d4r-ini-and-dll-pins'; passed=$true} + +# 2. Loader: GPU-free marker, LUID-based target selection, forwarding, and a +# clear failure when the folder has no build for the detected GPU. +$folder=Join-Path $OutputDirectory 'game/d4r' +New-Item -ItemType Directory -Force (Join-Path $folder 'hip/bin') | Out-Null +Build 'loader' ($common+@('-shared',(Join-Path $repo 'tools/windows/d4r_nvngx_loader.cpp'),'-o',(Join-Path $folder '_nvngx.dll'),'-lversion','-ldbghelp')) +$harness=Join-Path $OutputDirectory 'loader_selection_probe.exe' +Build 'harness' @('-municode','-static',(Join-Path $repo 'tools/windows/loader_selection_probe.cpp'),'-o',$harness,'-ldxgi','-ldxguid') +$stub=Join-Path $OutputDirectory 'stub.cpp' +@('#define API extern "C" __declspec(dllexport)', + 'struct Req { unsigned FeatureSupported, MinHWArchitecture; char MinOSVersion[255]; };', + 'API unsigned NVSDK_NGX_D3D12_GetFeatureRequirements(void*, const void*, Req* r) { r->FeatureSupported = 0; r->MinHWArchitecture = 0x5101; return 1; }') | + Set-Content -LiteralPath $stub -Encoding ASCII +$stubDll=Join-Path $OutputDirectory 'stub.dll' +Build 'stub' @('-shared','-static',$stub,'-o',$stubDll) +foreach ($name in @('amdhip64_7.dll','amd_comgr.dll','rocm_kpack.dll')) { + $source=Join-Path $HipRoot "bin/$name" + if (Test-Path -LiteralPath $source) { Copy-Item -LiteralPath $source -Destination (Join-Path $folder 'hip/bin') } +} +$log=Join-Path $OutputDirectory 'loader-without-builds.log' +[void](Invoke-Native $log $harness @((Join-Path $folder '_nvngx.dll'))) +$text=Get-Content -LiteralPath $log -Raw +if ($text -notmatch 'MARKER version=1 hip_loaded=0') { throw "Discovery marker loaded HIP or failed; see $log" } +if ($text -match 'supported=0') { throw "An adapter was reported supported without any target build; see $log" } +$gpuLog=Join-Path $folder 'd4r_nvngx.log' +$detected=$null +if (Test-Path -LiteralPath $gpuLog) { $detected=[regex]::Match((Get-Content -LiteralPath $gpuLog -Raw),'no build for your GPU \((gfx[0-9a-f]+)\)').Groups[1].Value } +if (!$detected) { + $results+=@{case='loader-selection'; passed=$null; skipped='no supported AMD HIP device detected'} + Write-Host 'SKIP loader selection: no supported AMD HIP device detected on this machine.' -ForegroundColor Yellow +} else { + if ((Get-Content -LiteralPath $gpuLog -Raw) -notmatch 'It supports: none \(the d4r folder is incomplete') { throw "Unclear unsupported-GPU message; see $gpuLog" } + Copy-Item -LiteralPath $stubDll -Destination (Join-Path $folder "_nvngx_$detected.dll") + $log=Join-Path $OutputDirectory 'loader-with-build.log' + [void](Invoke-Native $log $harness @((Join-Path $folder '_nvngx.dll'))) + if ((Get-Content -LiteralPath $log -Raw) -notmatch 'vendor=0x1002 status=0x1 supported=0 marker=0x5101') { throw "AMD adapter was not forwarded to _nvngx_$detected.dll; see $log" } + $results+=@{case='loader-selection'; passed=$true; architecture=$detected; gpuKernelsExecuted=$false} +} +$results | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $OutputDirectory 'summary.json') -Encoding UTF8 +Write-Host "PASS PLAYER_CONFIG cases=$(@($results | Where-Object { $_.passed }).Count) skipped=$(@($results | Where-Object { $_.skipped }).Count) Report: $OutputDirectory" diff --git a/scripts/windows/test-windows-release-package.ps1 b/scripts/windows/test-windows-release-package.ps1 new file mode 100644 index 0000000..1940789 --- /dev/null +++ b/scripts/windows/test-windows-release-package.ps1 @@ -0,0 +1,97 @@ +# Drag-in release packaging with synthetic per-target inputs. No GPU, no +# NVIDIA DLLs, no game. Code objects carry real AMDGPU ELF target headers. +[CmdletBinding()] +param([string]$OutputDirectory) +$ErrorActionPreference='Stop' +. (Join-Path $PSScriptRoot 'gpu-target.ps1') +$repo=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..')) +if (!$OutputDirectory) { $OutputDirectory=Join-Path $repo ('test-results/windows-release-package/'+(Get-Date -Format 'yyyyMMdd-HHmmss-fff')) } +$OutputDirectory=[IO.Path]::GetFullPath($OutputDirectory) +New-Item -ItemType Directory -Force $OutputDirectory | Out-Null +$targets=@(Get-D4RGpuTargets) + +function Write-CodeObject([string]$Path,[int]$Machine) { + $header=New-Object byte[] 64 + $header[0]=127; $header[1]=69; $header[2]=76; $header[3]=70; $header[4]=2; $header[5]=1; $header[18]=224; $header[48]=$Machine + New-Item -ItemType Directory -Force (Split-Path $Path) | Out-Null + [IO.File]::WriteAllBytes($Path,$header) +} +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/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 + } + $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 } + $files=@(Get-ChildItem -LiteralPath $root -Recurse -File | Where-Object { $_.Name -ne 'package.json' } | ForEach-Object { + @{path=$_.FullName.Substring($root.Length+1).Replace('\','/'); sha256=(Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash} + }) + @{architecture=$arch; d4rWorkingTreeDirty=$false; d4rCommit='fixture'; hardwareValidation='fixture'; + dlssSha256='3975567b8943c53acce397f2b72380092f84f162d00b0d2c7d08a1025c563983'; + zludaBuild=@{sourceCommit='fixture'}; optiScaler=@{sourceCommit='fixture'}; hipRuntime='fixture'; files=$files} | + ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $root 'package.json') -Encoding UTF8 + } +} +function Invoke-Package([string]$Prefix,[string]$Root) { + & (Join-Path $PSScriptRoot 'package-windows-release.ps1') -GamePackagePrefix $Prefix -LoaderDll $loader -PackageRoot $Root | Out-Null +} +$loader=Join-Path $OutputDirectory 'build/d4r_nvngx_loader.dll' +New-Item -ItemType Directory -Force (Split-Path $loader) | Out-Null +Set-Content -LiteralPath $loader 'loader fixture' -Encoding ASCII +$results=@() + +# 1. Layout of a good release. +$prefix=Join-Path $OutputDirectory 'inputs/good/game' +New-Inputs $prefix +$release=Join-Path $OutputDirectory 'release' +Invoke-Package $prefix $release +foreach ($path in @('dxgi.dll','OptiScaler.ini','d4r/_nvngx.dll','d4r/d4r.ini','d4r/dll-pins.txt','d4r/package.json', + 'd4r/hip/bin/amdhip64_7.dll','d4r/zluda/nvcuda.dll','d4r/licenses/d4r.txt','d4r/ngx/PUT _nvngx.dll HERE.txt')) { + if (!(Test-Path -LiteralPath (Join-Path $release $path))) { throw "Release lacks $path" } +} +if ((Get-Content -LiteralPath (Join-Path $release 'd4r/_nvngx.dll') -Raw).Trim() -ne 'loader fixture') { throw 'd4r/_nvngx.dll is not the loader.' } +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-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 } +} +foreach ($excluded in @('d4r/d4r_gpu_inventory.exe','d4r/d4r_debug_launcher.exe','windows-game.ps1','d4r/native/kernel1.hsaco','d4r/nvapi/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 +foreach ($file in $manifest.files) { + if ((Get-FileHash -LiteralPath (Join-Path $release $file.path) -Algorithm SHA256).Hash -ne $file.sha256) { throw "Manifest hash wrong: $($file.path)" } +} +$shipped=@(Get-ChildItem -LiteralPath $release -Recurse -File | ForEach-Object { $_.FullName.Substring($release.Length+1).Replace('\','/') } | Where-Object { $_ -ne 'd4r/package.json' }) +if (Compare-Object $shipped @($manifest.files.path)) { throw 'Manifest and shipped files differ.' } +$results+=@{case='drag-in-layout'; passed=$true; targets=$targets.Count; files=$shipped.Count} + +# 2. A code object for another GPU in a target's input is rejected. +$prefix=Join-Path $OutputDirectory 'inputs/mixed/game' +New-Inputs $prefix { param($root,$arch) if ($arch -eq 'gfx1101') { Write-CodeObject (Join-Path $root 'd4r/native/kernel3.hsaco') 65 } } +$rejected=$false +try { Invoke-Package $prefix (Join-Path $OutputDirectory 'release-mixed') } catch { $rejected=$_.Exception.Message -match 'does not target gfx1101' } +if (!$rejected) { throw 'A gfx1100 code object in the gfx1101 input was not rejected.' } +$results+=@{case='mixed-target-rejected'; passed=$true} + +# 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 } } +$rejected=$false +try { Invoke-Package $prefix (Join-Path $OutputDirectory 'release-runtime') } catch { $rejected=$_.Exception.Message -match 'Shared runtime differs' } +if (!$rejected) { throw 'A differing shared runtime was not rejected.' } +$results+=@{case='shared-runtime-mismatch-rejected'; passed=$true} + +$results | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $OutputDirectory 'summary.json') -Encoding UTF8 +Write-Host "PASS WINDOWS_RELEASE_PACKAGE cases=$($results.Count) Report: $OutputDirectory" diff --git a/tools/windows/d4r_nvngx_loader.cpp b/tools/windows/d4r_nvngx_loader.cpp new file mode 100644 index 0000000..cd4630c --- /dev/null +++ b/tools/windows/d4r_nvngx_loader.cpp @@ -0,0 +1,176 @@ +// Drag-in releases ship one d4r folder for every supported GPU. This small +// _nvngx.dll is what OptiScaler loads: on first use it reads the real HIP +// architecture of the game's adapter (matched by LUID) and loads that +// target's shim, d4r\_nvngx_.dll, from the same folder. The per-target +// shim still enforces its own compiled target, so a mismatch cannot run. +// Keeping every file in one folder preserves NGX's search for +// nvngx_dlss.dll beside the calling module. +#include "d3d12_external.h" +#include "ngx_public_abi.h" +#include +#include +#include + +namespace { +constexpr unsigned failure = 0xBAD00002, unsupported = 4; +std::mutex selectMutex; +HMODULE shim = nullptr; +bool selectionFailed = false; + +std::filesystem::path module_directory() { + HMODULE module = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&module_directory), &module); + wchar_t path[32768]{}; GetModuleFileNameW(module, path, 32768); + return std::filesystem::path(path).parent_path(); +} +// The shim never ran, so the loader writes the player's log itself. +// Fresh at each launch, then appended: OptiScaler can query several adapters. +void report(const std::string& message) { + static bool fresh = true; + std::ofstream(module_directory() / L"d4r_nvngx.log", fresh ? std::ios::trunc : std::ios::app) + << "d4r: DLSS disabled: " << message << "\n"; + fresh = false; + std::fprintf(stderr, "D4R_WINDOWS_FAILURE %s\n", message.c_str()); +} +std::string supported_list(const std::filesystem::path& dir) { + std::string list; + for (const auto& target : d4r::diag::gpu_targets) + if (std::filesystem::exists(dir / (L"_nvngx_" + d4r::diag::wide(target.architecture) + L".dll"))) + list += (list.empty() ? "" : ", ") + std::string(target.architecture); + return list.empty() ? "none (the d4r folder is incomplete; extract the ZIP again)" : list; +} + +// Loads the matching shim once. A null LUID (calls made before NGX init) +// accepts the only HIP device with a shim in this folder. +HMODULE select(const LUID* luid) { + std::lock_guard lock(selectMutex); + if (shim || selectionFailed) return shim; + const auto dir = module_directory(); + try { + // Spoofed architectures invalidate the target guarantee; see hip_api.h. + for (const char* name : {"HSA_OVERRIDE_GFX_VERSION", "HSA_OVERRIDE_GFX_VERSION_0"}) + if (std::getenv(name)) throw std::runtime_error(std::string("unset ") + name + "; d4r does not support architecture overrides"); + 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; + 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; + } + 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) + "."); + 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) + "."); + 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. + throw std::runtime_error("cannot load " + d4r::diag::utf8(path.c_str()) + " (Win32 error " + std::to_string(GetLastError()) + ")"); + } + } catch (const std::exception& error) { + // An adapter without a matching GPU (an iGPU queried first) must not + // prevent selecting the discrete GPU on a later call. + report(error.what()); + } + return shim; +} +template Fn resolve(const char* name, const LUID* luid = nullptr) { + HMODULE module = select(luid); + return module ? reinterpret_cast(reinterpret_cast(GetProcAddress(module, name))) : nullptr; +} +LUID device_luid(ID3D12Device* device) { return d4r::win::adapter_luid(device); } +} + +#define API extern "C" __declspec(dllexport) +// Discovery marker for OptiScaler: no GPU or runtime initialization. +API unsigned d4r_WindowsBackendVersion() { return 1; } + +API unsigned NVSDK_NGX_D3D12_Init_Ext(unsigned long long app, const wchar_t* data, ID3D12Device* device, unsigned sdk, const void* info) { + if (!device) return 0xBAD00005; + const LUID luid = device_luid(device); + auto fn = resolve("NVSDK_NGX_D3D12_Init_Ext", &luid); + return fn ? fn(app, data, device, sdk, info) : failure; +} +API unsigned NVSDK_NGX_D3D12_Init(unsigned long long app, const wchar_t* data, ID3D12Device* device, const void* info, unsigned sdk) { + if (!device) return 0xBAD00005; + const LUID luid = device_luid(device); + auto fn = resolve("NVSDK_NGX_D3D12_Init", &luid); + return fn ? fn(app, data, device, info, sdk) : failure; +} +API unsigned NVSDK_NGX_D3D12_Init_ProjectID(const char* project, int engine, const char* version, const wchar_t* data, ID3D12Device* device, unsigned sdk, const void* info) { + if (!device) return 0xBAD00005; + const LUID luid = device_luid(device); + auto fn = resolve("NVSDK_NGX_D3D12_Init_ProjectID", &luid); + return fn ? fn(project, engine, version, data, device, sdk, info) : failure; +} +API unsigned NVSDK_NGX_D3D12_GetFeatureRequirements(IDXGIAdapter* adapter, const d4r::ngx::FeatureDiscoveryInfo* info, + d4r::ngx::FeatureRequirement* requirements) { + if (!adapter || !info || !requirements) return 0xBAD00005; + DXGI_ADAPTER_DESC desc{}; + if (FAILED(adapter->GetDesc(&desc))) return failure; + auto fn = resolve("NVSDK_NGX_D3D12_GetFeatureRequirements", &desc.AdapterLuid); + if (fn) return fn(adapter, info, requirements); + // Same answer as the shim for an adapter without a matching HIP device. + *requirements = {}; requirements->FeatureSupported = unsupported; + std::strcpy(requirements->MinOSVersion, "10.0.22000.0"); + return 1; +} +API unsigned NVSDK_NGX_D3D12_AllocateParameters(void** p) { + auto fn = resolve("NVSDK_NGX_D3D12_AllocateParameters"); + return fn ? fn(p) : failure; +} +API unsigned NVSDK_NGX_D3D12_GetCapabilityParameters(void** p) { + auto fn = resolve("NVSDK_NGX_D3D12_GetCapabilityParameters"); + return fn ? fn(p) : failure; +} +API unsigned NVSDK_NGX_D3D12_GetParameters(void** p) { + auto fn = resolve("NVSDK_NGX_D3D12_GetParameters"); + return fn ? fn(p) : failure; +} +API unsigned NVSDK_NGX_D3D12_DestroyParameters(void* p) { + auto fn = resolve("NVSDK_NGX_D3D12_DestroyParameters"); + return fn ? fn(p) : failure; +} +API unsigned NVSDK_NGX_D3D12_GetScratchBufferSize(unsigned feature, const void* p, size_t* size) { + auto fn = resolve("NVSDK_NGX_D3D12_GetScratchBufferSize"); + return fn ? fn(feature, p, size) : failure; +} +API unsigned NVSDK_NGX_D3D12_CreateFeature(ID3D12GraphicsCommandList* list, unsigned feature, void* p, void** out) { + auto fn = resolve("NVSDK_NGX_D3D12_CreateFeature"); + return fn ? fn(list, feature, p, out) : failure; +} +API unsigned d4r_D3D12_EvaluateAtBoundary(ID3D12CommandQueue* queue, void* handle, void* p) { + auto fn = resolve("d4r_D3D12_EvaluateAtBoundary"); + return fn ? fn(queue, handle, p) : failure; +} +API unsigned NVSDK_NGX_D3D12_EvaluateFeature(ID3D12GraphicsCommandList* list, const void* handle, void* p, void* callback) { + auto fn = resolve("NVSDK_NGX_D3D12_EvaluateFeature"); + return fn ? fn(list, handle, p, callback) : failure; +} +API unsigned NVSDK_NGX_D3D12_EvaluateFeature_C(ID3D12GraphicsCommandList* list, const void* handle, void* p, void* callback) { + auto fn = resolve("NVSDK_NGX_D3D12_EvaluateFeature_C"); + return fn ? fn(list, handle, p, callback) : failure; +} +API unsigned NVSDK_NGX_D3D12_ReleaseFeature(void* handle) { + auto fn = resolve("NVSDK_NGX_D3D12_ReleaseFeature"); + return fn ? fn(handle) : failure; +} +API unsigned NVSDK_NGX_D3D12_Shutdown() { + // Nothing to shut down if no shim was ever selected. + { std::lock_guard lock(selectMutex); if (!shim) return 1; } + auto fn = resolve("NVSDK_NGX_D3D12_Shutdown"); + return fn ? fn() : failure; +} +API unsigned NVSDK_NGX_D3D12_Shutdown1(ID3D12Device* device) { + { std::lock_guard lock(selectMutex); if (!shim) return 1; } + auto fn = resolve("NVSDK_NGX_D3D12_Shutdown1"); + return fn ? fn(device) : failure; +} diff --git a/tools/windows/d4r_nvngx_windows.cpp b/tools/windows/d4r_nvngx_windows.cpp index 4d550fe..1ca5a43 100644 --- a/tools/windows/d4r_nvngx_windows.cpp +++ b/tools/windows/d4r_nvngx_windows.cpp @@ -1,5 +1,6 @@ // Native Windows backend. The Linux/Wine implementation remains separate. #include "ngx_windows_runtime.h" +#include "player_config.h" #include #include #include @@ -59,6 +60,7 @@ API unsigned d4r_WindowsBackendVersion() { return 1; } API unsigned NVSDK_NGX_D3D12_Init_Ext(unsigned long long app, const wchar_t* data, ID3D12Device* device, unsigned sdk, const void* info) { std::lock_guard lock(apiMutex); return call([&] { + d4r::win::player::ensure_environment(); if (!runtime) runtime = std::make_shared(device, app, data, sdk, static_cast(info)); if (const char* backend = std::getenv("D4R_D3D12_COMMAND_BACKEND"); backend && std::string(backend) == "1") d4r::win::commands::install(device); @@ -72,6 +74,7 @@ API unsigned NVSDK_NGX_D3D12_Init_ProjectID(const char* project, int engine, con std::lock_guard lock(apiMutex); return call([&] { if (!project || !*project) return invalid; + d4r::win::player::ensure_environment(); d4r::ngx::ProjectIdentity identity{project, engine, version ? version : ""}; if (!runtime) runtime = std::make_shared(device, 0, data, sdk, static_cast(info), &identity); if (const char* backend = std::getenv("D4R_D3D12_COMMAND_BACKEND"); backend && std::string(backend) == "1") d4r::win::commands::install(device); @@ -84,6 +87,7 @@ API unsigned NVSDK_NGX_D3D12_GetFeatureRequirements(IDXGIAdapter* adapter, const return call([&] { *requirements = {}; requirements->FeatureSupported = 16; // Unsupported feature. if (info->FeatureID != 1) return 1u; + d4r::win::player::ensure_environment(); DXGI_ADAPTER_DESC desc{}; d4r::win::dx(adapter->GetDesc(&desc), "NGX requirements adapter"); // OptiScaler can override the DXGI vendor ID. The physical HIP // architecture and full adapter LUID are the authoritative identity. @@ -116,6 +120,7 @@ API unsigned NVSDK_NGX_D3D12_CreateFeature(ID3D12GraphicsCommandList*, unsigned std::lock_guard lock(apiMutex); return call([&] { if (!runtime || feature != 1 || !p || !out) return invalid; + d4r::win::player::apply_preset(p); auto value = std::make_shared(runtime, p); auto handle = std::make_unique(); handle->Id = nextId++; features.emplace(handle.get(), std::move(value)); *out = handle.release(); return 1u; @@ -141,6 +146,7 @@ API unsigned NVSDK_NGX_D3D12_EvaluateFeature(ID3D12GraphicsCommandList* list, co }; auto snapshot = std::make_shared(); if (!snapshot->parameters) return failure; + d4r::win::player::apply_preset(p); d4r::ngx::copy_create(p, snapshot->parameters); d4r::ngx::copy_frame(p, snapshot->parameters); const char* names[] = {"Color", "Depth", "MotionVectors", "Output", "ExposureTexture"}; const char* states[] = {"D4R.Color.State", "D4R.Depth.State", "D4R.Motion.State", "D4R.Output.State", "D4R.Exposure.State"}; diff --git a/tools/windows/dll-pins.txt b/tools/windows/dll-pins.txt new file mode 100644 index 0000000..5f70608 --- /dev/null +++ b/tools/windows/dll-pins.txt @@ -0,0 +1,9 @@ +# NVIDIA DLLs the Windows backend accepts, by exact SHA256. NVIDIA DLLs are +# never shipped or committed; players copy their own. +# dlss = nvngx_dlss.dll (d4r\nvngx_dlss.dll) +# ngx = _nvngx.dll, NVIDIA's NGX core from a display driver (d4r\ngx\_nvngx.dll) +# status: validated = tested on Windows hardware; anything else is accepted +# but reported as unvalidated in d4r\d4r_nvngx.log. +# kind sha256 version status +dlss 3975567B8943C53ACCE397F2B72380092F84F162D00B0D2C7D08A1025C563983 310.9.1 validated +ngx 66767018C36B3BAB46398DADE3ADF173DAA3730FDA75965689EA848C9BC4E79B 32.0.16.1714 validated diff --git a/tools/windows/loader_selection_probe.cpp b/tools/windows/loader_selection_probe.cpp new file mode 100644 index 0000000..50fb53f --- /dev/null +++ b/tools/windows/loader_selection_probe.cpp @@ -0,0 +1,33 @@ +// Calls the drag-in loader the way OptiScaler does: the GPU-free discovery +// marker, then NGX feature requirements for every DXGI adapter. +#include +#include +#include + +struct Info { unsigned SDKVersion, FeatureID; char reserved[48]; }; +struct Requirement { unsigned FeatureSupported, MinHWArchitecture; char MinOSVersion[255]; }; +using Requirements = unsigned (*)(IDXGIAdapter*, const Info*, Requirement*); +using Version = unsigned (*)(); + +int wmain(int argc, wchar_t** argv) { + if (argc != 2) { std::printf("usage: loader_selection_probe LOADER_DLL\n"); return 2; } + HMODULE loader = LoadLibraryW(argv[1]); + if (!loader) { std::printf("LOAD_FAILED error=%lu\n", GetLastError()); return 2; } + auto version = reinterpret_cast(reinterpret_cast(GetProcAddress(loader, "d4r_WindowsBackendVersion"))); + auto requirements = reinterpret_cast(reinterpret_cast(GetProcAddress(loader, "NVSDK_NGX_D3D12_GetFeatureRequirements"))); + if (!version || !requirements) { std::printf("EXPORTS_MISSING\n"); return 2; } + std::printf("MARKER version=%u hip_loaded=%d\n", version(), GetModuleHandleW(L"amdhip64_7.dll") != nullptr); + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&factory)))) return 2; + IDXGIAdapter1* adapter = nullptr; + for (UINT i = 0; factory->EnumAdapters1(i, &adapter) == S_OK; ++i) { + DXGI_ADAPTER_DESC1 desc{}; adapter->GetDesc1(&desc); + Info info{0x15, 1, {}}; Requirement result{}; + const unsigned status = requirements(adapter, &info, &result); + std::printf("ADAPTER vendor=0x%04x status=0x%x supported=%u marker=0x%x name=%ls\n", desc.VendorId, status, + result.FeatureSupported, result.MinHWArchitecture, desc.Description); + adapter->Release(); + } + factory->Release(); + return 0; +} diff --git a/tools/windows/player_config.h b/tools/windows/player_config.h new file mode 100644 index 0000000..c86e72b --- /dev/null +++ b/tools/windows/player_config.h @@ -0,0 +1,314 @@ +#pragma once +// Drag-in install, as on Linux: the game folder holds dxgi.dll (OptiScaler), +// OptiScaler.ini and a d4r folder containing this shim and d4r.ini. When the +// launcher has not set D4R_HIP_ROOT and d4r.ini exists beside this DLL, the +// shim derives the backend's environment itself, so the game can be started +// from Steam, Epic or a shortcut. Without d4r.ini nothing changes: diagnostic +// runs keep using the launcher's environment. +// +// Codegen switches are fixed to the Windows-validated values; d4r.ini only +// selects the model (K/M), diagnostics and the cache folder. NVIDIA DLLs must +// match an identity in dll-pins.txt before anything is loaded. +#include "hip_api.h" +#include "ngx_parameters.h" +#include +#include +#include +#include +#include + +namespace d4r::win::player { +struct IniEntry { std::string section, key, value; }; +struct Pin { std::string kind, sha256, version, status; }; +struct Settings { + unsigned preset = 11; // K + bool asyncInterop = false, validateOutput = false, verbose = false; + std::wstring cacheDir; +}; +struct State { + bool active = false; + unsigned preset = 0; + std::filesystem::path dir; + std::vector notes; +}; +inline State& state() { static State value; return value; } + +inline std::string trim(const std::string& value) { + const auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return {}; + return value.substr(first, value.find_last_not_of(" \t\r\n") - first + 1); +} +inline std::string lower(std::string value) { + for (char& c : value) c = static_cast(std::tolower(static_cast(c))); + return value; +} +inline std::string upper(std::string value) { + for (char& c : value) c = static_cast(std::toupper(static_cast(c))); + return value; +} +inline std::filesystem::path module_directory() { + HMODULE module = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&module_directory), &module)) throw std::runtime_error("Locate the d4r shim directory"); + wchar_t path[32768]{}; const DWORD length = GetModuleFileNameW(module, path, 32768); + if (!length || length == 32768) throw std::runtime_error("d4r shim path is invalid"); + return std::filesystem::path(path).parent_path(); +} +inline std::string read_text(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("Cannot read " + diag::utf8(path.c_str())); + std::stringstream text; text << file.rdbuf(); return text.str(); +} + +// The configparser dialect of packaging/d4r.ini and the Linux shim: [Section], +// key = value, full-line ; or # comments, inline comments after whitespace. +inline std::vector parse_ini(const std::string& text) { + std::vector entries; std::string section; + size_t start = text.compare(0, 3, "\xEF\xBB\xBF") == 0 ? 3 : 0; + while (start < text.size()) { + size_t end = text.find('\n', start); + if (end == std::string::npos) end = text.size(); + const std::string line = trim(text.substr(start, end - start)); + start = end + 1; + if (line.empty() || line[0] == ';' || line[0] == '#') continue; + if (line[0] == '[') { section = trim(line.substr(1, line.find(']') - 1)); continue; } + const size_t equals = line.find('='); + if (equals == std::string::npos) continue; + std::string value = line.substr(equals + 1); + for (size_t i = 1; i < value.size(); ++i) + if ((value[i] == ';' || value[i] == '#') && (value[i - 1] == ' ' || value[i - 1] == '\t')) { value.resize(i); break; } + entries.push_back({section, trim(line.substr(0, equals)), trim(value)}); + } + return entries; +} + +// Invalid values fall back to the safe default and are reported in the log +// (and by d4r-check.ps1), rather than silently changing behaviour. +inline Settings read_settings(const std::vector& ini, std::vector& notes) { + Settings settings; + const auto flag = [&](const IniEntry& entry, bool fallback) { + const auto value = lower(entry.value); + if (value.empty() || value == "auto") return fallback; + if (value == "1" || value == "true" || value == "yes" || value == "on") return true; + if (value == "0" || value == "false" || value == "no" || value == "off") return false; + notes.push_back("d4r.ini: [" + entry.section + "] " + entry.key + " must be true or false, not '" + entry.value + + "'; using " + (fallback ? "true" : "false")); + return fallback; + }; + for (const auto& entry : ini) { + const auto name = lower(entry.section + "." + entry.key); // Matched case-insensitively, reported as written. + if (name == "dlss.model") { + const auto value = upper(entry.value); + if (value.empty() || value == "AUTO" || value == "K" || value == "DLSS4" || value == "11") settings.preset = 11; + else if (value == "M" || value == "DLSS4.5" || value == "13") settings.preset = 13; + else notes.push_back("d4r.ini: [DLSS] Model '" + entry.value + "' is not available on Windows (use K or M); using K"); + } else if (name == "interop.asyncinterop") settings.asyncInterop = flag(entry, false); + else if (name == "debug.validateoutput") settings.validateOutput = flag(entry, false); + else if (name == "debug.log") { + const auto value = lower(entry.value); + if (value == "verbose") settings.verbose = true; + else if (!value.empty() && value != "normal" && value != "auto") + notes.push_back("d4r.ini: [Debug] Log must be normal or verbose, not '" + entry.value + "'; using normal"); + } else if (name == "paths.cachedir") { + if (!entry.value.empty() && lower(entry.value) != "auto") settings.cacheDir = diag::wide(entry.value); + } else notes.push_back("d4r.ini: [" + entry.section + "] " + entry.key + " is not a Windows setting; ignored"); + } + return settings; +} + +// "kind sha256 version status" lines; kind is dlss or ngx. +inline std::vector parse_pins(const std::string& text) { + std::vector pins; std::istringstream lines(text); std::string line; + while (std::getline(lines, line)) { + line = trim(line); + if (line.empty() || line[0] == '#') continue; + std::istringstream fields(line); Pin pin; + if (!(fields >> pin.kind >> pin.sha256 >> pin.version >> pin.status) || pin.sha256.size() != 64 || + (pin.kind != "dlss" && pin.kind != "ngx")) throw std::runtime_error("dll-pins.txt is damaged; extract the d4r ZIP again"); + pin.sha256 = upper(pin.sha256); pins.push_back(pin); + } + return pins; +} + +inline std::string sha256(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return {}; + BCRYPT_ALG_HANDLE algorithm = nullptr; BCRYPT_HASH_HANDLE hash = nullptr; + struct Cleanup { BCRYPT_ALG_HANDLE& a; BCRYPT_HASH_HANDLE& h; + ~Cleanup() { if (h) BCryptDestroyHash(h); if (a) BCryptCloseAlgorithmProvider(a, 0); } } cleanup{algorithm, hash}; + if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) < 0 || + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) < 0) throw std::runtime_error("SHA256 unavailable"); + std::vector buffer(1 << 20); + while (file) { + file.read(buffer.data(), std::streamsize(buffer.size())); + if (file.gcount() > 0 && BCryptHashData(hash, reinterpret_cast(buffer.data()), ULONG(file.gcount()), 0) < 0) + throw std::runtime_error("SHA256 failed"); + } + unsigned char digest[32]{}; + if (BCryptFinishHash(hash, digest, sizeof(digest), 0) < 0) throw std::runtime_error("SHA256 failed"); + static const char hex[] = "0123456789ABCDEF"; std::string text; + for (unsigned char byte : digest) { text += hex[byte >> 4]; text += hex[byte & 15]; } + return text; +} +inline std::string file_version(const std::filesystem::path& path) { + DWORD ignored = 0; const DWORD size = GetFileVersionInfoSizeW(path.c_str(), &ignored); + std::vector data(size); + VS_FIXEDFILEINFO* info = nullptr; UINT length = 0; + if (!size || !GetFileVersionInfoW(path.c_str(), 0, size, data.data()) || + !VerQueryValueW(data.data(), L"\\", reinterpret_cast(&info), &length) || !info) return "unknown"; + return std::to_string(HIWORD(info->dwFileVersionMS)) + "." + std::to_string(LOWORD(info->dwFileVersionMS)) + "." + + std::to_string(HIWORD(info->dwFileVersionLS)) + "." + std::to_string(LOWORD(info->dwFileVersionLS)); +} +// Exact identity check with the fix spelled out: what was found, what is needed. +inline void verify_dll(const std::filesystem::path& dir, const std::vector& pins, const char* kind, + const wchar_t* relative, const char* label, std::vector& notes) { + const auto path = dir / relative; + std::string wanted; + for (const auto& pin : pins) if (pin.kind == kind) wanted += (wanted.empty() ? "" : " or ") + pin.version; + const auto hash = sha256(path); + if (hash.empty()) + throw std::runtime_error(std::string(label) + " is missing. Copy NVIDIA's " + label + " (version " + wanted + + ") to d4r\\" + diag::utf8(relative) + " and restart the game."); + for (const auto& pin : pins) if (pin.kind == kind && pin.sha256 == hash) { + if (pin.status != "validated") + notes.push_back(std::string(label) + " " + pin.version + " is accepted but not yet validated on Windows"); + return; + } + throw std::runtime_error("Wrong " + std::string(label) + ": you have version " + file_version(path) + + " (or a modified file); d4r on Windows needs exactly " + wanted + ". Replace d4r\\" + diag::utf8(relative) + + " and restart the game."); +} + +// The installer used to check these before touching the game; a drag-in +// install checks them at startup instead. +inline void verify_code_objects(const std::filesystem::path& dir) { + const std::string arch = diag::HipApi::target_arch(); + std::vector objects{dir / (L"pixel_convert_" + diag::wide(arch) + L".hsaco")}; + const auto native = dir / L"native" / diag::wide(arch); + if (!std::filesystem::is_directory(native)) throw std::runtime_error("d4r\\native\\" + arch + " is missing; extract the d4r ZIP again"); + for (const auto& entry : std::filesystem::directory_iterator(native)) + if (entry.path().extension() == L".hsaco") objects.push_back(entry.path()); + for (const auto& object : objects) { + try { diag::require_code_object_target(object, arch); } + catch (const std::exception&) { + throw std::runtime_error(diag::utf8(object.filename().c_str()) + " is not built for " + arch + + " or is damaged; extract the d4r ZIP again (do not rename or mix files from other GPUs)"); + } + } +} +inline void reject_translation_layer() { + wchar_t exe[32768]{}; GetModuleFileNameW(nullptr, exe, 32768); + const auto proxy = std::filesystem::path(exe).parent_path() / L"d3d12.dll"; + std::error_code error; + const auto size = std::filesystem::file_size(proxy, error); + if (error || size > 64ull << 20) return; + auto text = lower(read_text(proxy)); + if (text.find("vkd3d") != std::string::npos || text.find("wined3d") != std::string::npos) + throw std::runtime_error("the game folder contains a d3d12.dll from vkd3d-proton/Wine (left over from a Linux " + "install?). Move d3d12.dll and d3d12core.dll out of the game folder; d4r on Windows uses native D3D12"); +} + +// Update the OS block (ZLUDA/Rust, later DLLs) and this CRT's copy (getenv). +inline void set_variable(const wchar_t* name, const std::optional& value) { + SetEnvironmentVariableW(name, value ? value->c_str() : nullptr); + _wputenv_s(name, value ? value->c_str() : L""); +} +// Only when the game has no console/pipe: never steal a launcher's streams. +inline bool unattached(DWORD stream) { + const HANDLE current = GetStdHandle(stream); + return !current || current == INVALID_HANDLE_VALUE; +} +inline void redirect(FILE* crt, DWORD stream, const std::filesystem::path& path) { + FILE* reopened = nullptr; + if (_wfreopen_s(&reopened, path.c_str(), L"a", crt) || !reopened) return; + std::setvbuf(crt, nullptr, _IONBF, 0); + SetStdHandle(stream, reinterpret_cast(_get_osfhandle(_fileno(crt)))); +} + +inline void apply(const std::filesystem::path& dir, const Settings& settings) { + std::filesystem::path cache = settings.cacheDir; + if (cache.empty()) { + const wchar_t* localAppData = _wgetenv(L"LOCALAPPDATA"); + if (!localAppData || !*localAppData) throw std::runtime_error("LOCALAPPDATA is not set; set [Paths] CacheDir in d4r.ini"); + cache = std::filesystem::path(localAppData) / L"d4r" / L"cache"; + } else if (cache.is_relative()) cache = dir / cache; + std::filesystem::create_directories(cache); + 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"); + // Same codegen and paths as windows-game.ps1; only diagnostics differ. + const std::pair> values[] = { + {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", p(L"nvapi\\" + diag::wide(diag::HipApi::target_arch()) + L"\\nvapi64.dll")}, {L"D4R_NVAPI_BACKEND", p(L"zluda\\nvapi64.dll")}, + {L"D4R_NGX_CORE", p(L"ngx\\_nvngx.dll")}, {L"D4R_DLSS_DLL", p(L"nvngx_dlss.dll")}, + {L"D4R_FORMAT_MODULE", p(L"pixel_convert_" + diag::wide(diag::HipApi::target_arch()) + L".hsaco")}, + {L"D4R_ZLUDA_NATIVE_DIR", p(L"native\\" + diag::wide(diag::HipApi::target_arch()))},{L"D4R_D3D12_COMMAND_BACKEND", on}, + {L"D4R_ZLUDA_WMMA", on}, {L"D4R_ZLUDA_WMMA_FP8", on}, {L"D4R_ZLUDA_WMMA_FP8_NATIVE", off}, + {L"D4R_ZLUDA_WMMA_F16_REFERENCE", on}, + {L"D4R_ZLUDA_IGNORE_DENORMAL", unset}, {L"D4R_ZLUDA_FAST_MATH", unset}, {L"D4R_ZLUDA_WMMA_F32ACC", unset}, + {L"D4R_ZLUDA_WAVE64", unset}, {L"D4R_ZLUDA_WMMA_LAYOUT", unset}, + // Per-launch kernel lines are diagnostics; ZLUDA excludes this flag from its cache key. + {L"D4R_ZLUDA_VERBOSE", settings.verbose ? on : unset}, + {L"D4R_QUIET_API", settings.verbose ? unset : on}, + {L"D4R_VALIDATE_OUTPUT", settings.validateOutput ? on : unset}, + {L"D4R_ASYNC_INTEROP", settings.asyncInterop ? on : unset}, + {L"D4R_PROFILE_STAGES", unset}, {L"D4R_ZLUDA_PROFILE", unset}, {L"D4R_ZLUDA_PROFILE_DEFERRED", unset}, + {L"D4R_ZLUDA_PROFILE_ALLOW_LEGACY", unset}, {L"D4R_ZLUDA_PROFILE_EVERY", unset}, + {L"D4R_PROFILE_COMMAND_HOOKS", unset}, {L"D4R_ZLUDA_PROFILE_API", unset}, {L"D4R_PROFILE_GPU_BOUNDARY", unset}, + {L"D4R_DISABLE_INTEROP_LIST_CACHE", unset}, {L"D4R_BATCH_INPUT_COPIES", unset}, {L"D4R_INTEROP_VERIFY", unset}, + {L"D4R_DIAG_DIR", unset}, {L"ZLUDA_LOG_DIR", unset}, + {L"ZLUDA_CACHE_DIR", std::optional(cache.wstring())}, + {L"PATH", std::optional((dir / L"hip\\bin").wstring() + L";" + (dir / L"zluda").wstring() + + (path ? std::wstring(L";") + path : std::wstring()))}, + }; + for (const auto& [name, value] : values) set_variable(name, value); + const wchar_t* wgp = _wgetenv(L"D4R_ZLUDA_WGP"); + if (wgp && std::wstring(wgp) != L"1") set_variable(L"D4R_ZLUDA_WGP", unset); +} + +// Called before any path variable is read. A failure leaves DLSS unavailable +// (the game keeps its own upscaler) and is written to d4r\d4r_nvngx.log. +inline void ensure_environment() { + static std::once_flag once; static std::string failure; + std::call_once(once, [] { + try { + if (const char* root = std::getenv("D4R_HIP_ROOT"); root && *root) return; // Launcher/diagnostic run. + const auto dir = module_directory(); + if (!std::filesystem::exists(dir / L"d4r.ini")) return; // Developer layout: keep the original requirement. + auto& current = state(); current.dir = dir; + const auto log = dir / L"d4r_nvngx.log"; + if (unattached(STD_ERROR_HANDLE)) { + { std::ofstream truncate(log, std::ios::trunc); } // Rewritten at every launch, as on Linux. + redirect(stderr, STD_ERROR_HANDLE, log); + } + const auto settings = read_settings(parse_ini(read_text(dir / L"d4r.ini")), current.notes); + if (settings.verbose && unattached(STD_OUTPUT_HANDLE)) redirect(stdout, STD_OUTPUT_HANDLE, log); + const auto pins = parse_pins(read_text(dir / L"dll-pins.txt")); + verify_dll(dir, pins, "dlss", L"nvngx_dlss.dll", "nvngx_dlss.dll", current.notes); + verify_dll(dir, pins, "ngx", L"ngx\\_nvngx.dll", "_nvngx.dll", current.notes); + reject_translation_layer(); + verify_code_objects(dir); + apply(dir, settings); + current.active = true; current.preset = settings.preset; + std::fprintf(stderr, "d4r: player mode, model=%s validate=%u async=%u log=%s, architecture=%s\n", + settings.preset == 13 ? "M" : "K", unsigned(settings.validateOutput), unsigned(settings.asyncInterop), + settings.verbose ? "verbose" : "normal", diag::HipApi::target_arch()); + } catch (const std::exception& error) { failure = error.what(); } + for (const auto& note : state().notes) std::fprintf(stderr, "d4r: %s\n", note.c_str()); + if (!failure.empty()) std::fprintf(stderr, "d4r: DLSS disabled: %s\n", failure.c_str()); + }); + if (!failure.empty()) throw std::runtime_error(failure); +} + +// [DLSS] Model forces one render preset for every quality mode, as the Linux +// shim does. Applied at create and evaluate so a recreated feature cannot fall +// back to the game's preset. Not used by launcher runs (OptiScaler sets it). +inline void apply_preset(void* parameters) { + const auto& current = state(); + if (!current.active || !current.preset || !parameters) return; + for (const char* name : {"DLSS.Hint.Render.Preset.DLAA", "DLSS.Hint.Render.Preset.Quality", "DLSS.Hint.Render.Preset.Balanced", + "DLSS.Hint.Render.Preset.Performance", "DLSS.Hint.Render.Preset.UltraPerformance", "DLSS.Hint.Render.Preset.UltraQuality"}) + d4r_ngx_set_uint(parameters, name, current.preset); +} +} diff --git a/tools/windows/player_config_probe.cpp b/tools/windows/player_config_probe.cpp new file mode 100644 index 0000000..99f067e --- /dev/null +++ b/tools/windows/player_config_probe.cpp @@ -0,0 +1,76 @@ +// Checks the drag-in d4r.ini parser and NVIDIA DLL pin decisions with +// fixtures. No GPU work, no NVIDIA binaries. +#include "player_config.h" +#include + +namespace player = d4r::win::player; +namespace { +int failures = 0; +void expect(bool condition, const std::string& what) { + std::printf("%s %s\n", condition ? "ok " : "FAIL", what.c_str()); + if (!condition) ++failures; +} +bool has_note(const std::vector& notes, const std::string& text) { + for (const auto& note : notes) if (note.find(text) != std::string::npos) return true; + return false; +} +player::Settings settings(const std::string& ini, std::vector& notes) { + notes.clear(); return player::read_settings(player::parse_ini(ini), notes); +} +std::string error_of(const std::function& fn) { + try { fn(); } catch (const std::exception& error) { return error.what(); } + return {}; +} +} + +int wmain(int argc, wchar_t** argv) { + if (argc != 2) { std::printf("usage: player_config_probe FIXTURE_DIR\n"); return 2; } + const std::filesystem::path dir(argv[1]); + std::vector notes; + + auto s = settings("", notes); + expect(s.preset == 11 && !s.asyncInterop && !s.validateOutput && !s.verbose && notes.empty(), "empty d4r.ini uses K, sync, no validation"); + s = settings("\xEF\xBB\xBF[DLSS]\nModel = M ; DLSS 4.5\n", notes); + expect(s.preset == 13 && notes.empty(), "BOM and inline comment; Model = M"); + s = settings("[dlss]\nmodel = dlss4.5\n", notes); + expect(s.preset == 13, "case-insensitive section/key and alias DLSS4.5"); + for (const char* model : {"E", "L", "game", "7"}) { + s = settings(std::string("[DLSS]\nModel = ") + model + "\n", notes); + expect(s.preset == 11 && has_note(notes, "not available on Windows"), std::string("Model = ") + model + " is rejected with a note and K"); + } + s = settings("[Interop]\nAsyncInterop = yes\n[Debug]\nValidateOutput = on\nLog = verbose\n", notes); + expect(s.asyncInterop && s.validateOutput && s.verbose && notes.empty(), "boolean spellings and Log = verbose"); + s = settings("[Debug]\nValidateOutput = maybe\nLog = loud\n", notes); + expect(!s.validateOutput && !s.verbose && has_note(notes, "must be true or false, not 'maybe'") && + has_note(notes, "must be normal or verbose"), "invalid values fall back with plain notes"); + s = settings("[Kernels]\nNativeFp8 = on\n[Latency]\nFrameAge = 1\n", notes); + expect(has_note(notes, "[Kernels] NativeFp8 is not a Windows setting") && has_note(notes, "[Latency] FrameAge"), + "Linux-only settings are reported as ignored"); + s = settings("[Paths]\nCacheDir = D:\\d4r-cache\n", notes); + expect(s.cacheDir == L"D:\\d4r-cache" && notes.empty(), "CacheDir path"); + + // Pins: fixture DLL contents are ordinary text; their hashes are computed here. + std::filesystem::create_directories(dir / L"ngx"); + { std::ofstream(dir / L"nvngx_dlss.dll", std::ios::binary) << "validated dlss fixture"; } + { std::ofstream(dir / L"ngx" / L"_nvngx.dll", std::ios::binary) << "unvalidated core fixture"; } + const auto dlss = player::sha256(dir / L"nvngx_dlss.dll"), core = player::sha256(dir / L"ngx" / L"_nvngx.dll"); + const auto pins = player::parse_pins("# comment\ndlss " + dlss + " 310.9.1 validated\nngx " + core + " 32.0.15.9636 unvalidated\n" + "dlss " + std::string(64, 'A') + " 310.7.0 unvalidated\n"); + notes.clear(); + expect(error_of([&] { player::verify_dll(dir, pins, "dlss", L"nvngx_dlss.dll", "nvngx_dlss.dll", notes); }).empty() && notes.empty(), + "validated DLSS pin accepted silently"); + expect(error_of([&] { player::verify_dll(dir, pins, "ngx", L"ngx\\_nvngx.dll", "_nvngx.dll", notes); }).empty() && + has_note(notes, "not yet validated on Windows"), "unvalidated pin accepted with a note"); + { std::ofstream(dir / L"nvngx_dlss.dll", std::ios::binary) << "changed"; } + auto error = error_of([&] { player::verify_dll(dir, pins, "dlss", L"nvngx_dlss.dll", "nvngx_dlss.dll", notes); }); + expect(error.find("Wrong nvngx_dlss.dll: you have version") != std::string::npos && error.find("310.9.1 or 310.7.0") != std::string::npos, + "changed DLL rejected with you-have/you-need: " + error); + std::filesystem::remove(dir / L"nvngx_dlss.dll"); + error = error_of([&] { player::verify_dll(dir, pins, "dlss", L"nvngx_dlss.dll", "nvngx_dlss.dll", notes); }); + expect(error.find("is missing. Copy NVIDIA's nvngx_dlss.dll") != std::string::npos, "missing DLL explains where to copy it"); + expect(!error_of([] { player::parse_pins("dlss short 1 validated\n"); }).empty(), "damaged pins file rejected"); + expect(!error_of([] { player::parse_pins("other " + std::string(64, 'A') + " 1 validated\n"); }).empty(), "unknown pin kind rejected"); + + std::printf("%s PLAYER_CONFIG failures=%d\n", failures ? "FAIL" : "PASS", failures); + return failures ? 1 : 0; +}