Using a Custom Surface

The Default BitmapSurface

When using the CPU renderer each View is painted to a BitmapSurface by default:

class UExport BitmapSurface : public Surface {
public:
  virtual uint32_t width() const override;

  virtual uint32_t height() const override;

  virtual uint32_t row_bytes() const override;

  virtual size_t size() const override;

  virtual void* LockPixels() override;

  virtual void UnlockPixels() override;

  virtual void Resize(uint32_t width, uint32_t height) override;

  ///
  /// Get the underlying Bitmap.
  ///
  RefPtr<Bitmap> bitmap();
};

During your paint routine you should access the Surface for a View, cast it to a BitmapSurface, retrieve the underlying Bitmap and display it how you want:

///
/// 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 is convenient but you may want to provide your own Surface implementation so that the renderer can paint directly to a block of memory controlled by you.

To define a custom Surface, 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:

///
/// 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 at will, you also need to define a corresponding SurfaceFactory for your new class:

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 with Platform::instance().set_surface_factory() (make sure to call this before creating the Renderer or any Views):

///
/// 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:

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