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

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 +8 -6
  10. package/dist/adapter/helpers/get-bind-group.d.ts.map +1 -1
  11. package/dist/adapter/helpers/get-bind-group.js +110 -30
  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 +62 -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 +35 -35
  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 +176 -19
  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 +8160 -551
  94. package/dist/dist.min.js +171 -6
  95. package/dist/index.cjs +2001 -414
  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 +182 -42
  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 +90 -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 +41 -43
  128. package/src/adapter/webgpu-canvas-context.ts +108 -41
  129. package/src/adapter/webgpu-device.ts +243 -25
  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)();
@@ -47,17 +56,21 @@ export class WebGPURenderPipeline extends RenderPipeline {
47
56
  this.device.pushErrorScope('validation');
48
57
  this.handle = this.device.handle.createRenderPipeline(descriptor);
49
58
  this.device.popErrorScope((error: GPUError) => {
59
+ this.linkStatus = 'error';
50
60
  this.device.reportError(new Error(`${this} creation failed:\n"${error.message}"`), this)();
51
61
  this.device.debug();
52
62
  });
53
63
  }
64
+ this.descriptor = descriptor;
54
65
  this.handle.label = this.props.id;
66
+ this.linkStatus = 'success';
55
67
 
56
68
  // Note: Often the same shader in WebGPU
57
69
  this.vs = props.vs as WebGPUShader;
58
70
  this.fs = props.fs as WebGPUShader;
59
-
60
- this._bindings = {...this.props.bindings};
71
+ this._bindingsByGroup =
72
+ props.bindGroups || normalizeBindingsByGroup(this.shaderLayout, props.bindings);
73
+ this._bindGroupCacheKeysByGroup = createBindGroupCacheKeys(this._bindingsByGroup);
61
74
  }
62
75
 
63
76
  override destroy(): void {
@@ -67,17 +80,27 @@ export class WebGPURenderPipeline extends RenderPipeline {
67
80
  }
68
81
 
69
82
  /**
70
- * @todo Use renderpass.setBindings() ?
71
- * @todo Do we want to expose BindGroups in the API and remove this?
83
+ * Compatibility shim for code paths that still set bindings on the pipeline.
84
+ * The shared-model path passes bindings per draw and does not rely on this state.
72
85
  */
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;
86
+ setBindings(bindings: Bindings | BindingsByGroup): void {
87
+ const nextBindingsByGroup = normalizeBindingsByGroup(this.shaderLayout, bindings);
88
+ for (const [groupKey, groupBindings] of Object.entries(nextBindingsByGroup)) {
89
+ const group = Number(groupKey);
90
+ for (const [name, binding] of Object.entries(groupBindings || {})) {
91
+ const currentGroupBindings = this._bindingsByGroup[group] || {};
92
+ if (currentGroupBindings[name] !== binding) {
93
+ if (
94
+ !this._bindingsByGroup[group] ||
95
+ this._bindingsByGroup[group] === currentGroupBindings
96
+ ) {
97
+ this._bindingsByGroup[group] = {...currentGroupBindings};
98
+ }
99
+ this._bindingsByGroup[group][name] = binding;
100
+ this._bindGroupCacheKeysByGroup[group] = {};
101
+ }
78
102
  }
79
103
  }
80
- Object.assign(this._bindings, bindings);
81
104
  }
82
105
 
83
106
  /** @todo - should this be moved to renderpass? */
@@ -91,8 +114,19 @@ export class WebGPURenderPipeline extends RenderPipeline {
91
114
  firstIndex?: number;
92
115
  firstInstance?: number;
93
116
  baseVertex?: number;
117
+ bindings?: Bindings;
118
+ bindGroups?: BindingsByGroup;
119
+ _bindGroupCacheKeys?: Partial<Record<number, object>>;
120
+ uniforms?: Record<string, unknown>;
94
121
  }): boolean {
122
+ if (this.isErrored) {
123
+ log.info(2, `RenderPipeline:${this.id}.draw() aborted - pipeline initialization failed`)();
124
+ return false;
125
+ }
126
+
95
127
  const webgpuRenderPass = options.renderPass as WebGPURenderPass;
128
+ const instanceCount =
129
+ options.instanceCount && options.instanceCount > 0 ? options.instanceCount : 1;
96
130
 
97
131
  // Set pipeline
98
132
  this.device.pushErrorScope('validation');
@@ -103,9 +137,16 @@ export class WebGPURenderPipeline extends RenderPipeline {
103
137
  });
