@luma.gl/webgpu 9.2.6 → 9.3.0-alpha.10

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 (137) 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 +4 -6
  10. package/dist/adapter/helpers/get-bind-group.d.ts.map +1 -1
  11. package/dist/adapter/helpers/get-bind-group.js +39 -32
  12. package/dist/adapter/helpers/get-bind-group.js.map +1 -1
  13. package/dist/adapter/helpers/get-vertex-buffer-layout.d.ts +3 -1
  14. package/dist/adapter/helpers/get-vertex-buffer-layout.d.ts.map +1 -1
  15. package/dist/adapter/helpers/get-vertex-buffer-layout.js +17 -12
  16. package/dist/adapter/helpers/get-vertex-buffer-layout.js.map +1 -1
  17. package/dist/adapter/helpers/webgpu-parameters.d.ts.map +1 -1
  18. package/dist/adapter/helpers/webgpu-parameters.js +1 -0
  19. package/dist/adapter/helpers/webgpu-parameters.js.map +1 -1
  20. package/dist/adapter/resources/webgpu-buffer.d.ts +7 -0
  21. package/dist/adapter/resources/webgpu-buffer.d.ts.map +1 -1
  22. package/dist/adapter/resources/webgpu-buffer.js +58 -15
  23. package/dist/adapter/resources/webgpu-buffer.js.map +1 -1
  24. package/dist/adapter/resources/webgpu-command-buffer.js +1 -1
  25. package/dist/adapter/resources/webgpu-command-buffer.js.map +1 -1
  26. package/dist/adapter/resources/webgpu-command-encoder.d.ts +7 -16
  27. package/dist/adapter/resources/webgpu-command-encoder.d.ts.map +1 -1
  28. package/dist/adapter/resources/webgpu-command-encoder.js +89 -32
  29. package/dist/adapter/resources/webgpu-command-encoder.js.map +1 -1
  30. package/dist/adapter/resources/webgpu-compute-pass.d.ts +3 -3
  31. package/dist/adapter/resources/webgpu-compute-pass.d.ts.map +1 -1
  32. package/dist/adapter/resources/webgpu-compute-pass.js +30 -12
  33. package/dist/adapter/resources/webgpu-compute-pass.js.map +1 -1
  34. package/dist/adapter/resources/webgpu-compute-pipeline.d.ts +7 -9
  35. package/dist/adapter/resources/webgpu-compute-pipeline.d.ts.map +1 -1
  36. package/dist/adapter/resources/webgpu-compute-pipeline.js +30 -17
  37. package/dist/adapter/resources/webgpu-compute-pipeline.js.map +1 -1
  38. package/dist/adapter/resources/webgpu-fence.d.ts +13 -0
  39. package/dist/adapter/resources/webgpu-fence.d.ts.map +1 -0
  40. package/dist/adapter/resources/webgpu-fence.js +33 -0
  41. package/dist/adapter/resources/webgpu-fence.js.map +1 -0
  42. package/dist/adapter/resources/webgpu-framebuffer.d.ts +6 -0
  43. package/dist/adapter/resources/webgpu-framebuffer.d.ts.map +1 -1
  44. package/dist/adapter/resources/webgpu-framebuffer.js +16 -0
  45. package/dist/adapter/resources/webgpu-framebuffer.js.map +1 -1
  46. package/dist/adapter/resources/webgpu-pipeline-layout.d.ts +1 -1
  47. package/dist/adapter/resources/webgpu-pipeline-layout.d.ts.map +1 -1
  48. package/dist/adapter/resources/webgpu-pipeline-layout.js +11 -18
  49. package/dist/adapter/resources/webgpu-pipeline-layout.js.map +1 -1
  50. package/dist/adapter/resources/webgpu-query-set.d.ts +33 -4
  51. package/dist/adapter/resources/webgpu-query-set.d.ts.map +1 -1
  52. package/dist/adapter/resources/webgpu-query-set.js +145 -4
  53. package/dist/adapter/resources/webgpu-query-set.js.map +1 -1
  54. package/dist/adapter/resources/webgpu-render-pass.d.ts +6 -3
  55. package/dist/adapter/resources/webgpu-render-pass.d.ts.map +1 -1
  56. package/dist/adapter/resources/webgpu-render-pass.js +78 -34
  57. package/dist/adapter/resources/webgpu-render-pass.js.map +1 -1
  58. package/dist/adapter/resources/webgpu-render-pipeline.d.ts +14 -10
  59. package/dist/adapter/resources/webgpu-render-pipeline.d.ts.map +1 -1
  60. package/dist/adapter/resources/webgpu-render-pipeline.js +56 -35
  61. package/dist/adapter/resources/webgpu-render-pipeline.js.map +1 -1
  62. package/dist/adapter/resources/webgpu-sampler.d.ts.map +1 -1
  63. package/dist/adapter/resources/webgpu-sampler.js +4 -0
  64. package/dist/adapter/resources/webgpu-sampler.js.map +1 -1
  65. package/dist/adapter/resources/webgpu-shader.d.ts.map +1 -1
  66. package/dist/adapter/resources/webgpu-shader.js +17 -1
  67. package/dist/adapter/resources/webgpu-shader.js.map +1 -1
  68. package/dist/adapter/resources/webgpu-texture-view.d.ts +6 -0
  69. package/dist/adapter/resources/webgpu-texture-view.d.ts.map +1 -1
  70. package/dist/adapter/resources/webgpu-texture-view.js +47 -11
  71. package/dist/adapter/resources/webgpu-texture-view.js.map +1 -1
  72. package/dist/adapter/resources/webgpu-texture.d.ts +25 -3
  73. package/dist/adapter/resources/webgpu-texture.d.ts.map +1 -1
  74. package/dist/adapter/resources/webgpu-texture.js +211 -43
  75. package/dist/adapter/resources/webgpu-texture.js.map +1 -1
  76. package/dist/adapter/resources/webgpu-vertex-array.js +1 -1
  77. package/dist/adapter/resources/webgpu-vertex-array.js.map +1 -1
  78. package/dist/adapter/webgpu-adapter.d.ts.map +1 -1
  79. package/dist/adapter/webgpu-adapter.js +34 -34
  80. package/dist/adapter/webgpu-adapter.js.map +1 -1
  81. package/dist/adapter/webgpu-canvas-context.d.ts +6 -3
  82. package/dist/adapter/webgpu-canvas-context.d.ts.map +1 -1
  83. package/dist/adapter/webgpu-canvas-context.js +90 -30
  84. package/dist/adapter/webgpu-canvas-context.js.map +1 -1
  85. package/dist/adapter/webgpu-device.d.ts +12 -2
  86. package/dist/adapter/webgpu-device.d.ts.map +1 -1
  87. package/dist/adapter/webgpu-device.js +173 -16
  88. package/dist/adapter/webgpu-device.js.map +1 -1
  89. package/dist/adapter/webgpu-presentation-context.d.ts +25 -0
  90. package/dist/adapter/webgpu-presentation-context.d.ts.map +1 -0
  91. package/dist/adapter/webgpu-presentation-context.js +144 -0
  92. package/dist/adapter/webgpu-presentation-context.js.map +1 -0
  93. package/dist/dist.dev.js +8070 -547
  94. package/dist/dist.min.js +169 -6
  95. package/dist/index.cjs +1929 -410
  96. package/dist/index.cjs.map +4 -4
  97. package/dist/index.d.ts +2 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/wgsl/get-shader-layout-wgsl.d.ts +8 -0
  102. package/dist/wgsl/get-shader-layout-wgsl.d.ts.map +1 -0
  103. package/dist/wgsl/get-shader-layout-wgsl.js +144 -0
  104. package/dist/wgsl/get-shader-layout-wgsl.js.map +1 -0
  105. package/package.json +6 -5
  106. package/src/adapter/helpers/cpu-hotspot-profiler.ts +70 -0
  107. package/src/adapter/helpers/generate-mipmaps-webgpu.ts +583 -0
  108. package/src/adapter/helpers/get-bind-group.ts +52 -46
  109. package/src/adapter/helpers/get-vertex-buffer-layout.ts +31 -12
  110. package/src/adapter/helpers/webgpu-parameters.ts +2 -0
  111. package/src/adapter/resources/webgpu-buffer.ts +61 -15
  112. package/src/adapter/resources/webgpu-command-buffer.ts +1 -1
  113. package/src/adapter/resources/webgpu-command-encoder.ts +129 -50
  114. package/src/adapter/resources/webgpu-compute-pass.ts +48 -13
  115. package/src/adapter/resources/webgpu-compute-pipeline.ts +49 -18
  116. package/src/adapter/resources/webgpu-fence.ts +38 -0
  117. package/src/adapter/resources/webgpu-framebuffer.ts +21 -0
  118. package/src/adapter/resources/webgpu-pipeline-layout.ts +18 -17
  119. package/src/adapter/resources/webgpu-query-set.ts +185 -9
  120. package/src/adapter/resources/webgpu-render-pass.ts +92 -40
  121. package/src/adapter/resources/webgpu-render-pipeline.ts +83 -44
  122. package/src/adapter/resources/webgpu-sampler.ts +5 -0
  123. package/src/adapter/resources/webgpu-shader.ts +16 -1
  124. package/src/adapter/resources/webgpu-texture-view.ts +51 -11
  125. package/src/adapter/resources/webgpu-texture.ts +281 -101
  126. package/src/adapter/resources/webgpu-vertex-array.ts +1 -1
  127. package/src/adapter/webgpu-adapter.ts +40 -42
  128. package/src/adapter/webgpu-canvas-context.ts +107 -40
  129. package/src/adapter/webgpu-device.ts +231 -21
  130. package/src/adapter/webgpu-presentation-context.ts +180 -0
  131. package/src/index.ts +3 -0
  132. package/src/wgsl/get-shader-layout-wgsl.ts +165 -0
  133. package/dist/adapter/helpers/accessor-to-format.d.ts +0 -1
  134. package/dist/adapter/helpers/accessor-to-format.d.ts.map +0 -1
  135. package/dist/adapter/helpers/accessor-to-format.js +0 -105
  136. package/dist/adapter/helpers/accessor-to-format.js.map +0 -1
  137. package/src/adapter/helpers/accessor-to-format.ts +0 -104
