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

visgl / luma.gl / 28703724114

04 Jul 2026 10:45AM UTC coverage: 72.048% (-0.1%) from 72.186%
28703724114

Pull #2733

github

web-flow
Merge d2e0a0b57 into 30d8c7ace
Pull Request #2733: [codex] Add physical GPU data struct formats

11272 of 17603 branches covered (64.03%)

Branch coverage included in aggregate %.

7 of 64 new or added lines in 2 files covered. (10.94%)

17 existing lines in 3 files now uncovered.

21719 of 28187 relevant lines covered (77.05%)

5622.39 hits per line

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

4.76
/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
/** Resolved 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<Fields extends GPUDataStructFields = GPUDataStructFields> =
32
  Readonly<{
33
    /** Discriminator separating struct formats from string GPU vector formats. */
34
    type: 'struct';
35
    /** Packing rules used to resolve field offsets. */
36
    layout: GPUDataStructLayout;
37
    /** Resolved fields in declaration order. */
38
    fields: Readonly<{[Name in keyof Fields]: GPUDataStructField<Fields[Name]>}>;
39
    /** Total number of scalar components represented by one row. */
40
    components: number;
41
    /** Bytes between adjacent struct rows. */
42
    byteStride: number;
43
    /** Bytes through the end of the final field payload, excluding trailing row padding. */
44
    rowByteLength: number;
45
  }>;
46

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

50
/** Options for creating a physical GPU data struct format. */
51
export type MakeGPUDataStructFormatOptions = {
52
  /** Packing rules. Defaults to `wgsl-storage`. */
53
  layout?: GPUDataStructLayout;
54
};
55

56
/** Options for deriving a vertex buffer layout from a GPU data struct format. */
57
export type BufferLayoutFromGPUDataStructFormatOptions = {
58
  /** Whether rows advance per vertex or per instance. */
59
  stepMode?: 'vertex' | 'instance';
60
};
61

62
/** Returns true when a GPU data format describes named interleaved fields. */
63
export function isGPUDataStructFormat(
64
  format: GPUDataFormat | undefined
65
): format is GPUDataStructFormat {
66
  return Boolean(format && typeof format === 'object' && format.type === 'struct');
1,157!
67
}
68

69
/**
70
 * Resolves named physical field formats into one immutable interleaved row format.
71
 *
72
 * `packed` applies WebGPU vertex-buffer alignment: each field offset is aligned to
73
 * `min(4, byteLength)` and the final byte stride is aligned to four bytes.
74
 * `wgsl-storage` uses WGSL storage-struct alignment for raw storage carrier types.
75
 */
76
export function makeGPUDataStructFormat<const Fields extends GPUDataStructFields>(
77
  fieldFormats: Fields,
78
  options: MakeGPUDataStructFormatOptions = {}
×
79
): GPUDataStructFormat<Fields> {
NEW
80
  const fieldEntries = Object.entries(fieldFormats) as [keyof Fields & string, VertexFormat][];
×
NEW
81
  if (fieldEntries.length === 0) {
×
NEW
82
    throw new Error('GPUData struct format must declare at least one field');
×
83
  }
84

NEW
85
  const layout = options.layout ?? 'wgsl-storage';
×
NEW
86
  return layout === 'packed'
×
87
    ? makePackedGPUDataStructFormat(fieldEntries)
88
    : makeStorageGPUDataStructFormat(fieldEntries);
89
}
90

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

109
function makePackedGPUDataStructFormat<Fields extends GPUDataStructFields>(
110
  fieldEntries: [keyof Fields & string, VertexFormat][]
111
): GPUDataStructFormat<Fields> {
NEW
112
  const fieldLayouts: [string, GPUDataStructField][] = [];
×
NEW
113
  let byteOffset = 0;
×
NEW
114
  let components = 0;
×
115

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

NEW
136
  return Object.freeze({
×
137
    type: 'struct',
138
    layout: 'packed',
139
    fields: Object.freeze(Object.fromEntries(fieldLayouts)),
140
    components,
141
    byteStride: alignTo(byteOffset, 4),
142
    rowByteLength: byteOffset
143
  }) as GPUDataStructFormat<Fields>;
144
}
145

146
function makeStorageGPUDataStructFormat<Fields extends GPUDataStructFields>(
147
  fieldEntries: [keyof Fields & string, VertexFormat][]
148
): GPUDataStructFormat<Fields> {
NEW
149
  const storageTypes = Object.fromEntries(
×
NEW
150
    fieldEntries.map(([fieldName, format]) => [fieldName, getStorageType(format)])
×
151
  ) as Record<string, VariableShaderType>;
152

NEW
153
  const storageLayout = makeShaderBlockLayout(storageTypes, {layout: 'wgsl-storage'});
×
NEW
154
  const fieldLayouts: [string, GPUDataStructField][] = [];
×
NEW
155
  let rowByteLength = 0;
×
NEW
156
  let components = 0;
×
157

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

NEW
173
  return Object.freeze({
×
174
    type: 'struct',
175
    layout: 'wgsl-storage',
176
    fields: Object.freeze(Object.fromEntries(fieldLayouts)),
177
    components,
178
    byteStride: storageLayout.byteLength,
179
    rowByteLength
180
  }) as GPUDataStructFormat<Fields>;
181
}
182

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

199
function getComponentStorageType(
200
  primitiveType: 'f32' | 'i32' | 'u32',
201
  components: 1 | 2 | 3 | 4
202
): VariableShaderType {
NEW
203
  return components === 1 ? primitiveType : `vec${components}<${primitiveType}>`;
×
204
}
205

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