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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/animation-loop/animation-loop.d.ts +8 -4
  2. package/dist/animation-loop/animation-loop.d.ts.map +1 -1
  3. package/dist/animation-loop/animation-loop.js +70 -43
  4. package/dist/animation-loop/animation-loop.js.map +1 -1
  5. package/dist/animation-loop/make-animation-loop.js +7 -1
  6. package/dist/animation-loop/make-animation-loop.js.map +1 -1
  7. package/dist/animation-loop/request-animation-frame.d.ts.map +1 -1
  8. package/dist/animation-loop/request-animation-frame.js +23 -6
  9. package/dist/animation-loop/request-animation-frame.js.map +1 -1
  10. package/dist/dist.dev.js +442 -209
  11. package/dist/dist.min.js +37 -79
  12. package/dist/dynamic-texture/dynamic-texture.d.ts +3 -3
  13. package/dist/dynamic-texture/dynamic-texture.d.ts.map +1 -1
  14. package/dist/dynamic-texture/dynamic-texture.js +187 -36
  15. package/dist/dynamic-texture/dynamic-texture.js.map +1 -1
  16. package/dist/dynamic-texture/texture-data.d.ts +4 -0
  17. package/dist/dynamic-texture/texture-data.d.ts.map +1 -1
  18. package/dist/dynamic-texture/texture-data.js +9 -1
  19. package/dist/dynamic-texture/texture-data.js.map +1 -1
  20. package/dist/factories/pipeline-factory.d.ts +7 -5
  21. package/dist/factories/pipeline-factory.d.ts.map +1 -1
  22. package/dist/factories/pipeline-factory.js +71 -36
  23. package/dist/factories/pipeline-factory.js.map +1 -1
  24. package/dist/factories/shader-factory.d.ts +0 -3
  25. package/dist/factories/shader-factory.d.ts.map +1 -1
  26. package/dist/factories/shader-factory.js +13 -19
  27. package/dist/factories/shader-factory.js.map +1 -1
  28. package/dist/geometries/cone-geometry.d.ts +3 -1
  29. package/dist/geometries/cone-geometry.d.ts.map +1 -1
  30. package/dist/geometries/cone-geometry.js.map +1 -1
  31. package/dist/geometries/cylinder-geometry.d.ts +2 -1
  32. package/dist/geometries/cylinder-geometry.d.ts.map +1 -1
  33. package/dist/geometries/cylinder-geometry.js.map +1 -1
  34. package/dist/index.cjs +433 -208
  35. package/dist/index.cjs.map +2 -2
  36. package/dist/model/model.d.ts +3 -1
  37. package/dist/model/model.d.ts.map +1 -1
  38. package/dist/model/model.js +11 -9
  39. package/dist/model/model.js.map +1 -1
  40. package/dist/models/billboard-texture-model.d.ts.map +1 -1
  41. package/dist/models/billboard-texture-model.js +10 -8
  42. package/dist/models/billboard-texture-model.js.map +1 -1
  43. package/dist/models/clip-space.js +7 -7
  44. package/dist/modules/picking/index-picking.d.ts +1 -1
  45. package/dist/modules/picking/index-picking.d.ts.map +1 -1
  46. package/dist/modules/picking/index-picking.js +0 -6
  47. package/dist/modules/picking/index-picking.js.map +1 -1
  48. package/dist/passes/get-fragment-shader.js +11 -30
  49. package/dist/passes/get-fragment-shader.js.map +1 -1
  50. package/dist/passes/shader-pass-renderer.d.ts +0 -2
  51. package/dist/passes/shader-pass-renderer.d.ts.map +1 -1
  52. package/dist/passes/shader-pass-renderer.js +4 -31
  53. package/dist/passes/shader-pass-renderer.js.map +1 -1
  54. package/dist/scenegraph/group-node.d.ts +5 -0
  55. package/dist/scenegraph/group-node.d.ts.map +1 -1
  56. package/dist/scenegraph/group-node.js +12 -0
  57. package/dist/scenegraph/group-node.js.map +1 -1
  58. package/dist/scenegraph/model-node.d.ts +2 -2
  59. package/dist/scenegraph/model-node.d.ts.map +1 -1
  60. package/dist/scenegraph/model-node.js.map +1 -1
  61. package/dist/scenegraph/scenegraph-node.d.ts +1 -1
  62. package/dist/scenegraph/scenegraph-node.d.ts.map +1 -1
  63. package/dist/scenegraph/scenegraph-node.js +23 -15
  64. package/dist/scenegraph/scenegraph-node.js.map +1 -1
  65. package/dist/utils/buffer-layout-order.d.ts.map +1 -1
  66. package/dist/utils/buffer-layout-order.js +12 -2
  67. package/dist/utils/buffer-layout-order.js.map +1 -1
  68. package/package.json +4 -4
  69. package/src/animation-loop/animation-loop.ts +75 -46
  70. package/src/animation-loop/make-animation-loop.ts +13 -5
  71. package/src/animation-loop/request-animation-frame.ts +32 -6
  72. package/src/dynamic-texture/dynamic-texture.ts +248 -39
  73. package/src/dynamic-texture/texture-data.ts +15 -1
  74. package/src/factories/pipeline-factory.ts +87 -46
  75. package/src/factories/shader-factory.ts +16 -20
  76. package/src/geometries/cone-geometry.ts +6 -1
  77. package/src/geometries/cylinder-geometry.ts +5 -1
  78. package/src/model/model.ts +14 -10
  79. package/src/models/billboard-texture-model.ts +10 -8
  80. package/src/models/clip-space.ts +7 -7
  81. package/src/modules/picking/index-picking.ts +0 -6
  82. package/src/passes/get-fragment-shader.ts +11 -30
  83. package/src/passes/shader-pass-renderer.ts +4 -33
  84. package/src/scenegraph/group-node.ts +16 -0
  85. package/src/scenegraph/model-node.ts +2 -2
  86. package/src/scenegraph/scenegraph-node.ts +27 -16
  87. package/src/utils/buffer-layout-order.ts +18 -2
