DS2Revival/mods/common/d3dhub.h
DS2Revival a1e2f8f9b5
Some checks failed
build / build (push) Failing after 1m35s
CI: build on the project's own runner at git.dsii.uk
2026-10-27 22:02:02 -04:00

404 lines
21 KiB
C++

// The shared Direct3D 9 hook layer of the DS2 plugins (DS2Widescreen: display, DS2Graphics: picture quality).
//
// The patches that cannot be shared by chaining - the Direct3DCreate9 import, the three Present calls and the "3D world
// finished" call of the interface pass - must exist once per process, however many plugins want them. This header is
// compiled into every plugin that needs them; the first of them to connect (plugins load in any order, all in DllMain)
// becomes the OWNER and installs the hooks from its copy of this code. Every later plugin finds the owner through a
// named mapping (per process) and only REGISTERS callbacks with it. A hook is installed only when some plugin registers a
// callback that needs it, so a plugin that needs none (nothing wanted) leaves the game's Direct3D alone, and a game with
// only one of the plugins behaves exactly as if that plugin had its own hooks.
//
// stage installed by the hub used for
// device events Direct3DCreate9 import -> CreateDevice / Reset slots BeforeCreate, AdjustParams, Created,
// BeforeReset, AfterReset
// per-frame the three Present calls of the renderer Frame (every plugin re-installs its own
// vtable hooks: the native Direct3D 9 on AMD
// rewrites a new device's table), Present
// (one plugin may replace the call itself)
// world finished the radar call at +0x19 of the interface pass WorldFinished (in priority order: HUD scale
// 0, anti-aliasing 100, photo 200), then the
// interface layer (BindInterface/Unbind)
//
// Callbacks run in ascending priority. Version skew: the API carries a version and every Callbacks a size; a plugin that
// finds an older/newer incompatible owner logs it and goes without D3D features instead of crashing.
#pragma once
#include "patch.h"
#include "vtable_hook.h"
#include <d3d9.h>
#include <tlhelp32.h>
#include <cstddef>
namespace ds2::d3dhub {
constexpr DWORD kApiVersion = 1;
// What Register() reports as available (the hooks the registered callbacks need and could be installed).
constexpr DWORD kHaveDevice = 1, kHavePresent = 2, kHaveWorld = 4;
constexpr int kPrioHud = 0, kPrioGraphics = 100, kPrioPhoto = 200;
using PresentFn = HRESULT(__stdcall*)(void* device, const RECT*, const RECT*, HWND, const void*);
struct Callbacks {
DWORD size = sizeof(Callbacks); // of the registrant's struct (older plugins have fewer members)
const char* name = "";
int priority = 0;
// The game is about to replace its device (front end <-> world).
void (*BeforeCreate)() = nullptr;
// May change `mine` (a copy of the game's presentation parameters) for CreateDevice/Reset, e.g. a larger back buffer;
// true = changed. If the call succeeds the game's own block keeps the back-buffer size it asked for.
bool (*AdjustParams)(D3DPRESENT_PARAMETERS* mine, const D3DPRESENT_PARAMETERS* game) = nullptr;
// After a successful CreateDevice / Reset. `game` is the game's view of the parameters; adjusted = AdjustParams applied.
void (*Created)(IDirect3DDevice9* dev, const D3DPRESENT_PARAMETERS* game, bool adjusted) = nullptr;
void (*BeforeReset)(IDirect3DDevice9* dev) = nullptr;
void (*AfterReset)(IDirect3DDevice9* dev, const D3DPRESENT_PARAMETERS* game, bool adjusted) = nullptr;
// Once per frame at Present, before the Present itself.
void (*Frame)(IDirect3DDevice9* dev) = nullptr;
// Replaces the Present call (one plugin only: the first to register wins). real = the device's Present.
HRESULT (*Present)(IDirect3DDevice9* dev, PresentFn real) = nullptr;
// The 3D world is finished and the interface not drawn yet (the back buffer holds only the world).
void (*WorldFinished)(IDirect3DDevice9* dev) = nullptr;
// Around the radar call of the interface pass (one plugin only): returns true when it bound something to undo.
bool (*BindInterface)(IDirect3DDevice9* dev) = nullptr;
void (*UnbindInterface)() = nullptr;
// Like BindInterface, but also gets the radar object (the call's `this`); preferred over BindInterface when set. A hub of an
// older build ignores this member (it copies only the members it knows) and calls BindInterface.
bool (*BindRadar)(IDirect3DDevice9* dev, void* radar) = nullptr;
};
struct Api {
DWORD version;
DWORD size;
DWORD (*Register)(const Callbacks* cb);
void (*SetRevalidate)(bool on);
const char* owner;
// Reuse the device instead of replacing it (see HookCreateDevice). Check `size` before calling: an older owner lacks it.
void (*SetReuseDevice)(bool on);
};
inline bool HasReuse(const Api* api) { return api && api->size >= offsetof(Api, SetReuseDevice) + sizeof(void*) && api->SetReuseDevice; }
namespace impl {
constexpr int kMax = 8;
inline Callbacks g_cb[kMax];
inline int g_n = 0;
inline bool g_revalidate = true;
inline IDirect3DDevice9* g_device = nullptr; // the device the game created last
// ReuseDevice: the device we keep alive with a reference of our own, and what it was created with.
inline bool g_reuse = false;
struct Kept {
IDirect3DDevice9* dev = nullptr;
IDirect3D9* d3d = nullptr;
UINT adapter = 0;
D3DDEVTYPE type = D3DDEVTYPE_HAL;
HWND focus = nullptr;
DWORD flags = 0;
};
inline Kept g_kept;
// "push 0 x4; push eax; call [ecx+0x44]" = device->Present(NULL, NULL, NULL, NULL) in the renderer (end of frame and two
// loading-screen paths). Located by signature (same code in Steam v2.3 at 0x50702e/0x509e1d/0x509e8a and Broken World at
// 0x5152fe/0x5180ed/0x51815a); each pattern starts 16 bytes before the "push 0" sequence.
inline const char* const kPresentPatterns[] = {
"84 C0 75 29 8B 86 C0 06 00 00 85 C0 74 1F 8B 08 6A 00 6A 00 6A 00 6A 00 50 FF 51 44 8A 86 F8 06 00 00",
"C3 07 00 00 84 C0 74 31 8B 86 C0 06 00 00 8B 08 6A 00 6A 00 6A 00 6A 00 50 FF 51 44 8A 86 F8 06 00 00",
"E8 ?? ?? ?? ?? 02 B3 01 8B 86 C0 06 00 00 8B 08 6A 00 6A 00 6A 00 6A 00 50 FF 51 44 84 DB 5B 74 14",
};
constexpr DWORD kPresentPatternOffset = 16;
inline const BYTE kPresentOrig[12] = {0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x50, 0xFF, 0x51, 0x44};
// "mov ecx,[esi+0x4c]; call <radar>" at +0x19 of the interface pass (Steam sub_4783cf / BW sub_480348).
inline const char* const kPassPattern = "A1 ?? ?? ?? ?? 83 78 1C 2C 56 8B F1 74 ?? 8B 4E 3C E8 ?? ?? ?? ?? 8B 4E 4C E8 ?? ?? ?? ?? A1";
using Direct3DCreate9_t = IDirect3D9*(WINAPI*)(UINT);
using CreateDevice_t = HRESULT(STDMETHODCALLTYPE*)(IDirect3D9*, UINT, D3DDEVTYPE, HWND, DWORD, D3DPRESENT_PARAMETERS*,
IDirect3DDevice9**);
using Reset_t = HRESULT(STDMETHODCALLTYPE*)(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*);
using Radar_t = void(__fastcall*)(void* self, void* edx);
inline HRESULT STDMETHODCALLTYPE HookCreateDevice(IDirect3D9* d3d, UINT adapter, D3DDEVTYPE type, HWND focus, DWORD flags,
D3DPRESENT_PARAMETERS* pp, IDirect3DDevice9** out);
inline HRESULT STDMETHODCALLTYPE HookReset(IDirect3DDevice9* dev, D3DPRESENT_PARAMETERS* pp);
inline HRESULT __stdcall HubPresent(void* device);
inline void __fastcall HookRadar(void* self, void* edx);
inline IDirect3D9* WINAPI HookDirect3DCreate9(UINT sdk);
// IDirect3D9::CreateDevice and IDirect3DDevice9::Reset (the ds2dev harness hooks the same slots; see vtable_hook.h)
inline ds2::VtableHook g_createDeviceHook(16, reinterpret_cast<void*>(HookCreateDevice));
inline ds2::VtableHook g_resetHook(16, reinterpret_cast<void*>(HookReset));
inline Direct3DCreate9_t g_Direct3DCreate9 = nullptr;
inline Radar_t g_radar = nullptr;
inline bool g_haveDevice = false, g_havePresent = false, g_haveWorld = false;
inline bool g_triedDevice = false, g_triedPresent = false, g_triedWorld = false;
template <class Call>
inline HRESULT WithAdjustedParams(D3DPRESENT_PARAMETERS* pp, Call call, bool* adjusted) {
*adjusted = false;
D3DPRESENT_PARAMETERS mine = *pp;
for (int i = 0; i < g_n; ++i)
if (g_cb[i].AdjustParams && g_cb[i].AdjustParams(&mine, pp)) *adjusted = true;
if (*adjusted) {
HRESULT hr = call(&mine);
if (SUCCEEDED(hr)) { // the game keeps the back-buffer size it asked for (it reads it back)
UINT w = pp->BackBufferWidth, h = pp->BackBufferHeight;
*pp = mine;
pp->BackBufferWidth = w;
pp->BackBufferHeight = h;
return hr;
}
ds2::log().Write("d3dhub: adjusted presentation parameters refused (0x%08lX), the game's own are used",
static_cast<unsigned long>(hr));
*adjusted = false;
}
return call(pp);
}
inline HRESULT STDMETHODCALLTYPE HookReset(IDirect3DDevice9* dev, D3DPRESENT_PARAMETERS* pp) {
for (int i = 0; i < g_n; ++i)
if (g_cb[i].BeforeReset) g_cb[i].BeforeReset(dev);
bool adjusted = false;
HRESULT hr = WithAdjustedParams(pp, [&](D3DPRESENT_PARAMETERS* p) { return g_resetHook.Original<Reset_t>(dev)(dev, p); }, &adjusted);
if (SUCCEEDED(hr))
for (int i = 0; i < g_n; ++i)
if (g_cb[i].AfterReset) g_cb[i].AfterReset(dev, pp, adjusted);
return hr;
}
// One line per device the game creates: the process's thread count and free address space. The game is a 32-bit process with 2 GB of
// address space and replaces its Direct3D device at every menu <-> world change; a device that something keeps alive after the game
// let go of it (an overlay or capture tool holding a reference) leaves its driver threads and memory behind, so the threads climb by
// about 15 per device and the free address space shrinks until an allocation fails (docs/handoff/bw-crash-load-after-mp.md). A report
// whose log shows that climb names the cause without a debugger.
inline void LogProcessState(const char* what) {
static int created = 0;
++created;
int threads = 0;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snap != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{sizeof(te)};
const DWORD pid = GetCurrentProcessId();
for (BOOL ok = Thread32First(snap, &te); ok; ok = Thread32Next(snap, &te))
if (te.th32OwnerProcessID == pid) ++threads;
CloseHandle(snap);
}
SYSTEM_INFO si;
GetSystemInfo(&si);
SIZE_T freeBytes = 0, largest = 0;
MEMORY_BASIC_INFORMATION mbi;
for (auto at = static_cast<const BYTE*>(si.lpMinimumApplicationAddress); at < static_cast<const BYTE*>(si.lpMaximumApplicationAddress);) {
if (!VirtualQuery(at, &mbi, sizeof(mbi))) break;
if (mbi.State == MEM_FREE) {
freeBytes += mbi.RegionSize;
if (mbi.RegionSize > largest) largest = mbi.RegionSize;
}
at = static_cast<const BYTE*>(mbi.BaseAddress) + mbi.RegionSize;
}
ds2::log().Write("d3dhub: %s #%d: %d threads, %u MB free address space (largest block %u MB)", what, created, threads,
static_cast<unsigned>(freeBytes >> 20), static_cast<unsigned>(largest >> 20));
}
// ReuseDevice: the game replaces its Direct3D device at every menu <-> world change. A device that something outside the game keeps
// alive after the game let go of it (a Steam overlay or capture tool holding a reference is the suspect in a player's crash report,
// docs/handoff/bw-crash-load-after-mp.md) is never destroyed: its driver threads and memory stay until the 32-bit game runs out of
// address space. With ReuseDevice the hub keeps a reference of its own to the device it handed out and, when the game asks for the
// next one with the same adapter, device type, focus window and behaviour flags, resets that device to the new parameters (the
// plugins' resources are released first, exactly as for a Reset) and hands it out again instead of creating another: one device for
// the whole session, whoever else holds it. Anything that goes wrong (the Reset is refused, other parameters) falls back to the
// game's own CreateDevice, which is what happens without the setting.
inline void DropKept() {
if (!g_kept.dev) return;
IDirect3DDevice9* d = g_kept.dev;
g_kept = Kept();
d->Release();
}
inline HRESULT ReuseKept(D3DPRESENT_PARAMETERS* pp, IDirect3DDevice9** out) {
IDirect3DDevice9* dev = g_kept.dev;
for (int i = 0; i < g_n; ++i)
if (g_cb[i].BeforeReset) g_cb[i].BeforeReset(dev);
bool adjusted = false;
HRESULT hr = WithAdjustedParams(pp, [&](D3DPRESENT_PARAMETERS* p) { return g_resetHook.Original<Reset_t>(dev)(dev, p); }, &adjusted);
if (FAILED(hr)) {
ds2::log().Write("d3dhub: ReuseDevice: Reset refused (0x%08lX), the game creates a new device", static_cast<unsigned long>(hr));
return hr;
}
dev->AddRef(); // the game's reference
*out = dev;
g_device = dev;
g_resetHook.Install(dev);
for (int i = 0; i < g_n; ++i)
if (g_cb[i].Created) g_cb[i].Created(dev, pp, adjusted);
return hr;
}
inline HRESULT STDMETHODCALLTYPE HookCreateDevice(IDirect3D9* d3d, UINT adapter, D3DDEVTYPE type, HWND focus, DWORD flags,
D3DPRESENT_PARAMETERS* pp, IDirect3DDevice9** out) {
for (int i = 0; i < g_n; ++i)
if (g_cb[i].BeforeCreate) g_cb[i].BeforeCreate();
if (g_reuse && g_kept.dev) {
if (g_kept.d3d == d3d && g_kept.adapter == adapter && g_kept.type == type && g_kept.focus == focus && g_kept.flags == flags && out &&
SUCCEEDED(ReuseKept(pp, out))) {
LogProcessState("device reused");
return D3D_OK;
}
DropKept(); // not reusable: let it go before the game's next device exists
}
bool adjusted = false;
HRESULT hr = WithAdjustedParams(
pp, [&](D3DPRESENT_PARAMETERS* p) { return g_createDeviceHook.Original<CreateDevice_t>(d3d)(d3d, adapter, type, focus, flags, p, out); },
&adjusted);
if (SUCCEEDED(hr) && out && *out) {
g_device = *out;
if (g_reuse) {
g_kept = {*out, d3d, adapter, type, focus, flags};
(*out)->AddRef();
}
g_resetHook.Install(*out);
for (int i = 0; i < g_n; ++i)
if (g_cb[i].Created) g_cb[i].Created(*out, pp, adjusted);
LogProcessState("device created");
}
return hr;
}
inline IDirect3D9* WINAPI HookDirect3DCreate9(UINT sdk) {
IDirect3D9* d3d = g_Direct3DCreate9(sdk);
if (d3d) g_createDeviceHook.Install(d3d);
return d3d;
}
// The patched Present calls (stdcall, one argument = the device). Once per frame every plugin gets to put its vtable hooks
// back: the native Direct3D 9 on current AMD drivers swaps the device's method table for a heap copy a few frames after
// creation (seen with ds2dev on an RX 7900 XTX), which silently switches off every table hook installed at creation. A table
// that is hooked already is a lookup.
inline HRESULT __stdcall HubPresent(void* device) {
auto dev = static_cast<IDirect3DDevice9*>(device);
auto present = reinterpret_cast<PresentFn>((*static_cast<void***>(device))[17]);
if (g_revalidate) {
g_resetHook.Install(dev);
for (int i = 0; i < g_n; ++i)
if (g_cb[i].Frame) g_cb[i].Frame(dev);
}
for (int i = 0; i < g_n; ++i)
if (g_cb[i].Present) return g_cb[i].Present(dev, present);
return present(device, nullptr, nullptr, nullptr, nullptr);
}
inline void __fastcall HookRadar(void* self, void* edx) {
IDirect3DDevice9* dev = g_device;
static bool logged = false;
if (!logged) { // the order in which the plugins see the finished world (photo mode's picture must follow the anti-aliasing)
logged = true;
std::string order;
for (int i = 0; i < g_n; ++i)
if (g_cb[i].WorldFinished) order += std::string(order.empty() ? "" : " -> ") + g_cb[i].name + " (" + std::to_string(g_cb[i].priority) + ")";
ds2::log().Detail("d3dhub: first world-finished point, order: %s", order.c_str());
}
for (int i = 0; i < g_n; ++i)
if (g_cb[i].WorldFinished) g_cb[i].WorldFinished(dev);
int bound = -1;
for (int i = 0; i < g_n && bound < 0; ++i)
if (g_cb[i].BindRadar ? g_cb[i].BindRadar(dev, self) : (g_cb[i].BindInterface && g_cb[i].BindInterface(dev))) bound = i;
g_radar(self, edx);
if (bound >= 0 && g_cb[bound].UnbindInterface) g_cb[bound].UnbindInterface();
}
inline bool InstallDevice() {
if (g_triedDevice) return g_haveDevice;
g_triedDevice = true;
g_Direct3DCreate9 = static_cast<Direct3DCreate9_t>(ds2::HookImport(
"d3d9.dll", "Direct3DCreate9", reinterpret_cast<void*>(HookDirect3DCreate9), "d3dhub: Direct3DCreate9 (device events)"));
return g_haveDevice = g_Direct3DCreate9 != nullptr;
}
inline bool InstallPresent() {
if (g_triedPresent) return g_havePresent;
g_triedPresent = true;
for (const char* pattern : kPresentPatterns) {
DWORD found = ds2::FindUnique(pattern);
if (!found) {
ds2::log().Write("SKIP d3dhub: a Present call was not found (per-frame work may be missing)");
continue;
}
DWORD site = found + kPresentPatternOffset;
BYTE patch[12] = {0x50, 0xE8, 0, 0, 0, 0, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
LONG rel = static_cast<LONG>(reinterpret_cast<DWORD>(&HubPresent) - (site + 1 + 5));
std::memcpy(patch + 2, &rel, 4);
if (ds2::PatchBytes(site, kPresentOrig, patch, sizeof(patch), "d3dhub: Present call (per-frame stage)")) g_havePresent = true;
}
return g_havePresent;
}
inline bool InstallWorld() {
if (g_triedWorld) return g_haveWorld;
g_triedWorld = true;
DWORD pass = ds2::FindUnique(kPassPattern);
if (!pass) {
ds2::log().Write("SKIP d3dhub: interface pass not found (no world-finished point)");
return false;
}
DWORD site = pass + 0x19, target = site + 5 + *reinterpret_cast<const LONG*>(site + 1);
g_radar = reinterpret_cast<Radar_t>(target);
return g_haveWorld = ds2::RedirectCall(site, target, reinterpret_cast<DWORD>(&HookRadar), "d3dhub: interface pass (world-finished point)");
}
inline DWORD Register(const Callbacks* cb) {
if (!cb || cb->size < offsetof(Callbacks, BeforeCreate) || g_n >= kMax) return 0;
Callbacks c;
std::memcpy(&c, cb, cb->size < sizeof(Callbacks) ? cb->size : sizeof(Callbacks));
int at = g_n++; // insertion by priority (stable)
while (at > 0 && g_cb[at - 1].priority > c.priority) {
g_cb[at] = g_cb[at - 1];
--at;
}
g_cb[at] = c;
bool needWorld = c.WorldFinished || c.BindInterface;
bool needDevice = c.BeforeCreate || c.AdjustParams || c.Created || c.BeforeReset || c.AfterReset || needWorld;
bool needPresent = c.Frame || c.Present;
DWORD have = 0;
if (needDevice && InstallDevice()) have |= kHaveDevice;
if (needPresent && InstallPresent()) have |= kHavePresent;
if (needWorld && InstallWorld()) have |= kHaveWorld;
ds2::log().Detail("d3dhub: %s registered (priority %d; device %d, present %d, world %d)", c.name, c.priority,
needDevice ? (have & kHaveDevice ? 1 : -1) : 0, needPresent ? (have & kHavePresent ? 1 : -1) : 0,
needWorld ? (have & kHaveWorld ? 1 : -1) : 0);
return have;
}
inline void SetRevalidate(bool on) { g_revalidate = on; }
inline void SetReuseDevice(bool on) { g_reuse = on; }
inline Api g_api = {kApiVersion, sizeof(Api), Register, SetRevalidate, nullptr, SetReuseDevice};
} // namespace impl
// The hub of this process: the first plugin to call this owns it (its copy of the code installs the hooks), later ones
// get that copy's API. Call from DllMain. nullptr = unusable (version mismatch).
inline const Api* Connect(const char* who) {
char name[64];
sprintf_s(name, "Local\\DS2RevivalD3DHub.%lu", GetCurrentProcessId());
HANDLE map = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(void*), name);
bool owner = map && GetLastError() != ERROR_ALREADY_EXISTS; // (the handle stays open: the mapping lives as long as the process)
auto view = map ? static_cast<const Api**>(MapViewOfFile(map, FILE_MAP_WRITE, 0, 0, sizeof(void*))) : nullptr;
const Api* api = nullptr;
if (view) {
if (owner) {
impl::g_api.owner = who;
*view = &impl::g_api;
}
api = *view;
UnmapViewOfFile(view);
} else {
// no shared object: stand alone
impl::g_api.owner = who;
api = &impl::g_api;
owner = true;
}
if (!api || api->size < sizeof(DWORD) * 2 || api->version != kApiVersion) {
ds2::log().Write("d3dhub: the hub of %s has API version %lu, this plugin needs %lu - no Direct3D features",
api && api->owner ? api->owner : "another plugin", api ? api->version : 0, kApiVersion);
return nullptr;
}
ds2::log().Detail(owner ? "d3dhub: %s owns the Direct3D hooks" : "d3dhub: %s uses the Direct3D hooks of %s", who, api->owner);
return api;
}
} // namespace ds2::d3dhub