Updating and Rendering
Update timers, advance animations, and render pages from an existing frame loop.
On this page
You can integrate Ultralight into an existing run loop by calling three Renderer methods each frame.
The core library has no loop of its own— you call these methods from a game loop, UI loop, or display link so pages update, animate, and paint in step with the application.
📘 Handled Automatically by AppCore
If you use AppCore,
App::Run()makes these calls for you. See App Lifecycle and Settings for application setup, or Your First Game UI to configure aRendererandViewmanually.
Calling the Renderer Each Frame
Each frame, you call Renderer::Update(), Renderer::RefreshDisplay(), and Renderer::Render() in order.
RefPtr<Renderer> renderer;
void UpdateLogic() {
///
/// Give the library a chance to dispatch tasks, timers, and JavaScript
/// callbacks.
///
renderer->Update();
}
void RenderOneFrame() {
///
/// Notify the renderer that the display has refreshed (call this after
/// vsync).
///
renderer->RefreshDisplay(0);
///
/// Render all active Views (only the ones that need painting are repainted).
///
renderer->Render();
}
ULRenderer renderer;
void UpdateLogic(void) {
///
/// Give the library a chance to dispatch tasks, timers, and JavaScript
/// callbacks.
///
ulUpdate(renderer);
}
void RenderOneFrame(void) {
///
/// Notify the renderer that the display has refreshed (call this after
/// vsync).
///
ulRefreshDisplay(renderer, 0);
///
/// Render all active Views (only the ones that need painting are repainted).
///
ulRender(renderer);
}
Updating Timers and Callbacks
Call Renderer::Update() as often as possible from your run loop. It dispatches pending tasks, network requests, timers, and JavaScript callbacks.
Timers on the page fire only during this call. If your run loop pauses, timers wait until the next Renderer::Update() call rather than running in the background.
Advancing Animations
Call Renderer::RefreshDisplay() once per presented frame, usually right after vertical sync.
This call advances CSS animations, smooth scrolling, and window.requestAnimationFrame() callbacks for all views on that display. Those views then repaint during the next call to Renderer::Render().
🚧 Call RefreshDisplay Every Frame
Continue calling
Renderer::RefreshDisplay()every frame even whenView::needs_paint()returnsfalse. Animations request a repaint only from this call— skipping it causes CSS animations and smooth scrolling to stall.
Rendering Views
Call Renderer::Render() once per frame, after Renderer::RefreshDisplay().
The renderer repaints only the views that need it— an unchanged page costs nothing to render.
🚧 Call on the Renderer's Thread
You must make every
Viewcall and everyRenderercall on the Renderer's thread (the thread that created theRenderer), since views and the DOM aren't thread-safe.Renderer::PostTask()is the only exception— it is safe to call from any thread, and its task runs on the Renderer's thread during the nextRenderer::Update(). See Integrating into a Game Engine.
Displaying Rendered Output
Each View renders to either a CPU pixel buffer or a GPU texture, configured by ViewConfig::is_accelerated when created.
Displaying CPU Views
Views using the default CPU renderer draw into a pixel buffer accessed through View::surface()— see Render Surfaces for surface locking and custom memory.
After calling Renderer::Render(), inspect the surface dirty bounds and copy changed pixels to a texture:
void UploadViews() {
// Pseudo-code, loop through all active Views here.
for (auto view : view_list) {
///
/// Re-upload a View's pixels only if they changed.
///
Surface* surface = view->surface();
if (!surface->dirty_bounds().IsEmpty()) {
RefPtr<Bitmap> bitmap = static_cast<BitmapSurface*>(surface)->bitmap();
// Pseudo-code, upload the bitmap's pixels to a texture here.
CopyBitmapToTexture(bitmap);
surface->ClearDirtyBounds();
}
}
}
void UploadViews(void) {
// Pseudo-code, loop through all active Views here.
for (unsigned int i = 0; i < view_count; ++i) {
ULView view = view_list[i];
///
/// Re-upload a View's pixels only if they changed.
///
ULSurface surface = ulViewGetSurface(view);
if (!ulIntRectIsEmpty(ulSurfaceGetDirtyBounds(surface))) {
ULBitmap bitmap = ulBitmapSurfaceGetBitmap((ULBitmapSurface)surface);
// Pseudo-code, upload the bitmap's pixels to a texture here.
CopyBitmapToTexture(bitmap);
ulSurfaceClearDirtyBounds(surface);
}
}
}
Displaying GPU Views
When hardware acceleration is enabled, Renderer::Render() outputs drawing commands directly to the GPU driver, rendering each View to its own texture.
You display this texture using View::render_target(). See GPU Renderer Overview for driver setup.
Supporting Multiple Displays
Every View is assigned to a display through ViewConfig::display_id, which defaults to 0.
If your application uses one monitor, pass 0 to Renderer::RefreshDisplay(). When using multiple monitors, assign a unique ID to each display and call Renderer::RefreshDisplay() for each one as it refreshes.
When a View moves to another screen, call View::set_display_id() to update its association.
Timing Animations to the Screen
Engines that know when a frame reaches the screen or use their own timeline can synchronize animation timing explicitly.
Passing Presentation Timestamps
Engines that double-buffer or triple-buffer render frames ahead of presentation. Passing a target timestamp to Renderer::RefreshDisplay() aligns animation time with the moment the frame appears on screen rather than the current wall clock.
Timestamps must be in seconds using the system monotonic clock (on macOS, this is host time from a display link rather than std::chrono::steady_clock), and timestamps must never decrease.
Pass the target presentation time in seconds to Renderer::RefreshDisplay() before rendering:
void RenderOneFrame(double present_time) {
///
/// Time animations for the moment this frame reaches the screen (seconds,
/// eg the target time your display link or swap chain reports).
///
renderer->RefreshDisplay(0, present_time);
renderer->Render();
}
Declaring the Display Refresh Rate
Pages schedule window.requestAnimationFrame() callbacks based on the display refresh rate. You can declare this rate by calling Renderer::SetDisplayRefreshRate() with the nominal rate in Hz (such as 60.0, 59.94, or 120.0).
Call this method whenever the display mode changes, or pass 0 to let the library measure the rate from your Renderer::RefreshDisplay() calls.
Update the refresh rate when your application detects a display mode change:
void OnDisplayModeChanged(double refresh_rate) {
///
/// Declare the new rate (0 lets the library measure it instead).
///
renderer->SetDisplayRefreshRate(0, refresh_rate);
}
Using a Custom Clock
If your timestamps come from a custom timeline— such as simulation time in a game or offline video rendering— declare that the display uses a custom clock so animation follows your timeline directly.
Declare the custom clock before making your first timed refresh call:
///
/// Our timestamps are simulation time, not the system clock.
///
renderer->SetDisplayUsesCustomClock(0, true);
🚧 Declare Custom Clocks Before First Refresh
The first timed refresh anchors animation time, so declare a custom clock before that call. If no custom clock is declared and your first timestamp differs from the library clock by more than one second, the library logs a warning, starts animation time at the library clock, and advances by the differences between your timestamps.
Slowing Down or Pausing Views
To slow an on-screen View, call View::set_max_render_fps() to cap how fast it animates and repaints. Passing 0 removes the cap— the Free edition caps every View at 60 fps.
To pause a View, call View::set_visible(false). Renderer::Render() skips hidden views, and animations and window.requestAnimationFrame() callbacks pause. JavaScript timers continue running at their normal rate unless ViewConfig::enable_hidden_timer_throttling was set when creating the View, which slows repeating timers to roughly once a second. See Optimizing Performance for more on throttling and pausing views.