docs

Integrating into a Game Engine

Embed Ultralight into a game engine, configure rendering, and connect UI to game state.

On this page

You can embed Ultralight directly into an existing game engine while keeping the engine's own render loop, window management, and asset pipeline.

A complete integration connects the renderer to the graphics API, forwards input events, listens for page events, and synchronizes game state with the UI.

👍 First-time setup

If you haven't displayed a View yet, start with Your First Game UI to set up the CPU renderer and the frame loop (Update(), RefreshDisplay(), and Render()). For frame timing details, see Updating and Rendering.

Choose a Render Path

You select a render path for each View at creation time using ViewConfig::is_accelerated.

Path is_accelerated What Native Code Reads Destination
CPU (default) false View::surface() Pixel buffer in system memory
GPU true View::render_target() GPU texture created by the driver

CPU Renderer

The CPU renderer requires no graphics integration code, making it a straightforward choice for menus and static HUDs. The engine uploads the View's pixels to an engine texture whenever they change— see Your First Game UI.

To render directly into memory the engine already owns and avoid a copy, provide a custom Surface factory— see Render Surfaces.

GPU Renderer

The GPU renderer draws everything on the GPU, including paths, text, gradients, and video. It runs faster for animated or complex interfaces, and it allows the page to display live engine textures.

Register a GPU Driver

The library emits abstract drawing commands that you translate to the engine's graphics API through a GPUDriver implementation. You retain ownership of the driver.

Register the driver on the Platform singleton before calling Renderer::Create(), then enable acceleration on the View:

C++
Platform::instance().set_gpu_driver(gpu_driver);  // before Renderer::Create()

ViewConfig config;
config.is_accelerated = true;
RefPtr<View> view = renderer->CreateView(1280, 720, config, nullptr);

Display the Render Target

After calling Renderer::Render(), call View::render_target() to get the driver's texture handle for that View.

Because the texture may be padded, draw the quad using RenderTarget::uv_coords.

Reference Drivers

The SDK provides reference drivers for Direct3D 11, Direct3D 12, Metal, and OpenGL in the platform folder that you can copy and adapt.

For implementation details, see GPU Renderer Overview and Implementing a GPUDriver.

Transparency and Background Color

Create a Transparent HUD

To display UI over gameplay, enable transparency in the View configuration:

C++
ViewConfig hud_config;
hud_config.is_transparent = true;
RefPtr<View> hud = renderer->CreateView(1920, 1080, hud_config, nullptr);

Set a transparent background on the root elements in CSS as well:

CSS
html, body { background: transparent; }

Blend the HUD Over the Scene

Rendered pixels use 32-bit BGRA format with premultiplied alpha. Composite the texture over the scene using premultiplied-alpha blending— straight-alpha blending produces dark fringes along transparent edges.

Background Color of Opaque Views

An opaque View paints white until page styles finish loading, causing a white flash at startup. Set ViewConfig::background_color to match the page's background:

C++
ViewConfig menu_config;
menu_config.background_color = Color(0.08f, 0.08f, 0.1f);  // the page's own

Forward Input

Mouse, Keyboard, and Scroll

Forward input events to the page using View::FireMouseEvent(), View::FireKeyEvent(), and View::FireScrollEvent(). Coordinates must be in logical pixels relative to the top-left corner of the View.

For details on constructing these events, see Your First Game UI, Mouse and Scroll Input, and Keyboard Input.

Gamepad Input

Gamepad events route through the Renderer rather than an individual View. Call Renderer::SetGamepadDetails() for each controller before firing its events— see Gamepad Input.

Route Keys Between UI and Gameplay

When gameplay and a HUD share the keyboard, check View::HasInputFocus() before routing key events to the page:

C++
void OnEngineKey(const KeyEvent& evt) {
  if (hud->HasInputFocus()) {
    hud->FireKeyEvent(evt);  // the player is typing in a text field
    return;
  }
  HandleGameplayKey(evt);
}

The method returns true only when an editable element (such as a text field or text area) has visible keyboard focus with a blinking caret. It returns false for checkboxes, select elements, and when the View itself is unfocused.

When a menu or modal dialog opens, forward every key event to the View so keys like Tab, Enter, and the arrows can navigate the page.

Manage View Focus

Call View::Focus() and View::Unfocus() as input moves between Views or transitions between UI and gameplay. Focus activates visual styling on the page, such as selection highlights.

When you call View::Unfocus(), the focused element receives a blur event but remains document.activeElement. The next time you call View::Focus(), focus and the caret return to that element— see Keyboard Focus and Editable State.

Handle Page Events

To respond to page events like title changes, tooltips, and console messages, attach a ViewListener by calling View::set_view_listener()— see Handling View Events.

Update the Cursor

ViewListener::OnChangeCursor() fires when the CSS cursor under the pointer changes. Use this callback to update the OS or engine cursor.

Handle New Windows

ViewListener::OnCreateChildView() fires when the page attempts to open a window using window.open() or a link with target="_blank". Return a newly created View to allow the window, or return nullptr to block it.

đźš§ Retain child Views

The library does not keep a reference to the View returned from OnCreateChildView(). You must retain a reference for as long as the child View remains open.

Connect UI to Game State

You can bridge UI and game state using three header families, which can be mixed on the same View:

API Header Purpose
Data Bindings <Ultralight/dom/data/Context.h> Synchronizes C++ data with the page. Recommended for HUDs and menus— see About Data Bindings.
DOM API <Ultralight/DOM.h> Inspects and modifies elements, styles, and attributes from C++ without running page scripts— see About the DOM API.
JavaScript API <Ultralight/JS.h> Calls JavaScript functions from C++ and exposes C++ functions to the page with type checking— see About JavaScript Interop.

Manage Threads

đźš§ Keep View and Renderer calls on one thread

Call View and Renderer methods only on the thread that created the Renderer (such as the engine's main thread or a UI thread). Renderer::PostTask() is safe to call from any thread.

Pass Worker Results to the Renderer Thread

To pass results from a worker thread to the renderer thread, call Renderer::PostTask():

C++
worker_thread.OnFinished([renderer, result] {
  renderer->PostTask([result] { ApplyResult(result); });
});

Tasks run in posting order on the Renderer thread during the next call to Renderer::Update().

You should capture data by value so it outlives the worker thread.

Update Data Bindings on Other Threads

A data binding Context lives on the thread that created it (such as a simulation thread). You can modify bound data on that thread without locks.

Call Context::Sync() to send the changes to the page— see Binding Threads and Lifetime.

Performance

👍 Hide inactive Views

When a menu or overlay closes, call View::set_visible(false) instead of destroying and recreating it. A hidden View stops painting and keeps its DOM and script state for quick reuse.

For techniques on making the UI render faster and use less memory, see Optimizing Performance.