![]() |
[TUTORIAL] Complete D3D8 to D3D9 Wrapper Guide - Adding ImGui to Legacy Games - Printable Version +- PlayKuro Community (https://forum.playkuro.com) +-- Forum: Kuro Forums (https://forum.playkuro.com/forumdisplay.php?fid=1) +--- Forum: Server Guides (https://forum.playkuro.com/forumdisplay.php?fid=14) +--- Thread: [TUTORIAL] Complete D3D8 to D3D9 Wrapper Guide - Adding ImGui to Legacy Games (/showthread.php?tid=28) |
[TUTORIAL] Complete D3D8 to D3D9 Wrapper Guide - Adding ImGui to Legacy Games - Kuro - 06-23-2025 COMPLETE D3D8 TO D3D9 WRAPPER GUIDE - ADDING IMGUI TO LEGACY GAMES OVERVIEW - D3D8 TO D3D9 CONVERSION WITH IMGUI This guide explains how to integrate Dear ImGui into legacy Direct3D 8 games by using a D3D8 to D3D9 wrapper. This technique allows modern UI overlays in games that still use the deprecated D3D8 API. Useful Resources:
ARCHITECTURE OVERVIEW The integration consists of 4 main components:
STEP 1: D3D8TO9 WRAPPER SETUP Required Files:
Key Components: // d3d8to9.cpp - Export the Direct3DCreate8 function extern "C" IDirect3D8* WINAPI Direct3DCreate8(UINT SDKVersion) { // Load d3dx9_43.dll for shader compilation LoadLibrary(TEXT("d3dx9_43.dll")); // Create D3D9 interface IDirect3D9* d3d9 = Direct3DCreate9(D3D_SDK_VERSION); if (!d3d9) return nullptr; // Wrap in D3D8 interface return new Direct3D8(d3d9); } // d3d8to9.hpp - Wrapper class structure class Direct3D8 : public IDirect3D8 { private: IDirect3D9* ProxyInterface; ULONG ReferenceCount; public: Direct3D8(IDirect3D9* pDirect3D9) : ProxyInterface(pDirect3D9), ReferenceCount(1) {} // IUnknown methods virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObj); virtual ULONG STDMETHODCALLTYPE AddRef(); virtual ULONG STDMETHODCALLTYPE Release(); // IDirect3D8 methods (translate all calls to D3D9) virtual HRESULT STDMETHODCALLTYPE RegisterSoftwareDevice(void* pInitializeFunction); virtual UINT STDMETHODCALLTYPE GetAdapterCount(); virtual HRESULT STDMETHODCALLTYPE GetAdapterIdentifier(UINT Adapter, DWORD Flags, D3DADAPTER_IDENTIFIER8* pIdentifier); virtual UINT STDMETHODCALLTYPE GetAdapterModeCount(UINT Adapter); virtual HRESULT STDMETHODCALLTYPE EnumAdapterModes(UINT Adapter, UINT Mode, D3DDISPLAYMODE* pMode); virtual HRESULT STDMETHODCALLTYPE GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE* pMode); virtual HRESULT STDMETHODCALLTYPE CheckDeviceType(UINT Adapter, D3DDEVTYPE CheckType, D3DFORMAT DisplayFormat, D3DFORMAT BackBufferFormat, BOOL Windowed); virtual HRESULT STDMETHODCALLTYPE CheckDeviceFormat(UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT AdapterFormat, DWORD Usage, D3DRESOURCETYPE RType, D3DFORMAT CheckFormat); virtual HRESULT STDMETHODCALLTYPE CheckDeviceMultiSampleType(UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT SurfaceFormat, BOOL Windowed, D3DMULTISAMPLE_TYPE MultiSampleType); virtual HRESULT STDMETHODCALLTYPE CheckDepthStencilMatch(UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT AdapterFormat, D3DFORMAT RenderTargetFormat, D3DFORMAT DepthStencilFormat); virtual HRESULT STDMETHODCALLTYPE GetDeviceCaps(UINT Adapter, D3DDEVTYPE DeviceType, D3DCAPS8* pCaps); virtual HMONITOR STDMETHODCALLTYPE GetAdapterMonitor(UINT Adapter); virtual HRESULT STDMETHODCALLTYPE CreateDevice(UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS8* pPresentationParameters, IDirect3DDevice8** ppReturnedDeviceInterface); }; // Example of parameter conversion in CreateDevice HRESULT Direct3D8::CreateDevice(UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS8* pPresentationParameters, IDirect3DDevice8** ppReturnedDeviceInterface) { // Convert D3DPRESENT_PARAMETERS8 to D3DPRESENT_PARAMETERS9 D3DPRESENT_PARAMETERS PresentParams; PresentParams.BackBufferWidth = pPresentationParameters->BackBufferWidth; PresentParams.BackBufferHeight = pPresentationParameters->BackBufferHeight; PresentParams.BackBufferFormat = pPresentationParameters->BackBufferFormat; PresentParams.BackBufferCount = pPresentationParameters->BackBufferCount; PresentParams.MultiSampleType = pPresentationParameters->MultiSampleType; PresentParams.MultiSampleQuality = 0; // New in D3D9 PresentParams.SwapEffect = pPresentationParameters->SwapEffect; PresentParams.hDeviceWindow = pPresentationParameters->hDeviceWindow; PresentParams.Windowed = pPresentationParameters->Windowed; PresentParams.EnableAutoDepthStencil = pPresentationParameters->EnableAutoDepthStencil; PresentParams.AutoDepthStencilFormat = pPresentationParameters->AutoDepthStencilFormat; PresentParams.Flags = pPresentationParameters->Flags; PresentParams.FullScreen_RefreshRateInHz = pPresentationParameters->FullScreen_RefreshRateInHz; PresentParams.PresentationInterval = pPresentationParameters->FullScreen_PresentationInterval; IDirect3DDevice9* DeviceInterface; HRESULT hr = ProxyInterface->CreateDevice(Adapter, DeviceType, hFocusWindow, BehaviorFlags, &PresentParams, &DeviceInterface); if (SUCCEEDED(hr)) { *ppReturnedDeviceInterface = new Direct3DDevice8(DeviceInterface); // Copy back any changes pPresentationParameters->BackBufferWidth = PresentParams.BackBufferWidth; pPresentationParameters->BackBufferHeight = PresentParams.BackBufferHeight; pPresentationParameters->BackBufferFormat = PresentParams.BackBufferFormat; pPresentationParameters->FullScreen_RefreshRateInHz = PresentParams.FullScreen_RefreshRateInHz; } return hr; } STEP 2: HOOKING INFRASTRUCTURE Create D3D8to9Hook.cpp: #include <windows.h> #include <d3d8.h> #include <detours.h> // Function pointer for original Direct3DCreate8 typedef IDirect3D8* (WINAPI* Direct3DCreate8_t)(UINT SDKVersion); Direct3DCreate8_t pOriginalDirect3DCreate8 = nullptr; class D3D8to9Hook { public: static void Initialize() { // Get the original Direct3DCreate8 function HMODULE d3d8Module = GetModuleHandle(L"d3d8.dll"); if (!d3d8Module) { // If not loaded, load it d3d8Module = LoadLibrary(L"d3d8.dll"); } if (d3d8Module) { pOriginalDirect3DCreate8 = (Direct3DCreate8_t)GetProcAddress(d3d8Module, "Direct3DCreate8"); if (pOriginalDirect3DCreate8) { // Install hook using Detours DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)pOriginalDirect3DCreate8, Direct3DCreate8_Hook); LONG result = DetourTransactionCommit(); if (result == NO_ERROR) { OutputDebugStringA("D3D8to9Hook: Successfully hooked Direct3DCreate8\n"); } else { OutputDebugStringA("D3D8to9Hook: Failed to hook Direct3DCreate8\n"); } } } } static void Shutdown() { if (pOriginalDirect3DCreate8) { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)pOriginalDirect3DCreate8, Direct3DCreate8_Hook); DetourTransactionCommit(); } } private: static IDirect3D8* WINAPI Direct3DCreate8_Hook(UINT SDKVersion) { OutputDebugStringA("D3D8to9Hook: Direct3DCreate8 called - redirecting to d3d8to9 wrapper\n"); // Call our d3d8to9 wrapper instead of original return :: (Just remove this, this creates this emoji ![]() } }; // Alternative approach: Hook by replacing the export in the DLL // This is useful if you're creating a replacement d3d8.dll extern "C" { __declspec(dllexport) IDirect3D8* WINAPI Direct3DCreate8(UINT SDKVersion) { // This function will be called instead of the original // Initialize d3d8to9 wrapper here return CreateD3D8to9Wrapper(SDKVersion); } } Alternative: DLL Replacement Method If you prefer to replace d3d8.dll entirely, create a module definition file: ; d3d8.def - Module definition for d3d8.dll replacement LIBRARY d3d8 EXPORTS Direct3DCreate8 @1 NONAME DebugSetMute @2 NONAME ValidatePixelShader @3 NONAME ValidateVertexShader @4 NONAME Then in your DLL project settings, reference this .def file to ensure proper exports. STEP 3: IMGUI D3D9 HOOK SETUP Create SimpleD3D9ImGuiHook.cpp: #include <windows.h> #include <d3d9.h> #include <detours.h> #include "imgui.h" #include "imgui_impl_win32.h" #include "imgui_impl_dx9.h" // Function pointers for original D3D9 methods typedef HRESULT(WINAPI* EndScene_t)(LPDIRECT3DDEVICE9); typedef HRESULT(WINAPI* Reset_t)(LPDIRECT3DDEVICE9, D3DPRESENT_PARAMETERS*); EndScene_t oEndScene = nullptr; Reset_t oReset = nullptr; // Global variables static LPDIRECT3DDEVICE9 g_pd3dDevice = nullptr; static bool g_ImGuiInitialized = false; static HWND g_GameWindow = nullptr; // Forward declarations extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); LRESULT WINAPI WndProc_Hook(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); WNDPROC oWndProc = nullptr; HWND FindGameWindow() { // Try common game window names - adjust for your specific game HWND hwnd = FindWindow(NULL, L"MapleStory"); if (!hwnd) hwnd = FindWindow(NULL, L"MapleStory Client"); if (!hwnd) hwnd = FindWindow(L"MapleStoryClass", NULL); // If still not found, try to find the active window if (!hwnd) hwnd = GetForegroundWindow(); return hwnd; } // Hook EndScene to render ImGui HRESULT WINAPI EndScene_Hook(LPDIRECT3DDEVICE9 pDevice) { if (!g_ImGuiInitialized && pDevice) { g_pd3dDevice = pDevice; g_GameWindow = FindGameWindow(); if (g_GameWindow) { // Initialize ImGui IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; // Setup ImGui style ImGui::StyleColorsDark(); // Setup Platform/Renderer backends ImGui_ImplWin32_Init(g_GameWindow); ImGui_ImplDX9_Init(pDevice); // Hook window procedure for input oWndProc = (WNDPROC)SetWindowLongPtr(g_GameWindow, GWLP_WNDPROC, (LONG_PTR)WndProc_Hook); g_ImGuiInitialized = true; OutputDebugStringA("ImGui initialized successfully\n"); } } if (g_ImGuiInitialized && pDevice == g_pd3dDevice) { // Start the Dear ImGui frame ImGui_ImplDX9_NewFrame(); ImGui_ImplWin32_NewFrame(); ImGui::NewFrame(); // Your UI code here - example overlay static bool show_demo = false; static bool show_overlay = true; if (show_overlay) { ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_FirstUseEver); if (ImGui::Begin("Game Overlay", &show_overlay)) { ImGui::Text("Hello from ImGui!"); ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); if (ImGui::Button("Show Demo Window")) { show_demo = !show_demo; } if (ImGui::CollapsingHeader("Game Info")) { ImGui::Text("Device: 0x%p", pDevice); ImGui::Text("Window: 0x%p", g_GameWindow); } } ImGui::End(); } if (show_demo) { ImGui::ShowDemoWindow(&show_demo); } // Rendering ImGui::EndFrame(); ImGui::Render(); ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); } // Call original EndScene return oEndScene(pDevice); } // Handle device reset HRESULT WINAPI Reset_Hook(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters) { if (g_ImGuiInitialized) { ImGui_ImplDX9_InvalidateDeviceObjects(); } HRESULT result = oReset(pDevice, pPresentationParameters); if (g_ImGuiInitialized && SUCCEEDED(result)) { ImGui_ImplDX9_CreateDeviceObjects(); } return result; } // Window procedure hook for input handling LRESULT WINAPI WndProc_Hook(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (g_ImGuiInitialized) { // Let ImGui handle the message first if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) return true; // Check if ImGui wants to capture input ImGuiIO& io = ImGui::GetIO(); switch (msg) { case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK: case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK: case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK: case WM_LBUTTONUP: case WM_RBUTTONUP: case WM_MBUTTONUP: case WM_XBUTTONUP: case WM_MOUSEWHEEL: case WM_MOUSEHWHEEL: if (io.WantCaptureMouse) return true; break; case WM_KEYDOWN: case WM_KEYUP: case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_CHAR: if (io.WantCaptureKeyboard) return true; break; } } // Call original window procedure return CallWindowProc(oWndProc, hWnd, msg, wParam, lParam); } // Install hooks by creating temporary device to get vtable void InstallD3D9Hooks() { OutputDebugStringA("Installing D3D9 hooks...\n"); // Create temporary window class WNDCLASSEX wc = {}; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = DefWindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = L"TempD3D9Window"; RegisterClassEx(&wc); // Create temporary window HWND hwnd = CreateWindow(L"TempD3D9Window", L"", WS_OVERLAPPEDWINDOW, 0, 0, 100, 100, NULL, NULL, GetModuleHandle(NULL), NULL); if (hwnd) { // Create D3D9 interface and device LPDIRECT3D9 pD3D = Direct3DCreate9(D3D_SDK_VERSION); if (pD3D) { D3DPRESENT_PARAMETERS d3dpp = {}; d3dpp.Windowed = TRUE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; d3dpp.hDeviceWindow = hwnd; LPDIRECT3DDEVICE9 pd3dDevice; HRESULT hr = pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_DISABLE_DRIVER_MANAGEMENT, &d3dpp, &pd3dDevice); if (SUCCEEDED(hr)) { // Get vtable addresses void** vtable = *(void***)pd3dDevice; oEndScene = (EndScene_t)vtable[42]; // EndScene is at index 42 oReset = (Reset_t)vtable[16]; // Reset is at index 16 // Install hooks using Detours DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)oEndScene, EndScene_Hook); DetourAttach(&(PVOID&)oReset, Reset_Hook); if (DetourTransactionCommit() == NO_ERROR) { OutputDebugStringA("D3D9 hooks installed successfully\n"); } else { OutputDebugStringA("Failed to install D3D9 hooks\n"); } pd3dDevice->Release(); } pD3D->Release(); } DestroyWindow(hwnd); } UnregisterClass(L"TempD3D9Window", GetModuleHandle(NULL)); } // Cleanup function void CleanupImGuiHooks() { if (g_ImGuiInitialized) { if (oWndProc && g_GameWindow) { SetWindowLongPtr(g_GameWindow, GWLP_WNDPROC, (LONG_PTR)oWndProc); } ImGui_ImplDX9_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui:: (Just remove this, this creates this emoji ![]() g_ImGuiInitialized = false; } // Remove hooks if (oEndScene) { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)oEndScene, EndScene_Hook); DetourDetach(&(PVOID&)oReset, Reset_Hook); DetourTransactionCommit(); } } STEP 4: INPUT HANDLING Hook Window Procedure: WNDPROC oWndProc; extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM); LRESULT WINAPI WndProc_Hook(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (g_ImGuiInitialized) { ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam); // Block game input when ImGui wants it ImGuiIO& io = ImGui::GetIO(); if (io.WantCaptureMouse || io.WantCaptureKeyboard) { return true; } } return CallWindowProc(oWndProc, hWnd, msg, wParam, lParam); } void HookWindowProc() { HWND gameWindow = FindWindow(NULL, "MapleStory"); oWndProc = (WNDPROC)SetWindowLongPtr(gameWindow, GWLP_WNDPROC, (LONG_PTR)WndProc_Hook); } STEP 5: DLL ENTRY POINT dllmain.cpp: BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { switch (reason) { case DLL_PROCESS_ATTACH: // Initialize in order D3D8to9Hook::Initialize(); // Hook Direct3DCreate8 InstallD3D9Hooks(); // Hook D3D9 methods HookWindowProc(); // Hook input break; case DLL_PROCESS_DETACH: // Cleanup ImGui if (g_ImGuiInitialized) { ImGui_ImplDX9_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui:: (Just remove this, this creates this emoji ![]() } break; } return TRUE; } REQUIRED DEPENDENCIES GitHub Repositories:
Required Files to Download:
Runtime Requirements:
NuGet Packages (Visual Studio): <!-- Add to your .vcxproj or use Package Manager --> <PackageReference Include="Microsoft.Detours" Version="4.0.1" /> <PackageReference Include="directxtk" Version="2024.2.8.1" /> BUILD CONFIGURATION Visual Studio Project Settings: <!-- .vcxproj excerpt --> <ItemDefinitionGroup> <ClCompile> <PreprocessorDefinitions>DIRECT3D_VERSION=0x0800;%(PreprocessorDefinitions)</PreprocessorDefinitions> </ClCompile> <Link> <AdditionalDependencies>d3d9.lib;d3dx9.lib;detours.lib;%(AdditionalDependencies)</AdditionalDependencies> <ModuleDefinitionFile>d3d8.def</ModuleDefinitionFile> </Link> </ItemDefinitionGroup> Module Definition File (d3d8.def): LIBRARY d3d8 EXPORTS Direct3DCreate8 @1 COMMON ISSUES AND SOLUTIONS Problem: Game crashes on startup
Problem: ImGui not rendering
Problem: Input not working
ADVANCED FEATURES Multi-threaded Rendering: // Use critical section for thread safety CRITICAL_SECTION g_cs; InitializeCriticalSection(&g_cs); HRESULT WINAPI EndScene_Hook(LPDIRECT3DDEVICE9 pDevice) { EnterCriticalSection(&g_cs); // ImGui rendering code LeaveCriticalSection(&g_cs); } State Management: // Save D3D9 state before ImGui IDirect3DStateBlock9* d3d9_state_block = NULL; pDevice->CreateStateBlock(D3DSBT_ALL, &d3d9_state_block); d3d9_state_block->Capture(); // Render ImGui ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); // Restore state d3d9_state_block->Apply(); d3d9_state_block->Release(); EXAMPLE PROJECT STRUCTURE YourProject/ ├── src/ │ ├── d3d8to9/ │ │ ├── d3d8to9.cpp │ │ ├── d3d8to9.hpp │ │ ├── d3d8to9_device.cpp │ │ └── d3d8types.hpp │ ├── hooks/ │ │ ├── D3D8to9Hook.cpp │ │ └── SimpleD3D9ImGuiHook.cpp │ ├── imgui/ │ │ ├── imgui.cpp │ │ ├── imgui.h │ │ ├── imgui_impl_win32.cpp │ │ ├── imgui_impl_win32.h │ │ ├── imgui_impl_dx9.cpp │ │ └── imgui_impl_dx9.h │ └── dllmain.cpp ├── lib/ │ ├── detours.lib │ ├── d3d9.lib │ └── d3dx9.lib ├── include/ │ ├── detours.h │ ├── d3d9.h │ └── d3dx9.h ├── d3d8.def └── YourProject.vcxproj DEBUGGING TIPS Use OutputDebugString for logging: // Add this to track initialization OutputDebugStringA("D3D8to9: Direct3DCreate8 called\n"); OutputDebugStringA("ImGui: EndScene hook called\n"); // Use DebugView from Sysinternals to see output // Download: https://docs.microsoft.com/en-us/sysinternals/downloads/debugview Common vtable indices for D3D9: // IDirect3DDevice9 vtable indices // 0 - QueryInterface // 1 - AddRef // 2 - Release // 16 - Reset // 42 - EndScene // 43 - Present // Verify with: void** vtable = *(void***)pDevice; OutputDebugStringA("EndScene address: 0x%p\n", vtable[42]); Useful GitHub Examples: CONCLUSION This architecture allows seamless integration of modern UI frameworks into legacy D3D8 applications. The game continues using D3D8 APIs while ImGui renders through the underlying D3D9 device, providing the best of both worlds - compatibility and modern features. Key benefits:
Remember to handle edge cases specific to your target game, test thoroughly, and always backup original game files! Guide created based on Ezorsia V2 MapleStory V83 client implementation |