Skip to content

RFC: simplify CanvasContext drawing buffer sizing - #2765

Closed
ibgreen wants to merge 4 commits into
masterfrom
codex/canvas-context-sizing-rfc
Closed

RFC: simplify CanvasContext drawing buffer sizing#2765
ibgreen wants to merge 4 commits into
masterfrom
codex/canvas-context-sizing-rfc

Conversation

@ibgreen

@ibgreen ibgreen commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Motivation

deck.gl's canvas integrations are behavior-oriented:

  • Interleaved: attach to an externally owned canvas/context and read its drawing-buffer size without writing it.
  • Overlaid: render to a separate canvas whose drawing-buffer size follows the basemap canvas.

luma.gl currently exposes lower-level, overlapping sizing controls (autoResize, useDevicePixels, pixelSizeSource, and the boolean/object createCanvasContext union). This makes deck.gl reconstruct those behaviors through configuration merging and sizing-algorithm selection in deck.gl #10370 and deck.gl #10332.

This draft proposes fresh props that describe drawing-buffer ownership and source directly, deprecates the overloaded legacy props, and includes a tested proof of concept.

Proposal

drawingBufferSizeTracking selects where the target drawing-buffer dimensions come from:

Value Behavior
'none' The application owns the drawing-buffer size; luma.gl never changes it.
'canvas' luma.gl derives the drawing-buffer size from the target canvas and resizes it when needed.
'external-canvas' luma.gl mirrors drawingBufferSizeSource.width/height; if source and target are the same canvas, luma.gl only reads.

The setting controls only the drawing buffer (canvas.width and canvas.height). It does not copy an external canvas's CSS size, style, position, transforms, borders, scroll state, or containing block. The target's existing CSS-size, position, visibility, and callback observation remains separate.

The default is 'canvas'. With no fixed ratio it uses exact device-pixel observation when supported, falling back to CSS size × browser DPR. A numeric pixelRatio selects content-box sizing at that fixed ratio.

API

export type DrawingBufferSizeTracking =
  | 'none'
  | 'canvas'
  | 'external-canvas';

export type CanvasContextProps = {
  drawingBufferSizeTracking?: DrawingBufferSizeTracking;
  drawingBufferSizeSource?: HTMLCanvasElement | OffscreenCanvas | null;
  pixelRatio?: number;

  /** @deprecated */
  autoResize?: boolean;
  /** @deprecated */
  useDevicePixels?: boolean | number;
  /** @deprecated */
  pixelSizeSource?: 'exact' | 'css-dpr';
};

export type DeviceProps = {
  createCanvasContext?: boolean | CanvasContextProps;
  canvasContextProps?: CanvasContextProps;
};

Validation is deliberately explicit:

  • drawingBufferSizeSource is required for 'external-canvas' and invalid for the other values.
  • pixelRatio is valid only for 'canvas', must be finite and positive, and requires an explicit tracking value.
  • New drawing-buffer props take precedence over legacy sizing props.
  • CanvasContext.setProps() supports dynamic tracking, source, and ratio transitions and reconfigures the observer box when needed.

canvasContextProps is the preferred default/attached-context configuration. If the deprecated object form of createCanvasContext is also supplied, it is merged first and canvasContextProps wins.

Migration

Existing configuration Preferred configuration
autoResize: false drawingBufferSizeTracking: 'none'
useDevicePixels: false drawingBufferSizeTracking: 'canvas', pixelRatio: 1
numeric useDevicePixels drawingBufferSizeTracking: 'canvas', pixelRatio: number
exact automatic sizing drawingBufferSizeTracking: 'canvas'
reproduce another renderer's CSS/DPR algorithm drawingBufferSizeTracking: 'external-canvas' plus that canvas as drawingBufferSizeSource

The legacy CSS × live browser-DPR algorithm remains supported internally through v9 normalization, but is intentionally not a separate value in the fresh API. Its main integration use case is represented more directly by tracking the external backing store.

The RFC proposes deprecation in v9 and removal of autoResize, useDevicePixels, pixelSizeSource, and object-form createCanvasContext in v10.

PoC Implementation

The PoC:

  • implements all three tracking behaviors in the shared CanvasSurface, covering both CanvasContext and PresentationContext;
  • reads an external canvas's two integer backing-store dimensions at size-query and render boundaries;
  • changes the target only when those dimensions differ, avoiding layout reads, polling, and observer-ordering dependencies;
  • retains legacy exact and CSS-DPR behavior through internal normalization;
  • supports dynamic tracking/source/ratio changes;
  • adds and tests DeviceProps.canvasContextProps precedence; and
  • leaves stable API docs and release notes unchanged while the RFC is under review.

