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](/docs/2.0/app-lifecycle-and-settings) for application setup, or [Your First Game UI](/docs/2.0/your-first-game-ui) to configure a `Renderer` and `View` manually.

## Calling the Renderer Each Frame

Each frame, you call `Renderer::Update()`, `Renderer::RefreshDisplay()`, and `Renderer::Render()` in order.

<!-- tabs:start -->
```cpp
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();
}
```
```c
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);
}
```
<!-- tabs:end -->

### 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 when `View::needs_paint()` returns `false`. 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 `View` call and every `Renderer` call on the Renderer's thread (the thread that created the `Renderer`), 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 next `Renderer::Update()`. See [Integrating into a Game Engine](/docs/2.0/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](/docs/2.0/using-a-custom-surface) for surface locking and custom memory.

After calling `Renderer::Render()`, inspect the surface dirty bounds and copy changed pixels to a texture:

<!-- tabs:start -->
```cpp
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();
    }
  }
}
```
```c
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);
    }
  }
}
```
<!-- tabs:end -->

### 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](/docs/2.0/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:

```cpp
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:

```cpp
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:

```cpp
///
/// 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](/docs/2.0/optimizing-performance) for more on throttling and pausing views.
