Skip to main content

Shared texture rendering

By default, every painted overlay frame is delivered to the injected game as a CPU-side pixel copy before the game composites it. Shared texture rendering switches an overlay window to a GPU path: the overlay hands the GPU texture directly to the game, which composites it without a CPU-side pixel copy. This significantly lowers per-frame CPU overhead, which matters most for overlay windows that repaint continuously.

BETA

useSharedTexture is experimental and its behavior may change in a future release.

Requirements

  • ow-electron 39.8.10 or later. On earlier versions the option is ignored and the window uses the default CPU copy path.
  • A game running on D3D11 or D3D12. D3D9, OpenGL, and Vulkan games use the CPU copy path.
  • Application hardware acceleration must stay enabled. Shared texture is a GPU path, so do not call app.disableHardwareAcceleration() — it switches the whole application to CPU rendering. The window must also not set disableHardwareAcceleration. See Hardware acceleration.
  • Offscreen (OSR) overlay windows only.

Adopting shared texture requires no migration. It is opt-in and defaults to false.

Opting in

useSharedTexture is a top-level option on the createWindow options object, not a webPreferences entry:

const overlay = app.overwolf.packages.overlay;

const window = await overlay.createWindow({
name: 'my-shared-texture-overlay',
width: 1920,
height: 1080,
transparent: true,
useSharedTexture: true, // opt into the GPU shared-texture path
});

Knowing whether the path is active

Whether the path can actually be used depends on the game and on the user's machine, so it is only known after the overlay injects into a game. Whether to opt a window in is your decision — what the flag does not do is put that window at risk: if the path turns out to be unavailable, the window renders through regular CPU rendering instead of failing (see When shared texture is unavailable).

If you do want to know which path a window ended up on, game-window-changed carries two related fields on GameWindowInfo:

FieldAnswers
isSharedTextureSupportedDoes the game's graphics API support GPU textures? true for D3D11 / D3D12, false for D3D9 / OpenGL / Vulkan. A capability probe only.
isSharedTextureAvailableCan the path actually be used on this machine, for this game? false means the window is rendering on the CPU.
overlay.on('game-window-changed', (windowInfo, gameInfo, reason) => {
console.log('shared texture available:', windowInfo.isSharedTextureAvailable);
});

Both are undefined until the game is injected and its graphics API is detected, and isSharedTextureAvailable can turn false mid-game — it reflects the current state, not a one-time answer.

When shared texture is unavailable

You never get a blank overlay. When any requirement is unmet, the flag is silently ignored and the window renders through the CPU copy path with the only visible difference being default (rather than reduced) CPU overhead.

The rendering path also follows the active game for the lifetime of the window:

  • Moving to a game that does not support shared texture falls the window back to the CPU copy path.
  • Moving to a game that does support it restores the shared-texture path.

You do not need to destroy and recreate windows when the user switches games.

Note the difference between the two kinds of requirement:

  • Resolved once, at createWindow — the ow-electron version, offscreen rendering, and hardware acceleration. A window that was not eligible when it was created never becomes eligible; it has to be destroyed and recreated.
  • Re-evaluated per game, at runtime — the game's graphics API and its availability on this machine. This is what "follows the active game" refers to.

This is why hardware acceleration is a setup decision rather than a runtime one — see Hardware acceleration.

Reacting to the shared-texture-unavailable event

shared-texture-unavailable fires at most once per injected game and names the reason. By the time it fires the affected windows are already on the CPU copy path and remain visible and interactive.

ReasonWhat it meansWhat you can do
unsupportedGraphicsApiThe game's graphics API cannot composite GPU textures (D3D9 / OpenGL / Vulkan).Nothing — expected for these games.
gpuAdapterMismatchThe game and the overlay are running on different GPUs, which the shared-texture path cannot bridge.Fixable with setGpuPreference and an application restart.
copyFailureThe game could not use the textures it received.setGpuPreference may help when the cause is GPU-related.
overlay.on('shared-texture-unavailable', (reason) => {
// The overlay is already on the CPU copy path and still visible.
console.log(`Shared texture unavailable for this game: ${reason}`);
});

Pinning the application to one GPU

On multi-GPU machines — a laptop with integrated and discrete graphics, for example — the game may run on the discrete GPU while the overlay runs on the one driving the primary display. That is the gpuAdapterMismatch reason. setGpuPreference('highPerformance') records a Windows per-executable GPU preference so both run on the same adapter.

Read before calling

setGpuPreference is not a per-window switch, and it is never applied implicitly:

  • It moves the entire application's rendering to that GPU, draining laptop battery and keeping the discrete GPU awake.
  • It writes persistent, user-visible Windows state, visible to the user under Settings → System → Display → Graphics.
  • It is keyed on the executable path, so moving or renaming the application leaves a stale entry behind.
  • A restart is normally required — Windows reads the preference when the application starts.

Call setGpuPreference('default') to remove the entry. This is the recommended revert on uninstall, or when the user turns your overlay off.

getGpuPreference() tells you whether the application is already aligned and therefore needs no restart. It resolves to 'default' when no entry exists. setGpuPreference is idempotent, so calling it unconditionally is safe.

overlay.on('shared-texture-unavailable', async (reason) => {
if (reason !== 'gpuAdapterMismatch') return;

// Already pinned — nothing to do, and no restart to ask for.
if ((await overlay.getGpuPreference()) === 'highPerformance') return;

await overlay.setGpuPreference('highPerformance');
promptUserToRestart(); // takes effect on the next launch
});

Because this changes machine state on the user's behalf, prompt the user before calling it rather than pinning silently.

Full example

import { app } from 'electron';
import type { SharedTextureUnavailableReason } from '@overwolf/ow-electron-packages-types';

const overlay = app.overwolf.packages.overlay;

overlay.on('game-launched', (event, gameInfo) => {
if (gameInfo.supported === true) {
event.inject();
}
});

overlay.on(
'shared-texture-unavailable',
async (reason: SharedTextureUnavailableReason) => {
// The overlay already fell back to the CPU copy path and stays visible.
if (reason !== 'gpuAdapterMismatch') return;
if ((await overlay.getGpuPreference()) === 'highPerformance') return;

// Ask the user first — this changes a persistent Windows setting
// for the whole application.
if (!(await askUserToPinGpu())) return;

await overlay.setGpuPreference('highPerformance');
promptUserToRestart();
},
);

async function createOverlayWindow() {
return overlay.createWindow({
name: 'in-game-hud',
width: 1920,
height: 1080,
transparent: true,
// If the path cannot be used, this window renders through regular
// CPU rendering instead.
useSharedTexture: true,
});
}

BETA limitation: hit-testing ignores transparent pixels

On the default CPU copy path, the overlay hit-tests per pixel: mouse events over fully transparent areas of the window pass through to the game. Shared-texture windows do not do this. The hit region is the window's full bounding rectangle, so any mouse event inside it is captured by the overlay even over a fully transparent pixel.

This matters most for the common overlay shape — a small HUD inside a large, mostly transparent full-screen window. On the shared-texture path that window swallows clicks across the entire screen.

Workaround: pass through by default, capture per element

Invert the default. Create the window with passthrough set to 'passThroughAndNotify' so every mouse event reaches the game, then have the page capture input only while the pointer is over an interactive element.

In the main process, create the window and handle a message from the renderer that flips the mode. passthrough is writable on the window's overlayOptions:

// main.ts
import { ipcMain } from 'electron';
import type { PassthroughType } from '@overwolf/ow-electron-packages-types';

const sharedTextureWindow = await overlay.createWindow({
name: 'my-overlay-sht',
width: 1920,
height: 1080,
transparent: true,
useSharedTexture: true,
passthrough: 'passThroughAndNotify', // pass input to the game by default
});

ipcMain.on('setPassthrough', (_event, mode: PassthroughType) => {
sharedTextureWindow.overlayOptions.passthrough = mode;
});

In the renderer, block input while the pointer is over your UI and release it on the way out:

// page script (renderer)
const { ipcRenderer } = window.require('electron');

// Do this for every opaque, interactive element.
uiPanel.addEventListener('mouseenter', () => {
ipcRenderer.send('setPassthrough', 'noPassThrough');
});

uiPanel.addEventListener('mouseleave', () => {
ipcRenderer.send('setPassthrough', 'passThroughAndNotify');
});

'passThroughAndNotify' is what makes this work: input reaches the game and the window still receives the events, so mouseenter fires while the overlay is in pass-through mode. Plain 'passThrough' would deliver nothing to the page and the pointer could never be detected. See PassthroughType.

The cost is that you now maintain this on every interactive element, and elements added later need the same listeners. If that trade is not worth it, size the window to its visible content instead, or leave the window on the default CPU copy path.

Hardware acceleration

Shared texture is a GPU path, so application hardware acceleration is required. It is on by default, so the requirement in practice is that you do not turn it off.

Do not call app.disableHardwareAcceleration()

Electron's app.disableHardwareAcceleration() switches the application to a software output device: offscreen frames are produced by the CPU instead of the GPU. That is attractive on its own — for plain 2D content the software output device generates frames faster than the GPU-to-CPU bitmap path — but it comes with two catches:

  • It is application-wide. Every overlay window loses the GPU, so useSharedTexture is silently ignored everywhere and every window renders on the CPU copy path.
  • It only takes effect before the app is ready, and there is no way to re-enable it later. A window created while acceleration was off stays on the CPU path for its whole lifetime.

Software rendering also costs you GPU features in the page itself — no WebGL, no GPU-composited 3D CSS animations — and its CPU load scales with window size.

app.isHardwareAccelerationEnabled() reports the current state, and it is worth logging: it can be false without you doing anything, because a blocklisted GPU or driver leaves it off on machines you never tested. On those machines you simply lose the performance benefit, with nothing in the UI to indicate why.

Disable per window instead — disableHardwareAcceleration

ow-electron adds disableHardwareAcceleration as a window option, which is what makes the trade-off usable. It puts a single overlay window on software (CPU) compositing while the rest of the application keeps hardware acceleration — so you get the software output device where it pays off and shared texture everywhere else.

A good split: a small, static, text-only window that repaints rarely goes on disableHardwareAcceleration; the large HUD that repaints continuously stays on useSharedTexture.

// Small, static, no GPU content — software output device for this window only.
await overlay.createWindow({
name: 'my-software-rendered-overlay',
width: 800,
height: 600,
transparent: true,
disableHardwareAcceleration: true,
});

// The rest of the application is unaffected and still gets the GPU path.
await overlay.createWindow({
name: 'in-game-hud',
width: 1920,
height: 1080,
transparent: true,
useSharedTexture: true,
});

The two options are mutually exclusive on the same window. Shared texture requires hardware acceleration, so when both are set disableHardwareAcceleration wins and useSharedTexture is ignored. disableHardwareAcceleration requires ow-electron 39.8.10 or later and defaults to false.

API reference