• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 23116682781

15 Mar 2026 06:35PM UTC coverage: 76.95% (+1.0%) from 75.961%
23116682781

push

github

web-flow
fix: Fixes compressed texture loading and updates website (#2532)

Co-authored-by: markus <62662819+markus677@users.noreply.github.com>

2576 of 3302 branches covered (78.01%)

Branch coverage included in aggregate %.

538 of 648 new or added lines in 23 files covered. (83.02%)

11 existing lines in 1 file now uncovered.

30054 of 39102 relevant lines covered (76.86%)

91.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.49
/modules/engine/src/dynamic-texture/dynamic-texture.ts
1
// luma.gl, MIT license
1✔
2
// Copyright (c) vis.gl contributors
1✔
3

1✔
4
import type {TextureProps, SamplerProps, TextureView, Device, TextureFormat} from '@luma.gl/core';
1✔
5

1✔
6
import {Texture, Sampler, log} from '@luma.gl/core';
1✔
7

1✔
8
// import {loadImageBitmap} from '../application-utils/load-file';
1✔
9
import {uid} from '../utils/uid';
1✔
10
import {
1✔
11
  // cube constants
1✔
12
  type TextureCubeFace,
1✔
13
  TEXTURE_CUBE_FACE_MAP,
1✔
14

1✔
15
  // texture slice/mip data types
1✔
16
  type TextureSubresource,
1✔
17

1✔
18
  // props (dimension + data)
1✔
19
  type TextureDataProps,
1✔
20
  type TextureDataAsyncProps,
1✔
21

1✔
22
  // combined data for different texture types
1✔
23
  type Texture1DData,
1✔
24
  type Texture2DData,
1✔
25
  type Texture3DData,
1✔
26
  type TextureArrayData,
1✔
27
  type TextureCubeArrayData,
1✔
28
  type TextureCubeData,
1✔
29

1✔
30
  // Helpers
1✔
31
  getTextureSizeFromData,
1✔
32
  resolveTextureImageFormat,
1✔
33
  getTexture1DSubresources,
1✔
34
  getTexture2DSubresources,
1✔
35
  getTexture3DSubresources,
1✔
36
  getTextureCubeSubresources,
1✔
37
  getTextureArraySubresources,
1✔
38
  getTextureCubeArraySubresources
1✔
39
} from './texture-data';
1✔
40

1✔
41
/**
1✔
42
 * Properties for a dynamic texture
1✔
43
 */
1✔
44
export type DynamicTextureProps = Omit<TextureProps, 'data' | 'mipLevels' | 'width' | 'height'> &
1✔
45
  TextureDataAsyncProps & {
1✔
46
    /** Generate mipmaps after creating textures and setting data */
1✔
47
    mipmaps?: boolean;
1✔
48
    /** nipLevels can be set to 'auto' to generate max number of mipLevels */
1✔
49
    mipLevels?: number | 'auto';
1✔
50
    /** Width - can be auto-calculated when initializing from ExternalImage */
1✔
51
    width?: number;
1✔
52
    /** Height - can be auto-calculated when initializing from ExternalImage */
1✔
53
    height?: number;
1✔
54
  };
1✔
55

1✔
56
/**
1✔
57
 * Dynamic Textures
1✔
58
 *
1✔
59
 * - Mipmaps - DynamicTexture can generate mipmaps for textures (WebGPU does not provide built-in mipmap generation).
1✔
60
 *
1✔
61
 * - Texture initialization and updates - complex textures (2d array textures, cube textures, 3d textures) need multiple images
1✔
62
 *   `DynamicTexture` provides an API that makes it easy to provide the required data.
1✔
63
 *
1✔
64
 * - Texture resizing - Textures are immutable in WebGPU, meaning that they cannot be resized after creation.
1✔
65
 *   DynamicTexture provides a `resize()` method that internally creates a new texture with the same parameters
1✔
66
 *   but a different size.
1✔
67
 *
1✔
68
 * - Async image data initialization - It is often very convenient to be able to initialize textures with promises
1✔
69
 *   returned by image or data loading functions, as it allows a callback-free linear style of programming.
1✔
70
 *
1✔
71
 * @note GPU Textures are quite complex objects, with many subresources and modes of usage.
1✔
72
 * The `DynamicTexture` class allows luma.gl to provide some support for working with textures
1✔
73
 * without accumulating excessive complexity in the core Texture class which is designed as an immutable nature of GPU resource.
1✔
74
 */
1✔
75
export class DynamicTexture {
1✔
76
  readonly device: Device;
1✔
77
  readonly id: string;
1✔
78

1✔
79
  /** Props with defaults resolved (except `data` which is processed separately) */
1✔
80
  props: Readonly<Required<DynamicTextureProps>>;
1✔
81

1✔
82
  /** Created resources */
1✔
83
  private _texture: Texture | null = null;
1✔
84
  private _sampler: Sampler | null = null;
1✔
85
  private _view: TextureView | null = null;
1✔
86

1✔
87
  /** Ready when GPU texture has been created and data (if any) uploaded */
1✔
88
  readonly ready: Promise<Texture>;
1✔
89
  isReady = false;
1✔
90
  destroyed = false;
1✔
91

1✔
92
  private resolveReady: (t: Texture) => void = () => {};
1✔
93
  private rejectReady: (error: Error) => void = () => {};
1✔
94

1✔
95
  get texture(): Texture {
1✔
96
    if (!this._texture) throw new Error('Texture not initialized yet');
103!
97
    return this._texture;
103✔
98
  }
103✔
99
  get sampler(): Sampler {
1✔
100
    if (!this._sampler) throw new Error('Sampler not initialized yet');
×
101
    return this._sampler;
×
102
  }
×
103
  get view(): TextureView {
1✔
104
    if (!this._view) throw new Error('View not initialized yet');
×
105
    return this._view;
×
106
  }
×
107

1✔
108
  get [Symbol.toStringTag]() {
1✔
109
    return 'DynamicTexture';
×
110
  }
×
111
  toString(): string {
1✔
112
    return `DynamicTexture:"${this.id}":${this.texture.width}x${this.texture.height}px:(${this.isReady ? 'ready' : 'loading...'})`;
11!
113
  }
11✔
114

1✔
115
  constructor(device: Device, props: DynamicTextureProps) {
1✔
116
    this.device = device;
12✔
117

12✔
118
    const id = uid('dynamic-texture');
12✔
119
    // NOTE: We avoid holding on to data to make sure it can be garbage collected.
12✔
120
    const originalPropsWithAsyncData = props;
12✔
121
    this.props = {...DynamicTexture.defaultProps, id, ...props, data: null};
12✔
122
    this.id = this.props.id;
12✔
123

12✔
124
    this.ready = new Promise<Texture>((resolve, reject) => {
12✔
125
      this.resolveReady = resolve;
12✔
126
      this.rejectReady = reject;
12✔
127
    });
12✔
128

12✔
129
    this.initAsync(originalPropsWithAsyncData);
12✔
130
  }
12✔
131

1✔
132
  /** @note Fire and forget; caller can await `ready` */
1✔
133
  async initAsync(originalPropsWithAsyncData: DynamicTextureProps): Promise<void> {
1✔
134
    try {
12✔
135
      // TODO - Accept URL string for 2D: turn into ExternalImage promise
12✔
136
      // const dataProps =
12✔
137
      //   typeof props.data === 'string' && (props.dimension ?? '2d') === '2d'
12✔
138
      //     ? ({dimension: '2d', data: loadImageBitmap(props.data)} as const)
12✔
139
      //     : {};
12✔
140

12✔
141
      const propsWithSyncData = await this._loadAllData(originalPropsWithAsyncData);
12✔
142
      this._checkNotDestroyed();
12✔
143
      const subresources = propsWithSyncData.data ? getTextureSubresources(propsWithSyncData) : [];
12!
144
      const userProvidedFormat =
12✔
145
        'format' in originalPropsWithAsyncData && originalPropsWithAsyncData.format !== undefined;
12!
146
      const userProvidedUsage =
12✔
147
        'usage' in originalPropsWithAsyncData && originalPropsWithAsyncData.usage !== undefined;
12✔
148

12✔
149
      // Deduce size when not explicitly provided
12✔
150
      // TODO - what about depth?
12✔
151
      const deduceSize = (): {width: number; height: number} => {
12✔
152
        if (this.props.width && this.props.height) {
11!
153
          return {width: this.props.width, height: this.props.height};
×
154
        }
×
155

11✔
156
        const size = getTextureSizeFromData(propsWithSyncData);
11✔
157
        if (size) {
11✔
158
          return size;
11✔
159
        }
11!
160

×
161
        return {width: this.props.width || 1, height: this.props.height || 1};
11!
162
      };
11✔
163

12✔
164
      const size = deduceSize();
12✔
165
      if (!size || size.width <= 0 || size.height <= 0) {
12!
166
        throw new Error(`${this} size could not be determined or was zero`);
×
167
      }
✔
168

11✔
169
      // Normalize caller-provided subresources into one validated mip chain description.
11✔
170
      const textureData = analyzeTextureSubresources(this.device, subresources, size, {
11✔
171
        format: userProvidedFormat ? originalPropsWithAsyncData.format : undefined
12!
172
      });
12✔
173
      const resolvedFormat = textureData.format ?? this.props.format;
12✔
174

12✔
175
      // Create a minimal TextureProps and validate via `satisfies`
12✔
176
      const baseTextureProps = {
12✔
177
        ...this.props,
12✔
178
        ...size,
12✔
179
        format: resolvedFormat,
12✔
180
        mipLevels: 1, // temporary; updated below
12✔
181
        data: undefined
12✔
182
      } satisfies TextureProps;
12✔
183

12✔
184
      if (this.device.isTextureFormatCompressed(resolvedFormat) && !userProvidedUsage) {
12✔
185
        baseTextureProps.usage = Texture.SAMPLE | Texture.COPY_DST;
6✔
186
      }
6✔
187

11✔
188
      // Explicit mip arrays take ownership of the mip chain; otherwise we may auto-generate it.
11✔
189
      const shouldGenerateMipmaps =
11✔
190
        this.props.mipmaps &&
11✔
191
        !textureData.hasExplicitMipChain &&
1!
NEW
192
        !this.device.isTextureFormatCompressed(resolvedFormat);
×
193

12✔
194
      if (this.device.type === 'webgpu' && shouldGenerateMipmaps) {
12!
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

11✔
202
      // Compute mip levels (auto clamps to max)
11✔
203
      const maxMips = this.device.getMipLevelCount(baseTextureProps.width, baseTextureProps.height);
11✔
204
      const desired = textureData.hasExplicitMipChain
11✔
205
        ? textureData.mipLevels
5✔
206
        : this.props.mipLevels === 'auto'
6!
UNCOV
207
          ? maxMips
×
208
          : Math.max(1, Math.min(maxMips, this.props.mipLevels ?? 1));
6!
209

12✔
210
      const finalTextureProps: TextureProps = {...baseTextureProps, mipLevels: desired};
12✔
211

12✔
212
      this._texture = this.device.createTexture(finalTextureProps);
12✔
213
      this._sampler = this.texture.sampler;
12✔
214
      this._view = this.texture.view;
12✔
215

12✔
216
      // Upload data if provided
12✔
217
      if (textureData.subresources.length) {
12✔
218
        this._setTextureSubresources(textureData.subresources);
11✔
219
      }
11✔
220

11✔
221
      if (this.props.mipmaps && !textureData.hasExplicitMipChain && !shouldGenerateMipmaps) {
12!
NEW
222
        log.warn(`${this} skipping auto-generated mipmaps for compressed texture format`)();
×
NEW
223
      }
✔
224

11✔
225
      if (shouldGenerateMipmaps) {
12!
226
        this.generateMipmaps();
×
227
      }
✔
228

11✔
229
      this.isReady = true;
11✔
230
      this.resolveReady(this.texture);
11✔
231

11✔
232
      log.info(0, `${this} created`)();
11✔
233
    } catch (e) {
12✔
234
      const err = e instanceof Error ? e : new Error(String(e));
1!
235
      this.rejectReady(err);
1✔
236
      throw err;
1✔
237
    }
1✔
238
  }
12✔
239

1✔
240
  destroy(): void {
1✔
241
    if (this._texture) {
12✔
242
      this._texture.destroy();
11✔
243
      this._texture = null;
11✔
244
      this._sampler = null;
11✔
245
      this._view = null;
11✔
246
    }
11✔
247
    this.destroyed = true;
12✔
248
  }
12✔
249

1✔
250
  generateMipmaps(): void {
1✔
251
    if (this.device.type === 'webgl') {
×
252
      this.texture.generateMipmapsWebGL();
×
253
    } else if (this.device.type === 'webgpu') {
×
NEW
254
      this.device.generateMipmapsWebGPU(this.texture);
×
255
    } else {
×
256
      log.warn(`${this} mipmaps not supported on ${this.device.type}`);
×
257
    }
×
258
  }
×
259

1✔
260
  /** Set sampler or create one from props */
1✔
261
  setSampler(sampler: Sampler | SamplerProps = {}): void {
1✔
262
    this._checkReady();
×
263
    const s = sampler instanceof Sampler ? sampler : this.device.createSampler(sampler);
×
264
    this.texture.setSampler(s);
×
265
    this._sampler = s;
×
266
  }
×
267

1✔
268
  /**
1✔
269
   * Resize by cloning the underlying immutable texture.
1✔
270
   * Does not copy contents; caller may need to re-upload and/or regenerate mips.
1✔
271
   */
1✔
272
  resize(size: {width: number; height: number}): boolean {
1✔
273
    this._checkReady();
×
274

×
275
    if (size.width === this.texture.width && size.height === this.texture.height) {
×
276
      return false;
×
277
    }
×
278
    const prev = this.texture;
×
279
    this._texture = prev.clone(size);
×
280
    this._sampler = this.texture.sampler;
×
281
    this._view = this.texture.view;
×
282

×
283
    prev.destroy();
×
284
    log.info(`${this} resized`);
×
285
    return true;
×
286
  }
×
287

1✔
288
  /** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
1✔
289
  getCubeFaceIndex(face: TextureCubeFace): number {
1✔
290
    const index = TEXTURE_CUBE_FACE_MAP[face];
×
291
    if (index === undefined) throw new Error(`Invalid cube face: ${face}`);
×
292
    return index;
×
293
  }
×
294

1✔
295
  /** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
1✔
296
  getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
1✔
297
    return 6 * cubeIndex + this.getCubeFaceIndex(face);
×
298
  }
×
299

1✔
300
  /** @note experimental: Set multiple mip levels (1D) */
1✔
301
  setTexture1DData(data: Texture1DData): void {
1✔
302
    this._checkReady();
×
303
    if (this.texture.props.dimension !== '1d') {
×
304
      throw new Error(`${this} is not 1d`);
×
305
    }
×
306
    const subresources = getTexture1DSubresources(data);
×
307
    this._setTextureSubresources(subresources);
×
308
  }
×
309

1✔
310
  /** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
1✔
311
  setTexture2DData(lodData: Texture2DData, z: number = 0): void {
1✔
UNCOV
312
    this._checkReady();
×
UNCOV
313
    if (this.texture.props.dimension !== '2d') {
×
314
      throw new Error(`${this} is not 2d`);
×
315
    }
×
UNCOV
316

×
UNCOV
317
    const subresources = getTexture2DSubresources(z, lodData);
×
UNCOV
318
    this._setTextureSubresources(subresources);
×
UNCOV
319
  }
×
320

1✔
321
  /** 3D: multiple depth slices, each may carry multiple mip levels */
1✔
322
  setTexture3DData(data: Texture3DData): void {
1✔
323
    if (this.texture.props.dimension !== '3d') {
×
324
      throw new Error(`${this} is not 3d`);
×
325
    }
×
326
    const subresources = getTexture3DSubresources(data);
×
327
    this._setTextureSubresources(subresources);
×
328
  }
×
329

1✔
330
  /** 2D array: multiple layers, each may carry multiple mip levels */
1✔
331
  setTextureArrayData(data: TextureArrayData): void {
1✔
332
    if (this.texture.props.dimension !== '2d-array') {
×
333
      throw new Error(`${this} is not 2d-array`);
×
334
    }
×
335
    const subresources = getTextureArraySubresources(data);
×
336
    this._setTextureSubresources(subresources);
×
337
  }
×
338

1✔
339
  /** Cube: 6 faces, each may carry multiple mip levels */
1✔
340
  setTextureCubeData(data: TextureCubeData): void {
1✔
341
    if (this.texture.props.dimension !== 'cube') {
×
342
      throw new Error(`${this} is not cube`);
×
343
    }
×
344
    const subresources = getTextureCubeSubresources(data);
×
345
    this._setTextureSubresources(subresources);
×
346
  }
×
347

1✔
348
  /** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
1✔
349
  setTextureCubeArrayData(data: TextureCubeArrayData): void {
1✔
350
    if (this.texture.props.dimension !== 'cube-array') {
×
351
      throw new Error(`${this} is not cube-array`);
×
352
    }
×
353
    const subresources = getTextureCubeArraySubresources(data);
×
354
    this._setTextureSubresources(subresources);
×
355
  }
×
356

1✔
357
  /** Sets multiple mip levels on different `z` slices (depth/array index) */
1✔
358
  private _setTextureSubresources(subresources: TextureSubresource[]): void {
1✔
359
    // If user supplied multiple mip levels, warn if auto-mips also requested
11✔
360
    // if (lodArray.length > 1 && this.props.mipmaps !== false) {
11✔
361
    //   log.warn(
11✔
362
    //     `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
11✔
363
    //   )();
11✔
364
    // }
11✔
365

11✔
366
    for (const subresource of subresources) {
11✔
367
      const {z, mipLevel} = subresource;
14✔
368
      switch (subresource.type) {
14✔
369
        case 'external-image':
14!
370
          const {image, flipY} = subresource;
×
371
          this.texture.copyExternalImage({image, z, mipLevel, flipY});
×
372
          break;
×
373
        case 'texture-data':
14✔
374
          const {data, textureFormat} = subresource;
14✔
375
          if (textureFormat && textureFormat !== this.texture.format) {
14!
NEW
376
            throw new Error(
×
NEW
377
              `${this} mip level ${mipLevel} uses format "${textureFormat}" but texture format is "${this.texture.format}"`
×
NEW
378
            );
×
NEW
379
          }
×
380
          this.texture.writeData(data.data, {
14✔
381
            x: 0,
14✔
382
            y: 0,
14✔
383
            z,
14✔
384
            width: data.width,
14✔
385
            height: data.height,
14✔
386
            depthOrArrayLayers: 1,
14✔
387
            mipLevel
14✔
388
          });
14✔
389
          break;
14✔
390
        default:
14!
391
          throw new Error('Unsupported 2D mip-level payload');
×
392
      }
14✔
393
    }
14✔
394
  }
11✔
395

1✔
396
  // ------------------ helpers ------------------
1✔
397

1✔
398
  /** Recursively resolve all promises in data structures */
1✔
399
  private async _loadAllData(props: TextureDataAsyncProps): Promise<TextureDataProps> {
1✔
400
    const syncData = await awaitAllPromises(props.data);
12✔
401
    const dimension = (props.dimension ?? '2d') as TextureDataProps['dimension'];
12✔
402
    return {dimension, data: syncData ?? null} as TextureDataProps;
12!
403
  }
12✔
404

1✔
405
  private _checkNotDestroyed() {
1✔
406
    if (this.destroyed) {
12!
407
      log.warn(`${this} already destroyed`);
×
408
    }
×
409
  }
12✔
410

1✔
411
  private _checkReady() {
1✔
UNCOV
412
    if (!this.isReady) {
×
UNCOV
413
      log.warn(`${this} Cannot perform this operation before ready`);
×
UNCOV
414
    }
×
UNCOV
415
  }
×
416

1✔
417
  static defaultProps: Required<DynamicTextureProps> = {
1✔
418
    ...Texture.defaultProps,
1✔
419
    dimension: '2d',
1✔
420
    data: null,
1✔
421
    mipmaps: false
1✔
422
  };
1✔
423
}
1✔
424

1✔
425
type TextureSubresourceAnalysis = {
1✔
426
  readonly subresources: TextureSubresource[];
1✔
427
  readonly mipLevels: number;
1✔
428
  readonly format?: TextureFormat;
1✔
429
  readonly hasExplicitMipChain: boolean;
1✔
430
};
1✔
431

1✔
432
// Flatten dimension-specific texture data into one list of uploadable subresources.
1✔
433
function getTextureSubresources(props: TextureDataProps): TextureSubresource[] {
12✔
434
  if (!props.data) {
12!
NEW
435
    return [];
×
NEW
436
  }
×
437

12✔
438
  switch (props.dimension) {
12✔
439
    case '1d':
12!
NEW
440
      return getTexture1DSubresources(props.data);
×
441
    case '2d':
12✔
442
      return getTexture2DSubresources(0, props.data);
12✔
443
    case '3d':
12!
NEW
444
      return getTexture3DSubresources(props.data);
×
445
    case '2d-array':
12!
NEW
446
      return getTextureArraySubresources(props.data);
×
447
    case 'cube':
12!
NEW
448
      return getTextureCubeSubresources(props.data);
×
449
    case 'cube-array':
12!
NEW
450
      return getTextureCubeArraySubresources(props.data);
×
451
    default:
12!
NEW
452
      throw new Error(`Unhandled dimension ${(props as TextureDataProps).dimension}`);
×
453
  }
12✔
454
}
12✔
455

1✔
456
// Resolve a consistent texture format and the longest mip chain valid across all slices.
1✔
457
function analyzeTextureSubresources(
11✔
458
  device: Device,
11✔
459
  subresources: TextureSubresource[],
11✔
460
  size: {width: number; height: number},
11✔
461
  options: {format?: TextureFormat}
11✔
462
): TextureSubresourceAnalysis {
11✔
463
  if (subresources.length === 0) {
11!
NEW
464
    return {
×
NEW
465
      subresources,
×
NEW
466
      mipLevels: 1,
×
NEW
467
      format: options.format,
×
NEW
468
      hasExplicitMipChain: false
×
NEW
469
    };
×
NEW
470
  }
×
471

11✔
472
  const groupedSubresources = new Map<number, TextureSubresource[]>();
11✔
473
  for (const subresource of subresources) {
11✔
474
    const group = groupedSubresources.get(subresource.z) ?? [];
17✔
475
    group.push(subresource);
17✔
476
    groupedSubresources.set(subresource.z, group);
17✔
477
  }
17✔
478

11✔
479
  const hasExplicitMipChain = subresources.some(subresource => subresource.mipLevel > 0);
11✔
480
  let resolvedFormat = options.format;
11✔
481
  let resolvedMipLevels = Number.POSITIVE_INFINITY;
11✔
482
  const validSubresources: TextureSubresource[] = [];
11✔
483

11✔
484
  for (const [z, sliceSubresources] of groupedSubresources) {
11✔
485
    // Validate each slice independently, then keep only the mip levels that are valid everywhere.
11✔
486
    const sortedSubresources = [...sliceSubresources].sort(
11✔
487
      (left, right) => left.mipLevel - right.mipLevel
11✔
488
    );
11✔
489
    const baseLevel = sortedSubresources[0];
11✔
490
    if (!baseLevel || baseLevel.mipLevel !== 0) {
11!
NEW
491
      throw new Error(`DynamicTexture: slice ${z} is missing mip level 0`);
×
NEW
492
    }
×
493

11✔
494
    const baseSize = getTextureSubresourceSize(device, baseLevel);
11✔
495
    if (baseSize.width !== size.width || baseSize.height !== size.height) {
11!
NEW
496
      throw new Error(
×
NEW
497
        `DynamicTexture: slice ${z} base level dimensions ${baseSize.width}x${baseSize.height} do not match expected ${size.width}x${size.height}`
×
NEW
498
      );
×
NEW
499
    }
×
500

11✔
501
    const baseFormat = getTextureSubresourceFormat(baseLevel);
11✔
502
    if (baseFormat) {
11✔
503
      if (resolvedFormat && resolvedFormat !== baseFormat) {
10!
NEW
504
        throw new Error(
×
NEW
505
          `DynamicTexture: slice ${z} base level format "${baseFormat}" does not match texture format "${resolvedFormat}"`
×
NEW
506
        );
×
NEW
507
      }
×
508
      resolvedFormat = baseFormat;
10✔
509
    }
10✔
510

11✔
511
    const mipLevelLimit =
11✔
512
      resolvedFormat && device.isTextureFormatCompressed(resolvedFormat)
11✔
513
        ? // Block-compressed formats cannot have mips smaller than a single compression block.
6✔
514
          getMaxCompressedMipLevels(device, baseSize.width, baseSize.height, resolvedFormat)
6✔
515
        : device.getMipLevelCount(baseSize.width, baseSize.height);
5✔
516

11✔
517
    let validMipLevelsForSlice = 0;
11✔
518
    for (
11✔
519
      let expectedMipLevel = 0;
11✔
520
      expectedMipLevel < sortedSubresources.length;
11✔
521
      expectedMipLevel++
11✔
522
    ) {
11✔
523
      const subresource = sortedSubresources[expectedMipLevel];
17✔
524
      // Stop at the first gap so callers can provide extra trailing data without breaking creation.
17✔
525
      if (!subresource || subresource.mipLevel !== expectedMipLevel) {
17!
NEW
526
        break;
×
NEW
527
      }
×
528
      if (expectedMipLevel >= mipLevelLimit) {
17✔
529
        break;
1✔
530
      }
1✔
531

16✔
532
      const subresourceSize = getTextureSubresourceSize(device, subresource);
16✔
533
      const expectedWidth = Math.max(1, baseSize.width >> expectedMipLevel);
16✔
534
      const expectedHeight = Math.max(1, baseSize.height >> expectedMipLevel);
16✔
535
      if (subresourceSize.width !== expectedWidth || subresourceSize.height !== expectedHeight) {
17✔
536
        break;
1✔
537
      }
1✔
538

15✔
539
      const subresourceFormat = getTextureSubresourceFormat(subresource);
15✔
540
      if (subresourceFormat) {
17✔
541
        if (!resolvedFormat) {
14!
NEW
542
          resolvedFormat = subresourceFormat;
×
NEW
543
        }
×
544
        // Later mip levels must stay on the same format as the validated base level.
14✔
545
        if (subresourceFormat !== resolvedFormat) {
14✔
546
          break;
1✔
547
        }
1✔
548
      }
14✔
549

14✔
550
      validMipLevelsForSlice++;
14✔
551
      validSubresources.push(subresource);
14✔
552
    }
14✔
553

11✔
554
    resolvedMipLevels = Math.min(resolvedMipLevels, validMipLevelsForSlice);
11✔
555
  }
11✔
556

11✔
557
  const mipLevels = Number.isFinite(resolvedMipLevels) ? Math.max(1, resolvedMipLevels) : 1;
11!
558

11✔
559
  return {
11✔
560
    // Keep every slice trimmed to the same mip count so the texture shape stays internally consistent.
11✔
561
    subresources: validSubresources.filter(subresource => subresource.mipLevel < mipLevels),
11✔
562
    mipLevels,
11✔
563
    format: resolvedFormat,
11✔
564
    hasExplicitMipChain
11✔
565
  };
11✔
566
}
11✔
567

1✔
568
// Read the per-level format using the transitional textureFormat -> format fallback rules.
1✔
569
function getTextureSubresourceFormat(subresource: TextureSubresource): TextureFormat | undefined {
26✔
570
  if (subresource.type !== 'texture-data') {
26!
NEW
571
    return undefined;
×
NEW
572
  }
×
573
  return subresource.textureFormat ?? resolveTextureImageFormat(subresource.data);
26✔
574
}
26✔
575

1✔
576
// Resolve dimensions from either raw bytes or external-image subresources.
1✔
577
function getTextureSubresourceSize(
27✔
578
  device: Device,
27✔
579
  subresource: TextureSubresource
27✔
580
): {width: number; height: number} {
27✔
581
  switch (subresource.type) {
27✔
582
    case 'external-image':
27!
NEW
583
      return device.getExternalImageSize(subresource.image);
×
584
    case 'texture-data':
27✔
585
      return {width: subresource.data.width, height: subresource.data.height};
27✔
586
    default:
27!
NEW
587
      throw new Error('Unsupported texture subresource');
×
588
  }
27✔
589
}
27✔
590

1✔
591
// Count the mip levels that stay at or above one compression block in each dimension.
1✔
592
function getMaxCompressedMipLevels(
6✔
593
  device: Device,
6✔
594
  baseWidth: number,
6✔
595
  baseHeight: number,
6✔
596
  format: TextureFormat
6✔
597
): number {
6✔
598
  const {blockWidth = 1, blockHeight = 1} = device.getTextureFormatInfo(format);
6✔
599
  let mipLevels = 1;
6✔
600
  for (let mipLevel = 1; ; mipLevel++) {
6✔
601
    const width = Math.max(1, baseWidth >> mipLevel);
16✔
602
    const height = Math.max(1, baseHeight >> mipLevel);
16✔
603
    if (width < blockWidth || height < blockHeight) {
16✔
604
      break;
6✔
605
    }
6✔
606
    mipLevels++;
10✔
607
  }
10✔
608
  return mipLevels;
6✔
609
}
6✔
610

1✔
611
// HELPERS
1✔
612

1✔
613
/** Resolve all promises in a nested data structure */
1✔
614
async function awaitAllPromises(x: any): Promise<any> {
25✔
615
  x = await x;
25✔
616
  if (Array.isArray(x)) {
25✔
617
    return await Promise.all(x.map(awaitAllPromises));
7✔
618
  }
7✔
619
  if (x && typeof x === 'object' && x.constructor === Object) {
25✔
620
    const object: Record<string, any> = x;
18✔
621
    const values = await Promise.all(Object.values(object));
18✔
622
    const keys = Object.keys(object);
18✔
623
    const resolvedObject: Record<string, any> = {};
18✔
624
    for (let i = 0; i < keys.length; i++) {
18✔
625
      resolvedObject[keys[i]] = values[i];
84✔
626
    }
84✔
627
    return resolvedObject;
18✔
628
  }
18!
629
  return x;
×
630
}
×
631

1✔
632
// /** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
1✔
633
// setTexture2DData(lodData: Texture2DData, z: number = 0): void {
1✔
634
//   this._checkReady();
1✔
635

1✔
636
//   const lodArray = this._normalizeTexture2DData(lodData);
1✔
637

1✔
638
//   // If user supplied multiple mip levels, warn if auto-mips also requested
1✔
639
//   if (lodArray.length > 1 && this.props.mipmaps !== false) {
1✔
640
//     log.warn(
1✔
641
//       `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
1✔
642
//     )();
1✔
643
//   }
1✔
644

1✔
645
//   for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
1✔
646
//     const imageData = lodArray[mipLevel];
1✔
647
//     if (this.device.isExternalImage(imageData)) {
1✔
648
//       this.texture.copyExternalImage({image: imageData, z, mipLevel, flipY: true});
1✔
649
//     } else if (this._isTextureImageData(imageData)) {
1✔
650
//       this.texture.copyImageData({data: imageData.data, z, mipLevel});
1✔
651
//     } else {
1✔
652
//       throw new Error('Unsupported 2D mip-level payload');
1✔
653
//     }
1✔
654
//   }
1✔
655
// }
1✔
656

1✔
657
// /** Normalize 2D layer payload into an array of mip-level items */
1✔
658
// private _normalizeTexture2DData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
1✔
659
//   return Array.isArray(data) ? data : [data];
1✔
660
// }
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc