procmod-overlay 3.0.0

Game overlay rendering with transparent click-through windows
Documentation

Create a transparent, click-through overlay window on top of any game window and draw shapes, text, and HUD elements. Direct3D 11 backend, immediate-mode API, built-in font rendering.

Install

[dependencies]
procmod-overlay = "3.0.0"

Quick start

Draw status information and projected debug geometry over a game window:

use procmod_overlay::{Overlay, OverlayTarget, Color};

fn main() -> procmod_overlay::Result<()> {
    let mut overlay = Overlay::new(OverlayTarget::Title("My Game".into()))?;

    loop {
        overlay.begin_frame()?;

        overlay.rect(100.0, 50.0, 60.0, 120.0, Color::RED);
        overlay.rect_filled(100.0, 175.0, 45.0, 8.0, Color::GREEN);
        overlay.text(20.0, 20.0, "instrumentation active", 16.0, Color::WHITE);

        overlay.end_frame()?;
    }
}

Usage

Creating an overlay

Find the target window by title, class name, HWND, or process ID:

// by window title (substring match)
let overlay = Overlay::new(OverlayTarget::Title("Counter-Strike".into()))?;

// by window class
let overlay = Overlay::new(OverlayTarget::Class("UnrealWindow".into()))?;

// by process ID
let overlay = Overlay::new(OverlayTarget::Pid(1234))?;

// by raw HWND
let overlay = Overlay::new(OverlayTarget::Hwnd(0x00010A3C))?;

PID lookup only enumerates windows on the caller's current interactive desktop. The overlay must run in the logged-in desktop session that contains the target window. A process started directly from a noninteractive SSH session cannot discover windows on another desktop or session.

ProcessNotFound means the PID does not exist or cannot be queried. ProcessWindowNotFound means the process exists but has no visible top-level window on the current desktop. Windows does not expose enough information here to reliably distinguish a process with no window from one whose window belongs to another desktop or session.

Interaction

Overlays are click-through and non-activating by default. Applications can explicitly enter an interactive control mode and consume window input events:

use procmod_overlay::{InputEvent, InteractionMode, MouseButton};

overlay.set_interaction_mode(InteractionMode::Interactive)?;

for event in overlay.drain_input_events() {
    match event {
        InputEvent::MouseButton {
            button: MouseButton::Left,
            pressed: false,
            x,
            y,
        } => println!("clicked at {x}, {y}"),
        InputEvent::Key {
            virtual_key: 0x1b,
            ..
        } => overlay.set_interaction_mode(InteractionMode::PassThrough)?,
        _ => {}
    }
}

Interactive removes click-through and no-activate styles, requests foreground activation, displays the overlay cursor, and reports mouse, wheel, keyboard, text, focus, and close events. Returning to PassThrough immediately restores click-through behavior, releases mouse capture, and requests target-window activation. Windows can deny either foreground request; input routing still changes successfully, and the current focus is observable through Focused events, is_target_foreground, and is_overlay_foreground. Applications own hit testing, widgets, key bindings, and menu state.

Input is collected while begin_frame pumps the window message queue. Consecutive mouse-move events are coalesced and the queue is bounded. take_dropped_input_event_count reports and resets the number of events discarded at capacity. Text events decode UTF-16 characters, but IME composition is not exposed.

CloseRequested lets the application save state or ask for confirmation. Call overlay.close() to accept the request; the next begin_frame returns OverlayClosed.

Lifecycle

begin_frame reports OverlayClosed when the overlay window closes and TargetWindowLost when the target HWND is destroyed or becomes inaccessible. Renderer failures remain renderer errors. The consumer owns reconnection policy and any application-state reset after reconnecting.

A driver reset, a GPU hang, or an adapter change destroys the graphics device. The overlay releases the dead device, builds a replacement during the next begin_frame, and re-uploads the glyph atlas, so drawing code needs no changes and the render loop keeps running. The frame in flight when the device went away is dropped. take_device_reset_count reports and resets how many rebuilds happened, for applications that log it or reset frame timing:

overlay.begin_frame()?;

if overlay.take_device_reset_count() > 0 {
    // the previous frame was lost with the device
}

Error::DeviceLost is only returned when the replacement device cannot be built, which usually means the adapter is gone for good.

Drawing shapes

All drawing happens between begin_frame and end_frame:

overlay.begin_frame()?;

// filled and outlined rectangles
overlay.rect_filled(10.0, 10.0, 200.0, 30.0, Color::rgba(0, 0, 0, 180));
overlay.rect(10.0, 10.0, 200.0, 30.0, Color::WHITE);

// lines
overlay.line(0.0, 0.0, 100.0, 100.0, 2.0, Color::RED);

// circles
overlay.circle_filled(150.0, 150.0, 20.0, Color::rgba(56, 189, 248, 128));
overlay.circle(150.0, 150.0, 20.0, Color::CYAN);

overlay.end_frame()?;

Drawing text

Text rendering uses an embedded font with configurable size:

overlay.text(20.0, 20.0, "Player1 [100HP]", 16.0, Color::WHITE);
overlay.text(20.0, 40.0, "Distance: 42m", 12.0, Color::YELLOW);

// measure text bounds before drawing
let (w, h) = overlay.text_bounds("centered text", 16.0);
overlay.text(320.0 - w / 2.0, 10.0, "centered text", 16.0, Color::WHITE);

Outlined and aligned text

An overlay draws over scenery it does not control, and text with no outline vanishes wherever the background happens to match the text color. TextStyle adds an outline and per-line alignment:

use procmod_overlay::{TextAlign, TextStyle};

let label = TextStyle::new(16.0, Color::WHITE)
    .outlined(Color::BLACK, 1.5)
    .aligned(TextAlign::Center);

for enemy in &enemies {
    overlay.text_styled(enemy.x, enemy.y - 20.0, &enemy.name, &label);
}

The outline is read from a distance field baked into the glyph atlas when the overlay starts, so its thickness stays uniform around curves and corners, and drawing a string costs one extra draw call however long the string is. Outline width is clamped to TextStyle::max_outline_width(size), which is 7 pixels at size 16 and grows with the font size.

Each line of a multi-line string is aligned independently. text draws left-aligned text with no outline.

Platform support

Platform Backend Status
Windows Direct3D 11 Supported
Linux - Planned
macOS - Planned

The crate compiles on all platforms but only exports the overlay API on Windows.

How it works

The overlay creates a transparent, always-on-top window (WS_EX_LAYERED | WS_EX_TRANSPARENT) positioned over the target game window. All mouse and keyboard input passes through to the game. The overlay tracks the target window's position and resizes automatically.

Rendering uses Direct3D 11 with alpha blending. Geometry accumulates into one vertex and index buffer per frame, and consecutive runs that need the same pipeline state are merged into a single draw call. Text is rasterized into a two-channel glyph atlas at startup, holding coverage for the fill and a distance field for outlines, and rendered as textured quads.

Demo

Run the visual demo on Windows to see the overlay in action:

cargo run --example demo

This creates a dark window simulating a game scene, overlays it with ESP boxes, health bars, a crosshair, and text labels. The screenshot above was captured from this demo.

License

MIT