← Open Source
Sh0ckFR

Universal-Dear-ImGui-Hook

A universal Dear ImGui hook for DirectX 12/11/10/9, OpenGL, and Vulkan. Compatible with Windows, Linux, and Steam Deck, featuring integrated MCP via a NamedPipe for game debugging and control.

AI EngineeringGive agent toolsMake agent see hear talkC++
Open on GitHub
Momentum
+0stars in 24 hours0.0%
500
Stars
90
Forks
+0
This week
3
Contributors
Created 2020-01-13 · Updated 2026-11-03 · #18931 today
Top developers
Sh0ckFRprimembSirMestre
README

Universal-Dear-ImGui-Hook

A Dear ImGui overlay that draws your interface inside another application, whatever graphics API it uses, and lets an AI assistant such as Claude Code see and play the game through MCP over a local named pipe or Unix socket. The API is detected at runtime: build once, inject anywhere.

API Windows (x86, x64) Linux (x86-64, ARM64)
Direct3D 9, 9Ex ✅ NOT REQUIRED
Direct3D 10, 11, 12 ✅ NOT REQUIRED
Vulkan ✅ ✅
OpenGL 3.0+ ✅ (WGL) ✅ (GLX, EGL)

Windows games running under Wine or Proton use the Windows DLL.

Contents

Sections marked with ▶ unfold on click.

Features

  • One library for every API: the backend is picked from the first API that presents a frame. Inject before or after the game initializes its graphics.
  • Remote control by an AI assistant: through MCP and a local named pipe (Windows) or Unix socket (Linux), an assistant such as Claude Code sees the game, writes to the player over it and presses its keys.
  • Menu input: INSERT, or L3 + R3 on a controller, opens the menu; F11 removes the overlay and unloads the DLL. The keys are settings in example/config.cpp. While the menu is open the game sees no keyboard, mouse or controller input (Windows), and an XInput controller navigates the menu.
  • No crash from the overlay (Windows, MSVC builds): an exception in the overlay, from a renderer backend or the menu code, removes it instead of taking the game down; the menu key brings it back.
  • Images and frame capture: uih::CreateTexture() turns RGBA pixels into an image for ImGui::Image(), and uih::CaptureFrame() copies what the game draws, without the overlay, on every backend.
  • Several windows: when the game presents to more than one window or swap chain, the overlay stays on one of them and follows the focus.
  • Readable on any display: the interface follows the DPI of the game window, or a fixed scale, with correct colors on sRGB and floating-point back buffers.
  • No injector needed: the DLL can be loaded as dinput8.dll, and the Linux library with LD_PRELOAD.
  • No dependencies: Dear ImGui, MinHook and the Vulkan headers are vendored; the C and C++ runtimes are linked statically.

Quick start

1. Get the library. Prebuilt libraries are attached to each release, and every commit's builds are kept as workflow artifacts. Pick the one that matches the game, not your machine:

Game File
Windows, 64-bit Universal-ImGui-Hook-msvc-x64.dll
Windows, 32-bit Universal-ImGui-Hook-msvc-Win32.dll
Linux, x86-64 or ARM64 Universal-ImGui-Hook-linux-x86_64.so or -linux-aarch64.so (glibc 2.28 or later)

2. Or build it. On Windows, open Universal-ImGui-Hook.sln in Visual Studio 2022 or newer and build Release|x64: the DLL is build/vs/x64/Release/Universal-ImGui-Hook.dll. On Linux:

sudo apt install cmake ninja-build g++ pkg-config libxcb1-dev
cmake --preset linux && cmake --build --preset linux
LD_PRELOAD=$PWD/build/linux/Universal-ImGui-Hook.so ./game

3. Make it yours. You only edit the example; the hook itself, in src/, needs no change:

To change Edit
The menu example/menu.cpp, plain Dear ImGui code run every frame
Hotkeys, controller buttons, graphics API, interface scale example/config.cpp
Remote control by an AI assistant, its pipe or socket name example/config.cpp, see Remote control by an AI assistant
Fonts and style a config.setup callback, run when the ImGui context is created, set next to config.render in example/dllmain.cpp (Windows) or example/preload.cpp (Linux)

Usage

Windows

Inject Universal-ImGui-Hook.dll with any injector or LoadLibrary. An injected DLL is unloaded by F11; inject it again to bring the overlay back.

