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

visgl / luma.gl / 23355601514

20 Mar 2026 05:50PM UTC coverage: 52.166% (-25.8%) from 77.934%
23355601514

push

github

web-flow
chore: Migrate to vitest (#2554)

4188 of 11561 branches covered (36.23%)

Branch coverage included in aggregate %.

613 of 632 new or added lines in 22 files covered. (96.99%)

343 existing lines in 28 files now uncovered.

7757 of 11337 relevant lines covered (68.42%)

394.31 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

184
      if (this.device.isTextureFormatCompressed(resolvedFormat) && !userProvidedUsage) {
13✔
185
        baseTextureProps.usage = Texture.SAMPLE | Texture.COPY_DST;
11✔
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 &&
12!
191
        !textureData.hasExplicitMipChain &&
192
        !this.device.isTextureFormatCompressed(resolvedFormat);
193

194
      if (this.device.type === 'webgpu' && shouldGenerateMipmaps) {
13!
195
        const requiredUsage =
196
          this.props.dimension === '3d'
5!
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;
5✔
200
      }
201

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

395
  // ------------------ helpers ------------------
396

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

404
  private _checkNotDestroyed() {
405
    if (this.destroyed) {
13!
406
      log.warn(`${this} already destroyed`);
5✔
407
    }
408
  }
409

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

549
      validMipLevelsForSlice++;
15✔
550
      validSubresources.push(subresource);
15✔
551
    }
552

553
    resolvedMipLevels = Math.min(resolvedMipLevels, validMipLevelsForSlice);
12✔
554
  }
555

556
  const mipLevels = Number.isFinite(resolvedMipLevels) ? Math.max(1, resolvedMipLevels) : 1;
12!
557

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

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

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

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

610
// HELPERS
611

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

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

635
//   const lodArray = this._normalizeTexture2DData(lodData);
636

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

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

656
// /** Normalize 2D layer payload into an array of mip-level items */
657
// private _normalizeTexture2DData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
658
//   return Array.isArray(data) ? data : [data];
659
// }
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