@@ -1,7 +1,7 @@
1
1
  // luma.gl, MIT license
2
2
  // Copyright (c) vis.gl contributors
3
3
 
4
- import type {TextureProps, SamplerProps, TextureView, Device} from '@luma.gl/core';
4
+ import type {TextureProps, SamplerProps, TextureView, Device, TextureFormat} from '@luma.gl/core';
5
5
 
6
6
  import {Texture, Sampler, log} from '@luma.gl/core';
7
7
 
@@ -29,6 +29,7 @@ import {
29
29
 
30
30
  // Helpers
31
31
  getTextureSizeFromData,
32
+ resolveTextureImageFormat,
32
33
  getTexture1DSubresources,
33
34
  getTexture2DSubresources,
34
35
  getTexture3DSubresources,
@@ -129,7 +130,7 @@ export class DynamicTexture {
129
130
  }
130
131
 
131
132
  /** @note Fire and forget; caller can await `ready` */
132
- async initAsync(originalPropsWithAsyncData: TextureDataAsyncProps): Promise<void> {
133
+ async initAsync(originalPropsWithAsyncData: DynamicTextureProps): Promise<void> {
133
134
  try {
134
135
  // TODO - Accept URL string for 2D: turn into ExternalImage promise
135
136
  // const dataProps =
@@ -139,6 +140,11 @@ export class DynamicTexture {
139
140
 
140
141
  const propsWithSyncData = await this._loadAllData(originalPropsWithAsyncData);
141
142
  this._checkNotDestroyed();
143
+ const subresources = propsWithSyncData.data ? getTextureSubresources(propsWithSyncData) : [];
144
+ const userProvidedFormat =
145
+ 'format' in originalPropsWithAsyncData && originalPropsWithAsyncData.format !== undefined;
146
+ const userProvidedUsage =
147
+ 'usage' in originalPropsWithAsyncData && originalPropsWithAsyncData.usage !== undefined;
142
148
 
143
149
  // Deduce size when not explicitly provided
144
150
  // TODO - what about depth?
@@ -160,18 +166,44 @@ export class DynamicTexture {
160
166
  throw new Error(`${this} size could not be determined or was zero`);
161
167
  }
162
168
 
169
+ // Normalize caller-provided subresources into one validated mip chain description.
170
+ const textureData = analyzeTextureSubresources(this.device, subresources, size, {
171
+ format: userProvidedFormat ? originalPropsWithAsyncData.format : undefined
172
+ });
173
+ const resolvedFormat = textureData.format ?? this.props.format;
174
+
163
175
  // Create a minimal TextureProps and validate via `satisfies`
164
176
  const baseTextureProps = {
165
177
  ...this.props,
166
178
  ...size,
179
+ format: resolvedFormat,
167
180
  mipLevels: 1, // temporary; updated below
168
181
  data: undefined
169
182
  } satisfies TextureProps;
170
183
 
184
+ if (this.device.isTextureFormatCompressed(resolvedFormat) && !userProvidedUsage) {
185
+ baseTextureProps.usage = Texture.SAMPLE | Texture.COPY_DST;
186
+ }
187
+
188
+ // Explicit mip arrays take ownership of the mip chain; otherwise we may auto-generate it.
189
+ const shouldGenerateMipmaps =
190
+ this.props.mipmaps &&
191
+ !textureData.hasExplicitMipChain &&
192
+ !this.device.isTextureFormatCompressed(resolvedFormat);
193
+
194
+ if (this.device.type === 'webgpu' && shouldGenerateMipmaps) {
195
+ const requiredUsage =
196
+ this.props.dimension === '3d'
197
+ ? Texture.SAMPLE | Texture.STORAGE | Texture.COPY_DST | Texture.COPY_SRC
198
+ : Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST | Texture.COPY_SRC;
199
+ baseTextureProps.usage |= requiredUsage;
200
+ }
201
+
171
202
  // Compute mip levels (auto clamps to max)
172
203
  const maxMips = this.device.getMipLevelCount(baseTextureProps.width, baseTextureProps.height);
173
- const desired =
174
- this.props.mipLevels === 'auto'
204
+ const desired = textureData.hasExplicitMipChain
205
+ ? textureData.mipLevels
206
+ : this.props.mipLevels === 'auto'
175
207
  ? maxMips
176
208
  : Math.max(1, Math.min(maxMips, this.props.mipLevels ?? 1));
177
209
 
@@ -182,33 +214,15 @@ export class DynamicTexture {
182
214
  this._view = this.texture.view;
183
215
 
184
216
  // Upload data if provided
185
- if (propsWithSyncData.data) {
186
- switch (propsWithSyncData.dimension) {
187
- case '1d':
188
- this.setTexture1DData(propsWithSyncData.data);
189
- break;
190
- case '2d':
191
- this.setTexture2DData(propsWithSyncData.data);
192
- break;
193
- case '3d':
194
- this.setTexture3DData(propsWithSyncData.data);
195
- break;
196
- case '2d-array':
197
- this.setTextureArrayData(propsWithSyncData.data);
198
- break;
199
- case 'cube':
200
- this.setTextureCubeData(propsWithSyncData.data);
201
- break;
202
- case 'cube-array':
203
- this.setTextureCubeArrayData(propsWithSyncData.data);
204
- break;
205
- default: {
206
- throw new Error(`Unhandled dimension ${propsWithSyncData.dimension}`);
207
- }
208
- }
217
+ if (textureData.subresources.length) {
218
+ this._setTextureSubresources(textureData.subresources);
209
219
  }
210
220
 
211
- if (this.props.mipmaps) {
221
+ if (this.props.mipmaps && !textureData.hasExplicitMipChain && !shouldGenerateMipmaps) {
222
+ log.warn(`${this} skipping auto-generated mipmaps for compressed texture format`)();
223
+ }
224
+
225
+ if (shouldGenerateMipmaps) {
212
226
  this.generateMipmaps();
213
227
  }
214
228
 
@@ -234,14 +248,12 @@ export class DynamicTexture {
234
248
  }
235
249
 
236
250
  generateMipmaps(): void {
237
- // Call the WebGL-style mipmap generation helper
238
- // WebGL implementation generates mipmaps, WebGPU logs a warning
239
251
  if (this.device.type === 'webgl') {
240
252
  this.texture.generateMipmapsWebGL();
253
+ } else if (this.device.type === 'webgpu') {
254
+ this.device.generateMipmapsWebGPU(this.texture);
241
255
  } else {
242
- log.warn(
243
- 'Mipmap generation not yet implemented on WebGPU: your texture data will not be correctly initialized'
244
- );
256
+ log.warn(`${this} mipmaps not supported on ${this.device.type}`);
245
257
  }
246
258
  }
247
259
 
@@ -334,7 +346,7 @@ export class DynamicTexture {
334
346
  }
335
347
 
336
348
  /** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
337
- private setTextureCubeArrayData(data: TextureCubeArrayData): void {
349
+ setTextureCubeArrayData(data: TextureCubeArrayData): void {
338
350
  if (this.texture.props.dimension !== 'cube-array') {
339
351
  throw new Error(`${this} is not cube-array`);
340
352
  }
@@ -359,10 +371,21 @@ export class DynamicTexture {
359
371
  this.texture.copyExternalImage({image, z, mipLevel, flipY});
360
372
  break;
361
373
  case 'texture-data':
362
- const {data} = subresource;
363
- // TODO - we are throwing away some of the info in data.
364
- // Did we not need it in the first place? Can we use it to validate?
365
- this.texture.copyImageData({data: data.data, z, mipLevel});
374
+ const {data, textureFormat} = subresource;
375
+ if (textureFormat && textureFormat !== this.texture.format) {
376
+ throw new Error(
377
+ `${this} mip level ${mipLevel} uses format "${textureFormat}" but texture format is "${this.texture.format}"`
378
+ );
379
+ }
380
+ this.texture.writeData(data.data, {
381
+ x: 0,
382
+ y: 0,
383
+ z,
384
+ width: data.width,
385
+ height: data.height,
386
+ depthOrArrayLayers: 1,
387
+ mipLevel
388
+ });
366
389
  break;
367
390
  default:
368
391
  throw new Error('Unsupported 2D mip-level payload');
@@ -399,6 +422,192 @@ export class DynamicTexture {
399
422
  };
400
423
  }
401
424
 
425
+ type TextureSubresourceAnalysis = {
426
+ readonly subresources: TextureSubresource[];
427
+ readonly mipLevels: number;
428
+ readonly format?: TextureFormat;
429
+ readonly hasExplicitMipChain: boolean;
430
+ };
431
+
432
+ // Flatten dimension-specific texture data into one list of uploadable subresources.
433
+ function getTextureSubresources(props: TextureDataProps): TextureSubresource[] {
434
+ if (!props.data) {
435
+ return [];
436
+ }
437
+
438
+ switch (props.dimension) {
439
+ case '1d':
440
+ return getTexture1DSubresources(props.data);
441
+ case '2d':
442
+ return getTexture2DSubresources(0, props.data);
443
+ case '3d':
444
+ return getTexture3DSubresources(props.data);
445
+ case '2d-array':
446
+ return getTextureArraySubresources(props.data);
447
+ case 'cube':
448
+ return getTextureCubeSubresources(props.data);
449
+ case 'cube-array':
450
+ return getTextureCubeArraySubresources(props.data);
451
+ default:
452
+ throw new Error(`Unhandled dimension ${(props as TextureDataProps).dimension}`);
453
+ }
454
+ }
455
+
456
+ // Resolve a consistent texture format and the longest mip chain valid across all slices.
457
+ function analyzeTextureSubresources(
458
+ device: Device,
459
+ subresources: TextureSubresource[],
460
+ size: {width: number; height: number},
461
+ options: {format?: TextureFormat}
462
+ ): TextureSubresourceAnalysis {
463
+ if (subresources.length === 0) {
464
+ return {
465
+ subresources,
466
+ mipLevels: 1,
467
+ format: options.format,
468
+ hasExplicitMipChain: false
469
+ };
470
+ }
471
+
472
+ const groupedSubresources = new Map<number, TextureSubresource[]>();
473
+ for (const subresource of subresources) {
474
+ const group = groupedSubresources.get(subresource.z) ?? [];
475
+ group.push(subresource);
476
+ groupedSubresources.set(subresource.z, group);
477
+ }
478
+
479
+ const hasExplicitMipChain = subresources.some(subresource => subresource.mipLevel > 0);
480
+ let resolvedFormat = options.format;
481
+ let resolvedMipLevels = Number.POSITIVE_INFINITY;
482
+ const validSubresources: TextureSubresource[] = [];
483
+
484
+ for (const [z, sliceSubresources] of groupedSubresources) {
485
+ // Validate each slice independently, then keep only the mip levels that are valid everywhere.
486
+ const sortedSubresources = [...sliceSubresources].sort(
487
+ (left, right) => left.mipLevel - right.mipLevel
488
+ );
489
+ const baseLevel = sortedSubresources[0];
490
+ if (!baseLevel || baseLevel.mipLevel !== 0) {
491
+ throw new Error(`DynamicTexture: slice ${z} is missing mip level 0`);
492
+ }
493
+
494
+ const baseSize = getTextureSubresourceSize(device, baseLevel);
495
+ if (baseSize.width !== size.width || baseSize.height !== size.height) {
496
+ throw new Error(
497
+ `DynamicTexture: slice ${z} base level dimensions ${baseSize.width}x${baseSize.height} do not match expected ${size.width}x${size.height}`
498
+ );
499
+ }
500
+
501
+ const baseFormat = getTextureSubresourceFormat(baseLevel);
502
+ if (baseFormat) {
503
+ if (resolvedFormat && resolvedFormat !== baseFormat) {
504
+ throw new Error(
505
+ `DynamicTexture: slice ${z} base level format "${baseFormat}" does not match texture format "${resolvedFormat}"`
506
+ );
507
+ }
508
+ resolvedFormat = baseFormat;
509
+ }
510
+
511
+ const mipLevelLimit =
512
+ resolvedFormat && device.isTextureFormatCompressed(resolvedFormat)
513
+ ? // Block-compressed formats cannot have mips smaller than a single compression block.
514
+ getMaxCompressedMipLevels(device, baseSize.width, baseSize.height, resolvedFormat)
515
+ : device.getMipLevelCount(baseSize.width, baseSize.height);
516
+
517
+ let validMipLevelsForSlice = 0;
518
+ for (
519
+ let expectedMipLevel = 0;
520
+ expectedMipLevel < sortedSubresources.length;
521
+ expectedMipLevel++
522
+ ) {
523
+ const subresource = sortedSubresources[expectedMipLevel];
524
+ // Stop at the first gap so callers can provide extra trailing data without breaking creation.
525
+ if (!subresource || subresource.mipLevel !== expectedMipLevel) {
526
+ break;
527
+ }
528
+ if (expectedMipLevel >= mipLevelLimit) {
529
+ break;
530
+ }
531
+
532
+ const subresourceSize = getTextureSubresourceSize(device, subresource);
533
+ const expectedWidth = Math.max(1, baseSize.width >> expectedMipLevel);
534
+ const expectedHeight = Math.max(1, baseSize.height >> expectedMipLevel);
535
+ if (subresourceSize.width !== expectedWidth || subresourceSize.height !== expectedHeight) {
536
+ break;
537
+ }
538
+
539
+ const subresourceFormat = getTextureSubresourceFormat(subresource);
540
+ if (subresourceFormat) {
541
+ if (!resolvedFormat) {
542
+ resolvedFormat = subresourceFormat;
543
+ }
544
+ // Later mip levels must stay on the same format as the validated base level.
545
+ if (subresourceFormat !== resolvedFormat) {
546
+ break;
547
+ }
548
+ }
549
+
550
+ validMipLevelsForSlice++;
551
+ validSubresources.push(subresource);
552
+ }
553
+
554
+ resolvedMipLevels = Math.min(resolvedMipLevels, validMipLevelsForSlice);
555
+ }
556
+
557
+ const mipLevels = Number.isFinite(resolvedMipLevels) ? Math.max(1, resolvedMipLevels) : 1;
558
+
559
+ return {
560
+ // Keep every slice trimmed to the same mip count so the texture shape stays internally consistent.
561
+ subresources: validSubresources.filter(subresource => subresource.mipLevel < mipLevels),
562
+ mipLevels,
563
+ format: resolvedFormat,
564
+ hasExplicitMipChain
565
+ };
566
+ }
567
+
568
+ // Read the per-level format using the transitional textureFormat -> format fallback rules.
569
+ function getTextureSubresourceFormat(subresource: TextureSubresource): TextureFormat | undefined {
570
+ if (subresource.type !== 'texture-data') {
571
+ return undefined;
572
+ }
573
+ return subresource.textureFormat ?? resolveTextureImageFormat(subresource.data);
574
+ }
575
+
576
+ // Resolve dimensions from either raw bytes or external-image subresources.
577
+ function getTextureSubresourceSize(
578
+ device: Device,
579
+ subresource: TextureSubresource
580
+ ): {width: number; height: number} {
581
+ switch (subresource.type) {
582
+ case 'external-image':
583
+ return device.getExternalImageSize(subresource.image);
584
+ case 'texture-data':
585
+ return {width: subresource.data.width, height: subresource.data.height};
586
+ default:
587
+ throw new Error('Unsupported texture subresource');
588
+ }
589
+ }
590
+
591
+ // Count the mip levels that stay at or above one compression block in each dimension.
592
+ function getMaxCompressedMipLevels(
593
+ device: Device,
594
+ baseWidth: number,
595
+ baseHeight: number,
596
+ format: TextureFormat
597
+ ): number {
598
+ const {blockWidth = 1, blockHeight = 1} = device.getTextureFormatInfo(format);
599
+ let mipLevels = 1;
600
+ for (let mipLevel = 1; ; mipLevel++) {
601
+ const width = Math.max(1, baseWidth >> mipLevel);
602
+ const height = Math.max(1, baseHeight >> mipLevel);
603
+ if (width < blockWidth || height < blockHeight) {
604
+ break;
605
+ }
606
+ mipLevels++;
607
+ }
608
+ return mipLevels;
609
+ }
610
+
402
611
  // HELPERS
403
612
 
404
613
  /** Resolve all promises in a nested data structure */
@@ -9,6 +9,8 @@ export type TextureImageSource = ExternalImage;
9
9
  * additional optional fields can describe compressed texture data.
10
10
  */
11
11
  export type TextureImageData = {
12
+ /** Preferred WebGPU style format string. */
13
+ textureFormat?: TextureFormat;
12
14
  /** WebGPU style format string. Defaults to 'rgba8unorm' */
13
15
  format?: TextureFormat;
14
16
  /** Typed Array with the bytes of the image. @note beware row byte alignment requirements */
@@ -104,6 +106,7 @@ export type TextureSubresource = {
104
106
  | {
105
107
  type: 'texture-data';
106
108
  data: TextureImageData;
109
+ textureFormat?: TextureFormat;
107
110
  }
108
111
  );
109
112
 
@@ -184,6 +187,16 @@ function isTextureImageData(data: TextureMipLevelData): data is TextureImageData
184
187
  );
185
188
  }
186
189
 
190
+ export function resolveTextureImageFormat(data: TextureImageData): TextureFormat | undefined {
191
+ const {textureFormat, format} = data;
192
+ if (textureFormat && format && textureFormat !== format) {
193
+ throw new Error(
194
+ `Conflicting texture formats "${textureFormat}" and "${format}" provided for the same mip level`
195
+ );
196
+ }
197
+ return textureFormat ?? format;
198
+ }
199
+
187
200
  /** Resolve size for a single mip-level datum */
188
201
  // function getTextureMipLevelSizeFromData(data: TextureMipLevelData): {
189
202
  // width: number;
@@ -249,6 +262,7 @@ export function getTexture2DSubresources(
249
262
  subresources.push({
250
263
  type: 'texture-data',
251
264
  data: imageData,
265
+ textureFormat: resolveTextureImageFormat(imageData),
252
266
  z,
253
267
  mipLevel
254
268
  });
@@ -294,7 +308,7 @@ export function getTextureCubeArraySubresources(data: TextureCubeArrayData): Tex
294
308
  data.forEach((cubeData, cubeIndex) => {
295
309
  for (const [face, faceData] of Object.entries(cubeData)) {
296
310
  const faceDepth = getCubeArrayFaceIndex(cubeIndex, face as TextureCubeFace);
297
- getTexture2DSubresources(faceDepth, faceData);
311
+ subresources.push(...getTexture2DSubresources(faceDepth, faceData));
298
312
  }
299
313
  });
300
314
  return subresources;
@@ -2,15 +2,14 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {RenderPipelineProps, ComputePipelineProps} from '@luma.gl/core';
6
- import {Device, RenderPipeline, ComputePipeline, log} from '@luma.gl/core';
5
+ import type {RenderPipelineProps, ComputePipelineProps, SharedRenderPipeline} from '@luma.gl/core';
6
+ import {Device, RenderPipeline, ComputePipeline, Resource, log} from '@luma.gl/core';
7
7
  import type {EngineModuleState} from '../types';
8
8
  import {uid} from '../utils/uid';
9
9
 
10
10
  export type PipelineFactoryProps = RenderPipelineProps;
11
11
 
12
- type RenderPipelineCacheItem = {pipeline: RenderPipeline; useCount: number};
13
- type ComputePipelineCacheItem = {pipeline: ComputePipeline; useCount: number};
12
+ type CacheItem<ResourceT extends Resource<any>> = {resource: ResourceT; useCount: number};
14
13
 
15
14
  /**
16
15
  * Efficiently creates / caches pipelines
@@ -26,14 +25,12 @@ export class PipelineFactory {
26
25
  }
27
26
 
28
27
  readonly device: Device;
29
- readonly cachingEnabled: boolean;
30
- readonly destroyPolicy: 'unused' | 'never';
31
- readonly debug: boolean;
32
28
 
33
29
  private _hashCounter: number = 0;
34
30
  private readonly _hashes: Record<string, number> = {};
35
- private readonly _renderPipelineCache: Record<string, RenderPipelineCacheItem> = {};
36
- private readonly _computePipelineCache: Record<string, ComputePipelineCacheItem> = {};
31
+ private readonly _renderPipelineCache: Record<string, CacheItem<RenderPipeline>> = {};
32
+ private readonly _computePipelineCache: Record<string, CacheItem<ComputePipeline>> = {};
33
+ private readonly _sharedRenderPipelineCache: Record<string, CacheItem<SharedRenderPipeline>> = {};
37
34
 
38
35
  get [Symbol.toStringTag](): string {
39
36
  return 'PipelineFactory';
@@ -45,14 +42,11 @@ export class PipelineFactory {
45
42
 
46
43
  constructor(device: Device) {
47
44
  this.device = device;
48
- this.cachingEnabled = device.props._cachePipelines;
49
- this.destroyPolicy = device.props._cacheDestroyPolicy;
50
- this.debug = device.props.debugFactories;
51
45
  }
52
46
 
53
47
  /** Return a RenderPipeline matching supplied props. Reuses an equivalent pipeline if already created. */
54
48
  createRenderPipeline(props: RenderPipelineProps): RenderPipeline {
55
- if (!this.cachingEnabled) {
49
+ if (!this.device.props._cachePipelines) {
56
50
  return this.device.createRenderPipeline(props);
57
51
  }
58
52
 
@@ -61,23 +55,28 @@ export class PipelineFactory {
61
55
  const cache = this._renderPipelineCache;
62
56
  const hash = this._hashRenderPipeline(allProps);
63
57
 
64
- let pipeline: RenderPipeline = cache[hash]?.pipeline;
58
+ let pipeline: RenderPipeline = cache[hash]?.resource;
65
59
  if (!pipeline) {
60
+ const sharedRenderPipeline =
61
+ this.device.type === 'webgl' && this.device.props._sharePipelines
62
+ ? this.createSharedRenderPipeline(allProps)
63
+ : undefined;
66
64
  pipeline = this.device.createRenderPipeline({
67
65
  ...allProps,
68
- id: allProps.id ? `${allProps.id}-cached` : uid('unnamed-cached')
66
+ id: allProps.id ? `${allProps.id}-cached` : uid('unnamed-cached'),
67
+ _sharedRenderPipeline: sharedRenderPipeline
69
68
  });
70
69
  pipeline.hash = hash;
71
- cache[hash] = {pipeline, useCount: 1};
72
- if (this.debug) {
70
+ cache[hash] = {resource: pipeline, useCount: 1};
71
+ if (this.device.props.debugFactories) {
73
72
  log.log(3, `${this}: ${pipeline} created, count=${cache[hash].useCount}`)();
74
73
  }
75
74
  } else {
76
75
  cache[hash].useCount++;
77
- if (this.debug) {
76
+ if (this.device.props.debugFactories) {
78
77
  log.log(
79
78
  3,
80
- `${this}: ${cache[hash].pipeline} reused, count=${cache[hash].useCount}, (id=${props.id})`
79
+ `${this}: ${cache[hash].resource} reused, count=${cache[hash].useCount}, (id=${props.id})`
81
80
  )();
82
81
  }
83
82
  }
@@ -87,7 +86,7 @@ export class PipelineFactory {
87
86
 
88
87
  /** Return a ComputePipeline matching supplied props. Reuses an equivalent pipeline if already created. */
89
88
  createComputePipeline(props: ComputePipelineProps): ComputePipeline {
90
- if (!this.cachingEnabled) {
89
+ if (!this.device.props._cachePipelines) {
91
90
  return this.device.createComputePipeline(props);
92
91
  }
93
92
 
@@ -96,23 +95,23 @@ export class PipelineFactory {
96
95
  const cache = this._computePipelineCache;
97
96
  const hash = this._hashComputePipeline(allProps);
98
97
 
99
- let pipeline: ComputePipeline = cache[hash]?.pipeline;
98
+ let pipeline: ComputePipeline = cache[hash]?.resource;
100
99
  if (!pipeline) {
101
100
  pipeline = this.device.createComputePipeline({
102
101
  ...allProps,
103
102
  id: allProps.id ? `${allProps.id}-cached` : undefined
104
103
  });
105
104
  pipeline.hash = hash;
106
- cache[hash] = {pipeline, useCount: 1};
107
- if (this.debug) {
105
+ cache[hash] = {resource: pipeline, useCount: 1};
106
+ if (this.device.props.debugFactories) {
108
107
  log.log(3, `${this}: ${pipeline} created, count=${cache[hash].useCount}`)();
109
108
  }
110
109
  } else {
111
110
  cache[hash].useCount++;
112
- if (this.debug) {
111
+ if (this.device.props.debugFactories) {
113
112
  log.log(
114
113
  3,
115
- `${this}: ${cache[hash].pipeline} reused, count=${cache[hash].useCount}, (id=${props.id})`
114
+ `${this}: ${cache[hash].resource} reused, count=${cache[hash].useCount}, (id=${props.id})`
116
115
  )();
117
116
  }
118
117
  }
@@ -121,7 +120,7 @@ export class PipelineFactory {
121
120
  }
122
121
 
123
122
  release(pipeline: RenderPipeline | ComputePipeline): void {
124
- if (!this.cachingEnabled) {
123
+ if (!this.device.props._cachePipelines) {
125
124
  pipeline.destroy();
126
125
  return;
127
126
  }
@@ -132,40 +131,72 @@ export class PipelineFactory {
132
131
  cache[hash].useCount--;
133
132
  if (cache[hash].useCount === 0) {
134
133
  this._destroyPipeline(pipeline);
135
- if (this.debug) {
134
+ if (this.device.props.debugFactories) {
136
135
  log.log(3, `${this}: ${pipeline} released and destroyed`)();
137
136
  }
138
137
  } else if (cache[hash].useCount < 0) {
139
138
  log.error(`${this}: ${pipeline} released, useCount < 0, resetting`)();
140
139
  cache[hash].useCount = 0;
141
- } else if (this.debug) {
140
+ } else if (this.device.props.debugFactories) {
142
141
  log.log(3, `${this}: ${pipeline} released, count=${cache[hash].useCount}`)();
143
142
  }
144
143
  }
145
144
 
145
+ createSharedRenderPipeline(props: RenderPipelineProps): SharedRenderPipeline {
146
+ const sharedPipelineHash = this._hashSharedRenderPipeline(props);
147
+ let sharedCacheItem = this._sharedRenderPipelineCache[sharedPipelineHash];
148
+ if (!sharedCacheItem) {
149
+ const sharedRenderPipeline = this.device._createSharedRenderPipelineWebGL(props);
150
+ sharedCacheItem = {resource: sharedRenderPipeline, useCount: 0};
151
+ this._sharedRenderPipelineCache[sharedPipelineHash] = sharedCacheItem;
152
+ }
153
+ sharedCacheItem.useCount++;
154
+ return sharedCacheItem.resource;
155
+ }
156
+
157
+ releaseSharedRenderPipeline(pipeline: RenderPipeline): void {
158
+ if (!pipeline.sharedRenderPipeline) {
159
+ return;
160
+ }
161
+
162
+ const sharedPipelineHash = this._hashSharedRenderPipeline(pipeline.sharedRenderPipeline.props);
163
+ const sharedCacheItem = this._sharedRenderPipelineCache[sharedPipelineHash];
164
+ if (!sharedCacheItem) {
165
+ return;
166
+ }
167
+
168
+ sharedCacheItem.useCount--;
169
+ if (sharedCacheItem.useCount === 0) {
170
+ sharedCacheItem.resource.destroy();
171
+ delete this._sharedRenderPipelineCache[sharedPipelineHash];
172
+ }
173
+ }
174
+
146
175
  // PRIVATE
147
176
 
148
- /** Destroy a cached pipeline, removing it from the cache (depending on destroy policy) */
177
+ /** Destroy a cached pipeline, removing it from the cache if configured to do so. */
149
178
  private _destroyPipeline(pipeline: RenderPipeline | ComputePipeline): boolean {
150
179
  const cache = this._getCache(pipeline);
151
180
 
152
- switch (this.destroyPolicy) {
153
- case 'never':
154
- return false;
155
- case 'unused':
156
- delete cache[pipeline.hash];
157
- pipeline.destroy();
158
- return true;
181
+ if (!this.device.props._destroyPipelines) {
182
+ return false;
183
+ }
184
+
185
+ delete cache[pipeline.hash];
186
+ pipeline.destroy();
187
+ if (pipeline instanceof RenderPipeline) {
188
+ this.releaseSharedRenderPipeline(pipeline);
159
189
  }
190
+ return true;
160
191
  }
161
192
 
162
193
  /** Get the appropriate cache for the type of pipeline */
163
194
  private _getCache(
164
195
  pipeline: RenderPipeline | ComputePipeline
165
- ): Record<string, RenderPipelineCacheItem> | Record<string, ComputePipelineCacheItem> {
196
+ ): Record<string, CacheItem<RenderPipeline>> | Record<string, CacheItem<ComputePipeline>> {
166
197
  let cache:
167
- | Record<string, RenderPipelineCacheItem>
168
- | Record<string, ComputePipelineCacheItem>
198
+ | Record<string, CacheItem<RenderPipeline>>
199
+ | Record<string, CacheItem<ComputePipeline>>
169
200
  | undefined;
170
201
  if (pipeline instanceof ComputePipeline) {
171
202
  cache = this._computePipelineCache;
@@ -193,18 +224,16 @@ export class PipelineFactory {
193
224
  private _hashRenderPipeline(props: RenderPipelineProps): string {
194
225
  const vsHash = props.vs ? this._getHash(props.vs.source) : 0;
195
226
  const fsHash = props.fs ? this._getHash(props.fs.source) : 0;
196
-
197
- // WebGL specific
198
- // const {varyings = [], bufferMode = {}} = props;
199
- // const varyingHashes = varyings.map((v) => this._getHash(v));
200
- const varyingHash = '-'; // `${varyingHashes.join('/')}B${bufferMode}`
227
+ const varyingHash = this._getWebGLVaryingHash(props);
201
228
  const bufferLayoutHash = this._getHash(JSON.stringify(props.bufferLayout));
202
229
 
203
230
  const {type} = this.device;
204
231
  switch (type) {
205
232
  case 'webgl':
206
- // WebGL is more dynamic
207
- return `${type}/R/${vsHash}/${fsHash}V${varyingHash}BL${bufferLayoutHash}`;
233
+ // WebGL wrappers preserve default topology and parameter semantics for direct
234
+ // callers, even though the underlying linked program may be shared separately.
235
+ const webglParameterHash = this._getHash(JSON.stringify(props.parameters));
236
+ return `${type}/R/${vsHash}/${fsHash}V${varyingHash}T${props.topology}P${webglParameterHash}BL${bufferLayoutHash}`;
208
237
 
209
238
  case 'webgpu':
210
239
  default:
@@ -216,10 +245,22 @@ export class PipelineFactory {
216
245
  }
217
246
  }
218
247
 
248
+ private _hashSharedRenderPipeline(props: RenderPipelineProps): string {
249
+ const vsHash = props.vs ? this._getHash(props.vs.source) : 0;
250
+ const fsHash = props.fs ? this._getHash(props.fs.source) : 0;
251
+ const varyingHash = this._getWebGLVaryingHash(props);
252
+ return `webgl/S/${vsHash}/${fsHash}V${varyingHash}`;
253
+ }
254
+
219
255
  private _getHash(key: string): number {
220
256
  if (this._hashes[key] === undefined) {
221
257
  this._hashes[key] = this._hashCounter++;
222
258
  }
223
259
  return this._hashes[key];
224
260
  }
261
+
262
+ private _getWebGLVaryingHash(props: RenderPipelineProps): number {
263
+ const {varyings = [], bufferMode = null} = props;
264
+ return this._getHash(JSON.stringify({varyings, bufferMode}));
265
+ }
225
266
  }