Using a Custom Surface
The Default BitmapSurface
By default, when using the CPU renderer, each View paints to a corresponding BitmapSurface
:
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();
};
The general idea is that, during your paint routine, you would get the Surface for a View, cast it to a BitmapSurface, and retrieve the underlying Bitmap for display wherever 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...
///
This method is convenient but for better performance / memory utilization, you may want to provide your own custom Surface instead so that the renderer can paint directly to a block of memory controlled by you.
Defining a Custom Surface
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:
///
/// 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 just need to register this new SurfaceFactory with Platform::instance().set_surface_factory()
(make sure to call this before creating 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
Now we can 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...
///
Updated over 2 years ago