@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.
- package/dist/animation-loop/animation-loop.d.ts +8 -4
- package/dist/animation-loop/animation-loop.d.ts.map +1 -1
- package/dist/animation-loop/animation-loop.js +70 -43
- package/dist/animation-loop/animation-loop.js.map +1 -1
- package/dist/animation-loop/make-animation-loop.js +7 -1
- package/dist/animation-loop/make-animation-loop.js.map +1 -1
- package/dist/animation-loop/request-animation-frame.d.ts.map +1 -1
- package/dist/animation-loop/request-animation-frame.js +23 -6
- package/dist/animation-loop/request-animation-frame.js.map +1 -1
- package/dist/dist.dev.js +442 -209
- package/dist/dist.min.js +37 -79
- package/dist/dynamic-texture/dynamic-texture.d.ts +3 -3
- package/dist/dynamic-texture/dynamic-texture.d.ts.map +1 -1
- package/dist/dynamic-texture/dynamic-texture.js +187 -36
- package/dist/dynamic-texture/dynamic-texture.js.map +1 -1
- package/dist/dynamic-texture/texture-data.d.ts +4 -0
- package/dist/dynamic-texture/texture-data.d.ts.map +1 -1
- package/dist/dynamic-texture/texture-data.js +9 -1
- package/dist/dynamic-texture/texture-data.js.map +1 -1
- package/dist/factories/pipeline-factory.d.ts +7 -5
- package/dist/factories/pipeline-factory.d.ts.map +1 -1
- package/dist/factories/pipeline-factory.js +71 -36
- package/dist/factories/pipeline-factory.js.map +1 -1
- package/dist/factories/shader-factory.d.ts +0 -3
- package/dist/factories/shader-factory.d.ts.map +1 -1
- package/dist/factories/shader-factory.js +13 -19
- package/dist/factories/shader-factory.js.map +1 -1
- package/dist/geometries/cone-geometry.d.ts +3 -1
- package/dist/geometries/cone-geometry.d.ts.map +1 -1
- package/dist/geometries/cone-geometry.js.map +1 -1
- package/dist/geometries/cylinder-geometry.d.ts +2 -1
- package/dist/geometries/cylinder-geometry.d.ts.map +1 -1
- package/dist/geometries/cylinder-geometry.js.map +1 -1
- package/dist/index.cjs +433 -208
- package/dist/index.cjs.map +2 -2
- package/dist/model/model.d.ts +3 -1
- package/dist/model/model.d.ts.map +1 -1
- package/dist/model/model.js +11 -9
- package/dist/model/model.js.map +1 -1
- package/dist/models/billboard-texture-model.d.ts.map +1 -1
- package/dist/models/billboard-texture-model.js +10 -8
- package/dist/models/billboard-texture-model.js.map +1 -1
- package/dist/models/clip-space.js +7 -7
- package/dist/modules/picking/index-picking.d.ts +1 -1
- package/dist/modules/picking/index-picking.d.ts.map +1 -1
- package/dist/modules/picking/index-picking.js +0 -6
- package/dist/modules/picking/index-picking.js.map +1 -1
- package/dist/passes/get-fragment-shader.js +11 -30
- package/dist/passes/get-fragment-shader.js.map +1 -1
- package/dist/passes/shader-pass-renderer.d.ts +0 -2
- package/dist/passes/shader-pass-renderer.d.ts.map +1 -1
- package/dist/passes/shader-pass-renderer.js +4 -31
- package/dist/passes/shader-pass-renderer.js.map +1 -1
- package/dist/scenegraph/group-node.d.ts +5 -0
- package/dist/scenegraph/group-node.d.ts.map +1 -1
- package/dist/scenegraph/group-node.js +12 -0
- package/dist/scenegraph/group-node.js.map +1 -1
- package/dist/scenegraph/model-node.d.ts +2 -2
- package/dist/scenegraph/model-node.d.ts.map +1 -1
- package/dist/scenegraph/model-node.js.map +1 -1
- package/dist/scenegraph/scenegraph-node.d.ts +1 -1
- package/dist/scenegraph/scenegraph-node.d.ts.map +1 -1
- package/dist/scenegraph/scenegraph-node.js +23 -15
- package/dist/scenegraph/scenegraph-node.js.map +1 -1
- package/dist/utils/buffer-layout-order.d.ts.map +1 -1
- package/dist/utils/buffer-layout-order.js +12 -2
- package/dist/utils/buffer-layout-order.js.map +1 -1
- package/package.json +4 -4
- package/src/animation-loop/animation-loop.ts +75 -46
- package/src/animation-loop/make-animation-loop.ts +13 -5
- package/src/animation-loop/request-animation-frame.ts +32 -6
- package/src/dynamic-texture/dynamic-texture.ts +248 -39
- package/src/dynamic-texture/texture-data.ts +15 -1
- package/src/factories/pipeline-factory.ts +87 -46
- package/src/factories/shader-factory.ts +16 -20
- package/src/geometries/cone-geometry.ts +6 -1
- package/src/geometries/cylinder-geometry.ts +5 -1
- package/src/model/model.ts +14 -10
- package/src/models/billboard-texture-model.ts +10 -8
- package/src/models/clip-space.ts +7 -7
- package/src/modules/picking/index-picking.ts +0 -6
- package/src/passes/get-fragment-shader.ts +11 -30
- package/src/passes/shader-pass-renderer.ts +4 -33
- package/src/scenegraph/group-node.ts +16 -0
- package/src/scenegraph/model-node.ts +2 -2
- package/src/scenegraph/scenegraph-node.ts +27 -16
- 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:
|
|
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
|
-
|
|
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 (
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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,
|
|
36
|
-
private readonly _computePipelineCache: Record<string,
|
|
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.
|
|
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]?.
|
|
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.
|
|
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.
|
|
76
|
+
if (this.device.props.debugFactories) {
|
|
78
77
|
log.log(
|
|
79
78
|
3,
|
|
80
|
-
`${this}: ${cache[hash].
|
|
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.
|
|
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]?.
|
|
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.
|
|
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.
|
|
111
|
+
if (this.device.props.debugFactories) {
|
|
113
112
|
log.log(
|
|
114
113
|
3,
|
|
115
|
-
`${this}: ${cache[hash].
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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,
|
|
196
|
+
): Record<string, CacheItem<RenderPipeline>> | Record<string, CacheItem<ComputePipeline>> {
|
|
166
197
|
let cache:
|
|
167
|
-
| Record<string,
|
|
168
|
-
| Record<string,
|
|
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
|
|
207
|
-
|
|
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
|
}
|