|
|
Donner SVG 0.8.0-pre
SVG editor and embeddable C++20 engine.
|
Owns (or wraps) a WebGPU device/queue pair for GPU rendering. More...
#include "donner/svg/renderer/geode/GeodeDevice.h"
Classes | |
| struct | ReadbackStats |
| struct | SceneBatchBindGroupKey |
| Key identifying a scene-batch bind group by its exact buffer bindings: the shared batch uniform allocation, the geometry slab chunk, and the record span. More... | |
Public Member Functions | |
| ~GeodeDevice () | |
| Destructor releases the device and all GPU resources. All teardown waits are bounded; if the device has been declared lost (see isDeviceLost) the destructor performs no GPU waits at all and deliberately leaks the root WebGPU handles (queue, device, adapter, instance) rather than risking a blocking call into a hung driver. The leak is bounded to one device's worth of driver objects per loss, and a lost device is a process-fatal condition for GPU rendering anyway. | |
| GeodeDevice (const GeodeDevice &)=delete | |
| GeodeDevice & | operator= (const GeodeDevice &)=delete |
| GeodeDevice (GeodeDevice &&)=delete | |
| GeodeDevice & | operator= (GeodeDevice &&)=delete |
| const wgpu::Device & | device () const |
| Returns the wgpu::Device. Guaranteed valid for the lifetime of this object. | |
| uint32_t | maxTextureDimension2D () const |
| Maximum supported width or height of a 2D texture on this device. | |
| bool | pollSuspending (bool wait) const |
| Poll the device, bracketed for ASYNCIFY suspend attribution. | |
| GpuWaitResult | waitForQueueIdle (std::chrono::milliseconds timeout=kDefaultGpuWaitTimeout) const |
| Wait, bounded, for all submitted GPU work to complete. | |
| bool | isDeviceLost () const |
| True once this device has been declared lost, either by the WebGPU device-lost callback (driver-reported) or by a bounded GPU wait exceeding its deadline. Sticky: never resets. Once lost, rendering output is undefined, snapshots return empty bitmaps promptly, and teardown skips all GPU waits. | |
| void | markDeviceLost (const char *reason) const |
Declare this device lost. Idempotent; the first call logs reason. Called from bounded waits on timeout, and available to embedders whose own device-lost signal is not shared via GeodeEmbedConfig::lostState. Const because observers treat the flag as shared diagnostic state and waits that discover a hang run through const accessors. | |
| void | markDeviceLostAfterWaitTimeout (GpuWaitSite site, std::chrono::milliseconds elapsed, const char *reason) const |
| Declare this device lost because a bounded wait exceeded its deadline, recording which wait it was and how long it actually ran. | |
| const wgpu::Instance & | instance () const |
| Instance that created the headless device. Null for externally-owned devices. | |
| const wgpu::Queue & | queue () const |
| Returns the default queue. | |
| void | recordReadback (bool usedTimedWaitAny, int pollIterations) |
| Record one completed CPU readback from a renderer sharing this device. | |
| ReadbackStats | consumeReadbackStats () |
| Consume aggregate readback diagnostics for all renderers sharing this device. | |
| const wgpu::Adapter & | adapter () const |
| Returns the adapter backing this device. May be null when the host does not provide one, including embedded mode and browser headless imports. | |
| wgpu::TextureFormat | textureFormat () const |
| Render-target texture format. Defaults to RGBA8Unorm for headless devices; set by the host via GeodeEmbedConfig::textureFormat in embedded mode. | |
| void | deferDestroy (wgpu::Buffer buffer) |
| Enqueue a GPU buffer for deferred destruction. | |
| void | deferDestroy (wgpu::Texture texture) |
| Enqueue a GPU texture for deferred destruction. | |
| void | deferDestroy (gpu::BindGroup bindGroup) |
| Enqueue a bind group for deferred destruction. | |
| void | deferDestroy (gpu::Texture texture) |
| Enqueue a runtime texture for deferred destruction. | |
| void | drainDeferredDestroys () |
| Drop all deferred-destroy handles, releasing their GPU resources. | |
| std::size_t | deferredTextureDestroyCountForTesting () const |
| Number of textures waiting for the next frame-boundary destroy pass. Exposed to pin resource-retirement behavior in renderer regression tests. | |
| const gpu::BindGroup * | findSceneBatchBindGroup (const SceneBatchBindGroupKey &key) |
| Look up a cached scene-batch bind group. Returns a borrowed handle (valid while the cache entry lives), or an empty handle on miss. A hit refreshes the entry to most-recently-used, so eviction at the cap drops the coldest entry rather than the oldest-inserted one - with more than the cap of live groups, pure insertion order would evict the hottest steady-state entry every frame. | |
| const gpu::BindGroup & | storeSceneBatchBindGroup (const SceneBatchBindGroupKey &key, gpu::BindGroup group) |
| Store a scene-batch bind group under key, taking ownership of the +1 handle. At the cap, the OLDEST entries are evicted one at a time rather than the whole cache being dropped: the cache is device-wide and a key belongs to one document's buffers, so wholesale clearing punished the live document for documents that had already been torn down, and a steady frame then rebuilt every batch group it had just been using. Insertion order puts those dead entries first. Recorded command buffers keep their own references, so dropping the cache's handle mid-frame is safe either way. | |
| uint64_t | deviceId () const |
| Process-unique identity for this device instance, assigned at construction from a monotonic counter (never reused, starts at 1). | |
| void | setCounters (GeodeCounters *counters) |
| Install a GeodeCounters struct for this device. | |
| GeodeCounters * | counters () const |
| Non-owning pointer to the installed counters, or null. | |
| void | countBuffer () const |
| void | countBindGroup () const |
| void | countTexture () const |
| uint64_t | lifetimeTextureCreates () const |
| Cumulative number of countTexture() calls since this GeodeDevice was created. Does not account for textures released back into a pool - it is an allocation-site counter, not a live-count. | |
| uint64_t | lifetimeBufferCreates () const |
| Cumulative number of countBuffer() calls since this GeodeDevice was created. Same caveat as lifetimeTextureCreates(). | |
| void | countSubmit () const |
| void | countPathEncode () const |
| void | countDraw () const |
| void | countPipelineSwitch () const |
| void | countBufferWrite (uint64_t bytes) const |
| Record one wgpu::Queue::writeBuffer call of bytes payload bytes. | |
| void | countTextureWrite (uint64_t bytes) const |
| Record one wgpu::Queue::writeTexture call of bytes payload bytes. | |
| uint64_t | beginFrameGeneration () |
| Open a frame on this device and return its generation. | |
| void | endFrameGeneration (uint64_t generation) |
| Close a generation opened by beginFrameGeneration. Unknown values are ignored, so a renderer that never opened one (no-op mode) is safe. | |
| uint64_t | oldestOpenFrameGeneration () const |
| Oldest generation whose frame has not closed yet, or ~0 when no frame is open. Anything a generation at or after this touched may still be read by an unsubmitted draw. | |
| bool | frameStampClaimed (uint64_t stamp) const |
| True when a resource stamped with stamp (a generation minted by beginFrameGeneration) may still be read by a frame that has not submitted: some open frame is at or before the stamp, so a recorded draw in that frame can reference the stamped bytes, and every queue write in a frame lands before every draw in its submit. | |
| void | countGlyphResidencyHit () const |
| Record one glyph occurrence served from an already-resident outline. | |
| void | countGlyphResidencyUpload () const |
| Record one unique glyph outline encoded and made resident. | |
| void | countGlyphResidencyEvictions (uint64_t count) const |
| Record count cached glyph outlines dropped to stay inside the budget. | |
| const std::shared_ptr< std::atomic< int64_t > > & | residentBytesGauge () const |
| Shared live-resident-bytes gauge for GPU residence. Co-owned with each GeodeResidentSlot's buffer so resident-memory accounting stays lifetime-safe even if a document (and its ECS registry) outlives this device. Lazily created on first access. GeoEncoder bumps it when a slot gains residence; the slot decrements it on reset / destruction (geometry change or document teardown), which is the eviction signal for "many distinct
documents". | |
| int64_t | liveResidentBytesForTesting () const |
| Current live resident-geometry bytes across every GeodeResidentSlot buffer created against this device. Zero at construction and after all resident documents are torn down; used by eviction tests. | |
| bool | supportsTimestamps () const |
| Whether the driver supports GPU timestamp queries. | |
| bool | isVulkan () const |
| True when the active wgpu backend is Vulkan (Intel Arc hardware or Mesa lavapipe software). GeodeFilterEngine uses this to force inter-pass serialization that eliminates a nondeterministic cross-submit storage-write -> sampled-read visibility race observed on Arc Vulkan. Metal returns false and keeps the fast multi-submit path. | |
| GeodeWgpuAdapterDevice & | adapterDevice () const UTILS_LIFETIME_BOUND |
The TEMPORARY transition adapter implementing donner::gpu::Device over this device's wgpu objects. Owned here alongside the shared pipelines (which are created through it); see GeodeWgpuAdapterDevice.h for the removal gates. | |
| const GeodeGpuContext & | gpuContext () const UTILS_LIFETIME_BOUND |
| The recording context Geode's encoders record a frame against: this device's GPU runtime device, its shared bind-slot resources, and its counter sinks. Wired once with the shared pipelines and owned here, so it lives exactly as long as this device does. | |
Shared render / compute pipelines (issue #575 fix) | |
The shared bind-slot resources - 1x1 dummy pattern texture and view, 1x1 full-coverage clip-mask texture and view, their samplers, a one-element identity instance record, and a zero-filled gradient paint block - are reached through gpuContext. Every wgpu pipeline created by createRenderPipeline / createComputePipeline is retained internally by wgpu-native even after the public handle's refcount drops to zero - wgpuDevicePoll does not drain it. Prior to this, RendererGeode constructed the four pipeline objects below (~18 wgpu pipelines in total, most inside GeodeFilterEngine) per-instance, so the image-comparison suite leaked ~1.6 MB per test and ultimately exhausted the driver memory budget (see issue #575). Moving ownership here - one copy per GeodeDevice - caps the pipeline footprint at a fixed cost. Every renderer that talks to this device shares the same pipeline objects; their state is intentionally immutable after construction (no per-draw mutation), so concurrent use from sibling renderers is safe as long as it is serialized at the wgpu::Queue level (which Donner's render path already is). | |
| GeodePipeline & | pipeline () const |
| Slug solid-fill render pipeline. | |
| GeodeGradientPipeline & | gradientPipeline () const |
| Slug gradient-fill render pipeline. | |
| GeodeImagePipeline & | imagePipeline () const |
| Image-blit render pipeline (used by GeoEncoder::drawImage and the pattern / layer composition path). | |
| GeodeMaskPipeline & | maskPipeline () const |
| Clip-path mask render pipeline. Built lazily on first access rather than eagerly at device creation, matching the prior per-encoder lazy path - most documents don't use <clipPath> and the production WASM path avoids the cost. | |
| GeodeFilterEngine & | filterEngine () const |
| GPU filter-graph executor. Owns ~15 compute pipelines for SVG filter primitives. | |
| GeodeSnapshotReadbackPipeline & | snapshotReadbackPipeline () const |
| Snapshot-unpremultiply compute pipeline. Built lazily (thread-safe, once-only) on first access so consumers that never read back a snapshot avoid the compile cost. | |
| SnapshotReadbackResources | acquireSnapshotReadbackResources (uint32_t width, uint32_t height) |
| Acquire the pooled readback resource set for a GPU snapshot readback at the given size, allocating it on first use. | |
| void | releaseSnapshotReadbackResources (SnapshotReadbackResources resources) |
| Return a readback resource set acquired from acquireSnapshotReadbackResources to the device pool for reuse. | |
| GeodeCheckerboardPipeline & | checkerboardPipeline () const |
| Framebuffer checkerboard underlay pipeline used by the editor's direct presentation path. Built lazily on first access - only the editor draws it, so headless/WASM consumers never pay the compile cost. | |
| GeodeCheckerboardPipeline & | checkerboardUnderlayPipeline () const |
| Destination-over variant of checkerboardPipeline, for targets that already hold composed premultiplied content and need the checkerboard placed underneath it. Built lazily and independently of the replace-blend pipeline, so a consumer only pays for the variant it actually draws. | |
Static Public Member Functions | |
| static std::unique_ptr< GeodeDevice > | CreateHeadless (wgpu::TextureFormat textureFormat=wgpu::TextureFormat::RGBA8Unorm) |
| Create a headless GeodeDevice. | |
| static std::unique_ptr< GeodeDevice > | CreateFromExternal (const GeodeEmbedConfig &config) |
| Create a GeodeDevice wrapping a host-provided device and queue. | |
| static int | headlessCreationCountForTesting () |
| Number of CreateHeadless calls made so far in this process. Each headless creation stands up a full WebGPU instance/adapter/device, so hot paths must share one device instead of re-creating; tests pin that sharing by asserting this count stays flat across repeated operations. | |
| static std::size_t | outstandingDeviceLostCallbacksForTesting () |
| Number of retained headless-device loss callback states not yet consumed by WebGPU. | |
| static uint64_t | AllocateBufferId () |
| Allocate a process-unique identity for a GPU buffer handle, from a monotonic counter that starts at 1 and is never reused. | |
Owns (or wraps) a WebGPU device/queue pair for GPU rendering.
GeodeDevice is the entry point to the Geode rendering backend. In headless mode (CreateHeadless), it creates a WebGPU instance, selects a default adapter, and creates a device - all without any window system integration.
In embedded mode (CreateFromExternal), it wraps a device and queue already created by the host application. The host retains ownership of the underlying WebGPU objects; GeodeDevice's destructor will not destroy them.
Typical headless usage:
auto maybeDevice = GeodeDevice::CreateHeadless();
if (!maybeDevice) {
// No GPU available.
return;
}
Typical embedded usage:
GeodeEmbedConfig config; config.device = hostDevice; config.queue = hostQueue; auto geodeDevice = GeodeDevice::CreateFromExternal(config);
| struct donner::geode::GeodeDevice::ReadbackStats |
| Class Members | ||
|---|---|---|
| int | count = 0 | |
| bool | deviceLost = false | True once this device has been declared lost. Sticky, so every later consume keeps reporting it: a frame that never rendered has no other evidence to carry. |
| int | pollIterations = 0 | |
| int | timedOutWaitMs = 0 | Wall time that wait spent before giving up, in milliseconds. |
| GpuWaitSite | timedOutWaitSite = GpuWaitSite::None | Bounded wait that declared that loss, or None. |
| bool | usedTimedWaitAny = false | |
| SnapshotReadbackResources donner::geode::GeodeDevice::acquireSnapshotReadbackResources | ( | uint32_t | width, |
| uint32_t | height ) |
Acquire the pooled readback resource set for a GPU snapshot readback at the given size, allocating it on first use.
Repeated snapshots at the same dimensions reuse the pooled entry, so steady-state snapshot readback allocates nothing.
The caller owns the returned set until releaseSnapshotReadbackResources returns it to the pool, or until the set is destroyed unpooled. An empty set means allocation failed.
|
staticnodiscard |
Allocate a process-unique identity for a GPU buffer handle, from a monotonic counter that starts at 1 and is never reused.
Anything that outlives a buffer and still has to answer "is this the same buffer I saw before?" must compare these ids rather than WGPUBuffer handle addresses. Releasing the last handle reference frees the handle object even while WebGPU keeps the underlying resource alive for bind groups and recorded commands that already reference it, so a later allocation can reuse the address and make two unrelated buffers indistinguishable. deviceId() exists for the same reason one level up.
0 is reserved for "no buffer", so a default-constructed id never matches a real one.
|
inline |
Open a frame on this device and return its generation.
Generations are DEVICE-scoped and monotonic, so two renderers sharing one device cannot mint the same index. That happens routinely: an offscreen renderer built from this device renders an feImage fragment or a layer thumbnail from the same document while the outer renderer's frame is still open and has already recorded draws. With per-renderer counters the two frames alias, and a cache keyed on "was this touched in the current frame?" cannot tell the inner pass from the outer one - it would let the inner pass rewrite buffers the outer pass's recorded draws read, and every buffer write in a frame lands before every draw in that frame's submit.
Pair with endFrameGeneration. A cache that must not recycle memory an unsubmitted frame still reads compares against oldestOpenFrameGeneration() rather than against its own index.
|
static |
Create a GeodeDevice wrapping a host-provided device and queue.
The returned device does NOT own the underlying WebGPU instance, adapter, device, or queue - the host is responsible for keeping them alive.
| config | Embedding configuration with valid device/queue handles. |
config.device or config.queue is null.
|
static |
Create a headless GeodeDevice.
| void donner::geode::GeodeDevice::deferDestroy | ( | gpu::BindGroup | bindGroup | ) |
Enqueue a bind group for deferred destruction.
Same semantics as the buffer variant: a bind group evicted from a cache may still be named by draws that have been recorded but not yet replayed to the backend, and destroying it there fails those draws closed.
| void donner::geode::GeodeDevice::deferDestroy | ( | gpu::Texture | texture | ) |
Enqueue a runtime texture for deferred destruction.
Same semantics as the buffer variant: a render target superseded mid-frame may still be named by work that has been recorded but not yet submitted, so its slot is released at the next frame boundary instead of immediately.
| void donner::geode::GeodeDevice::deferDestroy | ( | wgpu::Buffer | buffer | ) |
Enqueue a GPU buffer for deferred destruction.
The buffer handle is kept alive until drainDeferredDestroys() is called, preventing the underlying GPU resource from being freed while an in-flight command buffer may still reference it.
| void donner::geode::GeodeDevice::deferDestroy | ( | wgpu::Texture | texture | ) |
Enqueue a GPU texture for deferred destruction.
Same semantics as the buffer variant.
|
inline |
Process-unique identity for this device instance, assigned at construction from a monotonic counter (never reused, starts at 1).
Used by GPU-residence slots to detect when a cached buffer / bind group belongs to a DIFFERENT device than the one now rendering: a document (and its ECS GeodeResidentPathComponents) can outlive the device that filled them and later be rendered by a second RendererGeode / GeodeDevice. WebGPU rejects cross-device resources inside a render pass, so a slot whose stored id does not match deviceId() is treated as non-resident and re-uploaded. A monotonic counter (rather than a raw this pointer) avoids the ABA hazard of a freed device's address being recycled by a later allocation.
| void donner::geode::GeodeDevice::drainDeferredDestroys | ( | ) |
Drop all deferred-destroy handles, releasing their GPU resources.
Called at the top of each frame (before new allocations) so resources from the previous frame's command buffer submission have had time to complete on the GPU. WebGPU internally reference-counts resources used by submitted command buffers, so dropping our handle here is safe even without an explicit device.poll().
|
inlinenodiscard |
True when a resource stamped with stamp (a generation minted by beginFrameGeneration) may still be read by a frame that has not submitted: some open frame is at or before the stamp, so a recorded draw in that frame can reference the stamped bytes, and every queue write in a frame lands before every draw in its submit.
The never-drawn sentinel ~0 used by the resident-slot stamps is explicitly NOT claimed - a naive >= would read a never-drawn slot as claimed forever, since oldestOpenFrameGeneration() is also ~0 when no frame is open.
| void donner::geode::GeodeDevice::markDeviceLostAfterWaitTimeout | ( | GpuWaitSite | site, |
| std::chrono::milliseconds | elapsed, | ||
| const char * | reason ) const |
Declare this device lost because a bounded wait exceeded its deadline, recording which wait it was and how long it actually ran.
Prefer this over markDeviceLost at every deadline: the attribution is what turns "rendering stopped" into a diagnosable report, and it is only available at the wait site. Loss stays sticky, and only the call that declares it records an attribution; see DeclareDeviceLostAfterWaitTimeout for why that rule is what keeps a driver-reported loss from being relabelled as a wait timeout.
| site | Which bounded wait expired. |
| elapsed | Wall time that wait spent before giving up. |
| reason | Human-readable cause, logged once like markDeviceLost. |
| bool donner::geode::GeodeDevice::pollSuspending | ( | bool | wait | ) | const |
Poll the device, bracketed for ASYNCIFY suspend attribution.
Under Emscripten, emdawnwebgpu implements poll by yielding the Asyncify-enabled thread for roughly one browser task regardless of wait, so every poll unwinds and later rewinds the wasm stack. With the whole application on one thread (single-canvas presenter architecture) that wall time is UI frame time, so it has to be attributable. Route every poll through here rather than calling device().poll directly; the probe is a pair of clock reads on native builds, where poll does not suspend at all.
Prefer wait = false: a waiting poll can block inside a hung driver with no bound. Callers that need to wait for the queue to drain should use waitForQueueIdle, which is bounded and reports a hang as a device-lost condition.
| void donner::geode::GeodeDevice::releaseSnapshotReadbackResources | ( | SnapshotReadbackResources | resources | ) |
Return a readback resource set acquired from acquireSnapshotReadbackResources to the device pool for reuse.
The returned set must be unmapped. Do not call this after the readback map was cancelled and the buffer destroyed; drop the set instead.
The pool holds at most a small fixed number of size buckets; when a release would exceed that, the least-recently-used entry's backing resources are destroyed (pooled entries are idle, so eager destroy is safe).
|
inline |
Install a GeodeCounters struct for this device.
Non-owning; the caller must keep the struct alive for as long as the device might increment it. Pass nullptr to disable instrumentation.
All Geode components that hold a GeodeDevice& route their per-frame hot-path allocation and submission sites through this hook.
|
inline |
Whether the driver supports GPU timestamp queries.
Always false today - reserved for future work.
| GpuWaitResult donner::geode::GeodeDevice::waitForQueueIdle | ( | std::chrono::milliseconds | timeout = kDefaultGpuWaitTimeout | ) | const |
Wait, bounded, for all submitted GPU work to complete.
Replaces unbounded poll(wait=true) loops: the wait is a non-blocking poll at kGpuWaitPollInterval cadence with a deadline, so a hung driver costs at most timeout instead of blocking the calling thread forever (in the worst case in uninterruptible kernel sleep). On timeout the device is marked lost (see markDeviceLost) and later waits on this device return immediately.
Completion condition: the wait observes "queue empty", not "the work submitted before this call completed". With a concurrent submitter on the same underlying queue (on native, the async-render thread and the editor framebuffer wrapper share one WebGPU queue), a healthy but continuously saturated queue could in principle be non-empty at every poll sample for the whole timeout and be falsely declared lost. This is accepted: real submit cadences leave idle gaps many orders of magnitude wider than the 100 us sampling interval, the exposure is bounded by the generous timeout, and a false positive degrades to the detection path (renderer reports device loss and tears down wait-free) rather than a hang or crash.
Under Emscripten this performs the pre-existing single poll-yield instead of a bounded drain loop: emdawnwebgpu's poll return value does not report queue-idle, and browser device hangs surface through the readback map deadline and the browser's own device-loss reporting.
| timeout | Wait budget; defaults to the shared generous bound. |