Skip to content

Adaptive resize

Resizing swaps the render target rather than reconfiguring the swapchain. The mode is chosen per frame by g.pendingResize.

StateTargetCost
Steady (pendingResize === null)Swapchain, directNone
Draggingg.offscreenTexture at live viewport size, blitted to the swapchainOne pass + one sampler op per pixel
Settlecanvas.width/height assigned once, swapchain rebuiltOne reconfigure

Settle fires after SETTLE_FRAMES consecutive RAF ticks with no new ResizeObserver event. A new event resets the counter.

  • 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. currentTexture is an alias for whichever target is active, and the background copy and sample chain read that alias.
  • 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/height instead — 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.

  • core/gpuState.ts — offscreenTexture, offscreenView, blitPipeline, blitBindGroup, blitSampler, pendingResize
  • core/engine.ts — ResizeObserver writes pendingResize and never touches canvas.width/height; the RAF loop counts idle frames and commits at settle
  • render/renderer.ts — picks the target, sizes depth and background copies from it, blits at end of frame
  • render/blit.ts — builds the fullscreen-triangle blit pipeline, lazily