305 lines
16 KiB
C++
305 lines
16 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 <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;
|
|
};
|
|
|
|
struct Api {
|
|
DWORD version;
|
|
DWORD size;
|
|
DWORD (*Register)(const Callbacks* cb);
|
|
void (*SetRevalidate)(bool on);
|
|
const char* owner;
|
|
};
|
|
|
|
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
|
|
|
|
// "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;
|
|
}
|
|
|
|
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();
|
|
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;
|
|
g_resetHook.Install(*out);
|
|
for (int i = 0; i < g_n; ++i)
|
|
if (g_cb[i].Created) g_cb[i].Created(*out, pp, adjusted);
|
|
}
|
|
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().Write("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].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().Write("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 Api g_api = {kApiVersion, sizeof(Api), Register, SetRevalidate, nullptr};
|
|
|
|
} // 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().Write(owner ? "d3dhub: %s owns the Direct3D hooks" : "d3dhub: %s uses the Direct3D hooks of %s", who, api->owner);
|
|
return api;
|
|
}
|
|
|
|
} // namespace ds2::d3dhub
|