@@ -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
  }
@@ -3,67 +3,114 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import type {TypedArray, NumberArray4} from '@math.gl/types';
6
- import type {RenderPassProps, RenderPassParameters, Binding} from '@luma.gl/core';
7
- import {Buffer, RenderPass, RenderPipeline, log} from '@luma.gl/core';
6
+ import type {RenderPassProps, RenderPassParameters, Bindings, BindingsByGroup} from '@luma.gl/core';
7
+ import {Buffer, RenderPass, RenderPipeline, _getDefaultBindGroupFactory, log} from '@luma.gl/core';
8
8
  import {WebGPUDevice} from '../webgpu-device';
9
9
  import {WebGPUBuffer} from './webgpu-buffer';
10
10
  // import {WebGPUCommandEncoder} from './webgpu-command-encoder';
11
11
  import {WebGPURenderPipeline} from './webgpu-render-pipeline';
12
12
  import {WebGPUQuerySet} from './webgpu-query-set';
13
13
  import {WebGPUFramebuffer} from './webgpu-framebuffer';
14
+ import {getCpuHotspotProfiler, getTimestamp} from '../helpers/cpu-hotspot-profiler';
14
15
 
15
16
  export class WebGPURenderPass extends RenderPass {
16
17
  readonly device: WebGPUDevice;
17
18
  readonly handle: GPURenderPassEncoder;
19
+ readonly framebuffer: WebGPUFramebuffer;
18
20
 
19
21
  /** Active pipeline */
20
22
  pipeline: WebGPURenderPipeline | null = null;
21
23
 
22
- constructor(device: WebGPUDevice, props: RenderPassProps = {}) {
24
+ /** Latest bindings applied to this pass */
25
+ bindings: Bindings | BindingsByGroup = {};
26
+
27
+ constructor(
28
+ device: WebGPUDevice,
29
+ props: RenderPassProps = {},
30
+ commandEncoder: GPUCommandEncoder = device.commandEncoder.handle
31
+ ) {
23
32
  super(device, props);
24
33
  this.device = device;
25
- const framebuffer =
26
- (props.framebuffer as WebGPUFramebuffer) || device.getCanvasContext().getCurrentFramebuffer();
27
-
28
- const renderPassDescriptor = this.getRenderPassDescriptor(framebuffer);
34
+ const {props: renderPassProps} = this;
35
+ this.framebuffer =
36
+ (renderPassProps.framebuffer as WebGPUFramebuffer) ||
37
+ device.getCanvasContext().getCurrentFramebuffer();
29
38
 
30
- const webgpuQuerySet = props.timestampQuerySet as WebGPUQuerySet;
31
- if (webgpuQuerySet) {
32
- renderPassDescriptor.occlusionQuerySet = webgpuQuerySet.handle;
39
+ const profiler = getCpuHotspotProfiler(this.device);
40
+ if (profiler) {
41
+ const counterName:
42
+ | 'explicitFramebufferRenderPassCount'
43
+ | 'defaultFramebufferRenderPassCount' = renderPassProps.framebuffer
44
+ ? 'explicitFramebufferRenderPassCount'
45
+ : 'defaultFramebufferRenderPassCount';
46
+ profiler[counterName] = (profiler[counterName] || 0) + 1;
33
47
  }
34
48
 
35
- if (device.features.has('timestamp-query')) {
36
- const webgpuTSQuerySet = props.timestampQuerySet as WebGPUQuerySet;
37
- renderPassDescriptor.timestampWrites = webgpuTSQuerySet
38
- ? ({
39
- querySet: webgpuTSQuerySet.handle,
40
- beginningOfPassWriteIndex: props.beginTimestampIndex,
41
- endOfPassWriteIndex: props.endTimestampIndex
42
- } as GPUComputePassTimestampWrites)
43
- : undefined;
44
- }
49
+ const startTime = profiler ? getTimestamp() : 0;
50
+ try {
51
+ const descriptorAssemblyStartTime = profiler ? getTimestamp() : 0;
52
+ const renderPassDescriptor = this.getRenderPassDescriptor(this.framebuffer);
45
53
 
46
- if (!device.commandEncoder) {
47
- throw new Error('commandEncoder not available');
48
- }
54
+ if (renderPassProps.occlusionQuerySet) {
55
+ renderPassDescriptor.occlusionQuerySet = (
56
+ renderPassProps.occlusionQuerySet as WebGPUQuerySet
57
+ ).handle;
58
+ }
49
59
 
50
- this.device.pushErrorScope('validation');
51
- this.handle =
52
- this.props.handle || device.commandEncoder.handle.beginRenderPass(renderPassDescriptor);
53
- this.device.popErrorScope((error: GPUError) => {
54
- this.device.reportError(new Error(`${this} creation failed:\n"${error.message}"`), this)();
55
- this.device.debug();
56
- });
57
- this.handle.label = this.props.id;
58
- log.groupCollapsed(3, `new WebGPURenderPass(${this.id})`)();
59
- log.probe(3, JSON.stringify(renderPassDescriptor, null, 2))();
60
- log.groupEnd(3)();
60
+ if (renderPassProps.timestampQuerySet) {
61
+ const webgpuTSQuerySet = renderPassProps.timestampQuerySet as WebGPUQuerySet;
62
+ webgpuTSQuerySet?._invalidateResults();
63
+ renderPassDescriptor.timestampWrites = webgpuTSQuerySet
64
+ ? ({
65
+ querySet: webgpuTSQuerySet.handle,
66
+ beginningOfPassWriteIndex: renderPassProps.beginTimestampIndex,
67
+ endOfPassWriteIndex: renderPassProps.endTimestampIndex
68
+ } as GPURenderPassTimestampWrites)
69
+ : undefined;
70
+ }
71
+ if (profiler) {
72
+ profiler.renderPassDescriptorAssemblyCount =
73
+ (profiler.renderPassDescriptorAssemblyCount || 0) + 1;
74
+ profiler.renderPassDescriptorAssemblyTimeMs =
75
+ (profiler.renderPassDescriptorAssemblyTimeMs || 0) +
76
+ (getTimestamp() - descriptorAssemblyStartTime);
77
+ }
78
+
79
+ this.device.pushErrorScope('validation');
80
+ const beginRenderPassStartTime = profiler ? getTimestamp() : 0;
81
+ this.handle = this.props.handle || commandEncoder.beginRenderPass(renderPassDescriptor);
82
+ if (profiler) {
83
+ profiler.renderPassBeginCount = (profiler.renderPassBeginCount || 0) + 1;
84
+ profiler.renderPassBeginTimeMs =
85
+ (profiler.renderPassBeginTimeMs || 0) + (getTimestamp() - beginRenderPassStartTime);
86
+ }
87
+ this.device.popErrorScope((error: GPUError) => {
88
+ this.device.reportError(new Error(`${this} creation failed:\n"${error.message}"`), this)();
89
+ this.device.debug();
90
+ });
91
+ this.handle.label = this.props.id;
92
+ log.groupCollapsed(3, `new WebGPURenderPass(${this.id})`)();
93
+ log.probe(3, JSON.stringify(renderPassDescriptor, null, 2))();
94
+ log.groupEnd(3)();
95
+ } finally {
96
+ if (profiler) {
97
+ profiler.renderPassSetupCount = (profiler.renderPassSetupCount || 0) + 1;
98
+ profiler.renderPassSetupTimeMs =
99
+ (profiler.renderPassSetupTimeMs || 0) + (getTimestamp() - startTime);
100
+ }
101
+ }
61
102
  }
62
103
 
63
- override destroy(): void {}
104
+ override destroy(): void {
105
+ this.destroyResource();
106
+ }
64
107
 
65
108
  end(): void {
109
+ if (this.destroyed) {
110
+ return;
111
+ }
66
112
  this.handle.end();
113
+ this.destroy();
67
114
  }
68
115
 
69
116
  setPipeline(pipeline: RenderPipeline): void {
@@ -77,11 +124,16 @@ export class WebGPURenderPass extends RenderPass {
77
124
  }
78
125
 
79
126
  /** Sets an array of bindings (uniform buffers, samplers, textures, ...) */
80
- setBindings(bindings: Record<string, Binding>): void {
81
- this.pipeline?.setBindings(bindings);
82
- const bindGroup = this.pipeline?._getBindGroup();
83
- if (bindGroup) {
84
- this.handle.setBindGroup(0, bindGroup);
127
+ setBindings(bindings: Bindings | BindingsByGroup): void {
128
+ this.bindings = bindings;
129
+ const bindGroups =
130
+ (this.pipeline &&
131
+ _getDefaultBindGroupFactory(this.device).getBindGroups(this.pipeline, bindings)) ||
132
+ {};
133
+ for (const [group, bindGroup] of Object.entries(bindGroups)) {
134
+ if (bindGroup) {
135
+ this.handle.setBindGroup(Number(group), bindGroup as GPUBindGroup);
136
+ }
85
137
  }
86
138
  }
87
139
 
@@ -1,13 +1,17 @@
1
1
  // luma.gl MIT license
2
2
 
3
- import type {Binding, RenderPass, VertexArray} from '@luma.gl/core';
4
- import {RenderPipeline, RenderPipelineProps, log} from '@luma.gl/core';
3
+ import type {Bindings, BindingsByGroup, RenderPass, VertexArray} from '@luma.gl/core';
4
+ import {
5
+ RenderPipeline,
6
+ RenderPipelineProps,
7
+ _getDefaultBindGroupFactory,
8
+ log,
9
+ normalizeBindingsByGroup
10
+ } from '@luma.gl/core';
5
11
  import {applyParametersToRenderPipelineDescriptor} from '../helpers/webgpu-parameters';
6
12
  import {getWebGPUTextureFormat} from '../helpers/convert-texture-format';
7
- import {getBindGroup} from '../helpers/get-bind-group';
8
13
  import {getVertexBufferLayout} from '../helpers/get-vertex-buffer-layout';
9
14
  // import {convertAttributesVertexBufferToLayout} from '../helpers/get-vertex-buffer-layout';
10
- // import {mapAccessorToWebGPUFormat} from './helpers/accessor-to-format';
11
15
  // import type {BufferAccessors} from './webgpu-pipeline';
12
16
 
13
17
  import type {WebGPUDevice} from '../webgpu-device';
@@ -21,14 +25,14 @@ import type {WebGPURenderPass} from './webgpu-render-pass';
21
25
  export class WebGPURenderPipeline extends RenderPipeline {
22
26
  readonly device: WebGPUDevice;
23
27
  readonly handle: GPURenderPipeline;
28
+ readonly descriptor: GPURenderPipelineDescriptor | null;
24
29
 
25
30
  readonly vs: WebGPUShader;
26
31
  readonly fs: WebGPUShader | null = null;
27
32
 
28
- /** For internal use to create BindGroups */
29
- private _bindings: Record<string, Binding>;
30
- private _bindGroupLayout: GPUBindGroupLayout | null = null;
31
- private _bindGroup: GPUBindGroup | null = null;
33
+ /** Compatibility path for direct pipeline.setBindings() usage */
34
+ private _bindingsByGroup: BindingsByGroup;
35
+ private _bindGroupCacheKeysByGroup: Partial<Record<number, object>> = {};
32
36
 
33
37
  override get [Symbol.toStringTag]() {
34
38
  return 'WebGPURenderPipeline';
@@ -37,9 +41,14 @@ export class WebGPURenderPipeline extends RenderPipeline {
37
41
  constructor(device: WebGPUDevice, props: RenderPipelineProps) {
38
42
  super(device, props);
39
43
  this.device = device;
44
+ this.shaderLayout ||= this.device.getShaderLayout((props.vs as WebGPUShader).source) || {
45
+ attributes: [],
46
+ bindings: []
47
+ };
40
48
  this.handle = this.props.handle as GPURenderPipeline;
49
+ let descriptor: GPURenderPipelineDescriptor | null = null;
41
50
  if (!this.handle) {
42
- const descriptor = this._getRenderPipelineDescriptor();
51
+ descriptor = this._getRenderPipelineDescriptor();
43
52
  log.groupCollapsed(1, `new WebGPURenderPipeline(${this.id})`)();
44
53
  log.probe(1, JSON.stringify(descriptor, null, 2))();
45
54
  log.groupEnd(1)();
@@ -51,13 +60,15 @@ export class WebGPURenderPipeline extends RenderPipeline {
51
60
  this.device.debug();
52
61
  });
53
62
  }
63
+ this.descriptor = descriptor;
54
64
  this.handle.label = this.props.id;
55
65
 
56
66
  // Note: Often the same shader in WebGPU
57
67
  this.vs = props.vs as WebGPUShader;
58
68
  this.fs = props.fs as WebGPUShader;
59
-
60
- this._bindings = {...this.props.bindings};
69
+ this._bindingsByGroup =
70
+ props.bindGroups || normalizeBindingsByGroup(this.shaderLayout, props.bindings);
71
+ this._bindGroupCacheKeysByGroup = createBindGroupCacheKeys(this._bindingsByGroup);
61
72
  }
62
73
 
63
74
  override destroy(): void {
@@ -67,17 +78,27 @@ export class WebGPURenderPipeline extends RenderPipeline {
67
78
  }
68
79
 
69
80
  /**
70
- * @todo Use renderpass.setBindings() ?
71
- * @todo Do we want to expose BindGroups in the API and remove this?
81
+ * Compatibility shim for code paths that still set bindings on the pipeline.
82
+ * The shared-model path passes bindings per draw and does not rely on this state.
72
83
  */
73
- setBindings(bindings: Record<string, Binding>): void {
74
- // Invalidate the cached bind group if any value has changed
75
- for (const [name, binding] of Object.entries(bindings)) {
76
- if (this._bindings[name] !== binding) {
77
- this._bindGroup = null;
84
+ setBindings(bindings: Bindings | BindingsByGroup): void {
85
+ const nextBindingsByGroup = normalizeBindingsByGroup(this.shaderLayout, bindings);
86
+ for (const [groupKey, groupBindings] of Object.entries(nextBindingsByGroup)) {
87
+ const group = Number(groupKey);
88
+ for (const [name, binding] of Object.entries(groupBindings || {})) {
89
+ const currentGroupBindings = this._bindingsByGroup[group] || {};
90
+ if (currentGroupBindings[name] !== binding) {
91
+ if (
92
+ !this._bindingsByGroup[group] ||
93
+ this._bindingsByGroup[group] === currentGroupBindings
94
+ ) {
95
+ this._bindingsByGroup[group] = {...currentGroupBindings};
96
+ }
97
+ this._bindingsByGroup[group][name] = binding;
98
+ this._bindGroupCacheKeysByGroup[group] = {};
99
+ }
78
100
  }
79
101
  }
80
- Object.assign(this._bindings, bindings);
81
102
  }
82
103
 
83
104
  /** @todo - should this be moved to renderpass? */
@@ -91,8 +112,14 @@ export class WebGPURenderPipeline extends RenderPipeline {
91
112
  firstIndex?: number;
92
113
  firstInstance?: number;
93
114
  baseVertex?: number;
115
+ bindings?: Bindings;
116
+ bindGroups?: BindingsByGroup;
117
+ _bindGroupCacheKeys?: Partial<Record<number, object>>;
118
+ uniforms?: Record<string, unknown>;
94
119
  }): boolean {
95
120
  const webgpuRenderPass = options.renderPass as WebGPURenderPass;
121
+ const instanceCount =
122
+ options.instanceCount && options.instanceCount > 0 ? options.instanceCount : 1;
96
123
 
97
124
  // Set pipeline
98
125
  this.device.pushErrorScope('validation');
@@ -103,9 +130,16 @@ export class WebGPURenderPipeline extends RenderPipeline {
103
130
  });
104
131
 
105
132
  // Set bindings (uniform buffers, textures etc)
106
- const bindGroup = this._getBindGroup();
107
- if (bindGroup) {
108
- webgpuRenderPass.handle.setBindGroup(0, bindGroup);
133
+ const hasExplicitBindings = Boolean(options.bindGroups || options.bindings);
134
+ const bindGroups = _getDefaultBindGroupFactory(this.device).getBindGroups(
135
+ this,
136
+ hasExplicitBindings ? options.bindGroups || options.bindings : this._bindingsByGroup,
137
+ hasExplicitBindings ? options._bindGroupCacheKeys : this._bindGroupCacheKeysByGroup
138
+ );
139
+ for (const [group, bindGroup] of Object.entries(bindGroups)) {
140
+ if (bindGroup) {
141
+ webgpuRenderPass.handle.setBindGroup(Number(group), bindGroup as GPUBindGroup);
142
+ }
109
143
  }
110
144
 
111
145
  // Set attributes
@@ -116,16 +150,17 @@ export class WebGPURenderPipeline extends RenderPipeline {
116
150
  if (options.indexCount) {
117
151
  webgpuRenderPass.handle.drawIndexed(
118
152
  options.indexCount,
119
- options.instanceCount,
120
- options.firstIndex,
121
- options.baseVertex,
122
- options.firstInstance
153
+ instanceCount,
154
+ options.firstIndex || 0,
155
+ options.baseVertex || 0,
156
+ options.firstInstance || 0
123
157
  );
124
158
  } else {
125
159
  webgpuRenderPass.handle.draw(
126
160
  options.vertexCount || 0,
127
- options.instanceCount || 1, // If 0, nothing will be drawn
128
- options.firstInstance
161
+ instanceCount,
162
+ options.firstVertex || 0,
163
+ options.firstInstance || 0
129
164
  );
130
165
  }
131
166
 
@@ -135,22 +170,12 @@ export class WebGPURenderPipeline extends RenderPipeline {
135
170
  return true;
136
171
  }
137
172
 
138
- /** Return a bind group created by setBindings */
139
- _getBindGroup() {
140
- if (this.shaderLayout.bindings.length === 0) {
141
- return null;
142
- }
143
-
144
- // Get hold of the bind group layout. We don't want to do this unless we know there is at least one bind group
145
- this._bindGroupLayout = this._bindGroupLayout || this.handle.getBindGroupLayout(0);
146
-
147
- // Set up the bindings
148
- // TODO what if bindings change? We need to rebuild the bind group!
149
- this._bindGroup =
150
- this._bindGroup ||
151
- getBindGroup(this.device.handle, this._bindGroupLayout, this.shaderLayout, this._bindings);
173
+ _getBindingsByGroupWebGPU(): BindingsByGroup {
174
+ return this._bindingsByGroup;
175
+ }
152
176
 
153
- return this._bindGroup;
177
+ _getBindGroupCacheKeysWebGPU(): Partial<Record<number, object>> {
178
+ return this._bindGroupCacheKeysByGroup;
154
179
  }
155
180
 
156
181
  /**
@@ -161,7 +186,9 @@ export class WebGPURenderPipeline extends RenderPipeline {
161
186
  const vertex: GPUVertexState = {
162
187
  module: (this.props.vs as WebGPUShader).handle,
163
188
  entryPoint: this.props.vertexEntryPoint || 'main',
164
- buffers: getVertexBufferLayout(this.shaderLayout, this.props.bufferLayout)
189
+ buffers: getVertexBufferLayout(this.shaderLayout, this.props.bufferLayout, {
190
+ pipelineId: this.id
191
+ })
165
192
  };
166
193
 
167
194
  // Populate color targets
@@ -211,6 +238,18 @@ export class WebGPURenderPipeline extends RenderPipeline {
211
238
  return descriptor;
212
239
  }
213
240
  }
241
+
242
+ function createBindGroupCacheKeys(
243
+ bindingsByGroup: BindingsByGroup
244
+ ): Partial<Record<number, object>> {
245
+ const bindGroupCacheKeys: Partial<Record<number, object>> = {};
246
+ for (const [groupKey, groupBindings] of Object.entries(bindingsByGroup)) {
247
+ if (groupBindings && Object.keys(groupBindings).length > 0) {
248
+ bindGroupCacheKeys[Number(groupKey)] = {};
249
+ }
250
+ }
251
+ return bindGroupCacheKeys;
252
+ }
214
253
  /**
215
254
  _setAttributeBuffers(webgpuRenderPass: WebGPURenderPass) {
216
255
  if (this._indexBuffer) {
@@ -40,6 +40,11 @@ export class WebGPUSampler extends Sampler {
40
40
  }
41
41
 
42
42
  override destroy(): void {
43
+ if (this.destroyed) {
44
+ return;
45
+ }
46
+
47
+ this.destroyResource();
43
48
  // GPUSampler does not have a destroy method
44
49
  // this.handle.destroy();
45
50
  // @ts-expect-error readonly
@@ -64,7 +64,22 @@ export class WebGPUShader extends Shader {
64
64
 
65
65
  /** Returns compilation info for this shader */
66
66
  async getCompilationInfo(): Promise<readonly CompilerMessage[]> {
67
- const compilationInfo = await this.handle.getCompilationInfo();
67
+ // `_checkCompilationError()` runs asynchronously after construction, so the shader can be
68
+ // destroyed before we await compilation info. Snapshot the handle and treat a destroyed shader
69
+ // as having no compiler messages instead of dereferencing `null`.
70
+ const handle = this.handle;
71
+ if (!handle) {
72
+ return [];
73
+ }
74
+ let compilationInfo;
75
+ try {
76
+ compilationInfo = await handle.getCompilationInfo();
77
+ } catch (error) {
78
+ if (this.device.shouldIgnoreDroppedInstanceError(error, 'getCompilationInfo')) {
79
+ return [];
80
+ }
81
+ throw error;
82
+ }
68
83
  return compilationInfo.messages;
69
84
  }
70
85
  }