104
138
 
105
139
  // Set bindings (uniform buffers, textures etc)
106
- const bindGroup = this._getBindGroup();
107
- if (bindGroup) {
108
- webgpuRenderPass.handle.setBindGroup(0, bindGroup);
140
+ const hasExplicitBindings = Boolean(options.bindGroups || options.bindings);
141
+ const bindGroups = _getDefaultBindGroupFactory(this.device).getBindGroups(
142
+ this,
143
+ hasExplicitBindings ? options.bindGroups || options.bindings : this._bindingsByGroup,
144
+ hasExplicitBindings ? options._bindGroupCacheKeys : this._bindGroupCacheKeysByGroup
145
+ );
146
+ for (const [group, bindGroup] of Object.entries(bindGroups)) {
147
+ if (bindGroup) {
148
+ webgpuRenderPass.handle.setBindGroup(Number(group), bindGroup as GPUBindGroup);
149
+ }
109
150
  }
110
151
 
111
152
  // Set attributes
@@ -116,16 +157,17 @@ export class WebGPURenderPipeline extends RenderPipeline {
116
157
  if (options.indexCount) {
117
158
  webgpuRenderPass.handle.drawIndexed(
118
159
  options.indexCount,
119
- options.instanceCount,
120
- options.firstIndex,
121
- options.baseVertex,
122
- options.firstInstance
160
+ instanceCount,
161
+ options.firstIndex || 0,
162
+ options.baseVertex || 0,
163
+ options.firstInstance || 0
123
164
  );
124
165
  } else {
125
166
  webgpuRenderPass.handle.draw(
126
167
  options.vertexCount || 0,
127
- options.instanceCount || 1, // If 0, nothing will be drawn
128
- options.firstInstance
168
+ instanceCount,
169
+ options.firstVertex || 0,
170
+ options.firstInstance || 0
129
171
  );
130
172
  }
131
173
 
@@ -135,22 +177,12 @@ export class WebGPURenderPipeline extends RenderPipeline {
135
177
  return true;
136
178
  }
137
179
 
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);
180
+ _getBindingsByGroupWebGPU(): BindingsByGroup {
181
+ return this._bindingsByGroup;
182
+ }
152
183
 
153
- return this._bindGroup;
184
+ _getBindGroupCacheKeysWebGPU(): Partial<Record<number, object>> {
185
+ return this._bindGroupCacheKeysByGroup;
154
186
  }
155
187
 
156
188
  /**
@@ -161,7 +193,9 @@ export class WebGPURenderPipeline extends RenderPipeline {
161
193
  const vertex: GPUVertexState = {
162
194
  module: (this.props.vs as WebGPUShader).handle,
163
195
  entryPoint: this.props.vertexEntryPoint || 'main',
164
- buffers: getVertexBufferLayout(this.shaderLayout, this.props.bufferLayout)
196
+ buffers: getVertexBufferLayout(this.shaderLayout, this.props.bufferLayout, {
197
+ pipelineId: this.id
198
+ })
165
199
  };
166
200
 
167
201
  // Populate color targets
@@ -211,6 +245,18 @@ export class WebGPURenderPipeline extends RenderPipeline {
211
245
  return descriptor;
212
246
  }
213
247
  }
248
+
249
+ function createBindGroupCacheKeys(
250
+ bindingsByGroup: BindingsByGroup
251
+ ): Partial<Record<number, object>> {
252
+ const bindGroupCacheKeys: Partial<Record<number, object>> = {};
253
+ for (const [groupKey, groupBindings] of Object.entries(bindingsByGroup)) {
254
+ if (groupBindings && Object.keys(groupBindings).length > 0) {
255
+ bindGroupCacheKeys[Number(groupKey)] = {};
256
+ }
257
+ }
258
+ return bindGroupCacheKeys;
259
+ }
214
260
  /**
215
261
  _setAttributeBuffers(webgpuRenderPass: WebGPURenderPass) {
216
262
  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
  }