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

visgl / luma.gl / 29102742641

10 Jul 2026 03:11PM UTC coverage: 72.239% (+0.002%) from 72.237%
29102742641

Pull #2738

github

web-flow
Merge 08287dab9 into 8e8dabc15
Pull Request #2738: fix(engine): clean up texture data types

11298 of 17606 branches covered (64.17%)

Branch coverage included in aggregate %.

21862 of 28297 relevant lines covered (77.26%)

5601.2 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 | TypedArray;
59

60
/** 6 face textures */
61
export type TextureCubeData = Record<TextureCubeFace, TextureSliceData>;
62

63
/** Array of textures */
64
export type Texture3DData = TextureSliceData[];
65

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

69
/** Array of 6 face textures */
70
export type TextureCubeArrayData = Record<TextureCubeFace, TextureSliceData>[];
71

72
type TextureData =
73
  | Texture1DData
74
  | Texture3DData
75
  | TextureArrayData
76
  | TextureCubeArrayData
77
  | TextureCubeData;
78

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

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

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

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

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

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

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

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

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

195
function isTypedArrayMipLevelData(data: unknown): data is TypedArray {
196
  return ArrayBuffer.isView(data);
3✔
197
}
198

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

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

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

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

236
// ------------------ Upload helpers ------------------
237

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

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

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

263
  const subresources: TextureSubresource[] = [];
119✔
264

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

300
  return subresources;
118✔
301
}
302

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

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

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

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