Imgui hook 注入 DirectX 和 OpenGL
Imgui hook 注入 DirectX 和 OpenGL
0x01 Imgui 工作流程
Imgui 的工作流程简单来说分为下面三步:
初始化
渲染
释放
下面以D3d11 Imgui Example为例解释需要获取的参数:
Imgui 初始化
// 创建Windows
WNDCLASSEXW wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr, L"ImGui Example", nullptr };
::RegisterClassExW(&wc);
HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"Dear ImGui DirectX11 Example", WS_OVERLAPPEDWINDOW, 100, 100, 1280, 800, nullptr, nullptr, wc.hInstance, nullptr);
// 创建d3d环境
if (!CreateDeviceD3D(hwnd))
{
CleanupDeviceD3D();
::UnregisterClassW(wc.lpszClassName, wc.hInstance);
return 1;
}
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
// Setup Platform/Renderer backends
ImGui_ImplWin32_Init(hwnd);
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);可以看出初始化需要 ID3D11Device 和 ID3D11DeviceContext
Imgui 渲染
// 改变大小
if (g_ResizeWidth != 0 && g_ResizeHeight != 0)
{
CleanupRenderTarget();
g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeightDXGI_FORMAT_UNKNOWN, 0);
g_ResizeWidth = g_ResizeHeight = 0;
CreateRenderTarget();
}
// 渲染初始化
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
// 渲染
{
ImGui::Begin("Hello, world!");
ImGui::End();
}
// 渲染结束
ImGui::Render();
const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w };
g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr);
g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData())
g_pSwapChain->Present(1, 0); // Present with vsync可以看出渲染将Imgui的数据写入 ID3D11DeviceContext 后 调用 IDXGISwapChain 的 Present 函数
Imgui 清理
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
CleanupDeviceD3D();0x02 Imgui hook
如果要在界面上显示我们自己的 Imgui 内容, 那么必须要在渲染完毕之前注入我们自定义渲染逻辑
在Dx11中, 渲染完毕函数为
IDXGISwapChain::Present在Dx9中, 渲染完毕为
LPDIRECT3DDEVICE9::EndScene在OpenGL中, 渲染完毕为
SwapBuffers
只需要hook这几个函数, 将我们的渲染逻辑注入即可
DirectX hook
在DirectX中, Present 和 EndScene 均为类中的成员, 这种hook通常通过虚函数表的方式进行hook
为了获取函数的位置, 我们得手动创建一个D3d对象, 对于D3d11, 我们需要创建 IDXGISwapChain, 对于D3d9, 需要创建 LPDIRECT3DDEVICE9
D3d9
BOOL GetDx9VTable(HWND hwnd, void **v_table, int size)
{
Microsoft::WRL::ComPtr<IDirect3DDevice9> device;
Microsoft::WRL::ComPtr<IDirect3D9> d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = hwnd;
d3dpp.Windowed = (GetWindowLongPtr(hwnd, GWL_STYLE) & WS_POPUP) == 0;
HRESULT hresult = d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, device.GetAddressOf());
if (FAILED(hresult)) {
DxTrace(hresult);
d3dpp.Windowed = !d3dpp.Windowed;
hresult = d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, device.GetAddressOf());
}
if (FAILED(hresult)) {
DxTrace(hresult, true);
return FALSE;
}
memcpy(v_table, *(void ***) device.Get(), size);
return TRUE;
}D3d11
void GetDx11VTable(HWND hwnd, void **v_table, int size)
{
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferCount = 2;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hwnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = (GetWindowLongPtr(sd.OutputWindow, GWL_STYLE) & WS_POPUP) == 0;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device;
Microsoft::WRL::ComPtr<IDXGISwapChain> d3d_swap_chain;
HRESULT hresult = D3D11CreateDeviceAndSwapChain(
nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
0,
nullptr,
0,
D3D11_SDK_VERSION,
&sd,
d3d_swap_chain.GetAddressOf(),
d3d_device.GetAddressOf(),
nullptr,
nullptr
);
if (hresult == DXGI_ERROR_UNSUPPORTED) {
hresult = D3D11CreateDeviceAndSwapChain(
nullptr,
D3D_DRIVER_TYPE_WARP,
nullptr,
0,
nullptr,
0,
D3D11_SDK_VERSION,
&sd,
d3d_swap_chain.GetAddressOf(),
d3d_device.GetAddressOf(),
nullptr,
nullptr
);
}
HR(hresult)
memcpy(v_table, *(void ***) (d3d_swap_chain.Get()), size);
}
他们都有三个参数
HWND: 当前窗口句柄v_table: 虚表列表size: 虚表大小
大小和偏移可由文档获取
获取 HWND 代码如下:
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
DWORD lpdwProcessId;
GetWindowThreadProcessId(hwnd, &lpdwProcessId);
if (lpdwProcessId == GetCurrentProcessId()) {
HWND *pWnd = reinterpret_cast<HWND *>(lParam);
if (pWnd) {
*pWnd = hwnd;
}
return FALSE;
}
return TRUE;
}
HWND win32::GetProcessWindow()
{
HWND h_wnd_ = nullptr;
EnumWindows(EnumWindowsProc, reinterpret_cast<LPARAM>(&h_wnd_));
return h_wnd_;
}获取到虚表后, 便可以定义方法的签名, 类中的方法有一个隐藏的参数 this, 则签名如下:
// d3d9
using FuncEndScene = HRESULT(APIENTRY *)(LPDIRECT3DDEVICE9 pDevice)
// d3d11
using FuncPresent = HRESULT(APIENTRY *)(IDXGISwapChain *p_this, UINT sync_interval, UINT flag)随后就可以保存其记录的地址了
// d3d9
hwnd_ = GetProcessWindow();
void *v_table[119];
if (!GetDx9VTable(hwnd_, v_table, sizeof(v_table))) {
SPDLOG_ERROR("GetDx9VTable failed");
return;
}
vfun_end_scene_ = (FuncEndScene) v_table[42];
// d3d11
hwnd_ = GetProcessWindow();
if (!hwnd_) {
SPDLOG_ERROR("Failed to get process window");
return;
}
void *d3d11_swap_chain[40];
GetDx11VTable(hwnd_, d3d11_swap_chain, sizeof(d3d11_swap_chain));
vfun_present_ = (FuncPresent) d3d11_swap_chain[8];获取到地址后即可对相应函数进行Hook, 其中 HookPresent 为要被替代的函数
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID &) vfun_present_, HookPresent);
DetourTransactionCommit();
OpenGL hook
OpenGL的相对来说更简单, 因为 SwapBuffers 是一个全局函数, 可以直接获取其地址, 而不用找虚函数表
using FuncWglSwapBuffer = BOOL(WINAPI *)(HDC hDc);
HMODULE h_module = GetModuleHandle(L"opengl32.dll")
vfun_wgl_swap_buffer_ = (FuncWglSwapBuffer) GetProcAddress(h_module, "wglSwapBuffers");
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID &) vfun_wgl_swap_buffer_, HookWglSwapBuffer);
DetourTransactionCommit();0x03 Imgui 初始化和渲染
替换掉渲染函数后, 就可以对Imgui初始化了, 由于渲染函数会调用多次, 但是初始化只能初始化一次, 所以其流程为
判断是否初始化, 如果初始化则初始化
Imgui NewFrame
绘制
绘制结束
调用原函数完成绘制
D3d11
HRESULT ImguiD311Impl::HookPresent(IDXGISwapChain *swap_chain, UINT sync_interval, UINT flags)
{
if (d3d_swap_chain_ == nullptr)
d3d_swap_chain_ = swap_chain;
if (!is_initialized_)
InitImgui();
ImGui_ImplDX11_NewFrame();
DrawFrame();
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
return vfun_present_(swap_chain, sync_interval, flags);
}
void ImguiD311Impl::InitImgui()
{
SPDLOG_INFO("ImguiD311Impl::InitImgui()");
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
(void) io;
io.IniFilename = nullptr;
io.Fonts->AddFontFromFileTTF(R"(c:\Windows\Fonts\msyh.ttc)", 18.0f, nullptr, io.Fonts->GetGlyphRangesChineseFull());
ImGui_ImplWin32_Init(hwnd_);
d3d_swap_chain_->GetDevice(__uuidof(ID3D11Device), (void **) &d3d_device_);
d3d_device_->GetImmediateContext(&d3d_device_context_);
ImGui_ImplDX11_Init(d3d_device_, d3d_device_context_);
is_initialized_ = true;
}D3d9
HRESULT ImguiD39Impl::HookEndScene(LPDIRECT3DDEVICE9 pDevice)
{
if (d3d_device_ == nullptr)
d3d_device_ = pDevice;
if (!is_initialized_)
InitImgui();
ImGui_ImplDX9_NewFrame();
DrawFrame();
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
return vfun_end_scene_(pDevice);
}
void ImguiD39Impl::InitImgui()
{
SPDLOG_INFO("ImguiD39Impl::InitImgui()");
D3DDEVICE_CREATION_PARAMETERS d3d_creation_parameters;
d3d_device_->GetCreationParameters(&d3d_creation_parameters);
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
(void) io;
io.IniFilename = nullptr;
io.Fonts->AddFontFromFileTTF(R"(c:\Windows\Fonts\msyh.ttc)", 18.0f, nullptr, io.Fonts->GetGlyphRangesChineseFull());
ImGui_ImplWin32_Init(hwnd_);
ImGui_ImplDX9_Init(d3d_device_);
is_initialized_ = true;
}OpenGL
BOOL ImGuiOpenGLImpl::HookWglSwapBuffer(HDC hdc)
{
if (!hwnd_)
hwnd_ = WindowFromDC(hdc);
if (!is_initialized_)
InitImgui();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplWin32_NewFrame();
DrawFrame();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
return vfun_wgl_swap_buffer_(hdc);
}
void ImGuiOpenGLImpl::InitImgui()
{
__try
{
SPDLOG_INFO("ImGuiOpenGLImpl::InitImgui()");
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
(void) io;
io.IniFilename = nullptr;
io.Fonts->AddFontFromFileTTF(R"(c:\Windows\Fonts\msyh.ttc)", 18.0f, nullptr,
io.Fonts->GetGlyphRangesChineseFull());
ImGui_ImplWin32_InitForOpenGL(hwnd_);
ImGui_ImplOpenGL3_Init();
is_initialized_ = true;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
SPDLOG_ERROR("Failed to init imgui, exception code: {:#x}", GetExceptionCode());
}
}可以看到逻辑基本上一致, 只有不同平台的Imgui接口不同
至此, Imgui的dll被注入后就可以显示出基础ui了, 完整代码可以在 github 上查看到