• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 29646907050

18 Jul 2026 01:50PM UTC coverage: 71.874% (-0.4%) from 72.255%
29646907050

Pull #2746

github

web-flow
Merge 136cf5204 into 5d8d46193
Pull Request #2746: feat: add WebGL extension and WebGPU feature support

11338 of 17802 branches covered (63.69%)

Branch coverage included in aggregate %.

76 of 194 new or added lines in 20 files covered. (39.18%)

5 existing lines in 1 file now uncovered.

21918 of 28468 relevant lines covered (76.99%)

5571.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.17
/modules/webgpu/src/adapter/webgpu-adapter.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
// biome-ignore format: preserve layout
6
// / <reference types="@webgpu/types" />
7

8
import {
9
  Adapter,
10
  type DeviceInfo,
11
  type DeviceProps,
12
  type WebGPUDeviceFeature,
13
  log
14
} from '@luma.gl/core';
15
import type {WebGPUDevice} from './webgpu-device';
16

17
type WebGPUSupportedLimitName = Exclude<keyof GPUSupportedLimits, '__brand'>;
18
type RequestedWebGPUFeatureLevel = NonNullable<DeviceProps['featureLevel']>;
19
type EffectiveWebGPUFeatureLevel = NonNullable<DeviceInfo['featureLevel']>;
20
type AssertNever<T extends never> = T;
21
/** Compile-time guard that keeps luma.gl feature names aligned with `@webgpu/types`. */
22
export type WebGPUFeatureNamesMissingFromLuma = AssertNever<
23
  Exclude<GPUFeatureName, WebGPUDeviceFeature>
24
>;
25
/** Compile-time guard that rejects stale luma.gl WebGPU feature names. */
26
export type LumaWebGPUFeatureNamesMissingFromWebGPU = AssertNever<
27
  Exclude<WebGPUDeviceFeature, GPUFeatureName>
28
>;
29

30
const CORE_FEATURES_AND_LIMITS = 'core-features-and-limits' as GPUFeatureName;
123✔
31

32
/** Optional WebGPU features that imply other feature names in luma.gl's portable feature set. */
33
const IMPLIED_WEBGPU_FEATURES: Partial<Record<WebGPUDeviceFeature, WebGPUDeviceFeature[]>> = {
123✔
34
  'texture-formats-tier2': ['texture-formats-tier1'],
35
  'subgroup-size-control': ['subgroups']
36
};
37

38
const WEBGPU_SUPPORTED_LIMIT_NAMES: readonly WebGPUSupportedLimitName[] = [
123✔
39
  'maxTextureDimension1D',
40
  'maxTextureDimension2D',
41
  'maxTextureDimension3D',
42
  'maxTextureArrayLayers',
43
  'maxBindGroups',
44
  'maxBindGroupsPlusVertexBuffers',
45
  'maxBindingsPerBindGroup',
46
  'maxDynamicUniformBuffersPerPipelineLayout',
47
  'maxDynamicStorageBuffersPerPipelineLayout',
48
  'maxSampledTexturesPerShaderStage',
49
  'maxSamplersPerShaderStage',
50
  'maxStorageBuffersPerShaderStage',
51
  'maxStorageBuffersInVertexStage',
52
  'maxStorageBuffersInFragmentStage',
53
  'maxStorageTexturesPerShaderStage',
54
  'maxStorageTexturesInVertexStage',
55
  'maxStorageTexturesInFragmentStage',
56
  'maxUniformBuffersPerShaderStage',
57
  'maxUniformBufferBindingSize',
58
  'maxStorageBufferBindingSize',
59
  'minUniformBufferOffsetAlignment',
60
  'minStorageBufferOffsetAlignment',
61
  'maxVertexBuffers',
62
  'maxBufferSize',
63
  'maxVertexAttributes',
64
  'maxVertexBufferArrayStride',
65
  'maxInterStageShaderVariables',
66
  'maxColorAttachments',
67
  'maxColorAttachmentBytesPerSample',
68
  'maxComputeWorkgroupStorageSize',
69
  'maxComputeInvocationsPerWorkgroup',
70
  'maxComputeWorkgroupSizeX',
71
  'maxComputeWorkgroupSizeY',
72
  'maxComputeWorkgroupSizeZ',
73
  'maxComputeWorkgroupsPerDimension',
74
  'maxImmediateSize'
75
];
76

77
/**
78
 * Returns every WebGPU limit that luma.gl can request from an adapter.
79
 * @param supportedLimits Limits exposed by the selected WebGPU adapter.
80
 * @returns Limits to forward through `GPUDeviceDescriptor.requiredLimits`.
81
 */
82
export function getRequiredWebGPULimits(
83
  supportedLimits: GPUSupportedLimits
84
): Record<string, number> {
85
  const requiredLimits: Record<string, number> = {};
70✔
86

87
  for (const limitName of WEBGPU_SUPPORTED_LIMIT_NAMES) {
70✔
88
    const limitValue = supportedLimits[limitName];
2,520✔
89
    if (typeof limitValue === 'number') {
2,520✔
90
      requiredLimits[limitName] = limitValue;
2,486✔
91
    }
92
  }
93

94
  return requiredLimits;
70✔
95
}
96

