import { Bounds, Renderer } from 'vega-scenegraph'; import type { ClipMaskTarget, GPUVegaCanvasContext, GPUVegaOptions, GPUVegaScene } from './types/context.js'; export default class WebGPURenderer extends Renderer { wgOptions: GPUVegaOptions; private _canvas; private _pickCanvas; private _pickContext; private _ctx; private _device; private _msaa; /** Whether the last frame had to draw off the frame. Read by the tests. */ private _offFrame; private _mask; private _layer; private _layerResolve; private _backdrop; private _offscreen; private _queue; private _renderCount; /** Reason the GPU device was lost, if it ever was. Set for every reason. */ deviceLostReason: string | null; /** Set to an object to accumulate per-mark draw time. Diagnostic only. */ markTimings: Record | null; /** Number of GPU devices this renderer has created. */ deviceGeneration: number; private _recoveries; private _capture; private _isRendering; private _pendingRender; private _gpuTimer; private _finalized; private _settling; private _settleTimer; private _lastRender; private _renderPromise; private _pendingPromise; private _resolvePending; private _dpr; private _scaleFactor; private _maxTextureDim; private _lockedRatio; private _warnedRatioCap; constructor(loader?: unknown); initialize(el: HTMLElement | null, width: number, height: number, origin: readonly number[], scaleFactor?: number, opt?: unknown): this; resize(width: number, height: number, origin: readonly number[], scaleFactor?: number): this; canvas(): HTMLCanvasElement | null; /** Stops following zoom, so the canvas keeps the size it has. */ private _unwatchPixelRatio; /** * Takes the real ceiling from the adapter in place of the assumed minimum, * and re-sizes when that changes what the canvas is allowed to be. */ private _applyTextureLimit; /** * Device pixels per logical pixel for the canvas, after holding it against * browser zoom when `redrawOnZoom` is off and capping it to what the GPU can * actually allocate. */ private _pixelRatio; /** * Redraws when the device pixel ratio changes, which browser zoom does * without changing the view's width or height, so nothing else asks for it. * A matchMedia query only fires for the ratio it was built with, so each * change registers the next one. */ private _watchPixelRatio; context(): GPUVegaCanvasContext | null; device(): GPUDevice | null; private _reinit; /** * Drops the device and everything built on it. Pipelines, textures and * buffers all belong to a device, so none of them outlive it. A texture slot * compares the device it was made on, so it rebuilds on the next one itself. */ private _dropDevice; /** * A device is never destroyed from here, so every loss is the browser's, * including reason 'destroyed' (memory pressure reclaims a device that way). * Keeping the dead one leaves the renderer permanently broken. */ private _handleDeviceLoss; /** * Unlike the base class, `_call` stays set after rendering: our `_render` * is asynchronous, so resource loads (images) that start mid-frame must * still find a live redraw callback once they complete. */ render(scene: GPUVegaScene, markTypes?: string[]): this; _render(scene: GPUVegaScene, markTypes?: string[], settle?: boolean): this; /** * Resolves when the frame has been submitted to the GPU. vega awaits this on * every frame, so waiting for the GPU to finish here would stop the cpu from * ever running ahead of it. A caller that needs the pixels reads them back * through captureFrame, which is ordered behind the frame in the same queue. */ renderAsync(scene: GPUVegaScene, markTypes?: string[]): Promise; /** Drops the device and the timer, without marking the renderer finished. */ private _releaseGpu; /** * Releases the GPU device and everything built on it. * * vega's own View.finalize does not reach the renderer, so a page that * creates and discards views leaks a device each time. Nothing recreates one * after this, so call it when the view is going away for good. */ finalize(): void; /** * Gpu time for the most recently measured frame, in milliseconds, or 0 where * the adapter offers no timestamps. Sampled a frame or more behind, so it * never stalls the one being drawn. */ get gpuFrameTime(): number; /** * Applies a changed wgOptions.sampleCount. Pipelines bake the sample count, * so the per-mark GPU resources are rebuilt, and the multisampled targets go * rather than outliving a drop to one sample. */ private _applySampleCount; private _frame; /** * Ends a frame on a canvas with no area, which is what the vega editor's empty * spec gives. Every target a frame draws into is the size of the canvas and a * texture cannot be empty, so each one failed validation and Chrome logged it, * every frame. Canvas draws nothing there and logs nothing. A capture waiting * on the frame gets the empty frame. */ private _skipEmptyFrame; private _endFrame; /** * Renders a frame and reads the result straight off the GPU. * * Presentation is what makes canvas content visible to screenshots and to * toDataURL, and a headless Linux runner never composites, so both come back * blank there. Copying the texture bypasses presentation entirely. */ captureFrame(timeoutMs?: number): Promise<{ width: number; height: number; data: Uint8Array; }>; private _captureOnce; /** Copies a texture into a mappable buffer and unpads it to tight RGBA rows. */ private _readback; /** * Destroys a GPU resource once the frame after this one is submitted, the way * the buffers a frame makes go. Safe to call from inside a mark's draw, where * the resource may still be referenced by a queued but not yet encoded draw. */ deferDestroy(resource: { destroy(): void; }): void; /** * Releases the render lock and flushes a coalesced request, if any. * * Every exit from a frame (completion, early return, or failure) must come * through here, or `_isRendering` stays stuck and awaiting callers never wake. */ private _finishFrame; /** * Re-renders the most recent scene (e.g. after options changed). `settle` * draws it at full quality, the way the frame a drag comes to rest on is. */ frame(settle?: boolean): this; /** True while drawing a frame that is not allowed to take a cheaper path. */ get settling(): boolean; /** * Asks for one more frame once renders stop arriving, for a mark that took a * cheaper path to keep up. Each render pushes it back, so a drag pays nothing * and the frame it comes to rest on is the full quality one. */ requestSettle(): void; draw(device: GPUDevice, ctx: GPUVegaCanvasContext, scene: GPUVegaScene, bounds: Bounds, markTypes?: string[]): void; /** The mark's own draw, timed when a benchmark has asked for it. */ private drawMark; /** * A 1x1 texture bound wherever a mark has no clip path, so the mask binding * is always satisfiable. Nothing reads it: the uniform flag is what decides * whether the shader looks at the mask at all. */ clipMaskPlaceholder(device: GPUDevice): GPUTexture; /** * A coverage target for one clip path, which has to survive while every mark * inside that clip draws. The stroke mask is one texture cleared by each * pass, so a clip cannot share it: two clipped groups in a frame would * overwrite each other before either one's marks were encoded. * * Pooled by index and reset per frame, so a scene with the same clips each * frame allocates nothing after the first. */ acquireClipMask(device: GPUDevice, samples: number): ClipMaskTarget | null; private _clipMasks; private _clipMaskNext; clipMaskPlaceholderView(device: GPUDevice): GPUTextureView; private _clipPlaceholder; /** * Single sampled coverage target, for a stroke that has to be composited as * one shape rather than band by band. One per frame is enough: each mask pass * clears it, and the composite that follows reads it before the next pass * fills it again. */ maskTexture(device?: GPUDevice): GPUTexture; /** The texture in `slot`, sized to the canvas. */ private canvasTexture; /** * The texture in `slot`, rebuilt when the device, the size or the sample count * it was made with is not what `want` describes. */ private slotTexture; /** * Where a mark whose blend has to be evaluated in a shader is drawn, and the * copy of the frame underneath it. The layer takes the frame's own format and * sample count, so a mark draws into it through the pipelines it already has. */ blendTargets(device: GPUDevice, samples: number): { layer: GPUTexture; resolve: GPUTexture; backdrop: GPUTexture; }; /** * Whether the last frame needed a pass outside the frame's own, for a * coverage mask or a blend evaluated against a copy of it. The fast path for * a blend is silent when it stops being taken, so a test watches this. */ drewOffFrame(): boolean; /** Multisampled color attachment, resolved into the canvas each frame. */ msaaTexture(device: GPUDevice, samples: number): GPUTexture; /** * Color target for offscreen mode. Acquiring the canvas swapchain destroys * the device where no compositor exists, so that call is skipped entirely and * the frame lands here instead. */ private offscreenTexture; clearColor(): GPUColor; }