Donner SVG Editor & Engine
SVG-native editor and embeddable SVG2 + CSS3 engine in C++20, with GPU (WebGPU) and compact CPU renderers, built for correctness, security, and performance.
Loading...
Searching...
No Matches
Embedding Geode in a host application

Geode is Donner's GPU-native SVG rendering backend. In most uses it runs headless and owns its own wgpu::Device, but the public API also supports embedded mode: the host application owns the WebGPU device/queue and a target texture (typically a swap-chain image), and Geode draws into that texture without creating any WebGPU objects of its own.

Reach for embedded mode when you want to render SVG into an existing WebGPU frame alongside other GPU work — a game engine UI layer, a native editor window, a composited WebGPU canvas — without paying for a second device.

Prerequisites

  • A valid wgpu::Device and its default wgpu::Queue, both created by the host.
  • The host's wgpu::Instance when synchronous snapshots must dispatch WebGPU map callbacks, as required by browser embedders.
  • A target wgpu::Texture with TextureUsage::RenderAttachment in its usage flags. CopySrc is required in addition if you plan to call RendererGeode::takeSnapshot().
  • A build with Geode enabled: bazel build --config=geode //your:target (the config sets --//donner/svg/renderer/geode:enable_geode=true).

Geode is confirmed against the wgpu-native backend that ships in third_party/webgpu-cpp. The Dawn C++ wrapper exposes the same C++ API surface but a few status enumerants differ (see Troubleshooting below).

Embedding walkthrough

1. Describe the embedding

GeodeEmbedConfig (declared in donner/svg/renderer/geode/GeodeDevice.h) bundles the host state Geode needs:

config.instance = hostInstance; // optional; enables browser snapshot callbacks.
config.device = hostDevice; // wgpu::Device, must not be null.
config.queue = hostQueue; // wgpu::Queue, must not be null.
config.textureFormat = wgpu::TextureFormat::BGRA8Unorm; // match your target.
config.adapter = hostAdapter; // optional; retained for adapter metadata queries.
RAII wrapper around a WebGPU device - headless or host-provided.
Configuration for embedding Geode into a host application that already owns a WebGPU device.
Definition GeodeDevice.h:47
wgpu::Instance instance
Optional host-provided WebGPU instance. Browser embedders should provide it so synchronous snapshot r...
Definition GeodeDevice.h:51
wgpu::Device device
Host-provided WebGPU device. Must not be null.
Definition GeodeDevice.h:54
wgpu::Queue queue
Host-provided queue associated with device. Must not be null.
Definition GeodeDevice.h:57
wgpu::Adapter adapter
Optional adapter handle. Preserved for hosts that need to query the adapter associated with the exter...
Definition GeodeDevice.h:65
wgpu::TextureFormat textureFormat
Texture format for render targets. Must match the format of any texture passed to RendererGeode::setT...
Definition GeodeDevice.h:61

The instance and adapter fields are optional. Browser embedders should supply instance when calling RendererGeode::takeSnapshot() so synchronous readback can wait for map callback completion through Instance::waitAny(). When supplied, Geode preserves adapter for callers that need metadata about the adapter associated with the external device.

2. Construct a non-owning GeodeDevice

if (!geodeDevice) {
// Either `config.device` or `config.queue` was null.
return;
}
static std::unique_ptr< GeodeDevice > CreateFromExternal(const GeodeEmbedConfig &config)
Create a GeodeDevice wrapping a host-provided device and queue.

CreateFromExternal is non-owning: the returned GeodeDevice's destructor will not release the underlying wgpu::Instance, adapter, device, or queue. That ownership stays with the host.

3. Construct the renderer

// shared_ptr allows the same GeodeDevice to back multiple renderers, and
// keeps the device alive for the full renderer lifetime.
std::shared_ptr<donner::geode::GeodeDevice> device = std::move(geodeDevice);
donner::svg::RendererGeode renderer(device);
Geode (WebGPU/Slug) implementation of donner::svg::RendererInterface.
Geode rendering backend - GPU-native via WebGPU + the Slug algorithm.
Definition RendererGeode.h:138

4. Per-frame rendering

