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

visgl / luma.gl / 17192422902

24 Aug 2025 06:41PM UTC coverage: 76.322% (+1.1%) from 75.234%
17192422902

push

github

web-flow
test(engine): add ShaderPassRenderer test (#2437)

2264 of 2952 branches covered (76.69%)

Branch coverage included in aggregate %.

520 of 666 new or added lines in 7 files covered. (78.08%)

14 existing lines in 1 file now uncovered.

28544 of 37414 relevant lines covered (76.29%)

65.14 hits per line

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

73.52
/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} 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
  getTexture1DSubresources,
1✔
33
  getTexture2DSubresources,
1✔
34
  getTexture3DSubresources,
1✔
35
  getTextureCubeSubresources,
1✔
36
  getTextureArraySubresources,
1✔
37
  getTextureCubeArraySubresources
1✔
38
} from './texture-data';
1✔
39

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

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

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

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

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

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

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

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

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

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

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

2✔
128
    this.initAsync(originalPropsWithAsyncData);
2✔
129
  }
2✔
130

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

2✔
140
      const propsWithSyncData = await this._loadAllData(originalPropsWithAsyncData);
2✔
141
      this._checkNotDestroyed();
2✔
142

2✔
143
      // Deduce size when not explicitly provided
2✔
144
      // TODO - what about depth?
2✔
145
      const deduceSize = (): {width: number; height: number} => {
2✔
146
        if (this.props.width && this.props.height) {
2!
NEW
147
          return {width: this.props.width, height: this.props.height};
×
NEW
148
        }
×
149

2✔
150
        const size = getTextureSizeFromData(propsWithSyncData);
2✔
151
        if (size) {
2✔
152
          return size;
2✔
153
        }
2!
UNCOV
154

×
155
        return {width: this.props.width || 1, height: this.props.height || 1};
2!
156
      };
2✔
157

2✔
158
      const size = deduceSize();
2✔
159
      if (!size || size.width <= 0 || size.height <= 0) {
2!
NEW
160
        throw new Error(`${this} size could not be determined or was zero`);
×
NEW
161
      }
×
162

2✔
163
      // Create a minimal TextureProps and validate via `satisfies`
2✔
164
      const baseTextureProps = {
2✔
165
        ...this.props,
2✔
166
        ...size,
2✔
167
        mipLevels: 1, // temporary; updated below
2✔
168
        data: undefined
2✔
169
      } satisfies TextureProps;
2✔
170

2✔
171
      // Compute mip levels (auto clamps to max)
2✔
172
      const maxMips = this.device.getMipLevelCount(baseTextureProps.width, baseTextureProps.height);
2✔
173
      const desired =
2✔
174
        this.props.mipLevels === 'auto'
2!
NEW
175
          ? maxMips
×
176
          : Math.max(1, Math.min(maxMips, this.props.mipLevels ?? 1));
2!
177

2✔
178
      const finalTextureProps: TextureProps = {...baseTextureProps, mipLevels: desired};
2✔
179

2✔
180
      this._texture = this.device.createTexture(finalTextureProps);
2✔
181
      this._sampler = this.texture.sampler;
2✔
182
      this._view = this.texture.view;
2✔
183

2✔
184
      // Upload data if provided
2✔
185
      if (propsWithSyncData.data) {
2✔
186
        switch (propsWithSyncData.dimension) {
2✔
187
          case '1d':
2!
NEW
188
            this.setTexture1DData(propsWithSyncData.data);
×
NEW
189
            break;
×
190
          case '2d':
2✔
191
            this.setTexture2DData(propsWithSyncData.data);
2✔
192
            break;
2✔
193
          case '3d':
2!
NEW
194
            this.setTexture3DData(propsWithSyncData.data);
×
NEW
195
            break;
×
196
          case '2d-array':
2!
NEW
197
            this.setTextureArrayData(propsWithSyncData.data);
×
NEW
198
            break;
×
199
          case 'cube':
2!
NEW
200
            this.setTextureCubeData(propsWithSyncData.data);
×
NEW
201
            break;
×
202
          case 'cube-array':
2!
NEW
203
            this.setTextureCubeArrayData(propsWithSyncData.data);
×
NEW
204
            break;
×
205
          default: {
2!
NEW
206
            throw new Error(`Unhandled dimension ${propsWithSyncData.dimension}`);
×
NEW
207
          }
×
208
        }
2✔
209
      }
2✔
210

2✔
211
      if (this.props.mipmaps) {
2!
NEW
212
        this.generateMipmaps();
×
NEW
213
      }
×
214

2✔
215
      this.isReady = true;
2✔
216
      this.resolveReady(this.texture);
2✔
217

2✔
218
      log.info(0, `${this} created`)();
2✔
219
    } catch (e) {
2!
NEW
220
      const err = e instanceof Error ? e : new Error(String(e));
×
NEW
221
      this.rejectReady(err);
×
NEW
222
      throw err;
×
NEW
223
    }
×
224
  }
2✔
225