97
/**
98
 * Returns the requested WebGPU feature level, defaulting to the portable core profile.
99
 * @param props Device creation props.
100
 * @returns Effective WebGPU feature level to request.
101
 */
102
export function getWebGPUFeatureLevel(props: DeviceProps): RequestedWebGPUFeatureLevel {
103
  return props.featureLevel ?? 'core';
150✔
104
}
105

106
/**
107
 * Returns WebGPU adapter options used while selecting an adapter.
108
 * @param props Device creation props.
109
 * @returns Options to pass to `navigator.gpu.requestAdapter()`.
110
 */
111
export function getWebGPURequestAdapterOptions(props: DeviceProps): GPURequestAdapterOptions {
112
  const featureLevel = getWebGPUFeatureLevel(props);
75✔
113
  const options: GPURequestAdapterOptions = {
75✔
114
    featureLevel:
115
      featureLevel === 'compatibility' || featureLevel === 'best-available'
224✔
116
        ? 'compatibility'
117
        : 'core'
118
  };
119

120
  if (props.powerPreference && props.powerPreference !== 'default') {
75✔
121
    options.powerPreference = props.powerPreference;
72✔
122
  }
123

124
  return options;
75✔
125
}
126

127
/**
128
 * Returns adapter features required for the requested WebGPU feature level.
129
 * @param supportedFeatures Features exposed by the selected WebGPU adapter.
130
 * @param featureLevel Effective WebGPU feature level to request.
131
 * @returns Features to forward through `GPUDeviceDescriptor.requiredFeatures`.
132
 */
133
export function getRequiredWebGPUFeatures(
134
  supportedFeatures: GPUSupportedFeatures,
135
  featureLevel: RequestedWebGPUFeatureLevel,
136
  requestedFeatures: readonly WebGPUDeviceFeature[] = []
5✔
137
): GPUFeatureName[] {
138
  const requiredFeatures = new Set<GPUFeatureName>();
78✔
139

140
  if (featureLevel === 'max') {
78✔
141
    for (const feature of supportedFeatures) {
70✔
142
      requiredFeatures.add(feature as GPUFeatureName);
1,244✔
143
    }
144
  }
145

146
  if (featureLevel === 'best-available' && supportedFeatures.has(CORE_FEATURES_AND_LIMITS)) {
78✔
147
    // Compatibility adapters expose this opt-in when they can be upgraded to
148
    // core. See WebGPU Fundamentals:
149
    // https://webgpufundamentals.org/webgpu/lessons/webgpu-compatibility-mode.html
150
    requiredFeatures.add(CORE_FEATURES_AND_LIMITS);
1✔
151
  }
152

153
  for (const feature of expandRequiredWebGPUFeatures(requestedFeatures)) {
78✔
154
    if (!supportedFeatures.has(feature as GPUFeatureName)) {
3✔
155
      throw new Error(`Required WebGPU feature is not supported: ${feature}`);
1✔
156
    }
157
    requiredFeatures.add(feature as GPUFeatureName);
2✔
158
  }
159

160
  return Array.from(requiredFeatures);
77✔
161
}
162

163
/** Expands requested WebGPU features with feature names implied by the WebGPU specification. */
164
export function expandRequiredWebGPUFeatures(
165
  requestedFeatures: readonly WebGPUDeviceFeature[]
166
): WebGPUDeviceFeature[] {
167
  const expandedFeatures = new Set<WebGPUDeviceFeature>();
79✔
168
  const addFeature = (feature: WebGPUDeviceFeature) => {
79✔
169
    if (expandedFeatures.has(feature)) {
7!
NEW
170
      return;
×
171
    }
172
    expandedFeatures.add(feature);
7✔
173
    for (const impliedFeature of IMPLIED_WEBGPU_FEATURES[feature] || []) {
7✔
174
      addFeature(impliedFeature);
3✔
175
    }
176
  };
177

178
  for (const feature of requestedFeatures) {
79✔
179
    addFeature(feature);
4✔
180
  }
181

182
  return Array.from(expandedFeatures);
79✔
183
}
184

185
/**
186
 * Returns the feature level exposed by the created WebGPU device.
187
 * @param requestedFeatureLevel Feature level requested by luma.gl.
188
 * @param deviceFeatures Features exposed by the created WebGPU device.
189
 * @returns Effective feature level reported through `device.info`.
190
 */
191
export function getEffectiveWebGPUFeatureLevel(
192
  requestedFeatureLevel: RequestedWebGPUFeatureLevel,
193
  deviceFeatures: GPUSupportedFeatures
194
): EffectiveWebGPUFeatureLevel {
195
  if (
75✔
196
    (requestedFeatureLevel === 'compatibility' || requestedFeatureLevel === 'best-available') &&
197
    deviceFeatures.has(CORE_FEATURES_AND_LIMITS)
198
  ) {
199
    return 'core';
2✔
200
  }
201

202
  return requestedFeatureLevel === 'best-available' ? 'compatibility' : requestedFeatureLevel;
73✔
203
}
204

