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

visgl / luma.gl / 28705267989

04 Jul 2026 11:49AM UTC coverage: 72.054% (-0.1%) from 72.186%
28705267989

Pull #2733

github

web-flow
Merge 8c7886bfa into 30d8c7ace
Pull Request #2733: [codex] Declare physical GPU data struct formats inline

11275 of 17606 branches covered (64.04%)

Branch coverage included in aggregate %.

22 of 79 new or added lines in 3 files covered. (27.85%)

10 existing lines in 3 files now uncovered.

21723 of 28190 relevant lines covered (77.06%)

5622.03 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
/** Named fixed-width memory formats stored in one interleaved GPU data row. */
15
export type GPUDataStructFields = Readonly<Record<string, VertexFormat>>;
16

17
/** Supported physical packing rules for one GPU data struct row. */
18
export type GPUDataStructLayout = 'wgsl-storage' | 'packed';
19

20
/** Computed physical metadata for one field in a GPU data struct row. */
21
export type GPUDataStructField<Format extends VertexFormat = VertexFormat> = Readonly<{
22
  /** Fixed-width memory format stored for this field. */
23
  format: Format;
24
  /** Byte offset of this field from the start of each row. */
25
  byteOffset: number;
26
  /** Number of bytes occupied by this field's physical format. */
27
  byteLength: number;
28
}>;
29

30
/** Physical format for named fixed-width fields interleaved in one GPU data row. */
31
export type GPUDataStructFormat<
32
  Fields extends GPUDataStructFields = GPUDataStructFields,
33
  Layout extends GPUDataStructLayout = GPUDataStructLayout
34
> = Readonly<{
35
  /** Discriminator separating struct formats from string GPU vector formats. */
36
  type: 'struct';
37
  /** Packing rules used to resolve field offsets. */
38
  layout: Layout;
39
  /** Computed fields in declaration order. */
40
  fields: Readonly<{[Name in keyof Fields]: GPUDataStructField<Fields[Name]>}>;
41
  /** Total number of scalar components represented by one row. */
42
  components: number;
43
  /** Bytes between adjacent struct rows. */
44
  byteStride: number;
45
  /** Bytes through the end of the final field payload, excluding trailing row padding. */
46
  rowByteLength: number;
47
}>;
48

49
/** Canonical physical metadata accepted by GPUData. */
50
export type GPUDataFormat = GPUVectorFormat | GPUDataStructFormat;
51

52
/** Format declaration accepted by the GPUData constructor. */
53
export type GPUDataFormatDeclaration = GPUVectorFormat | GPUDataStructFields;
54

55
/** Canonical runtime format stored by GPUData. @internal */
56
export type GPUDataFormatT<
57
  Format extends GPUDataFormatDeclaration,
58
  Layout extends GPUDataStructLayout | null
59
> = Layout extends GPUDataStructLayout
60
  ? GPUDataStructFormat<Extract<Format, GPUDataStructFields>, Layout>
61
  : Extract<Format, GPUVectorFormat>;
62

63
/** Options for deriving a vertex buffer layout from a GPU data struct format. */
64
export type BufferLayoutFromGPUDataStructFormatOptions = {
65
  /** Whether rows advance per vertex or per instance. */
66
  stepMode?: 'vertex' | 'instance';
67
};
68

69
/** Returns true when a GPU data format describes named interleaved fields. */
70
export function isGPUDataStructFormat(
71
  format: GPUDataFormat | undefined
72
): format is GPUDataStructFormat {
73
  return Boolean(format && typeof format === 'object' && format.type === 'struct');
1,157!
74
}
75

76
/**
77
 * Normalizes named physical field formats into one immutable interleaved row format.
78
 * @internal GPUData constructor implementation.
79
 */
80
export function normalizeGPUDataStructFormat<
81
  const Fields extends GPUDataStructFields,
82
  const Layout extends GPUDataStructLayout
83
>(fieldFormats: Fields, layout: Layout): GPUDataStructFormat<Fields, Layout> {
NEW
84
  const fieldEntries = Object.entries(fieldFormats) as [keyof Fields & string, VertexFormat][];
×
NEW
85
  if (fieldEntries.length === 0) {
×
NEW
86
    throw new Error('GPUData struct format must declare at least one field');
×
87
  }
88

NEW
89
  return (
×
90
    layout === 'packed'
×
91
      ? makePackedGPUDataStructFormat(fieldEntries)
92
      : makeStorageGPUDataStructFormat(fieldEntries)
93
  ) as GPUDataStructFormat<Fields, Layout>;
94
}
95

96
/** Converts physical GPU data struct metadata into an interleaved vertex buffer layout. */
97
export function getBufferLayoutFromGPUDataStructFormat(
98
  name: string,
99
  format: GPUDataStructFormat,
100
  options: BufferLayoutFromGPUDataStructFormatOptions = {}
×
101
): BufferLayout {
NEW
102
  return {
×
103
    name,
104
    byteStride: format.byteStride,
105
    ...(options.stepMode ? {stepMode: options.stepMode} : {}),
×
NEW
106
    attributes: Object.entries(format.fields).map(([attribute, field]) => ({
×
107
      attribute,
108
      format: field.format,
109
      byteOffset: field.byteOffset
110
    }))
111
  };
112
}
113

