Imgui hook 注入 DirectX 和 OpenGL

0x01 Imgui 工作流程

Imgui 的工作流程简单来说分为下面三步:

  1. 初始化
  2. 渲染
  3. 释放

下面以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
// 创建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);

可以看出初始化需要 ID3D11DeviceID3D11DeviceContext

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); // Present with vsync

可以看出渲染将Imgui的数据写入 ID3D11DeviceContext后 调用 IDXGISwapChainPresent 函数

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中, PresentEndScene 均为类中的成员, 这种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
// d3d9
using FuncEndScene = HRESULT(APIENTRY *)(LPDIRECT3DDEVICE9 pDevice)

// d3d11
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
// 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 为要被替代的函数

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初始化了, 由于渲染函数会调用多次, 但是初始化只能初始化一次, 所以其流程为

  1. 判断是否初始化, 如果初始化则初始化
  2. Imgui NewFrame
  3. 绘制
  4. 绘制结束
  5. 调用原函数完成绘制

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上查看到


Imgui hook 注入 DirectX 和 OpenGL
https://simonkimi.githubio.io/2024/02/12/Imgui-hook-注入DirectX-和-OpenGL/
作者
simonkimi
发布于
2024年2月12日
许可协议