From cb08e1c313c29f80a250dd8967fa798196a7bff5 Mon Sep 17 00:00:00 2001 From: ayo <93222141+countervolts@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:32:49 -0400 Subject: [PATCH] Fix OptiScaler NGX routing and align marker polling defaults - Explicitly route OptiScaler to d4r\nvngx.dll in release packages. - Extract INI generation into a helper that preserves line endings and rejects missing or duplicate settings. - Check NGX routing separately from file presence and return failure when the installation is incomplete or misconfigured. - Add an optional OptiScaler patch for external DLSS backends while preserving AMD identity for FSR4, with matching launch guidance. - Align portable and launcher marker polling defaults at 200 us. - Log when d4r's model override replaces game or OptiScaler presets. - Add configuration, package, routing and coexistence regression tests. - Update installation documentation, add FSR/DLSS comparison videos and ignore local .tools artifacts. Refs: #7, #14 --- .gitignore | 1 + README.md | 8 + docs/architecture.md | 8 +- docs/building.md | 3 +- external/MinHook | 1 + packaging/D4R_README.txt | 12 +- packaging/d4r-check.sh | 47 +++- packaging/d4r.ini | 2 + packaging/optiscaler.settings | 5 +- ...roton-external-dlss-fsr4-coexistence.patch | 205 ++++++++++++++++ scripts/configure_optiscaler.py | 43 ++++ scripts/d4r_config.py | 2 +- scripts/package_release.sh | 23 +- tests/test_optiscaler_coexistence.py | 80 ++++++ tests/test_optiscaler_packaging.py | 228 ++++++++++++++++++ tests/test_shim_config.py | 92 +++++++ tools/d4r_nvngx_shim.cpp | 9 +- 17 files changed, 738 insertions(+), 31 deletions(-) create mode 160000 external/MinHook create mode 100644 patches/optiscaler/0001-proton-external-dlss-fsr4-coexistence.patch create mode 100644 scripts/configure_optiscaler.py create mode 100644 tests/test_optiscaler_coexistence.py create mode 100644 tests/test_optiscaler_packaging.py create mode 100644 tests/test_shim_config.py diff --git a/.gitignore b/.gitignore index 104ea9d..71d5a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ __pycache__/ /vkd3d-proton.cache .vscode/ videos/ +.tools/ # Machine-specific notes, CI runner setup, local experiments, benchmark automation (kept locally, never published) /private/ diff --git a/README.md b/README.md index c41dc1c..cbb44a5 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ For reference, native 2560×1440 without upscaling (the game's TSR at 100%) runs On the same GPU DLSS starts at a disadvantage: its networks were designed for NVIDIA's tensor cores, and parts of them still run as translated NVIDIA code. How the numbers were measured, and what each optimisation contributed, is in [docs/performance.md](docs/performance.md). +## FSR 4.1.1 vs. DLSS 4.5 comparison + +- [Side-by-side comparison: FSR 4.1.1 (left) and DLSS 4.5 preset M (right)](https://cdn.ayois.gay/sk-ant-api03-4Dr0yN5YAzsuAOcyp7KEfeRyB0pbh2iAv2vfywa9QITHIbQScnVQnef9jsxJzDcvWMD13r8C1qGAoQQevjLqzGPT0bAQ9AA) +- [FSR 4.1.1 standalone video](https://cdn.ayois.gay/sk-ant-api03-YZpWqINTI2HBt6KWJTeyY2b9JJbxUTZdBVRR06KZD7b1x27QLjvQrZZX8Um5sZqkquuOlzJTHUh84FOOLsetNy-PAIiWxAA) +- [DLSS 4.5 preset M standalone video](https://cdn.ayois.gay/sk-ant-api03-0GLTr6qEYGfUKWYoZeseLdTzPslh1AvlVLdlBEUvV893bi19b7qha5wKnuQXN2tl6l4ffaRKj5warfAFDDjOlYJNYWVQ3AA) + ## Known issues - In some games using some models native upscaling can show visible artifacting. @@ -41,6 +47,8 @@ For users who prioritize fidelity over speed, set `[Kernels] PreferAccuracy = tr d4r builds for RDNA3 and RDNA4. A newly built release compiles native DLSS 4 and 4.5 network kernels for the targets below and selects the matching set at runtime. Only the RX 7700 XT has been tested by this project on a real GPU; an external video reports DLSS 4.5 running through d4r 0.1.2 on an RX 7900 XTX. RDNA4 runtime and performance remain unverified on hardware. +!WE ARE LOOKING FOR VIDEOS OF IT WORKING, IF YOU HAVE A MENTIONED HERE AND DOESNT HAVE A VIDEO PLEASE TAKE ONE! + | GPU | Chip | FP8 math | Runtime testing | |---|---|---|---| | RX 7700 XT, RX 7800 XT, RX 7700, Radeon PRO W7700 | gfx1101 | widened to f16 | RX 7700 XT only | diff --git a/docs/architecture.md b/docs/architecture.md index d1b40ba..05d2669 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -63,7 +63,7 @@ The release zip (`scripts/package_release.sh`) is unpacked into the folder that | File | Role | |---|---| -| `dxgi.dll`, `OptiScaler.ini` | OptiScaler 0.9.4. Its INI sets DLSS as the DX12 upscaler and `OptiDllPath=d4r`, so OptiScaler loads the shim as `d4r\nvngx.dll`. | +| `dxgi.dll`, `OptiScaler.ini` | OptiScaler 0.9.4. Its INI sets DLSS as the DX12 upscaler, `NvngxPath=d4r\nvngx.dll` to select the shim explicitly, and `OptiDllPath=d4r` for the other libraries. | | `d3d12.dll`, `d3d12core.dll` | the d4r-patched vkd3d-proton. Proton loads DLLs from the game folder before `system32`. | | `d4r\nvngx.dll`, `d4r\nvcuda.dll` | the shim and the CUDA bridge | | `d4r\zluda\libcuda.so` | ZLUDA | @@ -71,6 +71,12 @@ The release zip (`scripts/package_release.sh`) is unpacked into the folder that | `d4r\d4r.ini` | this game's settings | | `d4r\nvngx_dlss.dll`, `d4r\ngx\_nvngx.dll` | NVIDIA's files (the packager's `D4R_BUNDLE_NVIDIA=0` leaves them for the user to add) | +**NGX routing.** OptiScaler tries `_nvngx.dll` before `nvngx.dll`, with system fallback for each name. `OptiDllPath=d4r` alone can therefore select Proton's system `_nvngx.dll` before reaching the shim. A file-valued `NvngxPath` takes priority on the first probe, regardless of the probe's name. It must select `d4r\nvngx.dll`, never `d4r\ngx\_nvngx.dll`: the latter is NVIDIA's core, which the shim loads internally. See OptiScaler 0.9.4's [NGX loader](https://github.com/optiscaler/OptiScaler/blob/v0.9.4/OptiScaler/proxies/NVNGX_Proxy.h) and [override handling](https://github.com/optiscaler/OptiScaler/blob/v0.9.4/OptiScaler/Util.cpp). + +OptiScaler 0.9.4 checks a relative `NvngxPath` against the process working directory; it does not anchor that override to the executable like it does `OptiDllPath`. The portable INI assumes the game's working directory is the executable folder. Launchers that use another working directory need an absolute Windows filename for the shim. The Proton developer game launcher and OptiScaler harness already generate that form for `bin/d4r_nvngx.dll`. + +`sh d4r/d4r-check.sh` checks file presence and the explicit NGX route separately; it does not prove runtime initialization. OptiScaler's startup `nvngx.dll not found!` warning checks the executable folder and already loaded modules, not the override. It can appear before the shim loads from `d4r/`; verify the later `NVNGXProxy::InitNVNGX Loaded from ...\d4r\nvngx.dll` line and `d4r/d4r_nvngx.log` instead. Keep the shim in `d4r/`. + **Settings.** When an `d4r.ini` sits next to the shim, the shim turns it into the same environment variables the developer launcher sets, with the release's defaults. A variable that is already set, for example in the launch options, wins. Settings for the Linux side (ZLUDA's and the bridge's) are handed to the bridge through `d4rSetEnv` right after it is loaded, before its first CUDA call. **Loading ROCm.** Steam runs GE-Proton 11 inside its container runtime (SteamLinuxRuntime_4), whose library search path has no ROCm. The bridge therefore: diff --git a/docs/building.md b/docs/building.md index ed84278..60cd488 100644 --- a/docs/building.md +++ b/docs/building.md @@ -110,7 +110,7 @@ scripts/package_release.sh # -> dist/d4r-.zip 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 `-fp8` variant; - writes the kernel manifest from the DLLs you list (it records hashes of their PTX, nothing else); -- stages OptiScaler as `dxgi.dll` with the settings in `packaging/optiscaler.settings`; +- stages OptiScaler as `dxgi.dll` with the settings in `packaging/optiscaler.settings`, applied by `scripts/configure_optiscaler.py`; both full and clean ZIPs set `NvngxPath=d4r\nvngx.dll` explicitly, retaining `OptiDllPath=d4r` for other libraries; - adds NVIDIA's two DLLs and any supplied texture kernels (built from NVIDIA's PTX by `kernels/build.sh tex`); builds accuracy texture sets with `D4R_ZLUDA_EMIT` unless marked prebuilt sets are supplied in `D4R_BUNDLE_TEX/accuracy/`; - zips the result together with the ZLUDA and vkd3d-proton builds, the ROCm runtime (as `d4r/rocm`), `packaging/d4r.ini`, the licenses and the patches. @@ -143,6 +143,7 @@ Run `bash scripts/build_release_glibc241.sh` inside the container. The script ch ## Checks +- `python3 -m unittest discover -s tests -v` includes OptiScaler INI generation, full/clean package staging and NGX-routing checker regressions. Package tests use fixture binaries and GPU-build stand-ins; real DLL loading still needs the OptiScaler/Proton harness. - `scripts/check_environment.sh` lists the tools, GPU and Proton builds it finds. - The D3D12 harness (`scripts/run_d3d12_dlss_harness_proton.sh`) drives DLSS outside a game. - Native kernels have their own validation path; see [native-kernels.md](native-kernels.md). diff --git a/external/MinHook b/external/MinHook new file mode 160000 index 0000000..c3fcafd --- /dev/null +++ b/external/MinHook @@ -0,0 +1 @@ +Subproject commit c3fcafdc10146beb5919319d0683e44e3c30d537 diff --git a/packaging/D4R_README.txt b/packaging/D4R_README.txt index 26c52cb..cafb63d 100644 --- a/packaging/D4R_README.txt +++ b/packaging/D4R_README.txt @@ -78,6 +78,11 @@ Settings some GPU time. Restart after changing it; other scenes and RDNA4 hardware still need testing. - OptiScaler.ini: OptiScaler's settings, such as its fps overlay and the render resolution of each quality mode. Press Insert in game for OptiScaler's menu. + [Libraries] NvngxPath=d4r\nvngx.dll explicitly selects d4r's shim; OptiDllPath=d4r is also + needed for the other DLLs. Keep this route when restoring a customized INI during an update. + Do not point NvngxPath at d4r\ngx\_nvngx.dll: that is NVIDIA's core for the shim's internal use. + OptiScaler 0.9.4 resolves this relative override from the game's working directory. If a custom + launcher uses a different working directory, use the shim's absolute Windows filename instead. Each game has its own copy of both files. @@ -112,8 +117,11 @@ If something goes wrong - d4r's log is d4r/d4r_nvngx.log. It is rewritten at every launch. Lines starting with "d4r:" and "nvcuda bridge:" name missing files or libraries. - DLSS missing in the game's menu, or no d4r_nvngx.log: check the launch options and that the - files are next to the game's main .exe. For OptiScaler's own log, set LogToFile=true in - OptiScaler.ini's [Log] section. + files are next to the game's main .exe. Run sh d4r/d4r-check.sh: it checks file presence and + OptiScaler's explicit NGX route separately. For OptiScaler's own log, set LogToFile=true in + OptiScaler.ini's [Log] section. Its later "NVNGXProxy::InitNVNGX Loaded from" line should name + d4r\nvngx.dll, not C:\windows\system32\_nvngx.dll. The startup "nvngx.dll not found!" warning + can be expected with the shim under d4r/; do not move it into the game root to silence that check. - A wrong image: try Model = E in d4r/d4r.ini, and report the problem with both logs. - Do not use d4r in games with anti-cheat. OptiScaler's DLL injection can get an account banned. - Not supported: DLSS Frame Generation, DLSS Ray Reconstruction, DirectX 11 and Vulkan games. diff --git a/packaging/d4r-check.sh b/packaging/d4r-check.sh index e3bdf43..578a538 100755 --- a/packaging/d4r-check.sh +++ b/packaging/d4r-check.sh @@ -14,6 +14,15 @@ 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 } + dlss && tolower(line) ~ /^allowexternalbackend[ \t]*=/ { + count++; sub(/^[^=]*=[ \t]*/, "", line); value = tolower(line) + } + END { if (count == 1 && value == "true") print "true" } +' "$GAME/OptiScaler.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; } @@ -21,6 +30,34 @@ ls "$GAME"/*.exe >/dev/null 2>&1 && ok "game executable next to d4r/" || note "n 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 [ -f "$GAME/$f" ] && ok "$f" || bad "$f (re-extract the d4r zip)" done +# 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:]') + # 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' ] || + [ "$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 @@ -49,5 +86,11 @@ if [ -n "$target" ]; then else note "GPU $arch: no native kernels for it in this release (DLSS runs, much slower)"; fi fi -printf '\nSteam launch options for this game:\n PROTON_FORCE_NVAPI=1 DXVK_NVAPI_GPU_ARCH=AD100 %%command%%\n' -[ "$problems" -eq 0 ] && printf '\nEverything d4r needs is in place.\n' || printf '\n%d thing(s) to fix above.\n' "$problems" +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 ] diff --git a/packaging/d4r.ini b/packaging/d4r.ini index e6d5b7c..dcc24f6 100644 --- a/packaging/d4r.ini +++ b/packaging/d4r.ini @@ -52,6 +52,8 @@ NativeFp8 = on MotionVectorDilation = 0 ; Keep inputs and output in video memory. Input and output readiness waits use CPU sleeps. VramInterop = true +; Microseconds between checks for the game's input marker (default 200; 0 yields). +MarkerPollUs = 200 LinearInputs = true ElideNgxSync = true ; Lets DLSS's last kernel write straight into the game's output buffer (DLSS 4 / K only). It takes diff --git a/packaging/optiscaler.settings b/packaging/optiscaler.settings index ecb7b30..5d49d41 100644 --- a/packaging/optiscaler.settings +++ b/packaging/optiscaler.settings @@ -1,9 +1,12 @@ # OptiScaler.ini values the d4r release sets (Section.Key=Value); everything else keeps OptiScaler's # default. scripts/package_release.sh applies them to the OptiScaler.ini of the bundled release. -# DLSS as the DX12 upscaler, loaded from the d4r folder (OptiScaler looks for nvngx.dll there) +# DLSS as the DX12 upscaler. Select the shim by filename: OptiDllPath alone allows +# OptiScaler's first _nvngx.dll probe to load Proton's system core before trying nvngx.dll. Upscalers.Dx12Upscaler=dlss DLSS.Enabled=true Libraries.OptiDllPath=d4r +Libraries.NvngxPath=d4r\nvngx.dll +# Do not use d4r\ngx\_nvngx.dll here: that NVIDIA core is loaded internally by the shim. # The game's DLSS calls reach OptiScaler; d4r's own load of NVIDIA's NGX core is left alone Inputs.EnableDlssInputs=true Hooks.HookOriginalNvngxOnly=true diff --git a/patches/optiscaler/0001-proton-external-dlss-fsr4-coexistence.patch b/patches/optiscaler/0001-proton-external-dlss-fsr4-coexistence.patch new file mode 100644 index 0000000..01fc455 --- /dev/null +++ b/patches/optiscaler/0001-proton-external-dlss-fsr4-coexistence.patch @@ -0,0 +1,205 @@ +diff --git a/OptiScaler.ini b/OptiScaler.ini +index a9b7a88..adbb206 100644 +--- a/OptiScaler.ini ++++ b/OptiScaler.ini +@@ -612,6 +612,11 @@ FsrAgilitySDKUpgrade=auto + ; true or false - Default (auto) is true + Enabled=auto + ++; Permit an explicitly configured external NGX shim on non-Nvidia hardware. ++; NvngxPath must name an existing DLL file; keep physical GPU identity for FSR4. ++; false by default; this does not make Nvidia's driver NGX core work on AMD. ++AllowExternalBackend=false ++ + ; Set this to true to enable custom render preset overrides + ; true or false - Default (auto) is false + RenderPresetOverride=auto +diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp +index d0265c4..5403430 100644 +--- a/OptiScaler/Config.cpp ++++ b/OptiScaler/Config.cpp +@@ -263,6 +263,7 @@ bool Config::Reload(std::filesystem::path iniPath) + { + // Don't enable again if set false because of no nvngx found + DLSSEnabled.set_from_config(readBool("DLSS", "Enabled")); ++ DLSSAllowExternalBackend.set_from_config(readBool("DLSS", "AllowExternalBackend")); + UseGenericAppIdWithDlss.set_from_config(readBool("DLSS", "UseGenericAppIdWithDlss")); + + RenderPresetOverride.set_from_config(readBool("DLSS", "RenderPresetOverride")); +@@ -998,6 +999,8 @@ bool Config::SaveIni() + // DLSS + { + ini.SetValue("DLSS", "Enabled", GetBoolValue(Instance()->DLSSEnabled.value_for_config()).c_str()); ++ ini.SetValue("DLSS", "AllowExternalBackend", ++ GetBoolValue(Instance()->DLSSAllowExternalBackend.value_for_config()).c_str()); + ini.SetValue("DLSS", "RenderPresetOverride", + GetBoolValue(Instance()->RenderPresetOverride.value_for_config()).c_str()); + ini.SetValue("DLSS", "RenderPresetForAll", +diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h +index d7909b7..0225c87 100644 +--- a/OptiScaler/Config.h ++++ b/OptiScaler/Config.h +@@ -218,6 +218,7 @@ class Config + + // DLSS + CustomOptional DLSSEnabled { true }; ++ CustomOptional DLSSAllowExternalBackend { false }; + CustomOptional RenderPresetOverride { false }; + CustomOptional RenderPresetForAll { 0 }; + CustomOptional RenderPresetDLAA { 0 }; +diff --git a/OptiScaler/dllmain.cpp b/OptiScaler/dllmain.cpp +index 7580c65..e5f047c 100644 +--- a/OptiScaler/dllmain.cpp ++++ b/OptiScaler/dllmain.cpp +@@ -1,4 +1,5 @@ + #include "pch.h" ++#include "spoofing/BackendIdentity.h" + #include "dllmain.h" + + #include "Util.h" +@@ -1977,9 +1978,29 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv + spdlog::info(""); + State::Instance().isRunningOnNvidia = isNvidia(); + +- if (State::Instance().isRunningOnNvidia) ++ // External NGX backends (e.g. d4r) do not require a physical Nvidia GPU. ++ // Keep isRunningOnNvidia truthful so FSR4 and other hardware checks remain valid. ++ bool externalBackendFile = false; ++ if (Config::Instance()->DLSSAllowExternalBackend.value_or_default() && ++ Config::Instance()->NvngxPath.has_value()) ++ { ++ auto backendPath = std::filesystem::path(Config::Instance()->NvngxPath.value()); ++ if (backendPath.is_relative()) ++ backendPath = Util::ExePath().parent_path() / backendPath; ++ std::error_code error; ++ externalBackendFile = std::filesystem::is_regular_file(backendPath, error); ++ if (externalBackendFile) ++ Config::Instance()->NvngxPath.set_volatile_value(backendPath.wstring()); ++ else ++ spdlog::warn("External DLSS requires NvngxPath to name an existing shim DLL file"); ++ } ++ ++ if (BackendIdentity::AllowDlss(State::Instance().isRunningOnNvidia, ++ Config::Instance()->DLSSAllowExternalBackend.value_or_default(), ++ externalBackendFile)) + { +- spdlog::info("Running on Nvidia"); ++ spdlog::info(State::Instance().isRunningOnNvidia ? "Running on Nvidia" : ++ "External DLSS backend enabled; preserving physical GPU identity for FSR4"); + + if (!State::Instance().NVNGX_DLSS_Path.has_value() && + Config::Instance()->NVNGX_DLSS_Library.has_value()) +@@ -2020,7 +2041,7 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv + Config::Instance()->DLSSEnabled.set_volatile_value(false); + } + +- if (!Config::Instance()->DxgiSpoofing.has_value()) ++ if (State::Instance().isRunningOnNvidia && !Config::Instance()->DxgiSpoofing.has_value()) + { + spdlog::info("Disabling DxgiSpoofing"); + Config::Instance()->DxgiSpoofing.set_volatile_value(false); +diff --git a/OptiScaler/spoofing/BackendIdentity.h b/OptiScaler/spoofing/BackendIdentity.h +new file mode 100644 +index 0000000..c0e6a19 +--- /dev/null ++++ b/OptiScaler/spoofing/BackendIdentity.h +@@ -0,0 +1,41 @@ ++#pragma once ++ ++#include ++ ++// Physical hardware identity is independent of external DLSS availability. ++namespace BackendIdentity ++{ ++inline bool AllowDlss(bool isNvidia, bool allowExternalBackend, bool explicitBackendFile) ++{ ++ return isNvidia || (allowExternalBackend && explicitBackendFile); ++} ++ ++inline bool IsAmdFsrCaller(std::string_view caller) ++{ ++ constexpr std::string_view modules[] = { ++ "amdxc64.dll", "amdxcffx64.dll", "amd_fidelityfx_dx12.dll", ++ "amd_fidelityfx_loader_dx12.dll", "amd_fidelityfx_upscaler_dx12.dll", ++ "amd_fidelityfx_framegeneration_dx12.dll", "amd_fidelityfx_denoiser_dx12.dll", ++ "amd_fidelityfx_radiancecache_dx12.dll" ++ }; ++ for (auto module : modules) ++ { ++ if (caller.size() != module.size()) ++ continue; ++ bool match = true; ++ for (size_t index = 0; index < caller.size(); ++index) ++ { ++ const char c = caller[index]; ++ const char lower = c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; ++ if (lower != module[index]) ++ { ++ match = false; ++ break; ++ } ++ } ++ if (match) ++ return true; ++ } ++ return false; ++} ++} +diff --git a/OptiScaler/spoofing/Dxgi_Spoofing.cpp b/OptiScaler/spoofing/Dxgi_Spoofing.cpp +index 41b61b6..bd98862 100644 +--- a/OptiScaler/spoofing/Dxgi_Spoofing.cpp ++++ b/OptiScaler/spoofing/Dxgi_Spoofing.cpp +@@ -1,5 +1,6 @@ + #include "pch.h" + #include "Dxgi_Spoofing.h" ++#include "BackendIdentity.h" + + #include + +@@ -48,7 +49,7 @@ HRESULT DxgiSpoofing::hkGetDesc3(IDXGIAdapter4* This, DXGI_ADAPTER_DESC3* pDesc) + + auto caller = Util::WhoIsTheCaller(_ReturnAddress()); + +- if (iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || ++ if (BackendIdentity::IsAmdFsrCaller(caller) || iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || + iequals(caller, "dxgi.dll") || iequals(caller, "d3d12.dll") || iequals(caller, "d3d12Core.dll")) + { + return result; +@@ -109,7 +110,7 @@ HRESULT DxgiSpoofing::hkGetDesc2(IDXGIAdapter2* This, DXGI_ADAPTER_DESC2* pDesc) + + auto caller = Util::WhoIsTheCaller(_ReturnAddress()); + +- if (iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || ++ if (BackendIdentity::IsAmdFsrCaller(caller) || iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || + iequals(caller, "dxgi.dll") || iequals(caller, "d3d12.dll") || iequals(caller, "d3d12Core.dll")) + { + return result; +@@ -170,7 +171,7 @@ HRESULT DxgiSpoofing::hkGetDesc1(IDXGIAdapter1* This, DXGI_ADAPTER_DESC1* pDesc) + + auto caller = Util::WhoIsTheCaller(_ReturnAddress()); + +- if (iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || ++ if (BackendIdentity::IsAmdFsrCaller(caller) || iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || + iequals(caller, "dxgi.dll") || iequals(caller, "d3d12.dll") || iequals(caller, "d3d12Core.dll")) + { + return result; +@@ -231,7 +232,7 @@ HRESULT DxgiSpoofing::hkGetDesc(IDXGIAdapter* This, DXGI_ADAPTER_DESC* pDesc) + + auto caller = Util::WhoIsTheCaller(_ReturnAddress()); + +- if (iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || ++ if (BackendIdentity::IsAmdFsrCaller(caller) || iequals(caller, "fakenvapi.dll") || iequals(caller, "vulkan-1.dll") || iequals(caller, "amdvlk64.dll") || + iequals(caller, "dxgi.dll") || iequals(caller, "d3d12.dll") || iequals(caller, "d3d12Core.dll")) + { + return result; +diff --git a/OptiScaler/upscalers/fsr31/FSR31Feature_Dx12.cpp b/OptiScaler/upscalers/fsr31/FSR31Feature_Dx12.cpp +--- a/OptiScaler/upscalers/fsr31/FSR31Feature_Dx12.cpp ++++ b/OptiScaler/upscalers/fsr31/FSR31Feature_Dx12.cpp +@@ -848,6 +848,7 @@ + State::Instance().ffxUpscalerVersionNames[Config::Instance()->FfxUpscalerIndex.value_or_default()]; + _name = "FSR"; + parse_version(version); ++ LOG_INFO("FSR context initialized: provider {}, versionId {:#x}", version, override.versionId); + } + + SetInit(true); diff --git a/scripts/configure_optiscaler.py b/scripts/configure_optiscaler.py new file mode 100644 index 0000000..89ce1a4 --- /dev/null +++ b/scripts/configure_optiscaler.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Apply release settings to the supplied OptiScaler INI, preserving its defaults/CRLF.""" +import re +import sys +from pathlib import Path + + +def configure(source, settings): + lines = source.split("\n") + for setting in settings.splitlines(): + setting = setting.strip() + if not setting or setting.startswith("#"): + continue + path, value = setting.split("=", 1) + section, key = path.split(".", 1) + starts = [i for i, line in enumerate(lines) if line.strip().casefold() == f"[{section}]".casefold()] + if len(starts) != 1: + raise ValueError(f"OptiScaler.ini must have exactly one [{section}]") + start = starts[0] + end = next((i for i in range(start + 1, len(lines)) if lines[i].lstrip().startswith("[")), len(lines)) + matches = [i for i in range(start + 1, end) + if re.match(rf"\s*{re.escape(key)}\s*=", lines[i], re.IGNORECASE)] + if len(matches) != 1: + raise ValueError(f"OptiScaler.ini must have exactly one {key} in [{section}]") + i = matches[0] + lines[i] = f"{key}={value}" + ("\r" if lines[i].endswith("\r") else "") + return "\n".join(lines) + + +def main(): + source, settings, output = map(Path, sys.argv[1:]) + try: + # read_text() would translate CRLF before we could preserve it. + with source.open(encoding="utf-8", newline="") as handle: + result = configure(handle.read(), settings.read_text(encoding="utf-8")) + except ValueError as error: + sys.exit(str(error)) + with output.open("w", encoding="utf-8", newline="") as handle: + handle.write(result) + + +if __name__ == "__main__": + main() diff --git a/scripts/d4r_config.py b/scripts/d4r_config.py index 2ac1710..04febeb 100755 --- a/scripts/d4r_config.py +++ b/scripts/d4r_config.py @@ -146,7 +146,7 @@ def main(argv): value = flag("Interop", key) if value is not None: env[var] = "1" if value else "0" - value = get("Interop", "MarkerPollUs") + value = get("Interop", "MarkerPollUs") or "200" dilation = get("Interop", "MotionVectorDilation") if dilation is not None and dilation not in ("0", "1", "2"): fail("[Interop] MotionVectorDilation must be 0, 1 or 2") diff --git a/scripts/package_release.sh b/scripts/package_release.sh index c467f69..45829c3 100755 --- a/scripts/package_release.sh +++ b/scripts/package_release.sh @@ -77,27 +77,8 @@ else esac fi cp "$OPTI/OptiScaler.dll" "$STAGE/dxgi.dll" -python3 - "$OPTI/OptiScaler.ini" "$ROOT/packaging/optiscaler.settings" "$STAGE/OptiScaler.ini" <<'PY' -import re, sys -lines = open(sys.argv[1], encoding="utf-8", newline="").read().split("\n") # keeps the file's line endings -for setting in open(sys.argv[2], encoding="utf-8"): - setting = setting.strip() - if not setting or setting.startswith("#"): - continue - path, value = setting.split("=", 1) - section, key = path.split(".", 1) - start = next((i for i, l in enumerate(lines) if l.strip() == f"[{section}]"), None) - if start is None: - sys.exit(f"OptiScaler.ini has no [{section}]") - end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines)) - for i in range(start + 1, end): - if re.match(rf"{re.escape(key)}\s*=", lines[i]): - lines[i] = f"{key}={value}" + ("\r" if lines[i].endswith("\r") else "") - break - else: - sys.exit(f"OptiScaler.ini has no {key} in [{section}]") -open(sys.argv[3], "w", encoding="utf-8", newline="").write("\n".join(lines)) -PY +python3 "$ROOT/scripts/configure_optiscaler.py" \ + "$OPTI/OptiScaler.ini" "$ROOT/packaging/optiscaler.settings" "$STAGE/OptiScaler.ini" # d4r-patched vkd3d-proton: same-frame DLSS results cp "$VKD3D/d3d12.dll" "$VKD3D/d3d12core.dll" "$STAGE/" diff --git a/tests/test_optiscaler_coexistence.py b/tests/test_optiscaler_coexistence.py new file mode 100644 index 0000000..1b83e97 --- /dev/null +++ b/tests/test_optiscaler_coexistence.py @@ -0,0 +1,80 @@ +"""CPU regression checks for the upstream OptiScaler coexistence patch.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +PATCH = ROOT / 'patches/optiscaler/0001-proton-external-dlss-fsr4-coexistence.patch' + + +class OptiScalerCoexistenceTests(unittest.TestCase): + def test_backend_identity_policy(self): + patch = PATCH.read_text() + start = patch.index('diff --git a/OptiScaler/spoofing/BackendIdentity.h ') + section = patch[start:].split('\ndiff --git ', 1)[0] + # Compile the exact added header that an upstream build will consume. + header = '\n'.join(line[1:] for line in section.splitlines() + if line.startswith('+') and not line.startswith('+++')) + code = r''' +#include "BackendIdentity.h" +#include +int main() { + for (bool nvidia : {false, true}) + for (bool optedIn : {false, true}) + for (bool file : {false, true}) + assert(BackendIdentity::AllowDlss(nvidia, optedIn, file) == + (nvidia || (optedIn && file))); + const char* amd[] = {"amdxc64.dll", "AMDXCFFX64.DLL", "amd_fidelityfx_dx12.dll", + "amd_fidelityfx_loader_dx12.dll", "amd_fidelityfx_upscaler_dx12.dll", + "amd_fidelityfx_framegeneration_dx12.dll", "amd_fidelityfx_denoiser_dx12.dll", + "amd_fidelityfx_radiancecache_dx12.dll"}; + for (auto caller : amd) assert(BackendIdentity::IsAmdFsrCaller(caller)); + const char* game[] = {"Game.exe", "Game-Win64-Shipping.exe", "nvngx.dll", + "nvngx_dlss.dll", "notamdxc64.dll", "amdxc64.dll.exe", "", "AMDXC64"}; + for (auto caller : game) assert(!BackendIdentity::IsAmdFsrCaller(caller)); +} +''' + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + (directory / 'BackendIdentity.h').write_text(header) + runner = directory / 'identity' + subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror', '-I', temporary, + '-x', 'c++', '-', '-o', str(runner)], input=code, + text=True, capture_output=True, check=True) + subprocess.run([str(runner)], check=True) + + def test_upstream_integration_preserves_hardware_identity(self): + additions = '\n'.join(line[1:] for line in PATCH.read_text().splitlines() + if line.startswith('+') and not line.startswith('+++')) + self.assertEqual(additions.count('BackendIdentity::IsAmdFsrCaller(caller)'), 4) + self.assertIn('DLSSAllowExternalBackend { false }', additions) + self.assertIn('readBool("DLSS", "AllowExternalBackend")', additions) + self.assertIn('ini.SetValue("DLSS", "AllowExternalBackend"', additions) + self.assertIn('std::filesystem::is_regular_file(backendPath, error)', additions) + self.assertIn('backendPath = Util::ExePath().parent_path() / backendPath', additions) + self.assertIn('NvngxPath.set_volatile_value(backendPath.wstring())', additions) + self.assertIn('if (State::Instance().isRunningOnNvidia && !Config::Instance()->DxgiSpoofing.has_value())', additions) + self.assertNotIn('isRunningOnNvidia = true', additions) + self.assertNotIn('isRunningOnRDNA4 = true', additions) + + def test_checker_launch_options_follow_external_backend_config(self): + with tempfile.TemporaryDirectory() as temporary: + game = Path(temporary) + (game / 'd4r').mkdir() + for flag in ('true', 'false', 'auto'): + (game / 'OptiScaler.ini').write_bytes( + (f'[DLSS]\r\nAllowExternalBackend={flag}\r\n' + '[Libraries]\r\nNvngxPath=d4r\\nvngx.dll\r\n').encode()) + result = subprocess.run(['sh', str(ROOT / 'packaging/d4r-check.sh'), temporary], + text=True, capture_output=True) + if flag == 'true': + self.assertIn('WINE_HIDE_AMD_GPU=0 %command%', result.stdout) + self.assertNotIn('PROTON_FORCE_NVAPI=1', result.stdout) + self.assertNotIn('requires the game\'s working directory', result.stdout) + else: + self.assertIn('PROTON_FORCE_NVAPI=1', result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_optiscaler_packaging.py b/tests/test_optiscaler_packaging.py new file mode 100644 index 0000000..9edf722 --- /dev/null +++ b/tests/test_optiscaler_packaging.py @@ -0,0 +1,228 @@ +"""Release NGX routing: real INI generation, full/clean ZIP staging, install diagnostics. + +Package tests use disposable runtime files and stand-ins for GPU kernel compilation. +Actual OptiScaler/Proton loading additionally needs the D3D12 harness. +""" +import configparser +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +SETTINGS = ROOT / 'packaging/optiscaler.settings' +GENERATOR = ROOT / 'scripts/configure_optiscaler.py' + + +def upstream_ini(): + sections = {} + for line in SETTINGS.read_text().splitlines(): + if not line or line.startswith('#'): + continue + path, _ = line.split('=', 1) + section, key = path.split('.', 1) + sections.setdefault(section, []).append(key) + # Keep NvngxPath independent of the settings file so removing the release + # override reproduces the original bug rather than removing the fixture key. + if 'NvngxPath' not in sections['Libraries']: + sections['Libraries'].append('NvngxPath') + return '; upstream comment\n' + ''.join( + f'[{section}]\n' + ''.join(f'{key}=auto\n' for key in keys) + for section, keys in sections.items()) + '[Menu]\nShowFps=true\n' + + +class OptiScalerConfigTests(unittest.TestCase): + def generate(self, directory, source): + source_path = directory / 'upstream.ini' + output = directory / 'OptiScaler.ini' + source_path.write_bytes(source.encode()) + result = subprocess.run(['python3', str(GENERATOR), str(source_path), str(SETTINGS), str(output)], + capture_output=True, text=True) + return result, output + + def assert_routing(self, data): + ini = configparser.ConfigParser() + ini.read_string(data.decode()) + self.assertEqual(ini['Libraries']['NvngxPath'], r'd4r\nvngx.dll') + self.assertEqual(ini['Libraries']['OptiDllPath'], 'd4r') + self.assertEqual(ini['Upscalers']['Dx12Upscaler'], 'dlss') + self.assertEqual(ini['DLSS']['Enabled'], 'true') + self.assertEqual(ini['Hooks']['HookOriginalNvngxOnly'], 'true') + self.assertEqual(ini['Menu']['ShowFps'], 'true') + + def test_generated_config_and_line_endings(self): + for newline in ('\n', '\r\n'): + with self.subTest(newline=repr(newline)), tempfile.TemporaryDirectory() as temporary: + source = upstream_ini().replace('\n', newline) + result, output = self.generate(Path(temporary), source) + self.assertEqual(result.returncode, 0, result.stderr) + data = output.read_bytes() + self.assert_routing(data) + self.assertTrue(data.startswith(b'; upstream comment' + newline.encode())) + self.assertEqual(data.count(b'\r\n'), data.count(b'\n') if newline == '\r\n' else 0) + self.assertEqual(source.count(newline), data.count(newline.encode())) + + def test_missing_or_duplicate_override_fails_without_output(self): + for replacement in ('', 'NvngxPath=auto\nnvngxpath=d4r\\ngx\\_nvngx.dll\n'): + with self.subTest(replacement=replacement), tempfile.TemporaryDirectory() as temporary: + result, output = self.generate(Path(temporary), upstream_ini().replace('NvngxPath=auto\n', replacement)) + self.assertNotEqual(result.returncode, 0) + self.assertIn('exactly one NvngxPath', result.stderr) + self.assertFalse(output.exists()) + + def test_missing_or_duplicate_libraries_section_fails(self): + for source in (upstream_ini().replace('[Libraries]', '[Other]'), + upstream_ini() + '[libraries]\nNvngxPath=auto\n'): + with tempfile.TemporaryDirectory() as temporary: + result, output = self.generate(Path(temporary), source) + self.assertNotEqual(result.returncode, 0) + self.assertIn('exactly one [Libraries]', result.stderr) + self.assertFalse(output.exists()) + + def test_full_and_clean_release_archives(self): + with tempfile.TemporaryDirectory() as temporary: + repo = Path(temporary) / 'repo' + inputs = Path(temporary) / 'inputs' + inputs.mkdir() + (repo / 'scripts').mkdir(parents=True) + for name in ('package_release.sh', 'configure_optiscaler.py', 'check_glibc_compat.sh'): + shutil.copy2(ROOT / 'scripts' / name, repo / 'scripts' / name) + shutil.copytree(ROOT / 'packaging', repo / 'packaging') + for name in ('LICENSE', 'NOTICE'): + shutil.copy2(ROOT / name, repo / name) + for component in ('zluda', 'vkd3d-proton'): + (repo / 'patches' / component).mkdir(parents=True) + (repo / 'patches' / component / 'fixture.patch').write_text('fixture\n') + # All shell staging, INI generation, ABI checks and archiving are real; + # replace only expensive GPU builds/manifests with fixture artifacts. + (repo / 'kernels/tools').mkdir(parents=True) + kernel_build = repo / 'kernels/build.sh' + kernel_build.write_text('#!/bin/sh\nmkdir -p "$2"\nprintf fixture > "$2/layer.hsaco"\n') + kernel_build.chmod(0o755) + (repo / 'kernels/tools/kernel_manifest.py').write_text( + 'import pathlib, sys\npathlib.Path(sys.argv[1], "d4r-kernels.txt").write_text("layer 1234\\n")\n') + + def fixture(path, data=b'fixture'): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + return path + + # A real minimal ELF lets the package's existing GLIBC checker run. + elf = inputs / 'library.so' + 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/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 / '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') + 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) + # Prebuilt L texture names keep the full flow from calling an emitter. + names = ['rrlite_dec0_4x4'] + for mv in ('mvhi', 'mvlo'): + for dynamic_range in ('hdr', 'ldr'): + names.append(f'rrlite_enc0_4x4_{mv}_{dynamic_range}') + names.extend(f'rrlite_post_{v}_{mv}_{dynamic_range}' for v in ('3_1', '3_2')) + for prefix in ('gfx1101', 'accuracy/gfx1101'): + for name in names: + fixture(inputs / 'tex' / prefix / (name + '.hsaco')) + fixture(inputs / 'tex' / prefix / 'd4r-accuracy.txt', b'1\n') + env = {k: v for k, v in os.environ.items() if not k.startswith('D4R_')} + env.update(D4R_SKIP_BUILD='1', D4R_GPU_ARCHS='gfx1101', SOURCE_DATE_EPOCH='1700000000', + D4R_OPTISCALER=str(inputs / 'opti'), D4R_DLSS_DLLS=str(dlss), + D4R_ZLUDA_DIR=str(inputs / 'zluda'), D4R_VKD3D_DIR=str(inputs / 'vkd3d'), + D4R_ROCM_RUNTIME=str(inputs / 'rocm'), D4R_OPTISCALER_LICENSE=str(license_path), + D4R_ZLUDA_SRC=str(inputs / 'source'), D4R_VKD3D_SRC=str(inputs / 'source'), + D4R_BUNDLE_DLSS=str(dlss), D4R_BUNDLE_NGX=str(core), D4R_BUNDLE_TEX=str(inputs / 'tex')) + version = (repo / 'packaging/VERSION').read_text().strip() + configs = [] + for bundled in ('1', '0'): + with self.subTest(bundled=bundled): + env['D4R_BUNDLE_NVIDIA'] = bundled + 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 '') + with zipfile.ZipFile(repo / 'dist' / (name + '.zip')) as archive: + self.assertIsNone(archive.testzip()) + data = archive.read('OptiScaler.ini') + self.assert_routing(data) + configs.append(data) + self.assertEqual(data, (repo / 'dist' / name / 'OptiScaler.ini').read_bytes()) + self.assertIn('d4r/nvngx.dll', archive.namelist()) + self.assertNotIn('nvngx.dll', archive.namelist()) + self.assertNotIn('d4r/_nvngx.dll', archive.namelist()) + self.assertEqual('d4r/ngx/_nvngx.dll' in archive.namelist(), bundled == '1') + self.assertEqual('d4r/nvngx_dlss.dll' in archive.namelist(), bundled == '1') + self.assertEqual(archive.read('d4r/d4r-check.sh'), (ROOT / 'packaging/d4r-check.sh').read_bytes()) + self.assertEqual(configs[0], configs[1]) + + +class InstallCheckTests(unittest.TestCase): + def test_routing_separate_from_file_presence(self): + with tempfile.TemporaryDirectory() as temporary: + game = Path(temporary) / 'game with spaces' + for name in ('Game.exe', 'dxgi.dll', 'd3d12.dll', 'd3d12core.dll', 'd4r/nvngx.dll', + 'd4r/nvcuda.dll', 'd4r/zluda/libcuda.so', 'd4r/d4r.ini', 'd4r/nvngx_dlss.dll', + 'd4r/ngx/_nvngx.dll', 'd4r/rocm/lib/libamdhip64.so.7'): + path = game / name + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + for value, good in ((r'd4r\nvngx.dll', True), ('./d4r/nvngx.dll', True), + ('Z:' + str(game / 'd4r/nvngx.dll').replace('/', '\\'), True), + ('auto', False), ('d4r', False), (r'd4r\ngx\_nvngx.dll', False), + (r'C:\windows\system32\_nvngx.dll', False), ('', False)): + with self.subTest(value=value): + setting = f'NvngxPath={value}\n' if value else '' + (game / 'OptiScaler.ini').write_bytes( + ('[Libraries]\nOptiDllPath=d4r\n' + setting).replace('\n', '\r\n').encode()) + result = subprocess.run(['sh', str(ROOT / 'packaging/d4r-check.sh'), str(game)], + text=True, capture_output=True) + self.assertIn('ok d4r/nvngx.dll', result.stdout) + if good: + self.assertIn('OptiScaler NGX routing: NvngxPath=', result.stdout) + self.assertNotIn('CONFIG', result.stdout) + else: + self.assertIn('CONFIG', result.stdout) + self.assertIn('OptiDllPath alone can load system _nvngx.dll', result.stdout) + self.assertNotIn('checks passed', result.stdout) + self.assertNotEqual(result.returncode, 0) + + def test_section_and_duplicate_keys(self): + for ini in ('[Other]\nNvngxPath=d4r\\nvngx.dll\n[Libraries]\nOptiDllPath=d4r\n', + '[Libraries]\nNvngxPath=d4r\\nvngx.dll\nnvngxpath=auto\n'): + with tempfile.TemporaryDirectory() as temporary: + game = Path(temporary) + (game / 'd4r').mkdir() + (game / 'OptiScaler.ini').write_text(ini) + result = subprocess.run(['sh', str(ROOT / 'packaging/d4r-check.sh'), temporary], + capture_output=True, text=True) + self.assertIn('CONFIG', result.stdout) + self.assertNotEqual(result.returncode, 0) + + def test_correct_routing_does_not_hide_missing_shim(self): + with tempfile.TemporaryDirectory() as temporary: + game = Path(temporary) + (game / 'd4r').mkdir() + (game / 'OptiScaler.ini').write_text('[Libraries]\nNvngxPath=d4r\\nvngx.dll\n') + result = subprocess.run(['sh', str(ROOT / 'packaging/d4r-check.sh'), temporary], + capture_output=True, text=True) + self.assertIn('MISSING d4r/nvngx.dll', result.stdout) + self.assertIn('OptiScaler NGX routing: NvngxPath=', result.stdout) + self.assertNotIn('checks passed', result.stdout) + self.assertNotEqual(result.returncode, 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_shim_config.py b/tests/test_shim_config.py new file mode 100644 index 0000000..59ac250 --- /dev/null +++ b/tests/test_shim_config.py @@ -0,0 +1,92 @@ +"""Marker polling defaults and the shim's model-override diagnostics.""" +import configparser +import os +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SHIM = ROOT / 'tools/d4r_nvngx_shim.cpp' + + +class ShimConfigTests(unittest.TestCase): + def test_marker_poll_defaults_agree(self): + for name in ('packaging/d4r.ini', 'config/d4r.ini.default'): + ini = configparser.ConfigParser() + ini.read(ROOT / name) + self.assertEqual(ini.getint('Interop', 'MarkerPollUs'), 200, name) + source = SHIM.read_text() + portable = re.search(r'portable_set\("D4R_SHIM_MARKER_POLL_US", poll.empty\(\) \? "(\d+)" : poll\);', source) + runtime = re.search(r'env_uint\("D4R_SHIM_MARKER_POLL_US", (\d+)\)', source) + self.assertIsNotNone(portable, 'portable marker polling fallback missing') + self.assertIsNotNone(runtime, 'runtime marker polling fallback missing') + self.assertEqual(int(portable[1]), 200) + self.assertEqual(int(runtime[1]), 200) + + def test_launcher_marker_poll_defaults_overrides_and_validation(self): + with tempfile.TemporaryDirectory() as temporary: + ini = Path(temporary) / 'd4r.ini' + env = {key: value for key, value in os.environ.items() if not key.startswith('D4R_')} + cmd = ['python3', str(ROOT / 'scripts/d4r_config.py'), '--config', str(ini)] + for value, expected in ((None, '200'), ('auto', '200'), ('200', '200'), ('0', '0'), ('75', '75')): + ini.write_text('[Interop]\n' + (f'MarkerPollUs = {value}\n' if value is not None else '')) + result = subprocess.run(cmd, env=env, text=True, capture_output=True, check=True) + self.assertIn(f'export D4R_SHIM_MARKER_POLL_US={expected}\n', result.stdout) + # An explicit environment variable keeps its existing precedence. + result = subprocess.run(cmd, env=dict(env, D4R_SHIM_MARKER_POLL_US='35'), + text=True, capture_output=True, check=True) + self.assertNotIn('export D4R_SHIM_MARKER_POLL_US=', result.stdout) + for value in ('-1', '0.5', 'invalid'): + ini.write_text(f'[Interop]\nMarkerPollUs = {value}\n') + result = subprocess.run(cmd, env=env, text=True, capture_output=True) + self.assertEqual(result.returncode, 2) + self.assertIn('MarkerPollUs must be a number of microseconds', result.stderr) + + def test_production_model_override_logs_and_presets(self): + # Compile the actual feature-creation preset block with fake NGX inputs + # and a captured logger, so the test exercises the production message + # and override behavior without requiring Windows, NGX or a GPU. + source = SHIM.read_text() + start = source.index(' const char* presetNames[] =') + end = source.index(' feature->preset = presets[1];', start) + block = source[start:end] + runner_source = r''' +#include +#include +#include +#include +static unsigned nextPreset = 21; +unsigned get_uint_or(void*, const char*, unsigned) { return nextPreset++; } +std::string env_string(const char* name) { const char* value = getenv(name); return value ? value : ""; } +void logf(const char* format, ...) { + va_list args; va_start(args, format); vprintf(format, args); va_end(args); puts(""); +} +int main() { + void* parameters = nullptr; +''' + block + r''' + printf("presets:"); for (unsigned preset : presets) printf(" %u", preset); puts(""); +} +''' + with tempfile.TemporaryDirectory() as temporary: + runner = Path(temporary) / 'model-override' + subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror', '-x', 'c++', '-', '-o', str(runner)], + input=runner_source, text=True, capture_output=True, check=True) + env = {key: value for key, value in os.environ.items() if key != 'D4R_DLSS_PRESET'} + for preset, model in ((5, 'E'), (11, 'K'), (12, 'L'), (13, 'M')): + with self.subTest(model=model): + output = subprocess.check_output([str(runner)], env=dict(env, D4R_DLSS_PRESET=str(preset)), text=True) + self.assertIn(f'D4R_DLSS_PRESET={preset} (model {model}) overrides game/OptiScaler presets', output) + self.assertIn('[DLAA=21 Quality=22 Balanced=23 Performance=24 UltraPerformance=25 UltraQuality=26]', output) + self.assertIn(f'using preset={preset} for all quality modes', output) + self.assertIn('presets:' + f' {preset}' * 6, output) + for value in (None, ''): + current = env if value is None else dict(env, D4R_DLSS_PRESET=value) + output = subprocess.check_output([str(runner)], env=current, text=True) + self.assertNotIn('model override', output) + self.assertEqual(output.strip(), 'presets: 21 22 23 24 25 26') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/d4r_nvngx_shim.cpp b/tools/d4r_nvngx_shim.cpp index 31255f6..61a2e8f 100644 --- a/tools/d4r_nvngx_shim.cpp +++ b/tools/d4r_nvngx_shim.cpp @@ -273,7 +273,7 @@ static FILE* g_log = nullptr; static HMODULE g_selfModule = nullptr; // --- portable install ---------------------------------------------------------- -// The drag-in release keeps this DLL (as nvngx.dll, which OptiScaler finds through its OptiDllPath), +// The drag-in release keeps this DLL (as nvngx.dll, explicitly selected by OptiScaler's NvngxPath), // the nvcuda bridge, ZLUDA and the native kernels in an d4r folder next to the game's executable, // with that game's settings in d4r\d4r.ini. The file's settings become the environment variables the // developer launcher sets (config/d4r.ini.default documents them), and a variable that is already set, @@ -563,7 +563,7 @@ static void load_portable_config() portable_set(setting.variable, ini_flag(ini, "interop", setting.key, setting.fallback) ? "1" : "0"); portable_set_unix("D4R_ELIDE_NGX_SYNC", ini_flag(ini, "interop", "ElideNgxSync", 1) ? "1" : "0"); const std::string poll = ini_value(ini, "interop", "MarkerPollUs"); - portable_set("D4R_SHIM_MARKER_POLL_US", poll.empty() ? "20" : poll); + portable_set("D4R_SHIM_MARKER_POLL_US", poll.empty() ? "200" : poll); // Direct output takes effect only while NGX runs the native output kernel (the bridge reports it); the // release cannot ship that kernel, which is built from NVIDIA's PTX. portable_set("D4R_SHIM_OUTPUT_DIRECT", ini_flag(ini, "interop", "DirectOutput", nativeOn) ? "1" : "0"); @@ -4429,6 +4429,11 @@ D4R_EXPORT NgxResult NVSDK_NGX_D3D12_CreateFeature(ID3D12GraphicsCommandList*, u if (!forced.empty()) { const unsigned int value = static_cast(strtoul(forced.c_str(), nullptr, 0)); + const char* model = value == 5 ? "E" : value == 11 ? "K" : value == 12 ? "L" : value == 13 ? "M" : "custom"; + logf("DLSS model override: D4R_DLSS_PRESET=%s (model %s) overrides game/OptiScaler presets " + "[DLAA=%u Quality=%u Balanced=%u Performance=%u UltraPerformance=%u UltraQuality=%u]; " + "using preset=%u for all quality modes", + forced.c_str(), model, presets[0], presets[1], presets[2], presets[3], presets[4], presets[5], value); for (unsigned int& preset : presets) preset = value; }