---
updatedAt: 2025-06-17T12:54:24.000Z
---

Fetch the complete documentation index at: https://docs.ultralig.ht/llms.txt. Use this file to discover all available pages before exploring further.

# Using a Custom GPUDriver

Ultralight can emit raw GPU geometry / low-level draw calls to paint directly on the GPU without an intermediate CPU bitmap. We recommend this integration method for best performance when animating many paths / images.

## Renderer-Agnostic Architecture

Ultralight is designed to be renderer-agnostic. All draw calls are emitted via the GPUDriver interface and expected to be translated into various platform-specific GPU technologies (D3D, Metal, OpenGL, etc.).

This approach allows Ultralight to be integrated directly with the native renderer of your game.

![GPUDriver Diagram](https://files.readme.io/95028a4-using-custom-gpudriver.png)

## GPUDriver API

The first step to using a custom [GPUDriver](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_g_p_u_driver.html) is to subclass the `GPUDriver` interface and override all the virtual functions.

You'll need to handle tasks like creating a texture, creating vertex/index buffers, and binding shaders.

All calls to the `GPUDriver` interface are dispatched during `Renderer::Render()` but drawing is not performed immediately-- Ultralight queues drawing commands via `GPUDriver::UpdateCommandList()` and expects you to dispatch these yourself.

* For porting to **Direct3D 11**, see: [AppCore/src/win/d3d11](https://github.com/ultralight-ux/AppCore/tree/master/src/win/d3d11)
* For porting to **Direct3D 12**, see: [AppCore/src/win/d3d12](https://github.com/ultralight-ux/AppCore/tree/master/src/win/d3d12)
* For porting to **OpenGL**, see: [AppCore/src/linux/gl](https://github.com/ultralight-ux/AppCore/tree/master/src/linux/gl)
* For porting to **Metal**, see [AppCore/src/mac/metal](https://github.com/ultralight-ux/AppCore/tree/master/src/mac/metal)

## Using the GPU Renderer

### Set up Platform Singleton

You should notify the library of your custom [GPUDriver](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_g_p_u_driver.html) implementation by calling `Platform::instance().set_gpu_driver()` when setting up the [Platform](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_platform.html) singleton.

```cplusplus
#include <Ultralight/Ultralight.h>
  
using namespace ultralight;

void InitRenderer() {
  // Pseudo-code to create our custom Platform implementations
  MyGPUDriver* gpu_driver = new MyGPUDriver();
  MyFileSystem* file_system = new MyFileSystem();
  MyFontLoader* font_loader = new MyFontLoader();
  
  // Notify the Platform singleton of our implementations
  Platform::instance().set_gpu_driver(gpu_driver);
  Platform::instance().set_file_system(file_system);
  Platform::instance().set_font_loader(font_loader);

  // We can now create the Renderer singleton
  Ref<Renderer> renderer = Renderer::Create();
}
```

### Enable GPU-Acceleration on a View

Views are unaccelerated by default (ie, they use the CPU renderer and paint to a [Surface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface.html)).

You should set [`ViewConfig::is_accelerated`](https://ultralig.ht/api/cpp/1_4_0/structultralight_1_1_view_config.html) to `true` when creating a [View](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_view.html) to have it use the GPU renderer and paint to a [RenderTarget](https://ultralig.ht/api/cpp/1_4_0/structultralight_1_1_render_target.html) instead.

```cplusplus
RefPtr<View> view;

void CreateView() {
  ///
  /// Create our ViewConfig with GPU acceleration enabled.
  ///
  ViewConfig view_config;
  view_config.is_accelerated = true;
  
  ///
  /// Create an HTML view, 500 by 500 pixels large.
  ///
  view = renderer->CreateView(500, 500, view_config, nullptr);
  
  ///
  /// Load a raw string of HTML asynchronously into the View.
  ///
  view->LoadHTML("<h1>Hello World!</h1>");
}
```

## Shader Programs

Ultralight relies on vertex and pixel shaders for CSS transforms and to draw things like borders, rounded rectangles, shadows, and gradients.

Right now we have only two shader types: `kShaderType_Fill` and `kShaderType_FillPath`.

Both use the same uniforms but have different vertex types.

Here are the reference implementations for Direct3D (HLSL):

|                            | Vertex Shader (HLSL)                                                                                                            | Pixel Shader (HLSL)                                                                                    |
| :------------------------- | :------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------- |
| **`kShaderType_Fill`**     | [v2f\_c4f\_t2f\_t2f\_d28f.hlsl](https://github.com/ultralight-ux/AppCore/blob/master/shaders/hlsl/vs/v2f_c4f_t2f_t2f_d28f.hlsl) | [fill.hlsl](https://github.com/ultralight-ux/AppCore/blob/master/shaders/hlsl/ps/fill.hlsl)            |
| **`kShaderType_FillPath`** | [v2f\_c4f\_t2f.hlsl](https://github.com/ultralight-ux/AppCore/blob/master/shaders/hlsl/vs/v2f_c4f_t2f.hlsl)                     | [fill\_path.hlsl](https://github.com/ultralight-ux/AppCore/blob/master/shaders/hlsl/ps/fill_path.hlsl) |

If your engine uses a custom shader language, you'll need to port these files over.

## Blending Modes

Ultralight uses a custom blend mode-- you'll need to use the same blending functions in your engine to get color-accurate results when dispatching DrawGeometry commands with blending enabled.

For reference, here is the render target blend description for the D3D11 driver:

```cpp
  D3D11_RENDER_TARGET_BLEND_DESC rt_blend_desc;
  ZeroMemory(&rt_blend_desc, sizeof(rt_blend_desc));
  rt_blend_desc.BlendEnable = true;
  rt_blend_desc.SrcBlend = D3D11_BLEND_ONE;
  rt_blend_desc.DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
  rt_blend_desc.BlendOp = D3D11_BLEND_OP_ADD;
  rt_blend_desc.SrcBlendAlpha = D3D11_BLEND_INV_DEST_ALPHA;
  rt_blend_desc.DestBlendAlpha = D3D11_BLEND_ONE;
  rt_blend_desc.BlendOpAlpha = D3D11_BLEND_OP_ADD;
  rt_blend_desc.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
```

## Render Loop Integration

Within your application's main run loop, you should:

1. Call `Renderer::Update()` as often as possible
2. Call `Renderer::RefreshDisplay(0)` and `Renderer::Render()` once per frame.
   1. The renderer will call into the `GPUDriver` interface during this time synchronize textures, render buffers, geometry, and command lists.
3. After calling `Renderer::Render()`, you should consume any pending command lists (all drawing is performed via command lists).
4. Get the texture handle for each `View` and display it on an on-screen quad.

```cplusplus
void UpdateLogic() {
  // Calling Update() allows the library to service resource callbacks,
  // JavaScript events, and other timers.
  renderer->Update();
}

void RenderFrame() {
  renderer->RefreshDisplay(0);
  renderer->Render();

  // Pseudo-code, consume any pending command-lists in your GPUDriver here.
  gpu_driver->DrawCommandListIfNeeded();
    
  // Pseudo-code, draw any View textures to on-screen quads here.
  DrawViewQuadsToScreen();
}
```

## Getting the View Texture

Ultralight doesn't draw anything to the backbuffer-- all Views are drawn to an offscreen [RenderTarget](https://ultralig.ht/api/cpp/1_4_0/structultralight_1_1_render_target.html) that you can display how you wish.

Just call `View::render_target()` to get the render target for a [View](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_view.html).

```cplusplus
RenderTarget rtt_info = view->render_target();

// Get the Ultralight texture ID, use this with GPUDriver::BindTexture()
uint32_t tex_id = rtt_info.texture_id;

// Textures may have extra padding-- to compensate for this you'll need
// to adjust your UV coordinates when mapping onto geometry.
Rect uv_coords = rtt_info.uv_coords;
```

Once you have this texture you can display it on-screen as a quad or projected onto some other geometry in-game.