0x01 Imgui 工作流程
Imgui 的工作流程简单来说分为下面三步:
- 初始化
- 渲染
- 释放
下面以D3d11 Imgui Example为例解释需要获取的参数:
Imgui 初始化
| 12
 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 渲染
| 12
 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 清理
| 12
 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
| 12
 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
| 12
 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 代码如下:
| 12
 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, 则签名如下:
| 12
 3
 4
 5
 
 | using FuncEndScene = HRESULT(APIENTRY *)(LPDIRECT3DDEVICE9 pDevice)
 
 
 using FuncPresent = HRESULT(APIENTRY *)(IDXGISwapChain *p_this, UINT sync_interval, UINT flag)
 
 | 
随后就可以保存其记录的地址了
| 12
 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 为要被替代的函数
| 12
 3
 4
 5
 
 | DetourTransactionBegin();DetourUpdateThread(GetCurrentThread());
 DetourAttach(&(PVOID &) vfun_present_, HookPresent);
 DetourTransactionCommit();
 
 
 | 
OpenGL hook
OpenGL的相对来说更简单, 因为 SwapBuffers是一个全局函数, 可以直接获取其地址, 而不用找虚函数表
| 12
 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
| 12
 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
| 12
 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
| 12
 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上查看到