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

visgl / luma.gl / 23357510199

20 Mar 2026 06:40PM UTC coverage: 58.158% (+5.9%) from 52.213%
23357510199

push

github

web-flow
chore: Run tests on src instead of dist (#2555)

3021 of 6029 branches covered (50.11%)

Branch coverage included in aggregate %.

7102 of 11377 relevant lines covered (62.42%)

243.33 hits per line

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

71.72
/modules/engine/src/dynamic-texture/texture-data.ts
1
import type {TypedArray, TextureFormat, ExternalImage} from '@luma.gl/core';
2
import {isExternalImage, getExternalImageSize} from '@luma.gl/core';
3

4
export type TextureImageSource = ExternalImage;
5

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

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

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

39
/** Names of cube texture faces */
40
export type TextureCubeFace = '+X' | '-X' | '+Y' | '-Y' | '+Z' | '-Z';
41

42
/** Array of cube texture faces. @note: index in array is the face index */
43
// prettier-ignore
44
export const TEXTURE_CUBE_FACES = ['+X', '-X', '+Y', '-Y', '+Z', '-Z'] as const satisfies readonly TextureCubeFace[];
59✔
45

46
/** Map of cube texture face names to face indexes */
47
// prettier-ignore
48
export const TEXTURE_CUBE_FACE_MAP = {'+X': 0, '-X': 1, '+Y': 2, '-Y': 3, '+Z': 4, '-Z': 5} as const satisfies Record<TextureCubeFace, number>;
59✔
49

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

53
/** Texture data can be one or more mip levels */
54
export type Texture2DData = TextureSliceData;
55

56
/** 6 face textures */
57
export type TextureCubeData = Record<TextureCubeFace, TextureSliceData>;
58

59
/** Array of textures */
60
export type Texture3DData = TextureSliceData[];
61

62
/** Array of textures */
63
export type TextureArrayData = TextureSliceData[];
64

65
/** Array of 6 face textures */
66
export type TextureCubeArrayData = Record<TextureCubeFace, TextureSliceData>[];
67

68
type TextureData =
69
  | Texture1DData
70
  | Texture3DData
71
  | TextureArrayData
72
  | TextureCubeArrayData
73
  | TextureCubeData;
74

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

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

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

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

119
export function getFirstMipLevel(layer: TextureSliceData | null): TextureMipLevelData | null {
120
  if (!layer) return null;
25✔
121
  return Array.isArray(layer) ? (layer[0] ?? null) : layer;
24✔
122
}
123

124
export function getTextureSizeFromData(
125
  props: TextureDataProps
126
): {width: number; height: number} | null {
127
  const {dimension, data} = props;
25✔
128
  if (!data) {
25!
129
    return null;
×
130
  }
131

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

169
function getTextureMipLevelSize(data: TextureMipLevelData): {width: number; height: number} {
170
  if (isExternalImage(data)) {
21✔
171
    return getExternalImageSize(data);
1✔
172
  }
173
  if (typeof data === 'object' && 'width' in data && 'height' in data) {
20✔
174
    return {width: data.width, height: data.height};
19✔
175
  }
176
  throw new Error('Unsupported mip-level data');
1✔
177
}
178

179
/** Type guard: is a mip-level `TextureImageData` (vs ExternalImage) */
180
function isTextureImageData(data: TextureMipLevelData): data is TextureImageData {
181
  return (
19✔
182
    typeof data === 'object' &&
95✔
183
    data !== null &&
184
    'data' in data &&
185
    'width' in data &&
186
    'height' in data
187
  );
188
}
189

190
export function resolveTextureImageFormat(data: TextureImageData): TextureFormat | undefined {
191
  const {textureFormat, format} = data;
21✔
192
  if (textureFormat && format && textureFormat !== format) {
21✔
193
    throw new Error(
1✔
194
      `Conflicting texture formats "${textureFormat}" and "${format}" provided for the same mip level`
195
    );
196
  }
197
  return textureFormat ?? format;
20✔
198
}
199

200
/** Resolve size for a single mip-level datum */
201
// function getTextureMipLevelSizeFromData(data: TextureMipLevelData): {
202
//   width: number;
203
//   height: number;
204
// } {
205
//   if (this.device.isExternalImage(data)) {
206
//     return this.device.getExternalImageSize(data);
207
//   }
208
//   if (this.isTextureImageData(data)) {
209
//     return {width: data.width, height: data.height};
210
//   }
211
//   // Fallback (should not happen with current types)
212
//   throw new Error('Unsupported mip-level data');
213
// }
214

215
/** Convert cube face label to depth index */
216
export function getCubeFaceIndex(face: TextureCubeFace): number {
217
  const idx = TEXTURE_CUBE_FACE_MAP[face];
×
218
  if (idx === undefined) throw new Error(`Invalid cube face: ${face}`);
×
219
  return idx;
×
220
}
221

222
/** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
223
export function getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
224
  return 6 * cubeIndex + getCubeFaceIndex(face);
×
225
}
226

227
// ------------------ Upload helpers ------------------
228

229
/** Experimental: Set multiple mip levels (1D) */
230
export function getTexture1DSubresources(data: Texture1DData): TextureSubresource[] {
231
  // Not supported in WebGL; left explicit
232
  throw new Error('setTexture1DData not supported in WebGL.');
×
233
  // const subresources: TextureSubresource[] = [];
234
  // return subresources;
235
}
236

237
/** Normalize 2D layer payload into an array of mip-level items */
238
function _normalizeTexture2DData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
239
  return Array.isArray(data) ? data : [data];
13✔
240
}
241

242
/** Experimental: Set multiple mip levels (2D), optionally at `z` (depth/array index) */
243
export function getTexture2DSubresources(
244
  slice: number,
245
  lodData: Texture2DData
246
): TextureSubresource[] {
247
  const lodArray = _normalizeTexture2DData(lodData);
13✔
248
  const z = slice;
13✔
249

250
  const subresources: TextureSubresource[] = [];
13✔
251

252
  for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
13✔
253
    const imageData = lodArray[mipLevel];
19✔
254
    if (isExternalImage(imageData)) {
19!
255
      subresources.push({
×
256
        type: 'external-image',
257
        image: imageData,
258
        z,
259
        mipLevel
260
      });
261
    } else if (isTextureImageData(imageData)) {
19!
262
      subresources.push({
19✔
263
        type: 'texture-data',
264
        data: imageData,
265
        textureFormat: resolveTextureImageFormat(imageData),
266
        z,
267
        mipLevel
268
      });
269
    } else {
270
      throw new Error('Unsupported 2D mip-level payload');
×
271
    }
272
  }
273

274
  return subresources;
12✔
275
}
276

