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.

📘

Video Tutorial For DX11 Integration

Integrating Ultralight into a DX11 app? A member of our community graciously provided a complete video tutorial!

Watch it on YouTube Here

Virtual GPU Architecture

Ultralight was designed from the outset 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.

2600

GPUDriver API

The first step to using a custom GPUDriver is to subclass the GPUDriver interface.

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

All GPUDriver calls 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.

Enabling the GPU Renderer

You'll need to tell Config to enable the GPU renderer and pass your custom GPUDriver instance to Platform::instance().set_gpu_driver().

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

void Init() {
  Config config;
  config.use_gpu_renderer = true;
  config.device_scale = 1.0;
  
  Platform::instance().set_gpu_driver(custom_gpu_driver_instance);

  Ref<Renderer> renderer = Renderer::Create();
}

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_Fillv2f_c4f_t2f_t2f_d28f.hlslfill.hlsl
kShaderType_FillPathv2f_c4f_t2f.hlslfill_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:

  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::Render() once per frame.
  3. After calling Renderer::Render(), check if GPUDriver has any pending commands, and dispatch them by calling GPUDriver::DrawCommandList().
  4. Get the texture handle for each View and display it on an on-screen quad.
void UpdateLogic() {
  // Calling Update() allows the library to service resource callbacks,
  // JavaScript events, and other timers.
  renderer->Update();
}

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

  if (gpu_driver->HasCommandsPending())
    gpu_driver->DrawCommandList();
    
  // Do rest of your drawing here.
}

Binding the View Texture

Ultralight doesn't actually draw anything to the screen-- all Views are drawn to an offscreen render texture that you can display however you wish.

To get the texture for a View, you'll need to call View::render_target():

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.