@luma.gl/webgpu 9.3.0-alpha.2 → 9.3.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/adapter/helpers/cpu-hotspot-profiler.d.ts +54 -0
  2. package/dist/adapter/helpers/cpu-hotspot-profiler.d.ts.map +1 -0
  3. package/dist/adapter/helpers/cpu-hotspot-profiler.js +26 -0
  4. package/dist/adapter/helpers/cpu-hotspot-profiler.js.map +1 -0
  5. package/dist/adapter/helpers/generate-mipmaps-webgpu.d.ts +7 -0
  6. package/dist/adapter/helpers/generate-mipmaps-webgpu.d.ts.map +1 -0
  7. package/dist/adapter/helpers/generate-mipmaps-webgpu.js +490 -0
  8. package/dist/adapter/helpers/generate-mipmaps-webgpu.js.map +1 -0
  9. package/dist/adapter/helpers/get-bind-group.d.ts +2 -1
  10. package/dist/adapter/helpers/get-bind-group.d.ts.map +1 -1
  11. package/dist/adapter/helpers/get-bind-group.js +31 -21
  12. package/dist/adapter/helpers/get-bind-group.js.map +1 -1
  13. package/dist/adapter/resources/webgpu-buffer.d.ts +7 -0
  14. package/dist/adapter/resources/webgpu-buffer.d.ts.map +1 -1
  15. package/dist/adapter/resources/webgpu-buffer.js +58 -15
  16. package/dist/adapter/resources/webgpu-buffer.js.map +1 -1
  17. package/dist/adapter/resources/webgpu-command-buffer.js +1 -1
  18. package/dist/adapter/resources/webgpu-command-buffer.js.map +1 -1
  19. package/dist/adapter/resources/webgpu-command-encoder.d.ts +5 -4
  20. package/dist/adapter/resources/webgpu-command-encoder.d.ts.map +1 -1
  21. package/dist/adapter/resources/webgpu-command-encoder.js +23 -5
  22. package/dist/adapter/resources/webgpu-command-encoder.js.map +1 -1
  23. package/dist/adapter/resources/webgpu-compute-pass.d.ts +1 -1
  24. package/dist/adapter/resources/webgpu-compute-pass.d.ts.map +1 -1
  25. package/dist/adapter/resources/webgpu-compute-pass.js +14 -6
  26. package/dist/adapter/resources/webgpu-compute-pass.js.map +1 -1
  27. package/dist/adapter/resources/webgpu-compute-pipeline.d.ts.map +1 -1
  28. package/dist/adapter/resources/webgpu-compute-pipeline.js +19 -3
  29. package/dist/adapter/resources/webgpu-compute-pipeline.js.map +1 -1
  30. package/dist/adapter/resources/webgpu-framebuffer.d.ts +6 -0
  31. package/dist/adapter/resources/webgpu-framebuffer.d.ts.map +1 -1
  32. package/dist/adapter/resources/webgpu-framebuffer.js +16 -0
  33. package/dist/adapter/resources/webgpu-framebuffer.js.map +1 -1
  34. package/dist/adapter/resources/webgpu-pipeline-layout.d.ts.map +1 -1
  35. package/dist/adapter/resources/webgpu-pipeline-layout.js +1 -2
  36. package/dist/adapter/resources/webgpu-pipeline-layout.js.map +1 -1
  37. package/dist/adapter/resources/webgpu-query-set.d.ts +33 -4
  38. package/dist/adapter/resources/webgpu-query-set.d.ts.map +1 -1
  39. package/dist/adapter/resources/webgpu-query-set.js +145 -4
  40. package/dist/adapter/resources/webgpu-query-set.js.map +1 -1
  41. package/dist/adapter/resources/webgpu-render-pass.d.ts +3 -0
  42. package/dist/adapter/resources/webgpu-render-pass.d.ts.map +1 -1
  43. package/dist/adapter/resources/webgpu-render-pass.js +74 -30
  44. package/dist/adapter/resources/webgpu-render-pass.js.map +1 -1
  45. package/dist/adapter/resources/webgpu-render-pipeline.d.ts +7 -4
  46. package/dist/adapter/resources/webgpu-render-pipeline.d.ts.map +1 -1
  47. package/dist/adapter/resources/webgpu-render-pipeline.js +26 -15
  48. package/dist/adapter/resources/webgpu-render-pipeline.js.map +1 -1
  49. package/dist/adapter/resources/webgpu-sampler.d.ts.map +1 -1
  50. package/dist/adapter/resources/webgpu-sampler.js +4 -0
  51. package/dist/adapter/resources/webgpu-sampler.js.map +1 -1
  52. package/dist/adapter/resources/webgpu-texture-view.d.ts +6 -0
  53. package/dist/adapter/resources/webgpu-texture-view.d.ts.map +1 -1
  54. package/dist/adapter/resources/webgpu-texture-view.js +47 -11
  55. package/dist/adapter/resources/webgpu-texture-view.js.map +1 -1
  56. package/dist/adapter/resources/webgpu-texture.d.ts +10 -4
  57. package/dist/adapter/resources/webgpu-texture.d.ts.map +1 -1
  58. package/dist/adapter/resources/webgpu-texture.js +116 -57
  59. package/dist/adapter/resources/webgpu-texture.js.map +1 -1
  60. package/dist/adapter/resources/webgpu-vertex-array.js +1 -1
  61. package/dist/adapter/resources/webgpu-vertex-array.js.map +1 -1
  62. package/dist/adapter/webgpu-canvas-context.d.ts +2 -0
  63. package/dist/adapter/webgpu-canvas-context.d.ts.map +1 -1
  64. package/dist/adapter/webgpu-canvas-context.js +78 -19
  65. package/dist/adapter/webgpu-canvas-context.js.map +1 -1
  66. package/dist/adapter/webgpu-device.d.ts +5 -1
  67. package/dist/adapter/webgpu-device.d.ts.map +1 -1
  68. package/dist/adapter/webgpu-device.js +113 -9
  69. package/dist/adapter/webgpu-device.js.map +1 -1
  70. package/dist/adapter/webgpu-presentation-context.d.ts +25 -0
  71. package/dist/adapter/webgpu-presentation-context.d.ts.map +1 -0
  72. package/dist/adapter/webgpu-presentation-context.js +144 -0
  73. package/dist/adapter/webgpu-presentation-context.js.map +1 -0
  74. package/dist/dist.dev.js +2864 -1702
  75. package/dist/dist.min.js +167 -8
  76. package/dist/index.cjs +1363 -225
  77. package/dist/index.cjs.map +4 -4
  78. package/package.json +5 -5
  79. package/src/adapter/helpers/cpu-hotspot-profiler.ts +70 -0
  80. package/src/adapter/helpers/generate-mipmaps-webgpu.ts +583 -0
  81. package/src/adapter/helpers/get-bind-group.ts +37 -22
  82. package/src/adapter/resources/webgpu-buffer.ts +61 -15
  83. package/src/adapter/resources/webgpu-command-buffer.ts +1 -1
  84. package/src/adapter/resources/webgpu-command-encoder.ts +32 -6
  85. package/src/adapter/resources/webgpu-compute-pass.ts +14 -6
  86. package/src/adapter/resources/webgpu-compute-pipeline.ts +21 -3
  87. package/src/adapter/resources/webgpu-framebuffer.ts +21 -0
  88. package/src/adapter/resources/webgpu-pipeline-layout.ts +1 -2
  89. package/src/adapter/resources/webgpu-query-set.ts +185 -9
  90. package/src/adapter/resources/webgpu-render-pass.ts +82 -34
  91. package/src/adapter/resources/webgpu-render-pipeline.ts +36 -19
  92. package/src/adapter/resources/webgpu-sampler.ts +5 -0
  93. package/src/adapter/resources/webgpu-texture-view.ts +51 -11
  94. package/src/adapter/resources/webgpu-texture.ts +142 -93
  95. package/src/adapter/resources/webgpu-vertex-array.ts +1 -1
  96. package/src/adapter/webgpu-canvas-context.ts +91 -26
  97. package/src/adapter/webgpu-device.ts +128 -9
  98. package/src/adapter/webgpu-presentation-context.ts +180 -0
@@ -3,10 +3,12 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import type {ComputeShaderLayout, BindingDeclaration, Binding} from '@luma.gl/core';
6
- import {Buffer, Sampler, Texture, log} from '@luma.gl/core';
6
+ import {Buffer, Sampler, Texture, TextureView, log} from '@luma.gl/core';
7
+ import type {WebGPUDevice} from '../webgpu-device';
7
8
  import type {WebGPUBuffer} from '../resources/webgpu-buffer';
8
9
  import type {WebGPUSampler} from '../resources/webgpu-sampler';
9
10
  import type {WebGPUTexture} from '../resources/webgpu-texture';
11
+ import type {WebGPUTextureView} from '../resources/webgpu-texture-view';
10
12
 
11
13
  /**
12
14
  * Create a WebGPU "bind group layout" from an array of luma.gl bindings
@@ -28,21 +30,19 @@ export function makeBindGroupLayout(
28
30
  * Create a WebGPU "bind group" from an array of luma.gl bindings
29
31
  */
30
32
  export function getBindGroup(
31
- device: GPUDevice,
33
+ device: WebGPUDevice,
32
34
  bindGroupLayout: GPUBindGroupLayout,
33
35
  shaderLayout: ComputeShaderLayout,
34
36
  bindings: Record<string, Binding>
35
37
  ): GPUBindGroup {
36
38
  const entries = getBindGroupEntries(bindings, shaderLayout);
37
39
  device.pushErrorScope('validation');
38
- const bindGroup = device.createBindGroup({
40
+ const bindGroup = device.handle.createBindGroup({
39
41
  layout: bindGroupLayout,
40
42
  entries
41
43
  });
42
- device.popErrorScope().then((error: GPUError | null) => {
43
- if (error) {
44
- log.error(`bindGroup creation: ${error.message}`, bindGroup)();
45
- }
44
+ device.popErrorScope((error: GPUError) => {
45
+ log.error(`bindGroup creation: ${error.message}`, bindGroup)();
46
46
  });
47
47
  return bindGroup;
48
48
  }
@@ -74,23 +74,31 @@ function getBindGroupEntries(
74
74
  const entries: GPUBindGroupEntry[] = [];
75
75
 
76
76
  for (const [bindingName, value] of Object.entries(bindings)) {
77
- let bindingLayout = getShaderLayoutBinding(shaderLayout, bindingName);
78
- if (bindingLayout) {
79
- const entry = getBindGroupEntry(value, bindingLayout.location);
77
+ const exactBindingLayout = shaderLayout.bindings.find(binding => binding.name === bindingName);
78
+ const bindingLayout = exactBindingLayout || getShaderLayoutBinding(shaderLayout, bindingName);
79
+ const isShadowedAlias =
80
+ !exactBindingLayout && bindingLayout ? bindingLayout.name in bindings : false;
81
+
82
+ // Mirror the WebGL path: when both `foo` and `fooUniforms` exist in the bindings map,
83
+ // prefer the exact shader binding name and ignore the alias entry.
84
+ if (!isShadowedAlias) {
85
+ const entry = bindingLayout
86
+ ? getBindGroupEntry(value, bindingLayout.location, undefined, bindingName)
87
+ : null;
80
88
  if (entry) {
81
89
  entries.push(entry);
82
90
  }
83
- }
84
91
 
85
- // TODO - hack to automatically bind samplers to supplied texture default samplers
86
- if (value instanceof Texture) {
87
- bindingLayout = getShaderLayoutBinding(shaderLayout, `${bindingName}Sampler`, {
88
- ignoreWarnings: true
89
- });
90
- if (bindingLayout) {
91
- const entry = getBindGroupEntry(value, bindingLayout.location, {sampler: true});
92
- if (entry) {
93
- entries.push(entry);
92
+ // TODO - hack to automatically bind samplers to supplied texture default samplers
93
+ if (value instanceof Texture) {
94
+ const samplerBindingLayout = getShaderLayoutBinding(shaderLayout, `${bindingName}Sampler`, {
95
+ ignoreWarnings: true
96
+ });
97
+ const samplerEntry = samplerBindingLayout
98
+ ? getBindGroupEntry(value, samplerBindingLayout.location, {sampler: true}, bindingName)
99
+ : null;
100
+ if (samplerEntry) {
101
+ entries.push(samplerEntry);
94
102
  }
95
103
  }
96
104
  }
@@ -102,7 +110,8 @@ function getBindGroupEntries(
102
110
  function getBindGroupEntry(
103
111
  binding: Binding,
104
112
  index: number,
105
- options?: {sampler?: boolean}
113
+ options?: {sampler?: boolean},
114
+ bindingName: string = 'unknown'
106
115
  ): GPUBindGroupEntry | null {
107
116
  if (binding instanceof Buffer) {
108
117
  return {
@@ -118,6 +127,12 @@ function getBindGroupEntry(
118
127
  resource: (binding as WebGPUSampler).handle
119
128
  };
120
129
  }
130
+ if (binding instanceof TextureView) {
131
+ return {
132
+ binding: index,
133
+ resource: (binding as WebGPUTextureView).handle
134
+ };
135
+ }
121
136
  if (binding instanceof Texture) {
122
137
  if (options?.sampler) {
123
138
  return {
@@ -130,6 +145,6 @@ function getBindGroupEntry(
130
145
  resource: (binding as WebGPUTexture).view.handle
131
146
  };
132
147
  }
133
- log.warn(`invalid binding ${name}`, binding);
148
+ log.warn(`invalid binding ${bindingName}`, binding);
134
149
  return null;
135
150
  }
@@ -5,20 +5,28 @@
5
5
  import {log, Buffer, type BufferProps, type BufferMapCallback} from '@luma.gl/core';
6
6
  import {type WebGPUDevice} from '../webgpu-device';
7
7
 
8
+ /**
9
+ * WebGPU implementation of Buffer
10
+ * For byte alignment requirements see:
11
+ * @see https://www.w3.org/TR/webgpu/#dom-gpubuffer-mapasync
12
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/GPUBuffer/mapAsync
13
+ */
8
14
  export class WebGPUBuffer extends Buffer {
9
15
  readonly device: WebGPUDevice;
10
16
  readonly handle: GPUBuffer;
11
17
  readonly byteLength: number;
18
+ readonly paddedByteLength: number;
12
19
 
13
20
  constructor(device: WebGPUDevice, props: BufferProps) {
14
21
  super(device, props);
15
22
  this.device = device;
16
23
 
17
24
  this.byteLength = props.byteLength || props.data?.byteLength || 0;
25
+ this.paddedByteLength = Math.ceil(this.byteLength / 4) * 4;
18
26
  const mappedAtCreation = Boolean(this.props.onMapped || props.data);
19
27
 
20
28
  // WebGPU buffers must be aligned to 4 bytes
21
- const size = Math.ceil(this.byteLength / 4) * 4;
29
+ const size = this.paddedByteLength;
22
30
 
23
31
  this.device.pushErrorScope('out-of-memory');
24
32
  this.device.pushErrorScope('validation');
@@ -59,12 +67,27 @@ export class WebGPUBuffer extends Buffer {
59
67
  this.device.reportError(new Error(`${this} creation failed ${error.message}`), this)();
60
68
  this.device.debug();
61
69
  });
70
+
71
+ if (!this.props.handle) {
72
+ this.trackAllocatedMemory(size);
73
+ } else {
74
+ this.trackReferencedMemory(size, 'Buffer');
75
+ }
62
76
  }
63
77
 
64
78
  override destroy(): void {
65
- this.handle?.destroy();
66
- // @ts-expect-error readonly
67
- this.handle = null;
79
+ if (!this.destroyed && this.handle) {
80
+ this.removeStats();
81
+ if (!this.props.handle) {
82
+ this.trackDeallocatedMemory();
83
+ this.handle.destroy();
84
+ } else {
85
+ this.trackDeallocatedReferencedMemory('Buffer');
86
+ }
87
+ this.destroyed = true;
88
+ // @ts-expect-error readonly
89
+ this.handle = null;
90
+ }
68
91
  }
69
92
 
70
93
  write(data: ArrayBufferLike | ArrayBufferView | SharedArrayBuffer, byteOffset = 0) {
@@ -92,10 +115,11 @@ export class WebGPUBuffer extends Buffer {
92
115
  byteOffset: number = 0,
93
116
  byteLength: number = this.byteLength - byteOffset
94
117
  ): Promise<void> {
118
+ const alignedByteLength = Math.ceil(byteLength / 4) * 4;
95
119
  // Unless the application created and supplied a mappable buffer, a staging buffer is needed
96
120
  const isMappable = (this.usage & Buffer.MAP_WRITE) !== 0;
97
121
  const mappableBuffer: WebGPUBuffer | null = !isMappable
98
- ? this._getMappableBuffer(Buffer.MAP_WRITE | Buffer.COPY_SRC, 0, this.byteLength)
122
+ ? this._getMappableBuffer(Buffer.MAP_WRITE | Buffer.COPY_SRC, 0, this.paddedByteLength)
99
123
  : null;
100
124
 
101
125
  const writeBuffer = mappableBuffer || this;
@@ -105,13 +129,15 @@ export class WebGPUBuffer extends Buffer {
105
129
  this.device.pushErrorScope('validation');
106
130
  try {
107
131
  await this.device.handle.queue.onSubmittedWorkDone();
108
- await writeBuffer.handle.mapAsync(GPUMapMode.WRITE, byteOffset, byteLength);
109
- const arrayBuffer = writeBuffer.handle.getMappedRange(byteOffset, byteLength);
132
+ await writeBuffer.handle.mapAsync(GPUMapMode.WRITE, byteOffset, alignedByteLength);
133
+ const mappedRange = writeBuffer.handle.getMappedRange(byteOffset, alignedByteLength);
134
+ const arrayBuffer = mappedRange.slice(0, byteLength);
110
135
  // eslint-disable-next-line @typescript-eslint/await-thenable
111
136
  await callback(arrayBuffer, 'mapped');
137
+ new Uint8Array(mappedRange).set(new Uint8Array(arrayBuffer), 0);
112
138
  writeBuffer.handle.unmap();
113
139
  if (mappableBuffer) {
114
- this._copyBuffer(mappableBuffer, byteOffset, byteLength);
140
+ this._copyBuffer(mappableBuffer, byteOffset, alignedByteLength);
115
141
  }
116
142
  } finally {
117
143
  this.device.popErrorScope((error: GPUError) => {
@@ -138,17 +164,33 @@ export class WebGPUBuffer extends Buffer {
138
164
  byteOffset = 0,
139
165
  byteLength = this.byteLength - byteOffset
140
166
  ): Promise<T> {
167
+ const requestedEnd = byteOffset + byteLength;
168
+ if (requestedEnd > this.byteLength) {
169
+ throw new Error('Mapping range exceeds buffer size');
170
+ }
171
+
172
+ let mappedByteOffset = byteOffset;
173
+ let mappedByteLength = byteLength;
174
+ let sliceByteOffset = 0;
175
+ let lifetime: 'mapped' | 'copied' = 'mapped';
176
+
177
+ // WebGPU mapAsync requires 8-byte offsets and 4-byte lengths.
141
178
  if (byteOffset % 8 !== 0 || byteLength % 4 !== 0) {
142
- throw new Error('byteOffset must be multiple of 8 and byteLength multiple of 4');
179
+ mappedByteOffset = Math.floor(byteOffset / 8) * 8;
180
+ const alignedEnd = Math.ceil(requestedEnd / 4) * 4;
181
+ mappedByteLength = alignedEnd - mappedByteOffset;
182
+ sliceByteOffset = byteOffset - mappedByteOffset;
183
+ lifetime = 'copied';
143
184
  }
144
- if (byteOffset + byteLength > this.handle.size) {
185
+
186
+ if (mappedByteOffset + mappedByteLength > this.paddedByteLength) {
145
187
  throw new Error('Mapping range exceeds buffer size');
146
188
  }
147
189
 
148
190
  // Unless the application created and supplied a mappable buffer, a staging buffer is needed
149
191
  const isMappable = (this.usage & Buffer.MAP_READ) !== 0;
150
192
  const mappableBuffer: WebGPUBuffer | null = !isMappable
151
- ? this._getMappableBuffer(Buffer.MAP_READ | Buffer.COPY_DST, 0, this.byteLength)
193
+ ? this._getMappableBuffer(Buffer.MAP_READ | Buffer.COPY_DST, 0, this.paddedByteLength)
152
194
  : null;
153
195
 
154
196
  const readBuffer = mappableBuffer || this;
@@ -158,12 +200,16 @@ export class WebGPUBuffer extends Buffer {
158
200
  try {
159
201
  await this.device.handle.queue.onSubmittedWorkDone();
160
202
  if (mappableBuffer) {
161
- mappableBuffer._copyBuffer(this);
203
+ mappableBuffer._copyBuffer(this, mappedByteOffset, mappedByteLength);
162
204
  }
163
- await readBuffer.handle.mapAsync(GPUMapMode.READ, byteOffset, byteLength);
164
- const arrayBuffer = readBuffer.handle.getMappedRange(byteOffset, byteLength);
205
+ await readBuffer.handle.mapAsync(GPUMapMode.READ, mappedByteOffset, mappedByteLength);
206
+ const arrayBuffer = readBuffer.handle.getMappedRange(mappedByteOffset, mappedByteLength);
207
+ const mappedRange =
208
+ lifetime === 'mapped'
209
+ ? arrayBuffer
210
+ : arrayBuffer.slice(sliceByteOffset, sliceByteOffset + byteLength);
165
211
  // eslint-disable-next-line @typescript-eslint/await-thenable
166
- const result = await callback(arrayBuffer, 'mapped');
212
+ const result = await callback(mappedRange, lifetime);
167
213
  readBuffer.handle.unmap();
168
214
  return result;
169
215
  } finally {
@@ -13,7 +13,7 @@ export class WebGPUCommandBuffer extends CommandBuffer {
13
13
  readonly handle: GPUCommandBuffer;
14
14
 
15
15
  constructor(commandEncoder: WebGPUCommandEncoder, props: CommandBufferProps) {
16
- super(commandEncoder.device, {});
16
+ super(commandEncoder.device, props);
17
17
  this.device = commandEncoder.device;
18
18
  this.handle =
19
19
  this.props.handle ||
@@ -3,6 +3,7 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import type {
6
+ CommandBufferProps,
6
7
  RenderPassProps,
7
8
  ComputePassProps,
8
9
  CopyTextureToTextureOptions,
@@ -34,9 +35,11 @@ export class WebGPUCommandEncoder extends CommandEncoder {
34
35
  this.handle.label = this.props.id;
35
36
  }
36
37
 
37
- override destroy(): void {}
38
+ override destroy(): void {
39
+ this.destroyResource();
40
+ }
38
41
 
39
- finish(props?: CommandEncoderProps): WebGPUCommandBuffer {
42
+ finish(props?: CommandBufferProps): WebGPUCommandBuffer {
40
43
  this.device.pushErrorScope('validation');
41
44
  const commandBuffer = new WebGPUCommandBuffer(this, {
42
45
  id: props?.id || 'unnamed-command-buffer'
@@ -46,6 +49,7 @@ export class WebGPUCommandEncoder extends CommandEncoder {
46
49
  this.device.reportError(new Error(message), this)();
47
50
  this.device.debug();
48
51
  });
52
+ this.destroy();
49
53
  return commandBuffer;
50
54
  }
51
55
 
@@ -53,12 +57,12 @@ export class WebGPUCommandEncoder extends CommandEncoder {
53
57
  * Allows a render pass to begin against a canvas context
54
58
  * @todo need to support a "Framebuffer" equivalent (aka preconfigured RenderPassDescriptors?).
55
59
  */
56
- beginRenderPass(props: RenderPassProps): WebGPURenderPass {
57
- return new WebGPURenderPass(this.device, props);
60
+ beginRenderPass(props: RenderPassProps = {}): WebGPURenderPass {
61
+ return new WebGPURenderPass(this.device, this._applyTimeProfilingToPassProps(props));
58
62
  }
59
63
 
60
- beginComputePass(props: ComputePassProps): WebGPUComputePass {
61
- return new WebGPUComputePass(this.device, props);
64
+ beginComputePass(props: ComputePassProps = {}): WebGPUComputePass {
65
+ return new WebGPUComputePass(this.device, this._applyTimeProfilingToPassProps(props));
62
66
  }
63
67
 
64
68
  // beginRenderPass(GPURenderPassDescriptor descriptor): GPURenderPassEncoder;
@@ -174,6 +178,28 @@ export class WebGPUCommandEncoder extends CommandEncoder {
174
178
  options?.destinationOffset || 0
175
179
  );
176
180
  }
181
+
182
+ writeTimestamp(querySet: WebGPUQuerySet, queryIndex: number): void {
183
+ querySet._invalidateResults();
184
+ const writeTimestamp = (
185
+ this.handle as GPUCommandEncoder & {
186
+ writeTimestamp?: (querySet: GPUQuerySet, queryIndex: number) => void;
187
+ }
188
+ ).writeTimestamp;
189
+
190
+ if (writeTimestamp) {
191
+ writeTimestamp.call(this.handle, querySet.handle, queryIndex);
192
+ return;
193
+ }
194
+
195
+ const computePass = this.handle.beginComputePass({
196
+ timestampWrites: {
197
+ querySet: querySet.handle,
198
+ beginningOfPassWriteIndex: queryIndex
199
+ }
200
+ });
201
+ computePass.end();
202
+ }
177
203
  }
178
204
 
179
205
  /*
@@ -14,19 +14,21 @@ export class WebGPUComputePass extends ComputePass {
14
14
 
15
15
  _webgpuPipeline: WebGPUComputePipeline | null = null;
16
16
 
17
- constructor(device: WebGPUDevice, props: ComputePassProps) {
17
+ constructor(device: WebGPUDevice, props: ComputePassProps = {}) {
18
18
  super(device, props);
19
19
  this.device = device;
20
+ const {props: computePassProps} = this;
20
21
 
21
22
  // Set up queries
22
23
  let timestampWrites: GPUComputePassTimestampWrites | undefined;
23
- if (device.features.has('timestamp-query')) {
24
- const webgpuQuerySet = props.timestampQuerySet as WebGPUQuerySet;
24
+ if (computePassProps.timestampQuerySet) {
25
+ const webgpuQuerySet = computePassProps.timestampQuerySet as WebGPUQuerySet;
25
26
  if (webgpuQuerySet) {
27
+ webgpuQuerySet._invalidateResults();
26
28
  timestampWrites = {
27
29
  querySet: webgpuQuerySet.handle,
28
- beginningOfPassWriteIndex: props.beginTimestampIndex,
29
- endOfPassWriteIndex: props.endTimestampIndex
30
+ beginningOfPassWriteIndex: computePassProps.beginTimestampIndex,
31
+ endOfPassWriteIndex: computePassProps.endTimestampIndex
30
32
  };
31
33
  }
32
34
  }
@@ -40,10 +42,16 @@ export class WebGPUComputePass extends ComputePass {
40
42
  }
41
43
 
42
44
  /** @note no WebGPU destroy method, just gc */
43
- override destroy(): void {}
45
+ override destroy(): void {
46
+ this.destroyResource();
47
+ }
44
48
 
45
49
  end(): void {
50
+ if (this.destroyed) {
51
+ return;
52
+ }
46
53
  this.handle.end();
54
+ this.destroy();
47
55
  }
48
56
 
49
57
  setPipeline(pipeline: ComputePipeline): void {
@@ -7,6 +7,8 @@ import {getBindGroup} from '../helpers/get-bind-group';
7
7
  import {WebGPUDevice} from '../webgpu-device';
8
8
  import {WebGPUShader} from './webgpu-shader';
9
9
 
10
+ const EMPTY_BINDINGS: Record<string, Binding> = {};
11
+
10
12
  // COMPUTE PIPELINE
11
13
 
12
14
  /** Creates a new compute pipeline when parameters change */
@@ -18,7 +20,7 @@ export class WebGPUComputePipeline extends ComputePipeline {
18
20
  private _bindGroupLayout: GPUBindGroupLayout | null = null;
19
21
  private _bindGroup: GPUBindGroup | null = null;
20
22
  /** For internal use to create BindGroups */
21
- private _bindings: Record<string, Binding> = {};
23
+ private _bindings: Record<string, Binding>;
22
24
 
23
25
  constructor(device: WebGPUDevice, props: ComputePipelineProps) {
24
26
  super(device, props);
@@ -37,6 +39,8 @@ export class WebGPUComputePipeline extends ComputePipeline {
37
39
  },
38
40
  layout: 'auto'
39
41
  });
42
+
43
+ this._bindings = EMPTY_BINDINGS;
40
44
  }
41
45
 
42
46
  /**
@@ -44,7 +48,21 @@ export class WebGPUComputePipeline extends ComputePipeline {
44
48
  * @todo Do we want to expose BindGroups in the API and remove this?
45
49
  */
46
50
  setBindings(bindings: Record<string, Binding>): void {
47
- Object.assign(this._bindings, bindings);
51
+ let bindingsChanged = false;
52
+ for (const [name, binding] of Object.entries(bindings)) {
53
+ if (this._bindings[name] !== binding) {
54
+ if (!bindingsChanged) {
55
+ if (this._bindings === EMPTY_BINDINGS) {
56
+ this._bindings = {};
57
+ }
58
+ bindingsChanged = true;
59
+ }
60
+ this._bindings[name] = binding;
61
+ }
62
+ }
63
+ if (bindingsChanged) {
64
+ this._bindGroup = null;
65
+ }
48
66
  }
49
67
 
50
68
  /** Return a bind group created by setBindings */
@@ -55,7 +73,7 @@ export class WebGPUComputePipeline extends ComputePipeline {
55
73
  // Set up the bindings
56
74
  this._bindGroup =
57
75
  this._bindGroup ||
58
- getBindGroup(this.device.handle, this._bindGroupLayout, this.shaderLayout, this._bindings);
76
+ getBindGroup(this.device, this._bindGroupLayout, this.shaderLayout, this._bindings);
59
77
 
60
78
  return this._bindGroup;
61
79
  }
@@ -29,4 +29,25 @@ export class WebGPUFramebuffer extends Framebuffer {
29
29
  protected updateAttachments(): void {
30
30
  // WebGPU framebuffers are JS only objects, nothing to update
31
31
  }
32
+
33
+ /**
34
+ * Internal-only hook for the cached CanvasContext/PresentationContext swapchain path.
35
+ * Rebinds the long-lived default framebuffer wrapper to the current per-frame color view
36
+ * and optional depth attachment without allocating a new luma.gl Framebuffer object.
37
+ */
38
+ _reinitialize(
39
+ colorAttachment: WebGPUTextureView,
40
+ depthStencilAttachment: WebGPUTextureView | null
41
+ ): void {
42
+ this.colorAttachments[0] = colorAttachment;
43
+ // @ts-expect-error Internal-only canvas wrapper reuse mutates this otherwise-readonly attachment.
44
+ this.depthStencilAttachment = depthStencilAttachment;
45
+ this.width = colorAttachment.texture.width;
46
+ this.height = colorAttachment.texture.height;
47
+
48
+ this.props.width = this.width;
49
+ this.props.height = this.height;
50
+ this.props.colorAttachments = [colorAttachment.texture];
51
+ this.props.depthStencilAttachment = depthStencilAttachment?.texture || null;
52
+ }
32
53
  }
@@ -47,8 +47,7 @@ export class WebGPUPipelineLayout extends PipelineLayout {
47
47
  // TODO (kaapp): This only supports the first group, but so does the rest of the code
48
48
  const bindGroupEntries: GPUBindGroupLayoutEntry[] = [];
49
49
 
50
- for (let i = 0; i < this.props.shaderLayout.bindings.length; i++) {
51
- const binding = this.props.shaderLayout.bindings[i];
50
+ for (const binding of this.props.shaderLayout.bindings) {
52
51
  const bindingTypeInfo: Omit<GPUBindGroupLayoutEntry, 'binding' | 'visibility'> = {};
53
52
 
54
53
  switch (binding.type) {