Imgui hook 注入 DirectX 和 OpenGL

Imgui hook 注入 DirectX 和 OpenGL

0x01 Imgui 工作流程

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

  1. 初始化

  2. 渲染

  3. 释放

下面以D3d11 Imgui Example为例解释需要获取的参数:

Imgui 初始化

CPP
// 创建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 渲染

CPP
// 改变大小
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 清理

CPP
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

CPP
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

CPP
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 代码如下:

CPP
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, 则签名如下:

CPP
// d3d9
using FuncEndScene = HRESULT(APIENTRY *)(LPDIRECT3DDEVICE9 pDevice)

// d3d11
using FuncPresent = HRESULT(APIENTRY *)(IDXGISwapChain *p_this, UINT sync_interval, UINT flag)

随后就可以保存其记录的地址了

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

CPP
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID &) vfun_present_, HookPresent);
DetourTransactionCommit();

OpenGL hook

OpenGL的相对来说更简单, 因为 SwapBuffers 是一个全局函数, 可以直接获取其地址, 而不用找虚函数表

CPP
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

CPP
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

CPP
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

CPP
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/posts/20240212021951/
作者
simonkimi
发布于
2024年2月12日
许可协议