Files
minecraft-dlss/tools/feeder/Install-DLSS5Feeder.ps1
T

2864 lines
139 KiB
PowerShell

<#
.SYNOPSIS
Installs DLSS5-Feeder, ReShade, a motion-vector provider and a neural consumer into a
game folder, end to end, from one command.
.DESCRIPTION
Point it at a game's .exe. It works out the rest:
1. Architecture (parsed from the PE header) and render API (parsed from the import
table, cross-checked against ReShade's own compatibility list, DXVK and dgVoodoo2).
2. ReShade 6.8+ with add-on support: a local dxgi.dll / opengl32.dll for Direct3D and
OpenGL, the machine-wide implicit layer plus ReShadeApps.ini entry for Vulkan.
3. The feeder itself from the latest GitHub release: the add-on matching the game's
bitness, the 64-bit host helper for 32-bit games, the shader, the Vulkan fallback
layer, and the verification script.
4. The ReShade framework headers and LumeniteFX (the recommended motion-vector provider).
5. The neural consumer -- the RenoDX DLSS 5 add-on by default -- and the two NVIDIA
NGX runtimes, in the folder where the 64-bit code runs (the game folder, or host64\).
6. ReShade.ini and ReShadePreset.ini with the provider selected and both techniques
enabled in the right order (merged into existing files, which are backed up first).
7. dgVoodoo2 for Direct3D 8/9 games, configured the way the README says.
8. The d3dcompiler_47.dll trap and the "two neural consumers" trap, defused.
Then it runs Verify-DLSS5Feeder.ps1 on the result.
Everything downloaded is cached (default: %LOCALAPPDATA%\DLSS5-Feeder\downloads), so a
second install is mostly copying. Anything you already have can be handed in with
-LocalFiles or the per-file parameters, and is then never downloaded.
Two things need administrator rights: registering ReShade's Vulkan layer and adding an
exe to ReShadeApps.ini (Vulkan games only), and a Windows Defender exclusion. Each is
run as a separate elevated step, with a UAC prompt, and only when actually needed. The
Defender exclusion is never added without asking you first: the script tries the plain
download and extraction, and only if Defender removes Deep Fried Chicken (a known false
positive -- it hooks NVIDIA's NGX runtime with Detours, which heuristics dislike) does it
explain and ask.
.PARAMETER GameExe
The game's real executable (not a launcher). A folder is accepted too, in which case the
largest non-launcher .exe in it is picked. If omitted, the script looks in its own folder
(drop it next to the game exe and run it), proposes what it finds, and asks before using
it; failing that it prompts for a path.
.PARAMETER Api
Override the render-API detection: D3D (Direct3D 10/11/12), Vulkan, OpenGL, D3D9 or
D3D8 (both via dgVoodoo2). Default: Auto.
.PARAMETER Consumer
Which neural consumer does the DLSS 5 work: RenoDX (Krish's renodx-dlss5 add-on),
DFC (Deep Fried Chicken) or OptiScaler (the OptiScaler_DLSSNR fork, installed as
winmm.dll beside ReShade and set up to run its neural pass on the feed).
RenoDX and OptiScaler are downloaded automatically. Deep Fried Chicken is NOT: its
author does not publish the files, so choosing it prints its Discord invite, asks you
to drop Deep-Fried-Chicken-*.zip in the cache folder, and waits. Pass it non-
interactively with -DfcZip.
Omitted, the script asks at the start; with -Yes and no choice given it takes RenoDX,
which is the only one it can fetch unattended.
.PARAMETER MvProvider
DLSS5_MV_PROVIDER value: 3 (LumeniteFX Kernel, default) or 4 (LumeniteFX QuantMotion).
Both come from the same LumeniteFX download. Other providers are not automated.
.PARAMETER Downloads
Cache folder for downloads. Default: %LOCALAPPDATA%\DLSS5-Feeder\downloads.
.PARAMETER LocalFiles
A folder holding any of the pieces you already have; each is used instead of a
download when found there (matched by name): DLSS5-Feeder-*.zip,
ReShade_Setup_*_Addon.exe, Deep-Fried-Chicken*.zip, nvngx_dlssnr.dll, nvngx_dlss.dll,
renodx-dlss5*.addon64, OptiScaler-DLSSNR*.zip, LumeniteFX*.zip, dgVoodoo2_*.zip,
ReShade.fxh, ReShadeUI.fxh, DrawText.fxh. The NGX runtimes and the RenoDX add-on are
also accepted as the .zip they are published in.
.PARAMETER FeederZip, DfcZip, DlssNrDll, DlssDll, RenoDxAddon, OptiScalerZip, ReShadeSetup, LumeniteZip, DgVoodooZip
Explicit path or URL for one piece, overriding both -LocalFiles and the defaults.
.PARAMETER Prerelease
Take the newest GitHub release even if it is marked pre-release.
.PARAMETER DgVoodooWatermark
Leave dgVoodoo2's corner watermark on (it is turned off by default). Useful the first
time, to prove dgVoodoo is active at all.
.PARAMETER Force
Overwrite an existing, new-enough ReShade DLL, and replace (after backing up) rather
than merge existing ReShade.ini / ReShadePreset.ini files.
.PARAMETER Yes
Answer yes to every confirmation (Defender exclusion, disabling conflicting files).
For unattended use only.
.PARAMETER NoElevate
Never show a UAC prompt. Steps that need one are printed as manual instructions.
.PARAMETER NoVerify
Skip the closing Verify-DLSS5Feeder.ps1 run.
.PARAMETER NoPause
Skip the "Press Enter to exit" pause at the end.
.EXAMPLE
.\Install-DLSS5Feeder.ps1 "G:\Games\Dusk\Dusk.exe"
.EXAMPLE
.\Install-DLSS5Feeder.ps1 "E:\SteamLibrary\steamapps\common\DOOM\DOOMx64vk.exe" -Api Vulkan
.EXAMPLE
.\Install-DLSS5Feeder.ps1 "D:\Games\Fable Anniversary\Binaries\Win32\Fable Anniversary.exe" -LocalFiles D:\Downloads
.NOTES
Windows PowerShell 5.1 compatible. Needs an internet connection for anything not cached
or supplied. If Windows refuses to run it, use:
powershell -ExecutionPolicy Bypass -File .\Install-DLSS5Feeder.ps1 "<path to game.exe>"
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string] $GameExe,
[ValidateSet('Auto', 'D3D', 'Vulkan', 'OpenGL', 'D3D9', 'D3D8')]
[string] $Api = 'Auto',
[ValidateSet('Ask', 'DFC', 'RenoDX', 'OptiScaler')]
[string] $Consumer = 'Ask',
[ValidateSet(3, 4)]
[int] $MvProvider = 3,
[string] $Downloads,
[string] $LocalFiles,
[string] $FeederZip,
[string] $DfcZip,
[string] $DlssNrDll,
[string] $DlssDll,
[string] $RenoDxAddon,
[string] $OptiScalerZip,
[string] $ReShadeSetup,
[string] $LumeniteZip,
[string] $DgVoodooZip,
[switch] $Prerelease,
[switch] $DgVoodooWatermark,
[switch] $Force,
[switch] $Yes,
[switch] $NoElevate,
[switch] $NoVerify,
[switch] $NoPause
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# ---------------------------------------------------------------------------------------
# Where things come from. Edit here when a link moves.
#
# The NGX runtimes and the RenoDX add-on used to come from Discord CDN links. Those carry
# an "ex=" expiry (hex Unix time) and Discord now stamps them roughly 24 HOURS out, so the
# installer broke about a day after every release (#75). They are now pinned GitHub release
# assets instead, which do not expire.
#
# Pinned by exact tag on purpose, not "latest": these are the builds this release was tested
# against, and a new upstream build appearing overnight must not silently change what a user
# gets. Bump the tag here when a newer one has been tried.
#
# Deep Fried Chicken is the exception and has no URL: its author does not publish the files
# publicly. Choosing it takes the guided manual route in the consumer step below.
# ---------------------------------------------------------------------------------------
$Sources = @{
FeederReleases = 'https://api.github.com/repos/jlrouzies-fr/DLSS5-Feeder/releases'
ReShadeHome = 'https://reshade.me'
ReShadeFallback = 'https://reshade.me/downloads/ReShade_Setup_6.8.0_Addon.exe'
Headers = 'https://raw.githubusercontent.com/crosire/reshade-shaders/slim/Shaders/'
CompatIni = 'https://raw.githubusercontent.com/crosire/reshade-shaders/list/Compatibility.ini'
Lumenite = 'https://codeload.github.com/umar-afzaal/LumeniteFX/zip/refs/heads/mainline'
DgVoodoo = 'https://api.github.com/repos/dege-diosg/dgVoodoo2/releases/latest'
# No public download; see Get-ChickenManually.
DlssNr = 'https://github.com/RankFTW/rhi-repo/releases/download/dlssnr-310.8.0/nvngx_dlssnr_310.8.0.zip'
Dlss = 'https://github.com/RankFTW/rhi-repo/releases/download/dlss-310.9.1/nvngx_dlss_310.9.1.zip'
RenoDxDlss5 = 'https://github.com/RankFTW/rhi-repo/releases/download/renodx-dlss5-4.70/renodx-dlss5_4.70.zip'
OptiScalerReleases = 'https://api.github.com/repos/Dagherbou/OptiScaler_DLSSNR/releases'
OptiScalerHome = 'https://github.com/Dagherbou/OptiScaler_DLSSNR/releases'
DfcDiscord = 'https://discord.gg/g2v2XGqvR'
RenoDxDiscord = 'https://discord.com/invite/renodx'
}
$ReShadeMinVersion = '6.8'
# ---------------------------------------------------------------------------------------
# Output plumbing (same look as Verify-DLSS5Feeder.ps1)
# ---------------------------------------------------------------------------------------
$script:CountDone = 0
$script:CountWarn = 0
$script:CountFail = 0
$script:Manual = New-Object System.Collections.ArrayList # steps the user must do
$script:Changed = New-Object System.Collections.ArrayList # files written, for Unblock-File
$script:UseColour = $true
try {
if ($null -eq $Host -or $null -eq $Host.UI -or $null -eq $Host.UI.RawUI) { $script:UseColour = $false }
else { $null = $Host.UI.RawUI.ForegroundColor }
}
catch { $script:UseColour = $false }
function Write-Chunk
{
param([string] $Text, [string] $Colour, [switch] $NoNewline)
try {
if ($script:UseColour -and $Colour) { Write-Host $Text -ForegroundColor $Colour -NoNewline:$NoNewline }
else { Write-Host $Text -NoNewline:$NoNewline }
}
catch {
$script:UseColour = $false
Write-Host $Text -NoNewline:$NoNewline
}
}
function Write-Banner
{
$box = 'DarkGray'
$w = 68
Write-Host ''
Write-Chunk (' ' + [char]0x2554 + ([string][char]0x2550) * $w + [char]0x2557) $box
# The logo's mark: three rounded squares stacked back-to-front, offset down and left.
$blk = [string][char]0x2588 # full block
$upr = [string][char]0x2580 # upper half block
$lwr = [string][char]0x2584 # lower half block
$rows = @(
@{ Mark = ' ' + ($lwr * 5); Shade = 'DarkGreen'; Text = ''; TextColour = '' },
@{ Mark = ' ' + ($lwr * 2) + ($blk * 5); Shade = 'Green'; Text = ' DLSS5-Feeder installer'; TextColour = 'White' },
@{ Mark = ' ' + ($blk * 6) + ($upr * 2); Shade = 'Green'; Text = ' ReShade, feeder, motion vectors, neural consumer'; TextColour = 'DarkGray' },
@{ Mark = ' ' + ($upr * 6); Shade = 'DarkGreen'; Text = ''; TextColour = '' }
)
foreach ($r in $rows) {
Write-Chunk (' ' + [char]0x2551) $box -NoNewline
Write-Chunk $r.Mark $r.Shade -NoNewline
if ($r.Text) { Write-Chunk $r.Text $r.TextColour -NoNewline }
$used = $r.Mark.Length + $r.Text.Length
if ($used -lt $w) { Write-Chunk ((' ') * ($w - $used)) $null -NoNewline }
Write-Chunk ([string][char]0x2551) $box
}
Write-Chunk (' ' + [char]0x255A + ([string][char]0x2550) * $w + [char]0x255D) $box
Write-Host ''
}
function Write-Section
{
param([string] $Title)
Write-Host ''
Write-Chunk (' ' + [char]0x2500 + [char]0x2500 + ' ') 'Green' -NoNewline
Write-Chunk $Title 'White' -NoNewline
$pad = 62 - $Title.Length
if ($pad -lt 1) { $pad = 1 }
Write-Chunk (' ' + ([string][char]0x2500) * $pad) 'Green'
}
# $Status: Done (something was installed/written), Ok (already right, nothing to do),
# Skip (not applicable), Warn, Fail, Info.
function Report
{
param(
[ValidateSet('Done', 'Ok', 'Skip', 'Warn', 'Fail', 'Info')]
[string] $Status,
[string] $Text,
[string] $Detail,
[string] $Manual
)
switch ($Status) {
'Done' { $glyph = '[DONE]'; $colour = 'Green'; $script:CountDone++ }
'Ok' { $glyph = '[ OK ]'; $colour = 'Green' }
'Skip' { $glyph = '[ -- ]'; $colour = 'DarkGray' }
'Warn' { $glyph = '[WARN]'; $colour = 'Yellow'; $script:CountWarn++ }
'Fail' { $glyph = '[FAIL]'; $colour = 'Red'; $script:CountFail++ }
'Info' { $glyph = '[ .. ]'; $colour = 'DarkGray' }
}
if ($Manual) { $null = $script:Manual.Add($Manual) }
Write-Chunk (' ' + $glyph + ' ') $colour -NoNewline
if ($Status -eq 'Skip' -or $Status -eq 'Info') { Write-Chunk $Text 'DarkGray' } else { Write-Host $Text }
if ($Detail) {
foreach ($line in ($Detail -split "`n")) {
if ($line.Trim()) { Write-Chunk (' ' + $line.Trim()) 'DarkGray' }
}
}
if ($Manual) {
$first = $true
foreach ($line in ($Manual -split "`n")) {
if (-not $line.Trim()) { continue }
if ($first) { Write-Chunk (' ' + [char]0x2192 + ' ' + $line.Trim()) 'DarkYellow'; $first = $false }
else { Write-Chunk (' ' + $line.Trim()) 'DarkYellow' }
}
}
}
function Exit-Installer
{
param([int] $Code)
if (-not $NoPause) {
Write-Host ''
try { [void](Read-Host ' Press Enter to exit') } catch { }
}
exit $Code
}
function Stop-Install
{
param([string] $Text, [string] $Detail, [string] $Manual)
Report -Status 'Fail' -Text $Text -Detail $Detail -Manual $Manual
Write-Host ''
Write-Chunk ' Stopped: the install cannot continue from here.' 'Red'
Exit-Installer 1
}
function Confirm-Step
{
param([string] $Question)
if ($Yes) { return $true }
Write-Host ''
Write-Chunk (' ' + $Question + ' [y/N] ') 'Cyan' -NoNewline
try { $a = Read-Host } catch { return $false }
return ($a -match '^(?i)y(es)?$')
}
# ---------------------------------------------------------------------------------------
# Small, defensive helpers
# ---------------------------------------------------------------------------------------
function Join-Safe
{
param([string] $Parent, [string] $Child)
try { return [IO.Path]::Combine($Parent, $Child) } catch { return $null }
}
function Test-FileHere
{
param([string] $Path)
if (-not $Path) { return $false }
try { return (Test-Path -LiteralPath $Path -PathType Leaf) } catch { return $false }
}
function Test-DirHere
{
param([string] $Path)
if (-not $Path) { return $false }
try { return (Test-Path -LiteralPath $Path -PathType Container) } catch { return $false }
}
function New-DirSafe
{
param([string] $Path)
if (-not (Test-DirHere $Path)) { $null = New-Item -ItemType Directory -Path $Path -Force }
}
function Find-FileIn
{
param([string] $Dir, [string] $Name)
if (-not (Test-DirHere $Dir)) { return $null }
$p = Join-Safe $Dir $Name
if (Test-FileHere $p) { return $p }
try {
$hit = Get-ChildItem -LiteralPath $Dir -File -Filter $Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($hit) { return $hit.FullName }
}
catch { }
return $null
}
# Every match, not just the first. The RenoDX add-on ships under versioned names as well
# ('renodx-dlss5-4.7.addon64'), and ReShade loads EVERY *.addon64 in the folder. Disabling only
# the exactly-named one left a versioned copy loaded beside Deep Fried Chicken -- the state that
# makes Chicken silently inert while every check still reads healthy (#44). The C++ side has
# matched the prefix since #1 (FindRenodxAddon, src/dlss5-feed.cpp:320).
function Find-FilesIn
{
param([string] $Dir, [string] $Name)
if (-not (Test-DirHere $Dir)) { return @() }
try {
$hits = @(Get-ChildItem -LiteralPath $Dir -File -Filter $Name -ErrorAction SilentlyContinue)
return $hits
}
catch { return @() }
}
function Find-FileUnder
{
param([string] $Dir, [string] $Name)
if (-not (Test-DirHere $Dir)) { return $null }
try {
$hit = Get-ChildItem -LiteralPath $Dir -File -Filter $Name -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($hit) { return $hit.FullName }
}
catch { }
return $null
}
function Get-FileVersionSafe
{
param([string] $Path)
if (-not (Test-FileHere $Path)) { return $null }
try {
$vi = (Get-Item -LiteralPath $Path -ErrorAction Stop).VersionInfo
if ($vi -and $vi.FileVersion) { return ($vi.FileVersion.Trim() -replace '\s*,\s*', '.') }
}
catch { }
return $null
}
function Get-ProductNameSafe
{
param([string] $Path)
if (-not (Test-FileHere $Path)) { return $null }
try {
$vi = (Get-Item -LiteralPath $Path -ErrorAction Stop).VersionInfo
$bits = @()
if ($vi.ProductName) { $bits += $vi.ProductName }
if ($vi.FileDescription) { $bits += $vi.FileDescription }
if ($vi.InternalName) { $bits += $vi.InternalName }
return ($bits -join ' | ')
}
catch { }
return $null
}
function Format-Size
{
param([long] $Bytes)
if ($Bytes -ge 1073741824) { return ('{0:N1} GB' -f ($Bytes / 1073741824)) }
if ($Bytes -ge 1048576) { return ('{0:N1} MB' -f ($Bytes / 1048576)) }
if ($Bytes -ge 1024) { return ('{0:N0} KB' -f ($Bytes / 1024)) }
return ('{0} B' -f $Bytes)
}
# Scan a binary for an ASCII marker. Capped: never slurp a 160 MB NGX runtime.
function Get-BinaryMarker
{
param([string] $Path, [string] $Pattern, [int] $MaxBytes = 33554432)
if (-not (Test-FileHere $Path)) { return $null }
try {
$fi = Get-Item -LiteralPath $Path -ErrorAction Stop
if ($fi.Length -gt $MaxBytes -or $fi.Length -eq 0) { return $null }
$bytes = [IO.File]::ReadAllBytes($Path)
$m = [regex]::Match([Text.Encoding]::ASCII.GetString($bytes), $Pattern)
if ($m.Success) {
if ($m.Groups.Count -gt 1 -and $m.Groups[1].Success) { return $m.Groups[1].Value }
return $m.Value
}
}
catch { }
return $null
}
function Get-Sha256
{
param([string] $Path)
try { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256 -ErrorAction Stop).Hash.ToUpperInvariant() } catch { return $null }
}
function Copy-Tracked
{
# Copy with the destination folder created, and remember the file for Unblock-File.
param([string] $From, [string] $To)
New-DirSafe ([IO.Path]::GetDirectoryName($To))
Copy-Item -LiteralPath $From -Destination $To -Force
$null = $script:Changed.Add($To)
}
function Write-TextTracked
{
param([string] $Path, [string] $Text)
New-DirSafe ([IO.Path]::GetDirectoryName($Path))
[IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false))
$null = $script:Changed.Add($Path)
}
function Backup-File
{
param([string] $Path)
if (-not (Test-FileHere $Path)) { return $null }
$stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
$bak = $Path + '.bak-' + $stamp
Copy-Item -LiteralPath $Path -Destination $bak -Force
return $bak
}
function Read-TextSafe
{
param([string] $Path)
if (-not (Test-FileHere $Path)) { return $null }
try {
$t = [IO.File]::ReadAllText($Path, [Text.Encoding]::UTF8)
if ($t.Length -gt 0 -and [int]$t[0] -eq 0xFEFF) { $t = $t.Substring(1) }
return $t
}
catch { return $null }
}
# ---------------------------------------------------------------------------------------
# PE parsing: architecture and the static import table (what ReShade's setup does to pick
# the API). Works on a file a running game holds open.
# ---------------------------------------------------------------------------------------
function Get-PeInfo
{
param([string] $Path)
if (-not (Test-FileHere $Path)) { return $null }
$fs = $null
$br = $null
try {
$fs = New-Object IO.FileStream($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
if ($fs.Length -lt 0x40) { return $null }
$br = New-Object IO.BinaryReader($fs)
if ($br.ReadUInt16() -ne 0x5A4D) { return $null }
$fs.Position = 0x3C
$peOff = $br.ReadInt32()
if ($peOff -le 0 -or ($peOff + 24) -ge $fs.Length) { return $null }
$fs.Position = $peOff
if ($br.ReadUInt32() -ne 0x00004550) { return $null }
$machine = $br.ReadUInt16()
$nSections = $br.ReadUInt16()
$null = $br.ReadUInt32() # timestamp
$null = $br.ReadUInt32() # symbol table
$null = $br.ReadUInt32() # symbol count
$optSize = $br.ReadUInt16()
$null = $br.ReadUInt16() # characteristics
switch ($machine) {
0x014C { $bits = 32; $arch = 'x86' }
0x8664 { $bits = 64; $arch = 'x64' }
0xAA64 { $bits = 64; $arch = 'ARM64' }
0x01C4 { $bits = 32; $arch = 'ARM' }
default { $bits = 0; $arch = ('unknown machine 0x{0:X4}' -f $machine) }
}
$imports = @()
$subsystemVersion = $null
try {
$optOff = $peOff + 24
$fs.Position = $optOff
$magic = $br.ReadUInt16()
if ($magic -eq 0x10B) { $ddOff = $optOff + 96 } elseif ($magic -eq 0x20B) { $ddOff = $optOff + 112 } else { $ddOff = 0 }
if ($ddOff -gt 0) {
$fs.Position = $optOff + 40
$subsystemVersion = '{0}.{1}' -f $br.ReadUInt16(), $br.ReadUInt16()
$fs.Position = $ddOff + 8 # data directory [1] = imports
$impRva = $br.ReadUInt32()
$impSize = $br.ReadUInt32()
$sections = @()
$secOff = $optOff + $optSize
for ($i = 0; $i -lt $nSections; $i++) {
$fs.Position = $secOff + $i * 40 + 8
$vsize = $br.ReadUInt32(); $vaddr = $br.ReadUInt32(); $rsize = $br.ReadUInt32(); $rptr = $br.ReadUInt32()
$span = [math]::Max($vsize, $rsize)
$sections += New-Object psobject -Property @{ VA = $vaddr; Span = $span; Raw = $rptr }
}
$rvaToOff = {
param([uint32] $rva)
foreach ($s in $sections) {
if ($rva -ge $s.VA -and $rva -lt ($s.VA + $s.Span)) { return [long]($rva - $s.VA + $s.Raw) }
}
return -1
}
if ($impRva -gt 0 -and $impSize -gt 0) {
$descOff = & $rvaToOff $impRva
$n = 0
while ($descOff -ge 0 -and ($descOff + 20) -lt $fs.Length -and $n -lt 512) {
$fs.Position = $descOff + 12
$nameRva = $br.ReadUInt32()
if ($nameRva -eq 0) { break }
$nameOff = & $rvaToOff $nameRva
if ($nameOff -ge 0 -and $nameOff -lt $fs.Length) {
$fs.Position = $nameOff
$sb = New-Object Text.StringBuilder
for ($k = 0; $k -lt 260; $k++) {
$b = $fs.ReadByte()
if ($b -le 0) { break }
$null = $sb.Append([char]$b)
}
if ($sb.Length -gt 0) { $imports += $sb.ToString() }
}
$descOff += 20
$n++
}
}
}
}
catch { }
return New-Object psobject -Property @{
Bits = $bits; Arch = $arch; Imports = $imports; SubsystemVersion = $subsystemVersion
}
}
catch { return $null }
finally {
if ($br) { try { $br.Close() } catch { } }
if ($fs) { try { $fs.Dispose() } catch { } }
}
}
# RVA -> file offset, given a PE's section table. Split out from the reader below so the
# lookup does not depend on PowerShell's nested-function scoping.
function Convert-RvaToOffset
{
param($Sections, [uint32] $Rva, [long] $Length)
foreach ($s in $Sections) {
if ($Rva -ge $s.V -and $Rva -lt ($s.V + $s.Span)) {
$o = [long]$s.Raw + ([long]$Rva - [long]$s.V)
if ($o -ge 0 -and $o -lt $Length) { return [long]$o }
return [long](-1)
}
}
return [long](-1)
}
# The names in a PE's export directory. $null when the file is not a readable PE; an empty
# array when it is one and exports nothing.
function Get-PeExportNames
{
param([string] $Path, [int] $Max = 8192)
if (-not (Test-FileHere $Path)) { return $null }
$fs = $null
$br = $null
try {
$fs = New-Object IO.FileStream($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
if ($fs.Length -lt 0x40) { return $null }
$br = New-Object IO.BinaryReader($fs)
if ($br.ReadUInt16() -ne 0x5A4D) { return $null } # 'MZ'
$fs.Position = 0x3C
$peOff = $br.ReadInt32()
if ($peOff -le 0 -or ($peOff + 24) -ge $fs.Length) { return $null }
$fs.Position = $peOff
if ($br.ReadUInt32() -ne 0x00004550) { return $null } # 'PE\0\0'
$null = $br.ReadUInt16() # Machine
$nSections = $br.ReadUInt16()
$null = $br.ReadUInt32() # TimeDateStamp
$null = $br.ReadUInt32() # PointerToSymbolTable
$null = $br.ReadUInt32() # NumberOfSymbols
$optSize = $br.ReadUInt16()
$null = $br.ReadUInt16() # Characteristics
$optOff = [long]$peOff + 24
if ($optSize -lt 96 -or ($optOff + $optSize) -gt $fs.Length) { return $null }
$fs.Position = $optOff
$magic = $br.ReadUInt16()
# The data directories follow the optional header's fixed part: 96 bytes for PE32,
# 112 for PE32+ (four fields widened to 64-bit).
if ($magic -eq 0x20B) { $dirOff = $optOff + 112 }
elseif ($magic -eq 0x10B) { $dirOff = $optOff + 96 }
else { return $null }
if (($dirOff + 8) -gt $fs.Length) { return $null }
$fs.Position = $dirOff
$expRva = $br.ReadUInt32() # DataDirectory[0] = exports
$null = $br.ReadUInt32() # its size
if ($expRva -eq 0) { return ,([string[]] @()) }
$secOff = $optOff + $optSize
$sections = @()
for ($i = 0; $i -lt $nSections; $i++) {
$p = $secOff + ([long]$i * 40)
if (($p + 40) -gt $fs.Length) { break }
$fs.Position = $p + 8 # past the 8-byte name
$vsize = $br.ReadUInt32()
$vaddr = $br.ReadUInt32()
$rsize = $br.ReadUInt32()
$raw = $br.ReadUInt32()
$span = if ($vsize -gt 0) { $vsize } else { $rsize }
$sections += New-Object psobject -Property @{ V = $vaddr; Span = $span; Raw = $raw }
}
if ($sections.Count -eq 0) { return $null }
$eo = Convert-RvaToOffset $sections $expRva $fs.Length
if ($eo -lt 0 -or ($eo + 40) -gt $fs.Length) { return $null }
# IMAGE_EXPORT_DIRECTORY: NumberOfFunctions +20, NumberOfNames +24,
# AddressOfFunctions +28, AddressOfNames +32.
$fs.Position = $eo + 20
$null = $br.ReadUInt32()
$nNames = $br.ReadUInt32()
$null = $br.ReadUInt32()
$namesRva = $br.ReadUInt32()
if ($nNames -eq 0 -or $namesRva -eq 0) { return ,([string[]] @()) }
if ($nNames -gt $Max) { $nNames = $Max }
$no = Convert-RvaToOffset $sections $namesRva $fs.Length
if ($no -lt 0) { return $null }
$names = New-Object 'System.Collections.Generic.List[string]'
for ($i = 0; $i -lt $nNames; $i++) {
$p = $no + ([long]$i * 4)
if (($p + 4) -gt $fs.Length) { break }
$fs.Position = $p
$so = Convert-RvaToOffset $sections ($br.ReadUInt32()) $fs.Length
if ($so -lt 0) { continue }
$fs.Position = $so
$sb = New-Object Text.StringBuilder
for ($k = 0; $k -lt 256; $k++) {
$b = $fs.ReadByte()
if ($b -le 0) { break }
$null = $sb.Append([char]$b)
}
if ($sb.Length -gt 0) { $null = $names.Add($sb.ToString()) }
}
return ,([string[]] $names.ToArray())
}
catch { return $null }
finally {
if ($br) { try { $br.Close() } catch { } }
if ($fs) { try { $fs.Dispose() } catch { } }
}
}
# Does this ReShade build support add-ons?
#
# ReShade ships two builds. They carry the same version number and the same ProductName, so
# nothing here could tell them apart -- and this script only ever asked "is it new enough?",
# which meant a plain build already on the machine was kept and reported as fine while the
# add-on was never loaded (issue #53). The setup this script DOWNLOADS is always the right
# one; the hole was in what it accepted as already present.
#
# The exports settle it, and not by inference: ReShade's own add-on API finds the ReShade
# module in a process by testing GetProcAddress for exactly "ReShadeRegisterAddon" and
# "ReShadeUnregisterAddon" (reshade.hpp). A build that does not export those two cannot load
# an add-on, because that is the mechanism by which add-ons find it.
#
# $null means the file could not be read -- treat that as "assume it is fine", never as a fault.
function Test-ReShadeHasAddons
{
param([string] $Path)
$names = Get-PeExportNames $Path
if ($null -eq $names) { return $null }
return [bool](($names -contains 'ReShadeRegisterAddon') -and ($names -contains 'ReShadeUnregisterAddon'))
}
# Counts occurrences of the graphics-API DLL names (ASCII and UTF-16) inside a binary.
# Only used when the import table says nothing. Capped at 256 MB.
function Get-ApiStringHints
{
param([string] $Path)
$hits = @{}
try {
$fi = Get-Item -LiteralPath $Path -ErrorAction Stop
if ($fi.Length -gt 268435456 -or $fi.Length -eq 0) { return $hits }
$bytes = [IO.File]::ReadAllBytes($Path)
$ascii = [Text.Encoding]::ASCII.GetString($bytes)
$wide = [Text.Encoding]::Unicode.GetString($bytes)
# d3d10_1.dll earns its place: a Direct3D 10 game imports THAT, not d3d10.dll, and
# without it here a title like Devil May Cry 4 SE -- which also imports d3d9.dll for
# its D3DPERF markers -- was detected as Direct3D 9 and sent down the dgVoodoo2 route,
# which implements DirectX 1-9 and cannot touch Direct3D 10.
foreach ($n in @('vulkan-1.dll', 'dxgi.dll', 'd3d12.dll', 'd3d11.dll', 'd3d10_1.dll', 'd3d10.dll', 'd3d10core.dll', 'd3d9.dll', 'd3d8.dll', 'opengl32.dll')) {
$re = '(?i)(?<![\w.])' + [regex]::Escape($n)
$c = [regex]::Matches($ascii, $re).Count + [regex]::Matches($wide, $re).Count
if ($c -gt 0) { $hits[$n] = $c }
}
}
catch { }
return $hits
}
# ---------------------------------------------------------------------------------------
# Downloads
# ---------------------------------------------------------------------------------------
try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -bor 0x3000 } catch { }
function New-WebClient
{
$wc = New-Object Net.WebClient
$wc.Headers.Add('User-Agent', 'DLSS5-Feeder-Installer (PowerShell)')
return $wc
}
# The DLL names in a PE's import directory, lowercased. $null when the file is not a readable PE;
# an empty array when it imports nothing. Used to pick the name OptiScaler is renamed to: the
# process has to import that name at start, or OptiScaler is never in it.
function Get-PeImportNames
{
param([string] $Path, [int] $Max = 512)
if (-not (Test-FileHere $Path)) { return $null }
$fs = $null
$br = $null
try {
$fs = New-Object IO.FileStream($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
if ($fs.Length -lt 0x40) { return $null }
$br = New-Object IO.BinaryReader($fs)
if ($br.ReadUInt16() -ne 0x5A4D) { return $null } # 'MZ'
$fs.Position = 0x3C
$peOff = $br.ReadInt32()
if ($peOff -le 0 -or ($peOff + 24) -ge $fs.Length) { return $null }
$fs.Position = $peOff
if ($br.ReadUInt32() -ne 0x00004550) { return $null } # 'PE\0\0'
$null = $br.ReadUInt16() # Machine
$nSections = $br.ReadUInt16()
$null = $br.ReadUInt32() # TimeDateStamp
$null = $br.ReadUInt32() # PointerToSymbolTable
$null = $br.ReadUInt32() # NumberOfSymbols
$optSize = $br.ReadUInt16()
$null = $br.ReadUInt16() # Characteristics
$optOff = [long]$peOff + 24
if ($optSize -lt 96 -or ($optOff + $optSize) -gt $fs.Length) { return $null }
$fs.Position = $optOff
$magic = $br.ReadUInt16()
if ($magic -eq 0x20B) { $dirOff = $optOff + 112 }
elseif ($magic -eq 0x10B) { $dirOff = $optOff + 96 }
else { return $null }
if (($dirOff + 16) -gt $fs.Length) { return $null }
$fs.Position = $dirOff + 8 # DataDirectory[1] = imports
$impRva = $br.ReadUInt32()
if ($impRva -eq 0) { return ,([string[]] @()) }
$secOff = $optOff + $optSize
$sections = @()
for ($i = 0; $i -lt $nSections; $i++) {
$p = $secOff + ([long]$i * 40)
if (($p + 40) -gt $fs.Length) { break }
$fs.Position = $p + 8
$vsize = $br.ReadUInt32()
$vaddr = $br.ReadUInt32()
$rsize = $br.ReadUInt32()
$raw = $br.ReadUInt32()
$span = if ($vsize -gt 0) { $vsize } else { $rsize }
$sections += New-Object psobject -Property @{ V = $vaddr; Span = $span; Raw = $raw }
}
if ($sections.Count -eq 0) { return $null }
$io = Convert-RvaToOffset $sections $impRva $fs.Length
if ($io -lt 0) { return $null }
$names = New-Object 'System.Collections.Generic.List[string]'
for ($i = 0; $i -lt $Max; $i++) {
$p = $io + ([long]$i * 20) # IMAGE_IMPORT_DESCRIPTOR
if (($p + 20) -gt $fs.Length) { break }
$fs.Position = $p + 12 # Name RVA
$nameRva = $br.ReadUInt32()
if ($nameRva -eq 0) { break }
$no = Convert-RvaToOffset $sections $nameRva $fs.Length
if ($no -lt 0) { continue }
$fs.Position = $no
$sb = New-Object Text.StringBuilder
for ($k = 0; $k -lt 256; $k++) {
$b = $fs.ReadByte()
if ($b -le 0) { break }
$null = $sb.Append([char]$b)
}
if ($sb.Length -gt 0) { $null = $names.Add($sb.ToString().ToLowerInvariant()) }
}
return ,([string[]] $names.ToArray())
}
catch { return $null }
finally {
if ($br) { try { $br.Close() } catch { } }
if ($fs) { try { $fs.Dispose() } catch { } }
}
}
function Get-WebString
{
param([string] $Url)
$wc = New-WebClient
try { return $wc.DownloadString($Url) } catch { return $null } finally { $wc.Dispose() }
}
# Decodes the "ex=" expiry on a Discord CDN link. Returns a DateTime or $null.
function Get-DiscordExpiry
{
param([string] $Url)
$m = [regex]::Match($Url, '[?&]ex=([0-9a-fA-F]{6,10})')
if (-not $m.Success) { return $null }
try {
$secs = [Convert]::ToInt64($m.Groups[1].Value, 16)
return ([DateTimeOffset]::FromUnixTimeSeconds($secs)).LocalDateTime
}
catch { return $null }
}
# Downloads $Url to $Dest unless $Dest already exists. Returns $true on success. Never throws.
function Get-Download
{
param([string] $Url, [string] $Dest, [string] $Label)
if (Test-FileHere $Dest) {
Report -Status 'Ok' -Text ($Label + ': cached') -Detail $Dest
return $true
}
if ($Url -match '(?i)^https?://') {
$exp = Get-DiscordExpiry $Url
if ($exp -and $exp -lt (Get-Date)) {
Report -Status 'Fail' -Text ($Label + ': the download link expired on ' + $exp.ToString('yyyy-MM-dd HH:mm') + '.') `
-Detail 'Discord CDN links carry an expiry. Get a fresh link from the Discord server the README points at, and pass it (or the downloaded file) with the matching parameter.'
return $false
}
}
elseif (Test-FileHere $Url) {
# A local path was given where a URL was expected: just copy it.
try { Copy-Tracked -From $Url -To $Dest; Report -Status 'Done' -Text ($Label + ': copied from ' + $Url); return $true }
catch { Report -Status 'Fail' -Text ($Label + ': cannot copy ' + $Url) -Detail $_.Exception.Message; return $false }
}
else {
Report -Status 'Fail' -Text ($Label + ': not a URL and not an existing file: ' + $Url)
return $false
}
New-DirSafe ([IO.Path]::GetDirectoryName($Dest))
$tmp = $Dest + '.part'
Write-Chunk (' [ .. ] ' + $Label + ': downloading ...') 'DarkGray'
$wc = New-WebClient
try {
$wc.DownloadFile($Url, $tmp)
if (-not (Test-FileHere $tmp)) { throw 'the download produced no file' }
$len = (Get-Item -LiteralPath $tmp).Length
if ($len -lt 64) { throw 'the download is empty' }
Move-Item -LiteralPath $tmp -Destination $Dest -Force
$null = $script:Changed.Add($Dest)
Report -Status 'Done' -Text ($Label + ': downloaded (' + (Format-Size $len) + ')') -Detail $Dest
return $true
}
catch {
$msg = $_.Exception.Message
if ($_.Exception.InnerException) { $msg = $_.Exception.InnerException.Message }
try { if (Test-FileHere $tmp) { Remove-Item -LiteralPath $tmp -Force } } catch { }
$d = 'From: ' + $Url + "`n" + $msg
if ($msg -match '(?i)virus|potentially unwanted|0x800700E1') { $d += "`nWindows Defender blocked the download itself." }
Report -Status 'Fail' -Text ($Label + ': download failed.') -Detail $d
return $false
}
finally { $wc.Dispose() }
}
# Resolve where a piece comes from: explicit parameter (path or URL), -LocalFiles match,
# then the default URL. Returns a path in the cache, or $null.
function Resolve-Piece
{
param([string] $Label, [string] $Explicit, [string] $LocalPattern, [string] $DefaultUrl, [string] $CacheName)
$dest = Join-Safe $script:Cache $CacheName
if ($Explicit) {
if (Test-FileHere $Explicit) { return $Explicit }
if (Get-Download -Url $Explicit -Dest $dest -Label $Label) { return $dest }
return $null
}
if ($LocalFiles -and $LocalPattern) {
try {
$hit = Get-ChildItem -LiteralPath $LocalFiles -File -Filter $LocalPattern -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($hit) { Report -Status 'Ok' -Text ($Label + ': using ' + $hit.FullName); return $hit.FullName }
}
catch { }
}
if (-not $DefaultUrl) { return $null }
if (Get-Download -Url $DefaultUrl -Dest $dest -Label $Label) { return $dest }
return $null
}
# ---------------------------------------------------------------------------------------
# Zip helpers. ReShade's setup exe is a zip appended to an executable, which .NET refuses
# to open directly, so Open-Zip finds the archive start the way the setup itself does.
# ---------------------------------------------------------------------------------------
# A pinned GitHub release asset that wraps exactly one file (the NGX runtimes and the
# RenoDX add-on all ship this way). Downloads it, extracts that one entry into the cache
# and returns the extracted path -- so every caller downstream still receives a plain
# path to a .dll / .addon64 and nothing else had to learn about archives.
#
# An override or a -LocalFiles hit that is already the bare file is taken as-is: people
# who have been collecting these by hand have the loose file, not the zip.
# Deep Fried Chicken has no public download and no stable URL: its author does not want the
# files mirrored. So the installer never fetches it -- it looks in every place a person who
# just downloaded it would plausibly have put it, and otherwise asks.
function Find-ChickenZip
{
param([string] $Explicit)
if ($Explicit) {
if (Test-FileHere $Explicit) { Report -Status 'Ok' -Text ('Deep Fried Chicken: using ' + $Explicit); return $Explicit }
Report -Status 'Warn' -Text ('Deep Fried Chicken: -DfcZip points at nothing: ' + $Explicit)
}
$where = New-Object System.Collections.ArrayList
if ($LocalFiles) { $null = $where.Add($LocalFiles) }
$null = $where.Add($script:Cache)
if ($gameDir) { $null = $where.Add($gameDir) }
$null = $where.Add($PSScriptRoot)
try { $null = $where.Add((Join-Safe $env:USERPROFILE 'Downloads')) } catch { }
foreach ($d in $where) {
if (-not $d -or -not (Test-DirHere $d)) { continue }
try {
$hit = Get-ChildItem -LiteralPath $d -File -Filter 'Deep-Fried-Chicken*.zip' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($hit) { Report -Status 'Ok' -Text ('Deep Fried Chicken: found ' + $hit.FullName); return $hit.FullName }
}
catch { }
}
return $null
}
# The guided hand-fetch. Prints where to go and where to put it, then waits on Enter and
# looks again, rather than failing and making the user start the installer over.
function Get-ChickenManually
{
$target = if ($script:Cache) { $script:Cache } else { $PSScriptRoot }
for ($try = 1; $try -le 3; $try++) {
Write-Host ''
Write-Chunk ' Deep Fried Chicken has to be fetched by hand.' 'Yellow'
Write-Chunk ' Its author does not publish the files, so nothing can download it for you.' 'DarkGray'
Write-Host ''
Write-Chunk ' 1. Open its Discord: ' 'Gray' -NoNewline
Write-Chunk $Sources.DfcDiscord 'Cyan'
Write-Chunk ' 2. Download the latest Deep-Fried-Chicken-*.zip' 'Gray'
Write-Chunk ' 3. Put that file in this folder:' 'Gray'
Write-Chunk (' ' + $target) 'White'
Write-Chunk ' (the game folder or your Downloads folder work too)' 'DarkGray'
Write-Host ''
Write-Chunk ' Press Enter when the file is there, or type q to stop. ' 'Cyan' -NoNewline
try { $a = Read-Host } catch { $a = 'q' }
# No "switch to RenoDX" here on purpose: the consumer decides what has already been
# downloaded and what must never be installed beside it, and changing it this late
# would leave half of those decisions made for the other one. Stopping and re-running
# with option 1 is the honest way out.
if ($a.Trim().ToLowerInvariant() -eq 'q') {
Stop-Install 'Deep Fried Chicken was not provided.' `
'Nothing was installed for the neural consumer.' `
'Re-run and choose 1 (RenoDX DLSS 5), which this installer can download for you.'
}
$found = Find-ChickenZip -Explicit ''
if ($found) { return $found }
Write-Chunk (' Still no Deep-Fried-Chicken-*.zip in ' + $target + ' (or the other places checked).') 'Yellow'
}
return $null
}
function Resolve-PieceZipped
{
param([string] $Label, [string] $Explicit, [string] $LocalPattern, [string] $DefaultUrl,
[string] $CacheName, [string] $InnerName)
$inner = Join-Safe $script:Cache $InnerName
if (-not $Explicit -and (Test-FileHere $inner)) {
Report -Status 'Ok' -Text ($Label + ': using the copy already in the cache')
return $inner
}
$got = Resolve-Piece -Label $Label -Explicit $Explicit -LocalPattern $LocalPattern `
-DefaultUrl $DefaultUrl -CacheName $CacheName
if (-not $got) { return $null }
if ([IO.Path]::GetExtension($got) -ne '.zip') { return $got }
try {
$z = Open-Zip $got
try {
$e = Find-ZipEntry $z ('(^|/)' + [regex]::Escape($InnerName) + '$')
if (-not $e) {
Report -Status 'Fail' -Text ($Label + ': ' + $InnerName + ' is not inside ' + $got)
return $null
}
Expand-ZipEntry $e $inner
}
finally { $z.Dispose() }
}
catch {
Report -Status 'Fail' -Text ($Label + ': could not extract ' + $InnerName) -Detail $_.Exception.Message
return $null
}
return $inner
}
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
function Open-Zip
{
param([string] $Path)
$bytes = [IO.File]::ReadAllBytes($Path)
$start = -1
if ($bytes.Length -ge 4 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B -and $bytes[2] -eq 3 -and $bytes[3] -eq 4) {
$start = 0
}
else {
for ($i = 0; $i -le $bytes.Length - 30; $i += 512) {
if ($bytes[$i] -eq 0x50 -and $bytes[$i + 1] -eq 0x4B -and $bytes[$i + 2] -eq 3 -and $bytes[$i + 3] -eq 4) {
$nonZero = $false
for ($k = 4; $k -lt 30; $k++) { if ($bytes[$i + $k] -ne 0) { $nonZero = $true; break } }
if ($nonZero) { $start = $i; break }
}
}
}
if ($start -lt 0) { throw ('no zip archive found in ' + $Path) }
$ms = New-Object IO.MemoryStream(, $bytes)
$ms.Position = $start
if ($start -gt 0) {
$rest = New-Object IO.MemoryStream
$ms.CopyTo($rest)
$ms.Dispose()
$ms = $rest
$ms.Position = 0
}
return New-Object IO.Compression.ZipArchive($ms, [IO.Compression.ZipArchiveMode]::Read)
}
function Find-ZipEntry
{
param($Zip, [string] $Pattern) # regex on the full entry name
foreach ($e in $Zip.Entries) { if (($e.FullName -replace '\\', '/') -match $Pattern) { return $e } }
return $null
}
function Expand-ZipEntry
{
param($Entry, [string] $To)
New-DirSafe ([IO.Path]::GetDirectoryName($To))
$in = $Entry.Open()
try {
$out = New-Object IO.FileStream($To, [IO.FileMode]::Create, [IO.FileAccess]::Write)
try { $in.CopyTo($out) } finally { $out.Dispose() }
}
finally { $in.Dispose() }
$null = $script:Changed.Add($To)
}
function Read-ZipText
{
param($Entry)
$in = $Entry.Open()
try { $sr = New-Object IO.StreamReader($in); try { return $sr.ReadToEnd() } finally { $sr.Dispose() } }
finally { $in.Dispose() }
}
# ---------------------------------------------------------------------------------------
# Ini editing that keeps everything else in the file intact.
# ---------------------------------------------------------------------------------------
function Set-IniKey
{
# $Section '' means the key lives before the first [section] (ReShadePreset's Techniques=).
param([string] $Text, [string] $Section, [string] $Key, [string] $Value)
if ($null -eq $Text) { $Text = '' }
$nl = "`r`n"
$lines = New-Object System.Collections.ArrayList
foreach ($l in ($Text -split "`r?`n")) { $null = $lines.Add($l) }
# Drop one trailing empty line produced by a final newline, so we can append cleanly.
if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) }
$cur = ''
$secStart = -1
$secEnd = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
$t = $lines[$i].Trim()
if ($t -match '^\[(.+)\]$') {
if ($cur -ieq $Section -and $secStart -ge 0 -and $secEnd -lt 0) { $secEnd = $i }
$cur = $Matches[1]
if ($cur -ieq $Section) { $secStart = $i + 1 }
continue
}
if ($cur -ieq $Section -and $t -match ('(?i)^' + [regex]::Escape($Key) + '\s*=')) {
$lines[$i] = $Key + '=' + $Value
return (($lines -join $nl) + $nl)
}
}
if ($Section -eq '' -and $secStart -lt 0) { $secStart = 0; $secEnd = 0; for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i].Trim() -match '^\[') { break }; $secEnd = $i + 1 } }
if ($secStart -ge 0) {
if ($secEnd -lt 0) { $secEnd = $lines.Count }
# Insert after the last non-blank line of the section.
$at = $secEnd
while ($at -gt $secStart -and $lines[$at - 1].Trim() -eq '') { $at-- }
$lines.Insert($at, ($Key + '=' + $Value))
}
else {
if ($lines.Count -gt 0) { $null = $lines.Add('') }
$null = $lines.Add('[' + $Section + ']')
$null = $lines.Add($Key + '=' + $Value)
}
return (($lines -join $nl) + $nl)
}
function Get-IniKey
{
param([string] $Text, [string] $Section, [string] $Key)
if ($null -eq $Text) { return $null }
$cur = ''
foreach ($l in ($Text -split "`r?`n")) {
$t = $l.Trim()
if ($t -match '^\[(.+)\]$') { $cur = $Matches[1]; continue }
if ($cur -ieq $Section -and $t -match ('(?i)^' + [regex]::Escape($Key) + '\s*=\s*(.*)$')) { return $Matches[1] }
}
return $null
}
# dgVoodoo.conf keeps its values column-aligned; replace only the value part.
function Set-ConfValue
{
param([string] $Text, [string] $Section, [string] $Key, [string] $Value)
$lines = $Text -split "`r?`n"
$cur = ''
$done = $false
for ($i = 0; $i -lt $lines.Count; $i++) {
$t = $lines[$i].Trim()
if ($t -match '^\[(.+)\]$') { $cur = $Matches[1]; continue }
if (-not $done -and $cur -ieq $Section -and $lines[$i] -match ('(?i)^(\s*' + [regex]::Escape($Key) + '\s*=\s*)')) {
$lines[$i] = $Matches[1] + $Value
$done = $true
}
}
if (-not $done) { return (Set-IniKey -Text $Text -Section $Section -Key $Key -Value $Value) }
return ($lines -join "`r`n")
}
# ---------------------------------------------------------------------------------------
# Elevation: one temporary script, one UAC prompt, for a named list of tasks.
# ---------------------------------------------------------------------------------------
function Test-Elevated
{
try {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
catch { return $false }
}
# Returns 'ok', 'declined' or 'failed'. $Script is PowerShell text; it runs with
# $ErrorActionPreference = 'Stop' and exits 0 on success.
function Invoke-Elevated
{
param([string] $Script, [string] $What)
$body = "`$ErrorActionPreference = 'Stop'`r`ntry {`r`n" + $Script + "`r`n exit 0`r`n}`r`ncatch {`r`n Write-Host `$_.Exception.Message`r`n Start-Sleep -Seconds 4`r`n exit 1`r`n}`r`n"
$tmp = Join-Safe $script:Cache ('elevated-' + (Get-Date).ToString('yyyyMMdd-HHmmss-fff') + '.ps1')
[IO.File]::WriteAllText($tmp, $body, (New-Object Text.UTF8Encoding $true))
if (Test-Elevated) {
try { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $tmp; $code = $LASTEXITCODE }
catch { $code = 1 }
}
else {
Write-Chunk (' [ .. ] Asking for administrator rights: ' + $What) 'Cyan'
try {
$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru `
-ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"' + $tmp + '"'))
$code = $p.ExitCode
}
catch {
try { Remove-Item -LiteralPath $tmp -Force } catch { }
return 'declined'
}
}
try { Remove-Item -LiteralPath $tmp -Force } catch { }
if ($code -eq 0) { return 'ok' }
return 'failed'
}
function ConvertTo-PsLiteral
{
param([string] $S)
return ("'" + ($S -replace "'", "''") + "'")
}
# ---------------------------------------------------------------------------------------
# Windows Defender: detect a removal, and ask before excluding.
# ---------------------------------------------------------------------------------------
function Get-DefenderDetection
{
# A detection in the last 15 minutes naming $Path (or the Chicken zip) -- or $null.
param([string] $Path)
try {
$since = (Get-Date).AddMinutes(-15)
$all = Get-MpThreatDetection -ErrorAction Stop
foreach ($d in $all) {
if ($d.InitialDetectionTime -lt $since) { continue }
$res = @($d.Resources) -join ' '
if ($res -match [regex]::Escape($Path) -or $res -match '(?i)deep-fried-chicken') { return $d }
}
}
catch { }
return $null
}
$script:DefenderExcluded = @{}
# Returns $true when the exclusion is in place (added now, or already added this run).
function Request-DefenderExclusion
{
param([string[]] $Paths, [string] $Because)
$todo = @()
foreach ($p in $Paths) { if (-not $script:DefenderExcluded.ContainsKey($p.ToLowerInvariant())) { $todo += $p } }
if ($todo.Count -eq 0) { return $true }
Write-Host ''
Write-Chunk ' Windows Defender removed a file this install needs.' 'Yellow'
Write-Chunk (' ' + $Because) 'Yellow'
Write-Chunk ' This is a known false positive: a neural consumer interposes on NVIDIA''s NGX runtime,' 'DarkGray'
Write-Chunk ' which antivirus heuristics dislike. The fix is a Defender exclusion' 'DarkGray'
Write-Chunk ' (Settings > Virus & threat protection > Exclusions) for:' 'DarkGray'
foreach ($p in $todo) { Write-Chunk (' ' + $p) 'White' }
Write-Chunk ' Adding it needs administrator rights (a UAC prompt). Nothing else is changed.' 'DarkGray'
if ($NoElevate) {
$cmd = ($todo | ForEach-Object { 'Add-MpPreference -ExclusionPath ' + (ConvertTo-PsLiteral $_) }) -join '; '
Report -Status 'Fail' -Text 'Defender exclusion not added (-NoElevate).' `
-Manual ('From an elevated PowerShell: ' + $cmd + "`nThen re-run this installer.")
return $false
}
if (-not (Confirm-Step 'Add the Defender exclusion now?')) {
$cmd = ($todo | ForEach-Object { 'Add-MpPreference -ExclusionPath ' + (ConvertTo-PsLiteral $_) }) -join '; '
Report -Status 'Fail' -Text 'Defender exclusion declined.' `
-Manual ('Add it yourself (elevated PowerShell): ' + $cmd + "`nThen re-run this installer.")
return $false
}
$lines = ($todo | ForEach-Object { ' Add-MpPreference -ExclusionPath ' + (ConvertTo-PsLiteral $_) }) -join "`r`n"
$r = Invoke-Elevated -Script $lines -What 'add a Windows Defender exclusion'
if ($r -eq 'ok') {
foreach ($p in $todo) { $script:DefenderExcluded[$p.ToLowerInvariant()] = $true }
Report -Status 'Done' -Text ('Defender exclusion added for: ' + ($todo -join ', '))
# Defender may also have quarantined the file already; restoring is not automated.
return $true
}
if ($r -eq 'declined') { Report -Status 'Fail' -Text 'The UAC prompt was declined; no exclusion added.'; return $false }
Report -Status 'Fail' -Text 'Adding the Defender exclusion failed.' -Detail 'Tamper Protection or a policy may forbid it. Add it by hand in Windows Security > Exclusions.'
return $false
}
# ---------------------------------------------------------------------------------------
# 0. Resolve the target
# ---------------------------------------------------------------------------------------
Write-Banner
$exeSkip = '(?i)(launcher|unins|setup|crash|redist|vcredist|dxsetup|dxwebsetup|dgvoodoocpl|touchup|prereq|activation|helper|updater|report|dlss5-feed-host)'
# No argument: the script was most likely dropped into the game folder. Look next to it,
# propose what is there, and ask before using it.
if (-not $GameExe -or -not $GameExe.Trim()) {
$here = $null
try { $here = $PSScriptRoot } catch { }
if (-not $here) { try { $here = Split-Path -Parent $MyInvocation.MyCommand.Path } catch { } }
$found = @()
if ($here -and (Test-DirHere $here)) {
$found = @(Get-ChildItem -LiteralPath $here -File -Filter '*.exe' -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch $exeSkip } | Sort-Object Length -Descending)
}
if ($found.Count -gt 0) {
Write-Chunk (' Found next to this script (' + $here + '):') 'White'
$i = 1
foreach ($f in $found) {
$peF = Get-PeInfo $f.FullName
$arch = ''
if ($peF -and $peF.Bits -gt 0) { $arch = ', ' + $peF.Bits + '-bit' }
Write-Chunk (' ' + $i + '. ' + $f.Name + ' (' + (Format-Size $f.Length) + $arch + ')') $(if ($i -eq 1) { 'Gray' } else { 'DarkGray' })
$i++
}
if ($Yes) { $GameExe = $found[0].FullName }
else {
Write-Chunk (' Use ' + $found[0].Name + ' as the game executable? [Y/n, or a number from the list] ') 'Cyan' -NoNewline
try { $a = Read-Host } catch { $a = '' }
$a = $a.Trim()
if ($a -eq '' -or $a -match '^(?i)y(es)?$') { $GameExe = $found[0].FullName }
elseif ($a -match '^\d+$' -and [int]$a -ge 1 -and [int]$a -le $found.Count) { $GameExe = $found[[int]$a - 1].FullName }
else { $GameExe = '' }
}
}
if (-not $GameExe) {
Write-Chunk ' Path to the game''s .exe (the real one, not a launcher): ' 'Cyan' -NoNewline
try { $GameExe = Read-Host } catch { $GameExe = '' }
$GameExe = $GameExe.Trim().Trim('"')
}
}
if (-not $GameExe) { Stop-Install -Text 'No game executable given.' -Manual 'Drop this script into the game folder and run it, or pass the path of the game''s .exe.' }
try { $resolved = (Resolve-Path -LiteralPath $GameExe -ErrorAction Stop).ProviderPath } catch { $resolved = $null }
if (-not $resolved) { Stop-Install -Text ('Path not found: ' + $GameExe) }
$gameDir = $null
$exePath = $null
$exeGuessed = $false
if (Test-DirHere $resolved) {
$gameDir = $resolved
$exes = @(Get-ChildItem -LiteralPath $gameDir -File -Filter '*.exe' -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch $exeSkip } | Sort-Object Length -Descending)
if ($exes.Count -eq 0) { Stop-Install -Text ('No .exe found in ' + $gameDir) -Manual 'Pass the path of the game''s executable.' }
$exePath = $exes[0].FullName
$exeGuessed = $true
}
elseif ($resolved -match '(?i)\.exe$') {
$exePath = $resolved
$gameDir = [IO.Path]::GetDirectoryName($resolved)
}
else {
Stop-Install -Text ('Not an .exe and not a folder: ' + $resolved)
}
# Unreal Engine bootstrap: a tiny <Game>.exe next to <Game>\Binaries\Win64\<Game>-Win64-Shipping.exe.
try {
$leafNoExt = [IO.Path]::GetFileNameWithoutExtension($exePath)
foreach ($sub in @('Win64', 'WinGDK', 'Win32')) {
$cand = Join-Safe $gameDir ($leafNoExt + '\Binaries\' + $sub + '\' + $leafNoExt + '-' + $sub + '-Shipping.exe')
if (Test-FileHere $cand) {
Report -Status 'Info' -Text ('Unreal Engine bootstrap detected; using the real executable instead.') -Detail $cand
$exePath = $cand
$gameDir = [IO.Path]::GetDirectoryName($cand)
$exeGuessed = $false
break
}
}
}
catch { }
if ($gameDir -and $gameDir.StartsWith($env:WINDIR, [StringComparison]::OrdinalIgnoreCase)) {
Stop-Install -Text 'Refusing to install into the Windows directory.'
}
Write-Chunk ' Game ' 'DarkGray' -NoNewline
Write-Host $exePath
if ($exeGuessed) { Write-Chunk ' (picked as the largest non-launcher .exe in that folder; pass the .exe itself to override)' 'DarkGray' }
# Cache folder
if (-not $Downloads) { $Downloads = Join-Safe $env:LOCALAPPDATA 'DLSS5-Feeder\downloads' }
$script:Cache = $Downloads
New-DirSafe $script:Cache
Write-Chunk ' Cache ' 'DarkGray' -NoNewline
Write-Host $script:Cache
# Which neural consumer? Asked here, before anything is downloaded, because the answer
# decides what gets fetched and what must never be installed beside it.
# RenoDX is first and is the default: it is the only one of the three this installer can
# actually fetch. Deep Fried Chicken is not published anywhere public -- its author does not
# want the files mirrored -- so choosing it means a guided manual step rather than a download,
# and making it the default meant every unattended run stopped to ask a human (#75).
if ($Consumer -eq 'Ask') {
if ($Yes) {
$Consumer = 'RenoDX'
}
else {
Write-Host ''
Write-Chunk ' Neural consumer -- the add-on that turns the feed into neural rendering.' 'White'
Write-Chunk ' 1. RenoDX DLSS 5 ' 'Gray' -NoNewline
Write-Chunk 'recommended; Krish''s renodx-dlss5 add-on, downloaded for you' 'DarkGray'
Write-Chunk ' 2. Deep Fried Chicken ' 'Gray' -NoNewline
Write-Chunk 'negotiates with the feeder over its own interop ABI -- you must fetch it by hand from its Discord' 'DarkGray'
Write-Chunk ' 3. OptiScaler DLSS-NR ' 'Gray' -NoNewline
Write-Chunk 'the OptiScaler_DLSSNR fork: it takes the feed''s DLSS call, upscales, then runs the neural pass (menu on Insert)' 'DarkGray'
Write-Chunk ' Exactly one of them may be installed: each goes inert, or misbehaves, beside the others.' 'DarkGray'
Write-Chunk ' Which one? [1/2/3, Enter for 1] ' 'Cyan' -NoNewline
try { $a = Read-Host } catch { $a = '' }
switch ($a.Trim()) {
'2' { $Consumer = 'DFC' }
'dfc' { $Consumer = 'DFC' }
'chicken' { $Consumer = 'DFC' }
'3' { $Consumer = 'OptiScaler' }
'optiscaler' { $Consumer = 'OptiScaler' }
default { $Consumer = 'RenoDX' }
}
}
}
$consumerLabel = switch ($Consumer) { 'DFC' { 'Deep Fried Chicken' } 'RenoDX' { 'RenoDX DLSS 5' } default { 'OptiScaler DLSS-NR' } }
Write-Chunk ' Neural ' 'DarkGray' -NoNewline
Write-Host $consumerLabel
if ($LocalFiles) {
if (-not (Test-DirHere $LocalFiles)) { Stop-Install -Text ('-LocalFiles folder not found: ' + $LocalFiles) }
Write-Chunk ' Local ' 'DarkGray' -NoNewline
Write-Host $LocalFiles
}
# Running game?
try {
$running = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { try { $_.MainModule.FileName -ieq $exePath } catch { $false } })
if ($running.Count -gt 0) { Stop-Install -Text 'The game is running. Close it first; its files are locked.' }
}
catch { }
# ---------------------------------------------------------------------------------------
# 1. Architecture and render API
# ---------------------------------------------------------------------------------------
Write-Section 'Game executable and render API'
$pe = Get-PeInfo $exePath
if ($null -eq $pe) { Stop-Install -Text 'The executable''s PE header is unreadable.' }
if ($pe.Bits -eq 0) { Stop-Install -Text ('Unsupported architecture: ' + $pe.Arch) }
$gameBits = $pe.Bits
Report -Status 'Ok' -Text ('Executable: ' + [IO.Path]::GetFileName($exePath) + ' (' + $pe.Arch + ', ' + $gameBits + '-bit)')
# ReShade's compatibility list: known per-game overrides and bans.
$compat = $null
$compatText = Get-WebString $Sources.CompatIni
$exeLeaf = [IO.Path]::GetFileName($exePath)
if ($compatText) {
$banned = Get-IniKey -Text $compatText -Section $exeLeaf -Key 'Banned'
$target = Get-IniKey -Text $compatText -Section $exeLeaf -Key 'InstallTarget'
$rapi = Get-IniKey -Text $compatText -Section $exeLeaf -Key 'RenderApi'
$is64 = Get-IniKey -Text $compatText -Section $exeLeaf -Key 'Is64Bit'
if ($banned -eq '1') {
Stop-Install -Text 'ReShade''s compatibility list says this game bans or blocks ReShade.' -Detail 'Installing anyway risks an account ban. Not continuing.'
}
if ($target) {
Stop-Install -Text ('ReShade''s compatibility list says this game needs ReShade installed into a sub-folder (' + $target + ').') `
-Detail 'This installer does not handle that layout yet. Run ReShade''s own installer for this game, then deploy the feeder by hand (see the README).'
}
if ($rapi) { $compat = $rapi; Report -Status 'Info' -Text ('ReShade''s compatibility list knows this exe: RenderApi=' + $rapi) }
if ($is64 -eq '1' -and $gameBits -ne 64) { $gameBits = 64; Report -Status 'Info' -Text 'Compatibility list overrides the bitness to 64.' }
if ($is64 -eq '0' -and $gameBits -ne 32) { $gameBits = 32; Report -Status 'Info' -Text 'Compatibility list overrides the bitness to 32.' }
}
else {
Report -Status 'Info' -Text 'ReShade''s compatibility list could not be fetched (offline?); relying on the import table alone.'
}
# What the exe imports.
$imp = @($pe.Imports)
$impD3D8 = [bool]($imp | Where-Object { $_ -match '(?i)^d3d8' })
$impD3D9 = [bool]($imp | Where-Object { $_ -match '(?i)^d3d9' })
$impDXGI = [bool]($imp | Where-Object { $_ -match '(?i)^(dxgi|d3d1[012])' -or $_ -match 'GFSDK' })
$impGL = [bool]($imp | Where-Object { $_ -match '(?i)^opengl32' })
$impVK = [bool]($imp | Where-Object { $_ -match '(?i)^vulkan-1' })
$impDDraw = [bool]($imp | Where-Object { $_ -match '(?i)^ddraw' })
$gfx = @($imp | Where-Object { $_ -match '(?i)^(d3d|dxgi|opengl32|vulkan-1|ddraw)' })
if ($gfx.Count -gt 0) { Report -Status 'Info' -Text ('Graphics imports: ' + ($gfx -join ', ')) }
else { Report -Status 'Info' -Text 'No graphics API in the static import table (the engine loads it at run time).' }
# Local translation layers.
$dxvk = $false
foreach ($n in @('d3d9.dll', 'dxgi.dll', 'd3d11.dll', 'd3d10core.dll')) {
$p = Find-FileIn $gameDir $n
if ($p -and ((Get-ProductNameSafe $p) -match '(?i)dxvk')) { $dxvk = $true }
}
# Either wrapper counts. dgVoodoo goes in as D3D9.dll for a Direct3D 9 game and D3D8.dll for a
# Direct3D 8 one, but this only ever looked for the D3D9 name -- so a correctly installed D3D8
# game read as "not present" on every run: the zip was re-downloaded, and the "files kept"
# branch below never fired, which quietly overwrote the user's edited dgVoodoo.conf each time
# (it is backed up first, but nobody expects to need the backup). Found while checking #56.
$dgVoodooPresent = [bool]((Find-FileIn $gameDir 'dgVoodoo.conf') -and
((Find-FileIn $gameDir 'd3d9.dll') -or (Find-FileIn $gameDir 'd3d8.dll')))
$detected = 'Unknown'
if ($compat) {
switch -Regex ($compat) {
'^D3D8$' { $detected = 'D3D8' }
'^D3D9$' { $detected = 'D3D9' }
'^(D3D1[012]|DXGI)$' { $detected = 'D3D' }
'^OpenGL$' { $detected = 'OpenGL' }
'^Vulkan$' { $detected = 'Vulkan' }
}
}
if ($detected -eq 'Unknown') {
if ($impVK) { $detected = 'Vulkan' }
elseif ($impDXGI) { $detected = 'D3D' }
elseif ($impD3D9) { $detected = 'D3D9' }
elseif ($impD3D8) { $detected = 'D3D8' }
elseif ($impGL) { $detected = 'OpenGL' }
elseif ($impDDraw) { $detected = 'DDraw' }
}
# Second level: engines that LoadLibrary their API at run time (Max Payne 3, many RAGE /
# in-house engines) import nothing. Look for the DLL names as strings in the exe instead.
#
# And where the exe is a small launcher shim that only starts the real engine -- Watch Dogs
# ships a 136 KB Watch_Dogs.exe next to the Disrupt binaries -- the exe contains no such
# string either, and scanning it alone found nothing at all. So fall through to the DLLs
# beside it, largest first, since the renderer lives in the big one.
$ambiguous = $false
if ($detected -eq 'Unknown') {
$hintSource = 'the executable'
$hints = Get-ApiStringHints $exePath
if ($hints.Count -eq 0) {
# Two things make this harder than "read the biggest DLL". A graphics RUNTIME or
# proxy sitting in the folder answers with its own name -- ReShade's dxgi.dll, DXVK,
# dgVoodoo2, or the NGX runtimes this very installer puts there on an earlier run
# (nvngx_dlssnr.dll is 165 MB, so it sorts first and says Vulkan). And a game can
# ship a GPU-detection helper that names every API it has ever heard of: Watch Dogs'
# systemdetection64.dll claims OpenGL and Direct3D 9 while the engine is D3D11.
#
# So: skip anything that IS a graphics runtime, read the rest, and add the counts up
# rather than trusting whichever file happened to be largest. The precedence below
# then resolves a folder that mentions several.
$skipExact = @('dxgi.dll','d3d8.dll','d3d9.dll','d3d10.dll','d3d10_1.dll','d3d11.dll','d3d12.dll',
'opengl32.dll','vulkan-1.dll','ddraw.dll','dinput8.dll','winmm.dll','version.dll')
$skipLike = @('nvngx_*','reshade*','deep-fried-chicken*','*dlss5*','alexs-toolkit*','dxvk*',
'd3dcompiler*','nvapi*','amd_ags*','_nvngx*')
$scanned = @()
try {
$scanned = @(Get-ChildItem -LiteralPath $gameDir -File -Filter '*.dll' -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 262144 } |
Where-Object { $skipExact -notcontains $_.Name.ToLowerInvariant() } |
Where-Object { $n = $_.Name; -not (@($skipLike | Where-Object { $n -like $_ }).Count) } |
Sort-Object Length -Descending | Select-Object -First 8)
}
catch { }
$from = @()
foreach ($nb in $scanned) {
$h2 = Get-ApiStringHints $nb.FullName
if ($h2.Count -eq 0) { continue }
$from += $nb.Name
foreach ($k in $h2.Keys) {
if ($hints.ContainsKey($k)) { $hints[$k] = $hints[$k] + $h2[$k] } else { $hints[$k] = $h2[$k] }
}
}
if ($hints.Count -gt 0) {
$hintSource = ($from -join ' + ')
Report -Status 'Info' -Text ('The executable names no graphics API; read it out of ' + $hintSource + ' instead.') `
-Detail 'A small launcher exe that starts the real engine carries no API strings of its own.'
}
}
if ($hints.Count -gt 0) {
Report -Status 'Info' -Text ('API DLL names found inside ' + $hintSource + ': ' + (($hints.Keys | Sort-Object | ForEach-Object { $_ + ' x' + $hints[$_] }) -join ', '))
$hVK = $hints.ContainsKey('vulkan-1.dll')
$hDX = $hints.ContainsKey('dxgi.dll') -or $hints.ContainsKey('d3d11.dll') -or $hints.ContainsKey('d3d12.dll') -or
$hints.ContainsKey('d3d10.dll') -or $hints.ContainsKey('d3d10_1.dll') -or $hints.ContainsKey('d3d10core.dll')
$hD9 = $hints.ContainsKey('d3d9.dll')
$hD8 = $hints.ContainsKey('d3d8.dll')
$hGL = $hints.ContainsKey('opengl32.dll')
if ($hVK) { $detected = 'Vulkan' }
elseif ($hDX) { $detected = 'D3D'; if ($hD9) { $ambiguous = $true } }
elseif ($hD9) { $detected = 'D3D9' }
elseif ($hD8) { $detected = 'D3D8' }
elseif ($hGL) { $detected = 'OpenGL' }
if ($detected -ne 'Unknown') {
$d = 'This is a guess from strings, not from the import table.'
if ($ambiguous) { $d += "`nBoth Direct3D 9 and 10/11/12 are named there: the game can run either. Direct3D 10/11/12 is assumed (ReShade makes the same choice); if the game is set to DirectX 9, re-run with -Api D3D9." }
Report -Status 'Warn' -Text ('Render API guessed as ' + $detected + ' from strings in ' + $hintSource + '.') -Detail $d
}
}
}
if ($dxvk) { $detected = 'Vulkan'; Report -Status 'Info' -Text 'DXVK is installed next to the exe, so the game reaches the GPU through Vulkan.' }
if ($dgVoodooPresent -and $detected -match '^D3D[89]$') { Report -Status 'Info' -Text 'dgVoodoo2 is already installed next to the exe.' }
if ($Api -ne 'Auto') {
if ($detected -ne 'Unknown' -and $detected -ne $Api) {
Report -Status 'Warn' -Text ('-Api ' + $Api + ' overrides the detected API (' + $detected + ').')
}
$useApi = $Api
}
else {
$useApi = $detected
}
# Still nothing. This script already asks which executable and which neural consumer, so
# stopping dead here -- and making the user re-run the whole thing with -Api -- was the one
# unanswerable question in an otherwise interactive install. Ask it instead.
if ($useApi -eq 'Unknown' -and -not $Yes) {
Write-Host ''
Write-Chunk ' Which render API does this game use?' 'White'
Write-Chunk ' Nothing in the executable or the files beside it says, which usually means the engine' 'DarkGray'
Write-Chunk ' loads it at run time. If you are not sure: almost every 64-bit game of the last decade' 'DarkGray'
Write-Chunk ' is Direct3D 10/11/12, and the game''s own graphics options usually name it.' 'DarkGray'
Write-Chunk ' 1. Direct3D 10/11/12' 'Gray'
Write-Chunk ' 2. Vulkan' 'Gray'
Write-Chunk ' 3. OpenGL' 'Gray'
Write-Chunk ' 4. Direct3D 9 ' 'Gray' -NoNewline
Write-Chunk '(installed through dgVoodoo2)' 'DarkGray'
Write-Chunk ' 5. Direct3D 8 ' 'Gray' -NoNewline
Write-Chunk '(installed through dgVoodoo2)' 'DarkGray'
Write-Chunk ' Which one? [1-5, Enter for 1] ' 'Cyan' -NoNewline
try { $a = Read-Host } catch { $a = '' }
$useApi = switch ($a.Trim()) {
'2' { 'Vulkan' }
'3' { 'OpenGL' }
'4' { 'D3D9' }
'5' { 'D3D8' }
default { 'D3D' }
}
Report -Status 'Info' -Text ('Render API set to ' + $useApi + ' by hand.') `
-Detail 'If the install completes but the game shows no ReShade overlay, this was the wrong answer -- re-run with -Api and the right one.'
}
switch ($useApi) {
'Unknown' { Stop-Install -Text 'Could not tell which render API the game uses.' -Manual 'Re-run with -Api D3D, -Api Vulkan, -Api OpenGL, -Api D3D9 or -Api D3D8.' }
'DDraw' { Stop-Install -Text 'DirectDraw games are not supported by DLSS5-Feeder.' }
}
$apiLabel = switch ($useApi) {
'D3D' { 'Direct3D 10/11/12 (local ReShade dxgi.dll)' }
'Vulkan' { 'Vulkan (machine-wide ReShade layer)' }
'OpenGL' { 'OpenGL (local ReShade opengl32.dll)' }
'D3D9' { 'Direct3D 9 via dgVoodoo2 (local ReShade dxgi.dll)' }
'D3D8' { 'Direct3D 8 via dgVoodoo2 (local ReShade dxgi.dll)' }
}
Report -Status 'Ok' -Text ('Render API: ' + $apiLabel)
$isVulkan = ($useApi -eq 'Vulkan')
$isGL = ($useApi -eq 'OpenGL')
$isDgV = ($useApi -eq 'D3D9' -or $useApi -eq 'D3D8')
$is32 = ($gameBits -eq 32)
if ($is32 -and $isVulkan) { Report -Status 'Info' -Text '32-bit Vulkan: ReShade goes in as the 32-bit layer, and the feeder as addon32 + host64\.' }
# Where the 64-bit side (consumer + NGX) lives.
$hostDir = Join-Safe $gameDir 'host64'
if ($is32) { $consumerDir = $hostDir; $consumerWhere = 'host64\' } else { $consumerDir = $gameDir; $consumerWhere = 'the game folder' }
$shaderDir = Join-Safe $gameDir 'reshade-shaders\Shaders'
$textureDir = Join-Safe $gameDir 'reshade-shaders\Textures'
# ---------------------------------------------------------------------------------------
# 2. GPU
# ---------------------------------------------------------------------------------------
Write-Section 'GPU'
$gpus = $null
try { $gpus = Get-CimInstance -ClassName Win32_VideoController -ErrorAction Stop } catch { try { $gpus = Get-WmiObject -Class Win32_VideoController -ErrorAction Stop } catch { } }
if ($gpus) {
$rtx = @($gpus | Where-Object { $_.Name -and $_.Name -match '(?i)nvidia' -and $_.Name -match '(?i)(RTX|TITAN RTX)' })
if ($rtx.Count -gt 0) { Report -Status 'Ok' -Text ('NVIDIA RTX adapter: ' + (($rtx | ForEach-Object { $_.Name }) -join '; ')) }
else { Report -Status 'Warn' -Text 'No NVIDIA RTX adapter detected. DLSS needs one; installing anyway.' -Detail (@($gpus | ForEach-Object { $_.Name }) -join '; ') }
}
else { Report -Status 'Warn' -Text 'Could not query the display adapters.' }
# ---------------------------------------------------------------------------------------
# 3. Downloads
# ---------------------------------------------------------------------------------------
Write-Section 'Downloads'
# 3a. Feeder release
$feederPath = $null
if ($FeederZip) {
$feederPath = Resolve-Piece -Label 'DLSS5-Feeder release' -Explicit $FeederZip -CacheName 'DLSS5-Feeder-explicit.zip'
}
else {
$local = $null
if ($LocalFiles) {
$hit = Get-ChildItem -LiteralPath $LocalFiles -File -Filter 'DLSS5-Feeder-*.zip' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($hit) { $local = $hit.FullName; Report -Status 'Ok' -Text ('DLSS5-Feeder release: using ' + $local) }
}
if ($local) { $feederPath = $local }
else {
$relJson = Get-WebString ($Sources.FeederReleases + $(if ($Prerelease) { '?per_page=5' } else { '/latest' }))
$assetUrl = $null
$assetName = $null
if ($relJson) {
try {
$rel = $relJson | ConvertFrom-Json
if ($Prerelease) { $rel = @($rel)[0] }
foreach ($a in @($rel.assets)) {
if ($a.name -match '(?i)^DLSS5-Feeder-.*\.zip$') { $assetUrl = $a.browser_download_url; $assetName = $a.name; break }
}
if ($assetUrl) { Report -Status 'Info' -Text ('Latest release: ' + $rel.tag_name + ' (' + $assetName + ')') }
}
catch { }
}
if ($assetUrl) {
$dest = Join-Safe $script:Cache $assetName
if (Get-Download -Url $assetUrl -Dest $dest -Label 'DLSS5-Feeder release') { $feederPath = $dest }
}
else {
# Offline: fall back to the newest cached release.
$hit = Get-ChildItem -LiteralPath $script:Cache -File -Filter 'DLSS5-Feeder-*.zip' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($hit) { $feederPath = $hit.FullName; Report -Status 'Warn' -Text ('GitHub unreachable; using the cached release ' + $hit.Name) }
else { Report -Status 'Fail' -Text 'Could not find the DLSS5-Feeder release on GitHub.' -Detail 'Download DLSS5-Feeder-<version>.zip from https://github.com/jlrouzies-fr/DLSS5-Feeder/releases and pass it with -FeederZip.' }
}
}
}
if (-not $feederPath) { Stop-Install -Text 'No feeder release to install.' }
# 3b. ReShade setup (only needed if ReShade is missing or too old somewhere; cheap, always fetch)
$reshadePath = $null
if ($ReShadeSetup) {
$reshadePath = Resolve-Piece -Label 'ReShade setup' -Explicit $ReShadeSetup -CacheName 'ReShade_Setup_explicit_Addon.exe'
}
else {
$local = $null
if ($LocalFiles) {
$hit = Get-ChildItem -LiteralPath $LocalFiles -File -Filter 'ReShade_Setup_*_Addon.exe' -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1
if ($hit) { $local = $hit.FullName; Report -Status 'Ok' -Text ('ReShade setup: using ' + $local) }
}
if ($local) { $reshadePath = $local }
else {
$homePage = Get-WebString $Sources.ReShadeHome
$setupRel = $null
if ($homePage) {
$m = [regex]::Match($homePage, 'downloads/ReShade_Setup_([0-9.]+)_Addon\.exe')
if ($m.Success) { $setupRel = $m.Value; Report -Status 'Info' -Text ('reshade.me offers ReShade ' + $m.Groups[1].Value + ' (add-on build)') }
}
if ($setupRel) { $url = $Sources.ReShadeHome + '/' + $setupRel } else { $url = $Sources.ReShadeFallback }
$dest = Join-Safe $script:Cache ([IO.Path]::GetFileName(($url -split '\?')[0]))
if (Get-Download -Url $url -Dest $dest -Label 'ReShade setup') { $reshadePath = $dest }
}
}
# 3c. Framework headers
$headerPaths = @{}
foreach ($h in @('ReShade.fxh', 'ReShadeUI.fxh', 'DrawText.fxh')) {
if (Find-FileUnder $shaderDir $h) { $headerPaths[$h] = $null; continue } # already installed
$p = Resolve-Piece -Label $h -LocalPattern $h -DefaultUrl ($Sources.Headers + $h) -CacheName ('reshade-framework\' + $h)
$headerPaths[$h] = $p
}
# 3d. LumeniteFX
$lumenitePath = Resolve-Piece -Label 'LumeniteFX' -Explicit $LumeniteZip -LocalPattern 'LumeniteFX*.zip' -DefaultUrl $Sources.Lumenite -CacheName 'LumeniteFX-mainline.zip'
# 3e. Consumer
$dfcPath = $null
$renoPath = $null
$optiPath = $null
if ($Consumer -eq 'DFC') {
$dfcPath = Find-ChickenZip -Explicit $DfcZip
# Nothing to download: the author does not publish these files, so the only routes are a
# copy the user already has, or the user fetching one now. Ask, wait, and re-check --
# printing an invite and failing in the same breath just sends them round again (#75).
if (-not $dfcPath -and -not $Yes) { $dfcPath = Get-ChickenManually }
# A zip fetched by hand can still be quarantined after it lands: Chicken hooks NGX with
# Detours and heuristics dislike that. If the file we just found has gone, that is what
# happened -- offer the same exclusion as before, for the same one folder.
if ($dfcPath -and -not (Test-FileHere $dfcPath)) {
$det = Get-DefenderDetection $dfcPath
if ($det) {
$ok = Request-DefenderExclusion -Paths @($script:Cache, $gameDir) -Because 'It removed the Deep Fried Chicken archive after it was downloaded.'
if ($ok) { $dfcPath = Find-ChickenZip -Explicit $DfcZip }
}
else { $dfcPath = $null }
}
if (-not $dfcPath) {
Report -Status 'Fail' -Text 'Deep Fried Chicken is not available.' `
-Detail ('It has no public download. Join ' + $Sources.DfcDiscord + ', get Deep-Fried-Chicken-*.zip, then re-run with -DfcZip <path> -- or choose the RenoDX consumer, which this installer can fetch for you.')
}
}
elseif ($Consumer -eq 'RenoDX') {
$renoPath = Resolve-PieceZipped -Label 'RenoDX DLSS 5 add-on' -Explicit $RenoDxAddon `
-LocalPattern 'renodx-dlss5*' -DefaultUrl $Sources.RenoDxDlss5 `
-CacheName 'renodx-dlss5.zip' -InnerName 'renodx-dlss5.addon64'
if (-not $renoPath) {
Report -Status 'Fail' -Text 'renodx-dlss5.addon64 is not available.' `
-Detail ('Get it from the RenoDX Discord (' + $Sources.RenoDxDiscord + ') or the RHI installer, and pass it with -RenoDxAddon or via -LocalFiles.')
}
}
else {
# OptiScaler_DLSSNR ships as a GitHub release (about 130 MB; nvngx_dlssnr.dll is not in it).
# Newest asset, the way the feeder's own release is found above; offline, the newest cached copy.
if ($OptiScalerZip) {
$optiPath = Resolve-Piece -Label 'OptiScaler DLSS-NR' -Explicit $OptiScalerZip -CacheName 'OptiScaler-DLSSNR-explicit.zip'
}
else {
if ($LocalFiles) {
$hit = Get-ChildItem -LiteralPath $LocalFiles -File -Filter 'OptiScaler-DLSSNR*.zip' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($hit) { $optiPath = $hit.FullName; Report -Status 'Ok' -Text ('OptiScaler DLSS-NR: using ' + $optiPath) }
}
if (-not $optiPath) {
$relJson = Get-WebString ($Sources.OptiScalerReleases + '/latest')
$assetUrl = $null
$assetName = $null
if ($relJson) {
try {
$rel = $relJson | ConvertFrom-Json
foreach ($a in @($rel.assets)) {
if ($a.name -match '(?i)^OptiScaler-DLSSNR-.*\.zip$') { $assetUrl = $a.browser_download_url; $assetName = $a.name; break }
}
if ($assetUrl) { Report -Status 'Info' -Text ('OptiScaler DLSS-NR latest release: ' + $rel.tag_name + ' (' + $assetName + ')') }
}
catch { }
}
if ($assetUrl) {
$dest = Join-Safe $script:Cache $assetName
if (Get-Download -Url $assetUrl -Dest $dest -Label 'OptiScaler DLSS-NR') { $optiPath = $dest }
}
else {
$hit = Get-ChildItem -LiteralPath $script:Cache -File -Filter 'OptiScaler-DLSSNR*.zip' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($hit) { $optiPath = $hit.FullName; Report -Status 'Warn' -Text ('GitHub unreachable; using the cached OptiScaler release ' + $hit.Name) }
}
}
}
if (-not $optiPath) {
Report -Status 'Fail' -Text 'OptiScaler DLSS-NR is not available.' `
-Detail ('Download OptiScaler-DLSSNR-<version>.zip from ' + $Sources.OptiScalerHome + ' and pass it with -OptiScalerZip, or drop it in -LocalFiles.')
}
}
# 3f. NVIDIA runtimes
$dlssNrPath = Resolve-PieceZipped -Label 'nvngx_dlssnr.dll' -Explicit $DlssNrDll -LocalPattern 'nvngx_dlssnr*' -DefaultUrl $Sources.DlssNr -CacheName 'nvngx_dlssnr.zip' -InnerName 'nvngx_dlssnr.dll'
$dlssPath = Resolve-PieceZipped -Label 'nvngx_dlss.dll' -Explicit $DlssDll -LocalPattern 'nvngx_dlss.dll' -DefaultUrl $Sources.Dlss -CacheName 'nvngx_dlss.zip' -InnerName 'nvngx_dlss.dll'
if (-not $dlssNrPath) { Report -Status 'Fail' -Text 'nvngx_dlssnr.dll is not available.' -Detail ('It is on the RenoDX Discord (' + $Sources.RenoDxDiscord + '). Pass it with -DlssNrDll.') }
if (-not $dlssPath) { Report -Status 'Fail' -Text 'nvngx_dlss.dll is not available.' -Detail 'Any DLSS-enabled game ships one, or use DLSS Swapper. Pass it with -DlssDll.' }
# 3g. dgVoodoo2
$dgvPath = $null
if ($isDgV -and -not $dgVoodooPresent) {
if ($DgVoodooZip) {
$dgvPath = Resolve-Piece -Label 'dgVoodoo2' -Explicit $DgVoodooZip -CacheName 'dgVoodoo2-explicit.zip'
}
else {
$local = $null
if ($LocalFiles) {
$hit = Get-ChildItem -LiteralPath $LocalFiles -File -Filter 'dgVoodoo2_*.zip' -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch '(?i)(dbg|dev64)' } | Sort-Object Name -Descending | Select-Object -First 1
if ($hit) { $local = $hit.FullName; Report -Status 'Ok' -Text ('dgVoodoo2: using ' + $local) }
}
if ($local) { $dgvPath = $local }
else {
$j = Get-WebString $Sources.DgVoodoo
$url = $null; $name = $null
if ($j) {
try {
$rel = $j | ConvertFrom-Json
foreach ($a in @($rel.assets)) { if ($a.name -match '(?i)^dgVoodoo2_\d+_\d+\.zip$') { $url = $a.browser_download_url; $name = $a.name; break } }
if ($url) { Report -Status 'Info' -Text ('dgVoodoo2 latest: ' + $rel.tag_name + ' (' + $name + ')') }
}
catch { }
}
if ($url) { $dest = Join-Safe $script:Cache $name; if (Get-Download -Url $url -Dest $dest -Label 'dgVoodoo2') { $dgvPath = $dest } }
else {
$hit = Get-ChildItem -LiteralPath $script:Cache -File -Filter 'dgVoodoo2_*.zip' -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1
if ($hit) { $dgvPath = $hit.FullName; Report -Status 'Warn' -Text ('GitHub unreachable; using cached ' + $hit.Name) }
else { Report -Status 'Fail' -Text 'dgVoodoo2 could not be fetched.' -Detail 'Download it from https://github.com/dege-diosg/dgVoodoo2/releases and pass -DgVoodooZip.' }
}
}
}
}
# ---------------------------------------------------------------------------------------
# 4. ReShade
# ---------------------------------------------------------------------------------------
Write-Section 'ReShade'
function Test-ReShadeVersionOk
{
param([string] $Version)
if (-not $Version) { return $null }
$m = [regex]::Match($Version, '^\s*(\d+)\.(\d+)')
if (-not $m.Success) { return $null }
$want = $ReShadeMinVersion -split '\.'
$maj = [int]$m.Groups[1].Value; $min = [int]$m.Groups[2].Value
if ($maj -gt [int]$want[0]) { return $true }
if ($maj -eq [int]$want[0] -and $min -ge [int]$want[1]) { return $true }
return $false
}
function Test-IsReShade
{
param([string] $Path)
$n = Get-ProductNameSafe $Path
return [bool]($n -and $n -match '(?i)reshade')
}
$reshadeZip = $null
$reshadeSetupVersion = $null
if ($reshadePath) {
try {
$reshadeZip = Open-Zip $reshadePath
$m = [regex]::Match([IO.Path]::GetFileName($reshadePath), 'ReShade_Setup_([0-9.]+)')
if ($m.Success) { $reshadeSetupVersion = $m.Groups[1].Value }
if (-not (Find-ZipEntry $reshadeZip '^ReShade64\.dll$') -or -not (Find-ZipEntry $reshadeZip '^ReShade32\.dll$')) { throw 'ReShade32.dll / ReShade64.dll not found inside the setup' }
}
catch {
Report -Status 'Fail' -Text 'The ReShade setup could not be opened as an archive.' -Detail $_.Exception.Message
$reshadeZip = $null
}
}
# Places a ReShade DLL of the given bitness at $To, unless a new-enough one is already there.
function Install-ReShadeDll
{
param([int] $Bits, [string] $To, [string] $Label)
$name = [IO.Path]::GetFileName($To)
if (Test-FileHere $To) {
if (Test-IsReShade $To) {
$v = Get-FileVersionSafe $To
$ok = Test-ReShadeVersionOk $v
# Add-on support is checked before the version, and a build without it is replaced
# even though it is new enough: it is ReShade's plain build, which cannot load an
# add-on at all, and it is indistinguishable from the right one by version or
# ProductName (issue #53).
$addons = Test-ReShadeHasAddons $To
if ($addons -eq $false) {
Report -Status 'Info' -Text ($Label + ': ReShade ' + $v + ' is present but built WITHOUT add-on support; replacing.') `
-Detail 'It does not export ReShadeRegisterAddon, so dlss5-feed would never be loaded and nothing would say so.'
}
elseif ($ok -eq $true -and -not $Force) { Report -Status 'Ok' -Text ($Label + ': ReShade ' + $v + ' already present, kept.') -Detail $To; return $true }
elseif ($ok -ne $true) { Report -Status 'Info' -Text ($Label + ': ReShade ' + $v + ' is too old (need ' + $ReShadeMinVersion + '+); replacing.') }
}
else {
$what = Get-ProductNameSafe $To
if (-not $what) { $what = 'no version info' }
Report -Status 'Fail' -Text ($Label + ': a ' + $name + ' that is not ReShade is already there (' + $what + ').') `
-Detail $To -Manual ('Find out what that ' + $name + ' is. If the game needs it, ReShade must be installed under another name (see the README); otherwise move it away and re-run.')
return $false
}
}
if (-not $reshadeZip) { Report -Status 'Fail' -Text ($Label + ': no ReShade setup available to take ' + $name + ' from.'); return $false }
$entry = Find-ZipEntry $reshadeZip ('^ReShade' + $Bits + '\.dll$')
try {
Expand-ZipEntry -Entry $entry -To $To
Report -Status 'Done' -Text ($Label + ': ReShade ' + $reshadeSetupVersion + ' installed as ' + $name + '.') -Detail $To
return $true
}
catch { Report -Status 'Fail' -Text ($Label + ': cannot write ' + $To) -Detail $_.Exception.Message; return $false }
}
$reshadeLocalDll = $null
if ($isVulkan) {
# Machine-wide layer under C:\ProgramData\ReShade, registered in HKLM, gated per exe.
$pdRoot = Join-Safe $env:ProgramData 'ReShade'
$layerName = 'ReShade' + $gameBits
$pdDll = Join-Safe $pdRoot ($layerName + '.dll')
$pdJson = Join-Safe $pdRoot ($layerName + '.json')
$regKey = if ($gameBits -eq 32) { 'HKLM:\SOFTWARE\WOW6432Node\Khronos\Vulkan\ImplicitLayers' } else { 'HKLM:\SOFTWARE\Khronos\Vulkan\ImplicitLayers' }
$needDll = $true
if (Test-FileHere $pdDll) {
$v = Get-FileVersionSafe $pdDll
# This is the case issue #53 was actually reporting. The Vulkan layer is machine-wide,
# so one plain (no-add-on) ReShade install done for any earlier game is silently reused
# for every game after it -- the script said "already installed", the verifier said OK,
# and the add-on was never loaded. Keeping it only if it can actually load add-ons.
$pdAddons = Test-ReShadeHasAddons $pdDll
if ($pdAddons -eq $false) {
Report -Status 'Info' -Text ('Vulkan layer ' + $layerName + '.dll ' + $v + ' is built WITHOUT add-on support; replacing with ' + $reshadeSetupVersion + '.') `
-Detail ($pdDll + "`nIt does not export ReShadeRegisterAddon. This layer is shared by every Vulkan game on the machine, so it was probably installed by ReShade's plain setup for some other game.")
}
elseif ((Test-ReShadeVersionOk $v) -eq $true -and -not $Force) { $needDll = $false; Report -Status 'Ok' -Text ('Vulkan layer ' + $layerName + '.dll ' + $v + ' already installed (add-on build).') -Detail $pdDll }
else { Report -Status 'Info' -Text ('Vulkan layer ' + $layerName + '.dll is ' + $v + '; will replace with ' + $reshadeSetupVersion + '.') }
}
$needJson = -not (Test-FileHere $pdJson)
$needReg = $true
try {
$rp = Get-ItemProperty -Path $regKey -ErrorAction Stop
foreach ($prop in $rp.PSObject.Properties) { if ($prop.Name -ieq $pdJson) { $needReg = $false } }
}
catch { }
if (-not $needReg) { Report -Status 'Ok' -Text ('Vulkan layer registered in ' + $regKey) }
$appsIni = Join-Safe $pdRoot 'ReShadeApps.ini'
$needApps = $true
$staleApps = @()
$appsText = Read-TextSafe $appsIni
if ($appsText) {
$m = [regex]::Match($appsText, '(?im)^\s*Apps\s*=\s*(.*)$')
if ($m.Success) {
$entries = @($m.Groups[1].Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
foreach ($e in $entries) {
if ($e.TrimEnd('\') -ieq $exePath.TrimEnd('\')) { $needApps = $false }
elseif (([IO.Path]::GetFileName($e) -ieq $exeLeaf) -and -not (Test-FileHere $e)) { $staleApps += $e }
}
}
}
if (-not $needApps) { Report -Status 'Ok' -Text 'Game exe already on the ReShadeApps.ini Apps= list.' }
if ($needDll -or $needJson -or $needReg -or $needApps) {
if (($needDll -or $needJson) -and -not $reshadeZip) {
Report -Status 'Fail' -Text 'ReShade''s Vulkan layer is missing and no ReShade setup is available.'
}
else {
# Stage the files non-elevated; the elevated step only copies and registers.
$stage = Join-Safe $script:Cache ('reshade-' + $reshadeSetupVersion)
$lines = @()
$lines += ' $pd = ' + (ConvertTo-PsLiteral $pdRoot)
$lines += ' New-Item -ItemType Directory -Force -Path $pd | Out-Null'
if ($needDll -or $needJson) {
foreach ($b in @(32, 64)) {
foreach ($ext in @('dll', 'json')) {
$n = 'ReShade' + $b + '.' + $ext
$e = Find-ZipEntry $reshadeZip ('^' + [regex]::Escape($n) + '$')
if (-not $e) { continue }
$sp = Join-Safe $stage $n
Expand-ZipEntry -Entry $e -To $sp
$target = Join-Safe $pdRoot $n
# Replace the DLL only for the bitness that needs it, or both when installing fresh; always place missing manifests.
$replace = ($ext -eq 'json' -and -not (Test-FileHere $target)) -or ($ext -eq 'dll' -and ($needDll -and ($b -eq $gameBits -or -not (Test-FileHere $target))))
if ($replace) { $lines += ' Copy-Item -LiteralPath ' + (ConvertTo-PsLiteral $sp) + ' -Destination ' + (ConvertTo-PsLiteral $target) + ' -Force' }
}
}
$lines += ' try { $acl = Get-Acl $pd; $sid = New-Object Security.Principal.SecurityIdentifier("S-1-15-2-1"); $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($sid, "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow"))); Set-Acl $pd $acl } catch { }'
}
foreach ($b in @(32, 64)) {
$k = if ($b -eq 32) { 'HKLM:\SOFTWARE\WOW6432Node\Khronos\Vulkan\ImplicitLayers' } else { 'HKLM:\SOFTWARE\Khronos\Vulkan\ImplicitLayers' }
$j = Join-Safe $pdRoot ('ReShade' + $b + '.json')
$lines += ' if (Test-Path -LiteralPath ' + (ConvertTo-PsLiteral $j) + ') { New-Item -Path ' + (ConvertTo-PsLiteral $k) + ' -Force | Out-Null; New-ItemProperty -Path ' + (ConvertTo-PsLiteral $k) + ' -Name ' + (ConvertTo-PsLiteral $j) + ' -Value 0 -PropertyType DWord -Force | Out-Null }'
}
if ($needApps) {
$lines += ' $ini = Join-Path $pd "ReShadeApps.ini"'
$lines += ' $exe = ' + (ConvertTo-PsLiteral $exePath)
$lines += ' $stale = @(' + (($staleApps | ForEach-Object { ConvertTo-PsLiteral $_ }) -join ', ') + ')'
$lines += ' if (Test-Path -LiteralPath $ini) { Copy-Item -LiteralPath $ini -Destination ($ini + ".bak") -Force; $t = [IO.File]::ReadAllText($ini); if ($t.Length -gt 0 -and [int]$t[0] -eq 0xFEFF) { $t = $t.Substring(1) } } else { $t = "" }'
$lines += ' $m = [regex]::Match($t, "(?im)^\s*Apps\s*=\s*(.*)$")'
$lines += ' if ($m.Success) { $list = @($m.Groups[1].Value -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -and ($stale -notcontains $_) }) } else { $list = @() }'
$lines += ' if (-not ($list | Where-Object { $_ -ieq $exe })) { $list += $exe }'
$lines += ' $line = "Apps=" + ($list -join ",")'
$lines += ' if ($m.Success) { $t = $t.Substring(0, $m.Index) + $line + $t.Substring($m.Index + $m.Length) } else { $t = ($t.TrimEnd() + "`r`n" + $line).TrimStart() }'
$lines += ' [IO.File]::WriteAllText($ini, $t, (New-Object Text.UTF8Encoding $true))'
}
$what = 'install ReShade''s Vulkan layer'
if (-not $needDll -and -not $needJson -and -not $needReg) { $what = 'add the game to ReShadeApps.ini' }
if ($NoElevate) {
Report -Status 'Fail' -Text ('Vulkan: cannot ' + $what + ' without administrator rights (-NoElevate).') `
-Manual ('Run ReShade''s installer (' + $reshadePath + ') against the game exe, choose Vulkan, enable add-ons -- or re-run this script without -NoElevate.')
}
else {
$r = Invoke-Elevated -Script ($lines -join "`r`n") -What $what
if ($r -eq 'ok') {
Report -Status 'Done' -Text ('Vulkan: ReShade layer ' + $(if ($needDll) { 'installed' } else { 'confirmed' }) + ', registered, and the game added to ReShadeApps.ini.') -Detail $pdRoot
if ($staleApps.Count -gt 0) { Report -Status 'Info' -Text ('Replaced stale Apps= entries for the same exe: ' + ($staleApps -join '; ')) }
}
elseif ($r -eq 'declined') {
Report -Status 'Fail' -Text 'Vulkan: the UAC prompt was declined; ReShade''s layer is not set up for this game.' `
-Manual ('Re-run and accept the prompt, or run ReShade''s installer (' + $reshadePath + ') against the exe with Vulkan selected.')
}
else {
Report -Status 'Fail' -Text 'Vulkan: the elevated step failed.' -Manual ('Run ReShade''s installer (' + $reshadePath + ') against the exe with Vulkan selected.')
}
}
}
}
# A stray local ReShade DLL would fight the layer.
foreach ($n in @('dxgi.dll', 'opengl32.dll', 'd3d11.dll')) {
$p = Find-FileIn $gameDir $n
if ($p -and (Test-IsReShade $p) -and -not $dxvk) {
Report -Status 'Warn' -Text ('A local ReShade ' + $n + ' is also present; the Vulkan layer is what will load. Consider removing it.') -Detail $p
}
}
}
else {
# Local DLL next to the exe: dxgi.dll for Direct3D (dgVoodoo owns d3d9.dll), opengl32.dll for OpenGL.
$localName = if ($isGL) { 'opengl32.dll' } else { 'dxgi.dll' }
# An existing ReShade under another name? Use it rather than double-installing.
$existing = $null
foreach ($n in @('dxgi.dll', 'opengl32.dll', 'd3d11.dll', 'd3d9.dll', 'd3d12.dll', 'ddraw.dll', 'dinput8.dll')) {
$p = Find-FileIn $gameDir $n
if ($p -and (Test-IsReShade $p)) { $existing = $p; break }
}
if ($existing -and ([IO.Path]::GetFileName($existing) -ine $localName)) {
if ($isDgV -and ([IO.Path]::GetFileName($existing) -ieq 'd3d9.dll')) {
Report -Status 'Warn' -Text 'ReShade is installed as d3d9.dll, but dgVoodoo2 needs that name.' -Detail $existing `
-Manual ('Remove ' + $existing + ' (it is ReShade, not the game''s) and re-run; ReShade then goes in as dxgi.dll.')
}
else {
Report -Status 'Info' -Text ('ReShade is already installed here as ' + [IO.Path]::GetFileName($existing) + '; using that instead of ' + $localName + '.')
$localName = [IO.Path]::GetFileName($existing)
}
}
$reshadeLocalDll = Join-Safe $gameDir $localName
$null = Install-ReShadeDll -Bits $gameBits -To $reshadeLocalDll -Label ('Game folder (' + $gameBits + '-bit)')
}
# 32-bit: the host helper needs its own 64-bit ReShade as dxgi.dll.
if ($is32) {
New-DirSafe $hostDir
$null = Install-ReShadeDll -Bits 64 -To (Join-Safe $hostDir 'dxgi.dll') -Label 'host64\'
}
# ---------------------------------------------------------------------------------------
# 5. dgVoodoo2 (Direct3D 8/9 only)
# ---------------------------------------------------------------------------------------
if ($isDgV) {
Write-Section 'dgVoodoo2'
$confPath = Join-Safe $gameDir 'dgVoodoo.conf'
$wrapName = if ($useApi -eq 'D3D8') { 'D3D8.dll' } else { 'D3D9.dll' }
$wrapPath = Join-Safe $gameDir $wrapName
$sub = if ($gameBits -eq 64) { 'x64' } else { 'x86' }
if ($dgVoodooPresent -and -not $Force) {
Report -Status 'Ok' -Text 'dgVoodoo2 already installed; files kept, settings checked below.' -Detail $confPath
}
elseif ($dgvPath) {
try {
$z = Open-Zip $dgvPath
try {
$wanted = @(
@{ P = ('^MS/' + $sub + '/' + $wrapName + '$'); T = $wrapPath },
@{ P = '^dgVoodoo\.conf$'; T = $confPath },
@{ P = '^dgVoodooCpl\.exe$'; T = (Join-Safe $gameDir 'dgVoodooCpl.exe') }
)
foreach ($w in $wanted) {
$e = Find-ZipEntry $z $w.P
if (-not $e) { throw ('entry not found in the dgVoodoo2 zip: ' + $w.P) }
if (($w.T -ieq $wrapPath) -and (Test-FileHere $wrapPath) -and -not ((Get-ProductNameSafe $wrapPath) -match '(?i)dgvoodoo')) {
throw ('a ' + $wrapName + ' that is not dgVoodoo is already next to the exe (' + (Get-ProductNameSafe $wrapPath) + '); move it away first')
}
if (($w.T -ieq $confPath) -and (Test-FileHere $confPath)) { $null = Backup-File $confPath }
Expand-ZipEntry -Entry $e -To $w.T
}
Report -Status 'Done' -Text ('dgVoodoo2 installed: MS\' + $sub + '\' + $wrapName + ', dgVoodoo.conf, dgVoodooCpl.exe.')
}
finally { $z.Dispose() }
}
catch { Report -Status 'Fail' -Text 'dgVoodoo2 install failed.' -Detail $_.Exception.Message }
}
else {
Report -Status 'Fail' -Text 'dgVoodoo2 is not available; the Direct3D 8/9 path cannot work without it.'
}
# Settings from the README's table. Watermark off by default (-DgVoodooWatermark keeps it).
$ct = Read-TextSafe $confPath
if ($ct) {
$wm = if ($DgVoodooWatermark) { 'true' } else { 'false' }
$new = $ct
$new = Set-ConfValue -Text $new -Section 'General' -Key 'OutputAPI' -Value 'd3d11_fl11_0'
$new = Set-ConfValue -Text $new -Section 'DirectX' -Key 'DisableAndPassThru' -Value 'false'
$new = Set-ConfValue -Text $new -Section 'DirectX' -Key 'VideoCard' -Value 'internal3D'
$new = Set-ConfValue -Text $new -Section 'DirectX' -Key 'VRAM' -Value '1GB'
$new = Set-ConfValue -Text $new -Section 'DirectX' -Key 'dgVoodooWatermark' -Value $wm
if ($new.TrimEnd() -ne $ct.TrimEnd()) {
Write-TextTracked -Path $confPath -Text $new
Report -Status 'Done' -Text ('dgVoodoo.conf: DisableAndPassThru=false, VideoCard=internal3D, VRAM=1GB, OutputAPI=d3d11_fl11_0, dgVoodooWatermark=' + $wm + '.')
}
else { Report -Status 'Ok' -Text 'dgVoodoo.conf already has the required values.' }
}
}
# ---------------------------------------------------------------------------------------
# 6. The feeder
# ---------------------------------------------------------------------------------------
Write-Section 'DLSS5-Feeder'
$addonName = if ($is32) { 'dlss5-feed.addon32' } else { 'dlss5-feed.addon64' }
$wrongAddon = if ($is32) { 'dlss5-feed.addon64' } else { 'dlss5-feed.addon32' }
$verifyScript = $null
$feederVersion = $null
try {
$z = Open-Zip $feederPath
try {
$e = Find-ZipEntry $z ('(?i)(^|/)' + [regex]::Escape($addonName) + '$')
if (-not $e) { throw ($addonName + ' not found in ' + $feederPath) }
Expand-ZipEntry -Entry $e -To (Join-Safe $gameDir $addonName)
$fx = Find-ZipEntry $z '(?i)(^|/)DLSS5_Feed\.fx$'
if (-not $fx) { throw 'DLSS5_Feed.fx not found in the release' }
Expand-ZipEntry -Entry $fx -To (Join-Safe $shaderDir 'DLSS5_Feed.fx')
$m = [regex]::Match([IO.Path]::GetFileName($feederPath), 'DLSS5-Feeder-(.+)\.zip$')
if ($m.Success) { $feederVersion = $m.Groups[1].Value }
Report -Status 'Done' -Text ($addonName + ' and reshade-shaders\Shaders\DLSS5_Feed.fx installed' + $(if ($feederVersion) { ' (' + $feederVersion + ')' } else { '' }) + '.')
if ($is32) {
$h = Find-ZipEntry $z '(?i)(^|/)dlss5-feed-host64\.exe$'
if (-not $h) { throw 'host64/dlss5-feed-host64.exe not found in the release' }
Expand-ZipEntry -Entry $h -To (Join-Safe $hostDir 'dlss5-feed-host64.exe')
Report -Status 'Done' -Text 'host64\dlss5-feed-host64.exe installed (same release as the add-on).'
}
if ($isVulkan) {
$layerSub = if ($is32) { 'layer-x86' } else { 'layer-x64' }
$n = 0
foreach ($le in $z.Entries) {
$ln = $le.FullName -replace '\\', '/'
if ($ln -match ('(?i)^' + $layerSub + '/(.+)$') -and -not $ln.EndsWith('/')) {
Expand-ZipEntry -Entry $le -To (Join-Safe $gameDir ('layer\' + $Matches[1]))
$n++
}
}
if ($n -gt 0) { Report -Status 'Done' -Text ('Vulkan fallback layer copied to layer\ (' + $n + ' files).') -Detail 'Only needed if dlss5-feed.log says the Vulkan interop entry points are missing: launch through layer\run-with-feed-layer*.bat.' }
}
$v = Find-ZipEntry $z '(?i)(^|/)Verify-DLSS5Feeder\.ps1$'
if ($v) { $verifyScript = Join-Safe $gameDir 'Verify-DLSS5Feeder.ps1'; Expand-ZipEntry -Entry $v -To $verifyScript }
}
finally { $z.Dispose() }
}
catch { Report -Status 'Fail' -Text 'Feeder files could not be installed.' -Detail $_.Exception.Message }
$stray = Find-FileIn $gameDir $wrongAddon
if ($stray) {
try { Rename-Item -LiteralPath $stray -NewName ($wrongAddon + '.disabled-by-installer') -Force; Report -Status 'Done' -Text ($wrongAddon + ' (wrong architecture) renamed to .disabled-by-installer.') }
catch { Report -Status 'Warn' -Text ($wrongAddon + ' is also present and is for the other architecture; remove it.') }
}
# Framework headers
$missing = @()
foreach ($h in @('ReShade.fxh', 'ReShadeUI.fxh', 'DrawText.fxh')) {
if (Find-FileUnder $shaderDir $h) { continue }
$src = $headerPaths[$h]
if ($src) { try { Copy-Tracked -From $src -To (Join-Safe $shaderDir $h) } catch { $missing += $h } }
else { $missing += $h }
}
if ($missing.Count -eq 0) { Report -Status 'Ok' -Text 'ReShade framework headers in place (ReShade.fxh, ReShadeUI.fxh, DrawText.fxh).' }
else { Report -Status 'Fail' -Text ('Missing framework header(s): ' + ($missing -join ', ')) -Detail 'Copy them from https://github.com/crosire/reshade-shaders/tree/slim/Shaders into reshade-shaders\Shaders\.' }
# ---------------------------------------------------------------------------------------
# 7. Motion vectors: LumeniteFX
# ---------------------------------------------------------------------------------------
Write-Section 'Motion vectors (LumeniteFX)'
$providerFx = if ($MvProvider -eq 4) { 'lumenite_QuantMotion.fx' } else { 'lumenite_Kernel.fx' }
$providerTechnique = if ($MvProvider -eq 4) { 'Lumenite_QuantMotion@lumenite_QuantMotion.fx' } else { 'Lumenite_Kernel@lumenite_Kernel.fx' }
if ($lumenitePath) {
try {
$z = Open-Zip $lumenitePath
try {
$n = 0
foreach ($e in $z.Entries) {
$en = $e.FullName -replace '\\', '/'
if ($en.EndsWith('/')) { continue }
if ($en -match '(?i)(^|/)Shaders/(lumenite_[^/]+\.fx)$') { Expand-ZipEntry -Entry $e -To (Join-Safe $shaderDir $Matches[2]); $n++ }
elseif ($en -match '(?i)(^|/)Shaders/include/([^/]+\.fxh)$') { Expand-ZipEntry -Entry $e -To (Join-Safe $shaderDir ('include\' + $Matches[2])); $n++ }
elseif ($en -match '(?i)(^|/)Textures/([^/]+)$') { Expand-ZipEntry -Entry $e -To (Join-Safe $textureDir $Matches[2]); $n++ }
}
if ($n -eq 0) { throw 'no Shaders\ or Textures\ entries found in the LumeniteFX zip' }
Report -Status 'Done' -Text ('LumeniteFX installed (' + $n + ' files into reshade-shaders\Shaders\ and Textures\).')
}
finally { $z.Dispose() }
}
catch { Report -Status 'Fail' -Text 'LumeniteFX could not be installed.' -Detail $_.Exception.Message }
}
else {
Report -Status 'Fail' -Text 'LumeniteFX is not available.' -Detail 'https://github.com/umar-afzaal/LumeniteFX (Code > Download ZIP), then -LumeniteZip <file>.'
}
if (-not (Find-FileUnder $shaderDir $providerFx)) { Report -Status 'Fail' -Text ($providerFx + ' is not installed; DLSS5_MV_PROVIDER=' + $MvProvider + ' has nothing to read.') }
# ---------------------------------------------------------------------------------------
# 8. Neural consumer and NVIDIA runtimes
# ---------------------------------------------------------------------------------------
Write-Section ('Neural consumer (' + $consumerWhere + ')')
New-DirSafe $consumerDir
# Files that must not coexist with the chosen consumer.
function Disable-Conflict
{
param([string] $Path, [string] $Why)
if (-not (Test-FileHere $Path)) { return }
$n = [IO.Path]::GetFileName($Path)
if ($Yes -or (Confirm-Step ('Rename ' + $n + ' to .disabled-by-installer? (' + $Why + ')'))) {
try { Rename-Item -LiteralPath $Path -NewName ($n + '.disabled-by-installer') -Force; Report -Status 'Done' -Text ($n + ' renamed to .disabled-by-installer.') -Detail $Why }
catch { Report -Status 'Warn' -Text ($n + ' could not be renamed.') -Detail $_.Exception.Message }
}
else { Report -Status 'Warn' -Text ($n + ' left in place.') -Detail $Why -Manual ('Remove ' + $Path + ' before playing.') }
}
# OptiScaler under any of the names it installs as (dxgi.dll excluded: that is ReShade). It captures
# every nvngx load in the process, so beside another consumer it either takes that consumer's calls
# or doubles the neural pass -- and beside a 32-bit exe a 64-bit winmm.dll stops the game starting.
$OptiProxyNames = @('winmm.dll', 'version.dll', 'dbghelp.dll', 'winhttp.dll', 'wininet.dll', 'd3d12.dll', 'OptiScaler.dll', 'OptiScaler.asi')
function Find-OptiScalerDll
{
param([string] $Dir)
foreach ($n in $OptiProxyNames) {
$p = Find-FileIn $Dir $n
if (-not $p) { continue }
if ((Get-BinaryMarker -Path $p -Pattern 'nvngx\.dll_dlssnr\.dll') -or (Get-BinaryMarker -Path $p -Pattern 'OptiScaler\.ini')) { return $p }
}
return $null
}
Disable-Conflict -Path (Find-FileIn $consumerDir 'dlss5-dx11-bridge.addon64') -Why 'the DX11 bridge must never be combined with DLSS5-Feeder'
if ($is32) {
Disable-Conflict -Path (Find-OptiScalerDll $gameDir) -Why 'a 64-bit OptiScaler beside a 32-bit exe stops the game from starting; for a 32-bit game it belongs in host64\'
foreach ($n in @('deep-fried-chicken.addon64', 'renodx-dlss5*.addon64', 'alexs-toolkit.addon64')) {
foreach ($f in (Find-FilesIn $gameDir $n)) {
Disable-Conflict -Path $f.FullName -Why 'a 64-bit add-on beside a 32-bit exe is never loaded; the consumer belongs in host64\'
}
}
# The reverse mistake, and the damaging one: this project's own 64-bit add-on inside
# host64\. It DOES load -- host64\ is a 64-bit ReShade install -- and what loads is the
# add-on for a 64-bit game, running inside the helper that is already serving the 32-bit
# one: a second NGX session on a second private device, and an nvngx detour over the
# neural consumer's own hooks.
Disable-Conflict -Path (Find-FileIn $hostDir 'dlss5-feed.addon64') -Why 'that is the add-on for a 64-bit GAME; inside the helper it starts a second feeder in the process serving the first'
}
if ($Consumer -eq 'DFC') {
# Every copy, versioned names included: one left behind is enough to keep Chicken inert (#44).
foreach ($f in (Find-FilesIn $consumerDir 'renodx-dlss5*.addon64')) {
Disable-Conflict -Path $f.FullName -Why 'Deep Fried Chicken stays inert while a RenoDX neural provider is loaded'
}
Disable-Conflict -Path (Find-FileIn $consumerDir 'alexs-toolkit.addon64') -Why 'a third interposer on the same NGX module; Chicken''s docs ask for it to be removed'
Disable-Conflict -Path (Find-OptiScalerDll $consumerDir) -Why 'OptiScaler captures every nvngx load in the process; beside Chicken it takes Chicken''s own calls or doubles the neural pass'
if ($dfcPath) {
$dfcFiles = @('deep-fried-chicken.addon64', 'deep-fried-chicken-nvngx.dll', 'deep-fried-chicken.cfg')
$attempt = 0
$dfcOk = $false
while ($attempt -lt 2 -and -not $dfcOk) {
$attempt++
$sums = @{}
$failed = $null
$dfcVersion = $null
try {
$z = Open-Zip $dfcPath
try {
$se = Find-ZipEntry $z '(?i)(^|/)SHA256SUMS\.txt$'
if ($se) {
$st = Read-ZipText $se
$vm = [regex]::Match($st, 'Deep Fried Chicken (\S+)')
if ($vm.Success) { $dfcVersion = $vm.Groups[1].Value }
foreach ($sm in [regex]::Matches($st, '(?m)^\s*([0-9A-Fa-f]{64})\s+(\S+)\s*$')) { $sums[$sm.Groups[2].Value.ToLowerInvariant()] = $sm.Groups[1].Value.ToUpperInvariant() }
}
foreach ($f in $dfcFiles) {
$e = Find-ZipEntry $z ('(?i)(^|/)' + [regex]::Escape($f) + '$')
if (-not $e) { throw ($f + ' not found in the Deep Fried Chicken zip') }
$to = Join-Safe $consumerDir $f
# Keep an existing cfg: it holds the user's settings. Only add it when absent.
if ($f -eq 'deep-fried-chicken.cfg' -and (Test-FileHere $to) -and -not $Force) { continue }
Expand-ZipEntry -Entry $e -To $to
}
}
finally { $z.Dispose() }
# Did the files survive? Defender removes the add-on within seconds of extraction.
Start-Sleep -Milliseconds 1500
foreach ($f in @('deep-fried-chicken.addon64', 'deep-fried-chicken-nvngx.dll')) {
$to = Join-Safe $consumerDir $f
if (-not (Test-FileHere $to)) { $failed = $to; break }
if ($sums.ContainsKey($f)) {
$h = Get-Sha256 $to
if ($h -and $h -ne $sums[$f]) { throw ($f + ' does not match the SHA-256 in the zip''s SHA256SUMS.txt') }
}
}
}
catch {
$msg = $_.Exception.Message
if ($msg -match '(?i)virus|potentially unwanted|0x800700E1') { $failed = Join-Safe $consumerDir 'deep-fried-chicken.addon64' }
else { Report -Status 'Fail' -Text 'Deep Fried Chicken could not be installed.' -Detail $msg; break }
}
if (-not $failed) {
$dfcOk = $true
$t = 'Deep Fried Chicken'
if ($dfcVersion) { $t += ' ' + $dfcVersion }
$t += ' installed (add-on, NGX bridge, cfg)'
if ($sums.Count -gt 0) { $t += ', SHA-256 verified' }
Report -Status 'Done' -Text ($t + '.') -Detail ('in ' + $consumerDir)
break
}
# Removed. Confirm with Defender's own log, then ask.
$det = Get-DefenderDetection $failed
$why = 'It removed ' + $failed
if ($det) { $why += ' (Defender log: ' + $det.InitialDetectionTime.ToString('HH:mm:ss') + ', threat id ' + $det.ThreatID + ')' } else { $why += ' right after extraction' }
$why += '.'
if ($attempt -ge 2) {
Report -Status 'Fail' -Text 'Deep Fried Chicken was removed again after the exclusion was added.' `
-Detail 'Check Windows Security > Protection history, restore the quarantined file, and confirm the exclusion covers the folder above.'
break
}
$ok = Request-DefenderExclusion -Paths @($gameDir, $script:Cache) -Because $why
if (-not $ok) { break }
Report -Status 'Info' -Text 'Retrying the Deep Fried Chicken extraction.'
}
}
}
elseif ($Consumer -eq 'RenoDX') {
Disable-Conflict -Path (Find-FileIn $consumerDir 'deep-fried-chicken.addon64') -Why 'exactly one neural consumer: you chose RenoDX'
Disable-Conflict -Path (Find-FileIn $consumerDir 'deep-fried-chicken-nvngx.dll') -Why 'Chicken''s private NGX bridge has no business beside the RenoDX add-on'
Disable-Conflict -Path (Find-OptiScalerDll $consumerDir) -Why 'OptiScaler captures every nvngx load in the process; beside the RenoDX add-on it takes its calls or doubles the neural pass'
if ($renoPath) {
$to = Join-Safe $consumerDir 'renodx-dlss5.addon64'
$attempt = 0
while ($attempt -lt 2) {
$attempt++
$failed = $null
try {
Copy-Tracked -From $renoPath -To $to
# Same antivirus story as Chicken: an NGX interposer looks like a hooking tool.
Start-Sleep -Milliseconds 1000
if (-not (Test-FileHere $to)) { $failed = $to }
}
catch {
$msg = $_.Exception.Message
if ($msg -match '(?i)virus|potentially unwanted|0x800700E1') { $failed = $to }
else { Report -Status 'Fail' -Text 'renodx-dlss5.addon64 could not be copied.' -Detail $msg; break }
}
if (-not $failed) {
# Krish's builds all report the same file version resource, so the generation is
# only readable as the standalone banner literal the add-on prints at run time
# (the feeder reads the same string).
$banner = $null
if (Get-BinaryMarker -Path $to -Pattern 'RenoDX DLSS5 Generic') {
$banner = Get-BinaryMarker -Path $to -Pattern "\x00(v\d+\.\d+)\x00"
}
$t = 'renodx-dlss5.addon64 installed'
if ($banner) { $t += ' (' + $banner + ')' }
Report -Status 'Done' -Text ($t + '.') -Detail ('in ' + $consumerWhere)
break
}
$det = Get-DefenderDetection $failed
$why = 'It removed ' + $failed
if ($det) { $why += ' (Defender log: ' + $det.InitialDetectionTime.ToString('HH:mm:ss') + ', threat id ' + $det.ThreatID + ')' } else { $why += ' right after it was copied in' }
$why += '.'
if ($attempt -ge 2) {
Report -Status 'Fail' -Text 'renodx-dlss5.addon64 was removed again after the exclusion was added.' `
-Detail 'Check Windows Security > Protection history, restore the quarantined file, and confirm the exclusion covers the folder above.'
break
}
if (-not (Request-DefenderExclusion -Paths @($gameDir, $script:Cache) -Because $why)) { break }
Report -Status 'Info' -Text 'Retrying the RenoDX add-on copy.'
}
}
}
else {
Disable-Conflict -Path (Find-FileIn $consumerDir 'deep-fried-chicken.addon64') -Why 'exactly one neural consumer: you chose OptiScaler, and it captures Chicken''s own nvngx loads'
Disable-Conflict -Path (Find-FileIn $consumerDir 'deep-fried-chicken-nvngx.dll') -Why 'Chicken''s private NGX bridge ends in nvngx.dll, and OptiScaler''s hook would hand it OptiScaler'
foreach ($f in (Find-FilesIn $consumerDir 'renodx-dlss5*.addon64')) {
Disable-Conflict -Path $f.FullName -Why 'exactly one neural consumer: you chose OptiScaler'
}
Disable-Conflict -Path (Find-FileIn $consumerDir 'alexs-toolkit.addon64') -Why 'a cascade over the RenoDX add-on; with OptiScaler there is nothing for it to attach to'
if ($optiPath) {
# The name OptiScaler is renamed to has to be one the process imports at start, and free.
# The 64-bit helper imports winmm.dll and version.dll; a game imports what it imports.
$optiName = $null
if ($is32) { $imports = @('winmm.dll', 'version.dll') }
else {
$imports = Get-PeImportNames $exePath
if ($null -eq $imports) { $imports = @() } else { $imports = @($imports) }
}
$candidates = @('winmm.dll', 'version.dll', 'dbghelp.dll', 'winhttp.dll', 'wininet.dll')
foreach ($n in $candidates) {
$existing = Find-FileIn $consumerDir $n
if ($existing -and -not (Get-BinaryMarker -Path $existing -Pattern 'OptiScaler\.ini')) { continue } # taken by something else
if ($imports.Count -gt 0 -and -not ($imports -contains $n)) { continue }
$optiName = $n
break
}
if (-not $optiName) {
Report -Status 'Fail' -Text 'No DLL name for OptiScaler could be chosen.' `
-Detail ('The process must import the name OptiScaler is renamed to, and that name must be free. Imports read from the exe: ' + $(if ($imports.Count -gt 0) { $imports -join ', ' } else { 'none' })) `
-Action ('Pick a name by hand: rename OptiScaler.dll in ' + $consumerDir + ' to a DLL the game loads (OptiScaler''s own setup_windows.bat lists the choices; never dxgi.dll, which is ReShade).')
}
else {
if ($imports.Count -eq 0 -and -not $is32) { Report -Status 'Info' -Text ('Could not read the game''s imports; OptiScaler goes in as ' + $optiName + '. If OptiScaler.log never appears, rename it to a DLL the game does load.') }
try {
$z = Open-Zip $optiPath
try {
$entries = @($z.Entries | Where-Object { $_.FullName -notmatch '[\\/]$' -and $_.Length -gt 0 })
if (-not ($entries | Where-Object { $_.FullName -match '(?i)(^|[\\/])OptiScaler\.dll$' })) { throw 'OptiScaler.dll not found in the zip' }
$iniKept = $false
foreach ($e in $entries) {
$rel = $e.FullName -replace '/', '\'
if ($rel -match '(?i)^OptiScaler\.dll$') { $rel = $optiName }
elseif ($rel -match '(?i)^OptiScaler\.ini$') {
# An existing ini holds the user's settings (OptiScaler's menu saves into it).
if ((Test-FileHere (Join-Safe $consumerDir 'OptiScaler.ini')) -and -not $Force) { $iniKept = $true; continue }
}
elseif ($rel -match '(?i)^(setup_windows\.bat|setup_linux\.sh|!! .*)$') { continue } # OptiScaler's own installer; this script does its job
Expand-ZipEntry -Entry $e -To (Join-Safe $consumerDir $rel)
}
}
finally { $z.Dispose() }
Start-Sleep -Milliseconds 1000
$dll = Join-Safe $consumerDir $optiName
if (-not (Test-FileHere $dll)) { throw ($optiName + ' vanished right after extraction -- Defender? Check Windows Security > Protection history.') }
$t = 'OptiScaler DLSS-NR installed as ' + $optiName
if ($iniKept) { $t += ' (existing OptiScaler.ini kept)' }
Report -Status 'Done' -Text ($t + '.') -Detail ('in ' + $consumerWhere + '; the neural forwarder nvngx.dll_dlssnr.dll and the OptiScaler\ runtime folder beside it')
# The keys a bare NGX client needs. [DlssNr] Enabled is off in every release (the fork
# refuses to ship it on); the exposure scan hooks resource creation on the feeder's
# device for a buffer the feed never offers; the spoofs are for non-NVIDIA GPUs. A key
# the user has already set by hand (anything but "auto") is left alone.
$ini = Join-Safe $consumerDir 'OptiScaler.ini'
if (Test-FileHere $ini) {
$text = [IO.File]::ReadAllText($ini)
$wanted = @(
@('DlssNr', 'Enabled', 'true'), @('DlssNr', 'ScanExposure', 'false'),
@('Upscalers', 'Dx12Upscaler', 'dlss'),
@('Log', 'LogToFile', 'true'), @('Log', 'LogLevel', '2'),
@('Spoofing', 'Dxgi', 'false'), @('Spoofing', 'StreamlineSpoofing', 'false'),
@('Inputs', 'EnableXeSSInputs', 'false'), @('Inputs', 'EnableFsr2Inputs', 'false'),
@('Inputs', 'EnableFsr3Inputs', 'false'), @('Inputs', 'EnableFfxInputs', 'false'),
@('Hotfix', 'CheckForUpdate', 'false'))
$kept = @()
foreach ($kv in $wanted) {
$cur = Get-IniKey -Text $text -Section $kv[0] -Key $kv[1]
if ($null -eq $cur -or $cur.Trim() -ieq 'auto' -or $cur.Trim() -ieq $kv[2]) { $text = Set-IniKey -Text $text -Section $kv[0] -Key $kv[1] -Value $kv[2] }
else { $kept += ('[' + $kv[0] + '] ' + $kv[1] + '=' + $cur.Trim()) }
}
[IO.File]::WriteAllText($ini, $text, (New-Object Text.UTF8Encoding $false))
$null = $script:Changed.Add($ini)
$d = 'Keys you had set by hand are left alone'
if ($kept.Count -gt 0) { $d += ': ' + ($kept -join ', ') }
Report -Status 'Done' -Text 'OptiScaler.ini set up for the feed: [DlssNr] Enabled=true, ScanExposure=false, Dx12Upscaler=dlss, logging on, spoofing off.' -Detail ($d + '.')
if ($kept | Where-Object { $_ -match '(?i)^\[DlssNr\] Enabled=' }) {
Report -Status 'Warn' -Text 'OptiScaler.ini has [DlssNr] Enabled set to something other than true -- the neural pass is off until you turn it on in OptiScaler''s menu (Insert).'
}
}
else {
Report -Status 'Warn' -Text 'OptiScaler.ini is missing after the extraction.' -Action 'Extract OptiScaler.ini from the zip and set Enabled=true under [DlssNr].'
}
}
catch {
Report -Status 'Fail' -Text 'OptiScaler DLSS-NR could not be installed.' -Detail $_.Exception.Message
}
}
}
}
# NVIDIA runtimes
foreach ($pair in @(@{ N = 'nvngx_dlssnr.dll'; P = $dlssNrPath }, @{ N = 'nvngx_dlss.dll'; P = $dlssPath })) {
$to = Join-Safe $consumerDir $pair.N
if (-not $pair.P) { continue }
if ((Test-FileHere $to) -and -not $Force) {
$have = Get-Sha256 $to
$new = Get-Sha256 $pair.P
if ($have -and $new -and $have -eq $new) { Report -Status 'Ok' -Text ($pair.N + ' already present (identical).'); continue }
$vHave = Get-FileVersionSafe $to; $vNew = Get-FileVersionSafe $pair.P
Report -Status 'Info' -Text ($pair.N + ': replacing ' + $vHave + ' with ' + $vNew + '.')
}
try { Copy-Tracked -From $pair.P -To $to; Report -Status 'Done' -Text ($pair.N + ' ' + (Get-FileVersionSafe $to) + ' installed.') -Detail ('in ' + $consumerWhere) }
catch { Report -Status 'Fail' -Text ($pair.N + ' could not be copied.') -Detail $_.Exception.Message }
}
# ---------------------------------------------------------------------------------------
# 9. The d3dcompiler_47.dll trap
# ---------------------------------------------------------------------------------------
Write-Section 'd3dcompiler_47.dll'
$dcDirs = @($gameDir)
if ($is32) { $dcDirs += $hostDir }
$dcFound = $false
foreach ($d in $dcDirs) {
$p = Find-FileIn $d 'd3dcompiler_47.dll'
if (-not $p) { continue }
$dcFound = $true
$v = Get-FileVersionSafe $p
if ($v -match '^6\.3\.') {
try {
Rename-Item -LiteralPath $p -NewName 'd3dcompiler_47.dll.disabled-by-installer' -Force
Report -Status 'Done' -Text ('Windows 8.1-era d3dcompiler_47.dll (' + $v + ') renamed to .disabled-by-installer.') -Detail ($p + "`nIt cannot compile the cs_5_1 neural pass; System32's copy is used instead, which is fine.")
}
catch { Report -Status 'Fail' -Text ('Old d3dcompiler_47.dll (' + $v + ') could not be renamed.') -Detail $p -Manual ('Rename or delete ' + $p) }
}
else { Report -Status 'Ok' -Text ('d3dcompiler_47.dll ' + $v + ' next to the exe is new enough.') -Detail $p }
}
if (-not $dcFound) { Report -Status 'Ok' -Text 'No local d3dcompiler_47.dll; System32''s copy is used.' }
# ---------------------------------------------------------------------------------------
# 10. ReShade.ini and ReShadePreset.ini
# ---------------------------------------------------------------------------------------
Write-Section 'Configuration files'
$iniTemplate = @"
[ADDON]
AddonPath=.\
[DEPTH]
DepthCopyBeforeClears=0
[GENERAL]
EffectSearchPaths=.\reshade-shaders\Shaders\**
TextureSearchPaths=.\reshade-shaders\Textures\**
IntermediateCachePath=
NoDebugInfo=1
NoEffectCache=0
NoReloadOnInit=0
PerformanceMode=0
PreprocessorDefinitions=
PresetPath=.\ReShadePreset.ini
PresetShortcutKeys=
PresetShortcutPaths=
PresetTransitionDuration=1000
SkipLoadingDisabledEffects=0
StartupPresetPath=
[INPUT]
ForceShortcutModifiers=1
InputProcessing=2
KeyEffects=222,0,0,0
KeyOverlay=36,0,0,0
KeyReload=0,0,0,0
KeyScreenshot=220,0,0,0
[OVERLAY]
TutorialProgress=4
"@
$iniPath = Join-Safe $gameDir 'ReShade.ini'
$depthClears = if ($isDgV) { '1' } else { '0' } # UE3-era D3D9 games clear depth mid-frame
$existingIni = Read-TextSafe $iniPath
if ($existingIni -and -not $Force) {
$new = $existingIni
$new = Set-IniKey -Text $new -Section 'ADDON' -Key 'AddonPath' -Value '.\'
$new = Set-IniKey -Text $new -Section 'GENERAL' -Key 'EffectSearchPaths' -Value '.\reshade-shaders\Shaders\**'
$new = Set-IniKey -Text $new -Section 'GENERAL' -Key 'TextureSearchPaths' -Value '.\reshade-shaders\Textures\**'
if (-not (Get-IniKey -Text $new -Section 'GENERAL' -Key 'PresetPath')) { $new = Set-IniKey -Text $new -Section 'GENERAL' -Key 'PresetPath' -Value '.\ReShadePreset.ini' }
if ($isDgV) { $new = Set-IniKey -Text $new -Section 'DEPTH' -Key 'DepthCopyBeforeClears' -Value $depthClears }
if ($new.TrimEnd() -ne $existingIni.TrimEnd()) {
$bak = Backup-File $iniPath
Write-TextTracked -Path $iniPath -Text $new
Report -Status 'Done' -Text 'ReShade.ini: existing file kept, [ADDON]/[GENERAL] keys merged in.' -Detail ('backup: ' + $bak)
}
else { Report -Status 'Ok' -Text 'ReShade.ini already has the required keys.' }
}
else {
$bak = $null
if ($existingIni) { $bak = Backup-File $iniPath }
$t = $iniTemplate -replace 'DepthCopyBeforeClears=0', ('DepthCopyBeforeClears=' + $depthClears)
Write-TextTracked -Path $iniPath -Text $t
Report -Status 'Done' -Text 'ReShade.ini written from the template.' -Detail $(if ($bak) { 'previous file backed up: ' + $bak } else { '' })
}
# ReShadePreset.ini: the provider must be enabled and run BEFORE DLSS5_Feed, and the
# provider choice lives in the effect's own section (not in ReShade.ini).
$presetPath = Join-Safe $gameDir 'ReShadePreset.ini'
$presetPathFromIni = Get-IniKey -Text (Read-TextSafe $iniPath) -Section 'GENERAL' -Key 'PresetPath'
if ($presetPathFromIni -and -not [IO.Path]::IsPathRooted($presetPathFromIni)) { $presetPath = [IO.Path]::GetFullPath((Join-Safe $gameDir $presetPathFromIni)) }
elseif ($presetPathFromIni) { $presetPath = $presetPathFromIni }
$feedTechnique = 'DLSS5_Feed@DLSS5_Feed.fx'
$existingPreset = Read-TextSafe $presetPath
if ($existingPreset -and -not $Force) {
$new = $existingPreset
foreach ($key in @('Techniques', 'TechniqueSorting')) {
$cur = Get-IniKey -Text $new -Section '' -Key $key
$list = @()
if ($cur) { $list = @($cur -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
$list = @($list | Where-Object { $_ -ine $providerTechnique -and $_ -ine $feedTechnique -and $_ -notmatch '(?i)^DLSS5_Feed_Debug@' })
$list += $providerTechnique
$list += $feedTechnique
if ($key -eq 'TechniqueSorting') { $list += 'DLSS5_Feed_Debug@DLSS5_Feed.fx' }
$new = Set-IniKey -Text $new -Section '' -Key $key -Value ($list -join ',')
}
$defs = Get-IniKey -Text $new -Section 'DLSS5_Feed.fx' -Key 'PreprocessorDefinitions'
if ($defs) {
$parts = @($defs -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch '(?i)^DLSS5_MV_PROVIDER\s*=' })
$parts += ('DLSS5_MV_PROVIDER=' + $MvProvider)
$defs = $parts -join ','
}
else { $defs = 'DLSS5_MV_PROVIDER=' + $MvProvider }
$new = Set-IniKey -Text $new -Section 'DLSS5_Feed.fx' -Key 'PreprocessorDefinitions' -Value $defs
if ($new.TrimEnd() -ne $existingPreset.TrimEnd()) {
$bak = Backup-File $presetPath
Write-TextTracked -Path $presetPath -Text $new
Report -Status 'Done' -Text ('ReShadePreset.ini: existing preset kept; ' + $providerTechnique.Split('@')[0] + ' and DLSS5_Feed enabled in that order, DLSS5_MV_PROVIDER=' + $MvProvider + '.') -Detail ('backup: ' + $bak)
}
else { Report -Status 'Ok' -Text 'ReShadePreset.ini already correct.' }
}
else {
$bak = $null
if ($existingPreset) { $bak = Backup-File $presetPath }
$preset = 'Techniques=' + $providerTechnique + ',' + $feedTechnique + "`r`n" +
'TechniqueSorting=' + $providerTechnique + ',' + $feedTechnique + ',DLSS5_Feed_Debug@DLSS5_Feed.fx' + "`r`n`r`n" +
'[DLSS5_Feed.fx]' + "`r`n" + 'DEBUG_VIEW=0' + "`r`n" + 'MV_SCALE=1.000000' + "`r`n" + 'MV_SIGN=1.000000,1.000000' + "`r`n" +
'PreprocessorDefinitions=DLSS5_MV_PROVIDER=' + $MvProvider + "`r`n"
Write-TextTracked -Path $presetPath -Text $preset
Report -Status 'Done' -Text ('ReShadePreset.ini written: ' + $providerTechnique.Split('@')[0] + ' then DLSS5_Feed, DLSS5_MV_PROVIDER=' + $MvProvider + '.') -Detail $(if ($bak) { 'previous file backed up: ' + $bak } else { '' })
}
# A DLSS5_MV_PROVIDER in ReShade.ini does nothing and only confuses; say so.
$riNow = Read-TextSafe $iniPath
if ($riNow -and $riNow -match '(?i)DLSS5_MV_PROVIDER') {
Report -Status 'Warn' -Text 'ReShade.ini also mentions DLSS5_MV_PROVIDER; that is a per-effect key and is ignored there.' -Detail 'Only the [DLSS5_Feed.fx] section of ReShadePreset.ini counts. Harmless, but remove it to avoid confusion.'
}
# host64\ReShade.ini: minimal, and deliberately without AddonPath (the host loads its own folder's add-ons).
if ($is32) {
$hostIni = Join-Safe $hostDir 'ReShade.ini'
if (-not (Test-FileHere $hostIni)) {
Write-TextTracked -Path $hostIni -Text ("[GENERAL]`r`nEffectSearchPaths=.\`r`nTextureSearchPaths=.\`r`n")
Report -Status 'Done' -Text 'host64\ReShade.ini written (minimal).'
}
else { Report -Status 'Ok' -Text 'host64\ReShade.ini already present, kept.' }
}
# ---------------------------------------------------------------------------------------
# 11. Mark-of-the-web off everything we wrote
# ---------------------------------------------------------------------------------------
$unblocked = 0
foreach ($p in $script:Changed) {
try { if (Test-FileHere $p) { Unblock-File -LiteralPath $p -ErrorAction SilentlyContinue; $unblocked++ } } catch { }
}
# ---------------------------------------------------------------------------------------
# Summary, verification, next steps
# ---------------------------------------------------------------------------------------
Write-Host ''
Write-Chunk (' ' + ([string][char]0x2500) * 68) 'Green'
Write-Chunk ' ' $null -NoNewline
Write-Chunk ($script:CountDone.ToString() + ' installed') 'Green' -NoNewline
Write-Chunk ' ' $null -NoNewline
Write-Chunk ($script:CountWarn.ToString() + ' warning' + $(if ($script:CountWarn -eq 1) { '' } else { 's' })) 'Yellow' -NoNewline
Write-Chunk ' ' $null -NoNewline
Write-Chunk ($script:CountFail.ToString() + ' failure' + $(if ($script:CountFail -eq 1) { '' } else { 's' })) 'Red'
if ($script:Manual.Count -gt 0) {
Write-Host ''
Write-Chunk ' Still to do by hand:' 'White'
$i = 1
foreach ($a in $script:Manual) {
$first = $true
foreach ($line in ($a -split "`n")) {
if (-not $line.Trim()) { continue }
if ($first) { Write-Chunk (' ' + $i + '. ' + $line.Trim()) 'DarkYellow'; $first = $false }
else { Write-Chunk (' ' + $line.Trim()) 'DarkYellow' }
}
$i++
}
}
if (-not $NoVerify -and $verifyScript -and (Test-FileHere $verifyScript)) {
Write-Host ''
Write-Chunk ' Running Verify-DLSS5Feeder.ps1 on the result ...' 'White'
try {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $verifyScript -GamePath $exePath -NoPause
$vc = $LASTEXITCODE
if ($vc -ne 0) { Report -Status 'Warn' -Text 'The verifier reports failures; see above.' }
}
catch { Report -Status 'Warn' -Text 'Could not run the verifier.' -Detail $_.Exception.Message }
}
Write-Host ''
Write-Chunk ' Next, in the game:' 'White'
$steps = @()
$steps += 'Launch it. Press Home for the ReShade overlay and check there are no compile errors.'
$steps += ('Both techniques should already be ticked, in this order: ' + $providerTechnique.Split('@')[0] + ' above DLSS 5 Feed.')
if ($Consumer -eq 'DFC') {
if ($is32) { $steps += 'Open the ReShade overlay > Add-ons > DLSS 5 Feed and press "Show the DLSS 5 panel in-game" to reach the Deep Fried Chicken tab (it lives in the host64 helper).' }
else { $steps += 'Turn on neural rendering in the Deep Fried Chicken tab of the overlay.' }
$steps += 'On its first armed run Chicken registers itself for early load in ReShade.ini and asks for one more full restart. Do that restart.'
}
elseif ($Consumer -eq 'RenoDX') {
$steps += 'Turn on neural rendering in the DLSS 5 Neural Rendering add-on panel.'
}
else {
if ($is32) { $steps += 'OptiScaler''s menu lives in the host64 helper: open the ReShade overlay > Add-ons > DLSS 5 Feed, press "Show the DLSS 5 panel in-game", then press Insert. "DLSS Neural Rendering" is its last section; the pass is already switched on.' }
else { $steps += 'Press Insert for OptiScaler''s menu; "DLSS Neural Rendering" is its last section, and the pass is already switched on.' }
$steps += 'dlss5-feed.log (host64\dlss5-feed-host.log for a 32-bit game) should say "NGX calls are routed through OptiScaler DLSS-NR" and, after the first frame, "neural model (feature 18) loaded".'
}
$steps += 'Turn the game''s MSAA/SSAA off.'
if ($isDgV -and -not $DgVoodooWatermark) { $steps += 'dgVoodoo''s watermark is off. If nothing seems to happen, re-run with -DgVoodooWatermark to confirm dgVoodoo is active at all.' }
if ($isVulkan) { $steps += 'If dlss5-feed.log says the Vulkan interop entry points are missing, launch through layer\run-with-feed-layer' + $(if ($is32) { '32' } else { '' }) + '.bat "<path to exe>" instead.' }
$steps += 'Then check dlss5-feed.log next to the exe for "feature ready", "frame N delivered", and re-run Verify-DLSS5Feeder.ps1 for a runtime verdict.'
$i = 1
foreach ($s in $steps) { Write-Chunk (' ' + $i + '. ' + $s) 'Gray'; $i++ }
Write-Host ''
if ($script:CountFail -gt 0) { Exit-Installer 1 }
Exit-Installer 0