A `Surface` is the pixel buffer the CPU renderer draws a View into, acting as its backbuffer.

By default, the library provides a bitmap surface— you read its pixels after each frame to display them.

You can also supply a custom surface so the library draws directly into memory it controls (such as a mapped GPU buffer, an OS framebuffer, or shared memory). Drawing straight into that memory avoids an intermediate pixel copy before displaying the frame.

> 📘 Custom Surfaces Require Renderer::Create()
>
> AppCore manages surfaces automatically when you use `App::Create()`. To provide a custom `Surface` or `SurfaceFactory`, create the renderer using `Renderer::Create()` instead.

## Using the Default Surface

Without a custom factory, each CPU View automatically receives a `BitmapSurface` backed by a `Bitmap`.

### Reading View Pixels

After calling `Renderer::Render()`, cast `View::surface()` to `BitmapSurface` to retrieve the underlying `Bitmap`.

```cpp
///
/// 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...
///
```

You can lock and read the bitmap as described in [Working with Bitmaps](/docs/2.0/working-with-bitmaps).

Every surface type also provides `Surface::LockPixelsSafe()`, which returns an RAII guard that unlocks the pixel buffer when it goes out of scope.

### Tracking Dirty Bounds

`Surface::dirty_bounds()` returns the pixel rectangle painted since the last call to `Surface::ClearDirtyBounds()`.

If the dirty bounds are empty, you can skip updating the display. Dirty bounds accumulate until cleared— skipping an update preserves changes for the next frame.

Call `Surface::ClearDirtyBounds()` after copying the updated pixels to the display.

#### Copying Individual Dirty Rectangles

To transfer fewer pixels when separate regions of the page change, copy each dirty rectangle individually.

```cpp
void UploadDirtyRects(Surface* surface) {
  ///
  /// Copy each changed rectangle instead of their union to move fewer pixels.
  ///
  for (uint32_t i = 0; i < surface->dirty_rect_count(); ++i) {
    IntRect rect = surface->dirty_rect(i);

    // Pseudo-code, copy this rectangle to your destination here.
    CopyRectToTexture(surface, rect);
  }

  surface->ClearDirtyBounds();
}
```

## Implementing a Custom Surface

To draw directly into native memory, inherit from `Surface` and implement its virtual member functions.

A custom surface stores 32-bit premultiplied BGRA pixels (`BitmapFormat::BGRA8_UNORM_SRGB`), and rows can include padding.

| Method | Description |
| :--- | :--- |
| `width()`, `height()` | Return the dimensions in pixels. |
| `row_bytes()` | Returns the byte stride between consecutive rows. |
| `size()` | Returns the total allocation size in bytes. |
| `LockPixels()`, `UnlockPixels()` | Lock the buffer to return a writable pixel pointer, then unlock it when drawing finishes. |
| `Resize()` | Reallocates the buffer to a new width and height in pixels. Never called while pixels are locked. |
| `Scroll()` | Shifts an existing rectangle of pixels within the buffer when the page scrolls. |

### Surface Lifecycle

The library calls `SurfaceFactory::CreateSurface()` whenever you create a CPU View, and calls `SurfaceFactory::DestroySurface()` when the View is destroyed.

During `Renderer::Render()`, the library calls `LockPixels()`, paints updated regions into the buffer, and calls `UnlockPixels()`. The painted area is then added to `dirty_bounds()`.

### Shifting Pixels on Scroll

When a page scrolls, the library calls `Scroll()` to move existing pixels rather than repainting the entire view.

This call shifts the source surface buffer, not the destination display texture. Calling `Surface::ShiftPixels()` handles the in-place pixel copy.

| Return Value | Description |
| :--- | :--- |
| `false` | Default behavior. The library marks the shifted rectangle as dirty, and you re-copy the moved area to the destination. |
| `true` | Used only if you shifted the destination texture by the same offset. The library marks only the newly exposed strip as dirty. |

> 🚧 Pixels Are Locked During Scroll
>
> The library calls `Scroll()` while pixels are locked. Shift pixels using the pointer returned by `LockPixels()`, and never call `LockPixels()` again inside `Scroll()`.

### Example: OpenGL PBO Surface

This custom surface implementation draws directly into an OpenGL pixel buffer object and syncs non-empty dirty bounds to a texture.

```cpp
///
/// 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_);
    mapped_pixels_ = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_READ_WRITE);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    return mapped_pixels_;
  }

  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);
    mapped_pixels_ = nullptr;
  }

  virtual bool Scroll(const IntRect& rect, int dx, int dy) override {
    ///
    /// Shift the pixels through the pointer we handed out in LockPixels()
    /// (mapping the PBO a second time would return null). Our upload copies
    /// the whole PBO, so return false and let the library report the
    /// shifted rectangle dirty too.
    ///
    Surface::ShiftPixels(mapped_pixels_, row_bytes_, rect, dx, dy);
    return false;
  }

  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_ = 0;
  GLuint pbo_id_ = 0;
  void* mapped_pixels_ = nullptr;
  uint32_t width_;
  uint32_t height_;
  uint32_t row_bytes_;
  uint32_t size_;
};
```

## Registering a Surface Factory

To supply custom surfaces to the renderer, subclass `SurfaceFactory` and implement `CreateSurface()` and `DestroySurface()`.

```cpp
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);
  }
};
```

Register the factory instance with `Platform::instance().set_surface_factory()` and keep it alive for the lifetime of the program.

```cpp
///
/// 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());
```

> 🚧 Register Before Creating the Renderer
>
> Register the factory before calling `Renderer::Create()` and before creating any views— each view receives its surface when created. The library provides a default factory, so failures occur only if you set the factory to null or if `CreateSurface()` returns null. In either case, the library logs an error and exits the process.

## Displaying the Surface

To display the rendered content, cast `View::surface()` to the custom surface type and retrieve the texture handle.

```cpp
///
/// 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...
///
```

To display native graphics inside the page rather than rendering the page to a texture, see [Displaying Custom Textures](/docs/2.0/displaying-custom-textures).