To load it without an injector, rename it to dinput8.dll and put it next to the game's executable: it exports DirectInput8Create and forwards it to the system library, so any game that imports DirectInput 8 loads it. Under Wine or Proton, add WINEDLLOVERRIDES="dinput8=n,b" %command% to the launch options. Loaded this way the DLL stays in the game, which references it: F11 removes the overlay and INSERT brings it back.

Logs (config.debugLog) go to OutputDebugString: read them with DebugView. The example DLL writes them when the game runs with the environment variable UIH_DEBUG=1.

Linux

LD_PRELOAD=/path/to/Universal-ImGui-Hook.so ./game

For a Steam game, set the launch options to LD_PRELOAD=/path/to/Universal-ImGui-Hook.so %command%. UIH_DEBUG=1 prints the log on stderr. A preloaded library cannot leave the process: F11 removes the overlay and its hooks, and INSERT brings them back.

Settings

The settings of the example library are compiled into it: edit example/config.cpp, next to dllmain.cpp, and rebuild.

void ApplySettings(uih::Config& config) {
    config.menuKey = VK_INSERT;                              // opens and closes the menu
    config.unloadKey = VK_F11;                               // removes the overlay, unloads the DLL
    config.menuButtons = uih::GamepadL3 | uih::GamepadR3;    // controller buttons held together, 0 to disable
    config.backend = uih::Backend::None;                     // graphics API to hook, None detects it
    config.scale = 0.0f;                                     // interface size, 0 follows the DPI
    config.remoteControl = false;                            // remote control by an AI assistant
    config.remotePipe = "universal-imgui-hook";              // name of its pipe (Windows) or socket (Linux)
}
Values of each setting
Setting Values
menuKey, unloadKey Windows: virtual-key codes such as VK_F1 or 'M'. Linux: X11 keysyms from X11/keysymdef.h, such as 0xffbe for XK_F1; the file has its own #else branch for them. 0 disables the key.
menuButtons uih::GamepadA, B, X, Y, L1, R1, L3, R3, Start, Back, DpadUp, DpadDown, DpadLeft, DpadRight, combined with |; first XInput controller, Windows only.
backend uih::Backend::None detects the API from the first one that presents a frame, which is right for nearly every game. D3D9, D3D10, D3D11, D3D12, Vulkan or OpenGL forces one, for a game that uses several.
scale 0 follows the DPI of the game window; 1.5 draws the interface at 150%.
remoteControl, remotePipe true serves the remote control on the named pipe \\.\pipe\<remotePipe> on Windows, on the Unix socket $XDG_RUNTIME_DIR/<remotePipe> on Linux.

Writing your menu

The interface lives in example/menu.cpp. DrawMenu() is called every frame between ImGui::NewFrame() and ImGui::Render(), so any Dear ImGui code works there. The entry points, example/dllmain.cpp on Windows and example/preload.cpp on Linux, apply the settings and start the hook:

uih::Config config;
ApplySettings(config);         // example/config.cpp
config.render = DrawMenu;      // every frame
config.setup = LoadFonts;      // when the ImGui context is created: fonts, style
config.module = module;        // Windows: DLL unloaded by unloadKey
uih::Start(config);

Images are created once from RGBA pixels and drawn like any ImGui texture:

static ImTextureData* logo = uih::CreateTexture(pixels, 64, 64);
ImGui::Image(logo->GetTexRef(), ImVec2(64, 64));

The example menu is a tour of the common widgets (buttons, combo and list boxes, inputs, sliders, colors, tabs, trees, tables, plots, popups, draw lists) with imgui.png shown under the slider, and its Menu bar opens Dear ImGui's own demo window, which covers every function. example/image.cpp embeds the picture in the library and decodes it with stb_image.

Using the hook as a library

The repository is a CMake package; link uih::uih to get include/uih.h and Dear ImGui's headers:

