docs

Implementing a GPUDriver

Implement the GPUDriver interface to render views with a native graphics API.

On this page

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. 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:

C++
#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).

If you're updating a driver written for 1.4, see Porting 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). 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).
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.
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.

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:

C++
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():

C++
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 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:

C++
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:

C++
///
/// 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.

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).