On this page 13 sections
b0.5.49 (appended to BLSE_HostAPI; check BLSE_HOST_HAS(host, add_device_callback) before use).
The HUD overlay draws text, boxes, images and shapes. For anything else, such as a blurred background behind a menu, glow, heat haze or 3D shapes, a native plugin can draw with the game's own Direct3D 9 device, at a point BLSE chooses: just before BLSE's overlay paints its elements, or just after. A weapon wheel then draws a blurred copy of the screen before the overlay, and its wheel (made of hud_* elements) stays sharp on top.
Python has no access to this: Direct3D is for native code.
Come here only if the HUD overlay cannot draw what you need. This page assumes you know some Direct3D 9 programming: devices, render targets, textures, vertex formats and render states. The examples are C++ (a .cpp plugin), the usual way to call Direct3D's COM interfaces; the BLSE side is plain C from include/blse_api.h.
A minimal working example: darken the whole screen a little while the player is in play (not in menus), behind BLSE's own HUD elements.
#include <d3d9.h>
#include "blse_api.h"
static const BLSE_HostAPI *host;
struct Vertex { float x, y, z, rhw; DWORD color; }; /* pre-transformed: pixel positions */
static void BLSE_CALL draw(const BLSE_DrawFrame *f, void *user) {
if (!(f->flags & BLSE_DRAW_HUD) || !f->device) return; /* only over play, only with a device */
IDirect3DDevice9 *d = (IDirect3DDevice9 *)f->device;
const DWORD alpha = (DWORD)(80 * f->overlay_alpha / 255); /* fade in with the HUD */
const DWORD c = alpha << 24; /* black, partly transparent */
const float w = (float)f->width, h = (float)f->height;
const Vertex quad[4] = { {0, 0, 0, 1, c}, {w, 0, 0, 1, c}, {0, h, 0, 1, c}, {w, h, 0, 1, c} };
/* set every state we rely on: BLSE restores the game's afterwards */
d->SetVertexShader(NULL);
d->SetPixelShader(NULL);
d->SetTexture(0, NULL);
d->SetFVF(D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
d->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
d->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
d->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
d->SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE);
d->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
d->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
d->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
d->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
d->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
d->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
d->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
d->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(Vertex));
}
extern "C" BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {
host = h;
if (h->api_version != BLSE_API_VERSION || !BLSE_HOST_HAS(h, add_device_callback)) return 0;
if (!h->add_draw_callback(h->context, BLSE_DRAW_BEFORE_OVERLAY, draw, NULL))
h->log("drawing is not available");
return 1;
}This example keeps its callback all the time for brevity; a real plugin adds it only while it has something to draw (see below). It makes no Direct3D resources, so it needs no device callback.
Important, before you start:
- Only on the main thread, only inside your draw callback. The device is prepared for drawing (scene open, state saved) only during the callback. Outside it,
d3d9_devicegives you the device for creating resources, not for drawing. - Set every render state you rely on, and change nothing else. BLSE restores the game's state after your callback; the game leaves the device in any state before it.
- Never call
BeginScene,EndScene,PresentorReset, and neverReleasethe device. - Handle device loss if you create
D3DPOOL_DEFAULTresources (render targets, dynamic buffers): release them onBLSE_DEVICE_LOST, or the game cannot reset its device (Alt+Tab from full screen would then break the game). - Add a draw callback only while you draw: every callback costs time on every frame, even one that returns at once.
frame->devicemay beNULL: always check it.- A crash in a callback disables your plugin (see Crashes and unloading).
Words used on this page#
| Word | Meaning |
|---|---|
| Direct3D 9 device | The IDirect3DDevice9 object the game draws everything with. You draw with the same one |
| Overlay | BLSE's HUD overlay: the text, boxes, images and shapes from hud_* functions. Your callback runs just before or just after it |
| Paint | One frame of the game's interface being drawn. The game paints once per rendered frame |
| Scene | Direct3D's drawing window within a frame, between BeginScene and EndScene. The game opens and closes it; your callback runs inside it |
| Render target | The surface drawing goes to. Render target 0 is the frame being built (normally the back buffer, shown at Present) |
| State block | A Direct3D object that records device settings and puts them back. BLSE uses one to restore the game's state after your callback |
| Device loss | Direct3D 9 loses its video memory when the game leaves full screen (Alt+Tab) or changes resolution. The game then resets the device; every resource in the default pool must be released before that |
D3DPOOL_DEFAULT, D3DPOOL_MANAGED |
Where a resource lives: default-pool resources (render targets, dynamic buffers) are lost on a reset; managed ones survive it |
| Pre-transformed vertices | Vertices given in screen pixels (D3DFVF_XYZRHW), so no matrices are needed: the usual way to draw 2D |
The functions#
int add_draw_callback(void *context, int when, BLSE_DrawFn callback, void *user);
int remove_draw_callback(void *context, int when, BLSE_DrawFn callback, void *user);
void *d3d9_device(void *context); /* IDirect3DDevice9*, or NULL */
int add_device_callback(void *context, BLSE_DeviceEventFn callback, void *user);
typedef void (BLSE_CALL *BLSE_DrawFn)(const BLSE_DrawFrame *frame, void *user);
typedef void (BLSE_CALL *BLSE_DeviceEventFn)(int event, void *device, void *user);when is BLSE_DRAW_BEFORE_OVERLAY (after everything the game draws: the 3D view, its HUD, its menus and windows, mod panels; BLSE's overlay comes next) or BLSE_DRAW_AFTER_OVERLAY (after the overlay and the cursor-mode pointer: on top of everything). A callback (the same function and user) is added once per when; callbacks run in the order they were added. All four functions are for the main thread; they return 1, or 0 (add_*: an invalid when, a NULL callback, already added; remove_draw_callback: not added).
Nothing is installed until a plugin calls one of them. The draw callbacks run inside BLSE's existing overlay paint hook; device events come from the game's own release/restore step (no hook).
add_draw_callback#
int add_draw_callback(void *context, int when, BLSE_DrawFn callback, void *user);Asks BLSE to call callback(frame, user) on every painted frame, at the point when. (b0.5.49)
when:BLSE_DRAW_BEFORE_OVERLAY(0) to draw under BLSE's HUD elements,BLSE_DRAW_AFTER_OVERLAY(1) to draw over them and the cursor-mode pointer. The game's own HUD and menus are under both.callback: your function; it receives aBLSE_DrawFramedescribing the frame, and youruservalue unchanged.- The same function with the same
usercan be added once perwhen(add it twice with differentuservalues to get two calls). - Callbacks run in the order they were added, across plugins.
- Returns 1, or 0 if
whenis invalid,callbackisNULL, it is already added, or it is not called on the main thread. - When it is not called: see When callbacks are called.
remove_draw_callback#
int remove_draw_callback(void *context, int when, BLSE_DrawFn callback, void *user);Removes a callback added with the same when, callback and user. (b0.5.49) It is not called again from that moment, also not later in the same frame. A callback may remove itself or another one. Returns 1, or 0 if it was not added (by your plugin). Main thread.
/* open and close a panel that needs a blurred background */
static void open_wheel(void) { host->add_draw_callback(host->context, BLSE_DRAW_BEFORE_OVERLAY, draw_blur, NULL); }
static void close_wheel(void) { host->remove_draw_callback(host->context, BLSE_DRAW_BEFORE_OVERLAY, draw_blur, NULL); }d3d9_device#
void *d3d9_device(void *context);Returns the game's IDirect3DDevice9* (cast it), or NULL in the same cases as frame->device (see The device and what BLSE does around your callback) and when not called on the main thread. (b0.5.49)
- No reference is added: do not
Releaseit. - Use it to create resources ahead of time (textures, render targets) or from a device callback. Outside a draw callback the device is not prepared for drawing: do not draw with it there.
add_device_callback#
int add_device_callback(void *context, BLSE_DeviceEventFn callback, void *user);Asks BLSE to call callback(event, device, user) around every device reset. (b0.5.49)
event:BLSE_DEVICE_LOST(1), before the game resets the device: release every default-pool resource, state block, query and extra swap chain at once.BLSE_DEVICE_RESET(2), after the reset worked: create them again (or lazily the next time you draw).device: the device.- Called on the main thread, from the game's own release/restore step. A crash in it disables the plugin.
- The same function with the same
usercan be added once. There is no remove function: device callbacks go with the plugin. - Returns 1, or 0 if
callbackisNULL, it is already added, or it is not called on the main thread. - Plugins that make default-pool resources must add one. Details: Device loss and reset.
static IDirect3DTexture9 *target; /* a render target: D3DPOOL_DEFAULT */
static void BLSE_CALL device_event(int event, void *device, void *user) {
if (event == BLSE_DEVICE_LOST && target) { target->Release(); target = NULL; }
/* BLSE_DEVICE_RESET: nothing to do, draw() makes the texture again when it is NULL */
}BLSE_DrawFrame#
| Field | Meaning |
|---|---|
struct_size |
sizeof(BLSE_DrawFrame) when called |
when |
which of the two calls this is |
width, height |
the screen in pixels |
flags |
BLSE_DRAW_LEVEL a level runs; BLSE_DRAW_HUD BLSE's overlay shows over play (a level runs and no game screen covers it; the console does not count); BLSE_DRAW_MENU a game screen other than the console, or the game menu, is up; BLSE_DRAW_CONSOLE the console is open; BLSE_DRAW_LOADING the loading screen is up (also while it fades out) |
overlay_alpha |
0..255: how much of the game's HUD and the overlay shows: 255 in play, rising while the loading screen fades out, 0 when hidden. Fade your drawing with it to match the HUD |
device |
the IDirect3DDevice9*, or NULL (see below) |
frame |
counts painted frames; both calls of one frame carry the same number |
When callbacks are called#
On every frame the game paints its interface, which is every rendered frame:
- while a level runs (play, with or without the HUD);
- with game screens and the game menu open, in the main menu (no level), with the console open;
- for the frames the engine paints while a level loads (the loading screen; there are few);
- not for frames the game does not paint (the window minimized), not while the device is lost (callbacks run, but with
deviceNULL), and not at all if BLSE's HUD overlay is unavailable (anothervguimatsurface.dllthan the analyzed one; the log says "HUD overlay unavailable").
Decide from flags whether to draw: a weapon wheel draws while BLSE_DRAW_HUD is set (or only while it is open), an effect for menus while BLSE_DRAW_MENU is set.
The device and what BLSE does around your callback#
frame->device (and d3d9_device) is the game's device, without an added reference (do not Release it). It is NULL when the game's shaderapidx9.dll or MaterialSystem.dll is not the analyzed version (BLSE could not pass device loss on, so it does not hand the device out), before the game created it, between BLSE_DEVICE_LOST and BLSE_DEVICE_RESET, and when BLSE could not save the device state (logged once).
At that point the game's frame is being built: the scene is open (the game's BeginScene came before, its EndScene and Present come after), and render target 0 is the frame (normally the back buffer). Drawing lands on screen; StretchRect from render target 0 copies what is on screen so far.
Before each callback BLSE
- flushes the game's buffered drawing (the game batches interface quads: without this, quads drawn just before would land on top of yours, and a copy of the screen would miss them);
- saves the device state: a
D3DSBT_ALLstate block (render, sampler and texture stage states, textures, shaders and their constants, vertex streams, index buffer, vertex declaration and FVF, viewport, scissor rectangle, transforms, clip planes, lights, material), render targets 0..3 and the depth-stencil surface.
After the callback it sets the render targets and depth-stencil surface back and applies the state block. So:
Set every state you rely on. The game leaves the device in whatever state its last draw needed: z test, alpha test and blending, cull mode, scissor test, fog, clip planes, sRGB writes, shaders, texture stages, sampler filtering and addressing. The minimal example at the top of this page sets a short list. For 2D drawing with pre-transformed vertices and one texture, these are the usual ones:
d->SetVertexShader(nullptr); d->SetPixelShader(nullptr); d->SetFVF(your_fvf); d->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); d->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); d->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE); d->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); d->SetRenderState(D3DRS_STENCILENABLE, FALSE); d->SetRenderState(D3DRS_FOGENABLE, FALSE); d->SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE); d->SetRenderState(D3DRS_CLIPPLANEENABLE, 0); d->SetRenderState(D3DRS_LIGHTING, FALSE); d->SetRenderState(D3DRS_SPECULARENABLE, FALSE); d->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); d->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD); d->SetRenderState(D3DRS_COLORWRITEENABLE, 0xF); d->SetRenderState(D3DRS_SRGBWRITEENABLE, FALSE); d->SetRenderState(D3DRS_DITHERENABLE, FALSE); d->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, FALSE); d->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); d->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD); d->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); d->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); d->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); d->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); d->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); d->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); d->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); d->SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0); d->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); d->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); d->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); d->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); d->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); d->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_NONE); d->SetSamplerState(0, D3DSAMP_MAXMIPLEVEL, 0); d->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP); d->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP); d->SetSamplerState(0, D3DSAMP_SRGBTEXTURE, 0);Change nothing outside that state: do not release game objects or the device, do not call
BeginScene,EndScene,Present,Reset,SetGammaRamp,SetDialogBoxMode,SetCursorProperties,SetSoftwareVertexProcessing,EvictManagedResources.Unbind your own textures before returning if you want them freed promptly (the restore puts the game's back anyway).
Keep it short: it runs every frame, inside the game's paint.
Add the callback only while you draw. BLSE flushes the game's drawing and saves and restores the whole device state around every call, also when your callback returns at once: that costs every frame. While there is nothing to draw (the wheel is closed), remove it, and add it again when needed (from a message or key handler, on the main thread; a callback may remove itself).
Use only standard Direct3D 9 (no
d3dx9, which may not be installed; compile shaders offline or ship bytecode). DXVK and overlays such as ReShade then work too.
Draw before the overlay to put something under BLSE's elements, after it to cover them. The game's own HUD and menus are always under both.
Device loss and reset#
add_device_callback: BLSE_DEVICE_LOST comes before the game resets the device (Alt+Tab from full screen, a resolution or display mode change in the options): release, at once, every D3DPOOL_DEFAULT resource you made (render targets, dynamic buffers, textures in the default pool), state blocks, queries and additional swap chains, or the game's Reset fails. BLSE_DEVICE_RESET comes after the reset worked: make them again, or lazily the next time you draw. D3DPOOL_MANAGED resources survive a reset. device is the device. Called on the main thread; a crash disables the plugin.
A plugin that makes default-pool resources must add a device callback.
Crashes and unloading#
Each callback runs under BLSE's crash guard: a crash inside it disables the plugin (its callbacks are removed with everything else) and is logged; BLSE then restores the device state as usual. Callbacks also go when a plugin declines to load. A callback may remove itself or another one; a removed callback is not called again, also later in the same frame.
Example: a blurred box behind a wheel#
The outline:
#include <d3d9.h>
static void BLSE_CALL draw(const BLSE_DrawFrame* f, void* user) {
/* added when the wheel opens, removed when it closes (see above) */
if (!(f->flags & BLSE_DRAW_HUD) || !f->device) return;
IDirect3DDevice9* d = (IDirect3DDevice9*)f->device;
IDirect3DSurface9* screen = NULL;
if (FAILED(d->GetRenderTarget(0, &screen))) return;
/* 1. StretchRect(screen, &rect, copy, NULL, D3DTEXF_NONE): the region, same size.
2. Halve it a few times by drawing textured quads into smaller render-target textures
(bilinear filtering averages 2x2 texels), blur the smallest one (a few bilinear taps
at growing offsets), draw it back up level by level.
3. SetRenderTarget(0, screen) and draw the result over the region, darkened a little. */
screen->Release();
}
static void BLSE_CALL device_event(int event, void* device, void* user) {
if (event == BLSE_DEVICE_LOST) release_render_targets();
}
/* in BLSE_PluginLoad: */
host->add_device_callback(host->context, device_event, NULL);
/* when the wheel opens / closes (main thread): */
host->add_draw_callback(host->context, BLSE_DRAW_BEFORE_OVERLAY, draw, NULL);
host->remove_draw_callback(host->context, BLSE_DRAW_BEFORE_OVERLAY, draw, NULL);The wheel itself is ordinary hud_* elements (in Python or from the plugin), drawn by the overlay after the blur.
How it works#
The device comes from shaderapidx9.dll (its CShaderAPIDX8, the ShaderApi027 interface, keeps it at +0x1C). Device loss: the material system calls its list of release functions before every Reset and its restore functions after; BLSE adds one of each. The game creates a pure device on most hardware (D3DCREATE_PUREDEVICE): BLSE logs the creation flags once.