The durable proposal is in dev-docs/RFCs/vNext/canvas-context-configuration-rfc.md.

External Contexts

WebGL attachment defaults to:

{
  drawingBufferSizeTracking: 'external-canvas',
  drawingBufferSizeSource: gl.canvas
}

Because source and target are the same object, luma.gl updates bookkeeping before use but never writes the externally owned canvas. Explicit canvasContextProps can select target-canvas sizing or no tracking instead.

For an overlaid deck.gl canvas:

{
  canvas: overlayCanvas,
  drawingBufferSizeTracking: 'external-canvas',
  drawingBufferSizeSource: map.getCanvas()
}

This copies only the basemap canvas's actual drawing-buffer dimensions. Layout alignment remains the responsibility of deck.gl/the host application.

Validation

  • nvm use — Node v22.22.1
  • yarn lint fix — passed
  • Focused CanvasContext/CanvasObserver/device/WebGL attachment suite — 4 files, 44 tests passed
  • yarn build — passed
  • yarn test node phase — 53 files passed, 2 skipped; 195 tests passed, 2 skipped
  • yarn test headless phase — 235 files passed; 1291 tests passed, 25 skipped; 3 known unrelated WebGPU/DGGS failures remain:
    • modules/shadertools/test/modules/geospatial/dggs.spec.ts (A5 produces NaN)
    • modules/arrow/test/arrow/dggs-gpu-polygons.spec.ts (A5 produces NaN)
    • modules/arrow-layers/test/layers/arrow-layers.spec.ts (storage-backed instances are not drawable)

Open Questions

  • Should drawingBufferSizeSource remain canvas-only or accept a structural {width, height} source?
  • Should 'none' retain current exact-device-pixel bookkeeping and callbacks, as the PoC does?
  • Is the compatibility-only legacy CSS-DPR path sufficient for v9 now that new overlays can track the external backing store directly?

@chrisgervang

Copy link
Copy Markdown
Contributor

If we go with this it's likely safe to remove pixelSizeSource.. it might have technically released in a luma 9.3 patch, which is shame since it wasn't used in a deck release

Comment thread dev-docs/RFCs/vNext/canvas-context-configuration-rfc.md Outdated
Comment thread modules/core/src/adapter/canvas-surface.ts Outdated
Comment thread modules/core/src/adapter/canvas-surface.ts Outdated
Comment thread modules/core/src/adapter/device.ts
Comment thread modules/webgl/src/adapter/webgl-adapter.ts
@chrisgervang

chrisgervang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Should manual mode continue observing the device-pixel content box, as implemented, or use the cheaper content box when only callbacks/position are needed?

This seems like the risky part of this. If it comes up as a perf issue, we might first think to add a performantDrawingBufferSizeTracking: Boolean flag, but then it confuses the device pixel tracking mode. Maybe instead this could be solved with a 4th mode like "manual-cheaper-observation"

@ibgreen

ibgreen commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

@chrisgervang After looking at this again, it struck me that we are addressing the problem rather indirectly.

it seems to me that what deck wants is to either

  • create a canvas that track the size of an external context (overlay mode)
  • attach to a canvas that is managed externally (interleaved mode)

Perhaps the right API is to offer modes that take care of these things.

E.g. the fact that mapbox uses non-exact pixels and DPR multiplication is kind of an implementation detail that deck.gl doesn't really need to know about.

I am updating along these lines

@chrisgervang

Copy link
Copy Markdown
Contributor

What would interleaved deck.gl canvas use with this new pattern?

@ibgreen-openai
ibgreen-openai force-pushed the codex/canvas-context-sizing-rfc branch from e41356a to 730438e Compare August 2, 2026 22:41
@ibgreen-openai

Copy link
Copy Markdown
Collaborator

For interleaved deck.gl, attach the externally owned WebGL context. WebGLAdapter.attach now defaults the resulting CanvasContext to external-canvas tracking with gl.canvas as its source. Because source and target are the same canvas, luma.gl only refreshes size bookkeeping and never writes canvas.width or canvas.height; the basemap renderer remains the owner. Overlaid mode uses a separate target canvas with map.getCanvas() as the external source.

@ibgreen-openai

Copy link
Copy Markdown
Collaborator

Will close temporarily until we refocus on this again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants