---
updatedAt: 2025-06-16T21:14:37.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 Surface

When rendering on the CPU, the library paints pixels directly into a [Surface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface.html) object.

## The Default BitmapSurface

A default Surface implementation, [BitmapSurface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_bitmap_surface.html), is automatically provided by the library when you call `Renderer::Create()` without defining a custom [SurfaceFactory](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface_factory.html).

After calling `Renderer::Render()` you should get View's underlying Surface, cast it to a [BitmapSurface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_bitmap_surface.html), retrieve the underlying [Bitmap](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_bitmap.html) and display the pixels in your application.

```cplusplus
///
/// Get the pixel-buffer Surface for a View.
///
Surface* surface = view->surface();

///
/// Cast it to a BitmapSurface.
///
BitmapSurface* bitmap_surface = (BitmapSurface*)surface;

///
/// Get the underlying bitmap.
///
RefPtr<Bitmap> bitmap = bitmap_surface->bitmap();

///
/// Use the bitmap here...
///
```

## Using a Custom Surface Implementation

The default [BitmapSurface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_bitmap_surface.html) is convenient but, for less overhead, you may want to provide your own [Surface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface.html) implementation so that the renderer can paint directly into a block of memory controlled by you.

To define a custom [Surface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface.html), you just need to inherit from `Surface` and handle the virtual member functions.

Here is an example that allows us to paint directly to GPU-controlled memory via OpenGL PBOs.

The pixels are lazily uploaded to an OpenGL texture when  `GLPBOTextureSurface::GetTextureAndSyncIfNeeded()` is called:

```cplusplus
///
/// Custom Surface implementation that allows Ultralight to paint directly
/// into an OpenGL PBO (pixel buffer object).
///
/// PBOs in OpenGL allow us to get a pointer to a block of GPU-controlled
/// memory for lower-latency uploads to a texture.
///
/// For more info: <http://www.songho.ca/opengl/gl_pbo.html>
///
class GLPBOTextureSurface : public Surface {
public:
  GLPBOTextureSurface(uint32_t width, uint32_t height) {
    Resize(width, height);
  }

  virtual ~GLPBOTextureSurface() {
    ///
    /// Destroy our PBO and texture.
    ///
    if (pbo_id_) {
      glDeleteBuffers(1, &pbo_id_);
      pbo_id_ = 0;
      glDeleteTextures(1, &texture_id_);
      texture_id_ = 0;
    }
  }

  virtual uint32_t width() const override { return width_; }

  virtual uint32_t height() const override { return height_; }

  virtual uint32_t row_bytes() const override { return row_bytes_; }

  virtual size_t size() const override { return size_; }

  virtual void* LockPixels() override { 
    ///
    /// Map our PBO to system memory so Ultralight can draw to it.
    ///
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_id_);
    void* result = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_READ_WRITE);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    return result;
  }

  virtual void UnlockPixels() override { 
    ///
    /// Unmap our PBO.
    ///
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_id_);
    glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); 
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
  }

  virtual void Resize(uint32_t width, uint32_t height) override {
    if (pbo_id_ && width_ == width && height_ == height)
      return;

    ///
    /// Destroy any existing PBO and texture.
    ///
    if (pbo_id_) {
      glDeleteBuffers(1, &pbo_id_);
      pbo_id_ = 0;
      glDeleteTextures(1, &texture_id_);
      texture_id_ = 0;
    }

    width_ = width;
    height_ = height;
    row_bytes_ = width_ * 4;
    size_ = row_bytes_ * height_;

    ///
    /// Create our PBO (pixel buffer object), with a size of 'size_'
    ///
    glGenBuffers(1, &pbo_id_);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_id_);
    glBufferData(GL_PIXEL_UNPACK_BUFFER, size_, 0, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    ///
    /// Create our Texture object.
    ///
    glGenTextures(1, &texture_id_);
    glBindTexture(GL_TEXTURE_2D, texture_id_);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glBindTexture(GL_TEXTURE_2D, 0);
  }

  virtual GLuint GetTextureAndSyncIfNeeded() {
    ///
    /// This is NOT called by Ultralight.
    ///
    /// This helper function is called when our application wants to draw this
    /// Surface to an OpenGL quad. (We return an OpenGL texture handle)
    ///
    /// We take this opportunity to upload the PBO to the texture if the
    /// pixels have changed since the last call (indicated by dirty_bounds()
    /// being non-empty)
    ///
    if (!dirty_bounds().IsEmpty()) {
      ///
      /// Update our Texture from our PBO (pixel buffer object)
      ///
      glBindTexture(GL_TEXTURE_2D, texture_id_);
      glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_id_);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_,
        0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
      glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
      glBindTexture(GL_TEXTURE_2D, 0);

      ///
      /// Clear our Surface's dirty bounds to indicate we've handled any
      /// pending modifications to our pixels.
      ///
      ClearDirtyBounds();
    }

    return texture_id_;
  }

protected:
  GLuint texture_id_;
  GLuint pbo_id_ = 0;
  uint32_t width_;
  uint32_t height_;
  uint32_t row_bytes_;
  uint32_t size_;
};
```

## Registering a Custom Surface

To allow the library to create/destroy your custom [Surface](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface.html) at will, you also need to define a corresponding [SurfaceFactory](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface_factory.html) for your new class:

```cplusplus
class GLTextureSurfaceFactory : public ultralight::SurfaceFactory {
public:
  GLTextureSurfaceFactory() {}

  virtual ~GLTextureSurfaceFactory() {}

  virtual ultralight::Surface* CreateSurface(uint32_t width, uint32_t height) override {
    ///
    /// Called by Ultralight when it wants to create a Surface.
    ///
    return new GLPBOTextureSurface(width, height);
  }

  virtual void DestroySurface(ultralight::Surface* surface) override {
    //
    /// Called by Ultralight when it wants to destroy a Surface.
    ///
    delete static_cast<GLPBOTextureSurface*>(surface);
  }
};
```

Finally, you should register this new [SurfaceFactory](https://ultralig.ht/api/cpp/1_4_0/classultralight_1_1_surface_factory.html) with `Platform::instance().set_surface_factory()` (***make sure to call this before creating the Renderer or any Views***):

```cplusplus
///
/// You should keep this instance alive for the duration of your program.
///
std::unique_ptr<GLTextureSurfaceFactory> factory(new GLTextureSurfaceFactory());

Platform::instance().set_surface_factory(factory.get());
```

## Using Your Custom Surface

The library will use `GLTextureSurfaceFactory` to create all Surfaces from this point forward.

We can now safely cast all Surfaces to our custom `GLPBOTextureSurface` and use its texture handle in our OpenGL app:

```cplusplus
///
/// Get the Surface for a View.
///
Surface* surface = view->surface();

///
/// Cast it to a GLPBOTextureSurface.
///
GLPBOTextureSurface* texture_surface = (GLPBOTextureSurface*)surface;

///
/// Get the underlying texture handle.
///
GLuint texture_id = texture_surface->GetTextureAndSyncIfNeeded();

///
/// Use the texture here...
///
```