include(FetchContent)
FetchContent_Declare(uih GIT_REPOSITORY https://github.com/Sh0ckFR/Universal-Dear-ImGui-Hook.git GIT_TAG master)
FetchContent_MakeAvailable(uih)
target_link_libraries(my_overlay PRIVATE uih::uih)

tests/consumer/ is a complete example, built by the CI. On Linux, linking uih::uih into a shared library applies src/linux/exports.map, which only exports the hooked functions.

Remote control by an AI assistant

The library can let an AI assistant, such as Claude Code, see the game, write to the player over it and press its keys, through the Model Context Protocol (MCP) and a local named pipe on Windows, or a Unix socket on Linux. Every graphics API; on Linux, X11 and XWayland games.

flowchart LR
    A["Assistant, such as Claude Code"] -- "MCP on stdin and stdout" --> B["remote/uih_mcp.py"]
    B -- "local named pipe or Unix socket" --> C["The library in the game"]
Loading

1. Turn it on in example/config.cpp, then build the DLL or the .so:

config.remoteControl = true;
config.remotePipe = "universal-imgui-hook";   // \\.\pipe\universal-imgui-hook, or $XDG_RUNTIME_DIR/universal-imgui-hook on Linux

2. Load it into the game, with any injector, as dinput8.dll, or with the script, which needs Python 3.8 or newer and nothing else:

python remote/uih_mcp.py inject game.exe build/vs/x64/Release/Universal-ImGui-Hook.dll

On Linux, preload it: LD_PRELOAD=/path/to/Universal-ImGui-Hook.so ./game. The socket opens with the first frame of the overlay, in the process that draws it, not in the launch scripts that preload the library too.

3. Give the assistant the MCP server, once. With Claude Code:

claude mcp add game -- python C:/path/to/Universal-Dear-ImGui-Hook/remote/uih_mcp.py

On Linux, the same with python3 /path/to/Universal-Dear-ImGui-Hook/remote/uih_mcp.py. With another remotePipe, add --pipe <name> after the script. Other MCP clients, such as Claude Desktop, take the same command in their configuration:

{
  "mcpServers": {
    "game": { "command": "python", "args": ["C:/path/to/Universal-Dear-ImGui-Hook/remote/uih_mcp.py"] }
  }
}

4. Ask. Start a new session of the assistant, or run /mcp in Claude Code, and talk about the game:

  • Look at the game and tell me what you see.
  • Write "Well done!" in the game for 5 seconds.
  • Walk right to the edge of the platform, then jump.
  • Open the overlay menu and describe it.

The assistant gets these tools:

Tool Does
screenshot The current frame, without the overlay, at most 1280 pixels wide or high by default
show_text A message to the player at the top of the screen, drawn by the overlay, for 10 seconds by default
press_key Holds keys for a duration, 150 ms by default; right+space holds them together
game_info Graphics API, window size, keyboard focus, menu state
Good to know: security, focus, limits
  • Any program you run can use the pipe or the socket, so the remote control is off by default; other machines cannot, nor, on Linux, other users. Turn it on for single-player games only: anti-cheat software watches injected DLLs.
  • While you type to the assistant, the game does not have the focus: the keys then reach it as window messages, or X11 events on Linux, which games that read the keyboard state directly (DirectInput, raw input) do not see. Click the game window after sending your request, or let the assistant play on a second screen.
  • While the overlay menu is open, the game sees no keys: the assistant can close it with insert.
  • The assistant plays turn by turn, a screenshot and a few keys at a time: fine for menus, puzzles and slow games, not for reflexes.
  • On Linux, keys go through the XTEST extension of the X server; without it, they are sent as events to the window. Native Wayland games get no keys.
  • One game per pipe or socket name: a second game started with the same name logs that the remote control is unavailable.
  • unloadKey stops the remote control with the overlay.
  • Screenshots of HDR back buffers, with more than 10 bits per channel, fail.
The protocol, to write your own client

remote/uih_mcp.py speaks MCP with the assistant on its standard input and output, and forwards each tool call as one line to the library, which serves the pipe or the socket on a thread of its own (src/remote.cpp; the transport and the keys in src/windows/remote.cpp and src/linux/remote.cpp):

Command Answer
info ok <API> <width>x<height> focused=0|1 menu=0|1
screenshot <max size> ok <width> <height>, then the RGB pixels, rows from the top
text <seconds> <message> ok; \n in the message starts a new line, 0 seconds removes it
key <keys> <milliseconds> ok <how the keys were sent> once they are released

Failures answer error <reason>. The screenshots come from uih::CaptureFrame(); the keys go through SendInput on Windows and XTEST on Linux when the game has the focus.

API

Everything is in include/uih.h, namespace uih.

Function Description
Start(config) Installs the hooks on a worker thread and returns. Safe from DllMain or a library constructor.
Stop() Removes the overlay, releases the GPU resources on the rendering thread and removes the hooks. Blocks; not from DllMain or a hook. Start() can be called again afterwards.
ActiveBackend(), BackendName(backend) API the overlay is drawn with, Backend::None until the first frame.
IsMenuOpen(), SetMenuOpen(open) Menu state, also toggled by menuKey.
CreateTexture(rgba, width, height) Image from RGBA pixels, uploaded by the active backend on the next frame and again after a backend switch. Any thread. Null once kMaxTextures (256) images exist.
DestroyTexture(texture) Releases an image on the next frame.
CaptureFrame(pixels, width, height, timeoutMs) Copies the next frame of the game, without the overlay, as RGBA pixels. Waits for that frame on any thread but the render callback's; false on timeout, or with a back buffer of more than 10 bits per channel (HDR).
The Config fields
Config field Default Description
render nullptr Called every frame, between ImGui::NewFrame() and ImGui::Render()
setup nullptr Called when the ImGui context is created, again if the game switches devices or windows
backend None Restricts the hook to one API; None takes the first that presents
menuKey, unloadKey INSERT, F11 Virtual-key codes on Windows, X11 keysyms on Linux
menuButtons GamepadL3 | GamepadR3 Controller buttons held together to toggle the menu, 0 to disable; first XInput controller, Windows only
scale 0 Interface size; 0 follows the DPI of the game window
module nullptr Windows: module freed by unloadKey. With nullptr, unloadKey removes the overlay and the menu key or buttons bring it back, as on Linux
debugLog false OutputDebugString on Windows, stderr on Linux
remoteControl false Serves the remote control on a named pipe for local processes (Windows), or a Unix socket for the user's processes (Linux)
remotePipe "universal-imgui-hook" Name of that pipe, \\.\pipe\<remotePipe>, or socket, $XDG_RUNTIME_DIR/<remotePipe> (/tmp/<remotePipe>-<uid> without that variable); copied by Start()

How it works

Startup: hooks found through dummy objects, or symbol interposition on Linux

Start() launches a worker thread. On Windows it hooks every graphics and input DLL already loaded, then the others as they load (LdrRegisterDllNotification). The hooks are placed with MinHook on functions found through dummy objects: a hidden window, a D3D device, a Vulkan device per GPU. On Linux, the library exports the hooked functions under their own names and overrides dlsym(), so the game calls them whether it links them or looks them up like SDL and GLFW do.

Hook points: the function each API draws in
API Draws in Also hooked
D3D9 IDirect3DDevice9::Present, IDirect3DDevice9Ex::PresentEx, IDirect3DSwapChain9::Present Reset, ResetEx
D3D10, 11, 12 IDXGISwapChain::Present, IDXGISwapChain1::Present1; the device behind the swap chain picks the renderer ResizeBuffers, ResizeBuffers1
D3D12 (same) ID3D12CommandQueue::ExecuteCommandLists and the factory's CreateSwapChain*, to find the queue the swap chain presents on
Vulkan vkQueuePresentKHR Device, queue, swap chain and surface creation and destruction; in each driver on Windows, the loader's exports on Linux
OpenGL wglSwapBuffers, glXSwapBuffers, eglSwapBuffers glXGetProcAddress, eglGetProcAddress on Linux
Each frame: what the present hook does

uih::Frame() registers new textures, polls the hotkeys, applies the DPI scale and runs render; the backend then draws the ImGui data on the back buffer, uploading textures as needed, and the game's present goes on. Vertex colors are converted to linear for back buffers that store linear values; OpenGL and D3D9 turn sRGB writes off instead. Vulkan submits its own command buffer, which waits on the game's semaphores and signals the one the present waits on.

Resolution changes are followed on every API: D3D9 Reset/ResetEx, DXGI ResizeBuffers, a new Vulkan swap chain, or a new size of the OpenGL window; the overlay takes the new size on the next frame.

Input: how the game is kept from seeing it while the menu is open

Windows. The game window is subclassed to feed ImGui, and its input messages are swallowed while the menu is open, except releases, keyboard and mouse raw input (filtered below) and Alt+F4. GetRawInputData, GetRawInputBuffer, GetAsyncKeyState, GetKeyState, GetKeyboardState, DirectInput and XInput then report no input to the game, and GetCursorPos a frozen position. The cursor is freed from the game's ClipCursor rectangle, which is applied again when the menu closes. Releases always go through, so no key stays stuck; calls made by the hook itself see the real state.

Linux. The pointer and the keyboard are polled from the X server on a separate XCB connection.

Teardown: what Stop() and the unload key do

Stop() asks the rendering thread to release the backend at its next present, or does it itself after one second, then removes the hooks once none of them is running. When the library cannot leave the process (Linux, dinput8.dll proxy, no module), the unload key runs Stop() on a thread that then polls the menu key and buttons, and starts the hook again with the menu open.

Several windows or swap chains: which one gets the overlay

The overlay is drawn on one swap chain, its target, by one thread at a time; the others present untouched. The target changes when it stops presenting for half a second, when the window of another swap chain has the focus and the target's does not, or when the game replaces its swap chain on the same window. A Vulkan surface created before the hook is matched to the window of the swap chain's size. EGL does not tell the window of a surface: there the overlay only moves when its target stops presenting.

Faults: why an exception in the overlay does not crash the game

On Windows, what the overlay runs in the game's threads (frames, teardown, window messages) is protected by structured exception handling. An exception there, such as an access violation in the render callback, removes the overlay from another thread, as the unload key does without a module: the game keeps running, and the menu key or buttons bring the overlay back. The frame runs before the backends take a reference to the back buffer, so a fault leaks none.

Frame capture: how each API copies its back buffer

When CaptureFrame() waits, the backend copies its back buffer at the next present, before it draws the overlay: GetRenderTargetData on D3D9, a staging texture on D3D10 and 11, a readback buffer on D3D12 and Vulkan, glReadPixels on OpenGL. Multisampled back buffers are resolved first. To allow the copy, Vulkan swap chains are created with TRANSFER_SRC usage, and again without it if the surface refuses.

Remote control: the server thread

With remoteControl set, a thread of the library serves the named pipe with overlapped I/O on Windows, or the Unix socket with poll() and an eventfd on Linux, so that Stop() ends a connection or a command at once. Messages are drawn after the render callback, in the frame of the overlay; keys go through SendInput (Windows) or XTEST on a connection of its own to the X server (Linux) when the game has the focus, and as window messages or X11 events otherwise.

On Linux, every process the game starts preloads the library, so the socket is opened at the first frame of the overlay by the process that draws it. The socket file is only open to its owner (0600), and the server also checks the user of each client (SO_PEERCRED), for the /tmp fallback. A file left by a game that crashed is replaced when nobody answers on it.

Building

Target Output CMake presets
Windows game, 64-bit Universal-ImGui-Hook.dll msvc-x64, mingw-x64
Windows game, 32-bit Universal-ImGui-Hook.dll msvc-x86, mingw-x86
Native Linux game Universal-ImGui-Hook.so linux

Each preset builds into build/<preset>.

Visual Studio 2022 or newer, with the "Desktop development with C++" workload. Pick one:

  • Universal-ImGui-Hook.sln builds the DLL and uih_host without CMake, into build/vs/<x64|x86>/<configuration>/.
  • Open the folder: Visual Studio reads CMakePresets.json.
  • Command line: cmake --preset msvc-x64, then cmake --build --preset msvc-x64.
MinGW-w64 on Windows

From an MSYS2 UCRT64 shell or with a winlibs toolchain in PATH:

pacman -S mingw-w64-ucrt-x86_64-toolchain mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja
cmake --preset mingw-x64 && cmake --build --preset mingw-x64
Windows DLL from Linux or macOS

With the mingw-x64 and mingw-x86 presets and the toolchain files in cmake/:

System Packages
Debian, Ubuntu cmake ninja-build g++-mingw-w64-x86-64 g++-mingw-w64-i686
Fedora cmake ninja-build mingw64-gcc-c++ mingw32-gcc-c++
Arch cmake ninja mingw-w64-gcc
macOS (Homebrew) cmake ninja mingw-w64
Linux library

With the linux preset:

Distribution Packages
Debian, Ubuntu cmake ninja-build g++ pkg-config libxcb1-dev
Fedora cmake ninja-build gcc-c++ pkgconf-pkg-config libxcb-devel libstdc++-static
Arch base-devel cmake ninja libxcb

It only needs libxcb and glibc at run time, but a glibc at least as recent as the build machine's: to share it or to use it in the Steam Linux Runtime, build on an old distribution or take the prebuilt one. Set CC=clang CXX=clang++ before configuring to use Clang.

CMake options and minimum versions

Options are passed at configure time, such as -DUIH_BUILD_TESTS=ON:

Option Default Effect
UIH_BUILD_EXAMPLE ON at top level The example DLL or shared object
UIH_BUILD_TESTS OFF uih_host and the CTest suite
UIH_STATIC_RUNTIME ON at top level Static C and C++ runtimes
UIH_SANITIZE OFF Linux with GCC: AddressSanitizer and UndefinedBehaviorSanitizer in every target, for the tests

Minimum versions: CMake 3.21, Visual Studio 2022, mingw-w64 11 with GCC 13, GCC 13 or Clang 18 on Linux; Windows 10 at run time. The mingw-w64 headers of Ubuntu 22.04 are too old for D3D12.

Tests

-DUIH_BUILD_TESTS=ON builds uih_host, which renders a solid color with one API and checks the overlay, and registers the scenarios with CTest:

cmake --preset msvc-x64 -DUIH_BUILD_TESTS=ON && cmake --build --preset msvc-x64
ctest --test-dir build/msvc-x64 -C Release

Some tests press real keys and move the mouse. They are skipped when the host window does not have the focus: leave the machine alone while they run. A test whose screen check fails saves the capture as build/tests/capture-*.bmp.

Without a GPU, and with the validation and debug layers

Without a GPU, as on the CI runners, Direct3D uses WARP, and Mesa's software drivers from mesa-dist-win cover the rest: copy opengl32.dll, libgallium_wgl.dll and dxil.dll next to uih_host.exe with GALLIUM_DRIVER=llvmpipe (or d3d12 for the D3D12-based driver of Microsoft's OpenGL compatibility pack, which needs dxil.dll to sign its shaders), and register lvp_icd.*.json under HKLM\SOFTWARE\Khronos\Vulkan\Drivers with a vulkan-1.dll from the Vulkan runtime.

