added dx12 hooking

This commit is contained in:
2026-05-03 21:35:08 +03:00
parent 7e55b1d00e
commit 06d2752059
4 changed files with 496 additions and 1 deletions

View File

@@ -31,9 +31,10 @@ option(OMATH_SUPRESS_SAFETY_CHECKS
option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF) option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF)
option(OMATH_ENABLE_FORCE_INLINE option(OMATH_ENABLE_FORCE_INLINE
"Will for compiler to make some functions to be force inlined no matter what" ON) "Will for compiler to make some functions to be force inlined no matter what" ON)
option(OMATH_ENABLE_LUA option(OMATH_ENABLE_LUA
"omath bindings for lua" OFF) "omath bindings for lua" OFF)
option(OMATH_ENABLE_HOOKING "omath will HooksManager that can hook DirectX automatically" OFF)
if(VCPKG_MANIFEST_FEATURES) if(VCPKG_MANIFEST_FEATURES)
foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES) foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
if(omath_feature STREQUAL "imgui") if(omath_feature STREQUAL "imgui")
@@ -48,6 +49,8 @@ if(VCPKG_MANIFEST_FEATURES)
set(OMATH_BUILD_EXAMPLES ON) set(OMATH_BUILD_EXAMPLES ON)
elseif(omath_feature STREQUAL "lua") elseif(omath_feature STREQUAL "lua")
set(OMATH_ENABLE_LUA ON) set(OMATH_ENABLE_LUA ON)
elseif(omath_feature STREQUAL "hooking")
set(OMATH_ENABLE_HOOKING ON)
endif() endif()
endforeach() endforeach()
@@ -100,6 +103,17 @@ if (OMATH_ENABLE_LUA)
target_include_directories(${PROJECT_NAME} PRIVATE ${SOL2_INCLUDE_DIRS}) target_include_directories(${PROJECT_NAME} PRIVATE ${SOL2_INCLUDE_DIRS})
endif () endif ()
if (OMATH_ENABLE_HOOKING)
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_ENABLE_HOOKING)
find_package(safetyhook CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE safetyhook::safetyhook)
if (WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE d3d12 dxgi)
endif ()
endif ()
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}") target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}")

View File

@@ -0,0 +1,96 @@
#pragma once
#ifdef OMATH_ENABLE_HOOKING
#include <functional>
#include <optional>
#include <shared_mutex>
#include <vector>
#include <cstdint>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <dxgi.h>
#include <d3d12.h>
#include <safetyhook.hpp>
namespace omath::hooks
{
class HooksManager final
{
HooksManager() = default;
public:
using present_callback = std::function<void(IDXGISwapChain*, UINT, UINT)>;
using resize_buffers_callback = std::function<void(IDXGISwapChain*, UINT, UINT, UINT, DXGI_FORMAT, UINT)>;
using execute_command_lists_callback = std::function<void(ID3D12CommandQueue*, UINT, ID3D12CommandList* const*)>;
// Return nullopt to pass the message to the original WndProc; return a value to intercept it.
using wnd_proc_callback = std::function<std::optional<LRESULT>(HWND, UINT, WPARAM, LPARAM)>;
[[nodiscard]] static HooksManager& get();
HooksManager(const HooksManager&) = delete;
HooksManager& operator=(const HooksManager&) = delete;
~HooksManager();
[[nodiscard]] bool hook_dx12();
void unhook_dx12();
void set_on_present(present_callback callback);
void set_on_resize_buffers(resize_buffers_callback callback);
void set_on_execute_command_lists(execute_command_lists_callback callback);
[[nodiscard]] bool hook_wnd_proc(HWND hwnd);
void unhook_wnd_proc();
void set_on_wnd_proc(wnd_proc_callback callback);
private:
[[nodiscard]] bool build_vtable();
static HRESULT __stdcall present_detour(IDXGISwapChain* p_swap_chain, UINT sync_interval, UINT flags);
static HRESULT __stdcall resize_buffers_detour(IDXGISwapChain* p_swap_chain, UINT buffer_count,
UINT width, UINT height, DXGI_FORMAT new_format,
UINT swap_chain_flags);
static void __stdcall execute_command_lists_detour(ID3D12CommandQueue* p_command_queue,
UINT num_command_lists,
ID3D12CommandList* const* pp_command_lists);
static LRESULT __stdcall wnd_proc_detour(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param);
mutable std::shared_mutex m_mutex;
bool m_is_dx12_hooked = false;
bool m_is_wnd_proc_hooked = false;
std::vector<uintptr_t> m_vtable;
HWND m_hooked_hwnd = nullptr;
WNDPROC m_original_wndproc = nullptr;
safetyhook::InlineHook m_present_hook;
safetyhook::InlineHook m_resize_buffers_hook;
safetyhook::InlineHook m_execute_command_lists_hook;
present_callback m_present_cb;
resize_buffers_callback m_resize_buffers_cb;
execute_command_lists_callback m_execute_command_lists_cb;
wnd_proc_callback m_wnd_proc_cb;
};
}
#else // !OMATH_ENABLE_HOOKING
namespace omath::hooks
{
class HooksManager final
{
HooksManager() = default;
public:
[[nodiscard]] static HooksManager& get();
HooksManager(const HooksManager&) = delete;
~HooksManager();
};
}
#endif

