Files
d4r/packaging/d4r-check.sh
T
ayo d5c8be25c8 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.
2026-10-12 17:36:01 -04:00

161 lines
9.0 KiB
Bash
Executable File

#!/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 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}}}"
external_backend=$(awk '
{ sub(/\r$/, ""); line = $0; sub(/^[ \t]+/, "", line); sub(/[ \t]+$/, "", line) }
line ~ /^\[/ { dlss = (tolower(line) == "[dlss]"); next }
dlss && tolower(line) ~ /^allowexternalbackend[ \t]*=/ {
count++; sub(/^[^=]*=[ \t]*/, "", line); value = tolower(line)
}
END { if (count == 1 && value == "true") print "true" }
' "$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)"
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 "$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 [ "$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
z:*) ;;
*) [ "$external_backend" = true ] || note "relative NvngxPath requires the game's working directory to be this folder" ;;
esac
else
misconfigured 'OptiScaler NGX routing: set [Libraries] NvngxPath=d4r\nvngx.dll (exactly once); OptiDllPath alone can load system _nvngx.dll'
note 'd4r/ngx/_nvngx.dll is the NVIDIA core for the shim, not the OptiScaler target'
fi
fi
[ -f "$D4R/nvngx_dlss.dll" ] && ok "d4r/nvngx_dlss.dll (NVIDIA DLSS library)" || bad "d4r/nvngx_dlss.dll: copy NVIDIA's DLSS library here"
[ -f "$D4R/ngx/_nvngx.dll" ] && ok "d4r/ngx/_nvngx.dll (NVIDIA NGX runtime)" || bad "d4r/ngx/_nvngx.dll: copy NVIDIA's NGX runtime here"
if [ -f "$D4R/nvngx_dlss.dll" ] && command -v strings >/dev/null 2>&1; then
v=$(strings -el "$D4R/nvngx_dlss.dll" | grep -A1 '^FileVersion$' | sed -n 2p | tr ',' '.')
case "$v" in
310.7.*|310.9.*) ok "DLSS version $v (native kernels verified for 310.7 and 310.9)" ;;
"") note "cannot read the DLSS version" ;;
*) note "DLSS version $v: kernels whose code changed run without native kernels (slower)" ;;
esac
fi
found=
for dir in "$ROCM/lib" /opt/rocm/lib /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu; do
[ -e "$dir/libamdhip64.so.7" ] && { found="$dir"; break; }
done
[ -n "$found" ] && ok "ROCm HIP runtime ($found/libamdhip64.so.7)" || bad "ROCm HIP runtime 7.x (libamdhip64.so.7); re-extract the d4r zip, which includes it in d4r/rocm"
[ -e /dev/kfd ] && ok "/dev/kfd (ROCm compute device)" || bad "/dev/kfd: the amdgpu compute interface is not available"
target="" best_simds=0
for props in /sys/class/kfd/kfd/topology/nodes/*/properties; do
s=$(sed -n 's/^simd_count //p' "$props" 2>/dev/null); t=$(sed -n 's/^gfx_target_version //p' "$props" 2>/dev/null)
[ -n "$s" ] && [ "$s" -gt "$best_simds" ] && [ -n "$t" ] && [ "$t" != 0 ] && { target="$t"; best_simds="$s"; }
done
if [ -n "$target" ]; then
arch=$(printf 'gfx%d%d%x' $((target / 10000)) $(((target / 100) % 100)) $((target % 100)))
if [ -d "$D4R/kernels/$arch" ]; then ok "GPU $arch: native kernels present"
else note "GPU $arch: no native kernels for it in this release (DLSS runs, much slower)"; fi
fi
if [ "$external_backend" = true ]; then
note "AllowExternalBackend requires the patched OptiScaler DLL; keep native AMD identity visible for FSR4"
printf '\nSteam launch options for this patched setup:\n PROTON_ENABLE_NVAPI=1 DXVK_NVAPI_ALLOW_OTHER_DRIVERS=1 DXVK_NVAPI_GPU_ARCH=AD100 WINE_HIDE_AMD_GPU=0 %%command%%\n'
else
printf '\nSteam launch options for this game:\n PROTON_FORCE_NVAPI=1 DXVK_NVAPI_GPU_ARCH=AD100 %%command%%\n'
fi
[ "$problems" -eq 0 ] && printf '\nFiles and OptiScaler NGX routing checks passed (runtime initialization is not tested).\n' || printf '\n%d thing(s) to fix above.\n' "$problems"
[ "$problems" -eq 0 ]