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
+1091 -39

No files matched your search

+83 -1
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,7 +531,16 @@ static void load_portable_config()
if (length == 0 || length >= MAX_PATH)
return;
std::wstring dir(module, length);
dir.resize(dir.find_last_of(L"\\/"));
// 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;
if (!read_whole_file(dir + L"\\d4r.ini", 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)