wgpu::Texture swapChainTex = /* acquire from wgpu::Surface */;
renderer.setTargetTexture(swapChainTex);
renderer.draw(document); // `donner::svg::SVGDocument&`
renderer.clearTargetTexture();
// The host then presents its surface however it normally does.

Call setTargetTexture once per frame (before beginFrame / draw / endFrame, which RendererGeode::draw fuses together internally), and call clearTargetTexture after each frame if you intend to mix embedded and headless output — it reverts the renderer to the internal offscreen target path.

Lifetime rules

  • The host owns wgpu::Instance, wgpu::Adapter, wgpu::Device, and wgpu::Queue. GeodeDevice::CreateFromExternal does not retain refcounts; you must keep the host objects alive for the full lifetime of every GeodeDevice and RendererGeode derived from them.
  • The target texture (the argument to setTargetTexture) must stay live through the matching frame's endFrame or draw call. For a swap chain, the natural boundary is "don't release until after `surface.present()`".
  • Destroy RendererGeode and GeodeDevice instances before the host's wgpu::Device. Geode's pipeline objects are released in the renderer's destructor and require a live device.

Target-texture requirements

Requirement Why
usage includes wgpu::TextureUsage::RenderAttachment Geode draws into the texture via a render pass.
format matches GeodeEmbedConfig::textureFormat The internal pipelines are built against a single color format.
usage includes wgpu::TextureUsage::CopySrc Only needed for RendererGeode::takeSnapshot(); omit otherwise.
sampleCount is 1 Geode renders directly into a single-sample target.

If the format doesn't match, Geode will reject the texture at beginFrame time and fall back to its internal offscreen target for that frame.

Complete example

A runnable GLFW host lives at examples/geode_embed.cc alongside its two platform-specific surface helpers (geode_embed_surface_linux.cc, geode_embed_surface_macos.mm). Build and run it with:

bazel run --config=geode //examples:geode_embed -- path/to/drawing.svg

The example handles the full host lifecycle:

  1. Parses the SVG via SVGParser::ParseSVG.
  2. Creates a GLFW window with GLFW_NO_API (no GL context).
  3. Creates wgpu::Instance, wgpu::Surface, wgpu::Adapter, wgpu::Device, and queries SurfaceCapabilities for a supported 8-bit color format.
  4. Wraps the resulting device via GeodeDevice::CreateFromExternal, then constructs one RendererGeode and reuses it across every frame.
  5. In the main loop: glfwPollEvents, surface.getCurrentTexture, renderer.setTargetTexture, renderer.draw, surface.present, wgpuTextureRelease.

Troubleshooting

X11 header ordering on Linux

Defining GLFW_EXPOSE_NATIVE_X11 pulls in <X11/Xlib.h>, which #defines None, True, False, and Status — all of which collide with C++ names used elsewhere (for example wgpu::Status, an enum class in third_party/webgpu-cpp/webgpu.hpp). Two fixes, in order of preference:

  1. Isolate the GLFW-native call in its own translation unit that includes only webgpu.hpp plus GLFW/glfw3native.h and #undefs the Xlib macros before returning. The example does exactly this in geode_embed_surface_linux.cc.
  2. Include donner headers first, then the GLFW native header, then #undef None, #undef True, #undef False, #undef Status. Acceptable for small prototypes, but the macros will re-trip anyone who later adds an include above the #undefs.

wgpu-native vs. Dawn surface-texture API drift

wgpu::Surface::getCurrentTexture writes status into WGPUSurfaceTexture::status, and the success enumerant on wgpu-native is SuccessOptimal (not Success). Treat both SuccessOptimal and SuccessSuboptimal as renderable; skip the frame on Timeout, Outdated, Lost, and reconfigure the surface on Outdated. Similarly, Instance::requestAdapter and Adapter::requestDevice are callback-based in the C API; the webgpu-cpp wrapper provides synchronous overloads that work on wgpu-native because the callback fires before the call returns. Dawn requires driving the future explicitly — the wrapper handles that transparently too, but be aware that synchronous wrapper calls are a wgpu-native-specific convenience.

If you see surface.getCurrentTexture always reporting Outdated, the surface dimensions probably don't match the SurfaceConfiguration — call surface.configure again after any window resize.