Background render worker with foreground document and low-priority thumbnail lanes. More...
#include "donner/editor/AsyncRenderer.h"
Classes | |
| struct | GpuWaitFailure |
| Device-health outcome the render worker observed, independent of any frame it produced. More... | |
Public Types | |
| enum class | CompletionBarrier : std::uint32_t { Off , Requested , Armed , Holding } |
| State of the test-only completion barrier; see requestCompletionBarrierForTesting. More... | |
Public Member Functions | |
| AsyncRenderer (AsyncRendererStartMode startMode=AsyncRendererStartMode::Immediate) | |
| Construct the worker, starting its thread unless deferred. | |
| AsyncRenderer (const AsyncRenderer &)=delete | |
| AsyncRenderer & | operator= (const AsyncRenderer &)=delete |
| AsyncRenderer (AsyncRenderer &&)=delete | |
| AsyncRenderer & | operator= (AsyncRenderer &&)=delete |
| void | start () |
| Start the render worker after its borrowed main-thread dependencies are ready. | |
| bool | workerStartedForTesting () const |
| True when this instance currently owns a joinable/running render worker. | |
| void | shutdown () |
| Stop accepting work, cancel both priority lanes, detach the wake callback, and join the worker. | |
| bool | isBusy () const |
| Returns true while a render/result or document-reading cache warmup owns the input gate. The UI thread must not touch the Renderer or mutate the SVGDocument while this returns true. | |
| bool | isBusyWithIdleWake () |
| Like isBusy, and when busy also registers for a wake so a deferring caller need not poll. | |
| bool | isFontResourceAdoptionSafe () const |
| True only when no document, warmup, or auxiliary preview can observe a changed font store. Pending results must be consumed before adoption so they retain their original font epoch. | |
| bool | hasRenderInFlightForTesting () const |
| Returns true only while the worker may still be computing or cancelling a render. Unlike isBusy(), a staged result waiting in DoneState is not in flight. | |
| bool | hasPendingRenderForTesting () const |
| Whether a render request is still queued rather than owned by the worker. | |
| bool | waitUntilNoRenderInFlightForTesting (std::chrono::steady_clock::time_point deadline) |
| Wait until no worker render is actively in flight. | |
| bool | compositorUsesRetainedDocumentForTesting () const |
| Whether the compositor's document reference is bound to the renderer-owned retained value. A compositor bound to a request-local RenderLease becomes dangling before deferred warmup. | |
| void | setReplayRenderDelayForTesting (std::chrono::milliseconds delay) |
| Inject a fixed delay into each worker render attempt for replay tests. | |
| void | setReplayRenderDelayIgnoresCancellationForTesting (bool ignore) |
| Keep the replay render delay running after a cancel request, as raster work keeps the document until it reaches its next cancellation check, so input can arrive while the worker holds it. | |
| void | setReplayDocumentAccessBlockedForTesting (bool blocked) |
| Pause queued renders before they acquire the document, for deterministic handoff replays. Shutdown always releases this pause. | |
| void | setReplayResultHoldFramesForTesting (int frameCount) |
| Hold each staged result for a fixed number of poll attempts in replay tests. | |
| void | releaseReplayResultHoldForTesting () |
| End replay-only delivery holds for the staged result and later results. Leaves any staged result intact for normal polling and wakes the owner outside the mutex. | |
| void | setWithholdCompositorTilesForTesting (bool withhold) |
| Drop every compositor tile a completed render produced, as a render whose tile payloads all failed would, so tests can drive the result that carries nothing to present. | |
| void | stageCompositorWarmupForTesting (bool pending, bool active) |
| Install a synthetic low-priority warmup state for document-access gate tests. | |
| void | completeCompositorWarmupForTesting () |
| Simulate the active warmup releasing its document guard. | |
| std::uint64_t | replayResultHoldPollCountForTesting () const |
| Number of poll attempts that intentionally withheld a staged result for replay tests. | |
| void | requestRender (const RenderRequest &request) |
| Post a render request to the worker. Non-blocking. If the worker is busy, this cancels the in-flight render at the next compositor safe point and replaces the pending request slot with the latest request. | |
| void | cancelInFlight () |
| Cancel an in-flight render without posting a replacement. Use when the current render is dispensable and the UI needs the worker to become idle before dispatching registry-touching input. Safe to call from any thread. | |
| std::uint64_t | cancelledRenderCount () const |
| Count of renders that were cancelled mid-flight by a subsequent requestRender. Exposed for tests to assert preemption is engaging (vs. the worker silently queueing requests). Incremented under the internal mutex; safe to read from any thread. | |
| std::optional< RenderResult > | pollResult () |
| If a render has completed since the last call, returns the resulting bitmap and transitions the worker back to idle. Returns std::nullopt if no render is pending-ready (either still busy or idle with nothing to hand off). | |
| void | discardUnpresentedResult (const RenderResult &result) |
| Forget payload identities from a polled result that the presenter discarded. | |
| void | noteOverviewPresentedAsActive (const RenderResult &result) |
| Record an overview replacing the active split tile set as a full-canvas fallback. | |
| bool | requestSampleThumbnail (SampleThumbnailRenderRequest request) |
| Queue one low-priority SVG thumbnail on this renderer's existing worker. | |
| std::optional< SampleThumbnailRenderResult > | pollSampleThumbnailResult () |
| Poll one completed sample-thumbnail result without changing main-document busy state. | |
| void | cancelSampleThumbnailWork () |
| Drop queued/unpolled sample-thumbnail work and cancel an active attempt. | |
| SampleThumbnailRenderStats | sampleThumbnailRenderStats () const |
| Snapshot low-priority worker counters and slot state. | |
| void | setSampleThumbnailRenderDelayForTesting (std::chrono::milliseconds delay) |
| Inject a cancellation-aware delay before thumbnail parsing for deterministic priority tests. | |
| void | setSampleThumbnailRendererCreationPlanForTesting (int requestNumber, std::chrono::milliseconds delay) |
| Recreate and pause one numbered offscreen renderer construction for browser handoff tests. | |
| void | setWakeCallback (std::function< void()> callback) |
| Install a callback that the worker thread invokes when a render result or cancellation completes. Used by the editor's on-demand render loop to wake the UI thread (e.g. via glfwPostEmptyEvent) so fresh results or newly-idle deferred input get picked up without continuous polling. | |
| void | setIdleMaintenance (std::function< void()> poll, std::function< bool()> hasWork) |
| Owner-thread GPU maintenance for a renderer that has stopped drawing. The worker runs poll before sleeping and on short timed wakes only while completed work may free resources. | |
| void | requestIdleMaintenance () |
| Wake the worker when another thread retires a handle to its GPU context. | |
| void | setTightBoundedSegmentsEnabled (bool enabled) |
| Toggle whether the compositor uses tight-bounded segment rasterization. The change applies at the start of the next worker iteration - renderFrame calls CompositorController::setTightBoundedSegmentsEnabled before compositing, which marks all cached segments dirty so the flip takes full effect that frame. | |
| bool | tightBoundedSegmentsEnabled () const |
| Mirror of the current toggle state. UI reads this to render the correct check state in the View menu without racing the worker. | |
| void | setCompositedRenderingMode (CompositedRenderingMode mode) |
| Select how the worker uses the compositor: full compositing, cached layers for filters only, or no compositor at all (direct full-document renders). See CompositedRenderingMode. The change applies at the start of the next worker iteration: entering Off destroys the live CompositorController; leaving Off or changing between the other modes reconstructs it with the matching CompositorConfig. Every mode produces identical pixels. | |
| CompositedRenderingMode | compositedRenderingMode () const |
| Mirror of the current mode. UI reads this to render the correct radio state in the View menu without racing the worker. | |
| void | setGeometryDebugOverlayEnabled (bool enabled) |
| Toggle the Geode geometry debug overlay (RendererInterface::setDebugGeometryOverlay) on the root document renderer. The change applies at the start of the next worker iteration. Each state transition clears retained compositor state once. While enabled, selection promotion/prewarm remains suppressed and every render presents one flat full-document root frame so retained tiles cannot crop or cover the frame-final wireframe. Disabling performs one transition reset, then normal retained promotion resumes. | |
| std::uint64_t | compositorResetCountForTesting () const |
| Number of times the worker has called CompositorController::resetAllLayers() since construction. Tests use this to assert that frame-version mutations do not masquerade as document replacements. | |
| std::uint64_t | compositorReconstructCountForTesting () const |
| Number of times the worker has reconstructed compositor_ from scratch. First construction counts as one. Tests use this to verify that structural remaps preserve cached layer state across drag-writeback reparses. | |
| svg::compositor::CompositorController::FastPathCounters | compositorFastPathCountersForTesting () const |
| Snapshot of the compositor's fast-path counters. Read-only - the worker writes them under the mutex when transitioning to Done. Returns zeros before the compositor is constructed (first render not yet requested). UI-thread safe. | |
| svg::compositor::CompositorController::RenderFrameStats | compositorRenderFrameStats () const |
| Snapshot of the worker compositor's immediate-vs-cached raster costs from the latest completed render. | |
| GpuWaitFailure | gpuWaitFailure () const |
| Snapshot of the worker's GPU-wait failure record. UI-thread safe. | |
| void | noteGpuWaitOutcomeForTesting (const svg::RendererReadbackStats &readbackStats) |
| Fold one render attempt's device-health stats into the failure record, as the worker does at the end of every iteration. Tests use this to pin the record's deduplication without hanging a real GPU. | |
| std::vector< svg::compositor::CompositorController::LayerInspectorRow > | compositorLayerInspectorRows () const |
| Snapshot of the compositor's per-layer diagnostic rows. Captured under the worker mutex at every Done transition; the UI thread copies the cached vector out under the lock. Empty before the first render lands or when the compositor isn't instantiated. | |
| std::vector< svg::compositor::CompositorController::SegmentInspectorRow > | compositorSegmentInspectorRows () const |
| Snapshot of the compositor's per-segment diagnostic rows. Same capture point and locking as the per-layer rows. | |
| std::vector< svg::compositor::CompositorController::CompositeTileSnapshot > | compositorCompositeTiles () const |
| Unified in-paint-order snapshot of every tile the compositor samples to produce the final composite. The editor's layer-inspector panel renders this list with thumbnails for every tile so the operator can see the comprehensive composite at a glance. | |
| svg::compositor::CompositorController::StateSnapshot | compositorState () const |
| Compositor-wide diagnostic state: active-hints count, layer count, split-path active flag, drag-target entity, canvas size. The editor's layer-inspector panel renders this as a state header so the operator can spot mismatches between the editor's idea of the drag target and the compositor's. | |
| Entity | workerCompositorEntity () const |
| The worker's current view of which entity is promoted. Read on the UI thread; the worker updates it under the same mutex as the other snapshot fields when transitioning to Done. entt::null when the worker hasn't promoted anything (e.g. promotion was refused). Compare against the editor's selection to spot races between the editor's selectedEntity and what the compositor actually holds. | |
| Vector2i | lastDocumentCanvasSize () const |
| Output raster size from the worker's last-completed render. This is the presentation epoch: at high zoom it can be smaller than the SVG document's semantic canvas size because the worker rendered only the visible viewport plus margin. | |
| void | setCompositorDiagnosticsEnabled (bool enabled) |
| Enable expensive compositor inspector snapshots for the developer panel. Scalar render timing and fast-path counters remain available when disabled. | |
Static Public Member Functions | |
| static CompletionBarrier | requestCompletionBarrierForTesting () |
| Request a test-only barrier between a finished foreground render and its result. | |
| static void | armRequestedCompletionBarrierForTesting () |
| Arm a requested completion barrier; without a request this does nothing. | |
| static CompletionBarrier | releaseCompletionBarrierForTesting () |
| Turn the completion barrier off, letting a held render publish its result. | |
| static CompletionBarrier | completionBarrierForTesting () |
| Current completion barrier state. | |
Background render worker with foreground document and low-priority thumbnail lanes.
| struct donner::editor::AsyncRenderer::GpuWaitFailure |
Device-health outcome the render worker observed, independent of any frame it produced.
A bounded GPU wait that burns its deadline declares the device lost and ends the worker iteration with nothing to hand over, so the per-frame diagnostics a result carries are never published. That makes the most severe renderer failure the only one that leaves no trace in the frame stats. The worker records the outcome here instead, so a poller can report it without a frame ever landing.
| Class Members | ||
|---|---|---|
| bool | deviceLost = false | True once the worker's backend device has been declared lost. Sticky. |
| uint64_t | generation = 0 | Bumped whenever the record above changes. A poller compares it against what it last reported so a sticky loss is reported once rather than on every frame. |
| int | timedOutWaitMs = 0 | Wall time that wait spent before giving up, in milliseconds. |
| GpuWaitTimeoutSite | timedOutWaitSite = svg::GpuWaitTimeoutSite::None | Which bounded GPU wait declared that loss, when a deadline did. |
|
strong |
State of the test-only completion barrier; see requestCompletionBarrierForTesting.
| Enumerator | |
|---|---|
| Off | Every render publishes its result as soon as it finishes. |
| Requested | Armed by the next armRequestedCompletionBarrierForTesting call. |
| Armed | The next render that finishes with a result to publish will wait. |
| Holding | A finished render is waiting, unpublished and without its completion wake. |
|
explicit |
Construct the worker, starting its thread unless deferred.
| startMode | Whether to start immediately or wait for an explicit start call. |
|
inlinenodiscard |
Number of times the worker has reconstructed compositor_ from scratch. First construction counts as one. Tests use this to verify that structural remaps preserve cached layer state across drag-writeback reparses.
Safe to read from the UI thread; incremented under the internal mutex on the worker.
|
inlinenodiscard |
Number of times the worker has called CompositorController::resetAllLayers() since construction. Tests use this to assert that frame-version mutations do not masquerade as document replacements.
Counts resets driven by a request.documentGeneration mismatch and geometry-debug state transitions; not the implicit reset performed on first compositor construction.
Safe to read from the UI thread; incremented under the internal mutex on the worker.
| void donner::editor::AsyncRenderer::discardUnpresentedResult | ( | const RenderResult & | result | ) |
Forget payload identities from a polled result that the presenter discarded.
Call immediately after polling, before posting another render. Retained GPU tiles are unchanged.
| result | The most recently polled, unpresented result. |
|
nodiscard |
Like isBusy, and when busy also registers for a wake so a deferring caller need not poll.
Foreground renders and cancellations wake when they finish, and a staged result woke when it was staged; the caller opens that gate itself with its next pollResult. Speculative compositor warmup wakes only for a caller registered here or by cancelInFlight. A caller that polls results every frame before this check can therefore wait for the next wake instead of requesting a frame every frame, which would redraw at display rate for the whole render.
| void donner::editor::AsyncRenderer::noteOverviewPresentedAsActive | ( | const RenderResult & | result | ) |
Record an overview replacing the active split tile set as a full-canvas fallback.
Call after presentation and before posting another render.
| result | A previously polled overview with a valid full-canvas preview. |
|
static |
Turn the completion barrier off, letting a held render publish its result.
|
static |
Request a test-only barrier between a finished foreground render and its result.
The next armRequestedCompletionBarrierForTesting call arms it; the editor makes that call when a sample becomes the active document, so only renders of that document can be held. The first armed render that finishes with a result to publish then waits in Holding until releaseCompletionBarrierForTesting: no frame can poll its result and its completion wake has not fired. A held render that is cancelled, superseded or shut down publishes nothing, so it passes and the barrier stays armed for the next render. A browser test uses this to stop driving frames and let them drain before a result can exist. The barrier is process-wide so a browser test can reach it without a renderer instance; product code never requests it.
|
nodiscard |
Queue one low-priority SVG thumbnail on this renderer's existing worker.
The lane has exactly one slot spanning pending, active, and completed-but-unpolled work. Main document renders always take priority and cancel an active thumbnail at the next safe point.
|
inline |
Select how the worker uses the compositor: full compositing, cached layers for filters only, or no compositor at all (direct full-document renders). See CompositedRenderingMode. The change applies at the start of the next worker iteration: entering Off destroys the live CompositorController; leaving Off or changing between the other modes reconstructs it with the matching CompositorConfig. Every mode produces identical pixels.
Same threading contract as setTightBoundedSegmentsEnabled: safe to call from the UI thread while a render is in flight.
|
inline |
Toggle the Geode geometry debug overlay (RendererInterface::setDebugGeometryOverlay) on the root document renderer. The change applies at the start of the next worker iteration. Each state transition clears retained compositor state once. While enabled, selection promotion/prewarm remains suppressed and every render presents one flat full-document root frame so retained tiles cannot crop or cover the frame-final wireframe. Disabling performs one transition reset, then normal retained promotion resumes.
Same threading contract as setTightBoundedSegmentsEnabled: safe to call from the UI thread while a render is in flight.
| void donner::editor::AsyncRenderer::setReplayRenderDelayForTesting | ( | std::chrono::milliseconds | delay | ) |
Inject a fixed delay into each worker render attempt for replay tests.
| delay | Delay duration. Negative durations are clamped to zero. |
|
inline |
Keep the replay render delay running after a cancel request, as raster work keeps the document until it reaches its next cancellation check, so input can arrive while the worker holds it.
| ignore | True to finish every later delay even after cancellation. |
| void donner::editor::AsyncRenderer::setReplayResultHoldFramesForTesting | ( | int | frameCount | ) |
Hold each staged result for a fixed number of poll attempts in replay tests.
| frameCount | Number of poll attempts to withhold a newly staged result. |
|
inline |
Toggle whether the compositor uses tight-bounded segment rasterization. The change applies at the start of the next worker iteration - renderFrame calls CompositorController::setTightBoundedSegmentsEnabled before compositing, which marks all cached segments dirty so the flip takes full effect that frame.
Safe to call from the UI thread while a render is in flight; the flag is stored in an std::atomic<bool>, and the worker reads it at a well-defined point in each iteration.
| void donner::editor::AsyncRenderer::setWakeCallback | ( | std::function< void()> | callback | ) |
Install a callback that the worker thread invokes when a render result or cancellation completes. Used by the editor's on-demand render loop to wake the UI thread (e.g. via glfwPostEmptyEvent) so fresh results or newly-idle deferred input get picked up without continuous polling.
The callback runs on the worker thread. It must be thread-safe and must NOT re-enter the renderer - a simple wake-up post into the window's event queue is the intended use.
|
inline |
Drop every compositor tile a completed render produced, as a render whose tile payloads all failed would, so tests can drive the result that carries nothing to present.
Unlike a real failure, the compositor's own tiles stay valid and clean; only the published result omits them.
| withhold | True to withhold the tiles of every later render, false to publish them again. |
| void donner::editor::AsyncRenderer::shutdown | ( | ) |
Stop accepting work, cancel both priority lanes, detach the wake callback, and join the worker.
Safe to call more than once from the owning thread. The destructor calls this automatically; owners with borrowed worker dependencies may call it earlier to control teardown order.
| void donner::editor::AsyncRenderer::start | ( | ) |
Start the render worker after its borrowed main-thread dependencies are ready.
Native construction starts immediately for existing standalone users. Browser editor startup calls this explicitly after synchronous main-thread WebGPU initialization, because Safari cannot safely interleave another device's Promise completion with an Asyncify readback. Repeated calls and calls after shutdown are no-ops.
|
nodiscard |
Wait until no worker render is actively in flight.
| deadline | Steady-clock deadline for the bounded wait. |