UIH_TEST_VULKAN_VALIDATION=1 runs the Vulkan host under VK_LAYER_KHRONOS_validation, from the Vulkan SDK on Windows or vulkan-validationlayers on Linux, and UIH_TEST_D3D_DEBUG=1 creates the Direct3D 10, 11 and 12 devices with the debug layer, from the Graphics Tools optional feature of Windows. Any error they report, from the host or from the hook, fails the test.

The uih_host scenarios (Windows), one option each
uih_host option (Windows) Scenario
--api d3d9|d3d9ex|d3d10|d3d11|d3d12|vulkan|opengl Hook started in-process, overlay drawn once per frame, then Stop()
--inject-at 0, --settle <ms> Hook started before the API; delay before the swap chain is created
--capture Checks the overlay and an exact color on screen
--format srgb|float Back buffer storing linear values
--decoy-queue A second D3D12 device executes work every frame
--ignore-suboptimal The Vulkan host recreates its swap chain after VK_ERROR_OUT_OF_DATE_KHR only, as many engines do: the overlay, started after the swap chain was created, must still show
--scale <s>, --warp Fixed interface scale; software rasterizer
--resize Window and back buffers resized to 960x640, then 720x560, with the menu open
--fullscreen 800x600 exclusive fullscreen (D3D9, DXGI) or display mode change with a borderless window (Vulkan, OpenGL), then back; the window must cover the display and the overlay follow it. Skipped unless UIH_TEST_FULLSCREEN=1, set by the CI
--reload The overlay is started a second time: Start() after Stop(), or with --dll the DLL injected again, or, for a DLL named dinput8.dll, brought back by INSERT after F11
--remove F11 removes the overlay and INSERT brings it back, twice, without a module to free
--second-window A second window with its own device presents too: the overlay must stay on one window, follow the focus, and come back when the second window stops presenting. With --shared-device (Vulkan, D3D12), the second window renders with the device of the first
--gpu-latency ms The D3D12 GPU runs each frame ms after it was submitted, as a busy GPU does: the frames of the overlay are still in flight when it moves to another swap chain
--capture-frame Another thread copies a frame with uih::CaptureFrame() while the menu is open: the copy must have the size of the back buffer and show the game where the menu covers it
--remote Drives the remote control as remote/uih_mcp.py does: game description, screenshot without the overlay, message drawn, keys received by the window; then uih::Stop() must end it while a client stays connected
--fault access|exception|draw The render callback writes to a null pointer or throws, or a draw callback writes to a null pointer while the backend draws: the overlay must be removed while the host keeps rendering and can resize its back buffers, then start again (MSVC)
--slow-frame ms One frame of the menu lasts ms while Stop() is called from another thread: nothing that frame uses may be released under it. _slow_frame tests last 2.5 s, within the waits of Stop(); _stuck_frame tests last 6 s, beyond them. The hook must then start again
--bound-framebuffer The OpenGL host leaves a framebuffer object and a pixel unpack buffer bound at every swap: the overlay must still show on the window
--switch-api api Once the overlay showed, the host deletes its OpenGL context and renders with another API in a new window: the overlay follows it
--recreate-context The OpenGL context is deleted with the menu open and a new one, with textures of its own, takes over: the overlay comes back and leaves them alone
--input raw|directinput What the game reads while the menu is open, and in its window procedure: a key released while the menu is open must not stay down, Alt+F4 still closes the window, and the cursor is confined again when the menu closes, or when uih::Stop() closes it
--gamepad <fake xinput1_4.dll> Controller toggling the menu and hidden from the game while it is open
--resubclass The host subclasses its window after the overlay: with --reload the overlay starts again without calling itself through the host's procedure, with --dll the unload key leaves the DLL loaded and the procedures still chain
--render-thread The host presents from a thread of its own while the window thread dispatches a flood of input messages, and the hook starts and stops 20 times: no access violation may occur, and on x64 freed Dear ImGui memory is made inaccessible so that any use of it does
--dll <path> Loads the real DLL and drives it with INSERT and F11
--menu-key <vk>, --unload-key <vk> The keys a DLL built with other settings uses, such as 0x70 for F1 (X11 keysyms on Linux)
--proxy <path> Loads the DLL as dinput8.dll
--preview <path> Not a test: loads the DLL, opens its menu and renders until the window is closed, to look at a menu without a game
Real games: uih_game on Windows, tests/game.sh on Linux