205
export class WebGPUAdapter extends Adapter {
206
  /** type of device's created by this adapter */
207
  readonly type: WebGPUDevice['type'] = 'webgpu';
123✔
208
  protected gpuAdapterPromises = new Map<string, Promise<GPUAdapter | null>>();
123✔
209

210
  isSupported(): boolean {
211
    // Check if WebGPU is available
212
    return Boolean(typeof navigator !== 'undefined' && navigator.gpu);
2✔
213
  }
214

215
  isDeviceHandle(handle: unknown): boolean {
216
    if (typeof GPUDevice !== 'undefined' && handle instanceof GPUDevice) {
×
217
      return true;
×
218
    }
219

220
    // TODO - WebGPU does not yet seem to have a stable in-browser API, so we "sniff" for members instead
221
    if ((handle as any)?.queue) {
×
222
      return true;
×
223
    }
224

225
    return false;
×
226
  }
227

228
  async create(props: DeviceProps): Promise<WebGPUDevice> {
229
    if (!navigator.gpu) {
71!
230
      throw new Error('WebGPU not available. Recent Chrome browsers should work.');
×
231
    }
232

233
    const requestedFeatureLevel = getWebGPUFeatureLevel(props);
71✔
234
    const requestAdapterOptions = getWebGPURequestAdapterOptions(props);
71✔
235
    const gpuAdapterCacheKey = this.getGPUAdapterCacheKey(
71✔
236
      requestedFeatureLevel,
237
      requestAdapterOptions
238
    );
239
    const adapterPromise = this.getGPUAdapterPromise(gpuAdapterCacheKey, requestAdapterOptions);
71✔
240

241
    const adapter = await adapterPromise;
71✔
242

243
    if (!adapter) {
71!
244
      throw new Error('Failed to request WebGPU adapter');
×
245
    }
246

247
    //  Note: adapter.requestAdapterInfo() has been replaced with adapter.info. Fall back in case adapter.info is not available
248
    const adapterInfo =
249
      adapter.info ||
71!
250
      // @ts-ignore
251
      (await adapter.requestAdapterInfo?.());
252
    // log.probe(2, 'Adapter available', adapterInfo)();
253

254
    const deviceDescriptor: GPUDeviceDescriptor = {};
71✔
255

256
    const requiredFeatures = getRequiredWebGPUFeatures(
71✔
257
      adapter.features,
258
      requestedFeatureLevel,
259
      props.requiredFeatures
260
    );
261
    if (requiredFeatures.length > 0) {
71✔
262
      deviceDescriptor.requiredFeatures = requiredFeatures;
69✔
263
    }
264

265
    if (requestedFeatureLevel === 'max') {
71✔
266
      deviceDescriptor.requiredLimits = getRequiredWebGPULimits(adapter.limits);
69✔
267
    }
268

269
    const gpuDevice = await adapter.requestDevice(deviceDescriptor);
71✔
270
    this.gpuAdapterPromises.delete(gpuAdapterCacheKey);
71✔
271

272
    // log.probe(1, 'GPUDevice available')();
273

274
    const {WebGPUDevice} = await import('./webgpu-device');
71✔
275
    const featureLevel = getEffectiveWebGPUFeatureLevel(requestedFeatureLevel, gpuDevice.features);
71✔
276
    const deviceProps = {...props, featureLevel};
71✔
277

278
    log.groupCollapsed(1, 'WebGPUDevice created')();
71✔
279
    try {
71✔
280
      const device = new WebGPUDevice(deviceProps, gpuDevice, adapter, adapterInfo);
71✔
281
      log.probe(
71✔
282
        1,
283
        'Device created. For more info, set chrome://flags/#enable-webgpu-developer-features'
284
      )();
285
      log.table(1, device.info)();
71✔
286
      return device;
71✔
287
    } finally {
288
      log.groupEnd(1)();
71✔
289
    }
290
  }
291

292
  async attach(handle: GPUDevice): Promise<WebGPUDevice> {
293
    throw new Error('WebGPUAdapter.attach() not implemented');
×
294
  }
295

296
  protected getGPUAdapterPromise(
297
    cacheKey: string,
298
    requestAdapterOptions: GPURequestAdapterOptions
299
  ): Promise<GPUAdapter | null> {
300
    let gpuAdapterPromise = this.gpuAdapterPromises.get(cacheKey);
71✔
301
    if (!gpuAdapterPromise) {
71!
302
      gpuAdapterPromise = navigator.gpu.requestAdapter(requestAdapterOptions);
71✔
303
      this.gpuAdapterPromises.set(cacheKey, gpuAdapterPromise);
71✔
304
    }
305
    return gpuAdapterPromise;
71✔
306
  }
307

308
  protected getGPUAdapterCacheKey(
309
    featureLevel: RequestedWebGPUFeatureLevel,
310
    requestAdapterOptions: GPURequestAdapterOptions
311
  ): string {
312
    return [
71✔
313
      featureLevel,
314
      requestAdapterOptions.featureLevel || 'core',
71!
315
      requestAdapterOptions.powerPreference || 'default'
71!
316
    ].join(':');
317
  }
318
}
319

320
export const webgpuAdapter = new WebGPUAdapter();
123✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc