|
|
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.
|
Drawing API for the Geode GPU renderer. More...
#include "donner/svg/renderer/geode/GeoEncoder.h"
Classes | |
| struct | PatternPaint |
| Describes a pattern tile used as a paint source for fillPathPattern. More... | |
Public Member Functions | |
| GeoEncoder (GeodeDevice &device, const GeodePipeline &fillPipeline, const GeodeGradientPipeline &gradientPipeline, const GeodeImagePipeline &imagePipeline, const wgpu::Texture &target) | |
| Create an encoder targeting the given texture. | |
| GeoEncoder (GeodeDevice &device, const GeodePipeline &fillPipeline, const GeodeGradientPipeline &gradientPipeline, const GeodeImagePipeline &imagePipeline, const wgpu::Texture &target, wgpu::CommandEncoder sharedCommandEncoder) | |
| Shared-CommandEncoder constructor (design doc 0030 Milestone 3). | |
| void | setBufferPool (GeodeBufferPool *pool) |
| Install a cross-frame buffer pool (design doc 0030 M1). | |
| GeoEncoder (const GeoEncoder &)=delete | |
| GeoEncoder & | operator= (const GeoEncoder &)=delete |
| GeoEncoder (GeoEncoder &&) noexcept | |
| Move constructor. | |
| GeoEncoder & | operator= (GeoEncoder &&) noexcept |
| Move assignment operator. | |
| void | clear (const css::RGBA &color) |
| Clear the target texture to the given color. | |
| void | setLoadPreserve () |
| Switch the next render pass's load op from Clear to Load, preserving whatever the target texture already contains. | |
| void | setAntialias (bool antialias) |
| Enable analytic edge anti-aliasing for subsequent path and mask draws. Disabled mode emits binary coverage at the pixel center. | |
| void | setTransform (const Transform2d &transform) |
| Set the model-view transform for subsequent draw calls. | |
| void | setScissorRect (int32_t x, int32_t y, int32_t w, int32_t h) |
| Set a scissor rectangle in target-pixel coordinates. | |
| void | clearScissorRect () |
| Remove any active scissor, restoring full-target rasterization. | |
| void | setClipPolygon (const Vector2d corners[4]) |
| Activate a convex 4-vertex clip polygon (Phase 3a). | |
| void | clearClipPolygon () |
| Clear any active clip polygon, restoring unclipped rasterisation (or falling back to just the scissor rect, if one is set). | |
| void | beginMaskPass (const wgpu::Texture &mask) |
| Phase 3b: open a new render pass that writes into the given mask texture. | |
| void | fillPathIntoMask (const Path &path, FillRule rule, const EncodedPath *precomputedEncoded=nullptr) |
| Fill path into the currently open mask pass using the Slug mask pipeline. | |
| void | endMaskPass () |
| Close the mask render pass opened by beginMaskPass. | |
| void | setClipMask (const wgpu::TextureView &maskView) |
| Bind maskView as the clip mask texture for subsequent fill / gradient draws. | |
| void | setClipMask (const wgpu::Texture &maskTexture, const wgpu::TextureView &maskView) |
| Preferred overload: sets both the view AND the parent texture so the encoder keeps the underlying Vulkan resource alive for as long as it's bound. | |
| void | clearClipMask () |
| Remove any active clip mask, restoring unclipped rasterisation. | |
| void | blitFullTarget (const wgpu::Texture &src, double opacity) |
| Blit an offscreen texture across the entire target with an alpha multiplier. | |
| void | blitFullTargetMasked (const wgpu::Texture &content, const wgpu::Texture &mask, const std::optional< Box2d > &maskBounds) |
| Phase 3c <mask> compositing. | |
| void | blitFullTargetBlended (const wgpu::Texture &layer, const wgpu::Texture &dstSnapshot, uint32_t blendMode, double opacity) |
| Phase 3d mix-blend-mode compositing. | |
| void | drawImage (const svg::ImageResource &image, const Box2d &destRect, double opacity, bool pixelated) |
| Draw a raster image into the given destination rectangle. | |
| void | drawTexture (const wgpu::Texture &texture, const Box2d &destRect, double opacity, bool pixelated) |
| Draw an already-GPU-resident RGBA texture into the destination rectangle. | |
| void | fillPath (const Path &path, const css::RGBA &color, FillRule rule, const EncodedPath *precomputedEncoded=nullptr) |
| Fill a path with a solid color. | |
| void | fillPathResident (GeodeResidentSlot &slot, const EncodedPath &encoded, const css::RGBA &color, FillRule rule, uint64_t frameId) |
| Solid fill of a cached path with persistent GPU residence (design doc 0030 wave 2: GPU residence). | |
| void | fillPathInstanced (const EncodedPath &encoded, const css::RGBA &color, FillRule rule, std::span< const float > instanceTransforms) |
| Fill N copies of the same encoded path at N different affine transforms, in one GPU draw call (Milestone 6 Bullet 2). | |
| void | fillPathLinearGradient (const Path &path, const LinearGradientParams ¶ms, FillRule rule, const EncodedPath *precomputedEncoded=nullptr) |
| Fill a path with a linear gradient. | |
| void | fillPathRadialGradient (const Path &path, const RadialGradientParams ¶ms, FillRule rule, const EncodedPath *precomputedEncoded=nullptr) |
| Fill a path with a radial gradient. | |
| void | fillPathPattern (const Path &path, FillRule rule, const PatternPaint &paint, const EncodedPath *precomputedEncoded=nullptr) |
| Fill a path with a repeating pattern tile. | |
| void | finish () |
| Submit all encoded commands to the GPU queue. | |
Drawing API for the Geode GPU renderer.
GeoEncoder is a per-frame command builder. Construct one against a target texture, issue draw calls (fillPath, clear, etc.), then call finish() to submit the command buffer to the GPU.
The encoder owns no GPU buffers itself - each draw call allocates fresh vertex / band / curve / uniform buffers. This is the simplest possible implementation; later phases will add buffer pooling and the ECS-backed GeodePathCacheComponent for paths whose geometry hasn't changed.
Typical usage:
GeoEncoder encoder(device, pipeline, targetTexture); encoder.clear(css::RGBA::White); encoder.setTransform(Transform2d::Scale(2.0)); encoder.fillPath(myPath, css::RGBA::Red, FillRule::NonZero); encoder.finish();
| struct donner::geode::GeoEncoder::PatternPaint |
Describes a pattern tile used as a paint source for fillPathPattern.
The tile texture is expected to contain pre-rendered pattern content in premultiplied RGBA. The Slug fill shader samples it with the Repeat wrap mode (equivalent to SVG <pattern> default behaviour) using the provided transform to map path-space positions into tile-space.
| Class Members | ||
|---|---|---|
| double | opacity = 1.0 | Multiplicative alpha applied to the sampled tile color. Usually fill-opacity * opacity. |
| Transform2d | patternFromPath | Transform from path space (where the path being filled lives) to pattern tile space. Typically inverse(targetFromPattern) composed with the current encoder transform - the RendererGeode layer builds the right composition. |
| Texture | tile | Pre-rendered tile texture (RGBA8, premultiplied). |
| Vector2d | tileSize | Size of the tile rectangle in pattern space (width, height). The shader uses this to wrap sample positions via fract(). |
| donner::geode::GeoEncoder::GeoEncoder | ( | GeodeDevice & | device, |
| const GeodePipeline & | fillPipeline, | ||
| const GeodeGradientPipeline & | gradientPipeline, | ||
| const GeodeImagePipeline & | imagePipeline, | ||
| const wgpu::Texture & | target ) |
Create an encoder targeting the given texture.
| device | The Geode device (owns the wgpu::Device + queue). |
| fillPipeline | The Slug fill pipeline. |
| gradientPipeline | The Slug gradient-fill pipeline. |
| imagePipeline | The image-blit pipeline. |
| target | Single-sample render target. Usage must include RenderAttachment; add TextureBinding or CopySrc when callers sample or read it after rendering. The texture must outlive finish(). |
| donner::geode::GeoEncoder::GeoEncoder | ( | GeodeDevice & | device, |
| const GeodePipeline & | fillPipeline, | ||
| const GeodeGradientPipeline & | gradientPipeline, | ||
| const GeodeImagePipeline & | imagePipeline, | ||
| const wgpu::Texture & | target, | ||
| wgpu::CommandEncoder | sharedCommandEncoder ) |
Shared-CommandEncoder constructor (design doc 0030 Milestone 3).
Uses the provided sharedCommandEncoder instead of creating its own. When this overload is used, finish() only ends any open render pass - it does NOT finish the CommandEncoder or submit to the queue. The caller (typically RendererGeode) owns the lifetime of the shared encoder and is responsible for calling sharedCommandEncoder.finish() + queue.submit() exactly once at the end of the frame.
Enables push/pop of isolated layers / filter layers / mask layers without forcing a queue submit per layer boundary - the whole frame's render passes batch into a single command buffer.
| void donner::geode::GeoEncoder::beginMaskPass | ( | const wgpu::Texture & | mask | ) |
Phase 3b: open a new render pass that writes into the given mask texture.
Used by RendererGeode::pushClip to materialise a path-based clip into an RGBA8Unorm coverage texture that subsequent fill/gradient draws can sample.
The main render pass, if open, is closed first. Subsequent fillPathIntoMask calls add paths to the mask via the Slug mask pipeline. endMaskPass closes the mask pass and re-opens the main pass (with LoadOp::Load) when the next draw lands.
| mask | Single-sample RGBA8Unorm target. Sampled by setClipMask after endMaskPass. |
| void donner::geode::GeoEncoder::blitFullTarget | ( | const wgpu::Texture & | src, |
| double | opacity ) |
Blit an offscreen texture across the entire target with an alpha multiplier.
Used by RendererGeode::popIsolatedLayer to composite a sub-layer's content back onto the outer target. The source texture must be the SAME SIZE as this encoder's target - the blit takes the full texture and maps it 1:1 onto the full target. The current setTransform does NOT affect the blit (it's a device-pixel-space operation, not a model-space draw).
| src | Source texture (RGBA8, premultiplied). |
| opacity | Overall alpha multiplier in [0, 1]. |
| void donner::geode::GeoEncoder::blitFullTargetBlended | ( | const wgpu::Texture & | layer, |
| const wgpu::Texture & | dstSnapshot, | ||
| uint32_t | blendMode, | ||
| double | opacity ) |
Phase 3d mix-blend-mode compositing.
Blits layer across the entire target using one of the 16 W3C Compositing Level 1 blend formulas, reading the backdrop from dstSnapshot (which the caller has already copied from the parent target because the shader cannot read the render pass's own color attachment).
Both textures must be the SAME SIZE as this encoder's target and already stored in premultiplied alpha. The encoder's pass must be in a LoadOp::Clear state - the blend shader writes the final pixel directly and relies on the render target being zeroed before the draw.
| layer | Offscreen RGBA8 layer texture (premultiplied). |
| dstSnapshot | Frozen copy of the parent target's current state (premultiplied) - the blend's backdrop. |
| blendMode | Value in 1..=16 matching the donner::svg::MixBlendMode enumeration. A value of 0 silently falls through to no-op. |
| opacity | Group opacity in [0, 1]. Applied to the layer BEFORE the blend formula so an opacity + mix-blend-mode combo on the same element composes correctly - the source colour that enters the blend is layer * opacity. |
| void donner::geode::GeoEncoder::blitFullTargetMasked | ( | const wgpu::Texture & | content, |
| const wgpu::Texture & | mask, | ||
| const std::optional< Box2d > & | maskBounds ) |
Phase 3c <mask> compositing.
Same as blitFullTarget but additionally samples a luminance mask texture and multiplies the content by the mask's BT.709 luminance (× alpha, matching tiny-skia's Mask::fromPixmap(Luminance)). When maskBounds is provided, pixels outside the rect are discarded so the <mask> element's x/y/width/height are honoured.
| content | Offscreen RGBA8 content texture, premultiplied, same size as this encoder's target. |
| mask | Offscreen RGBA8 mask texture, premultiplied, same size as content. The mask's luminance × alpha becomes a per-pixel coverage multiplier on the content. |
| maskBounds | Optional clip rect in target-pixel space. |
| void donner::geode::GeoEncoder::clear | ( | const css::RGBA & | color | ) |
Clear the target texture to the given color.
Must be called before any draw calls - clear is implemented as the load op of the first render pass, so calling it after a draw is a no-op. Subsequent calls override the previous clear color.
| void donner::geode::GeoEncoder::drawImage | ( | const svg::ImageResource & | image, |
| const Box2d & | destRect, | ||
| double | opacity, | ||
| bool | pixelated ) |
Draw a raster image into the given destination rectangle.
The image's straight-alpha RGBA8 pixels are uploaded to a fresh wgpu::Texture and sampled through the image-blit pipeline. The current transform is applied via the MVP matrix, and the destination rectangle is specified in local (pre-transform) coordinates - i.e., the transform and the rect compose the same way as any other draw call against this encoder.
| image | Decoded RGBA8 image resource. No-op if empty or zero-size. |
| destRect | Destination rectangle in local (pre-transform) space. |
| opacity | Overall opacity in [0, 1], combined with the sampled texel's alpha in the fragment shader. |
| pixelated | If true, use nearest-neighbor filtering (for image-rendering: pixelated). Otherwise, bilinear. |
| void donner::geode::GeoEncoder::drawTexture | ( | const wgpu::Texture & | texture, |
| const Box2d & | destRect, | ||
| double | opacity, | ||
| bool | pixelated ) |
Draw an already-GPU-resident RGBA texture into the destination rectangle.
The current transform is applied exactly like drawImage, but the texture is sampled directly without a CPU upload. The texture is expected to contain premultiplied RGBA, matching Geode render-target snapshots.
| void donner::geode::GeoEncoder::fillPath | ( | const Path & | path, |
| const css::RGBA & | color, | ||
| FillRule | rule, | ||
| const EncodedPath * | precomputedEncoded = nullptr ) |
Fill a path with a solid color.
The path is encoded into Slug band data on the CPU, uploaded to GPU buffers, and a draw call is recorded. The fill is applied with the current transform.
| path | The path to fill. |
| color | Solid fill color (NOT premultiplied - the encoder handles premultiplication for the blend pipeline). |
| rule | Fill rule (NonZero or EvenOdd). |
| precomputedEncoded | Optional M2 cache-hit payload - see GeodePathCacheComponent. When non-null, skips encode + counter bump. |
| void donner::geode::GeoEncoder::fillPathInstanced | ( | const EncodedPath & | encoded, |
| const css::RGBA & | color, | ||
| FillRule | rule, | ||
| std::span< const float > | instanceTransforms ) |
Fill N copies of the same encoded path at N different affine transforms, in one GPU draw call (Milestone 6 Bullet 2).
The caller has already packed each transform into the wire format the shader expects: two vec4f rows per instance (row-major affine, 32 bytes per entry). See donner/svg/renderer/geode/shaders/slug_fill.wgsl struct InstanceTransform for the exact layout.
The vertex shader composes each instance's transform with the currently-bound uniforms.mvp, so encoder.setTransform(...) still applies an outer world transform; per-instance data is the delta relative to that. Pass Transform2d::Identity to setTransform if instance transforms already encode the full path→clip-space mapping (the common case for <use> batching, where each instance's transform is the full worldFromEntity).
If instanceTransforms is empty the call is a no-op. If instanceTransforms.size() == 1 this is equivalent to a single fillPath with the given transform folded in - the caller can still prefer it when bouncing through the batcher.
| encoded | Precomputed EncodedPath shared across all instances. Required (non-null) - there's no "encode inline" path here; the whole point is to amortize one encode across many draws. |
| color | Solid fill color (NOT premultiplied). |
| rule | Fill rule. |
| instanceTransforms | Span of {row0, row1} vec4f pairs, two consecutive vec4f per instance (32 bytes each). The span must contain exactly 8 * instanceCount floats. |
| void donner::geode::GeoEncoder::fillPathIntoMask | ( | const Path & | path, |
| FillRule | rule, | ||
| const EncodedPath * | precomputedEncoded = nullptr ) |
Fill path into the currently open mask pass using the Slug mask pipeline.
Must be called between beginMaskPass and endMaskPass. The current encoder transform applies (so clip paths use the same device-pixel mapping as the content being clipped).
| precomputedEncoded | Optional M2 cache-hit payload. When non-null, the encoder skips GeodePathEncoder::encode and the pathEncodes counter bump. Used by RendererGeode to plumb a cached GeodePathCacheComponent::strokeSlot result. |
| void donner::geode::GeoEncoder::fillPathLinearGradient | ( | const Path & | path, |
| const LinearGradientParams & | params, | ||
| FillRule | rule, | ||
| const EncodedPath * | precomputedEncoded = nullptr ) |
Fill a path with a linear gradient.
Same CPU-side Slug band encoding as fillPath, but the shading stage uses the linear-gradient pipeline - each pixel inside the path is colored by sampling the stop list at a per-pixel parameter t computed from params.gradientFromPath, params.startGrad, and params.endGrad, then folded through params.spreadMode.
If the path encodes to zero bands or the stop list is empty, the call is a no-op.
| path | The path to fill. |
| params | Linear gradient parameters. Colors are straight alpha in 0..1 per channel; the encoder premultiplies before upload. Up to 16 stops are honored; excess are silently truncated with a one-shot verbose warning at the call site in RendererGeode. |
| rule | Fill rule (NonZero or EvenOdd). |
| void donner::geode::GeoEncoder::fillPathPattern | ( | const Path & | path, |
| FillRule | rule, | ||
| const PatternPaint & | paint, | ||
| const EncodedPath * | precomputedEncoded = nullptr ) |
Fill a path with a repeating pattern tile.
The pattern texture must have been rendered earlier (e.g., by a nested GeoEncoder) and contains premultiplied RGBA. The shader applies the Slug winding-number coverage test identically to fillPath, and samples the tile for pixels inside the path.
| void donner::geode::GeoEncoder::fillPathRadialGradient | ( | const Path & | path, |
| const RadialGradientParams & | params, | ||
| FillRule | rule, | ||
| const EncodedPath * | precomputedEncoded = nullptr ) |
Fill a path with a radial gradient.
Same CPU encoding and GPU-dispatch machinery as fillPathLinearGradient, but the gradient parameter t is derived from a two-circle radial construction in the shader (see radial_t() in shaders/slug_gradient.wgsl).
If the path encodes to zero bands, the stop list is empty, or params.radius <= 0, the call is a no-op.
| path | The path to fill. |
| params | Radial gradient parameters (center + radius, optional focal point + radius, shared transform and stops). |
| rule | Fill rule (NonZero or EvenOdd). |
| void donner::geode::GeoEncoder::fillPathResident | ( | GeodeResidentSlot & | slot, |
| const EncodedPath & | encoded, | ||
| const css::RGBA & | color, | ||
| FillRule | rule, | ||
| uint64_t | frameId ) |
Solid fill of a cached path with persistent GPU residence (design doc 0030 wave 2: GPU residence).
slot is a per-entity GeodeResidentSlot (owned by GeodeResidentPathComponent, invalidated by the same ComputedPathComponent listener that clears the M2 encode cache). On first use the encoder uploads encoded into a persistent combined Vertex|Storage|Uniform buffer and builds a cached bind group; on every subsequent unchanged frame it re-uses both and writes zero geometry bytes. Only the 288-byte per-draw uniform is rewritten, and only when it actually changed (camera/color move), so a static document's steady-state frame writes ~zero bytes and creates zero bind groups.
Residency is taken ONLY for the fast, cacheable case: no active clip mask, no clip polygon, no open mask pass. When any of those hold the call transparently falls back to the wave-1 arena path (a fresh per-draw bind group) so clipped / masked draws stay bit-exact.
| slot | Persistent residence for this entity's fill or stroke encode. Must outlive the frame's submit (it does: the ECS component is only removed on geometry change, after the prior frame submitted). |
| encoded | Cached encode (stable across frames). Must be non-empty for a draw to land. |
| color | Solid fill color (NOT premultiplied - premultiplied here, identically to fillPath). |
| rule | Fill rule. |
| frameId | Monotonic frame index. A slot serves at most one resident draw per frame (its single uniform buffer cannot hold distinct per-instance uniforms); repeat draws in the same frame fall back to the arena path. |
| void donner::geode::GeoEncoder::finish | ( | ) |
Submit all encoded commands to the GPU queue.
After this call, the encoder is in a "finished" state and no further draws can be issued. The caller is responsible for any synchronization (e.g., MapAsync + Tick loop) needed to actually use the rendered output.
| void donner::geode::GeoEncoder::setAntialias | ( | bool | antialias | ) |
Enable analytic edge anti-aliasing for subsequent path and mask draws. Disabled mode emits binary coverage at the pixel center.
| antialias | True to retain analytic edge coverage. |
| void donner::geode::GeoEncoder::setBufferPool | ( | GeodeBufferPool * | pool | ) |
Install a cross-frame buffer pool (design doc 0030 M1).
When set, arena buffer growth prefers recycled buffers from the pool and the destructor returns the fully-grown arena buffers to it instead of destroying them, driving steady-state per-frame bufferCreates toward zero.
Call before the first draw. The pool (typically owned by RendererGeode::Impl) must outlive this encoder. Pass nullptr to disable (default).
| void donner::geode::GeoEncoder::setClipMask | ( | const wgpu::Texture & | maskTexture, |
| const wgpu::TextureView & | maskView ) |
Preferred overload: sets both the view AND the parent texture so the encoder keeps the underlying Vulkan resource alive for as long as it's bound.
The 1-arg overload is kept only for call sites that guarantee the parent's lifetime by other means.
See the activeClipMaskTexture field comment (and issue #551) for the bug this addresses - without the parent keepalive, popping the clip stack can free a VkImage while the encoder's createBindGroup still references its view.
| void donner::geode::GeoEncoder::setClipMask | ( | const wgpu::TextureView & | maskView | ) |
Bind maskView as the clip mask texture for subsequent fill / gradient draws.
The view must reference a 1-sample RGBA8Unorm texture the same size as the encoder's target - typically the resolve texture produced by beginMaskPass + endMaskPass.
The shader samples .r at the pixel center and multiplies it into fragment coverage, so the mask represents a pre-rendered clip region in [0, 1].
| void donner::geode::GeoEncoder::setClipPolygon | ( | const Vector2d | corners[4] | ) |
Activate a convex 4-vertex clip polygon (Phase 3a).
Unlike setScissorRect, this clips to the exact parallelogram described by the 4 corners - used for <symbol> / <svg> / <use> viewports that have a non-axis-aligned ancestor transform where WebGPU's rectangular scissor can only express the AABB of the transformed rect, not the true polygon. The fragment shader tests each of 4 edge half-planes at the fragment position and discards fragments outside the polygon.
| corners | 4 polygon vertices in target-pixel space, given in consistent (clockwise OR counter-clockwise) winding order. The encoder normalises edge normals so a fragment strictly INSIDE the polygon satisfies every half-plane test. |
| void donner::geode::GeoEncoder::setLoadPreserve | ( | ) |
Switch the next render pass's load op from Clear to Load, preserving whatever the target texture already contains.
Useful when the encoder is being reused to append draws on top of previously submitted content (e.g., resuming outer-frame drawing after a nested pattern-tile pass).
Must be called before any draws - once a render pass is open it's too late to change its load op.
| void donner::geode::GeoEncoder::setScissorRect | ( | int32_t | x, |
| int32_t | y, | ||
| int32_t | w, | ||
| int32_t | h ) |
Set a scissor rectangle in target-pixel coordinates.
Subsequent draws are clipped to the intersection of (0,0,targetWidth,targetHeight) and this rectangle. Used by RendererGeode::pushClip to implement SVG viewport rect clipping (nested <svg> clip, overflow: hidden, etc.).
The scissor persists across setTransform, fillPath, and friends until explicitly cleared via clearScissorRect.
Negative x/y and widths/heights that extend past the target are clamped internally; the caller can pass any AABB in pixel space without bounds-checking.