mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 03:17:05 -06:00
* docs: design bounded GPU stack prefix reuse * docs: advanced blending stays on the CPU; cache admits whole grid chunks and serves partial rects
79 lines
5.2 KiB
Markdown
79 lines
5.2 KiB
Markdown
# GPU stack prefix reuse
|
||
|
||
Design for #209, particularly the opacity, fill, blend-mode and visibility scenarios P2–P5.
|
||
The current compositor caches input pages and effect maps, but executes the complete pass list
|
||
over every damaged chunk. This proposal adds reusable lower-stack composites without changing
|
||
the blend equations, colour conversion or damage policy.
|
||
|
||
## Safe boundaries
|
||
|
||
The planner records a checkpoint after a complete root-level stack unit: one base layer and all
|
||
layers clipped to it. Nested groups, pass-through groups and adjustments finish through the
|
||
existing planner before this boundary. Only the accumulated backdrop is live there; restoring
|
||
it into the checkpoint's slot is sufficient to execute the remaining passes. No checkpoint is
|
||
placed inside a group, clipping unit or adjustment program.
|
||
|
||
Blend-If currently returns `Unsupported` from the GPU planner and uses the existing CPU path.
|
||
The same holds for Advanced Blending (on main since b0680fd): it must keep falling back to the
|
||
CPU, or keep the checkpoint boundary rule — a later GPU implementation of advanced blending
|
||
preserves the boundary invariant either way.
|
||
|
||
## Invalidation
|
||
|
||
Retain a COW snapshot of the checkpoint's lower layers and the document's non-layer state.
|
||
Compare the complete layer structures, including masks, nested children, effects, derived caches
|
||
and pixel surfaces. Compare all non-layer document fields conservatively, including colour mode,
|
||
depth, profile, global light and patterns. This deliberately accepts unnecessary invalidations
|
||
(for example selection or metadata changes) in exchange for avoiding an incomplete render key.
|
||
Shared `Arc<Tile>` data stays pinned and equality short-circuits for unchanged shared tiles;
|
||
there is no fingerprint collision or dependence on command IDs or declared damage being complete.
|
||
|
||
If a lower layer changes, choose an earlier root checkpoint before that unit, discard cached
|
||
chunks and render once to populate it. Subsequent edits above that checkpoint reuse it. Start
|
||
with the boundary before the last root unit. Validate the planner's starting layer, pass offset
|
||
and root slot too: an opaque layer above the checkpoint can change occlusion pruning.
|
||
|
||
Admit only whole grid chunks — rectangles aligned to the compositor's chunk grid — keyed by
|
||
their grid position, in the compositor's accumulation format. Partial damage is served, not
|
||
missed: a damage rectangle maps to the whole chunks it intersects, and a partial rectangle is
|
||
copied as a sub-rect of the cached whole chunk, so brush strokes (a new partial rect every
|
||
frame) populate whole chunks instead of filling the budget with entries keyed to exact damage
|
||
rects that never hit again. A lower-prefix change invalidates every chunk, including offscreen
|
||
ones.
|
||
Switching documents, closing the cached document, resizing, changing the texture limit or losing
|
||
the device releases or invalidates the cache. Undo/redo is covered by snapshot comparison.
|
||
|
||
## Memory and submission
|
||
|
||
Keep one prefix per compositor, with a bounded set of chunk textures. Limit it to 512 MiB and
|
||
reserve its allowance from the existing effect/cache part of `set_memory_budget`; do not reduce
|
||
the existing layer-page allowance or silently exceed the configured total. Stop admitting new
|
||
chunks when full so a repeated full-canvas refresh cannot evict its own next hits. Budget changes
|
||
trim the cache immediately. Tiny budgets and documents without useful boundaries render normally.
|
||
|
||
Copy the stored chunk into its accumulator slot, execute the suffix, and send the final root to
|
||
the existing display sink. Capture the checkpoint when its last prefix pass finishes. No readback
|
||
or lower-precision storage is introduced. Expose reused-chunk and skipped-pass counters and the
|
||
held byte count for tests and measurement.
|
||
|
||
Only `render` owns submission and can establish reuse. `encode` takes a caller-owned encoder,
|
||
which may be dropped without submission: it must neither read nor populate prefix checkpoints.
|
||
On a failed render discard prefix state; device-health checks still gate all GPU work.
|
||
|
||
## Validation and delivery
|
||
|
||
Before implementing, publish this design separately for review. The implementation is a focused
|
||
follow-up and references this document. Keep all existing public rendering results and CPU fallback
|
||
semantics intact. Compare warm cached output with both the uncached GPU path and CPU oracle, at
|
||
8/16/32-bit depths and simulated small texture limits. Cover top/middle/bottom edits, property
|
||
changes, clipping, pass-through and isolated groups, adjustments, masks, effects, global inputs,
|
||
occlusion, structural changes, undo-like snapshots, disjoint partial damage, budget shrink/zero,
|
||
document replacement and discarded encoders. Run existing GPU parity and real-file corpora.
|
||
|
||
Measure release rendering on dense 24–36 MP stacks, cold population and repeated property edits,
|
||
with reuse enabled and disabled in the same binary. Also run the existing P2–P5 scenarios before
|
||
and after; their styled fixture edits a low layer, so only the layers below that unit can be reused.
|
||
Report timings, actual skipped work and cache bytes. This cannot promise the 10–20 ms budgets:
|
||
all layers above the change still blend, and full-canvas output encoding/mips still run. Retain
|
||
published baselines until a representative full measurement justifies changing them.
|