0x01 Imgui 工作流程
Imgui 的工作流程简单来说分为下面三步:
- 初始化
- 渲染
- 释放
下面以D3d11 Imgui Example为例解释需要获取的参数:
Imgui 初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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);
if (!CreateDeviceD3D(hwnd)) { CleanupDeviceD3D(); ::UnregisterClassW(wc.lpszClassName, wc.hInstance); return 1; }
IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
ImGui_ImplWin32_Init(hwnd); ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
|
可以看出初始化需要 ID3D11Device
和 ID3D11DeviceContext
Imgui 渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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);
|
可以看出渲染将Imgui的数据写入 ID3D11DeviceContext
后 调用 IDXGISwapChain
的 Present
函数
Imgui 清理
1 2 3 4 5
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| 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
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
, 则签名如下:
1 2 3 4 5
| using FuncEndScene = HRESULT(APIENTRY *)(LPDIRECT3DDEVICE9 pDevice)
using FuncPresent = HRESULT(APIENTRY *)(IDXGISwapChain *p_this, UINT sync_interval, UINT flag)
|
随后就可以保存其记录的地址了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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];
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
为要被替代的函数
1 2 3 4 5
| DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID &) vfun_present_, HookPresent); DetourTransactionCommit();
|
OpenGL hook
OpenGL的相对来说更简单, 因为 SwapBuffers
是一个全局函数, 可以直接获取其地址, 而不用找虚函数表
1 2 3 4 5 6 7 8
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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上查看到