View File

@@ -0,0 +1,369 @@
#include "omath/hooks/hooks_manager.hpp"
#ifdef OMATH_ENABLE_HOOKING
#include <cstring>
namespace
{
class DummyWindow final
{
WNDCLASSEX m_window_class{};
HWND m_window_handle = nullptr;
public:
DummyWindow()
{
m_window_class.cbSize = sizeof(WNDCLASSEX);
m_window_class.style = CS_HREDRAW | CS_VREDRAW;
m_window_class.lpfnWndProc = DefWindowProc;
m_window_class.hInstance = GetModuleHandle(nullptr);
m_window_class.lpszClassName = "OM";
RegisterClassEx(&m_window_class);
m_window_handle = CreateWindow(m_window_class.lpszClassName, "Dummy", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, nullptr, nullptr, m_window_class.hInstance, nullptr);
}
~DummyWindow()
{
if (m_window_handle)
DestroyWindow(m_window_handle);
UnregisterClass(m_window_class.lpszClassName, m_window_class.hInstance);
}
[[nodiscard]] HWND handle() const { return m_window_handle; }
[[nodiscard]] bool valid() const { return m_window_handle != nullptr; }
};
// Method counts per interface (vtable slots including inherited ones)
constexpr std::size_t k_device_methods = 44;
constexpr std::size_t k_command_queue_methods = 19;
constexpr std::size_t k_command_allocator_methods = 9;
constexpr std::size_t k_command_list_methods = 60;
constexpr std::size_t k_swap_chain_methods = 18;
constexpr std::size_t k_total_methods = k_device_methods + k_command_queue_methods +
k_command_allocator_methods + k_command_list_methods +
k_swap_chain_methods; // 150
// Base offsets in the combined table
constexpr std::size_t k_cmd_queue_base = k_device_methods; // 44
constexpr std::size_t k_swap_chain_base = k_device_methods + k_command_queue_methods +
k_command_allocator_methods + k_command_list_methods; // 132
// IDXGISwapChain vtable: Present=8, ResizeBuffers=13 (from IUnknown base)
// ID3D12CommandQueue vtable: ExecuteCommandLists=8 (from IUnknown base)
constexpr std::size_t k_present_index = k_swap_chain_base + 8; // 140
constexpr std::size_t k_resize_buffers_index = k_swap_chain_base + 13; // 145
constexpr std::size_t k_execute_cmd_lists_index = k_cmd_queue_base + 8; // 52
} // namespace
namespace omath::hooks
{
HooksManager& HooksManager::get()
{
static HooksManager obj;
return obj;
}
HooksManager::~HooksManager()
{
unhook_wnd_proc();
unhook_dx12();
}
bool HooksManager::build_vtable()
{
const DummyWindow window;
if (!window.valid())
return false;
const HMODULE d3d12_module = GetModuleHandle("d3d12.dll");
const HMODULE dxgi_module = GetModuleHandle("dxgi.dll");
if (!d3d12_module || !dxgi_module)
return false;
using create_dxgi_factory_fn = HRESULT(__stdcall*)(REFIID, void**);
using d3d12_create_device_fn = HRESULT(__stdcall*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
const auto create_dxgi_factory = reinterpret_cast<create_dxgi_factory_fn>(
GetProcAddress(dxgi_module, "CreateDXGIFactory"));
const auto d3d12_create_device = reinterpret_cast<d3d12_create_device_fn>(
GetProcAddress(d3d12_module, "D3D12CreateDevice"));
if (!create_dxgi_factory || !d3d12_create_device)
return false;
IDXGIFactory* factory = nullptr;
if (FAILED(create_dxgi_factory(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&factory))))
return false;
IDXGIAdapter* adapter = nullptr;
if (factory->EnumAdapters(0, &adapter) == DXGI_ERROR_NOT_FOUND)
{
factory->Release();
return false;
}
ID3D12Device* device = nullptr;
if (FAILED(d3d12_create_device(adapter, D3D_FEATURE_LEVEL_11_0,
__uuidof(ID3D12Device), reinterpret_cast<void**>(&device))))
{
adapter->Release();
factory->Release();
return false;
}
adapter->Release();
D3D12_COMMAND_QUEUE_DESC queue_desc{};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ID3D12CommandQueue* command_queue = nullptr;
if (FAILED(device->CreateCommandQueue(&queue_desc, __uuidof(ID3D12CommandQueue),
reinterpret_cast<void**>(&command_queue))))
{
device->Release();
factory->Release();
return false;
}
ID3D12CommandAllocator* command_allocator = nullptr;
if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
__uuidof(ID3D12CommandAllocator),
reinterpret_cast<void**>(&command_allocator))))
{
command_queue->Release();
device->Release();
factory->Release();
return false;
}
ID3D12GraphicsCommandList* command_list = nullptr;
if (FAILED(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, command_allocator, nullptr,
__uuidof(ID3D12GraphicsCommandList),
reinterpret_cast<void**>(&command_list))))
{
command_allocator->Release();
command_queue->Release();
device->Release();
factory->Release();
return false;
}
DXGI_SWAP_CHAIN_DESC swap_chain_desc{};
swap_chain_desc.BufferDesc.Width = 100;
swap_chain_desc.BufferDesc.Height = 100;
swap_chain_desc.BufferDesc.RefreshRate = {60, 1};
swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swap_chain_desc.SampleDesc = {1, 0};
swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swap_chain_desc.BufferCount = 2;
swap_chain_desc.OutputWindow = window.handle();
swap_chain_desc.Windowed = TRUE;
swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swap_chain_desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
IDXGISwapChain* swap_chain = nullptr;
if (FAILED(factory->CreateSwapChain(command_queue, &swap_chain_desc, &swap_chain)))
{
command_list->Release();
command_allocator->Release();
command_queue->Release();
device->Release();
factory->Release();
return false;
}
m_vtable.resize(k_total_methods);
const auto copy_vtable = [](uintptr_t* dst, void* com_obj, std::size_t count)
{
std::memcpy(dst, *reinterpret_cast<uintptr_t**>(com_obj), count * sizeof(uintptr_t));
};
copy_vtable(m_vtable.data(), device, k_device_methods);
copy_vtable(m_vtable.data() + k_device_methods, command_queue, k_command_queue_methods);
copy_vtable(m_vtable.data() + k_device_methods + k_command_queue_methods, command_allocator, k_command_allocator_methods);
copy_vtable(m_vtable.data() + k_device_methods + k_command_queue_methods + k_command_allocator_methods, command_list, k_command_list_methods);
copy_vtable(m_vtable.data() + k_swap_chain_base, swap_chain, k_swap_chain_methods);
swap_chain->Release();
command_list->Release();
command_allocator->Release();
command_queue->Release();
device->Release();
factory->Release();
return true;
}
bool HooksManager::hook_dx12()
{
std::unique_lock lock(m_mutex);
if (m_is_dx12_hooked)
return true;
if (!build_vtable())
return false;
m_present_hook = safetyhook::create_inline(
reinterpret_cast<void*>(m_vtable[k_present_index]),
reinterpret_cast<void*>(&present_detour));
m_resize_buffers_hook = safetyhook::create_inline(
reinterpret_cast<void*>(m_vtable[k_resize_buffers_index]),
reinterpret_cast<void*>(&resize_buffers_detour));
m_execute_command_lists_hook = safetyhook::create_inline(
reinterpret_cast<void*>(m_vtable[k_execute_cmd_lists_index]),
reinterpret_cast<void*>(&execute_command_lists_detour));
if (!m_present_hook || !m_resize_buffers_hook || !m_execute_command_lists_hook)
{
m_present_hook = {};
m_resize_buffers_hook = {};
m_execute_command_lists_hook = {};
return false;
}
m_is_dx12_hooked = true;
return true;
}
void HooksManager::unhook_dx12()
{
std::unique_lock lock(m_mutex);
m_present_hook = {};
m_resize_buffers_hook = {};
m_execute_command_lists_hook = {};
m_vtable.clear();
m_is_dx12_hooked = false;
}
void HooksManager::set_on_present(present_callback callback)
{
std::unique_lock lock(m_mutex);
m_present_cb = std::move(callback);
}
void HooksManager::set_on_resize_buffers(resize_buffers_callback callback)
{
std::unique_lock lock(m_mutex);
m_resize_buffers_cb = std::move(callback);
}
void HooksManager::set_on_execute_command_lists(execute_command_lists_callback callback)
{
std::unique_lock lock(m_mutex);
m_execute_command_lists_cb = std::move(callback);
}
bool HooksManager::hook_wnd_proc(HWND hwnd)
{
std::unique_lock lock(m_mutex);
if (m_is_wnd_proc_hooked)
return true;
const auto prev = reinterpret_cast<WNDPROC>(
SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&wnd_proc_detour)));
if (!prev)
return false;
m_hooked_hwnd = hwnd;
m_original_wndproc = prev;
m_is_wnd_proc_hooked = true;
return true;
}
void HooksManager::unhook_wnd_proc()
{
std::unique_lock lock(m_mutex);
if (!m_is_wnd_proc_hooked)
return;
SetWindowLongPtr(m_hooked_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(m_original_wndproc));
m_hooked_hwnd = nullptr;
m_original_wndproc = nullptr;
m_is_wnd_proc_hooked = false;
}
void HooksManager::set_on_wnd_proc(wnd_proc_callback callback)
{
std::unique_lock lock(m_mutex);
m_wnd_proc_cb = std::move(callback);
}
// Detour implementations: copy callback under shared lock, call it unlocked,
// then call original. This avoids a deadlock if the callback itself calls set_on_*().
HRESULT __stdcall HooksManager::present_detour(IDXGISwapChain* p_swap_chain, UINT sync_interval, UINT flags)
{
auto& mgr = get();
present_callback cb;
{
std::shared_lock lock(mgr.m_mutex);
cb = mgr.m_present_cb;
}
if (cb)
cb(p_swap_chain, sync_interval, flags);
return mgr.m_present_hook.call<HRESULT>(p_swap_chain, sync_interval, flags);
}
HRESULT __stdcall HooksManager::resize_buffers_detour(IDXGISwapChain* p_swap_chain, UINT buffer_count,
UINT width, UINT height, DXGI_FORMAT new_format,
UINT swap_chain_flags)
{
auto& mgr = get();
resize_buffers_callback cb;
{
std::shared_lock lock(mgr.m_mutex);
cb = mgr.m_resize_buffers_cb;
}
if (cb)
cb(p_swap_chain, buffer_count, width, height, new_format, swap_chain_flags);
return mgr.m_resize_buffers_hook.call<HRESULT>(p_swap_chain, buffer_count, width, height, new_format,
swap_chain_flags);
}
void __stdcall HooksManager::execute_command_lists_detour(ID3D12CommandQueue* p_command_queue,
UINT num_command_lists,
ID3D12CommandList* const* pp_command_lists)
{
auto& mgr = get();
execute_command_lists_callback cb;
{
std::shared_lock lock(mgr.m_mutex);
cb = mgr.m_execute_command_lists_cb;
}
if (cb)
cb(p_command_queue, num_command_lists, pp_command_lists);
mgr.m_execute_command_lists_hook.call<void>(p_command_queue, num_command_lists, pp_command_lists);
}
LRESULT __stdcall HooksManager::wnd_proc_detour(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param)
{
auto& mgr = get();
wnd_proc_callback cb;
WNDPROC original;
{
std::shared_lock lock(mgr.m_mutex);
cb = mgr.m_wnd_proc_cb;
original = mgr.m_original_wndproc;
}
if (cb)
{
if (const auto result = cb(hwnd, msg, w_param, l_param))
return *result;
}
return CallWindowProc(original, hwnd, msg, w_param, l_param);
}
} // namespace omath::hooks
#else // !OMATH_ENABLE_HOOKING
namespace omath::hooks
{
HooksManager& HooksManager::get()
{
static HooksManager obj;
return obj;
}
HooksManager::~HooksManager() = default;
} // namespace omath::hooks
#endif

View File

@@ -16,6 +16,15 @@
} }
], ],
"features": { "features": {
"all": {
"description": "Enable all additional features",
"dependencies": [
{
"name": "omath",
"features": ["imgui", "lua", "hooking"]
}
]
},
"avx2": { "avx2": {
"description": "omath will use AVX2 to boost performance", "description": "omath will use AVX2 to boost performance",
"supports": "!arm" "supports": "!arm"
@@ -26,6 +35,13 @@
"benchmark" "benchmark"
] ]
}, },
"hooking": {
"description": "Add interface for automatic hooking of DirectX",
"dependencies": [
"safetyhook"
],
"supports": "(windows | linux) & !arm & !uwp"
},
"examples": { "examples": {
"description": "Build examples", "description": "Build examples",
"dependencies": [ "dependencies": [