uih_game --dll <path> --backend <name> -- game.exe [arguments] runs a game and injects the DLL once its window shows, as an injector does: the backend must become active, INSERT must show the menu on screen and F11 remove it while the game keeps running. It reads the log of the DLL as DebugView does, so DebugView must not run at the same time, and saves its captures as game-before.bmp, game-menu.bmp and game-after.bmp (--captures <prefix>).

tests/game.sh runs the same scenario on a Linux game, such as DDNet or a Godot project.

tests/game_mcp.py libuih_remote_preload.so <backend> mcp.png game [arguments] plays a Linux game through remote/uih_mcp.py, as an assistant does, with the remote control on: game description, screenshot, message, INSERT to open the menu and F11 to remove the overlay, after which the socket must be gone and the game still running.

Linux tests

On Linux, uih_host does not link the hook: each test preloads the .so, opens the menu, checks the pixels and removes the overlay. They cover:

  • GLX, EGL, dlsym() lookups and Vulkan;
  • --resize, --reload, --second-window, --recreate-context and --bound-framebuffer variants;
  • the remote control (--remote), with tests/remote_preload.cpp, the hook built with it on: game description, screenshot without the message of the assistant, message on screen, keys through XTEST with the focus and as X11 events without it;
  • presents through eglSwapBuffersWithDamage that redraw and post a single pixel once EGL_EXT_buffer_age allows it (--swap-with-damage);
  • the Vulkan loader opened as libvulkan.so (--vulkan-library).

They need libx11-dev libxtst-dev libgl-dev libegl-dev; without a display, run xvfb-run -a ctest --test-dir build/linux. With -DUIH_SANITIZE=ON, the preload tests load the sanitizer runtime before the library, and any memory error or undefined behavior in the hook fails them.

The CI: what runs on every push and pull request

The CI runs on every push and pull request:

Job Checks
MSVC x64, Win32 Build, D3D11 on WARP, dinput8.dll proxy
Windows tests x64, Win32 The whole CTest suite without GPU: WARP for Direct3D, Mesa's llvmpipe and lavapipe for OpenGL and Vulkan, then the OpenGL scenarios again on Mesa's D3D12-based driver, the Direct3D 10 to 12 ones under the debug layers and, on x64, the Vulkan ones under the validation layer; no test may be skipped
Visual Studio solution x64, x86 The .sln built with MSBuild, same tests
MinGW-w64 on Ubuntu and macOS Cross-compiled DLLs, proxy export
Linux GCC, Clang x86_64, GCC aarch64 The preload tests under Xvfb with llvmpipe and lavapipe, including resizes and the overlay brought back after F11, then the Vulkan ones under the validation layer
Linux sanitizers The preload tests with the library built with AddressSanitizer and UndefinedBehaviorSanitizer
Linux portable x86_64, aarch64 The released libraries, built and tested against glibc 2.28
DDNet, Godot on Linux The released library preloaded into DDNet and an empty Godot project (tests/godot), with OpenGL and Vulkan; then the same games played through the MCP server (tests/game_mcp.py)
DDNet, Godot on Windows The DLL injected into the same games once they render: DDNet with OpenGL and Vulkan, Godot with Direct3D 12, Vulkan and Direct3D 11 (its OpenGL renderer through ANGLE), without GPU
FetchContent consumer The repository as a CMake dependency

