Adaptive resize
Resizing swaps the render target rather than reconfiguring the swapchain. The
mode is chosen per frame by g.pendingResize.
| State | Target | Cost |
|---|---|---|
Steady (pendingResize === null) | Swapchain, direct | None |
| Dragging | g.offscreenTexture at live viewport size, blitted to the swapchain | One pass + one sampler op per pixel |
| Settle | canvas.width/height assigned once, swapchain rebuilt | One reconfigure |
Settle fires after SETTLE_FRAMES consecutive RAF ticks with no new
ResizeObserver event. A new event resets the counter.
What it solves
Section titled “What it solves”- Safari white screen. The swapchain is never reconfigured mid-drag.
- Constant point size. The offscreen renders 1:1 with the viewport, so shader pixel units stay visually fixed as the window changes.
- Sharpness at rest. The swapchain is rebuilt to viewport size at settle, so steady state is 1:1 again.
- Glass during a drag.
currentTextureis an alias for whichever target is active, and the background copy and sample chain read that alias.
Do not
Section titled “Do not”- Always render direct to the swapchain — returns the Safari white screen.
- Always lock
canvas.width/height— point size scales with the viewport and quality degrades as the window grows. - Debounce
canvas.width/heightinstead — the scene visibly freezes until settle. The offscreen keeps frames flowing. - Reconfigure the context mid-drag — breaks Chrome and Safari in different ways.
If Safari goes white, point size scales, or the scene freezes during a drag, check whether one of these crept back in.
Where it lives
Section titled “Where it lives”core/gpuState.ts—offscreenTexture,offscreenView,blitPipeline,blitBindGroup,blitSampler,pendingResizecore/engine.ts—ResizeObserverwritespendingResizeand never touchescanvas.width/height; the RAF loop counts idle frames and commits at settlerender/renderer.ts— picks the target, sizes depth and background copies from it, blits at end of framerender/blit.ts— builds the fullscreen-triangle blit pipeline, lazily