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

visgl / luma.gl / 29588364653

17 Jul 2026 02:31PM UTC coverage: 72.239% (+0.002%) from 72.237%
29588364653

push

github

web-flow
feat(examples): add mediabunny video file source to video-texture example (#2739)

11298 of 17606 branches covered (64.17%)

Branch coverage included in aggregate %.

21862 of 28297 relevant lines covered (77.26%)

5601.28 hits per line

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

89.81
/modules/engine/src/dynamic-texture/texture-data.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {TypedArray, TextureFormat, ExternalImage} from '@luma.gl/core';
6
import {isExternalImage, getExternalImageSize} from '@luma.gl/core';
7

8
export type TextureImageSource = ExternalImage;
9

10
/**
11
 * One mip level
12
 * Basic data structure is similar to `ImageData`
13
 * additional optional fields can describe compressed texture data.
14
 */
15
export type TextureImageData = {
16
  /** Preferred WebGPU style format string. */
17
  textureFormat?: TextureFormat;
18
  /** WebGPU style format string. Defaults to 'rgba8unorm' */
19
  format?: TextureFormat;
20
  /** Typed Array with the bytes of the image. @note beware row byte alignment requirements */
21
  data: TypedArray;
22
  /** Width of the image, in pixels, @note beware row byte alignment requirements */
23
  width: number;
24
  /** Height of the image, in rows */
25
  height: number;
26
};
27

28
/**
29
 * A single mip-level can be initialized by data or an ImageBitmap etc
30
 * @note in the WebGPU spec a mip-level is called a subresource
31
 */
32
export type TextureMipLevelData = TextureImageData | TextureImageSource;
33

34
/**
35
 * Texture data for one image "slice" (which can consist of multiple miplevels)
36
 * Thus data for one slice be a single mip level or an array of miplevels
37
 * @note in the WebGPU spec each cross-section image in a 3D texture is called a "slice",
38
 * in a array texture each image in the array is called an array "layer"
39
 * luma.gl calls one image in a GPU texture a "slice" regardless of context.
40
 */
41
export type TextureSliceData = TextureMipLevelData | TextureMipLevelData[];
42

43
/** Names of cube texture faces */
44
export type TextureCubeFace = '+X' | '-X' | '+Y' | '-Y' | '+Z' | '-Z';
45

46
/** Array of cube texture faces. @note: index in array is the face index */
47
// biome-ignore format: preserve layout
48
export const TEXTURE_CUBE_FACES = ['+X', '-X', '+Y', '-Y', '+Z', '-Z'] as const satisfies readonly TextureCubeFace[];
147✔
49

50
/** Map of cube texture face names to face indexes */
51
// biome-ignore format: preserve layout
52
export const TEXTURE_CUBE_FACE_MAP = {'+X': 0, '-X': 1, '+Y': 2, '-Y': 3, '+Z': 4, '-Z': 5} as const satisfies Record<TextureCubeFace, number>;
147✔
53

54
/** @todo - Define what data type is supported for 1D textures. TextureImageData with height = 1 */
55
export type Texture1DData = TextureSliceData;
56

57
/** Texture data can be one or more mip levels */
58
export type Texture2DData = TextureSliceData;
59

60
/** Texture data accepted when initializing a standalone 2D texture. */
61
type Texture2DDataSource = Texture2DData | TypedArray;
62

63
/** 6 face textures */
64
export type TextureCubeData = Record<TextureCubeFace, TextureSliceData>;
65

66
/** Array of textures */
67
export type Texture3DData = TextureSliceData[];
68

69
/** Array of textures */
70
export type TextureArrayData = TextureSliceData[];
71

72
/** Array of 6 face textures */
73
export type TextureCubeArrayData = Record<TextureCubeFace, TextureSliceData>[];
74

75
type TextureData =
76
  | Texture1DData
77
  | Texture3DData
78
  | TextureArrayData
79
  | TextureCubeArrayData
80
  | TextureCubeData;
81

82
/** Sync data props */
83
export type TextureDataProps =
84
  | {dimension: '1d'; data: Texture1DData | null}
85
  | {dimension?: '2d'; data: Texture2DDataSource | null}
86
  | {dimension: '3d'; data: Texture3DData | null}
87
  | {dimension: '2d-array'; data: TextureArrayData | null}
88
  | {dimension: 'cube'; data: TextureCubeData | null}
89
  | {dimension: 'cube-array'; data: TextureCubeArrayData | null};
90

91
/** Async data props */
92
export type TextureDataAsyncProps =
93
  | {dimension: '1d'; data?: Promise<Texture1DData> | Texture1DData | null}
94
  | {dimension?: '2d'; data?: Promise<Texture2DDataSource> | Texture2DDataSource | null}
95
  | {dimension: '3d'; data?: Promise<Texture3DData> | Texture3DData | null}
96
  | {dimension: '2d-array'; data?: Promise<TextureArrayData> | TextureArrayData | null}
97
  | {dimension: 'cube'; data?: Promise<TextureCubeData> | TextureCubeData | null}
98
  | {dimension: 'cube-array'; data?: Promise<TextureCubeArrayData> | TextureCubeArrayData | null};
99

100
/** Describes data for one sub resource (one mip level of one slice (depth or array layer)) */
101
export type TextureSubresource = {
102
  /** slice (depth or array layer)) */
103
  z: number;
104
  /** mip level (0 - max mip levels) */
105
  mipLevel: number;
106
} & (
107
  | {
108
      type: 'external-image';
109
      image: ExternalImage;
110
      /** @deprecated is this an appropriate place for this flag? */
111
      flipY?: boolean;
112
    }
113
  | {
114
      type: 'texture-data';
115
      data: TextureImageData;
116
      textureFormat?: TextureFormat;
117
    }
118
);
119

120
/** Check if texture data is a typed array */
121
export function isTextureSliceData(data: TextureData): data is TextureImageData {
122
  const typedArray = (data as TextureImageData)?.data;
5✔
123
  return ArrayBuffer.isView(typedArray);
5✔
124
}
125

126
export function getFirstMipLevel(layer: TextureSliceData | null): TextureMipLevelData | null {
127
  if (!layer) return null;
77✔
128
  return Array.isArray(layer) ? (layer[0] ?? null) : layer;
76✔
129
}
130

131
export function getTextureSizeFromData(
132
  props: TextureDataProps
133
): {width: number; height: number} | null {
134
  const {dimension, data} = props;
78✔
135
  if (!data) {
78!
136
    return null;
×
137
  }
138

139
  switch (dimension) {
78!
140
    case '1d': {
141
      const mipLevel = getFirstMipLevel(data);
1✔
142
      if (!mipLevel) return null;
1!
143
      const {width} = getTextureMipLevelSize(mipLevel);
1✔
144
      return {width, height: 1};
1✔
145
    }
146
    case '2d': {
147
      if (ArrayBuffer.isView(data)) return null;
31✔
148
      const mipLevel = getFirstMipLevel(data);
30✔
149
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
30!
150
    }
151
    case '3d':
152
    case '2d-array': {
153
      if (!Array.isArray(data) || data.length === 0) return null;
40✔
154
      const mipLevel = getFirstMipLevel(data[0]);
38✔
155
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
38!
156
    }
157
    case 'cube': {
158
      const face = (Object.keys(data)[0] as TextureCubeFace) ?? null;
4✔
159
      if (!face) return null;
4✔
160
      const faceData = (data as Record<TextureCubeFace, TextureSliceData>)[face];
3✔
161
      const mipLevel = getFirstMipLevel(faceData);
3✔
162
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
3!
163
    }
164
    case 'cube-array': {
165
      if (!Array.isArray(data) || data.length === 0) return null;
2✔
166
      const firstCube = data[0];
1✔
167
      const face = (Object.keys(firstCube)[0] as TextureCubeFace) ?? null;
1!
168
      if (!face) return null;
1!
169
      const mipLevel = getFirstMipLevel(firstCube[face]);
1✔
170
      return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
1!
171
    }
172
    default:
173
      return null;
×
174
  }
175
}
176

177
function getTextureMipLevelSize(data: TextureMipLevelData): {width: number; height: number} {
178
  if (isExternalImage(data)) {
73✔
179
    return getExternalImageSize(data);
40✔
180
  }
181
  if (typeof data === 'object' && 'width' in data && 'height' in data) {
33✔
182
    return {width: data.width, height: data.height};
32✔
183
  }
184
  throw new Error('Unsupported mip-level data');
1✔
185
}
186

187
/** Type guard: is a mip-level `TextureImageData` (vs ExternalImage or bare typed array) */
188
function isTextureImageData(data: unknown): data is TextureImageData {
189
  return (
76✔
190
    typeof data === 'object' &&
374✔
191
    data !== null &&
192
    'data' in data &&
193
    'width' in data &&
194
    'height' in data
195
  );
196
}
197

198
function isTypedArrayMipLevelData(data: unknown): data is TypedArray {
199
  return ArrayBuffer.isView(data);
3✔
200
}
201

202
export function resolveTextureImageFormat(data: TextureImageData): TextureFormat | undefined {
203
  const {textureFormat, format} = data;
155✔
204
  if (textureFormat && format && textureFormat !== format) {
155✔
205
    throw new Error(
1✔
206
      `Conflicting texture formats "${textureFormat}" and "${format}" provided for the same mip level`
207
    );
208
  }
209
  return textureFormat ?? format;
154✔
210
}
211

212
/** Resolve size for a single mip-level datum */
213
// function getTextureMipLevelSizeFromData(data: TextureMipLevelData): {
214
//   width: number;
215
//   height: number;
216
// } {
217
//   if (this.device.isExternalImage(data)) {
218
//     return this.device.getExternalImageSize(data);
219
//   }
220
//   if (this.isTextureImageData(data)) {
221
//     return {width: data.width, height: data.height};
222
//   }
223
//   // Fallback (should not happen with current types)
224
//   throw new Error('Unsupported mip-level data');
225
// }
226

227
/** Convert cube face label to depth index */
228
export function getCubeFaceIndex(face: TextureCubeFace): number {
229
  const idx = TEXTURE_CUBE_FACE_MAP[face];
36✔
230
  if (idx === undefined) throw new Error(`Invalid cube face: ${face}`);
36!
231
  return idx;
36✔
232
}
233

234
/** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
235
export function getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
236
  return 6 * cubeIndex + getCubeFaceIndex(face);
12✔
237
}
238

239
// ------------------ Upload helpers ------------------
240

241
/** Experimental: Set multiple mip levels (1D) */
242
export function getTexture1DSubresources(data: Texture1DData): TextureSubresource[] {
243
  // Not supported in WebGL; left explicit
244
  throw new Error('setTexture1DData not supported in WebGL.');
×
245
  // const subresources: TextureSubresource[] = [];
246
  // return subresources;
247
}
248

249
/** Normalize 2D layer payload into an array of mip-level items */
250
function _normalizeTexture2DData(
251
  data: Texture2DDataSource
252
): (TextureImageData | ExternalImage | TypedArray)[] {
253
  return Array.isArray(data) ? data : [data];
119✔
254
}
255

256
/** Experimental: Set multiple mip levels (2D), optionally at `z` (depth/array index) */
257
export function getTexture2DSubresources(
258
  slice: number,
259
  lodData: Texture2DDataSource,
260
  baseLevelSize?: {width: number; height: number},
261
  textureFormat?: TextureFormat
262
): TextureSubresource[] {
263
  const lodArray = _normalizeTexture2DData(lodData);
119✔
264
  const z = slice;
119✔
265

266
  const subresources: TextureSubresource[] = [];
119✔
267

268
  for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
119✔
269
    const imageData = lodArray[mipLevel];
173✔
270
    if (isExternalImage(imageData)) {
173✔
271
      subresources.push({
97✔
272
        type: 'external-image',
273
        image: imageData,
274
        z,
275
        mipLevel
276
      });
277
    } else if (isTextureImageData(imageData)) {
76✔
278
      subresources.push({
73✔
279
        type: 'texture-data',
280
        data: imageData,
281
        textureFormat: resolveTextureImageFormat(imageData),
282
        z,
283
        mipLevel
284
      });
285
    } else if (isTypedArrayMipLevelData(imageData) && baseLevelSize) {
3!
286
      subresources.push({
3✔
287
        type: 'texture-data',
288
        data: {
289
          data: imageData,
290
          width: Math.max(1, baseLevelSize.width >> mipLevel),
291
          height: Math.max(1, baseLevelSize.height >> mipLevel),
292
          ...(textureFormat ? {format: textureFormat} : {})
3!
293
        },
294
        textureFormat,
295
        z,
296
        mipLevel
297
      });
298
    } else {
299
      throw new Error('Unsupported 2D mip-level payload');
×
300
    }
301
  }
302

303
  return subresources;
118✔
304
}
305

