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

visgl / luma.gl / 21063730492

16 Jan 2026 10:34AM UTC coverage: 76.381% (-0.003%) from 76.384%
21063730492

push

github

web-flow
fix(core): Auto-convert uint8 buffers to uint16 (#2486) (#2491)

2291 of 2972 branches covered (77.09%)

Branch coverage included in aggregate %.

9 of 15 new or added lines in 4 files covered. (60.0%)

51 existing lines in 2 files now uncovered.

28602 of 37474 relevant lines covered (76.32%)

70.36 hits per line

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

73.08
/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');
55!
96
    return this._texture;
55✔
97
  }
55✔
98
  get sampler(): Sampler {
1✔
99
    if (!this._sampler) throw new Error('Sampler not initialized yet');
×
100
    return this._sampler;
×
101
  }
×
102
  get view(): TextureView {
1✔
103
    if (!this._view) throw new Error('View not initialized yet');
×
104
    return this._view;
×
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...'})`;
10✔
112
  }
10✔
113

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
236
  generateMipmaps(): void {
1✔
237
    // Call the WebGL-style mipmap generation helper
×
238
    // WebGL implementation generates mipmaps, WebGPU logs a warning
×
NEW
239
    if (this.device.type === 'webgl') {
×
240
      this.texture.generateMipmapsWebGL();
×
241
    } else {
×
NEW
242
      log.warn(
×
NEW
243
        'Mipmap generation not yet implemented on WebGPU: your texture data will not be correctly initialized'
×
NEW
244
      );
×
UNCOV
245
    }
×
UNCOV
246
  }
×
247

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

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

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

×
271
    prev.destroy();
×
272
    log.info(`${this} resized`);
×
273
    return true;
×
UNCOV
274
  }
×
275

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

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

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

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

5✔
305
    const subresources = getTexture2DSubresources(z, lodData);
5✔
306
    this._setTextureSubresources(subresources);
5✔
307
  }
5✔
308

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

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

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

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

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

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

1✔
373
  // ------------------ helpers ------------------
1✔
374

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

1✔
382
  private _checkNotDestroyed() {
1✔
383
    if (this.destroyed) {
5!
384
      log.warn(`${this} already destroyed`);
×
UNCOV
385
    }
×
386
  }
5✔
387

1✔
388
  private _checkReady() {
1✔
389
    if (!this.isReady) {
5✔
390
      log.warn(`${this} Cannot perform this operation before ready`);
5✔
391
    }
5✔
392
  }
5✔
393

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

1✔
402
// HELPERS
1✔
403

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

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

1✔
427
//   const lodArray = this._normalizeTexture2DData(lodData);
1✔
428

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

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

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