Repository navigation
Container: opt-in texture caching for a static subtree, so it costs one quad instead of N draws #1655
Copy link
Copy link
Open
Description
A Container whose children do not change still pays the full per-child cost every frame: transform resolution, bounds work, batching and draw calls. A UI panel, a parallax backdrop, a decorative tilemap chunk or a crowd of static props all redraw from scratch on every tick to produce identical pixels.
The proposal is an opt-in flag that renders such a subtree once into an offscreen texture and then draws it as a single quad until something actually changes.
We already own every piece
This is an ergonomics and lifecycle layer over machinery that exists:
- Offscreen targets —
src/video/rendertarget/render_target_pool.jsandwebglrendertarget.json the GL side,WebGPURenderer.setRenderTarget()(webgpu_renderer.js:869) on the other. - A closely related path already in use — a renderable carrying
postEffects(renderable/renderable.js:329) is already rendered into an offscreen target each frame and composited back. Texture caching is that same flow with one difference: the offscreen result is kept across frames instead of being rebuilt every time. Whatever is added here should reuse that path rather than open a second one. Texture2dand capture —renderer.toFrameTexture({ region })returns a GPU-residentTexture2don all three backends.- An invalidation signal —
Container.isDirtyis already maintained on add/remove, re-sort and transform changes, andcontainer.js:1246ORs in each child'supdate()return, so a sprite advancing an animation frame already propagates upward.
Proposed shape
container.cacheAsTexture = true, off by default. When enabled:
- on the next draw, render the subtree once into a pooled target sized to the container's bounds
- thereafter draw one textured quad
- re-render when
isDirtyis set, then clear it
What needs deciding
- Invalidation beyond
isDirty. It covers structure, transform and anything whoseupdate()returns true. Content that mutates without that — aTextwhose string is set directly, a canvas texture repainted in place — needs an explicitinvalidateCache(), and the JSDoc has to be blunt about it. This is the failure mode that will look like "my UI froze". - Sizing. Bounds changes and device pixel ratio both force a re-render at a new size; a container larger than
MAX_TEXTURE_SIZEmust fall back to uncached drawing rather than fail. - Pointer events. Hit testing must keep using the real children, not the quad, or every cached container becomes one big hit box.
floatingchildren and the transparent queue. A cached subtree flattens ordering decisions that those two make per draw; either exclude them from caching or define the interaction.- Memory. Targets come from the pool and must be released on
cacheAsTexture = false, ondestroy(), and on context loss. - Is the flag on
Containeror onRenderable?Containeris the useful case; putting it onRenderableis broader but only meaningful where there are children to collapse.
Acceptance
- draw calls for a static cached subtree are constant regardless of child count
- a cached container renders pixel-identical to the same container uncached
- add / remove / move / animate a child, or resize the container, and the next frame reflects it
- disabling the flag, destroying the container, or losing the context releases the target
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity