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

visgl / luma.gl / 28707854106

04 Jul 2026 01:33PM UTC coverage: 72.095% (-0.1%) from 72.227%
28707854106

push

github

web-flow
feat(tables) Declare physical GPU data struct formats inline (#2733)

11252 of 17588 branches covered (63.98%)

Branch coverage included in aggregate %.

25 of 83 new or added lines in 3 files covered. (30.12%)

21826 of 28293 relevant lines covered (77.14%)

5601.54 hits per line

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

5.08
/modules/tables/src/table/gpu-data-format.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {
6
  makeShaderBlockLayout,
7
  vertexFormatDecoder,
8
  type BufferLayout,
9
  type VariableShaderType,
10
  type VertexFormat
11
} from '@luma.gl/core';
12
import type {GPUVectorFormat} from './gpu-vector-format';
13

14
/**
15
 * Named fixed-width memory formats stored in one interleaved GPU data row.
16
 * Object insertion order defines field declaration order.
17
 */
18
export type GPUDataStructFields = Readonly<Record<string, VertexFormat>>;
19

20
/**
21
 * Supported physical packing rules for one GPU data struct row.
22
 * `wgsl-storage` follows WGSL storage alignment; `packed` follows WebGPU vertex alignment.
23
 */
24
export type GPUDataStructLayout = 'wgsl-storage' | 'packed';
25

26
/**
27
 * Computed physical metadata for one field in a GPU data struct row.
28
 *
29
 * @typeParam Format - Fixed-width format stored for the field.
30
 */
31
export type GPUDataStructField<Format extends VertexFormat = VertexFormat> = Readonly<{
32
  /** Fixed-width memory format stored for this field. */
33
  format: Format;
34
  /** Byte offset of this field from the start of each row. */
35
  byteOffset: number;
36
  /** Number of bytes occupied by this field's physical format. */
37
  byteLength: number;
38
}>;
39

40
/**
41
 * Canonical physical metadata for named fixed-width fields interleaved in one GPU data row.
42
 * The constructor derives this immutable object from an inline field declaration.
43
 *
44
 * @typeParam Fields - Field names and fixed-width formats in declaration order.
45
 * @typeParam Layout - Physical alignment rules applied to the fields.
46
 */
47
export type GPUDataStructFormat<
48
  Fields extends GPUDataStructFields = GPUDataStructFields,
49
  Layout extends GPUDataStructLayout = GPUDataStructLayout
50
> = Readonly<{
51
  /** Discriminator separating struct formats from string GPU vector formats. */
52
  type: 'struct';
53
  /** Packing rules used to resolve field offsets. */
54
  layout: Layout;
55
  /** Computed fields in declaration order. */
56
  fields: Readonly<{[Name in keyof Fields]: GPUDataStructField<Fields[Name]>}>;
57
  /** Total number of scalar components represented by one row. */
58
  components: number;
59
  /** Bytes between adjacent struct rows. */
60
  byteStride: number;
61
  /** Bytes through the end of the final field payload, excluding trailing row padding. */
62
  rowByteLength: number;
63
}>;
64

65
/** Canonical physical metadata retained by a `GPUData` instance. */
66
export type GPUDataFormat = GPUVectorFormat | GPUDataStructFormat;
67

68
/**
69
 * Scalar/list format string or inline struct fields accepted by the `GPUData` constructor.
70
 *
71
 * @internal
72
 */
73
export type GPUDataFormatDeclaration = GPUVectorFormat | GPUDataStructFields;
74

75
/** Options for lowering a GPU data struct format into a vertex buffer layout. */
76
export type BufferLayoutFromGPUDataStructFormatOptions = {
77
  /** Whether rows advance per vertex or per instance. */
78
  stepMode?: 'vertex' | 'instance';
79
};
80

81
/**
82
 * Tests whether canonical GPU data metadata describes named interleaved fields.
83
 *
84
 * @param format - Canonical GPU data metadata to inspect.
85
 * @returns `true` when `format` is a `GPUDataStructFormat`.
86
 */
87
export function isGPUDataStructFormat(
88
  format: GPUDataFormat | undefined
89
): format is GPUDataStructFormat {
90
  return Boolean(format && typeof format === 'object' && format.type === 'struct');
1,167!
91
}
92

93
/**
94
 * Normalizes named physical field formats into one immutable interleaved row format.
95
 *
96
 * @param fieldFormats - Named fixed-width formats in declaration order.
97
 * @param layout - Physical alignment rules to apply.
98
 * @returns Canonical field offsets and row metadata.
99
 *
100
 * @internal
101
 */
102
export function normalizeGPUDataStructFormat<
103
  const Fields extends GPUDataStructFields,
104
  const Layout extends GPUDataStructLayout
105
>(fieldFormats: Fields, layout: Layout): GPUDataStructFormat<Fields, Layout> {
NEW
106
  const fieldEntries = Object.entries(fieldFormats) as [keyof Fields & string, VertexFormat][];
×
NEW
107
  if (fieldEntries.length === 0) {
×
NEW
108
    throw new Error('GPUData struct format must declare at least one field');
×
109
  }
110

NEW
111
  return (
×
112
    layout === 'packed'
×
113
      ? makePackedGPUDataStructFormat(fieldEntries)
114
      : makeStorageGPUDataStructFormat(fieldEntries)
115
  ) as GPUDataStructFormat<Fields, Layout>;
116
}
117

118
/**
119
 * Converts physical GPU data struct metadata into an interleaved vertex buffer layout.
120
 *
121
 * @param name - Logical buffer binding name.
122
 * @param format - Canonical struct metadata to lower.
123
 * @param options - Optional vertex step mode.
124
 * @returns A buffer layout that preserves field order, offsets, formats, and row stride.
125
 */
126
export function getBufferLayoutFromGPUDataStructFormat(
127
  name: string,
128
  format: GPUDataStructFormat,
129
  options: BufferLayoutFromGPUDataStructFormatOptions = {}
×
130
): BufferLayout {
NEW
131
  return {
×
132
    name,
133
    byteStride: format.byteStride,
134
    ...(options.stepMode ? {stepMode: options.stepMode} : {}),
×
NEW
135
    attributes: Object.entries(format.fields).map(([attribute, field]) => ({
×
136
      attribute,
137
      format: field.format,
138
      byteOffset: field.byteOffset
139
    }))
140
  };
141
}
142

143
/** Computes the minimum padding permitted by WebGPU vertex buffer layout rules. */
144
function makePackedGPUDataStructFormat<Fields extends GPUDataStructFields>(
145
  fieldEntries: [keyof Fields & string, VertexFormat][]
146
): GPUDataStructFormat<Fields, 'packed'> {
NEW
147
  const fieldLayouts: [string, GPUDataStructField][] = [];
×
NEW
148
  let byteOffset = 0;
×
NEW
149
  let components = 0;
×
150

NEW
151
  for (const [fieldName, format] of fieldEntries) {
×
NEW
152
    const formatInfo = vertexFormatDecoder.getVertexFormatInfo(format);
×
NEW
153
    if (formatInfo.webglOnly) {
×
NEW
154
      throw new Error(
×
155
        `Packed GPUData struct field "${fieldName}" uses WebGL-only format ${format}`
156
      );
157
    }
158
    // WebGPU vertex attributes align to their byte width, capped at four bytes.
NEW
159
    byteOffset = alignTo(byteOffset, Math.min(4, formatInfo.byteLength));
×
NEW
160
    fieldLayouts.push([
×
161
      fieldName,
162
      Object.freeze({
163
        format,
164
        byteOffset,
165
        byteLength: formatInfo.byteLength
166
      })
167
    ]);
NEW
168
    byteOffset += formatInfo.byteLength;
×
NEW
169
    components += formatInfo.components;
×
170
  }
171

NEW
172
  return Object.freeze({
×
173
    type: 'struct',
174
    layout: 'packed',
175
    fields: Object.freeze(Object.fromEntries(fieldLayouts)),
176
    components,
177
    byteStride: alignTo(byteOffset, 4),
178
    rowByteLength: byteOffset
179
  }) as GPUDataStructFormat<Fields, 'packed'>;
180
}
181

182
/** Computes WGSL storage offsets using shader types that can carry each physical format. */
183
function makeStorageGPUDataStructFormat<Fields extends GPUDataStructFields>(
184
  fieldEntries: [keyof Fields & string, VertexFormat][]
185
): GPUDataStructFormat<Fields, 'wgsl-storage'> {
NEW
186
  const storageTypes = Object.fromEntries(
×
NEW
187
    fieldEntries.map(([fieldName, format]) => [fieldName, getStorageType(format)])
×
188
  ) as Record<string, VariableShaderType>;
189

NEW
190
  const storageLayout = makeShaderBlockLayout(storageTypes, {layout: 'wgsl-storage'});
×
NEW
191
  const fieldLayouts: [string, GPUDataStructField][] = [];
×
NEW
192
  let rowByteLength = 0;
×
NEW
193
  let components = 0;
×
194

NEW
195
  for (const [fieldName, format] of fieldEntries) {
×
NEW
196
    const formatInfo = vertexFormatDecoder.getVertexFormatInfo(format);
×
NEW
197
    const byteOffset = storageLayout.fields[fieldName].offset * 4;
×
NEW
198
    fieldLayouts.push([
×
199
      fieldName,
200
      Object.freeze({
201
        format,
202
        byteOffset,
203
        byteLength: formatInfo.byteLength
204
      })
205
    ]);
NEW
206
    rowByteLength = Math.max(rowByteLength, byteOffset + formatInfo.byteLength);
×
NEW
207
    components += formatInfo.components;
×
208
  }
209

NEW
210
  return Object.freeze({
×
211
    type: 'struct',
212
    layout: 'wgsl-storage',
213
    fields: Object.freeze(Object.fromEntries(fieldLayouts)),
214
    components,
215
    byteStride: storageLayout.byteLength,
216
    rowByteLength
217
  }) as GPUDataStructFormat<Fields, 'wgsl-storage'>;
218
}
219

220
/** Returns the WGSL scalar or vector carrier used to lay out one physical field. */
221
function getStorageType(format: VertexFormat): VariableShaderType {
NEW
222
  const formatInfo = vertexFormatDecoder.getVertexFormatInfo(format);
×
NEW
223
  switch (formatInfo.type) {
×
224
    case 'float32':
NEW
225
      return getComponentStorageType('f32', formatInfo.components);
×
226
    case 'sint32':
NEW
227
      return getComponentStorageType('i32', formatInfo.components);
×
228
    case 'uint32':
NEW
229
      return getComponentStorageType('u32', formatInfo.components);
×
230
    default: {
231
      // Compact and normalized formats are stored in one or more raw u32 words.
NEW
232
      const wordCount = Math.ceil(formatInfo.byteLength / 4);
×
NEW
233
      return getComponentStorageType('u32', wordCount as 1 | 2);
×
234
    }
235
  }
236
}
237

238
/** Builds a scalar or vector shader type for a primitive carrier and component count. */
239
function getComponentStorageType(
240
  primitiveType: 'f32' | 'i32' | 'u32',
241
  components: 1 | 2 | 3 | 4
242
): VariableShaderType {
NEW
243
  return components === 1 ? primitiveType : `vec${components}<${primitiveType}>`;
×
244
}
245

246
/** Rounds a byte offset up to the requested positive alignment. */
247
function alignTo(value: number, alignment: number): number {
NEW
248
  return Math.ceil(value / alignment) * alignment;
×
249
}
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