The renderer#

createRenderer returns an initialized three.js WebGPURenderer set up the way Radix expects: WebGPU when the browser has it and the WebGL2 backend otherwise, with the device limits that about 95% of devices support. This page also covers getGpuCaps, HDR output (Hdr), and GPU timing (GpuProfiler).

Code: src/gpu/createRenderer.ts, baseline.ts, hdr.ts, and GpuProfiler.ts.

const renderer = await createRenderer({ antialias: false });
const caps = getGpuCaps(renderer);
console.log(caps.backend); // "webgpu" or "webgpu-gl"

Shaders in Radix are written in TSL (three.js Shading Language) only, with no raw WGSL or GLSL, so the same code runs on both backends (Shaders).

createRenderer#

An option passed to the call takes precedence over its URL parameter. An option left out comes from its URL parameter, or else from its default. A page that wants its own default and still wants the URL to override it reads the parameter and passes the value itself.

OptionURLDefaultWhat it does
backend?gpu=webgpu | webgpu-gl"auto""webgpu", "webgpu-gl" (the WebGL2 backend), or "auto": WebGPU when available
canvasa new canvasthe canvas to draw into
antialiasthree's defaultMSAA
hdr?hdr=1 | autooffan rgba16float canvas with extended tone mapping (WebGPU): true, "auto" (when the display reports high dynamic range), or false. getGpuCaps(renderer).hdr tells whether it was enabled
profile?profile=1offtimestamp queries plus a GpuProfiler as renderer.inspector
timing?timing=1offtimestamp queries only
multiview?multiview=1offboth XR eyes in one draw where supported (OVR_multiview2 on WebGL2)
directCanvas?direct=0ondraws straight to the canvas, without three's linear target and output pass: materials write sRGB-encoded colors themselves
xroffenables renderer.xr before init: the WebGPU adapter must be requested XR-compatible for the WebXR / WebGPU binding
limitsWebGPU limits added to or replacing the baselines below: a number (capped at the adapter's) or "adapter" for the adapter's full limit (LimitRequest)
logarithmicDepthBufferoffa logarithmic depth buffer, for scenes from centimeters to kilometers. Splats get it too: three derives a material's depth from its clip-space position when it has its own vertex shader

Device baselines#

On WebGPU, createRenderer asks for these limits above WebGPU's defaults. Each is capped at what the adapter offers, so the request never fails. Each value is met by about 95% of devices or more (web3dsurvey.com, Sep 2026). Design WebGPU code against these, not against the development machine.

LimitDefaultBaseline
maxTextureDimension1D, maxTextureDimension2D819216384
maxTextureArrayLayers2562048
maxBufferSize256 MiB1 GiB
maxStorageBufferBindingSize128 MiB256 MiB (98%; 1 GiB: 92%)
maxComputeWorkgroupStorageSize16 KiB32 KiB
maxColorAttachmentBytesPerSample3264
maxStorageBuffersPerShaderStage, ...InVertexStage, ...InFragmentStage810

Kept at the default because the next tier is under 95%: 4 storage textures per stage, 256 compute invocations per workgroup, 16 sampled textures per stage, 16 inter-stage variables, 4 bind groups. Optional features at or above 95%: timestamp-query, indirect-first-instance, depth32float-stencil8, rg11b10ufloat-renderable, depth-clip-control, and bgra8unorm-storage; shader-f16, float32-filterable, float32-blendable, subgroups, and texture compression need a fallback.

WebGL2 parameters to design against: 8192 texture size, 512 array layers, 4 draw buffers, 16 texture units per stage, 16 KiB uniform blocks, 15 varying vectors, 16 vertex attributes, and 4 MSAA samples. snorm8 is not renderable there (EXT_render_snorm is at 80%).

getGpuCaps#

getGpuCaps(renderer) returns a GpuCaps:

FieldWebGPUWebGL2
backend"webgpu""webgpu-gl"
computetruefalse
maxTextureSize, maxTextureLayers, maxColorAttachmentsthe device's limitsthe context's parameters
maxTexturesPerStagesampled textures per stagethe smaller of fragment and vertex texture units
maxStorageTexturesPerStage, maxStorageBuffersPerStage, maxWorkgroupStoragethe device's limits0
float32Blendable, float32Filterablethe featuresEXT_float_blend, OES_texture_float_linear
subgroupsthe featurefalse
multiview"webgpu" or false"ovr" or false
hdrHdr.canvasSupports(renderer)false

Types: GpuBackend ("webgpu" | "webgpu-gl"), CreateRendererOptions, LimitRequest, and GpuCaps.

HDR output#

HDR output has two independent parts:

const renderer = await createRenderer({ hdr: "auto" });
// Headroom 4 on an HDR canvas, else 1
const tone = Hdr.useToneMapping(renderer);

// Uniforms: change them any time
tone.headroom.value = settings.headroom;
// Restores three's tone mapping
tone.enabled = false;
MemberWhat it does
Hdr.displaySupports()whether the display reports high dynamic range
Hdr.canvasSupports(renderer)whether the canvas can show the extended range (WebGPU, an rgba16float canvas, and a browser that kept the extended mode)
Hdr.setOutput(renderer, extended)switches an HDR canvas between extended and standard range without recreating anything; false when the canvas cannot
Hdr.useToneMapping(renderer, { headroom?, knee?, whiten?, enabled? })the renderer's tone mapping (one per renderer; later calls return it with the options applied). Returns an HdrToneMapping
HdrToneMapping.headroomthe brightest linear value the output may reach: 1 for SDR, the display's headroom otherwise (a uniform; default 4 on an HDR canvas, else 1)
HdrToneMapping.kneethe fraction of the headroom kept unchanged before the shoulder (a uniform; default 0.75)
HdrToneMapping.whitenhow fast colors past the headroom fade to white; 0 never (a uniform; default 0.5)
HdrToneMapping.exposurerenderer.toneMappingExposure
HdrToneMapping.enabledturns the pass on and off; on sets toneMapping and an sRGB output color space, off restores the previous ones

Splats blend sRGB-encoded colors, as they were trained. The tone mapping pass decodes them first, and three encodes its result, so below the knee the frame is the same as without tone mapping. There is no web API for a display's actual headroom yet; pick one (about 2 to 8 on current HDR laptops and monitors) or make it a setting.

GpuProfiler#

GpuProfiler records GPU and CPU milliseconds per named phase. GPU times come from three's timestamp queries (WebGPU timestamp-query, or EXT_disjoint_timer_query_webgl2 on WebGL2); CPU times wrap the phase's code. createRenderer({ profile: true }) installs one as renderer.inspector.

const profiler = renderer.inspector as GpuProfiler;
// "project" and "sort" get their own rows
splatRenderer.phase = (name, run) => profiler.phase(name, run);

renderer.setAnimationLoop(async () => {
  profiler.beginFrame();
  profiler.label = "draw";
  renderer.render(scene, camera);
  // ms per phase, averaged; undefined if nothing resolved
  const report = await profiler.collect();
  // "GPU draw 2.31, project 0.09 | CPU ... ms"
  if (report) console.log(GpuProfiler.format(report));
});
MemberWhat it does
beginFrame()starts a frame; times are averaged over frames
labelthe phase of the passes recorded from now on (default "other")
phase(name, fn)runs fn with its passes labeled name, and records its CPU time
mark(name, ms)records CPU milliseconds for a phase, such as an async worker round trip
collect()resolves pending queries: a GpuProfile (gpu and cpu: ms per phase per frame; frames: resolved frames), or undefined when nothing new resolved
GpuProfiler.format(profile)one line of text, phases sorted by time
activewhether the renderer tracks timestamps; settable between frames (not inside a render: WebGL2 timer queries must end as they began). CPU phases are timed either way
supportedwhether the device has timestamp queries

Timestamps resolve a frame or two late. Chrome quantizes them to 100 µs unless WebGPU developer features are on (--enable-webgpu-developer-features).

Source: docs/renderer.md