Pushing a v* tag publishes the MSVC and Linux portable libraries as a release, once every job above has passed.

Contributing

  • A new source file goes into CMakeLists.txt and the matching project in vs/.
  • A fix comes with a uih_host scenario that fails without it.
  • Run the CTest suite on Windows; the CI covers Linux.
Where the code is
Path Content
include/uih.h Public API
src/uih.cpp, src/internal.h Platform-independent core: lifecycle, frame, textures, scale
src/vulkan.cpp Vulkan renderer, shared by both platforms
src/windows/ MinHook, Direct3D, WGL, Win32 input, DirectInput, XInput, remote control (remote.cpp)
src/gl_state.h OpenGL state set aside while the overlay draws, and the frame capture shared by WGL, GLX and EGL
src/linux/ Symbol interposition, GLX, EGL, X11 input, exports.map
remote/ uih_mcp.py, the MCP server of the remote control, and the DLL injector it includes
example/ The built library: config.cpp (settings), menu.cpp (the widget tour), image.cpp and image.rc (imgui.png embedded), dllmain.cpp, proxy.cpp, preload.cpp
tests/ uih_host, uih_game, game.sh and game_mcp.py for real games, the Godot project they run, the FetchContent consumer
vs/, Universal-ImGui-Hook.sln Visual Studio projects
third_party/ Dear ImGui, MinHook, Vulkan headers, stb_image (example only)