114
function makePackedGPUDataStructFormat<Fields extends GPUDataStructFields>(
115
  fieldEntries: [keyof Fields & string, VertexFormat][]
116
): GPUDataStructFormat<Fields, 'packed'> {
NEW
117
  const fieldLayouts: [string, GPUDataStructField][] = [];
×
NEW
118
  let byteOffset = 0;
×
NEW
119
  let components = 0;
×
120

NEW
121
  for (const [fieldName, format] of fieldEntries) {
×
NEW
122
    const formatInfo = vertexFormatDecoder.getVertexFormatInfo(format);
×
NEW
123
    if (formatInfo.webglOnly) {
×
NEW
124
      throw new Error(
×
125
        `Packed GPUData struct field "${fieldName}" uses WebGL-only format ${format}`
126
      );
127
    }
NEW
128
    byteOffset = alignTo(byteOffset, Math.min(4, formatInfo.byteLength));
×
NEW
129
    fieldLayouts.push([
×
130
      fieldName,
131
      Object.freeze({
132
        format,
133
        byteOffset,
134
        byteLength: formatInfo.byteLength
135
      })
136
    ]);
NEW
137
    byteOffset += formatInfo.byteLength;
×
NEW
138
    components += formatInfo.components;
×
139
  }
140

NEW
141
  return Object.freeze({
×
142
    type: 'struct',
143
    layout: 'packed',
144
    fields: Object.freeze(Object.fromEntries(fieldLayouts)),
145
    components,
146
    byteStride: alignTo(byteOffset, 4),
147
    rowByteLength: byteOffset
148
  }) as GPUDataStructFormat<Fields, 'packed'>;
149
}
150

151
function makeStorageGPUDataStructFormat<Fields extends GPUDataStructFields>(
152
  fieldEntries: [keyof Fields & string, VertexFormat][]
153
): GPUDataStructFormat<Fields, 'wgsl-storage'> {
NEW
154
  const storageTypes = Object.fromEntries(
×
NEW
155
    fieldEntries.map(([fieldName, format]) => [fieldName, getStorageType(format)])
×
156
  ) as Record<string, VariableShaderType>;
157

NEW
158
  const storageLayout = makeShaderBlockLayout(storageTypes, {layout: 'wgsl-storage'});
×
NEW
159
  const fieldLayouts: [string, GPUDataStructField][] = [];
×
NEW
160
  let rowByteLength = 0;
×
NEW
161
  let components = 0;
×
162

NEW
163
  for (const [fieldName, format] of fieldEntries) {
×
NEW
164
    const formatInfo = vertexFormatDecoder.getVertexFormatInfo(format);
×
NEW
165
    const byteOffset = storageLayout.fields[fieldName].offset * 4;
×
NEW
166
    fieldLayouts.push([
×
167
      fieldName,
168
      Object.freeze({
169
        format,
170
        byteOffset,
171
        byteLength: formatInfo.byteLength
172
      })
173
    ]);
NEW
174
    rowByteLength = Math.max(rowByteLength, byteOffset + formatInfo.byteLength);
×
NEW
175
    components += formatInfo.components;
×
176
  }
177

NEW
178
  return Object.freeze({
×
179
    type: 'struct',
180
    layout: 'wgsl-storage',
181
    fields: Object.freeze(Object.fromEntries(fieldLayouts)),
182
    components,
183
    byteStride: storageLayout.byteLength,
184
    rowByteLength
185
  }) as GPUDataStructFormat<Fields, 'wgsl-storage'>;
186
}
187

188
function getStorageType(format: VertexFormat): VariableShaderType {
NEW
189
  const formatInfo = vertexFormatDecoder.getVertexFormatInfo(format);
×
NEW
190
  switch (formatInfo.type) {
×
191
    case 'float32':
NEW
192
      return getComponentStorageType('f32', formatInfo.components);
×
193
    case 'sint32':
NEW
194
      return getComponentStorageType('i32', formatInfo.components);
×
195
    case 'uint32':
NEW
196
      return getComponentStorageType('u32', formatInfo.components);
×
197
    default: {
NEW
198
      const wordCount = Math.ceil(formatInfo.byteLength / 4);
×
NEW
199
      return getComponentStorageType('u32', wordCount as 1 | 2);
×
200
    }
201
  }
202
}
203

204
function getComponentStorageType(
205
  primitiveType: 'f32' | 'i32' | 'u32',
206
  components: 1 | 2 | 3 | 4
207
): VariableShaderType {
NEW
208
  return components === 1 ? primitiveType : `vec${components}<${primitiveType}>`;
×
209
}
210

211
function alignTo(value: number, alignment: number): number {
NEW
212
  return Math.ceil(value / alignment) * alignment;
×
213
}
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