|
|
Donner 0.8.0-pre
Embeddable browser-grade SVG2 engine
|
Controls compositor layer promotion/demotion and orchestrates composited rendering. More...
#include "donner/svg/compositor/CompositorController.h"
Classes | |
| struct | PromoteResult |
| Result of requesting an editor-facing compositor presentation plan. More... | |
| struct | FastPathCounters |
| Diagnostic counters for the translation-only fast path. Tests read these to assert that a drag is taking the fast path every frame, not falling through to prepareDocumentForRendering. More... | |
| struct | LayerInspectorRow |
| One row of diagnostic state per active compositor layer, intended for the editor's read-only layer-inspector panel (design doc 0033 M1). Cheap to build, cheap to copy: a few ints and one short string per layer. More... | |
| struct | SegmentInspectorRow |
| One row of diagnostic state per static segment (the non-promoted content between promoted layers, plus the pre-first and post-last slots). Sized N+1 where N is layerCount(). More... | |
| struct | StaticSpanPlan |
| Conservative draw-cost plan for one static segment slot. More... | |
| struct | CompositeTileSnapshot |
| One row of the unified "everything composited together" view that the layer-inspector panel renders in paint order — design doc 0033 §M1++. Mirrors what composeLayers actually draws so the operator sees the same sequence of blits the renderer performs. More... | |
| struct | RenderFrameStats |
| Worker render costs from the most recent renderFrame call, split by whether the work was caused by immediate-mode transient spans or retained cached tiles. More... | |
| struct | StateSnapshot |
Public Types | |
| enum class | SnapshotThumbnails : uint8_t { Include , Omit } |
| Whether diagnostic snapshots should synthesize CPU thumbnails. | |
| enum class | PromoteRefusalReason : uint8_t { None = 0 , InvalidEntity , LayerLimit , MemoryLimit , DescendantPromoted } |
| Compositor-wide state useful for diagnosing why the editor's expected drag fast path didn't engage. Lets the operator confirm at a glance: More... | |
Public Member Functions | |
| CompositorController (SVGDocument &document, RendererInterface &renderer, CompositorConfig config={}) | |
| Construct a compositor controller. | |
| const CompositorConfig & | config () const |
| Returns the runtime config this controller was constructed with. | |
| ~CompositorController () | |
| Destructor. | |
| CompositorController (const CompositorController &)=delete | |
| CompositorController & | operator= (const CompositorController &)=delete |
| CompositorController (CompositorController &&) noexcept | |
| CompositorController & | operator= (CompositorController &&) noexcept |
| PromoteResult | promoteEntity (Entity entity, InteractionHint interactionKind=InteractionHint::ActiveDrag) |
| Promote an entity to its own compositor layer. | |
| void | demoteEntity (Entity entity) |
| Demote a previously promoted entity back to the root layer. | |
| bool | isPromoted (Entity entity) const |
| Returns true if the given entity is currently promoted to its own layer. | |
| Transform2d | layerComposeOffset (Entity entity) const |
| Returns the current bitmap-compose offset for a promoted entity's layer, or identity if the entity is not promoted or has no cached bitmap. | |
| void | renderFrame (const RenderViewport &viewport) |
| Prepare and render a composited frame. | |
| void | renderFrame (const RenderViewport &viewport, const Transform2d &surfaceFromCanvas) |
Prepare and render a composited frame into viewport after applying surfaceFromCanvas to canvas-space content. | |
| bool | renderFrame (const RenderViewport &viewport, CancellationToken &token) |
Design doc 0033 §M4 — cancellable variant. The token is polled at coarse safe points (between rasterizeLayer / segment rasterize calls) and renderFrame returns early when set. The compositor's internal dirty flags are left intact for the work the early return skipped, so the next renderFrame picks up without re-doing already-rasterized layers / segments. | |
| bool | renderFrame (const RenderViewport &viewport, CancellationToken &token, const Transform2d &surfaceFromCanvas) |
| Cancellable variant of the camera-transform render path. | |
| size_t | layerCount () const |
| Returns the number of currently active layers (excluding the root layer). | |
| size_t | totalBitmapMemory () const |
| Returns the total memory used by all layer bitmaps, in bytes. | |
| SVGDocument & | document () const |
| Returns a reference to the underlying SVG document. | |
| FallbackReason | fallbackReasonsOf (Entity entity) const |
| Returns the fallback reasons for a promoted entity, or FallbackReason::None if not promoted. | |
| bool | hasSplitStaticLayers () const |
| Returns true when the compositor has cached a split underlay/overlay pair for drag preview. | |
| const RendererBitmap & | layerBitmapOf (Entity entity) const |
| Cached bitmap for the promoted entity, or an empty bitmap if unavailable. | |
| Vector2d | layerCanvasOffsetOf (Entity entity) const |
| Canvas-space top-left of the entity's cached layer bitmap. Zero() when the entity is not promoted, when its layer hasn't rasterized yet, or when the layer fell back to canvas-size rasterization (design doc 0033 §M2). The editor's CompositedPreview consumer blits the promoted texture at this offset (converted to doc units) plus the per-frame DOM drag delta. | |
| const FastPathCounters & | fastPathCountersForTesting () const |
| std::vector< LayerInspectorRow > | snapshotLayerInspectorRows (SnapshotThumbnails thumbnails=SnapshotThumbnails::Include) const |
| Build a per-layer snapshot of compositor state for the layer- inspector panel. Safe to call from the renderer worker thread when the compositor isn't mid-render (the editor calls it at the same Done-transition point as fastPathCountersForTesting). Allocates a vector and one short string per layer — fine for diagnostics, not hot-path-grade. | |
| std::vector< SegmentInspectorRow > | snapshotSegmentInspectorRows () const |
| Snapshot per-segment diagnostic state. Mirrors the per-layer snapshot but covers the static-segment cache, which dominates the per-frame rasterize cost on documents like the splash. Same invocation rules as snapshotLayerInspectorRows. | |
| std::vector< StaticSpanPlan > | snapshotStaticSpanPlansForTesting () const |
| Snapshot the most recent static span plans. Test-only diagnostics. | |
| std::vector< CompositeTileSnapshot > | snapshotCompositeTiles (SnapshotThumbnails thumbnails=SnapshotThumbnails::Include) const |
| Build the unified composite-tile snapshot in paint order. The sequence mirrors composeLayers: | |
| const RenderFrameStats & | lastRenderFrameStats () const |
| Return the current render-frame raster cost split. | |
| StateSnapshot | snapshotState () const |
| void | resetAllLayers (bool documentReplaced=false) |
| Clear all layers and cached state. | |
| bool | remapAfterStructuralReplace (const std::unordered_map< Entity, Entity > &remap) |
| Rewire the compositor's entity-keyed state (activeHints_, mandatoryDetector_, complexityBucketer_, layers_) from the old document's entity space onto a new one, after a structurally identical setDocument. | |
| void | setTightBoundedSegmentsEnabled (bool enabled) |
| Flip tight-bounded segment rasterization on or off at runtime. See CompositorConfig::tightBoundedSegments for semantics. Marks every cached static segment dirty so the next renderFrame call re-rasterizes under the new policy (otherwise the flip would affect only segments that happened to get re-rasterized for other reasons). | |
| bool | tightBoundedSegmentsEnabled () const |
| Returns the current tight-bounded-segments setting. Mirrors config().tightBoundedSegments for convenience. | |
| void | setSkipMainComposeDuringSplit (bool skip) |
| When true, renderFrame() skips the main-renderer compose step while the split-static-layers cache (bg/drag/fg triple) is populated. The editor's drag overlay reads those bitmaps directly via GL, so the per-frame drawImage calls into the main renderer are wasted work — on a 892×512 Skia backend with a few filter layers the skip saves ~100 ms per drag frame. The flat snapshot the editor uploads stays stale during drag but is only drawn after drag ends, by which point the caller must disable the skip for a settle render that refreshes it. | |
| std::vector< CompositorTile > | snapshotTilesForUpload (CompositorTileBitmapPayload payload=CompositorTileBitmapPayload::All) const |
| Enumerate every cacheable unit (static segments + promoted layer bitmaps) interleaved in paint order. Each tile carries a generation counter that advances only when the tile's pixel content was actually re-rasterized — so the editor can gate its GL texture uploads to the minimum set that actually changed this frame. On a click-to-drag, the user should observe at most 3 tiles advance: the two halves of the split segment and the new drag-target layer. All other filter layers, segments, and bucket layers keep their generation and their GL texture binding. | |
| const CompositorLayer * | findLayerForTest (Entity entity) const |
Read-only accessor for the layer bound to entity. Test-only — lets regression tests inspect canvasFromBitmap / bitmapEntityFromWorldTransform after a drag frame to verify the translation-only fast path engaged (bitmap stamp unchanged, composition transform carries the delta). Production callers should go through isPromoted / promoteEntity instead. | |
| void | flushPendingDemotionsForTesting () |
| Test-only: bypass the §M9 hysteresis window and process every pending demotion immediately. Provided so unit tests can keep using the "promote → demote → assert layer gone" pattern without having to render kDemotionHysteresisFrames frames in the middle. Production code calls happen via the normal renderFrame flow, which ages the queue one frame at a time. | |
Static Public Attributes | |
| static constexpr int | kLayerThumbnailMaxSide = 64 |
| Max side length (in pixels) of the thumbnail bitmap synthesized into each LayerInspectorRow::thumbnailPixels. The downsample preserves aspect ratio, so the smaller side is round(otherSide * shortSide /
longSide). 64 px keeps the memory cost trivial (16 KiB / layer max) while staying legible at the editor's right-pane width. | |
| static constexpr uint32_t | kDemotionHysteresisFrames = 30 |
| Frames the demotion waits before actually firing. ~0.5s at 60Hz — long enough to absorb the typical "click-deselect-click" rhythm (a few hundred ms), short enough that an actual commit-to-demote stays inside one human reaction time. Public so tests can drive renderFrame exactly the right number of times to observe the expiry transition. | |
Controls compositor layer promotion/demotion and orchestrates composited rendering.
The compositor splits the document into layers: one root layer (everything not promoted) and zero or more promoted layers (one per promoted entity subtree). The DOM is the sole source of truth for entity position: during a drag, callers mutate the entity's transform directly (element.setTransform(...)) and the compositor's fast path diffs the new absolute transform against the cached bitmap's rasterize-time transform. When the delta is a pure translation, the bitmap is reused and only the internal compose offset updates — no re-rasterization.
Usage:
| struct donner::svg::compositor::CompositorController::FastPathCounters |
Diagnostic counters for the translation-only fast path. Tests read these to assert that a drag is taking the fast path every frame, not falling through to prepareDocumentForRendering.
| struct donner::svg::compositor::CompositorController::LayerInspectorRow |
One row of diagnostic state per active compositor layer, intended for the editor's read-only layer-inspector panel (design doc 0033 M1). Cheap to build, cheap to copy: a few ints and one short string per layer.
| Class Members | ||
|---|---|---|
| Vector2i | bitmapSize = Vector2i::Zero() | Pixel size of the layer's cached bitmap. Vector2i::Zero() when the layer has not yet been rasterized (hasValidBitmap false). |
| Vector2d | canvasOffset = Vector2d::Zero() | Canvas-space top-left position where this layer's bitmap blits back. Vector2d::Zero() for canvas-sized layers; non-zero for intrinsic-sized layers (design doc 0033 §M2). |
| bool | dirty = false | Whether this layer is currently flagged dirty (a rasterize is pending on the next renderFrame call). |
| Entity | entity = entt::null | Root entity of the promoted subtree. |
| FallbackReason | fallbackReasons = FallbackReason::None | Raw fallback flags (handy for tests; the panel renders fallbackReasonsText for display). |
| string | fallbackReasonsText | Pre-formatted fallback flag list (e.g. "Filter | IsolatedLayer"). |
| uint64_t | generation = 0 | Monotonic generation counter from CompositorLayer::generation. |
| bool | hasValidBitmap = false | Whether the layer has a valid cached bitmap. |
| double | lastRasterizeMs = 0.0 | Wall-clock duration of the most recent rasterize, in ms. |
| uint32_t | layerId = 0 | Layer's stable numeric id (CompositorLayer::id). |
| uint32_t | rasterizeCount = 0 | Cumulative rasterize count from CompositorLayer::rasterizeCount. |
| Vector2i | thumbnailDims = Vector2i::Zero() | Pixel dimensions of the downsampled thumbnail. Vector2i::Zero() when the layer has no valid bitmap. Otherwise the longer side is kLayerThumbnailMaxSide and the shorter side preserves aspect. |
| vector< uint8_t > | thumbnailPixels | RGBA8 thumbnail pixels, tightly packed (thumbnailDims.x * 4 row stride), suitable for direct upload via glTexImage2D. Empty when the layer has no valid bitmap. |
| struct donner::svg::compositor::CompositorController::SegmentInspectorRow |
One row of diagnostic state per static segment (the non-promoted content between promoted layers, plus the pre-first and post-last slots). Sized N+1 where N is layerCount().
| Class Members | ||
|---|---|---|
| Vector2i | bitmapSize = Vector2i::Zero() | Pixel dimensions of the cached segment bitmap. Vector2i::Zero() when the slot has no bitmap yet. |
| Vector2d | canvasOffset = Vector2d::Zero() | Canvas-space top-left offset where this segment's bitmap blits back (non-zero only on the tight-bounded path, design doc 0027). |
| bool | dirty = false | Whether this slot is currently flagged dirty (a rasterize is pending on the next renderFrame call). |
| uint64_t | generation = 0 | Monotonic per-slot generation counter. |
| bool | hasValidBitmap = false | Whether the slot has a non-empty cached bitmap. |
| double | lastRasterizeMs = 0.0 | Wall-clock duration of the most recent rasterize, in ms. Zero when this slot has never been rasterized in the current session. |
| size_t | slotIndex = 0 | Slot index (0..N inclusive). Segment 0 is the pre-first-layer content; segment N is the post-last-layer content. |
| struct donner::svg::compositor::CompositorController::StaticSpanPlan |
Conservative draw-cost plan for one static segment slot.
| Class Members | ||
|---|---|---|
| Box2d | boundsCanvas | Snapped canvas-space bounds used for immediate eligibility. |
| bool | demotedDynamicImmediate = false | True when the span was dynamically immediate last frame but this render exceeded budget, so the freshly-rendered payload is retained as a cached tile instead of staying immediate. |
| bool | dynamicHeuristicImmediate = false | True when timing expanded the span into immediate presentation. |
| double | estimatedCacheOverheadCost = 0.0 | Relative fixed/cache memory cost avoided by immediate presentation. |
| int | estimatedDrawOps = 0 | Estimated number of direct geometry draws in the span. |
| int | estimatedPathVerbs = 0 | Estimated number of path verbs across direct geometry draws. |
| double | estimatedRedrawCost = 0.0 | Relative redraw cost from tight area and geometry complexity. |
| uint64_t | estimatedRetainedBytes = 0 | Estimated presentation texture bytes retained by a cached tile. |
| Entity | firstEntity = entt::null | First render instance covered by the span, or null for an empty slot. |
| bool | hasExpensiveEffect = false | True when the span uses effects or resources that force cached-tile presentation. |
| double | immediateBudgetChargeMs = 0.0 | Budget charged by this span when it is immediate. |
| double | immediateBudgetMs = 0.0 | Total dynamic immediate-span frame budget for 120 Hz interaction. |
| Entity | lastEntity = entt::null | Last render instance covered by the span, or null for an empty slot. |
| double | measuredRasterizeMs = 0.0 | Raster time from the most recent span render. |
| StaticSpanMode | mode = StaticSpanMode::CachedTile | Presentation mode selected for this span. |
| size_t | slotIndex = 0 | Slot index in the static segment array. |
| string | spanRangeLabel | Human-readable first/last element range covered by this span. |
| bool | staticHeuristicImmediate = false | True when the static cost heuristic chose immediate presentation. |
| bool | visible = false | True when the span has a visible, bounded contribution to the canvas. |
| struct donner::svg::compositor::CompositorController::RenderFrameStats |
Worker render costs from the most recent renderFrame call, split by whether the work was caused by immediate-mode transient spans or retained cached tiles.
| struct donner::svg::compositor::CompositorController::StateSnapshot |
| Class Members | ||
|---|---|---|
| uint32_t | activeHintsCount = 0 | Editor-driven explicit promotions (drag target + selection prewarm). Mandatory-detector hints don't count toward this number — they live in a separate map. |
| Vector2i | canvasSize = Vector2i::Zero() | Canvas size the compositor most recently rendered against. |
| Entity | lastPromoteRefusalEntity = entt::null | Entity the failed promoteEntity was called with. entt::null when lastPromoteRefusalReason is None. |
| PromoteRefusalReason | lastPromoteRefusalReason = PromoteRefusalReason::None | Reason the most-recent promoteEntity call returned false, or None when the most-recent call succeeded (or no call has happened yet). Sticky on failure. |
| uint32_t | layerCount = 0 | Total layers currently in layers_ (mandatory + explicit). |
| bool | splitPathActive = false | hasSplitStaticLayers() — the editor's drag-overlay fast path is active when this is true. |
| Entity | splitStaticLayersEntity = entt::null | Entity the compositor cached the bg/fg split for. entt::null when split-path is inactive. |
|
strong |
Compositor-wide state useful for diagnosing why the editor's expected drag fast path didn't engage. Lets the operator confirm at a glance:
| Enumerator | |
|---|---|
| InvalidEntity | Entity was destroyed / never existed in the registry. |
| LayerLimit | Too many entities already promoted (kMaxCompositorLayers). |
| MemoryLimit | Total bitmap memory already at kMaxCompositorMemoryBytes. |
| DescendantPromoted | A descendant already has its own promoted layer (typically a mandatorily-detected filter / mask / isolated-layer). Allowing the parent promote would double-rasterize the descendant. |
| donner::svg::compositor::CompositorController::CompositorController | ( | SVGDocument & | document, |
| RendererInterface & | renderer, | ||
| CompositorConfig | config = {} ) |
Construct a compositor controller.
| document | The SVG document to composite. |
| renderer | The renderer backend to use for rasterization and composition. |
| config | Runtime feature gates. Default-constructed enables everything. |
| void donner::svg::compositor::CompositorController::demoteEntity | ( | Entity | entity | ) |
Demote a previously promoted entity back to the root layer.
The entity's explicit compositor hint is removed, its computed layer assignment is updated, and the layer is destroyed. The root layer is marked dirty to include the demoted entity on the next render.
| entity | The entity to demote. No-op if not currently promoted. |
|
nodiscard |
Returns the fallback reasons for a promoted entity, or FallbackReason::None if not promoted.
| entity | The entity to query. |
|
nodiscard |
Returns true if the given entity is currently promoted to its own layer.
| entity | The entity to check. |
|
nodiscard |
Returns the current bitmap-compose offset for a promoted entity's layer, or identity if the entity is not promoted or has no cached bitmap.
The compose offset is the delta between the cached bitmap's rasterize-time world transform and the entity's current absolute world transform. Callers who draw the promoted layer's bitmap independently (e.g. the editor's split-layer display path) must apply this offset so the bitmap aligns with the bg/fg render the compositor just produced.
| entity | The entity to query. |
| PromoteResult donner::svg::compositor::CompositorController::promoteEntity | ( | Entity | entity, |
| InteractionHint | interactionKind = InteractionHint::ActiveDrag ) |
Promote an entity to its own compositor layer.
The entity and its subtree will be rasterized into a separate bitmap. During composition, the layer bitmap is blitted with its composition transform, avoiding re-rasterization of the rest of the scene.
Under CompositorConfig::autoPromoteInteractions (default on), this publishes an Interaction hint tagged with interactionKind. When the gate is off it falls back to an Explicit hint, ignoring interactionKind.
| entity | The entity to promote. |
| interactionKind | Semantic kind for the Interaction hint. Use Selection for selection-driven pre-warm (no drag in progress) and ActiveDrag for an active user drag. Defaults to ActiveDrag for callers that only use this API during drag. |
|
nodiscard |
Rewire the compositor's entity-keyed state (activeHints_, mandatoryDetector_, complexityBucketer_, layers_) from the old document's entity space onto a new one, after a structurally identical setDocument.
Interaction-layer bitmaps are preserved only when the remapped layer still has a pixel-exact reuse transform and raster rectangle; otherwise the affected layer is marked dirty for the next renderFrame(). This is the fast alternative to resetAllLayers(documentReplaced=true) for the editor's drag-end writeback round-trip through ReplaceDocumentCommand: with a structurally equal reparse, the compositor can swap ids and surgically re-rasterize only caches whose pixels no longer match the settled DOM.
| remap | Mapping from old entity id → new entity id. Every entity in activeHints_ and in each CompositorLayer (entity, firstEntity, lastEntity) must have an entry; detectors rebuild against the new registry so their hint set doesn't need remap entries. |
| void donner::svg::compositor::CompositorController::renderFrame | ( | const RenderViewport & | viewport | ) |
Prepare and render a composited frame.
This method:
| viewport | The viewport for the render pass. |
| bool donner::svg::compositor::CompositorController::renderFrame | ( | const RenderViewport & | viewport, |
| CancellationToken & | token ) |
Design doc 0033 §M4 — cancellable variant. The token is polled at coarse safe points (between rasterizeLayer / segment rasterize calls) and renderFrame returns early when set. The compositor's internal dirty flags are left intact for the work the early return skipped, so the next renderFrame picks up without re-doing already-rasterized layers / segments.
Returns true on full completion, false on early cancellation. The non-token overload above delegates with a no-op token.
| void donner::svg::compositor::CompositorController::renderFrame | ( | const RenderViewport & | viewport, |
| const Transform2d & | surfaceFromCanvas ) |
Prepare and render a composited frame into viewport after applying surfaceFromCanvas to canvas-space content.
| viewport | The output viewport for the render pass. |
| surfaceFromCanvas | Transform from document canvas coordinates to the output surface. |
| void donner::svg::compositor::CompositorController::resetAllLayers | ( | bool | documentReplaced = false | ) |
Clear all layers and cached state.
Two callers, two semantics:
After this call, layerCount() is 0 and all cached bitmaps are released. The next renderFrame() will do a full render.
| void donner::svg::compositor::CompositorController::setTightBoundedSegmentsEnabled | ( | bool | enabled | ) |
Flip tight-bounded segment rasterization on or off at runtime. See CompositorConfig::tightBoundedSegments for semantics. Marks every cached static segment dirty so the next renderFrame call re-rasterizes under the new policy (otherwise the flip would affect only segments that happened to get re-rasterized for other reasons).
Intended as a bisection knob for the editor: if a visual regression seems to originate in 0027-tight_bounded_segments, flip the toggle and watch whether it disappears. Not a hot path — re-rasterizing every segment on the next frame costs one full render's worth of work.
|
nodiscard |
Build the unified composite-tile snapshot in paint order. The sequence mirrors composeLayers: