@luma.gl/webgpu 9.3.0-alpha.4 → 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 (93) 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 +22 -18
  12. package/dist/adapter/helpers/get-bind-group.js.map +1 -1
  13. package/dist/adapter/resources/webgpu-buffer.d.ts.map +1 -1
  14. package/dist/adapter/resources/webgpu-buffer.js +19 -3
  15. package/dist/adapter/resources/webgpu-buffer.js.map +1 -1
  16. package/dist/adapter/resources/webgpu-command-buffer.js +1 -1
  17. package/dist/adapter/resources/webgpu-command-buffer.js.map +1 -1
  18. package/dist/adapter/resources/webgpu-command-encoder.d.ts +5 -4
  19. package/dist/adapter/resources/webgpu-command-encoder.d.ts.map +1 -1
  20. package/dist/adapter/resources/webgpu-command-encoder.js +23 -5
  21. package/dist/adapter/resources/webgpu-command-encoder.js.map +1 -1
  22. package/dist/adapter/resources/webgpu-compute-pass.d.ts +1 -1
  23. package/dist/adapter/resources/webgpu-compute-pass.d.ts.map +1 -1
  24. package/dist/adapter/resources/webgpu-compute-pass.js +14 -6
  25. package/dist/adapter/resources/webgpu-compute-pass.js.map +1 -1
  26. package/dist/adapter/resources/webgpu-compute-pipeline.d.ts.map +1 -1
  27. package/dist/adapter/resources/webgpu-compute-pipeline.js +19 -3
  28. package/dist/adapter/resources/webgpu-compute-pipeline.js.map +1 -1
  29. package/dist/adapter/resources/webgpu-framebuffer.d.ts +6 -0
  30. package/dist/adapter/resources/webgpu-framebuffer.d.ts.map +1 -1
  31. package/dist/adapter/resources/webgpu-framebuffer.js +16 -0
  32. package/dist/adapter/resources/webgpu-framebuffer.js.map +1 -1
  33. package/dist/adapter/resources/webgpu-query-set.d.ts +33 -4
  34. package/dist/adapter/resources/webgpu-query-set.d.ts.map +1 -1
  35. package/dist/adapter/resources/webgpu-query-set.js +145 -4
  36. package/dist/adapter/resources/webgpu-query-set.js.map +1 -1
  37. package/dist/adapter/resources/webgpu-render-pass.d.ts +3 -0
  38. package/dist/adapter/resources/webgpu-render-pass.d.ts.map +1 -1
  39. package/dist/adapter/resources/webgpu-render-pass.js +74 -30
  40. package/dist/adapter/resources/webgpu-render-pass.js.map +1 -1
  41. package/dist/adapter/resources/webgpu-render-pipeline.d.ts +7 -4
  42. package/dist/adapter/resources/webgpu-render-pipeline.d.ts.map +1 -1
  43. package/dist/adapter/resources/webgpu-render-pipeline.js +26 -15
  44. package/dist/adapter/resources/webgpu-render-pipeline.js.map +1 -1
  45. package/dist/adapter/resources/webgpu-sampler.d.ts.map +1 -1
  46. package/dist/adapter/resources/webgpu-sampler.js +4 -0
  47. package/dist/adapter/resources/webgpu-sampler.js.map +1 -1
  48. package/dist/adapter/resources/webgpu-texture-view.d.ts +6 -0
  49. package/dist/adapter/resources/webgpu-texture-view.d.ts.map +1 -1
  50. package/dist/adapter/resources/webgpu-texture-view.js +47 -11
  51. package/dist/adapter/resources/webgpu-texture-view.js.map +1 -1
  52. package/dist/adapter/resources/webgpu-texture.d.ts +10 -4
  53. package/dist/adapter/resources/webgpu-texture.d.ts.map +1 -1
  54. package/dist/adapter/resources/webgpu-texture.js +116 -57
  55. package/dist/adapter/resources/webgpu-texture.js.map +1 -1
  56. package/dist/adapter/resources/webgpu-vertex-array.js +1 -1
  57. package/dist/adapter/resources/webgpu-vertex-array.js.map +1 -1
  58. package/dist/adapter/webgpu-canvas-context.d.ts +2 -0
  59. package/dist/adapter/webgpu-canvas-context.d.ts.map +1 -1
  60. package/dist/adapter/webgpu-canvas-context.js +78 -19
  61. package/dist/adapter/webgpu-canvas-context.js.map +1 -1
  62. package/dist/adapter/webgpu-device.d.ts +5 -1
  63. package/dist/adapter/webgpu-device.d.ts.map +1 -1
  64. package/dist/adapter/webgpu-device.js +113 -9
  65. package/dist/adapter/webgpu-device.js.map +1 -1
  66. package/dist/adapter/webgpu-presentation-context.d.ts +25 -0
  67. package/dist/adapter/webgpu-presentation-context.d.ts.map +1 -0
  68. package/dist/adapter/webgpu-presentation-context.js +144 -0
  69. package/dist/adapter/webgpu-presentation-context.js.map +1 -0
  70. package/dist/dist.dev.js +2815 -1681
  71. package/dist/dist.min.js +167 -8
  72. package/dist/index.cjs +1314 -199
  73. package/dist/index.cjs.map +4 -4
  74. package/package.json +4 -4
  75. package/src/adapter/helpers/cpu-hotspot-profiler.ts +70 -0
  76. package/src/adapter/helpers/generate-mipmaps-webgpu.ts +583 -0
  77. package/src/adapter/helpers/get-bind-group.ts +26 -24
  78. package/src/adapter/resources/webgpu-buffer.ts +18 -3
  79. package/src/adapter/resources/webgpu-command-buffer.ts +1 -1
  80. package/src/adapter/resources/webgpu-command-encoder.ts +32 -6
  81. package/src/adapter/resources/webgpu-compute-pass.ts +14 -6
  82. package/src/adapter/resources/webgpu-compute-pipeline.ts +21 -3
  83. package/src/adapter/resources/webgpu-framebuffer.ts +21 -0
  84. package/src/adapter/resources/webgpu-query-set.ts +185 -9
  85. package/src/adapter/resources/webgpu-render-pass.ts +82 -34
  86. package/src/adapter/resources/webgpu-render-pipeline.ts +36 -19
  87. package/src/adapter/resources/webgpu-sampler.ts +5 -0
  88. package/src/adapter/resources/webgpu-texture-view.ts +51 -11
  89. package/src/adapter/resources/webgpu-texture.ts +142 -93
  90. package/src/adapter/resources/webgpu-vertex-array.ts +1 -1
  91. package/src/adapter/webgpu-canvas-context.ts +91 -26
  92. package/src/adapter/webgpu-device.ts +128 -9
  93. package/src/adapter/webgpu-presentation-context.ts +180 -0
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type {ComputeShaderLayout, BindingDeclaration, Binding} from '@luma.gl/core';
6
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';
@@ -29,21 +30,19 @@ export function makeBindGroupLayout(
29
30
  * Create a WebGPU "bind group" from an array of luma.gl bindings
30
31
  */
31
32
  export function getBindGroup(
32
- device: GPUDevice,
33
+ device: WebGPUDevice,
33
34
  bindGroupLayout: GPUBindGroupLayout,
34
35
  shaderLayout: ComputeShaderLayout,
35
36
  bindings: Record<string, Binding>
36
37
  ): GPUBindGroup {
37
38
  const entries = getBindGroupEntries(bindings, shaderLayout);
38
39
  device.pushErrorScope('validation');
39
- const bindGroup = device.createBindGroup({
40
+ const bindGroup = device.handle.createBindGroup({
40
41
  layout: bindGroupLayout,
41
42
  entries
42
43
  });
43
- device.popErrorScope().then((error: GPUError | null) => {
44
- if (error) {
45
- log.error(`bindGroup creation: ${error.message}`, bindGroup)();
46
- }
44
+ device.popErrorScope((error: GPUError) => {
45
+ log.error(`bindGroup creation: ${error.message}`, bindGroup)();
47
46
  });
48
47
  return bindGroup;
49
48
  }
@@ -75,28 +74,31 @@ function getBindGroupEntries(
75
74
  const entries: GPUBindGroupEntry[] = [];
76
75
 
77
76
  for (const [bindingName, value] of Object.entries(bindings)) {
78
- let bindingLayout = getShaderLayoutBinding(shaderLayout, bindingName);
79
- if (bindingLayout) {
80
- const entry = getBindGroupEntry(value, bindingLayout.location, undefined, bindingName);
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;
81
88
  if (entry) {
82
89
  entries.push(entry);
83
90
  }
84
- }
85
91
 
86
- // TODO - hack to automatically bind samplers to supplied texture default samplers
87
- if (value instanceof Texture) {
88
- bindingLayout = getShaderLayoutBinding(shaderLayout, `${bindingName}Sampler`, {
89
- ignoreWarnings: true
90
- });
91
- if (bindingLayout) {
92
- const entry = getBindGroupEntry(
93
- value,
94
- bindingLayout.location,
95
- {sampler: true},
96
- bindingName
97
- );
98
- if (entry) {
99
- 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);
100
102
  }
101
103
  }
102
104
  }
@@ -67,12 +67,27 @@ export class WebGPUBuffer extends Buffer {
67
67
  this.device.reportError(new Error(`${this} creation failed ${error.message}`), this)();
68
68
  this.device.debug();
69
69
  });
70
+
71
+ if (!this.props.handle) {
72
+ this.trackAllocatedMemory(size);
73
+ } else {
74
+ this.trackReferencedMemory(size, 'Buffer');
75
+ }
70
76
  }
71
77
 
72
78
  override destroy(): void {
73
- this.handle?.destroy();
74
- // @ts-expect-error readonly
75
- 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
+ }
76
91
  }
77
92
 
78
93
  write(data: ArrayBufferLike | ArrayBufferView | SharedArrayBuffer, byteOffset = 0) {
@@ -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
  }
@@ -2,13 +2,13 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {QuerySet, QuerySetProps} from '@luma.gl/core';
5
+ import {Buffer, QuerySet, QuerySetProps} from '@luma.gl/core';
6
6
  import {WebGPUDevice} from '../webgpu-device';
7
-
8
- export type QuerySetProps2 = {
9
- type: 'occlusion' | 'timestamp';
10
- count: number;
11
- };
7
+ import {WebGPUBuffer} from './webgpu-buffer';
8
+ import {
9
+ getCpuHotspotSubmitReason,
10
+ setCpuHotspotSubmitReason
11
+ } from '../helpers/cpu-hotspot-profiler';
12
12
 
13
13
  /**
14
14
  * Immutable
@@ -17,6 +17,12 @@ export class WebGPUQuerySet extends QuerySet {
17
17
  readonly device: WebGPUDevice;
18
18
  readonly handle: GPUQuerySet;
19
19
 
20
+ protected _resolveBuffer: WebGPUBuffer | null = null;
21
+ protected _readBuffer: WebGPUBuffer | null = null;
22
+ protected _cachedResults: bigint[] | null = null;
23
+ protected _readResultsPromise: Promise<bigint[]> | null = null;
24
+ protected _resultsPendingResolution: boolean = false;
25
+
20
26
  constructor(device: WebGPUDevice, props: QuerySetProps) {
21
27
  super(device, props);
22
28
  this.device = device;
@@ -30,8 +36,178 @@ export class WebGPUQuerySet extends QuerySet {
30
36
  }
31
37
 
32
38
  override destroy(): void {
33
- this.handle?.destroy();
34
- // @ts-expect-error readonly
35
- this.handle = null;
39
+ if (!this.destroyed) {
40
+ this.handle?.destroy();
41
+ this.destroyResource();
42
+ // @ts-expect-error readonly
43
+ this.handle = null;
44
+ }
45
+ }
46
+
47
+ isResultAvailable(queryIndex?: number): boolean {
48
+ if (!this._cachedResults) {
49
+ return false;
50
+ }
51
+
52
+ return queryIndex === undefined
53
+ ? true
54
+ : queryIndex >= 0 && queryIndex < this._cachedResults.length;
55
+ }
56
+
57
+ async readResults(options?: {firstQuery?: number; queryCount?: number}): Promise<bigint[]> {
58
+ const firstQuery = options?.firstQuery || 0;
59
+ const queryCount = options?.queryCount || this.props.count - firstQuery;
60
+
61
+ if (firstQuery < 0 || queryCount < 0 || firstQuery + queryCount > this.props.count) {
62
+ throw new Error('Query read range is out of bounds');
63
+ }
64
+
65
+ let needsFreshResults = true;
66
+ while (needsFreshResults) {
67
+ if (!this._readResultsPromise) {
68
+ this._readResultsPromise = this._readAllResults();
69
+ }
70
+
71
+ const readResultsPromise = this._readResultsPromise;
72
+ const results = await readResultsPromise;
73
+
74
+ // A later submit may have invalidated the query set while this read was in flight.
75
+ // Retry so each caller observes the freshest resolved results instead of stale data.
76
+ needsFreshResults = this._resultsPendingResolution;
77
+ if (!needsFreshResults) {
78
+ return results.slice(firstQuery, firstQuery + queryCount);
79
+ }
80
+ }
81
+
82
+ throw new Error('Query read unexpectedly failed to resolve');
83
+ }
84
+
85
+ async readTimestampDuration(beginIndex: number, endIndex: number): Promise<number> {
86
+ if (this.props.type !== 'timestamp') {
87
+ throw new Error('Timestamp durations require a timestamp QuerySet');
88
+ }
89
+ if (beginIndex < 0 || endIndex <= beginIndex || endIndex >= this.props.count) {
90
+ throw new Error('Timestamp duration range is out of bounds');
91
+ }
92
+
93
+ const results = await this.readResults({
94
+ firstQuery: beginIndex,
95
+ queryCount: endIndex - beginIndex + 1
96
+ });
97
+ return Number(results[results.length - 1] - results[0]) / 1e6;
98
+ }
99
+
100
+ /** Marks any cached query results as stale after new writes have been encoded. */
101
+ _invalidateResults(): void {
102
+ this._cachedResults = null;
103
+ this._resultsPendingResolution = true;
104
+ }
105
+
106
+ protected async _readAllResults(): Promise<bigint[]> {
107
+ this._ensureBuffers();
108
+
109
+ try {
110
+ // Use a dedicated encoder so async profiling reads cannot flush or replace the
111
+ // device's active frame encoder while application rendering is in flight.
112
+ if (this._resultsPendingResolution) {
113
+ const commandEncoder = this.device.createCommandEncoder({
114
+ id: `${this.id}-read-results`
115
+ });
116
+ commandEncoder.resolveQuerySet(this, this._resolveBuffer!);
117
+ commandEncoder.copyBufferToBuffer({
118
+ sourceBuffer: this._resolveBuffer!,
119
+ destinationBuffer: this._readBuffer!,
120
+ size: this._resolveBuffer!.byteLength
121
+ });
122
+ const commandBuffer = commandEncoder.finish({
123
+ id: `${this.id}-read-results-command-buffer`
124
+ });
125
+ const previousSubmitReason = getCpuHotspotSubmitReason(this.device) || undefined;
126
+ setCpuHotspotSubmitReason(this.device, 'query-readback');
127
+ try {
128
+ this.device.submit(commandBuffer);
129
+ } finally {
130
+ setCpuHotspotSubmitReason(this.device, previousSubmitReason);
131
+ }
132
+ }
133
+
134
+ const data = await this._readBuffer!.readAsync(0, this._readBuffer!.byteLength);
135
+ const resultView = new BigUint64Array(data.buffer, data.byteOffset, this.props.count);
136
+ this._cachedResults = Array.from(resultView, value => value);
137
+ this._resultsPendingResolution = false;
138
+ return this._cachedResults;
139
+ } finally {
140
+ this._readResultsPromise = null;
141
+ }
142
+ }
143
+
144
+ protected _ensureBuffers(): void {
145
+ if (this._resolveBuffer && this._readBuffer) {
146
+ return;
147
+ }
148
+
149
+ const byteLength = this.props.count * 8;
150
+ this._resolveBuffer = this.device.createBuffer({
151
+ id: `${this.id}-resolve-buffer`,
152
+ usage: Buffer.QUERY_RESOLVE | Buffer.COPY_SRC,
153
+ byteLength
154
+ });
155
+ this.attachResource(this._resolveBuffer);
156
+
157
+ this._readBuffer = this.device.createBuffer({
158
+ id: `${this.id}-read-buffer`,
159
+ usage: Buffer.COPY_DST | Buffer.MAP_READ,
160
+ byteLength
161
+ });
162
+ this.attachResource(this._readBuffer);
163
+ }
164
+
165
+ _encodeResolveToReadBuffer(
166
+ commandEncoder: {
167
+ resolveQuerySet: (
168
+ querySet: WebGPUQuerySet,
169
+ destination: WebGPUBuffer,
170
+ options?: {firstQuery?: number; queryCount?: number; destinationOffset?: number}
171
+ ) => void;
172
+ copyBufferToBuffer: (options: {
173
+ sourceBuffer: WebGPUBuffer;
174
+ destinationBuffer: WebGPUBuffer;
175
+ sourceOffset?: number;
176
+ destinationOffset?: number;
177
+ size?: number;
178
+ }) => void;
179
+ },
180
+ options?: {firstQuery?: number; queryCount?: number}
181
+ ): boolean {
182
+ if (!this._resultsPendingResolution) {
183
+ return false;
184
+ }
185
+
186
+ // If a readback is already mapping the shared read buffer, defer to the fallback read path.
187
+ // That path will submit resolve/copy commands once the current read has completed.
188
+ if (this._readResultsPromise) {
189
+ return false;
190
+ }
191
+
192
+ this._ensureBuffers();
193
+ const firstQuery = options?.firstQuery || 0;
194
+ const queryCount = options?.queryCount || this.props.count - firstQuery;
195
+ const byteLength = queryCount * BigUint64Array.BYTES_PER_ELEMENT;
196
+ const byteOffset = firstQuery * BigUint64Array.BYTES_PER_ELEMENT;
197
+
198
+ commandEncoder.resolveQuerySet(this, this._resolveBuffer!, {
199
+ firstQuery,
200
+ queryCount,
201
+ destinationOffset: byteOffset
202
+ });
203
+ commandEncoder.copyBufferToBuffer({
204
+ sourceBuffer: this._resolveBuffer!,
205
+ sourceOffset: byteOffset,
206
+ destinationBuffer: this._readBuffer!,
207
+ destinationOffset: byteOffset,
208
+ size: byteLength
209
+ });
210
+ this._resultsPendingResolution = false;
211
+ return true;
36
212
  }
37
213
  }