ASI add-on for existing OptiScaler installs

Add an optional D4R_INSTALL_MODE=asi release (d4r-<version>-asi.zip)
holding only d4r.asi and a companion d4r/ folder, to drop into an
existing OptiScaler plugin directory.

- Shim: export InitializeASI. It identifies OptiScaler by its PE export
  name to find the active INI, then launches d4r-setup.exe out of
  process (no GPU or network work under the loader lock). The .asi
  build loads its runtime and config from the companion d4r/ folder.

- tools/d4r_asi_setup.cpp: native helper. It waits for the game to
  exit, installs the D3D12 wrapper and the private d4r12core.dll beside
  the EXE, and merges the NGX route into the loader's INI (including
  GE-Proton's prefix-local INI) with backups. Unchanged launches do
  nothing. It skips Proton helpers and background utilities and runs
  setup for one plugin install at a time.

- scripts/private_d3d12_core.py: points vkd3d's d3d12.dll forwarder at
  d4r12core.dll without moving code addresses.

- install.sh runs from inside the ASI's d4r/ as well as the proxy
  release root. d4r-check.sh checks ASI installs (loader, plugin path,
  LoadAsiPlugins, HookOriginalNvngxOnly, Wine drive paths).

- Packaging, build script, docs, ASI_README.txt and tests.
This commit is contained in:
ayo committed 2026-10-12 17:36:01 -04:00
1 parent 85175e0b27
commit d5c8be25c8
16 files changed
+1086 -34

No files matched your search

+19
View File
@@ -126,6 +126,25 @@ When updating, replace `d3d12.dll` and `d3d12core.dll` together with `d4r/nvngx.
ROCm does not need to be installed: the zip includes its runtime (from AMD's Ubuntu 22.04 packages, which run under Steam's container runtime on any distribution). The downloaded NVIDIA files and the bundled kernels built from NVIDIA's code are not covered by this repository's license (see [NOTICE](NOTICE)). The Proton prefix and the system are not changed. [packaging/D4R_README.txt](packaging/D4R_README.txt) is the full guide that ships in the zip; `scripts/package_release.sh` builds the zip (see [docs/building.md](docs/building.md#7-package-a-release)).
### Existing OptiScaler: ASI add-on
The optional `d4r-<version>-asi.zip` contains only `d4r.asi` and a companion
`d4r/` folder. Drop both into your existing OptiScaler plugin folder, then run
`bash install.sh` inside that `d4r/` folder once for NVIDIA's DLLs. Keep the Steam
launch options above and ensure `[Plugins] LoadAsiPlugins=true` is already enabled.
On the first game launch, the ASI starts its native setup helper. Close the game
at the setup message; the helper installs the D3D12 wrapper and private core beside the EXE
and configures OptiScaler after the game exits, with backups. Launch again when
the ready message appears. Unrelated settings, other plugins and the existing
OptiScaler loader are preserved. The rest of the runtime stays beside the ASI
in its companion folder. No Python or configuration command is needed.
Use OptiScaler 0.9.4 through its game-local proxy or GE-Proton's built-in loader;
setup finds the active loader's INI even when it lives in the prefix. OptiScaler's
`nvngx.dll` mode does not load ASIs. See [the ASI installation guide](packaging/ASI_README.txt)
for custom plugin paths, updating and removal. ASI loading still needs game testing.
## Requirements (building from source)
- Linux with an AMD RDNA3 or RDNA4 GPU. The native kernels use gfx11 or gfx12 WMMA. `scripts/package_release.sh` builds network kernels for gfx1100–gfx1103 and gfx1200–gfx1201 by default; `D4R_GPU_ARCH` selects one target for a standalone `kernels/build.sh` invocation. Runtime support outside the RX 7700 XT remains unverified on hardware (see [GPU support](#gpu-support)).
+14
View File
@@ -137,6 +137,20 @@ D4R_ZLUDA_DIR=~/.cache/d4r-zluda-current D4R_VKD3D_DIR=~/.cache/d4r-vkd3d-d4r D4
scripts/package_release.sh # -> dist/d4r-<version>.zip
```
Set `D4R_INSTALL_MODE=asi` to build `d4r-<version>-asi.zip` for an existing
game-local OptiScaler installation. Its only top-level entries are `d4r.asi`
and `d4r/`, ready to extract into the configured plugin folder. It omits
OptiScaler's DLL/INI and does not require `D4R_OPTISCALER`. The runtime and
`install.sh` are inside `d4r/`; the NVIDIA installer also accepts its original
proxy-package layout. The native `d4r/d4r-setup.exe` helper installs the supplied
D3D12 pair beside the game EXE and merges the NGX route into the active loader's
INI (including GE-Proton's prefix-local INI) with backups, after
waiting for the invoking game process to exit. Subsequent unchanged launches do
nothing. `InitializeASI` launches setup without GPU work under the loader lock.
The build writes `build/d4r.asi` and `build/d4r-setup.exe`.
See [ASI_README.txt](../packaging/ASI_README.txt).
The script:
- builds the shim, the bridge and both fast and accuracy network-layer kernels for gfx1100–gfx1103 and gfx1200–gfx1201 by default (`D4R_GPU_ARCHS` can select fewer targets); each gfx12 target also gets an `<arch>-fp8` variant;
- writes the kernel manifest from the DLLs you list (it records hashes of their PTX, nothing else);
+9 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Release installer: copy this beside D4R_README.txt and the extracted d4r/ folder.
# NVIDIA installer: beside the proxy release's d4r/ or inside the ASI's d4r/.
set -euo pipefail
DLSS_URL=https://raw.githubusercontent.com/NVIDIA/DLSS/a291cc7d2cc642a51566f3dfd5376f635cd1b284/lib/Windows_x86_64/rel/nvngx_dlss.dll
DLSS_SHA256=be6e434a94ca32499515eb62ca0e6c274526055d568d0426e4c652dcdfb6ee6e
@@ -18,12 +18,14 @@ if [[ ${1:-} == --help || ${1:-} == -h ]]; then
fi
[[ $# -le 1 ]] || fail 'Usage: bash install.sh [extracted-release-directory]'
ROOT=$(cd -- "${1:-$(dirname -- "${BASH_SOURCE[0]}")}" && pwd)
[[ -f "$ROOT/d4r/nvngx.dll" && -f "$ROOT/d4r/d4r.ini" ]] || fail "Extract the release ZIP first; no d4r runtime found in $ROOT"
RUNTIME="$ROOT/d4r"
[[ ! -f "$ROOT/d4r.ini" ]] || RUNTIME="$ROOT"
[[ -f "$RUNTIME/nvngx.dll" && -f "$RUNTIME/d4r.ini" ]] || fail "Extract the release ZIP first; no d4r runtime found in $ROOT"
command -v sha256sum >/dev/null || fail 'Missing sha256sum (install coreutils).'
valid() { [[ -f "$1" ]] && [[ $(sha256sum -- "$1" | cut -d' ' -f1) == "$2" ]]; }
need_dlss=1; need_ngx=1
valid "$ROOT/d4r/nvngx_dlss.dll" "$DLSS_SHA256" && need_dlss=0
valid "$ROOT/d4r/ngx/_nvngx.dll" "$NGX_SHA256" && need_ngx=0
valid "$RUNTIME/nvngx_dlss.dll" "$DLSS_SHA256" && need_dlss=0
valid "$RUNTIME/ngx/_nvngx.dll" "$NGX_SHA256" && need_ngx=0
if (( need_dlss == 0 && need_ngx == 0 )); then
echo 'NVIDIA DLLs are already installed and verified.'
exit 0
@@ -79,6 +81,6 @@ place() {
STAGED=''
printf 'Installed: %s\n' "$dst"
}
(( need_dlss == 0 )) || place "$TMP/nvngx_dlss.dll" "$ROOT/d4r/nvngx_dlss.dll"
(( need_ngx == 0 )) || place "$TMP/ngx/_nvngx.dll" "$ROOT/d4r/ngx/_nvngx.dll"
echo 'NVIDIA DLLs installed and verified. Follow D4R_README.txt for Steam launch options.'
(( need_dlss == 0 )) || place "$TMP/nvngx_dlss.dll" "$RUNTIME/nvngx_dlss.dll"
(( need_ngx == 0 )) || place "$TMP/ngx/_nvngx.dll" "$RUNTIME/ngx/_nvngx.dll"
echo 'NVIDIA DLLs installed and verified. Follow the included README for Steam launch options.'
+65
View File
@@ -0,0 +1,65 @@
d4r ASI add-on for Linux / Proton
================================
1. Drop d4r.asi and the d4r folder into OptiScaler's existing plugins folder.
For example: <game>/plugins/d4r.asi and <game>/plugins/d4r/.
Custom plugin folders work too: keep the ASI and its companion folder together.
2. Run bash install.sh inside that d4r folder once to download and verify the
official NVIDIA DLSS/NGX DLLs. No Python or configuration command is needed.
3. Launch the game. At the first setup message, close the game. Setup installs
the bundled D3D12 wrapper and private core and updates OptiScaler's settings after the game exits,
with backups. Wait for the ready message, then launch the game again.
Requires OptiScaler 0.9.4 through a game-local proxy (dxgi.dll, winmm.dll, etc.)
or GE-Proton's built-in loader, with [Plugins] LoadAsiPlugins=true already enabled. A plugin cannot enable
its own loader before it has been loaded. OptiScaler's nvngx.dll mode does not
load ASIs. OptiScaler installed as d3d12.dll conflicts with the patched D3D12 pair;
use another supported proxy name first. GE-Proton's built-in OptiScaler also
works with its prefix-local plugins folder. Setup edits OptiScaler.ini beside
the active loader, including windows/system32/umu/OptiScaler.ini. Explicit
PROTON_OPTISCALER_CONFIG values are applied again by Proton at launch; keep
those consistent with d4r's route if you use that environment option.
The runtime, kernels, NVIDIA files, logs and d4r.ini stay inside plugins/d4r/.
The patched d3d12.dll and d4r12core.dll are kept there as an installation payload;
the helper installs d3d12.dll and d4r12core.dll beside the main game EXE.
The game's original D3D12Core.dll remains untouched. d4r's forwarder uses the
private core name to avoid Agility SDK loader interception.
Setup uses OptiScaler's regular Windows library hooks and disables its Agility
SDK upgrade so the supplied D3D12 implementation stays selected.
It waits for the running game to exit before changing either DLL or the INI.
Changed files get unique .d4r-backup.* copies. Unrelated settings, your existing
OptiScaler proxy and other plugins are preserved. Matching files are skipped on
later launches. Background utilities such as xalia.exe, launchers and crash
reporters do not run setup; one helper per plugin installation owns the setup. The helper never changes the Proton installation or system packages.
install.sh requires Bash, curl, sha256sum and 7zz, 7z or 7za; no sudo is needed.
The NVIDIA driver archive (~915 MiB on first use) is extracted without being
executed, and temporary downloads are removed afterward. For a nonvidia archive,
supply the NVIDIA DLLs inside this d4r folder before launching instead; full
archives already include them. Internet access and NVIDIA's license terms apply:
https://github.com/NVIDIA/DLSS/blob/v310.7.0/LICENSE.txt
https://www.nvidia.com/en-us/drivers/nvidia-license/
Use GE-Proton 11 with these Steam launch options:
PROTON_FORCE_NVAPI=1 DXVK_NVAPI_GPU_ARCH=AD100 %command%
Keep PROTON_USE_OPTISCALER for Proton's built-in OptiScaler. Remove it only when
using a separate game-local OptiScaler install.
Select DLSS in OptiScaler; choose your model in plugins/d4r/d4r.ini.
Errors and completion status are recorded in d4r/installation-status.txt.
Setup retries on the next launch if it fails. Moving the game/plugin folder
refreshes the paths automatically after another close-and-relaunch cycle.
The ASI is the NGX backend itself; NvngxPath selects it, so only one backend is
active. HookOriginalNvngxOnly=true keeps the internal NVIDIA core load separate.
CUDA/GPU initialization stays lazy until NGX Init. Installation and loader tests
do not prove successful rendering in a game; native Vulkan remains experimental.
Uninstall: close the game and setup helper, restore your OptiScaler.ini backup beside its loader and
the D3D12 backups beside the EXE (or delete this setup's D3D12 files if none existed
before), then remove only d4r.asi and this d4r folder from the plugins directory.
Keep your existing OptiScaler loader and other plugins.
d4r is GPL-3.0-only. Third-party notices and provenance are in d4r/licenses/ and
d4r/source/SOURCES.txt. NVIDIA files and derived kernels have separate terms.
https://github.com/countervolts/d4r
+4
View File
@@ -39,6 +39,10 @@ What you need
Install
-------
For an existing game-local OptiScaler installation, the separate -asi.zip add-on
uses its ASI loader and keeps its proxy DLL and unrelated INI settings. Follow
that archive's d4r/README.txt: setup runs automatically when the ASI loads.
1. Extract everything in this zip into the folder that holds the game's main .exe. For Unreal
Engine games that is <game>/<Project>/Binaries/Win64/, next to <Project>-Win64-Shipping.exe.
You get dxgi.dll, OptiScaler.ini, d3d12.dll, d3d12core.dll, this file and a d4r folder.
+4
View File
@@ -1,6 +1,10 @@
<!-- Modified in this fork for CUDA Ray Reconstruction support and validation (2026). -->
# Unreleased
- ASI setup ignores Proton accessibility helpers and other background utilities, and serializes setup per plugin installation. Its D3D12 configuration uses regular Windows library hooks and disables Agility SDK upgrades. Game-specific startup remains a separate runtime check.
- ASI setup now finds the INI beside the active OptiScaler loader, including GE-Proton's `system32/umu` installation. Missing-file errors identify the actual missing path, and INI updates are staged beside their destination even when the game and prefix are on different filesystems.
- Optional OptiScaler ASI add-on (`D4R_INSTALL_MODE=asi`): drop `d4r.asi` and its companion `d4r/` folder into the existing plugin directory, run its `install.sh` once for NVIDIA's DLLs, then launch. The ASI installs the supplied D3D12 wrapper/private core beside the EXE and configures NGX after the game closes, with backups. The runtime stays in the plugin folder; no Python is needed. Game rendering still requires validation.
- Native engine, preset L (DLSS 4.5, render preset 12): the engine now runs L ([Engine] Enabled, models in `engine/l` and `engine/l-ldr`, built by `engine/compile_m.py` from a capture of preset L; `scripts/install_d4r_runtime.sh` stages `D4R_ENGINE_MODEL_L` / `D4R_ENGINE_MODEL_L_LDR`). Every stage, `d4r-m` on captured frames and the D3D12 harness are byte-identical to the CUDA backend running NVIDIA's translated kernels, apart from isolated single-frame knife edges (96-122 dB). 1280x720 -> 3840x2160 takes 3.8 ms on an RX 9070 XT. Display-resolution motion vectors keep the CUDA backend, as for M. Details: `engine/L_NOTES.md`.
- Native engine fixes found with L, which also apply to M (rebuild M packages with the current `compile_m.py`): the encoders' patch merge is clamped to ±2π as NVIDIA's kernels do; the automatic exposure is measured on NGX's grid at Ultra Performance (half the render size, at least 256, when the output is more than twice the render size; half the output was used, which changed every value downstream); the downsample's filter weights are exact (glslang rounded them toward zero: 92% of the output values were identical, now all); and the network uses the summation order of NVIDIA's kernels as ZLUDA runs them. M is now byte-identical to that CUDA backend in the harness too, where it measured 36-41 dB.
- `scripts/check_redistributable.py` refuses NVIDIA's binaries, NGX SDK headers, PTX and code objects, d4r's PTX translations and the engine's model data. `scripts/package_release.sh` runs it on every release before zipping, and `git config core.hooksPath scripts/git-hooks` runs it before each commit.
+83 -19
View File
@@ -1,20 +1,60 @@
#!/bin/sh
# Checks an d4r install: run it from the game folder (the one holding the game's .exe and d4r/),
# or give that folder as the argument. It only reads files and prints what it finds.
# usage: sh d4r/d4r-check.sh [GAME_FOLDER] [ROCM_DIR]
# usage: sh plugins/d4r/d4r-check.sh GAME_FOLDER [ROCM_DIR] (or sh d4r/d4r-check.sh for proxy installs)
GAME="${1:-.}"
D4R="$GAME/d4r"
INI="${D4R_OPTISCALER_INI:-$GAME/OptiScaler.ini}"
script_dir=$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)
if [ -z "${D4R_OPTISCALER_INI:-}" ] && [ ! -f "$INI" ] && [ "${script_dir##*/}" = d4r ]; then
loader_dir=$(cd "$script_dir/../.." 2>/dev/null && pwd -P)
[ ! -f "$loader_dir/OptiScaler.ini" ] || INI="$loader_dir/OptiScaler.ini"
fi
wine_file() {
windows_path=$(printf '%s' "$1" | tr '\\' '/')
case "$windows_path" in
[zZ]:*) printf '%s\n' "${windows_path#??}" ;;
[a-zA-Z]:*)
case "$INI" in
*/drive_c/*)
prefix="${INI%/drive_c/*}"
drive=$(printf '%s' "${windows_path%%:*}" | tr '[:upper:]' '[:lower:]')
drive_root=$(readlink -f "$prefix/dosdevices/$drive:" 2>/dev/null)
if [ -n "$drive_root" ]; then printf '%s%s\n' "$drive_root" "${windows_path#??}"; else return 1; fi ;;
*) return 1 ;;
esac ;;
*) printf '%s/%s\n' "$GAME" "$windows_path" ;;
esac
}
problems=0
ok() { printf ' ok %s\n' "$1"; }
bad() { printf ' MISSING %s\n' "$1"; problems=$((problems + 1)); }
note() { printf ' note %s\n' "$1"; }
misconfigured() { printf ' CONFIG %s\n' "$1"; problems=$((problems + 1)); }
ini_value() {
awk -v section="$1" -v key="$2" '
{ sub(/\r$/, ""); line = $0; sub(/^[ \t]+/, "", line); sub(/[ \t]+$/, "", line) }
line ~ /^\[/ { active = (tolower(line) == "[" section "]"); next }
active && tolower(line) ~ "^" key "[ \t]*=" {
count++; sub(/^[^=]*=[ \t]*/, "", line); value = line
}
END { if (count != 1) exit 1; print value }
' "$INI"
}
nvngx_path=$(ini_value libraries nvngxpath 2>/dev/null) && unique_path=yes || unique_path=no
normalized_path=$(printf '%s' "$nvngx_path" | tr '\\' '/' | tr '[:upper:]' '[:lower:]')
asi_backend=no
case "$normalized_path" in */d4r.asi) asi_backend=yes ;; esac
if [ "$asi_backend" = yes ]; then
asi_file=$(wine_file "$nvngx_path") || asi_file=
D4R="$(dirname "$asi_file")/d4r"
fi
# ROCm: the argument, else d4r.ini's RocmDir, else the bundled d4r/rocm, else D4R_ROCM_DIR, else /opt/rocm
ini_rocm=$(sed -n 's/^[[:space:]]*RocmDir[[:space:]]*=[[:space:]]*\([^;#]*\).*/\1/p' "$D4R/d4r.ini" 2>/dev/null | tail -1 | sed 's/[[:space:]]*$//')
[ "$ini_rocm" = auto ] && ini_rocm=
case "$ini_rocm" in "~/"*) ini_rocm="$HOME/${ini_rocm#\~/}" ;; esac
[ -z "$ini_rocm" ] && [ -d "$D4R/rocm/lib" ] && ini_rocm="$D4R/rocm"
ROCM="${2:-${ini_rocm:-${D4R_ROCM_DIR:-/opt/rocm}}}"
problems=0
ok() { printf ' ok %s\n' "$1"; }
bad() { printf ' MISSING %s\n' "$1"; problems=$((problems + 1)); }
note() { printf ' note %s\n' "$1"; }
misconfigured() { printf ' CONFIG %s\n' "$1"; problems=$((problems + 1)); }
external_backend=$(awk '
{ sub(/\r$/, ""); line = $0; sub(/^[ \t]+/, "", line); sub(/[ \t]+$/, "", line) }
line ~ /^\[/ { dlss = (tolower(line) == "[dlss]"); next }
@@ -22,31 +62,55 @@ external_backend=$(awk '
count++; sub(/^[^=]*=[ \t]*/, "", line); value = tolower(line)
}
END { if (count == 1 && value == "true") print "true" }
' "$GAME/OptiScaler.ini" 2>/dev/null)
' "$INI" 2>/dev/null)
printf 'd4r install in %s\n' "$(cd "$GAME" 2>/dev/null && pwd || echo "$GAME")"
[ -d "$D4R" ] || { printf 'no d4r folder here; run this from the folder with the game .exe\n'; exit 1; }
ls "$GAME"/*.exe >/dev/null 2>&1 && ok "game executable next to d4r/" || note "no .exe next to d4r/ (it must be the game's main executable folder)"
for f in dxgi.dll OptiScaler.ini d3d12.dll d3d12core.dll d4r/nvngx.dll d4r/nvcuda.dll d4r/zluda/libcuda.so d4r/d4r.ini; do
if [ "$asi_backend" = yes ]; then
loader=no
loader_dir=$(dirname "$INI")
for folder in "$GAME" "$loader_dir"; do
for f in dxgi.dll winmm.dll version.dll winhttp.dll wininet.dll dbghelp.dll OptiScaler.dll; do
[ ! -f "$folder/$f" ] || { ok "$folder/$f (verify this is your existing OptiScaler loader)"; loader=yes; }
done
done
[ "$loader" = yes ] || bad 'OptiScaler proxy loader (dxgi.dll, winmm.dll, etc.; nvngx.dll mode does not load ASIs)'
else
[ -f "$GAME/dxgi.dll" ] && ok dxgi.dll || bad 'dxgi.dll (re-extract the d4r zip)'
fi
[ -f "$INI" ] && ok "OptiScaler config: $INI" || bad "OptiScaler config: $INI"
core_name=d3d12core.dll
[ "$asi_backend" != yes ] || [ ! -f "$D4R/d4r12core.dll" ] || core_name=d4r12core.dll
for f in d3d12.dll "$core_name"; do
[ -f "$GAME/$f" ] && ok "$f" || bad "$f (re-extract the d4r zip)"
done
for f in nvcuda.dll zluda/libcuda.so d4r.ini; do
[ -f "$D4R/$f" ] && ok "$D4R/$f" || bad "$D4R/$f (re-extract the d4r zip)"
done
[ "$asi_backend" = yes ] || { [ -f "$GAME/d4r/nvngx.dll" ] && ok d4r/nvngx.dll || bad 'd4r/nvngx.dll (re-extract the d4r zip)'; }
# File presence alone does not establish NGX routing: OptiDllPath=d4r can still
# select system32/_nvngx.dll before d4r/nvngx.dll. Check the per-DLL override.
if [ -f "$GAME/OptiScaler.ini" ]; then
nvngx_path=$(awk '
{ sub(/\r$/, ""); line = $0; sub(/^[ \t]+/, "", line); sub(/[ \t]+$/, "", line) }
line ~ /^\[/ { libraries = (tolower(line) == "[libraries]"); next }
libraries && tolower(line) ~ /^nvngxpath[ \t]*=/ {
count++; sub(/^[^=]*=[ \t]*/, "", line); value = line
}
END { if (count != 1) exit 1; print value }
' "$GAME/OptiScaler.ini") && unique_path=yes || unique_path=no
normalized_path=$(printf '%s' "$nvngx_path" | tr '\\' '/' | tr '[:upper:]' '[:lower:]')
if [ -f "$INI" ]; then
# Also recognize an absolute Wine Z: path to this install's shim (as used
# by developer launches); do not accept an arbitrary nvngx.dll elsewhere.
game_path=$(cd "$GAME" 2>/dev/null && pwd -P)
absolute_path=$(printf 'z:%s/d4r/nvngx.dll' "$game_path" | tr '[:upper:]' '[:lower:]')
if [ "$unique_path" = yes ] && { [ "$normalized_path" = 'd4r/nvngx.dll' ] ||
if [ "$asi_backend" = yes ]; then
plugins_path=$(ini_value plugins path 2>/dev/null) || plugins_path=auto
case "$plugins_path" in auto|'') plugins_path=plugins ;; esac
plugin_route=$(printf '%s/d4r.asi' "$plugins_path" | tr '\\' '/' | tr '[:upper:]' '[:lower:]')
if [ "$unique_path" = yes ] && [ "$normalized_path" = "$plugin_route" ]; then
ok "OptiScaler NGX routing: NvngxPath=$nvngx_path (d4r ASI)"
else
misconfigured 'NvngxPath must select d4r.asi in the configured [Plugins] Path; relaunch to retry automatic setup'
fi
[ -f "$asi_file" ] && ok "ASI backend: $asi_file" || bad "ASI backend: $asi_file"
load_asi=$(ini_value plugins loadasiplugins 2>/dev/null | tr '[:upper:]' '[:lower:]')
[ "$load_asi" = true ] || misconfigured 'Set [Plugins] LoadAsiPlugins=true'
original_hook=$(ini_value hooks hookoriginalnvngxonly 2>/dev/null | tr '[:upper:]' '[:lower:]')
[ "$original_hook" = true ] || misconfigured 'Set [Hooks] HookOriginalNvngxOnly=true'
elif [ "$unique_path" = yes ] && { [ "$normalized_path" = 'd4r/nvngx.dll' ] ||
[ "$normalized_path" = './d4r/nvngx.dll' ] || [ "$normalized_path" = "$absolute_path" ]; }; then
ok "OptiScaler NGX routing: NvngxPath=$nvngx_path (d4r shim)"
case "$normalized_path" in
+5
View File
@@ -10,6 +10,9 @@ CLANG_CL="${CLANG_CL:-clang-cl}"
VULKAN_INCLUDE="${VULKAN_INCLUDE:-/usr/include}"
mkdir -p "$ROOT/build"
# Native configuration helper, launched automatically by InitializeASI.
"$MINGW_CXX" -std=c++20 -O2 -Wall -Wextra -Werror -static -municode \
"$ROOT/tools/d4r_asi_setup.cpp" -o "$ROOT/build/d4r-setup.exe"
bash "$ROOT/scripts/build_vulkan_staging.sh"
# Only the Vulkan headers from the host include tree, not its libc headers.
mkdir -p "$ROOT/build/vulkan-include"
@@ -24,3 +27,5 @@ PARAM_OBJECT="$ROOT/build/d4r_ngx_param_msvc.obj"
"$ROOT/engine/runtime.cpp" "$ROOT/engine/game.cpp" "$ROOT/engine/game_m.cpp" "$ROOT/engine/game_rr.cpp" "$ROOT/engine/vk_dispatch.cpp" \
-Wl,--entry,d4r_dll_entry -o "$ROOT/build/d4r_nvngx.dll"
printf 'Built %s\n' "$ROOT/build/d4r_nvngx.dll"
cp "$ROOT/build/d4r_nvngx.dll" "$ROOT/build/d4r.asi"
printf 'Built %s\n' "$ROOT/build/d4r.asi"
+32 -4
View File
@@ -7,6 +7,7 @@
#
# Inputs (environment):
# D4R_OPTISCALER OptiScaler release archive (.7z or .zip) or extracted folder (required; 0.9.4 tested)
# D4R_INSTALL_MODE proxy (default), or asi: add-on for an existing OptiScaler install
# D4R_DLSS_DLLS colon-separated nvngx_dlss.dll files whose kernel code the native kernels accept
# (required; only hashes of their PTX go into the release)
# D4R_ZLUDA_DIR ZLUDA build with patches/zluda applied (libnvcuda.so) default ~/.cache/d4r-zluda-current
@@ -53,13 +54,21 @@ case "${D4R_BUNDLE_NVIDIA:-installer}" in
esac
NAME="d4r-$VERSION"
[[ "$VARIANT" == clean ]] && NAME="$NAME-nonvidia"
INSTALL_MODE="${D4R_INSTALL_MODE:-proxy}"
case "$INSTALL_MODE" in
proxy) ;;
asi) NAME="$NAME-asi" ;;
*) echo "D4R_INSTALL_MODE must be proxy or asi" >&2; exit 2 ;;
esac
STAGE="$OUT/$NAME"
ZLUDA="${D4R_ZLUDA_DIR:-$HOME/.cache/d4r-zluda-current}"
VKD3D="${D4R_VKD3D_DIR:-$HOME/.cache/d4r-vkd3d-d4r}"
ROCM="${D4R_ROCM_DIR:-/opt/rocm}"
ROCM_RUNTIME="${D4R_ROCM_RUNTIME:-$HOME/.cache/d4r-rocm-runtime}"
ARCHS="${D4R_GPU_ARCHS:-gfx1100 gfx1101 gfx1102 gfx1103 gfx1200 gfx1201}"
: "${D4R_OPTISCALER:?set D4R_OPTISCALER to the OptiScaler release archive or folder}"
if [[ "$INSTALL_MODE" == proxy ]]; then
: "${D4R_OPTISCALER:?set D4R_OPTISCALER to the OptiScaler release archive or folder}"
fi
: "${D4R_DLSS_DLLS:?set D4R_DLSS_DLLS to the nvngx_dlss.dll files the kernel manifest accepts}"
for f in "$ZLUDA/libnvcuda.so" "$VKD3D/d3d12.dll" "$VKD3D/d3d12core.dll" "$ROCM_RUNTIME/lib/libamdhip64.so.7"; do
[[ -f "$f" ]] || { echo "missing $f (the ROCm runtime comes from scripts/fetch_rocm_runtime.sh)" >&2; exit 2; }
@@ -80,6 +89,7 @@ TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
# OptiScaler, as dxgi.dll with its INI preconfigured for d4r
if [[ "$INSTALL_MODE" == proxy ]]; then
if [[ -d "$D4R_OPTISCALER" ]]; then
OPTI="$D4R_OPTISCALER"
else
@@ -94,9 +104,20 @@ fi
cp "$OPTI/OptiScaler.dll" "$STAGE/dxgi.dll"
python3 "$ROOT/scripts/configure_optiscaler.py" \
"$OPTI/OptiScaler.ini" "$ROOT/packaging/optiscaler.settings" "$STAGE/OptiScaler.ini"
else
cp "$ROOT/build/d4r-setup.exe" "$STAGE/d4r/d4r-setup.exe"
# Same NGX exports and implementation, delivered through the ASI loader.
# A custom plugin path requires placing this file in that existing folder.
cp "$ROOT/build/d4r_nvngx.dll" "$STAGE/d4r.asi"
fi
# d4r-patched vkd3d-proton: same-frame DLSS results
cp "$VKD3D/d3d12.dll" "$VKD3D/d3d12core.dll" "$STAGE/"
if [[ "$INSTALL_MODE" == asi ]]; then
python3 "$ROOT/scripts/private_d3d12_core.py" "$VKD3D/d3d12.dll" "$STAGE/d4r/d3d12.dll"
cp "$VKD3D/d3d12core.dll" "$STAGE/d4r/d4r12core.dll"
else
cp "$VKD3D/d3d12.dll" "$VKD3D/d3d12core.dll" "$STAGE/"
fi
# d4r itself
cp "$ROOT/build/d4r_nvngx.dll" "$STAGE/d4r/nvngx.dll"
@@ -115,8 +136,10 @@ if [[ "$VARIANT" != clean ]]; then
cp "$D4R_BUNDLE_NGX" "$STAGE/d4r/ngx/_nvngx.dll"
[[ -z "${D4R_BUNDLE_DLSSD:-}" ]] || cp "$D4R_BUNDLE_DLSSD" "$STAGE/d4r/nvngx_dlssd.dll"
else
cp "$ROOT/install.sh" "$STAGE/install.sh"
chmod +x "$STAGE/install.sh"
installer_dir="$STAGE"
[[ "$INSTALL_MODE" != asi ]] || installer_dir="$STAGE/d4r"
cp "$ROOT/install.sh" "$installer_dir/install.sh"
chmod +x "$installer_dir/install.sh"
# The downloader is pinned to DLSS 310.7.0: refuse mismatched release inputs.
expected=$(sed -n "s/^DLSS_SHA256=//p" "$ROOT/install.sh")
[[ "$(sha256sum "$D4R_BUNDLE_DLSS" | cut -d' ' -f1)" == "$expected" ]] || {
@@ -277,6 +300,11 @@ if [[ -n "${D4R_BUILD_INFO:-}" ]]; then
cp "$D4R_BUILD_INFO" "$STAGE/d4r/source/BUILD_INFO.txt"
fi
sed -e "s/@VERSION@/$VERSION/g" "$ROOT/packaging/D4R_README.txt" | variant | sed 's/$/\r/' > "$STAGE/D4R_README.txt"
if [[ "$INSTALL_MODE" == asi ]]; then
rm "$STAGE/D4R_README.txt"
cp "$ROOT/packaging/ASI_README.txt" "$STAGE/d4r/README.txt"
printf '\nASI delivery: drop d4r.asi and d4r/ into the existing plugin folder.\nd4r.asi is d4r/nvngx.dll renamed; d4r/d4r-setup.exe (tools/d4r_asi_setup.cpp) installs the D3D12 pair and configures its route automatically after the game closes.\nThe dxgi.dll / OptiScaler.ini entries above apply only to proxy packages.\nIn ASI mode install.sh, d3d12.dll and the private d4r12core.dll are inside d4r/. The forwarder loader strings are retargeted by scripts/private_d3d12_core.py without changing code addresses.\n' >> "$STAGE/d4r/source/SOURCES.txt"
fi
# Last line of defence for the published zip: only what this variant contains, never values read from DLSS.
python3 "$ROOT/scripts/check_redistributable.py" release "$VARIANT" "$STAGE" ||
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""Retarget d4r's vkd3d forwarder to a private core DLL without changing code RVAs."""
from pathlib import Path
import sys
def retarget(data):
original = b'd3d12core.dll\0'
private = b'd4r12core.dll\0'
if not data.startswith(b'MZ') or data.count(original) != 2:
raise ValueError('Expected the d4r-built vkd3d forwarder with two core-loader strings')
result = data.replace(original, private)
assert len(result) == len(data)
return result
if __name__ == '__main__':
try:
Path(sys.argv[2]).write_bytes(retarget(Path(sys.argv[1]).read_bytes()))
except (ValueError, OSError) as error:
sys.exit(str(error))
+365
View File
@@ -0,0 +1,365 @@
"""Native ASI configuration: backups, existing plugins, loader contract and automatic dispatch."""
import configparser
import hashlib
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time
import unittest
ROOT = Path(__file__).resolve().parents[1]
@unittest.skipUnless(shutil.which('wine') and shutil.which('x86_64-w64-mingw32-g++'), 'Wine/MinGW required')
class AsiSetupTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temporary = tempfile.TemporaryDirectory(prefix='d4r-asi-tests-')
cls.directory = Path(cls.temporary.name)
cls.helper = cls.directory / 'd4r-setup.exe'
cls.probe = cls.directory / 'probe.exe'
for source, output in (('d4r_asi_setup.cpp', cls.helper), ('d4r_asi_probe.cpp', cls.probe)):
subprocess.run(['x86_64-w64-mingw32-g++', '-std=c++20', '-O2', '-Wall', '-Wextra',
'-Werror', '-static', '-municode', str(ROOT / 'tools' / source), '-o', str(output)],
check=True, capture_output=True)
# Hold the second destination open without delete sharing. The helper
# must restore the first DLL when the second replacement fails.
lock_source = cls.directory / 'locked-dll.cpp'
lock_source.write_text(r'''
#include <windows.h>
#include <string>
int wmain(int argc, wchar_t** argv) {
if (argc != 5) return 2;
HANDLE locked = CreateFileW(argv[2], GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (locked == INVALID_HANDLE_VALUE) return 3;
std::wstring command = L"\"" + std::wstring(argv[1]) + L"\" \"" + argv[3] + L"\" \"" + argv[4] + L"\"";
STARTUPINFOW startup = {}; startup.cb = sizeof(startup);
PROCESS_INFORMATION process = {};
if (!CreateProcessW(argv[1], command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &process)) return 4;
WaitForSingleObject(process.hProcess, INFINITE);
DWORD code = 0; GetExitCodeProcess(process.hProcess, &code);
CloseHandle(process.hThread); CloseHandle(process.hProcess); CloseHandle(locked);
return code == 1 ? 0 : 5;
}
''')
cls.lock_probe = cls.directory / 'locked-dll.exe'
subprocess.run(['x86_64-w64-mingw32-g++', '-std=c++20', '-O2', '-Wall', '-Wextra',
'-Werror', '-static', '-municode', str(lock_source), '-o', str(cls.lock_probe)],
check=True, capture_output=True)
cls.env = {k: v for k, v in os.environ.items() if not k.startswith('D4R_')}
cls.env.update(WINEPREFIX=str(cls.directory / 'prefix'), WINEDEBUG='-all', WINEARCH='win64',
WINEDLLOVERRIDES='winemenubuilder.exe=d', D4R_ASI_SETUP_QUIET='1')
@classmethod
def tearDownClass(cls):
cls.temporary.cleanup()
@staticmethod
def winpath(path):
return 'Z:' + str(path).replace('/', '\\')
def fixture(self, game, source):
runtime = game / 'custom plugins/d4r'
runtime.mkdir(parents=True)
(game / 'Game.exe').touch()
(game / 'winmm.dll').write_bytes(b'existing OptiScaler loader')
(game / 'OptiScaler.ini').write_bytes(source)
plugin = game / 'custom plugins/d4r.asi'
plugin.write_bytes(b'ASI fixture')
(plugin.parent / 'Other.asi').write_bytes(b'other mod')
for name in ('nvcuda.dll', 'nvngx_dlss.dll', 'ngx/_nvngx.dll',
'zluda/libcuda.so', 'rocm/lib/libamdhip64.so.7'):
path = runtime / name
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
(runtime / 'd4r.ini').write_text('[DLSS]\nModel=K\n')
shutil.copy2(self.helper, runtime / 'd4r-setup.exe')
for name in ('d3d12.dll', 'd4r12core.dll'):
(runtime / name).write_bytes(('new ' + name).encode())
(game / name).write_bytes(('old ' + name).encode())
(game / 'D3D12Core.dll').write_bytes(b'original Microsoft SDK core')
return plugin
def setup(self, game, plugin):
return subprocess.run(['wine', str(self.helper), self.winpath(game), self.winpath(plugin)],
env=self.env, cwd=self.directory, text=True, capture_output=True, timeout=60)
def test_custom_plugin_folder_preservation_backup_and_retry(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp) / 'Game with spaces'
original = (b'; keep this comment\r\n[Plugins]\r\nPath=custom plugins\r\n'
b'LoadAsiPlugins=true\r\nLoadReShade=true\r\n'
b'[FrameGen]\r\nFGOutput=fsr31\r\n[Menu]\r\nShortcutKey=45\r\n')
plugin = self.fixture(game, original)
result = self.setup(game, plugin)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(next(game.glob('OptiScaler.ini.d4r-backup.*')).read_bytes(), original)
configured = (game / 'OptiScaler.ini').read_bytes()
self.assertEqual(configured.count(b'\n'), configured.count(b'\r\n'))
self.assertIn(b'LoadReShade=true\r\n', configured)
self.assertIn(b'[FrameGen]\r\nFGOutput=fsr31\r\n', configured)
self.assertIn(b'; keep this comment\r\n', configured)
ini = configparser.ConfigParser()
ini.read_string(configured.decode())
self.assertEqual(ini['Libraries']['NvngxPath'], self.winpath(plugin))
self.assertEqual(ini['Plugins']['Path'], self.winpath(plugin.parent))
self.assertEqual((game / 'winmm.dll').read_bytes(), b'existing OptiScaler loader')
self.assertEqual((plugin.parent / 'Other.asi').read_bytes(), b'other mod')
self.assertEqual((game / 'D3D12Core.dll').read_bytes(), b'original Microsoft SDK core')
for name in ('d3d12.dll', 'd4r12core.dll'):
self.assertEqual((game / name).read_bytes(), ('new ' + name).encode())
self.assertEqual(next(game.glob(name + '.d4r-backup.*')).read_bytes(), ('old ' + name).encode())
self.assertEqual(self.setup(game, plugin).returncode, 0)
self.assertEqual(len(list(game.glob('*.d4r-backup.*'))), 3)
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), configured)
check = subprocess.run(['sh', str(ROOT / 'packaging/d4r-check.sh'), str(game)],
text=True, capture_output=True)
self.assertEqual(check.returncode, 0, check.stdout + check.stderr)
self.assertIn('(d4r ASI)', check.stdout)
for old, new, message in ((b'LoadAsiPlugins=true', b'LoadAsiPlugins=false', 'LoadAsiPlugins=true'),
(b'HookOriginalNvngxOnly=true', b'HookOriginalNvngxOnly=false',
'HookOriginalNvngxOnly=true')):
(game / 'OptiScaler.ini').write_bytes(configured.replace(old, new))
check = subprocess.run(['sh', str(ROOT / 'packaging/d4r-check.sh'), str(game)],
text=True, capture_output=True)
self.assertNotEqual(check.returncode, 0)
self.assertIn(message, check.stdout)
def test_duplicate_settings_and_missing_nvidia_leave_config_untouched(self):
for source in (b'[Libraries]\nNvngxPath=auto\nnvngxpath=other\n',
b'[Plugins]\nPath=plugins\n[plugins]\nPath=elsewhere\n',
b'[Plugins]\nLoadAsiPlugins=true\n'):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp)
plugin = self.fixture(game, source)
if source == b'[Plugins]\nLoadAsiPlugins=true\n':
(plugin.parent / 'd4r/nvngx_dlss.dll').unlink()
result = self.setup(game, plugin)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), source)
self.assertFalse(list(game.glob('*.d4r-backup.*')))
self.assertFalse(list(game.glob('.d4r-setup.*')))
@unittest.skipUnless((ROOT / 'build/d4r.asi').exists(), 'Build the ASI first')
def test_actual_asi_load_and_automatic_configuration(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp) / 'Game with spaces'
original = b'[Plugins]\nLoadAsiPlugins=true\nPath=custom plugins\n'
plugin = self.fixture(game, original)
shutil.copy2(ROOT / 'build/d4r.asi', plugin)
shutil.copy2(self.probe, game / 'probe.exe')
result = subprocess.run(['wine', str(game / 'probe.exe'), self.winpath(plugin),
self.winpath(plugin.parent / 'd4r')], env=self.env, cwd=self.directory,
text=True, capture_output=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('ASI loader contract passed', result.stdout)
deadline = time.monotonic() + 15
status = plugin.parent / 'd4r/installation-status.txt'
while (not status.exists() or 'setup complete' not in status.read_text()) and time.monotonic() < deadline:
time.sleep(0.05)
self.assertIn(self.winpath(plugin), (game / 'OptiScaler.ini').read_text())
log = (plugin.parent / 'd4r/d4r_nvngx.log').read_text()
self.assertEqual(log.count('InitializeASI:'), 1)
self.assertFalse((plugin.parent / 'd4r_nvngx.log').exists())
def test_invalid_second_dll_destination_preserves_first_dll_and_config(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp)
source = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, source)
(game / 'd4r12core.dll').unlink()
(game / 'd4r12core.dll').mkdir()
result = self.setup(game, plugin)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((game / 'd3d12.dll').read_bytes(), b'old d3d12.dll')
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), source)
self.assertFalse(list(game.glob('.d4r-setup.*')))
def test_nvidia_installer_accepts_both_runtime_layouts(self):
for nested in (False, True):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
base = Path(tmp)
runtime = base / 'd4r'
(runtime / 'ngx').mkdir(parents=True)
for name in ('nvngx.dll', 'd4r.ini'):
(runtime / name).touch()
dlss, ngx = b'verified DLSS fixture', b'verified NGX fixture'
(runtime / 'nvngx_dlss.dll').write_bytes(dlss)
(runtime / 'ngx/_nvngx.dll').write_bytes(ngx)
script = (ROOT / 'install.sh').read_text()
replacements = {'DLSS_SHA256': hashlib.sha256(dlss).hexdigest(),
'NGX_SHA256': hashlib.sha256(ngx).hexdigest()}
script = '\n'.join(key + '=' + replacements[key] if key in replacements else line
for line in script.splitlines() for key in [line.split('=', 1)[0]]) + '\n'
installer = (runtime if nested else base) / 'install.sh'
installer.write_text(script)
result = subprocess.run(['bash', str(installer)], cwd=self.directory,
text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn('already installed and verified', result.stdout)
self.assertEqual((runtime / 'nvngx_dlss.dll').read_bytes(), dlss)
def test_locked_second_dll_rolls_back_first_dll(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp)
original = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, original)
result = subprocess.run(['wine', str(self.lock_probe), self.winpath(self.helper),
self.winpath(game / 'd4r12core.dll'), self.winpath(game), self.winpath(plugin)],
env=self.env, cwd=self.directory, text=True, capture_output=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
for name in ('d3d12.dll', 'd4r12core.dll'):
self.assertEqual((game / name).read_bytes(), ('old ' + name).encode())
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), original)
self.assertIn('earlier changes were restored', (plugin.parent / 'd4r/installation-status.txt').read_text())
def test_d3d12_optiscaler_loader_is_preserved(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp)
original = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, original)
loader = b'MZ fixture OptiScaler.dll\0'
(game / 'd3d12.dll').write_bytes(loader)
result = self.setup(game, plugin)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((game / 'd3d12.dll').read_bytes(), loader)
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), original)
self.assertFalse(list(game.glob('*.d4r-backup.*')))
def test_prefix_config_detected_without_game_local_ini(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
base = Path(tmp)
game = base / 'game'
original = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, original)
umu = base / 'prefix/drive_c/windows/system32/umu'
umu.mkdir(parents=True)
shutil.move(str(plugin.parent), umu / 'plugins')
plugin = umu / 'plugins/d4r.asi'
config = umu / 'OptiScaler.ini'
shutil.move(str(game / 'OptiScaler.ini'), config)
result = self.setup(game, plugin)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn(self.winpath(plugin), config.read_text())
self.assertFalse((game / 'OptiScaler.ini').exists())
self.assertEqual(next(umu.glob('OptiScaler.ini.d4r-backup.*')).read_bytes(), original)
# The real GE-Proton loader writes C: paths and lives in the prefix,
# while the game EXE lives outside that prefix on Z:.
dosdevices = base / 'prefix/dosdevices'
dosdevices.mkdir()
(dosdevices / 'c:').symlink_to('../drive_c')
(umu / 'OptiScaler.dll').touch()
win_plugins = r'C:\windows\system32\umu\plugins'
data = config.read_text().replace(self.winpath(plugin.parent), win_plugins)
config.write_text(data)
checker = plugin.parent / 'd4r/d4r-check.sh'
shutil.copy2(ROOT / 'packaging/d4r-check.sh', checker)
(game / 'winmm.dll').unlink()
result = subprocess.run(['sh', str(checker), str(game)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('(d4r ASI)', result.stdout)
@unittest.skipUnless((ROOT / 'build/d4r.asi').exists(), 'Build the ASI first')
def test_active_proxy_ini_selected_over_unrelated_game_ini(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
base = Path(tmp)
game = base / 'game'
original = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, original)
shutil.copy2(ROOT / 'build/d4r.asi', plugin)
umu = base / 'prefix/drive_c/windows/system32/umu'
umu.mkdir(parents=True)
config = umu / 'OptiScaler.ini'
config.write_bytes(b'; active proxy config\n[Plugins]\nLoadAsiPlugins=true\n')
proxy_source = base / 'proxy.cpp'
proxy_source.write_text('extern "C" __declspec(dllexport) void DummyOpti() {}\n')
definition = base / 'proxy.def'
definition.write_text('LIBRARY OptiScaler.dll\nEXPORTS\nDummyOpti\n')
proxy = umu / 'winmm.dll'
subprocess.run(['x86_64-w64-mingw32-g++', '-shared', '-static', str(proxy_source),
str(definition), '-o', str(proxy)], check=True, capture_output=True)
source = base / 'active-proxy.cpp'
source.write_text(r'''
#include <windows.h>
int wmain(int argc, wchar_t** argv) {
if (argc != 3 || !LoadLibraryW(argv[1])) return 2;
HMODULE asi = LoadLibraryW(argv[2]);
if (!asi) return 3;
auto init = reinterpret_cast<void (*)()>(GetProcAddress(asi, "InitializeASI"));
if (!init) return 4;
init();
return 0;
}
''')
probe = game / 'active-proxy.exe'
subprocess.run(['x86_64-w64-mingw32-g++', '-static', '-municode', str(source), '-o', str(probe)],
check=True, capture_output=True)
result = subprocess.run(['wine', str(probe), self.winpath(proxy), self.winpath(plugin)],
env=self.env, cwd=self.directory, text=True, capture_output=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
status = plugin.parent / 'd4r/installation-status.txt'
deadline = time.monotonic() + 15
while (not status.exists() or 'setup complete' not in status.read_text()) and time.monotonic() < deadline:
time.sleep(0.05)
self.assertTrue(status.exists())
self.assertIn('setup complete', status.read_text())
self.assertIn(self.winpath(plugin), config.read_text())
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), original)
@unittest.skipUnless((ROOT / 'build/d4r.asi').exists(), 'Build the ASI first')
def test_xalia_does_not_start_setup_or_modify_settings(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp)
original = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, original)
shutil.copy2(ROOT / 'build/d4r.asi', plugin)
utility = game / 'xalia.exe'
shutil.copy2(self.probe, utility)
result = subprocess.run(['wine', str(utility), self.winpath(plugin),
self.winpath(plugin.parent / 'd4r')], env=self.env, cwd=self.directory,
text=True, capture_output=True, timeout=30)
# The probe's environment assertions intentionally fail because
# InitializeASI must leave this auxiliary process untouched.
self.assertEqual(result.returncode, 5, result.stdout + result.stderr)
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), original)
self.assertFalse((plugin.parent / 'd4r/installation-status.txt').exists())
self.assertFalse((plugin.parent / 'd4r/d4r_nvngx.log').exists())
@unittest.skipUnless((ROOT / 'build/d4r.asi').exists(), 'Build the ASI first')
def test_installation_waits_for_game_exit(self):
with tempfile.TemporaryDirectory(dir=self.directory) as tmp:
game = Path(tmp)
original = b'[Plugins]\nLoadAsiPlugins=true\n'
plugin = self.fixture(game, original)
shutil.copy2(ROOT / 'build/d4r.asi', plugin)
shutil.copy2(self.probe, game / 'probe.exe')
process = subprocess.Popen(['wine', str(game / 'probe.exe'), self.winpath(plugin),
self.winpath(plugin.parent / 'd4r')],
env=dict(self.env, D4R_ASI_PROBE_PAUSE_MS='5000'), cwd=self.directory,
text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
status = plugin.parent / 'd4r/installation-status.txt'
deadline = time.monotonic() + 15
while not status.exists() and process.poll() is None and time.monotonic() < deadline:
time.sleep(0.05)
self.assertTrue(status.exists())
self.assertIn('Close the game', status.read_text())
self.assertEqual((game / 'd3d12.dll').read_bytes(), b'old d3d12.dll')
self.assertEqual((game / 'OptiScaler.ini').read_bytes(), original)
output, errors = process.communicate(timeout=20)
self.assertEqual(process.returncode, 0, output + errors)
deadline = time.monotonic() + 10
while 'setup complete' not in status.read_text() and time.monotonic() < deadline:
time.sleep(0.05)
self.assertIn('setup complete', status.read_text())
self.assertEqual((game / 'd3d12.dll').read_bytes(), b'new d3d12.dll')
finally:
if process.poll() is None:
process.kill()
process.communicate()
if __name__ == '__main__':
unittest.main()
+30 -4
View File
@@ -4,6 +4,8 @@ Package tests use disposable runtime files and stand-ins for GPU kernel compilat
Actual OptiScaler/Proton loading additionally needs the D3D12 harness.
"""
import configparser
import hashlib
import itertools
import os
from pathlib import Path
import shutil
@@ -89,7 +91,7 @@ class OptiScalerConfigTests(unittest.TestCase):
inputs.mkdir()
(repo / 'scripts').mkdir(parents=True)
for name in ('package_release.sh', 'configure_optiscaler.py', 'check_glibc_compat.sh',
'check_redistributable.py'):
'check_redistributable.py', 'private_d3d12_core.py'):
shutil.copy2(ROOT / 'scripts' / name, repo / 'scripts' / name)
shutil.copytree(ROOT / 'packaging', repo / 'packaging')
for name in ('LICENSE', 'NOTICE'):
@@ -116,16 +118,22 @@ class OptiScalerConfigTests(unittest.TestCase):
subprocess.run(['cc', '-shared', '-x', 'c', '-o', str(elf), '-'],
input='int fixture(void) { return 0; }\n', text=True, check=True)
fixture(repo / 'build/d4r_nvngx.dll')
fixture(repo / 'build/d4r-setup.exe')
fixture(repo / 'build/wine-nvcuda/x86_64-unix/nvcuda.dll.so', elf.read_bytes())
fixture(inputs / 'zluda/libnvcuda.so', elf.read_bytes())
fixture(inputs / 'rocm/lib/libamdhip64.so.7', elf.read_bytes())
fixture(inputs / 'rocm/licenses/runtime.txt')
for name in ('d3d12.dll', 'd3d12core.dll'):
fixture(inputs / 'vkd3d' / name)
fixture(inputs / 'vkd3d' / name, b'MZ fixture d3d12core.dll\0 then d3d12core.dll\0' if name == 'd3d12.dll' else b'fixture')
fixture(inputs / 'opti/OptiScaler.dll')
fixture(inputs / 'opti/OptiScaler.ini', upstream_ini().replace('\n', '\r\n').encode())
dlss = fixture(inputs / 'nvngx_dlss.dll')
core = fixture(inputs / '_nvngx.dll')
installer = (ROOT / 'install.sh').read_text()
installer = '\n'.join('DLSS_SHA256=' + hashlib.sha256(dlss.read_bytes()).hexdigest()
if line.startswith('DLSS_SHA256=') else line
for line in installer.splitlines()) + '\n'
(repo / 'install.sh').write_text(installer)
license_path = fixture(inputs / 'license.txt')
for name in ('LICENSE-APACHE', 'LICENSE-MIT', 'ext/llvm-project/llvm/LICENSE.TXT', 'LICENSE', 'COPYING'):
fixture(inputs / 'source' / name)
@@ -153,19 +161,37 @@ class OptiScalerConfigTests(unittest.TestCase):
'direct_origins.bin', 'weights.bin', 'offsets.bin', 'lut.bin', 'hipnet.bin', 'model.json'):
fixture(inputs / 'engine' / name)
env['D4R_BUNDLE_ENGINE'] = str(inputs / 'engine')
for bundled in ('1', '0'):
with self.subTest(bundled=bundled):
for mode, bundled in itertools.product(('proxy', 'asi'), ('1', '0', 'installer')):
with self.subTest(mode=mode, bundled=bundled):
env['D4R_BUNDLE_NVIDIA'] = bundled
env['D4R_INSTALL_MODE'] = mode
if mode == 'asi':
env.pop('D4R_OPTISCALER', None)
result = subprocess.run(['bash', str(repo / 'scripts/package_release.sh'), str(repo / 'dist')],
env=env, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
name = f'd4r-{version}' + ('-nonvidia' if bundled == '0' else '')
if mode == 'asi':
name += '-asi'
with zipfile.ZipFile(repo / 'dist' / (name + '.zip')) as archive:
self.assertIsNone(archive.testzip())
if mode == 'proxy':
data = archive.read('OptiScaler.ini')
self.assert_routing(data)
configs.append(data)
self.assertEqual(data, (repo / 'dist' / name / 'OptiScaler.ini').read_bytes())
else:
self.assertNotIn('OptiScaler.ini', archive.namelist())
self.assertNotIn('dxgi.dll', archive.namelist())
self.assertEqual(archive.read('d4r.asi'), archive.read('d4r/nvngx.dll'))
self.assertIn('d4r/d4r-setup.exe', archive.namelist())
self.assertEqual('d4r/install.sh' in archive.namelist(), bundled == 'installer')
self.assertFalse(any(n.endswith('.py') for n in archive.namelist()))
self.assertIn(b'ASI add-on', archive.read('d4r/README.txt'))
self.assertEqual({n.split('/')[0] for n in archive.namelist()}, {'d4r.asi', 'd4r'})
self.assertIn('d4r/d3d12.dll', archive.namelist())
self.assertIn('d4r/d4r12core.dll', archive.namelist())
self.assertNotIn('d4r/d3d12core.dll', archive.namelist())
self.assertIn('d4r/nvngx.dll', archive.namelist())
self.assertNotIn('nvngx.dll', archive.namelist())
self.assertNotIn('d4r/_nvngx.dll', archive.namelist())
+66
View File
@@ -0,0 +1,66 @@
// CPU-only smoke probe for the ASI loader contract. No NGX/CUDA/GPU initialization.
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <string>
int wmain(int argc, wchar_t** argv)
{
if (argc != 3 && argc != 4)
return 2; // ASI absolute path, expected runtime directory
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
HMODULE optiscaler = nullptr;
if (argc == 4)
{
optiscaler = LoadLibraryW(argv[3]);
if (!optiscaler || !GetModuleHandleW(argv[1]) ||
GetEnvironmentVariableW(L"D4R_NVCUDA_BRIDGE", nullptr, 0) == 0)
{
std::fprintf(stderr, "OptiScaler did not initialize the ASI: %lu\n", GetLastError());
return 8;
}
}
HMODULE plugin = LoadLibraryW(argv[1]);
if (!plugin)
{
std::fprintf(stderr, "LoadLibrary failed: %lu\n", GetLastError());
return 3;
}
for (const char* name : {"InitializeASI", "NVSDK_NGX_D3D12_Init", "NVSDK_NGX_VULKAN_Init",
"NVSDK_NGX_D3D12_CreateFeature", "NVSDK_NGX_VULKAN_CreateFeature"})
if (!GetProcAddress(plugin, name))
{
std::fprintf(stderr, "Missing export: %s\n", name);
return 4;
}
auto initialize = reinterpret_cast<void (*)()>(GetProcAddress(plugin, "InitializeASI"));
initialize();
initialize(); // ASI initialization is idempotent.
struct Expected { const wchar_t* name; const wchar_t* suffix; };
for (const auto& expected : {Expected{L"D4R_NGX_FEATURE_DIR", L""},
Expected{L"D4R_NVCUDA_BRIDGE", L"\\nvcuda.dll"},
Expected{L"D4R_NGX_CORE", L"\\ngx\\_nvngx.dll"},
Expected{L"D4R_SHIM_LOG", L"\\d4r_nvngx.log"}})
{
wchar_t value[MAX_PATH] = {};
GetEnvironmentVariableW(expected.name, value, MAX_PATH);
if (std::wstring(value) != std::wstring(argv[2]) + expected.suffix)
{
std::fwprintf(stderr, L"Wrong %ls: %ls\n", expected.name, value);
return 5;
}
}
HMODULE backend = LoadLibraryW(argv[1]);
if (backend != plugin)
return 6;
if (GetModuleHandleA("nvcuda.dll") || GetModuleHandleA("_nvngx.dll"))
return 7; // The ASI loader must not trigger GPU initialization.
char delay[16] = {};
if (GetEnvironmentVariableA("D4R_ASI_PROBE_PAUSE_MS", delay, sizeof(delay)))
Sleep(static_cast<DWORD>(std::strtoul(delay, nullptr, 10)));
FreeLibrary(backend);
FreeLibrary(plugin);
if (optiscaler) FreeLibrary(optiscaler);
std::puts("ASI loader contract passed; backend reused, runtime located, GPU initialization deferred");
return 0;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <cwctype>
#include <string>
// GE-Proton redirects DXGI in background utilities too. They share the ASI
// directory with the game but must never install files or show setup dialogs.
inline bool d4r_asi_game_process(std::wstring executable)
{
for (auto& c : executable) { c = std::towlower(c); if (c == L'/') c = L'\\'; }
const auto slash = executable.find_last_of(L'\\');
const std::wstring name = executable.substr(slash == std::wstring::npos ? 0 : slash + 1);
for (const wchar_t* utility : {L"xalia.exe", L"steam.exe", L"d4r-setup.exe", L"start_protected_game.exe",
L"crashreportclient.exe", L"unrealcefsubprocess.exe", L"unitycrashhandler64.exe",
L"unitycrashhandler32.exe", L"winedevice.exe", L"services.exe", L"explorer.exe",
L"rundll32.exe", L"conhost.exe", L"wineboot.exe", L"wineserver.exe"})
if (name == utility) return false;
return executable.find(L"\\windows\\system32\\") == std::wstring::npos &&
executable.find(L"\\windows\\syswow64\\") == std::wstring::npos;
}
+270
View File
@@ -0,0 +1,270 @@
// Native first-run setup, launched by InitializeASI. No Python, shell or system install.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cwchar>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#include "d4r_asi_process.h"
namespace fs = std::filesystem;
static std::string utf8(const std::wstring& value)
{
int length = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
std::string result(length, '\0');
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), length, nullptr, nullptr);
return result;
}
static std::string lower(std::string value)
{
for (char& c : value)
if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
return value;
}
static std::string trim(const std::string& value)
{
auto start = value.find_first_not_of(" \t\r\n");
return start == std::string::npos ? "" : value.substr(start, value.find_last_not_of(" \t\r\n") - start + 1);
}
static std::string read(const fs::path& path)
{
std::ifstream file(path, std::ios::binary);
if (!file) throw std::runtime_error("Cannot read " + utf8(path.wstring()));
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
static void write(const fs::path& path, const std::string& data)
{
std::ofstream file(path, std::ios::binary | std::ios::trunc);
file.write(data.data(), static_cast<std::streamsize>(data.size()));
file.close();
if (!file) throw std::runtime_error("Cannot write " + utf8(path.wstring()));
}
static std::wstring unique_suffix()
{
FILETIME now;
GetSystemTimeAsFileTime(&now);
return L"." + std::to_wstring(GetCurrentProcessId()) + L"." + std::to_wstring(
(static_cast<unsigned long long>(now.dwHighDateTime) << 32) | now.dwLowDateTime);
}
struct Replacement
{
fs::path staged, destination, backup;
};
static void commit(std::vector<Replacement>& files)
{
// Prepare all backups before replacing any file. Reject aliases/directories.
for (auto& file : files) {
if (fs::is_symlink(file.destination) || (fs::exists(file.destination) && !fs::is_regular_file(file.destination)))
throw std::runtime_error("Destination must be a regular file: " + utf8(file.destination.wstring()));
if (fs::exists(file.destination)) {
file.backup = file.destination.wstring() + L".d4r-backup" + unique_suffix();
if (!CopyFileW(file.destination.c_str(), file.backup.c_str(), TRUE))
throw std::runtime_error("Cannot back up " + utf8(file.destination.wstring()));
}
}
size_t applied = 0;
for (; applied < files.size(); ++applied) {
auto& file = files[applied];
if (MoveFileExW(file.staged.c_str(), file.destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
continue;
bool restored = true;
while (applied > 0) {
auto& previous = files[--applied];
if (previous.backup.empty()) restored &= DeleteFileW(previous.destination.c_str()) != FALSE;
else restored &= CopyFileW(previous.backup.c_str(), previous.destination.c_str(), FALSE) != FALSE;
}
throw std::runtime_error("Cannot install " + utf8(file.destination.wstring()) +
(restored ? "; earlier changes were restored" : "; restore the .d4r-backup files before retrying"));
}
}
static bool same_file(const fs::path& source, const fs::path& destination)
{
return fs::is_regular_file(destination) && fs::file_size(source) == fs::file_size(destination) && read(source) == read(destination);
}
static std::string configure(std::string source, const fs::path& plugin)
{
const std::string bom = source.starts_with("\xef\xbb\xbf") ? "\xef\xbb\xbf" : "";
if (!bom.empty()) source.erase(0, 3);
std::string newline = source.find("\r\n") == std::string::npos ? "\n" : "\r\n";
std::vector<std::string> lines;
for (size_t pos = 0; pos < source.size();) {
auto end = source.find('\n', pos);
end = end == std::string::npos ? source.size() : end + 1;
lines.push_back(source.substr(pos, end - pos));
pos = end;
}
struct Setting { std::string section, key, value; };
const std::array<Setting, 11> settings = {{{"Upscalers", "Dx12Upscaler", "dlss"}, {"DLSS", "Enabled", "true"},
{"Inputs", "EnableDlssInputs", "true"}, {"Hooks", "HookOriginalNvngxOnly", "true"},
{"Hooks", "UseNtdllHooks", "false"},
{"NvApi", "OverrideNvapiDll", "false"}, {"Spoofing", "Dxgi", "true"},
{"FSR", "FsrAgilitySDKUpgrade", "false"},
{"Plugins", "LoadAsiPlugins", "true"}, {"Plugins", "Path", utf8(plugin.parent_path().wstring())},
{"Libraries", "NvngxPath", utf8(plugin.wstring())}}};
for (const auto& setting : settings) {
size_t start = lines.size(), end = lines.size(), match = lines.size();
for (size_t i = 0; i < lines.size(); ++i)
if (lower(trim(lines[i])) == "[" + lower(setting.section) + "]") {
if (start != lines.size()) throw std::runtime_error("Duplicate [" + setting.section + "] section");
start = i;
}
if (start == lines.size()) {
if (!lines.empty() && !lines.back().ends_with('\n')) lines.back() += newline;
lines.push_back("[" + setting.section + "]" + newline);
lines.push_back(setting.key + "=" + setting.value + newline);
continue;
}
for (size_t i = start + 1; i < lines.size(); ++i) {
auto line = trim(lines[i]);
if (line.starts_with('[')) { end = i; break; }
auto equals = line.find('=');
if (equals != std::string::npos && lower(trim(line.substr(0, equals))) == lower(setting.key)) {
if (match != lines.size()) throw std::runtime_error("Duplicate " + setting.key + " setting");
match = i;
}
}
if (match != lines.size()) {
std::string ending = lines[match].ends_with("\r\n") ? "\r\n" : lines[match].ends_with('\n') ? "\n" : "";
// Keep original spelling, spacing and comments if the active value already matches.
auto line = trim(lines[match]);
auto value = trim(line.substr(line.find('=') + 1));
if (lower(value) != lower(setting.value))
lines[match] = setting.key + "=" + setting.value + ending;
} else {
if (end && !lines[end - 1].ends_with('\n')) lines[end - 1] += newline;
lines.insert(lines.begin() + end, setting.key + "=" + setting.value + newline);
}
}
std::string result = bom;
for (const auto& line : lines) result += line;
return result;
}
int wmain(int argc, wchar_t** argv)
{
if (argc < 3 || argc > 5) return 2;
fs::path game = fs::absolute(argv[1]), plugin = fs::absolute(argv[2]);
fs::path runtime = plugin.parent_path() / "d4r", config = game / "OptiScaler.ini";
// Also reject background callers from older ASIs before taking locks or
// touching files. A copied helper must not continue an xalia installation.
if (argc >= 4) {
HANDLE caller = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, wcstoul(argv[3], nullptr, 10));
if (caller) {
wchar_t executable[32768] = {}; DWORD size = 32768;
bool queried = QueryFullProcessImageNameW(caller, 0, executable, &size) != FALSE;
CloseHandle(caller);
if (!queried || !d4r_asi_game_process(executable)) return 0;
}
}
// The ASI supplies the active loader's location when available. Older ASIs
// still work with the usual plugins/ folder under OptiScaler's directory.
wchar_t quiet[2] = {};
bool silent = GetEnvironmentVariableW(L"D4R_ASI_SETUP_QUIET", quiet, 2) && quiet[0] == L'1';
// One setup process per game, even if two copies/launches call InitializeASI.
unsigned long long key = 14695981039346656037ull;
for (auto c : runtime.wstring()) { key ^= static_cast<unsigned short>(std::towlower(c)); key *= 1099511628211ull; }
HANDLE mutex = CreateMutexW(nullptr, FALSE, (L"Local\\d4r-asi-setup-" + std::to_wstring(key)).c_str());
if (!mutex) return 3;
DWORD wait = WaitForSingleObject(mutex, 0);
if (wait != WAIT_OBJECT_0 && wait != WAIT_ABANDONED) { CloseHandle(mutex); return 0; }
struct Lock { HANDLE handle; ~Lock() { ReleaseMutex(handle); CloseHandle(handle); } } lock{mutex};
fs::path temporary = game / (L".d4r-setup" + unique_suffix());
try {
if (argc == 5 && *argv[4]) config = fs::absolute(argv[4]);
else if (fs::is_regular_file(plugin.parent_path().parent_path() / "OptiScaler.ini"))
config = plugin.parent_path().parent_path() / "OptiScaler.ini";
if (!fs::is_regular_file(plugin))
throw std::runtime_error("ASI file not found: " + utf8(plugin.wstring()));
if (!fs::is_regular_file(runtime / "d4r.ini"))
throw std::runtime_error("Companion d4r.ini not found: " + utf8((runtime / "d4r.ini").wstring()));
if (!fs::is_regular_file(config))
throw std::runtime_error("OptiScaler.ini not found: " + utf8(config.wstring()) +
"; keep the INI beside the OptiScaler loader");
const std::string original = read(config);
const std::string updated = configure(original, plugin); // reject ambiguity before mutations
if (!fs::is_regular_file(runtime / "nvngx_dlss.dll") || !fs::is_regular_file(runtime / "ngx/_nvngx.dll"))
throw std::runtime_error("Run bash install.sh inside the plugin's d4r folder to download and verify the NVIDIA DLLs, then launch the game again");
for (const char* name : {"d3d12.dll", "d4r12core.dll"})
if (!fs::is_regular_file(runtime / name))
throw std::runtime_error("The bundled D3D12 files are missing; extract the complete ASI archive");
if (fs::is_regular_file(game / "d3d12.dll") &&
lower(read(game / "d3d12.dll")).find("optiscaler.dll") != std::string::npos)
throw std::runtime_error("OptiScaler is installed as d3d12.dll. Use a supported loader such as dxgi.dll or winmm.dll before installing d4r, so setup can preserve your OptiScaler loader");
bool pairChanged = !same_file(runtime / "d3d12.dll", game / "d3d12.dll") ||
!same_file(runtime / "d4r12core.dll", game / "d4r12core.dll");
if (original == updated && !pairChanged) return 0;
// A running game may hold these DLLs mapped or save its old INI on exit.
// The ASI gives us its Windows PID; finish only after that game closes.
if (argc >= 4) {
wchar_t* end = nullptr;
unsigned long pid = wcstoul(argv[3], &end, 10);
if (!pid || *end) throw std::runtime_error("Invalid game process ID");
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, pid);
if (!parent && GetLastError() != ERROR_INVALID_PARAMETER)
throw std::runtime_error("Cannot wait for the game to close");
if (parent) {
write(runtime / "installation-status.txt", "Close the game to finish d4r setup. Files will be backed up and installed automatically after it exits.\r\n");
if (!silent) MessageBoxW(nullptr, L"Close the game to finish d4r setup.\n\nSetup will back up your existing D3D12 files and OptiScaler settings after the game exits, then show a ready message.",
L"d4r setup", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND);
DWORD waited = WaitForSingleObject(parent, INFINITE);
CloseHandle(parent);
if (waited != WAIT_OBJECT_0) throw std::runtime_error("Could not wait for game shutdown");
}
}
fs::create_directories(temporary);
// The loader's INI can be in the prefix on another filesystem. Stage it
// beside its own destination, so its replacement remains atomic too.
fs::path configStage = config.parent_path() / (L".d4r-config" + unique_suffix());
struct Cleanup {
fs::path path, config;
~Cleanup() { std::error_code error; fs::remove_all(path, error); fs::remove(config, error); }
} cleanup{temporary, configStage};
// Merge the latest settings in case OptiScaler changed them during setup.
const std::string current = read(config);
const std::string finalConfig = configure(current, plugin);
std::vector<Replacement> replacements;
for (const char* name : {"d3d12.dll", "d4r12core.dll"}) {
fs::path source = runtime / name, destination = game / name;
if (!same_file(source, destination)) {
fs::copy_file(source, temporary / name);
replacements.push_back({temporary / name, destination, {}});
}
}
if (current != finalConfig) {
write(configStage, finalConfig);
replacements.push_back({configStage, config, {}});
}
commit(replacements);
write(runtime / "installation-status.txt", "d4r setup complete. Launch the game again to activate DLSS.\r\n");
if (!silent) MessageBoxW(nullptr, L"d4r is ready. Launch the game again to activate DLSS.\n\nChanged D3D12 files and OptiScaler settings have been backed up.",
L"d4r ready", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND);
return 0;
} catch (const std::exception& error) {
std::string message = "d4r setup failed: " + std::string(error.what()) + "\r\nLaunch the game again to retry.\r\n";
try { write(runtime / "installation-status.txt", message); } catch (...) {}
std::fprintf(stderr, "%s", message.c_str());
if (!silent) {
int count = MultiByteToWideChar(CP_UTF8, 0, message.data(), static_cast<int>(message.size()), nullptr, 0);
std::wstring wide(count, '\0');
MultiByteToWideChar(CP_UTF8, 0, message.data(), static_cast<int>(message.size()), wide.data(), count);
MessageBoxW(nullptr, wide.c_str(), L"d4r setup", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
}
return 1;
}
}
+82
View File
@@ -48,6 +48,8 @@
// Independent of D4R_DLSS_PRESET, which only affects Super Sampling.
#define WIDL_EXPLICIT_AGGREGATE_RETURNS
#include <windows.h>
#include <tlhelp32.h>
#include "d4r_asi_process.h"
#include <d3d12.h>
#include <dxgi1_4.h>
#include <vulkan/vulkan_core.h>
@@ -529,6 +531,15 @@ static void load_portable_config()
if (length == 0 || length >= MAX_PATH)
return;
std::wstring dir(module, length);
// ASI delivery keeps its runtime in a companion d4r/ folder beside the ASI.
// DLL delivery stays self-relative.
const auto dot = dir.find_last_of(L'.');
if (dot != std::wstring::npos && _wcsicmp(dir.c_str() + dot, L".asi") == 0)
{
dir.resize(dir.find_last_of(L"\\/"));
dir += L"\\d4r";
}
else
dir.resize(dir.find_last_of(L"\\/"));
g_portable.dir = dir; // also without d4r.ini: the default log goes here
std::string text;
@@ -9458,6 +9469,77 @@ D4R_EXPORT NgxResult NVSDK_NGX_UpdateFeature(const void*, unsigned int)
#include "d4r_vulkan_frontend.inl"
// Proxy filenames vary (dxgi.dll, winmm.dll, ...). Identify OptiScaler by its
// PE export name rather than guessing from the EXE or plugin folder location.
static std::wstring optiscaler_config_path()
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (snapshot == INVALID_HANDLE_VALUE) return {};
MODULEENTRY32W module = {};
module.dwSize = sizeof(module);
std::wstring result;
if (Module32FirstW(snapshot, &module)) do
{
const BYTE* base = module.modBaseAddr;
const size_t size = module.modBaseSize;
if (size < sizeof(IMAGE_NT_HEADERS64)) continue;
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew < 0 ||
static_cast<size_t>(dos->e_lfanew) > size - sizeof(IMAGE_NT_HEADERS64)) continue;
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) continue;
const DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!rva || rva > size - sizeof(IMAGE_EXPORT_DIRECTORY)) continue;
const auto* exports = reinterpret_cast<const IMAGE_EXPORT_DIRECTORY*>(base + rva);
if (exports->Name >= size) continue;
const char* name = reinterpret_cast<const char*>(base + exports->Name);
if (!std::memchr(name, 0, size - exports->Name) || _stricmp(name, "OptiScaler.dll") != 0) continue;
result = module.szExePath;
result.resize(result.find_last_of(L"\\/"));
result += L"\\OptiScaler.ini";
break;
} while (Module32NextW(snapshot, &module));
CloseHandle(snapshot);
return result;
}
// OptiScaler calls this after LoadLibrary returns, outside our DllMain.
// Launch first-time setup in a separate native process; never do network/GPU
// work under the loader lock. CUDA/NGX work remains lazy until NGX Init.
// NvngxPath must name this ASI, so backend calls reuse this module's state.
D4R_EXPORT void InitializeASI()
{
static std::once_flag once;
std::call_once(once, [] {
wchar_t exe[MAX_PATH] = {}, plugin[MAX_PATH] = {};
if (!GetModuleFileNameW(nullptr, exe, MAX_PATH) ||
!GetModuleFileNameW(g_selfModule, plugin, MAX_PATH))
return;
if (!d4r_asi_game_process(exe)) return;
logf("InitializeASI: d4r ASI backend ready (GPU initialization deferred)");
if (env_uint("D4R_ASI_AUTOINSTALL", 1) == 0) return;
std::wstring game(exe);
game.resize(game.find_last_of(L"\\/"));
std::wstring pluginDir(plugin);
pluginDir.resize(pluginDir.find_last_of(L"\\/"));
const std::wstring helper = pluginDir + L"\\d4r\\d4r-setup.exe";
const std::wstring optiscalerConfig = optiscaler_config_path();
std::wstring command = L"\"" + helper + L"\" \"" + game + L"\" \"" + plugin + L"\" " +
std::to_wstring(GetCurrentProcessId()) + L" \"" + optiscalerConfig + L"\"";
STARTUPINFOW startup = {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process = {};
if (CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, game.c_str(), &startup, &process))
{
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
}
else
logf("InitializeASI: automatic setup could not start (%lu); extract the complete ASI archive", GetLastError());
});
}
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH)