`ULGPUDriver` exposes the GPU driver interface to C applications, letting you render accelerated Views through your own graphics pipeline. Each pure virtual method from C++ corresponds to a function pointer field in the `ULGPUDriver` struct.

Before implementing a driver in C, read [GPU Renderer Overview](/docs/2.0/gpu-renderer-overview) and [Implementing a GPUDriver](/docs/2.0/implementing-a-gpudriver) to understand the rendering architecture and pipeline requirements. This page focuses on what differs in C. For the memory and threading rules shared by all C headers, see [C API Conventions](/docs/2.0/c-api-conventions). For registering other platform interfaces, see [Platform Handlers in C](/docs/2.0/c-platform-handlers).

## Differences from C++

The C GPU driver replaces C++ virtual methods with a struct of function pointers and manages state outside class instances.

| C++ Feature | C API Equivalent | What Changes |
| :--- | :--- | :--- |
| Subclass `GPUDriver` and call `Platform::instance().set_gpu_driver()` | Populate `ULGPUDriver` and call `ulPlatformSetGPUDriver()` | Pass the 15 function pointers by value in a struct that the library copies. |
| Store driver state in member variables | Callbacks have no `user_data` pointer | Keep driver state at file scope instead. |
| Override `GetDeviceCaps()` optionally | `get_device_caps` can be `NULL` | You must implement the other 14 callbacks. |
| `GPUState` and `Command` use enum types | Fields are stored as `unsigned char` | Cast fields to their corresponding `UL` enum type before switching on them. |
| Call `Matrix::SetOrthographicProjection()` then `Transform()` | Call `ulApplyProjection()` | Multiplies the transform by the orthographic projection in a single call. |
| `kGPUCompressedFormat_BC7` and `GetBitmapFormatInfo()` | `kULGPUCompressedFormat_BC7` and `ulGetBitmapFormatInfo()` | Format flags and block geometry queries share the same meaning under C names. |

## Installing the Driver

Populate a `ULGPUDriver` struct, register it before calling `ulCreateRenderer()`, and enable acceleration on each View you create:

```c
#include <Ultralight/CAPI.h>

static ULRenderer renderer;
static ULView view;

void InstallGPUDriver(void) {
  ULGPUDriver driver = {
      .begin_synchronize = OnBeginSynchronize,
      .end_synchronize = OnEndSynchronize,
      .next_texture_id = OnNextTextureId,
      .create_texture = OnCreateTexture,
      .update_texture = OnUpdateTexture,
      .destroy_texture = OnDestroyTexture,
      .next_render_buffer_id = OnNextRenderBufferId,
      .create_render_buffer = OnCreateRenderBuffer,
      .destroy_render_buffer = OnDestroyRenderBuffer,
      .next_geometry_id = OnNextGeometryId,
      .create_geometry = OnCreateGeometry,
      .update_geometry = OnUpdateGeometry,
      .destroy_geometry = OnDestroyGeometry,
      .update_command_list = OnUpdateCommandList,
      .get_device_caps = OnGetDeviceCaps,  // optional, can stay NULL
  };
  ulPlatformSetGPUDriver(driver);
}

void CreateAcceleratedView(void) {
  ULConfig config = ulCreateConfig();
  renderer = ulCreateRenderer(config);
  ulDestroyConfig(config);

  ULViewConfig view_config = ulCreateViewConfig();
  ulViewConfigSetIsAccelerated(view_config, true);
  view = ulCreateView(renderer, 1280, 720, view_config, NULL);
  ulDestroyViewConfig(view_config);
}
```

Register your driver once before creating the renderer. Like all platform handlers, your callbacks and any file-scope state they access must remain valid until you destroy the renderer— see [Platform Handlers in C](/docs/2.0/c-platform-handlers).

> 📘 Reference Drivers Use C++
>
> The reference drivers in the SDK **platform** folder (Direct3D 11, Direct3D 12, and OpenGL in C++, and Metal in Objective-C++) have no plain C versions. You can port the driver for your graphics API to C, or compile it in C++ and register it from C++ code— see [GPU Renderer Overview](/docs/2.0/gpu-renderer-overview#content-registering-the-driver).

## Rendering Each Frame

On each frame, update the renderer, execute your saved drawing commands, and draw the View's render target texture:

```c
void RenderOneFrame(void) {
  ulUpdate(renderer);
  ulRefreshDisplay(renderer, 0);
  ulRender(renderer);
  DrawCommandList();  // our function (see Replaying Commands)

  ULRenderTarget target = ulViewGetRenderTarget(view);
  if (!target.is_empty) {
    // Pseudo-code, draw target.texture_id as a quad mapped with
    // target.uv_coords here.
  }
}
```

`ulViewGetRenderTarget()` returns a `ULRenderTarget` struct by value, so you don't need to destroy it.

Read the render target on every frame rather than caching its texture ID or dimensions, since both can change between frames. When drawing the quad, map its texture coordinates with `uv_coords` to account for padding— see [GPU Renderer Overview](/docs/2.0/gpu-renderer-overview#content-drawing-the-render-target).

## Managing Textures and Geometry

Resource callbacks receive texture and geometry data borrowed for the duration of the call, so you must read or copy it before returning.

### Creating Textures

Implement `create_texture` to inspect the incoming bitmap and allocate GPU storage:

```c
static void OnCreateTexture(unsigned int texture_id, ULBitmap bitmap,
                            unsigned int flags) {
  (void)texture_id;
  if (flags & kULGPUTextureFlag_RenderTarget) {
    // Pseudo-code, create a ulBitmapGetWidth(bitmap) x
    // ulBitmapGetHeight(bitmap) render target here (no pixels to upload).
    return;
  }

  void* pixels = ulBitmapLockPixels(bitmap);
  if (ulBitmapIsCompressed(bitmap)) {
    // Pseudo-code, upload ulBitmapGetSize(bitmap) bytes of blocks,
    // ulBitmapGetRowBytes(bitmap) per row of blocks, in the plain UNORM
    // format for ulBitmapGetFormat(bitmap) here.
  } else {
    // Pseudo-code, upload pixels, ulBitmapGetRowBytes(bitmap) per row,
    // here.
  }
  (void)pixels;
  ulBitmapUnlockPixels(bitmap);
}
```

When `flags` contains `kULGPUTextureFlag_RenderTarget`, the bitmap provides dimensions and a format but contains no pixel data. Check this flag first and allocate the render target without locking pixels— see [Implementing a GPUDriver](/docs/2.0/implementing-a-gpudriver#content-managing-textures) for available texture flags and formats.

### Uploading Texture Data

Keep these rules in mind when handling bitmap data:

- **Match every lock with an unlock.** Every call to `ulBitmapLockPixels()` requires a matching `ulBitmapUnlockPixels()` call on all code paths before returning.
- **Never destroy borrowed bitmaps.** The `ULBitmap` handle passed to `create_texture` and `update_texture` is borrowed— never call `ulDestroyBitmap()` on it.
- **Copy data for asynchronous uploads.** Copy the locked bytes to a staging buffer or call `ulCreateBitmapFromCopy()`. Destroy an owned copy created this way with `ulDestroyBitmap()` once the upload finishes.

### Handling Geometry and Render Buffers

In `create_geometry` and `update_geometry`, the `ULVertexBuffer` and `ULIndexBuffer` structs arrive by value, but their `data` pointers are borrowed for the call. If you upload vertex or index data asynchronously, copy the bytes before the callback returns.

In contrast, `create_render_buffer` receives a `ULRenderBuffer` struct that holds only scalar values— a texture ID, dimensions, and flags. It contains no data pointers, so there is nothing to copy or free.

## Managing the Command List

The library batches all drawing operations into command lists dispatched during `ulRender()`.

### Saving the Command List

The `update_command_list` callback receives drawing commands during `ulRender()`, which you must copy before returning:

```c
#include <Ultralight/CAPI.h>
#include <stdlib.h>
#include <string.h>

static ULCommand* pending = NULL;
static unsigned int pending_count = 0;

static void OnUpdateCommandList(ULCommandList list) {
  pending_count = 0;
  if (list.size == 0)
    return;

  ULCommand* grown = realloc(pending, list.size * sizeof(ULCommand));
  if (!grown)
    return;
  pending = grown;
  memcpy(pending, list.commands, list.size * sizeof(ULCommand));
  pending_count = list.size;
}
```

`list.commands` points to internal memory that the library overwrites on subsequent calls.

Execute the saved commands after `ulRender()` completes, before starting the next frame.

### Replaying Commands

To execute your saved commands, cast each byte-sized field to its enum type before switching on it:

```c
static void ApplyState(const ULGPUState* state) {
  ULShaderType shader = (ULShaderType)state->shader_type;
  ULBlendFactor src = (ULBlendFactor)state->blend_src_factor;
  ULBlendFactor dst = (ULBlendFactor)state->blend_dst_factor;
  ULBlendEquation equation = (ULBlendEquation)state->blend_equation;
  // Pseudo-code, bind state->render_buffer_id, the viewport, the shader
  // pair, texture slots 1-4, the constant block (see below), the blend
  // state, and the scissor here.
  (void)shader;
  (void)src;
  (void)dst;
  (void)equation;
}

void DrawCommandList(void) {
  for (unsigned int i = 0; i < pending_count; i++) {
    const ULCommand* cmd = &pending[i];
    switch ((ULCommandType)cmd->command_type) {
      case kCommandType_ClearRenderBuffer:
        // Pseudo-code, clear cmd->gpu_state.render_buffer_id here (the
        // rest of gpu_state doesn't apply).
        break;
      case kCommandType_DrawGeometry:
        ApplyState(&cmd->gpu_state);
        // Pseudo-code, draw cmd->indices_count indices of
        // cmd->geometry_id from cmd->indices_offset here.
        break;
      case kCommandType_Flush:
        // Pseudo-code, submit pending GPU work here (Direct3D 11 and
        // OpenGL).
        break;
    }
  }
  pending_count = 0;
}
```

For what each command and state field requires, see [Implementing a GPUDriver](/docs/2.0/implementing-a-gpudriver#content-applying-gpu-state).

### Building the Constant Buffer

Stock shaders read an 800-byte constant block matching the C++ `Uniforms` struct from [GPU Shader Programs](/docs/2.0/gpu-shader-programs), which you populate from `ULGPUState`:

```c
#include <Ultralight/CAPI.h>
#include <string.h>

typedef struct {
  float state[4];         // 0, viewport width, viewport height, 1
  ULMatrix4x4 transform;  // the projected transform
  int integer[8];
  float scalar[8];
  ULvec4 vector[8];
  int clip_size[4];       // x = clip_size, the rest 0
  ULMatrix4x4 clip[8];
} Uniforms;

_Static_assert(sizeof(Uniforms) == 800, "the stock shaders read 800 bytes");

void FillUniforms(Uniforms* u, const ULGPUState* state, bool flip_y) {
  float width = (float)state->viewport_width;
  float height = (float)state->viewport_height;

  u->state[0] = 0.0f;
  u->state[1] = width;
  u->state[2] = height;
  u->state[3] = 1.0f;
  u->transform = ulApplyProjection(state->transform, width, height, flip_y);
  memcpy(u->integer, state->uniform_integer, sizeof(u->integer));
  memcpy(u->scalar, state->uniform_scalar, sizeof(u->scalar));
  memcpy(u->vector, state->uniform_vector, sizeof(u->vector));
  u->clip_size[0] = state->clip_size;
  u->clip_size[1] = u->clip_size[2] = u->clip_size[3] = 0;
  memcpy(u->clip, state->clip, sizeof(u->clip));
}
```

`ulApplyProjection()` multiplies `state->transform` by an orthographic projection matrix for the current viewport dimensions. Pass `flip_y = true` when targeting OpenGL.

> 🚧 Copy Constant Fields Individually
>
> `ULGPUState` is not laid out like the constant block. Its enum fields and `clip_size` are single `unsigned char` bytes, and the C struct uses natural alignment instead of the packed C++ layout. Never `memcpy` the entire state struct into the constant buffer— copy each field individually as shown above.

## Reporting Device Capabilities

The `get_device_caps` callback receives a pointer to a zero-initialized `ULGPUDeviceCaps` struct where you enable the features your graphics hardware supports:

```c
static void OnGetDeviceCaps(ULGPUDeviceCaps* caps) {
  caps->compressed_formats =
      kULGPUCompressedFormat_BC1_BC3 | kULGPUCompressedFormat_BC7;
  caps->max_texture_dimension = 16384;
}
```

Setting `get_device_caps` to `NULL` reports no optional capabilities and leaves all limits at their library defaults. For field definitions and default limits, see [GPU Device Capabilities](/docs/2.0/gpu-device-capabilities).

Compressed texture formats require the Pro edition or higher— see [Compressed Textures](/docs/2.0/compressed-textures).

## Displaying Custom Textures

You can display engine textures on a page by calling your own `next_texture_id` function to reserve an identifier. Because the C API does not provide a getter for the installed driver, you must call your function directly rather than querying the platform.

Pass the reserved texture ID to `ulCreateImageSourceFromTexture()`. When a draw command in the command list references that ID, bind your native texture in your driver's draw handler.

For image source operations in C, see [Views in C](/docs/2.0/c-views). To learn more about custom texture workflows, see [Displaying Custom Textures](/docs/2.0/displaying-custom-textures).