Limitations

Windows

  • HDR10 swap chains (PQ color space) show the overlay with wrong colors, and CaptureFrame() only copies 8-bit and 10-bit back buffers: the floating-point ones of HDR output are not captured.
  • A Vulkan swap chain created before the hook has no overlay until the game recreates it: its presents report VK_SUBOPTIMAL_KHR, which games answer by recreating it.
  • When the game, or another overlay, subclasses the window after the hook, the hook's window procedure stays in its chain: the unload key leaves the DLL loaded, and injecting it again does not start it.
  • Native ARM64 Windows games are not supported: MinHook only handles x86 and x64.
  • Only Windows builds made with MSVC, the released ones, survive an exception in the overlay: MinGW-w64 has no structured exception handling, and Linux builds are not protected either.
  • D3D9: after an exception in a draw callback (ImDrawList::AddCallback), the state block of the Dear ImGui backend is never released, and the game cannot reset its device.

Linux

  • Input is read from the X server: native Wayland windows get no menu input (run SDL games with SDL_VIDEODRIVER=x11), and the game still receives the keyboard and the mouse while the menu is open. The mouse wheel and input methods are not handled.
  • 32-bit games, OpenGL ES, and games that link their GL or Vulkan loader statically are not supported.
  • The remote control sends no keys to native Wayland games.

Credits

Built with Dear ImGui, MinHook, Vulkan-Headers and stb_image (the example's picture).

Who What
Sh0ckFR, Revan600 ImGui and input hooks
Rebzzel Initial work on the kiero library (no longer used)
primemb Bug fixes

Support

This project is free and MIT-licensed. Building it from scratch represents roughly 83 days of senior C++ work, about €54,000 at French freelance rates: six graphics backends on two operating systems, input blocking, crash protection, frame capture and the remote control by an AI assistant, and about 130 test scenarios run on every commit, real games included. If it saved you time, consider sponsoring or starring the repository.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Nearby in the ranking
Similar items