The GPU engine (src/engine/WebGPUEngine.ts) is a thin facade that coordinates all rendering subsystems. Preview, scrubbing, and export run through the same WebGPU ping-pong compositor. Video textures are imported as GPUExternalTexture — zero-copy, no CPU roundtrip.
WebGPUContext
GPU adapter, device, canvas configuration, and device loss recovery.
RenderTargetManager
Ping-pong buffers, independent preview buffers, and effect temp textures.
CompositorPipeline
37 blend modes, 3D transforms, and inline color effects in a single WGSL shader per layer.
Allocates and owns all GPU render textures. All textures are rgba8unorm.
Texture
Purpose
Ping / Pong
Main compositing double-buffer
Independent Ping / Pong
Separate buffers for multi-composition preview
Effect Temp 1 / 2
Pre-processing source textures for complex effects
Black texture
1×1 fallback for empty frame output
createPingPongTextures() nulls texture references for GC instead of calling .destroy() to avoid Destroyed texture used in a submit warnings (VRAM leak fix).
All texture handling lives in src/engine/texture/TextureManager.ts and src/engine/texture/MaskTextureManager.ts.
Source
GPU type
Copy behavior
Caching
HTMLVideoElement
GPUExternalTexture (texture_external)
Zero-copy
Per-video last-frame + scrubbing cache
HTMLVideoElement on Firefox
texture_2d<f32>
Copied to persistent texture per frame
Reused per layer to avoid 30+ tex/sec
VideoFrame (WebCodecs)
GPUExternalTexture (texture_external)
Zero-copy
None — frame buffer managed by decoder
HTMLImageElement
texture_2d<f32> (rgba8unorm)
Copied once
By HTMLImageElement reference
HTMLCanvasElement (text clips)
texture_2d<f32> (rgba8unorm)
Copied once
By canvas reference
ImageBitmap (Native Helper)
texture_2d<f32> (rgba8unorm)
Re-uploaded per frame
Reused by layer ID
Firefox does not support importExternalTexture reliably for HTMLVideoElement. The htmlVideoPreviewFallback.ts path copies frames to a persistent texture_2d<f32> to avoid intermittent black frames.
~435 lines of WGSL are inlined in src/engine/pipeline/CompositorPipeline.ts (copy shader ~30 lines, external copy ~30 lines, external composite shader ~375 lines with all 37 blend modes). These are not in separate .wgsl files.
Three tiers of frame caching in src/engine/texture/ScrubbingCache.ts:
Tier
Purpose
Key
Capacity
Eviction
Tier 1 — GPU textures
Instant access during timeline scrub
videoSrc:quantizedTime (30fps)
300 frames (~10s at 30fps)
LRU via Map insertion order
Tier 2 — Last frame
Visible during seeks/pauses
HTMLVideoElement reference
1 per video
Overwrite
Tier 3 — RAM Preview
Fully composited frames for instant playback
Quantized time (30fps)
900 frames / 512 MB
LRU, frame count + memory limit
A separate GPU frame cache (max 60 textures) avoids CPU-to-GPU re-upload during RAM Preview playback.When the cache is warm, scrubbing does not decode at all.
Runtime toggles via window.__ENGINE_FLAGS__ in src/engine/featureFlags.ts:
flags = { useRenderGraph: false, // Render Graph executor (stubs — not ready) useDecoderPool: false, // Shared decoder pool (not wired yet) useFullWebCodecsPlayback: false, // Preview uses HTML video by default; // WebCodecs is used for export and full-mode only}
// Toggle in browser consolewindow.__ENGINE_FLAGS__.useFullWebCodecsPlayback = true
After 1 second of inactivity the render loop pauses (RAF stays alive for wake-up). Idle detection is suppressed until the first play event to keep video GPU surfaces warm after page reload.