1✔
226
  destroy(): void {
1✔
227
    if (this._texture) {
2✔
228
      this._texture.destroy();
2✔
229
      this._texture = null;
2✔
230
      this._sampler = null;
2✔
231
      this._view = null;
2✔
232
    }
2✔
233
    this.destroyed = true;
2✔
234
  }
2✔
235

1✔
236
  generateMipmaps(): void {
1✔
NEW
237
    // Only supported via WebGL helper (luma.gl path)
×
NEW
238
    if (this.device.type === 'webgl') {
×
NEW
239
      this.texture.generateMipmapsWebGL();
×
NEW
240
    } else {
×
NEW
241
      throw new Error('Automatic mipmap generation not supported on this device');
×
NEW
242
    }
×
UNCOV
243
  }
×
244

1✔
245
  /** Set sampler or create one from props */
1✔
246
  setSampler(sampler: Sampler | SamplerProps = {}): void {
1✔
NEW
247
    this._checkReady();
×
NEW
248
    const s = sampler instanceof Sampler ? sampler : this.device.createSampler(sampler);
×
NEW
249
    this.texture.setSampler(s);
×
NEW
250
    this._sampler = s;
×
UNCOV
251
  }
×
252

1✔
253
  /**
1✔
254
   * Resize by cloning the underlying immutable texture.
1✔
255
   * Does not copy contents; caller may need to re-upload and/or regenerate mips.
1✔
256
   */
1✔
257
  resize(size: {width: number; height: number}): boolean {
1✔
NEW
258
    this._checkReady();
×
259

×
260
    if (size.width === this.texture.width && size.height === this.texture.height) {
×
261
      return false;
×
262
    }
×
NEW
263
    const prev = this.texture;
×
NEW
264
    this._texture = prev.clone(size);
×
NEW
265
    this._sampler = this.texture.sampler;
×
NEW
266
    this._view = this.texture.view;
×
267

×
NEW
268
    prev.destroy();
×
NEW
269
    log.info(`${this} resized`);
×
270
    return true;
×
271
  }
×
272

1✔
273
  /** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
1✔
274
  getCubeFaceIndex(face: TextureCubeFace): number {
1✔
NEW
275
    const index = TEXTURE_CUBE_FACE_MAP[face];
×
NEW
276
    if (index === undefined) throw new Error(`Invalid cube face: ${face}`);
×
NEW
277
    return index;
×
UNCOV
278
  }
×
279

1✔
280
  /** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
1✔
281
  getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
1✔
NEW
282
    return 6 * cubeIndex + this.getCubeFaceIndex(face);
×
UNCOV
283
  }
×
284

1✔
285
  /** @note experimental: Set multiple mip levels (1D) */
1✔
286
  setTexture1DData(data: Texture1DData): void {
1✔
NEW
287
    this._checkReady();
×
NEW
288
    if (this.texture.props.dimension !== '1d') {
×
NEW
289
      throw new Error(`${this} is not 1d`);
×
NEW
290
    }
×
NEW
291
    const subresources = getTexture1DSubresources(data);
×
NEW
292
    this._setTextureSubresources(subresources);
×
UNCOV
293
  }
×
294

1✔
295
  /** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
1✔
296
  setTexture2DData(lodData: Texture2DData, z: number = 0): void {
1✔
297
    this._checkReady();
2✔
298
    if (this.texture.props.dimension !== '2d') {
2!
NEW
299
      throw new Error(`${this} is not 2d`);
×
UNCOV
300
    }
×
301

2✔
302
    const subresources = getTexture2DSubresources(z, lodData);
2✔
303
    this._setTextureSubresources(subresources);
2✔
304
  }
2✔
305

1✔
306
  /** 3D: multiple depth slices, each may carry multiple mip levels */
1✔
307
  setTexture3DData(data: Texture3DData): void {
1✔
NEW
308
    if (this.texture.props.dimension !== '3d') {
×
NEW
309
      throw new Error(`${this} is not 3d`);
×
UNCOV
310
    }
×
NEW
311
    const subresources = getTexture3DSubresources(data);
×
NEW
312
    this._setTextureSubresources(subresources);
×
NEW
313
  }
×
314

1✔
315
  /** 2D array: multiple layers, each may carry multiple mip levels */
1✔
316
  setTextureArrayData(data: TextureArrayData): void {
1✔
NEW
317
    if (this.texture.props.dimension !== '2d-array') {
×
NEW
318
      throw new Error(`${this} is not 2d-array`);
×
UNCOV
319
    }
×
NEW
320
    const subresources = getTextureArraySubresources(data);
×
NEW
321
    this._setTextureSubresources(subresources);
×
UNCOV
322
  }
×
323

1✔
324
  /** Cube: 6 faces, each may carry multiple mip levels */
1✔
325
  setTextureCubeData(data: TextureCubeData): void {
1✔
NEW
326
    if (this.texture.props.dimension !== 'cube') {
×
NEW
327
      throw new Error(`${this} is not cube`);
×
328
    }
×
NEW
329
    const subresources = getTextureCubeSubresources(data);
×
NEW
330
    this._setTextureSubresources(subresources);
×
UNCOV
331
  }
×
332

