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(), andRender()). 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:
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:
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:
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:
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:
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
ViewandRenderermethods only on the thread that created theRenderer(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():
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.