277
/** 3D: multiple depth slices, each may carry multiple mip levels */
278
export function getTexture3DSubresources(data: Texture3DData): TextureSubresource[] {
279
  const subresources: TextureSubresource[] = [];
×
280
  for (let depth = 0; depth < data.length; depth++) {
×
281
    subresources.push(...getTexture2DSubresources(depth, data[depth]));
×
282
  }
283
  return subresources;
×
284
}
285

286
/** 2D array: multiple layers, each may carry multiple mip levels */
287
export function getTextureArraySubresources(data: TextureArrayData): TextureSubresource[] {
288
  const subresources: TextureSubresource[] = [];
×
289
  for (let layer = 0; layer < data.length; layer++) {
×
290
    subresources.push(...getTexture2DSubresources(layer, data[layer]));
×
291
  }
292
  return subresources;
×
293
}
294

295
/** Cube: 6 faces, each may carry multiple mip levels */
296
export function getTextureCubeSubresources(data: TextureCubeData): TextureSubresource[] {
297
  const subresources: TextureSubresource[] = [];
×
298
  for (const [face, faceData] of Object.entries(data) as [TextureCubeFace, TextureSliceData][]) {
×
299
    const faceDepth = getCubeFaceIndex(face);
×
300
    subresources.push(...getTexture2DSubresources(faceDepth, faceData));
×
301
  }
302
  return subresources;
×
303
}
304

305
/** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
306
export function getTextureCubeArraySubresources(data: TextureCubeArrayData): TextureSubresource[] {
307
  const subresources: TextureSubresource[] = [];
×
308
  data.forEach((cubeData, cubeIndex) => {
×
309
    for (const [face, faceData] of Object.entries(cubeData)) {
×
310
      const faceDepth = getCubeArrayFaceIndex(cubeIndex, face as TextureCubeFace);
×
311
      subresources.push(...getTexture2DSubresources(faceDepth, faceData));
×
312
    }
313
  });
314
  return subresources;
×
315
}
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