Skip to content

The Device

Everything in Beam starts from a device. The device wraps a GPUAdapter, a GPUDevice, and the canvas's presentation context into one handle — the thing you make pipelines and resources from, and the thing you draw with.

Creating a device is asynchronous: acquiring an adapter and a device are both promises in WebGPU. Beam folds the whole handshake — request adapter, request device, read the preferred format, configure the canvas — into a single await.

ts
import { Beam } from 'beam-gpu'

const canvas = document.querySelector('canvas')!
canvas.width = 400
canvas.height = 400

const beam = await Beam.gpu(canvas)

That's it. beam is ready to make a pipeline, allocate verts/uniforms/textures, and run a frame. Beam.create is an exact alias if you prefer that name.

A live device

Here is a real device driving the hello-world triangle. The canvas, the await Beam.gpu(canvas), and the draw loop all run in your browser:

(The <BeamCanvas> component already does await Beam.gpu(canvas) for you and hands your setup the live beam. In a normal app you write the Beam.gpu call yourself, as in the snippet above.)

Config options

Beam.gpu(canvas, config?) takes an optional config object. Every field is optional and has a sensible default, so most apps pass nothing.

ts
const beam = await Beam.gpu(canvas, {
  format: 'rgba8unorm',          // swapchain format; defaults to the preferred format
  alpha: 'premultiplied',        // 'opaque' (default) or 'premultiplied'
  depth: true,                   // allocate a depth buffer for the screen pass
  hidpi: true,                   // scale the drawing buffer by devicePixelRatio
  power: 'high-performance',     // adapter power preference
  features: ['float32-filterable'],
  limits: { maxColorAttachments: 8 },
  device: existingDevice         // reuse a device you already own
})
OptionTypeDefaultWhat it does
formatGPUTextureFormatgetPreferredCanvasFormat()The swapchain color format. Leave it unset unless you have a reason.
alpha'opaque' | 'premultiplied''opaque'How the canvas composites with the page. Use 'premultiplied' for blending over HTML.
depthbooleanfalseAllocate a depth texture for the default screen pass (needed for 3D).
hidpibooleanfalseMultiply the drawing-buffer size by devicePixelRatio for crisp Retina output.
powerGPUPowerPreferenceadapter default'high-performance' or 'low-power' hint for adapter selection.
featuresGPUFeatureName[]noneOptional device features to request (e.g. 'float32-filterable').
limitsRecord<string, number>adapter defaultsRaise specific requiredLimits on the device.
deviceGPUDevicefreshly requestedSkip the adapter/device handshake and adopt a device you already created.

A few notes worth remembering:

  • depth is off by default. The hello-world triangle is 2D and needs no depth. Turn it on the moment you draw 3D geometry — then per-pipeline depth: true and this device depth: true work together.
  • hidpi is opt-in. By default the drawing buffer is exactly the size you set on the canvas. With hidpi: true, Beam.gpu and later beam.resize() multiply by devicePixelRatio, so a 400×400 CSS canvas allocates 800×800 pixels on a 2× display.
  • features and limits must be supported by the adapter. Request only what you use; asking for an unsupported feature rejects the device request (see error handling below).
  • device lets two parts of an app share one GPUDevice. Beam will configure the canvas against it instead of requesting a new one.

Escape hatches

Beam never hides the real WebGPU objects. The device exposes five read-only handles, so you can always drop one level down without re-acquiring anything:

ts
beam.device   // the GPUDevice — make raw buffers, compute pipelines, query sets…
beam.adapter  // the GPUAdapter — inspect features, limits, info
beam.ctx      // the GPUCanvasContext — getCurrentTexture(), reconfigure()
beam.format   // the GPUTextureFormat the swapchain was configured with
beam.canvas   // the HTMLCanvasElement you passed in

This is the design promise: every Beam verb maps to a real WebGPU call, and every handle exposes its raw object. Need a feature Beam's terse surface doesn't cover — a compute pass, a storage buffer, an indirect draw? Reach through beam.device and write plain WebGPU; the resources Beam made (verts.buffers, uniforms.buffer, texture.gpu) are all real GPU objects you can mix in.

ts
// e.g. read the configured format when building a raw pipeline by hand
const myPipeline = beam.device.createRenderPipeline({
  // ...
  fragment: { module, entryPoint: 'fs', targets: [{ format: beam.format }] }
})

Error handling on init

Beam.gpu can fail for two distinct reasons, and they want different handling.

1. WebGPU is unavailable. On a browser without WebGPU, navigator.gpu is undefined and no adapter can be acquired. Check first and show a friendly fallback rather than throwing into a blank canvas:

ts
if (!navigator.gpu) {
  showMessage('WebGPU is not available — try the latest Chrome, Edge, or Safari.')
} else {
  const beam = await Beam.gpu(canvas)
  // … render …
}

2. The request rejects. Even with WebGPU present, the adapter or device request can reject — most commonly when you ask for a feature or limit the adapter can't provide. Beam.gpu returns a promise, so wrap the await in try/catch:

ts
try {
  const beam = await Beam.gpu(canvas, {
    features: ['float32-filterable']
  })
  // … render …
} catch (err) {
  console.error('Beam init failed:', err)
  showMessage('Could not initialize the GPU. ' + err.message)
}

A robust startup combines both checks: bail early when navigator.gpu is missing, then try/catch the await for everything else. (This is exactly what the live <BeamCanvas> above does — it guards navigator.gpu, catches setup errors, and renders a graceful message instead of a dead canvas.)

Resizing

When the canvas changes size, call resize. With no arguments it re-reads the canvas's current dimensions (and reapplies hidpi if enabled); pass explicit pixels to set them:

ts
beam.resize()              // re-read canvas.width/height, reconfigure swapchain + depth
beam.resize(1280, 720)     // set an explicit drawing-buffer size

resize reconfigures the swapchain and reallocates the screen depth buffer (if depth was enabled), so call it after the canvas's CSS box changes — typically from a ResizeObserver or a window resize handler.

Cleanup

When you're done with a device — unmounting a component, tearing down a demo — call destroy() to release the GPUDevice and the resources Beam created for the screen pass:

ts
beam.destroy()

Next: turn this device into draws. Continue to Pipelines to learn how beam.pipeline(template) builds a GPURenderPipeline from WGSL plus schemas.