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.
  • Hardware acceleration must be enabled, both for the application and for the window. Shared texture is a GPU path, so app.isHardwareAccelerationEnabled() must return true and the window must 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
});

Gate on per-game availability

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. Gate window creation on GameWindowInfo.isSharedTextureAvailable from the game-window-changed event:

overlay.on('game-window-changed', (windowInfo, gameInfo, reason) => {
if (windowInfo.isSharedTextureAvailable === true) {
// Safe to create windows with useSharedTexture: true for this game.
} else {
// Not available for this game — create windows normally (CPU copy path).
}
});

GameWindowInfo exposes two related fields, and they answer different questions:

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? Gate on this one.

Both are undefined until the game is injected and its graphics API is detected.

note

isSharedTextureAvailable can turn false mid-game. Treat it as the current state rather than a one-time answer, and read it again on every game-window-changed.

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 worth checking before you create the window.

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 {
GameWindowInfo,
SharedTextureUnavailableReason,
} from '@overwolf/ow-electron-packages-types';

const overlay = app.overwolf.packages.overlay;

let sharedTextureAvailable = false;

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

overlay.on('game-window-changed', (windowInfo: GameWindowInfo) => {
sharedTextureAvailable = windowInfo.isSharedTextureAvailable === true;
});

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,
// Both must hold: the application is GPU-accelerated, and the current
// game can use the path. Resolved once, here.
useSharedTexture:
app.isHardwareAccelerationEnabled() && sharedTextureAvailable,
});
}

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 it depends on hardware acceleration at two independent levels. Either one being off disables the path.

Application level — app.isHardwareAccelerationEnabled()

If hardware acceleration is off for the whole application, useSharedTexture is silently ignored and the window renders on the CPU copy path. app.isHardwareAccelerationEnabled() is the check — verify it returns true before you create windows that expect shared-texture frames:

import { app } from 'electron';

const canUseSharedTexture =
app.isHardwareAccelerationEnabled() &&
windowInfo.isSharedTextureAvailable === true;

await overlay.createWindow({
name: 'in-game-hud',
width: 1920,
height: 1080,
transparent: true,
useSharedTexture: canUseSharedTexture,
});

Two things make this worth an explicit check rather than an assumption:

  • It can be false without you doing anything. Your own call to app.disableHardwareAcceleration() turns it off, but so can the user's system — a blocklisted GPU or driver leaves it off on machines you never tested.
  • It is evaluated once, when the window is created, and app.disableHardwareAcceleration() only takes effect before the app is ready. There is no way to recover an already-created window, so a window created while acceleration was off stays on the CPU path for its whole lifetime.

Because the flag is silently ignored, an app that never checks this simply loses the performance benefit on those machines with nothing in the UI to indicate why.

Window level — disableHardwareAcceleration

disableHardwareAcceleration renders a single overlay window via software (CPU) compositing. Unlike app.disableHardwareAcceleration(), it is scoped to one window and does not affect the rest of the application.

await overlay.createWindow({
name: 'my-software-rendered-overlay',
width: 800,
height: 600,
transparent: true,
disableHardwareAcceleration: true, // this window only, CPU compositing
});

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

Removed option

The reserved enableHWAcceleration option had no effect and has been removed. If you set it, replace it with disableHardwareAcceleration — note that the meaning is inverted.

API reference