1✔
333
  /** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
1✔
334
  private setTextureCubeArrayData(data: TextureCubeArrayData): void {
1✔
NEW
335
    if (this.texture.props.dimension !== 'cube-array') {
×
NEW
336
      throw new Error(`${this} is not cube-array`);
×
337
    }
×
NEW
338
    const subresources = getTextureCubeArraySubresources(data);
×
NEW
339
    this._setTextureSubresources(subresources);
×
UNCOV
340
  }
×
341

1✔
342
  /** Sets multiple mip levels on different `z` slices (depth/array index) */
1✔
343
  private _setTextureSubresources(subresources: TextureSubresource[]): void {
1✔
344
    // If user supplied multiple mip levels, warn if auto-mips also requested
2✔
345
    // if (lodArray.length > 1 && this.props.mipmaps !== false) {
2✔
346
    //   log.warn(
2✔
347
    //     `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
2✔
348
    //   )();
2✔
349
    // }
2✔
350

2✔
351
    for (const subresource of subresources) {
2✔
352
      const {z, mipLevel} = subresource;
2✔
353
      switch (subresource.type) {
2✔
354
        case 'external-image':
2!
NEW
355
          const {image, flipY} = subresource;
×
NEW
356
          this.texture.copyExternalImage({image, z, mipLevel, flipY});
×
NEW
357
          break;
×
358
        case 'texture-data':
2✔
359
          const {data} = subresource;
2✔
360
          // TODO - we are throwing away some of the info in data.
2✔
361
          // Did we not need it in the first place? Can we use it to validate?
2✔
362
          this.texture.copyImageData({data: data.data, z, mipLevel});
2✔
363
          break;
2✔
364
        default:
2!
NEW
365
          throw new Error('Unsupported 2D mip-level payload');
×
366
      }
2✔
367
    }
2✔
368
  }
2✔
369

1✔
370
  // ------------------ helpers ------------------
1✔
371

1✔
372
  /** Recursively resolve all promises in data structures */
1✔
373
  private async _loadAllData(props: TextureDataAsyncProps): Promise<TextureDataProps> {
1✔
374
    const syncData = await awaitAllPromises(props.data);
2✔
375
    const dimension = (props.dimension ?? '2d') as TextureDataProps['dimension'];
2✔
376
    return {dimension, data: syncData ?? null} as TextureDataProps;
2!
377
  }
2✔
378

1✔
379
  private _checkNotDestroyed() {
1✔
380
    if (this.destroyed) {
2!
NEW
381
      log.warn(`${this} already destroyed`);
×
382
    }
×
383
  }
2✔
384

1✔
385
  private _checkReady() {
1✔
386
    if (!this.isReady) {
2✔
387
      log.warn(`${this} Cannot perform this operation before ready`);
2✔
388
    }
2✔
389
  }
2✔
390

1✔
391
  static defaultProps: Required<DynamicTextureProps> = {
1✔
392
    ...Texture.defaultProps,
1✔
393
    dimension: '2d',
1✔
394
    data: null,
1✔
395
    mipmaps: false
1✔
396
  };
1✔
397
}
1✔
398

1✔
399
// HELPERS
1✔
400

1✔
401
/** Resolve all promises in a nested data structure */
1✔
402
async function awaitAllPromises(x: any): Promise<any> {
2✔
403
  x = await x;
2✔
404
  if (Array.isArray(x)) {
2!
405
    return await Promise.all(x.map(awaitAllPromises));
×
406
  }
×
407
  if (x && typeof x === 'object' && x.constructor === Object) {
2✔
408
    const object: Record<string, any> = x;
2✔
409
    const values = await Promise.all(Object.values(object));
2✔
410
    const keys = Object.keys(object);
2✔
411
    const resolvedObject: Record<string, any> = {};
2✔
412
    for (let i = 0; i < keys.length; i++) {
2✔
413
      resolvedObject[keys[i]] = values[i];
7✔
414
    }
7✔
415
    return resolvedObject;
2✔
416
  }
2!
UNCOV
417
  return x;
×
UNCOV
418
}
×
419

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

1✔
424
//   const lodArray = this._normalizeTexture2DData(lodData);
1✔
425

1✔
426
//   // If user supplied multiple mip levels, warn if auto-mips also requested
1✔
427
//   if (lodArray.length > 1 && this.props.mipmaps !== false) {
1✔
428
//     log.warn(
1✔
429
//       `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
1✔
430
//     )();
1✔
431
//   }
1✔
432

1✔
433
//   for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
1✔
434
//     const imageData = lodArray[mipLevel];
1✔
435
//     if (this.device.isExternalImage(imageData)) {
1✔
436
//       this.texture.copyExternalImage({image: imageData, z, mipLevel, flipY: true});
1✔
437
//     } else if (this._isTextureImageData(imageData)) {
1✔
438
//       this.texture.copyImageData({data: imageData.data, z, mipLevel});
1✔
439
//     } else {
1✔
440
//       throw new Error('Unsupported 2D mip-level payload');
1✔
441
//     }
1✔
442
//   }
1✔
443
// }
1✔
444

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