306
/** 3D: multiple depth slices, each may carry multiple mip levels */
307
export function getTexture3DSubresources(data: Texture3DData): TextureSubresource[] {
308
  const subresources: TextureSubresource[] = [];
3✔
309
  for (let depth = 0; depth < data.length; depth++) {
3✔
310
    subresources.push(...getTexture2DSubresources(depth, data[depth]));
8✔
311
  }
312
  return subresources;
3✔
313
}
314

315
/** 2D array: multiple layers, each may carry multiple mip levels */
316
export function getTextureArraySubresources(data: TextureArrayData): TextureSubresource[] {
317
  const subresources: TextureSubresource[] = [];
37✔
318
  for (let layer = 0; layer < data.length; layer++) {
37✔
319
    subresources.push(...getTexture2DSubresources(layer, data[layer]));
38✔
320
  }
321
  return subresources;
37✔
322
}
323

324
/** Cube: 6 faces, each may carry multiple mip levels */
325
export function getTextureCubeSubresources(data: TextureCubeData): TextureSubresource[] {
326
  const subresources: TextureSubresource[] = [];
4✔
327
  for (const [face, faceData] of Object.entries(data) as [TextureCubeFace, TextureSliceData][]) {
4✔
328
    const faceDepth = getCubeFaceIndex(face);
24✔
329
    subresources.push(...getTexture2DSubresources(faceDepth, faceData));
24✔
330
  }
331
  return subresources;
4✔
332
}
333

334
/** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
335
export function getTextureCubeArraySubresources(data: TextureCubeArrayData): TextureSubresource[] {
336
  const subresources: TextureSubresource[] = [];
1✔
337
  data.forEach((cubeData, cubeIndex) => {
1✔
338
    for (const [face, faceData] of Object.entries(cubeData)) {
2✔
339
      const faceDepth = getCubeArrayFaceIndex(cubeIndex, face as TextureCubeFace);
12✔
340
      subresources.push(...getTexture2DSubresources(faceDepth, faceData));
12✔
341
    }
342
  });
343
  return subresources;
1✔
344
}
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