You can render web content through your graphics pipeline by implementing the `GPUDriver` interface. This lets you receive texture updates, geometry buffers, and draw calls directly from the renderer.

To register a driver and display a view, see [GPU Renderer Overview](/docs/2.0/gpu-renderer-overview). Reference implementations for major graphics APIs are available in the SDK **platform** folder.

## Implementing the Interface

To define a custom GPU driver, inherit from `GPUDriver` and implement its pure virtual methods:

```cpp
#include <Ultralight/Ultralight.h>
#include <vector>

using namespace ultralight;

class MyGPUDriver : public GPUDriver {
 public:
  void BeginSynchronize() override {}
  void EndSynchronize() override {}

  uint32_t NextTextureId() override { return next_texture_id_++; }
  uint32_t NextRenderBufferId() override { return next_render_buffer_id_++; }
  uint32_t NextGeometryId() override { return next_geometry_id_++; }

  void CreateTexture(uint32_t texture_id, RefPtr<Bitmap> bitmap,
                     uint32_t flags) override;
  void UpdateTexture(uint32_t texture_id, RefPtr<Bitmap> bitmap,
                     const IntRect& dirty_rect) override;
  void DestroyTexture(uint32_t texture_id) override;

  void CreateRenderBuffer(uint32_t render_buffer_id,
                          const RenderBuffer& buffer) override;
  void DestroyRenderBuffer(uint32_t render_buffer_id) override;

  void CreateGeometry(uint32_t geometry_id, const VertexBuffer& vertices,
                      const IndexBuffer& indices) override;
  void UpdateGeometry(uint32_t geometry_id, const VertexBuffer& vertices,
                      const IndexBuffer& indices) override;
  void DestroyGeometry(uint32_t geometry_id) override;

  void UpdateCommandList(const CommandList& list) override;

  // Not part of the interface. Call this after Renderer::Render().
  void DrawCommandList();

 private:
  void ApplyState(const GPUState& state);

  uint32_t next_texture_id_ = 1;
  uint32_t next_render_buffer_id_ = 1;
  uint32_t next_geometry_id_ = 1;
  std::vector<Command> pending_;
};
```

You must implement all 14 pure virtual methods. Overriding `GetDeviceCaps()` is optional— use it to report features like multisampling and compressed textures (see [GPU Device Capabilities](/docs/2.0/gpu-device-capabilities)).

If you're updating a driver written for 1.4, see [Porting from 1.4 to 2.0](/docs/2.0/migrating-from-1-4-to-2-0).

## Managing IDs and Synchronization

`NextTextureId()`, `NextRenderBufferId()`, and `NextGeometryId()` must each return a unique non-zero ID (0 represents no resource). The driver tracks these values and maps each ID to a native handle.

You can also reserve a texture ID by calling `NextTextureId()` directly (see [Displaying Custom Textures](/docs/2.0/displaying-custom-textures)). That ID can appear in a draw command's texture slot without a matching `CreateTexture()` call— the driver binds your own texture for it.

The library makes all create, update, and destroy calls inside `Renderer::Render()` on the Renderer's thread. Every call occurs between `BeginSynchronize()` and `EndSynchronize()`— use these callbacks to open and close an upload batch on the GPU.

## Managing Textures

### Creating Textures

When the library allocates a texture, it calls `CreateTexture()` with a unique ID, a `Bitmap` object, and a bitmask of `GPUTextureFlags`.

| Flag | Meaning |
| :--- | :--- |
| `kGPUTextureFlag_RenderTarget` | Backs a render buffer. The bitmap contains dimensions and a format but no pixel rows— create a texture that can be rendered into and sampled from. |
| `kGPUTextureFlag_Antialiased` | Requires a multisampled render target that resolves to a single-sample texture when sampled. The library passes this flag only when your driver reports MSAA support and analytic rendering is off (never under the default `Config::enable_photon = true`— see [GPU Device Capabilities](/docs/2.0/gpu-device-capabilities)). |
| `kGPUTextureFlag_Immutable` | The texture is uploaded once and never updated. You can use static GPU storage and avoid CPU-writable buffers. |

### Texture Formats

The `BitmapFormat` enum specifies the pixel layout for each allocated texture.

| Format | Native Texture |
| :--- | :--- |
| `BitmapFormat::BGRA8_UNORM_SRGB` | Standard 8-bit BGRA UNORM (eg, `DXGI_FORMAT_B8G8R8A8_UNORM`), never a hardware sRGB format. The library blends stored sRGB values directly— a hardware sRGB format causes the GPU to decode values on sampling, which alters colors. |
| `BitmapFormat::A8_UNORM` | Single-channel 8-bit format. Read this as alpha when sampled. When created as a render target, back it with a renderable single-channel format (R8) with no alpha swizzle. |
| `BitmapFormat::RG8_UNORM` | Two 8-bit unsigned normalized channels. |
| `BitmapFormat::RGBA16F`, `BitmapFormat::RGBA16UI`, `BitmapFormat::RGBA32F` | Data pages for analytic shader programs. Create these textures without filtering or mipmaps— see [GPU Shader Programs](/docs/2.0/gpu-shader-programs). |
| `BitmapFormat::BC1_UNORM`, `BC2_UNORM`, `BC3_UNORM`, `BC7_UNORM` | Block-compressed color formats. The library passes these only when your driver reports that family in `GetDeviceCaps()`— see [Compressed Textures](/docs/2.0/compressed-textures). |

### Updating Textures

When pixel content changes, the library calls `UpdateTexture()` with the full bitmap surface and a `dirty_rect` bounding the modified pixels. This rectangle is never empty and serves as a hint— your driver may upload only the dirty sub-region or re-upload the entire bitmap. The library never calls `UpdateTexture()` on render targets.

> 🚧 Copy Temporary Buffer Data
>
> Data pointers passed to `CreateTexture()`, `UpdateTexture()`, `CreateGeometry()`, `UpdateGeometry()`, and `UpdateCommandList()` are valid only until the call returns. If you stage or upload data asynchronously on another thread, make a deep copy before returning.

Implement `CreateTexture()` and `UpdateTexture()` to allocate native textures and upload pixel data:

```cpp
void MyGPUDriver::CreateTexture(uint32_t texture_id, RefPtr<Bitmap> bitmap,
                                uint32_t flags) {
  if (flags & kGPUTextureFlag_RenderTarget) {
    ///
    /// The bitmap holds dimensions and format only. Create a texture we can
    /// both render into and sample from (multisampled if
    /// kGPUTextureFlag_Antialiased is set).
    ///
    // Pseudo-code, create a bitmap->width() x bitmap->height() render target
    // here.
    return;
  }

  ///
  /// Upload the pixels (copy them first if the upload is asynchronous).
  ///
  auto pixels = bitmap->LockPixelsSafe();
  // Pseudo-code, upload pixels.data() (pixels.size() bytes,
  // bitmap->row_bytes() per row) here.
}

void MyGPUDriver::UpdateTexture(uint32_t texture_id, RefPtr<Bitmap> bitmap,
                                const IntRect& dirty_rect) {
  ///
  /// The bitmap is always the whole surface. Upload just dirty_rect, or all
  /// of it.
  ///
  auto pixels = bitmap->LockPixelsSafe();
  // Pseudo-code, upload the changed rows here.
}
```

## Creating Render Buffers and Geometry

To configure render targets and vertex geometry, implement `CreateRenderBuffer()` and `CreateGeometry()`:

```cpp
void MyGPUDriver::CreateRenderBuffer(uint32_t render_buffer_id,
                                     const RenderBuffer& buffer) {
  ///
  /// buffer.texture_id is a texture created with kGPUTextureFlag_RenderTarget.
  ///
  // Pseudo-code, create a framebuffer / render-target view over that
  // texture here.
}

void MyGPUDriver::CreateGeometry(uint32_t geometry_id,
                                 const VertexBuffer& vertices,
                                 const IndexBuffer& indices) {
  ///
  /// vertices.format picks the vertex struct; indices.data is an array of
  /// IndexType.
  ///
  // Pseudo-code, create a vertex buffer of vertices.size bytes and an index
  // buffer of indices.size bytes here (copy both if the upload is
  // asynchronous).
}
```

`CreateRenderBuffer()` wraps an existing texture created with `kGPUTextureFlag_RenderTarget`. Use `RenderBuffer::texture_id` to locate the backing texture, then wrap it in a native framebuffer or render-target view.

`CreateGeometry()` and `UpdateGeometry()` manage vertex and index data for drawing operations. `VertexBuffer::format` selects one of three vertex layouts— see [GPU Shader Programs](/docs/2.0/gpu-shader-programs) for the layout structures. The index buffer contains an array of 32-bit `IndexType` values, and both buffers provide their data size in bytes.

## Executing the Command List

Store drawing commands during `UpdateCommandList()` and replay them after `Renderer::Render()` returns:

```cpp
void MyGPUDriver::UpdateCommandList(const CommandList& list) {
  ///
  /// The list does not outlive this call, so keep our own copy.
  ///
  pending_.assign(list.commands, list.commands + list.size);
}

void MyGPUDriver::DrawCommandList() {
  for (const Command& cmd : pending_) {
    switch (cmd.command_type) {
      case CommandType::ClearRenderBuffer:
        // Pseudo-code, clear cmd.gpu_state.render_buffer_id to transparent
        // black here.
        break;
      case CommandType::DrawGeometry:
        ApplyState(cmd.gpu_state);
        // Pseudo-code, draw cmd.indices_count indices of cmd.geometry_id,
        // starting at cmd.indices_offset, here.
        break;
      case CommandType::Flush:
        // Pseudo-code, submit pending GPU work here (Direct3D 11 / OpenGL
        // only).
        break;
    }
  }
  pending_.clear();
}
```

The library sends one `CommandList` for each frame that paints, and sends nothing on idle frames. Copy the command array during `UpdateCommandList()` because the data does not outlive the call. Execute the saved commands after `Renderer::Render()` completes, before starting the next frame.

| Command | What to Do |
| :--- | :--- |
| `CommandType::ClearRenderBuffer` | Clear the buffer identified by `gpu_state.render_buffer_id` to transparent black (`#00000000`). Ignore all other fields in `gpu_state`. |
| `CommandType::DrawGeometry` | Apply `gpu_state` to the pipeline, then draw `indices_count` indices from `geometry_id` starting at `indices_offset`. |
| `CommandType::Flush` | Submit pending GPU work before executing the next command. The library emits this before rendering into a texture that earlier draws in the list sampled. Direct3D 12, Metal, and Vulkan drivers generally treat this as a no-op if resource barriers handle synchronization, but Direct3D 11 and OpenGL drivers must submit pending work (`Flush()` or `glFlush()`) to prevent rendering dropouts. |

## Applying GPU State

`GPUState` holds the complete pipeline configuration for a `CommandType::DrawGeometry` command. In the example above, `ApplyState()` sets up the GPU in the stages described below.

### Setting the Render Target, Viewport, and Scissor

Bind the native framebuffer for `render_buffer_id`, then set the viewport to `viewport_width` by `viewport_height` pixels.

When `enable_scissor` is true, enable scissor testing and clip to `scissor_rect` (in pixels)— otherwise, disable scissor testing.

### Projecting the Transform

Multiply `transform` by an orthographic projection using `Matrix::SetOrthographicProjection()` before passing the matrix to the vertex shader:

```cpp
///
/// Combine the draw's transform with a screen-space orthographic projection
/// (pass flip_y = true on OpenGL).
///
Matrix4x4 ProjectedTransform(const GPUState& state, bool flip_y) {
  Matrix projection;
  projection.SetOrthographicProjection(state.viewport_width,
                                       state.viewport_height, flip_y);
  Matrix transform;
  transform.Set(state.transform);
  projection.Transform(transform);
  return projection.GetMatrix4x4();
}
```

Pass `flip_y = true` on OpenGL drivers, since the library draws into offscreen render targets.

### Binding Shaders, Textures, and Constants

`shader_type` identifies the vertex and pixel shader pair to bind for the draw call.

Bind `texture_1_id` through `texture_4_id` to texture slots 1 through 4 (leave a slot unbound if its ID is 0). Slot 4 appears only during analytic shader draws and requires an integer texture view.

Combine the uniform arrays, the clip stack, and the projected transform into a single constant buffer. For buffer layouts and register assignments, see [GPU Shader Programs](/docs/2.0/gpu-shader-programs).

### Configuring Blending

When `enable_blend` is false, disable blending so incoming pixels overwrite the target.

When `enable_blend` is true, configure the blend state using `blend_src_factor`, `blend_dst_factor`, and `blend_equation`. Apply those same factors to the alpha channel— since Direct3D rejects color factors on alpha channels, pass the alpha equivalent instead (eg, `InvDestAlpha` for `InvDestColor`).
