Skip to content

Wrap D3D9 COM objects instead of MinHook for the cold path#41

Open
3vcloud wants to merge 2 commits into
devfrom
feat/d3d9-wrapper-hooking
Open

Wrap D3D9 COM objects instead of MinHook for the cold path#41
3vcloud wants to merge 2 commits into
devfrom
feat/d3d9-wrapper-hooking

Conversation

@3vcloud

@3vcloud 3vcloud commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Replaces InstallD3D9Hooks' MinHook vtable-slot patching with COM wrapper/proxy objects for the normal load-before-launch path. Direct3DCreate9/Direct3DCreate9Ex now return a WrappedDirect3D9/WrappedDirect3D9Ex instead of the real object; its CreateDevice/CreateDeviceEx wraps the resulting device in WrappedDirect3DDevice9/WrappedDirect3DDevice9Ex, and CreateTexture/CreateVolumeTexture/CreateCubeTexture wrap their results in WrappedTexture9/WrappedVolumeTexture9/WrappedCubeTexture9.
  • Every method is a compiled virtual override forwarding to the real object; gMod's mod-texture logic (CreateTexture, UpdateTexture, BeginScene, SetTexture, GetTexture, Release) is intercepted the same way, replicating the exact semantics of the current h_* MinHook detours in D3D9Hooks.cpp — just via ordinary dispatch instead of a patched vtable slot. Nothing is ever patched, so this sidesteps every self-modifying-code/JIT-cache concern the ARM64 emulator (Windows-on-ARM's xtajit, or Wine on FEX-Emu/box64) has with inline code patching.
  • The late-attach path (SetDeviceRegisterExistingDevice, D3D9Hooks.cpp) is untouched: wrapping only works when gMod controls object creation from the start (the game already holds a pointer to the real object by the time late-attach happens), so that path still needs MinHook and remains gated off under ARM64 emulation (see Refuse late device-attach under ARM64 emulation #40).
  • TextureClient is constructed with the wrapper device pointer (not the real one), so DirectX::CreateDDSTextureFromMemoryEx's internal CreateTexture call re-enters the wrapper's intercepted CreateTexture, which is what makes loading_fake-gated fake-texture registration work at all. A small real-texture→wrapper side map (D3D9TextureWrappers.cpp) recovers wrapper identity for the one call (GetTexture) that only ever hands back real, unwrapped pointers.
  • No build-file changes needed — CMakeLists.txt globs header/*.h/source/*.cpp.

Verified full method coverage of all 7 wrapped interfaces (IDirect3D9, IDirect3D9Ex, IDirect3DDevice9, IDirect3DDevice9Ex, IDirect3DTexture9, IDirect3DVolumeTexture9, IDirect3DCubeTexture9) programmatically against the Microsoft DirectX SDK header declarations (cross-referenced via wine-mirror/wine and apitrace/dxsdk on GitHub) — every interface method has a matching override, and every declared override has a matching implementation. No Windows/MSVC toolchain was available to compile-check this, so please build before merging.

Known limitation

QueryInterface on any wrapper only recognizes its own interface family (IUnknown, IDirect3D9/Ex, IDirect3DDevice9/Ex, or the specific texture interface); an unrecognized-but-successful QueryInterface call falls through to the real object and hands back an unwrapped pointer. Not expected to matter in practice — noted inline where it applies.

Test plan

  • Build on Windows and confirm the game still renders/mods textures correctly through the normal load-before-launch path
  • Confirm mod texture swapping (AddFile/modlist-driven replacement) still works end to end
  • Confirm device teardown (FreeLibrary) still cleans up TextureClient correctly
  • Test under ARM64 emulation (Windows-on-ARM or Wine/FEX-Emu/box64) to confirm this path no longer relies on any code patching

GWToolbox Bot added 2 commits July 7, 2026 10:17
CreateDevice(Ex)/CreateTexture/CreateVolumeTexture/CreateCubeTexture/
UpdateTexture/BeginScene/SetTexture/GetTexture/Release are now intercepted
via ordinary compiled virtual dispatch on wrapper objects returned from
Direct3DCreate9(Ex), instead of MinHook patching the real object's vtable
slots. Nothing is ever patched in this path, so it sidesteps every
self-modifying-code/JIT-cache concern the ARM64 emulator (Windows-on-ARM's
xtajit, or Wine on FEX-Emu/box64) has with inline code patching.

The late-attach path (SetDevice/RegisterExistingDevice/D3D9Hooks.cpp) is
unchanged: wrapping only works when gMod controls object creation from the
start, so late-attach still needs MinHook and remains gated off under ARM64
emulation.
RegisterExistingDevice deduped against D3D9Hooks.cpp's own g_devices map,
but the cold Direct3DCreate9(Ex) path (D3D9Wrappers.cpp) never touches that
map. If gMod is early-injected and something (e.g. GWToolbox adopting a
pre-loaded gMod) also calls SetDevice on the resulting wrapper device,
RegisterExistingDevice would treat it as new: MinHook-patch the wrapper's
own vtable on top, and construct a second TextureClient, hitting its
single-instance ASSERT. Check TextureClient::CurrentClient() first so any
already-active device (via either path) is a no-op.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant