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

visgl / luma.gl / 28705022289

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

Pull #2733

github

web-flow
Merge 5c9311f3d 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%)

5621.91 hits per line

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

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

5
import {
6
  type Buffer,
7
  type Device,
8
  type BufferAttributeLayout,
9
  type BufferLayout,
10
  type BufferProps
11
} from '@luma.gl/core';
12
import {DynamicBuffer, type DynamicBufferProps} from '@luma.gl/engine';
13
import {GPUData} from './gpu-data';
14
import {getGPUVectorFormatInfo, type GPUVectorFormat} from './gpu-vector-format';
15

16
/** Buffer creation props used by format-specific producers before wrapping storage in a GPUVector. */
17
export type GPUVectorBufferProps = Omit<BufferProps, 'byteLength' | 'data'>;
18
/** Dynamic buffer props used when creating appendable GPU vectors. */
19
export type GPUVectorDynamicBufferProps = Omit<DynamicBufferProps, 'byteLength' | 'data'>;
20

21
/** Constructor props that wrap an existing typed GPU buffer. */
22
export type GPUVectorFromBufferProps<T extends GPUVectorFormat = GPUVectorFormat> = {
23
  /** Discriminator for existing-buffer construction. */
24
  type: 'buffer';
25
  /** Stable vector name. */
26
  name: string;
27
  /** Existing GPU buffer. */
28
  buffer: Buffer | DynamicBuffer;
29
  /** Canonical memory-layout descriptor for values stored in the buffer, when representable. */
30
  format?: T;
31
  /** Number of logical rows in the buffer. */
32
  length: number;
33
  /** Number of fixed rows or flattened vertex-list values in the buffer. */
34
  valueLength?: number;
35
  /** Number of scalar values represented by one fixed row or flattened element. */
36
  stride?: number;
37
  /** Byte offset of the first logical row. */
38
  byteOffset?: number;
39
  /** Bytes between adjacent fixed rows or flattened elements. */
40
  byteStride?: number;
41
  /** Number of bytes occupied by one fixed row or flattened element payload. */
42
  rowByteLength?: number;
43
  /** Whether this vector should destroy the wrapped buffer. */
44
  ownsBuffer?: boolean;
45
  /** Optional adapter-owned metadata; core tables do not inspect this value. */
46
  dataType?: unknown;
47
};
48

49
/** Constructor props that wrap one interleaved GPU buffer. */
50
export type GPUVectorFromInterleavedProps<T extends GPUVectorFormat = GPUVectorFormat> = {
51
  /** Discriminator for interleaved-buffer construction. */
52
  type: 'interleaved';
53
  /** Stable vector name. */
54
  name: string;
55
  /** Existing interleaved GPU buffer. */
56
  buffer: Buffer | DynamicBuffer;
57
  /** Optional canonical memory-layout descriptor when the interleaved row also has one value view. */
58
  format?: T;
59
  /** Number of logical rows in the buffer. */
60
  length: number;
61
  /** Number of fixed rows or flattened vertex-list values in the buffer. */
62
  valueLength?: number;
63
  /** Byte offset of the first logical row. */
64
  byteOffset?: number;
65
  /** Bytes between adjacent logical rows. */
66
  byteStride: number;
67
  /** Attribute views stored in each interleaved row. */
68
  attributes: BufferAttributeLayout[];
69
  /** Whether this vector should destroy the wrapped buffer. */
70
  ownsBuffer?: boolean;
71
  /** Optional adapter-owned metadata; core tables do not inspect this value. */
72
  dataType?: unknown;
73
};
74

75
/** Constructor props that expose existing GPU data chunks as one logical vector. */
76
export type GPUVectorFromDataProps<T extends GPUVectorFormat = GPUVectorFormat> = {
77
  /** Discriminator for chunk-backed construction. */
78
  type: 'data';
79
  /** Stable vector name. */
80
  name: string;
81
  /** Canonical memory-layout descriptor shared by every chunk. Defaults to the first chunk format. */
82
  format?: T;
83
  /** Existing GPU data chunks to expose through this vector. */
84
  data: GPUData<T>[];
85
  /** Number of scalar values represented by one fixed row or flattened element. */
86
  stride?: number;
87
  /** Number of fixed rows or flattened vertex-list values across all chunks. */
88
  valueLength?: number;
89
  /** Bytes between adjacent fixed rows or flattened elements. Defaults to the first chunk stride. */
90
  byteStride?: number;
91
  /** Number of bytes occupied by one fixed row or flattened element payload. */
92
  rowByteLength?: number;
93
  /** Optional buffer layout retained for interleaved chunk collections. */
94
  bufferLayout?: BufferLayout;
95
  /** Whether this vector should destroy the supplied GPU data chunks. */
96
  ownsData?: boolean;
97
  /** Optional adapter-owned metadata; core tables do not inspect this value. */
98
  dataType?: unknown;
99
};
100

101
/** Constructor props for an appendable vector that accepts future GPUData chunks. */
102
export type GPUVectorFromAppendableProps<T extends GPUVectorFormat = GPUVectorFormat> = {
103
  /** Discriminator for appendable GPUData-backed construction. */
104
  type: 'appendable';
105
  /** Stable vector name. */
106
  name: string;
107
  /** Device that creates the DynamicBuffer. */
108
  device: Device;
109
  /** Canonical memory-layout descriptor for appended data. */
110
  format: T;
111
  /** Number of scalar values represented by one fixed row or flattened element. */
112
  stride?: number;
113
  /** Initial number of flattened vertex-list values. Defaults to `0`. */
114
  valueLength?: number;
115
  /** Bytes between adjacent fixed rows or flattened elements. */
116
  byteStride?: number;
117
  /** Number of bytes occupied by one fixed row or flattened element payload. */
118
  rowByteLength?: number;
119
  /** Initial row capacity. Defaults to `0`. */
120
  initialCapacityRows?: number;
121
  /** Capacity growth multiplier. Defaults to `1.5`. */
122
  capacityGrowthFactor?: number;
123
  /** Buffer props forwarded when adapter code creates appended GPUData buffers. */
124
  bufferProps?: GPUVectorDynamicBufferProps;
125
  /** Optional adapter-owned metadata; core tables do not inspect this value. */
126
  dataType?: unknown;
127
};
128

129
/** Discriminated constructor props for {@link GPUVector}. */
130
export type GPUVectorCreateProps<T extends GPUVectorFormat = GPUVectorFormat> =
131
  | GPUVectorFromBufferProps<T>
132
  | GPUVectorFromInterleavedProps<T>
133
  | GPUVectorFromDataProps<T>
134
  | GPUVectorFromAppendableProps<T>;
135

136
/**
137
 * GPU memory and format metadata for one vector-valued table column.
138
 *
139
 * Format-specific modules upload bytes and use these vectors to expose shared
140
 * lifecycle, chunking, batching, and ownership semantics.
141
 */
142
export class GPUVector<T extends GPUVectorFormat = GPUVectorFormat> {
143
  /** Stable vector name. */
144
  readonly name: string;
145
  /** Optional adapter-owned metadata; core tables do not inspect this value. */
146
  readonly dataType?: unknown;
147
  /** Canonical memory-layout descriptor for the uploaded bytes. */
148
  readonly format?: T;
149
  /** Number of logical rows represented by the vector. */
150
  length: number;
151
  /** Number of fixed rows or flattened vertex-list values represented by the vector. */
152
  valueLength: number;
153
  /** Number of scalar values represented by one fixed row or flattened element. */
154
  readonly stride: number;
155
  /** Byte offset of the first logical row when this vector has a single GPUData chunk. */
156
  readonly byteOffset: number;
157
  /** Bytes between adjacent fixed rows or flattened elements in {@link buffer}. */
158
  readonly byteStride: number;
159
  /** Bytes occupied by one fixed row or flattened element payload. */
160
  readonly rowByteLength: number;
161
  /** Optional GPU buffer layout described by this vector. */
162
  readonly bufferLayout?: BufferLayout;
163
  /** GPU data chunks preserved across table/batch aggregation. Each chunk owns or borrows its buffer. */
164
  readonly data: GPUData<T>[] = [];
1,193✔
165
  /** Device retained by appendable vectors so adapters can create future GPUData chunks. */
166
  readonly device?: Device;
167
  /** Buffer props retained by appendable vectors for future GPUData chunks. */
168
  readonly bufferProps?: GPUVectorDynamicBufferProps;
169
  private isAppendable = false;
1,193✔
170
  private ownsDataChunks = true;
1,193✔
171
  private readonly ownedVectors: GPUVector[] = [];
1,193✔
172
  private appendableByteLength = 0;
1,193✔
173

174
  constructor(props: GPUVectorCreateProps<T>) {
175
    switch (props.type) {
1,193!
176
      case 'buffer': {
177
        const {
178
          name,
179
          buffer,
180
          format,
181
          length,
182
          valueLength = length,
611✔
183
          byteOffset = 0,
118✔
184
          ownsBuffer = false
×
185
        } = props;
637✔
186
        const {stride, byteStride, rowByteLength} = getResolvedGPUVectorLayout(props);
637✔
187
        this.name = name;
637✔
188
        this.dataType = props.dataType;
637✔
189
        this.format = format;
637✔
190
        this.length = length;
637✔
191
        this.valueLength = valueLength;
637✔
192
        this.stride = stride;
637✔
193
        this.byteOffset = byteOffset;
637✔
194
        this.byteStride = byteStride;
637✔
195
        this.rowByteLength = rowByteLength;
637✔
196
        this.data.push(
637✔
197
          new GPUData<T>({
198
            buffer,
199
            format,
200
            length,
201
            valueLength,
202
            stride,
203
            byteOffset,
204
            byteStride,
205
            rowByteLength,
206
            ownsBuffer,
207
            dataType: props.dataType
208
          })
209
        );
210
        return;
637✔
211
      }
212

213
      case 'interleaved': {
214
        const {
215
          name,
216
          buffer,
217
          format,
218
          length,
219
          valueLength = length,
14✔
220
          byteOffset = 0,
10✔
221
          byteStride,
222
          attributes,
223
          ownsBuffer = false
×
224
        } = props;
14✔
225
        this.name = name;
14✔
226
        this.dataType = props.dataType;
14✔
227
        this.format = format;
14✔
228
        this.length = length;
14✔
229
        this.valueLength = valueLength;
14✔
230
        this.stride = byteStride;
14✔
231
        this.byteOffset = byteOffset;
14✔
232
        this.byteStride = byteStride;
14✔
233
        this.rowByteLength = byteStride;
14✔
234
        this.bufferLayout = {name, byteStride, attributes};
14✔
235
        this.data.push(
14✔
236
          new GPUData<T>({
237
            buffer,
238
            format,
239
            length,
240
            valueLength,
241
            stride: byteStride,
242
            byteOffset,
243
            byteStride,
244
            rowByteLength: byteStride,
245
            ownsBuffer,
246
            dataType: props.dataType
247
          })
248
        );
249
        return;
14✔
250
      }
251

252
      case 'data': {
253
        const format = props.format ?? getFirstGPUVectorDataFormat(props.data);
542✔
254
        const formatInfo = format ? getGPUVectorFormatInfo(format) : undefined;
542✔
255
        const {
256
          name,
257
          data,
258
          stride = data[0]?.stride ?? formatInfo?.components ?? 1,
40!
259
          valueLength = data.reduce(
542✔
260
            (totalValueLength, chunk) => totalValueLength + chunk.valueLength,
616✔
261
            0
262
          ),
263
          byteStride = data[0]?.byteStride ?? formatInfo?.byteLength,
40!
264
          rowByteLength = data[0]?.rowByteLength ?? formatInfo?.byteLength,
40!
265
          bufferLayout,
266
          ownsData = false
192✔
267
        } = props;
542✔
268
        if (byteStride === undefined || rowByteLength === undefined) {
542!
269
          throw new Error('GPUVector requires format or explicit byte layout metadata');
×
270
        }
271
        if (format) {
542✔
272
          validateGPUVectorDataFormats(data, format);
535✔
273
        }
274
        this.name = name;
542✔
275
        this.dataType = props.dataType;
542✔
276
        this.format = format;
542✔
277
        this.length = data.reduce((totalLength, chunk) => totalLength + chunk.length, 0);
616✔
278
        this.valueLength = valueLength;
542✔
279
        this.stride = stride;
542✔
280
        this.byteOffset = data.length === 1 ? data[0].byteOffset : 0;
542✔
281
        this.byteStride = byteStride;
542✔
282
        this.rowByteLength = rowByteLength;
542✔
283
        this.bufferLayout = bufferLayout;
542✔
284
        this.ownsDataChunks = ownsData;
542✔
285
        this.data.push(...data);
542✔
286
        return;
542✔
287
      }
288

289
      case 'appendable': {
290
        const {name, device, format, valueLength = 0, bufferProps} = props;
×
291
        const {stride, byteStride, rowByteLength} = getResolvedGPUVectorLayout(props);
×
292
        this.name = name;
×
293
        this.dataType = props.dataType;
×
294
        this.format = format;
×
295
        this.length = 0;
×
296
        this.valueLength = valueLength;
×
297
        this.stride = stride;
×
298
        this.byteOffset = 0;
×
299
        this.byteStride = byteStride;
×
300
        this.rowByteLength = rowByteLength;
×
301
        this.device = device;
×
302
        this.bufferProps = bufferProps;
×
303
        this.isAppendable = true;
×
304
        return;
×
305
      }
306
    }
307
  }
308

309
  /** Whether destroying this vector releases any retained GPU storage. */
310
  get ownsBuffer(): boolean {
311
    return (
4✔
312
      (this.ownsDataChunks && this.data.some(data => data.ownsBuffer)) ||
4✔
313
      this.ownedVectors.some(vector => vector.ownsBuffer)
×
314
    );
315
  }
316

317
  /** Number of rows available in retained GPUData chunks. */
318
  get capacityRows(): number | undefined {
319
    return this.isAppendable ? this.length : undefined;
×
320
  }
321

322
  /** Bytes occupied by already-appended payloads in appendable storage. */
323
  get appendedByteLength(): number {
324
    return this.appendableByteLength;
×
325
  }
326

327
  /** Adds one already-materialized GPU data chunk to this logical vector. */
328
  addData(data: GPUData<T>): this {
329
    if (this.format && data.format !== this.format) {
20!
330
      throw new Error('GPUVector.addData() requires matching formats');
×
331
    }
332
    if (data.byteStride !== this.byteStride) {
20!
333
      throw new Error('GPUVector.addData() requires matching byteStride');
×
334
    }
335
    if (data.rowByteLength !== this.rowByteLength) {
20!
336
      throw new Error('GPUVector.addData() requires matching rowByteLength');
×
337
    }
338

339
    this.data.push(data);
20✔
340
    this.length += data.length;
20✔
341
    this.valueLength += data.valueLength;
20✔
342
    return this;
20✔
343
  }
344

345
  /** Adds one adapter-created GPUData chunk to an appendable logical vector. */
346
  appendDataChunk(
347
    data: GPUData<T>,
348
    appendedByteLength = this.appendableByteLength + data.buffer.byteLength
×
349
  ): this {
350
    if (!this.isAppendable) {
×
351
      throw new Error('GPUVector.appendDataChunk() requires appendable vector storage');
×
352
    }
353
    if (this.format && data.format !== this.format) {
×
354
      throw new Error('GPUVector.appendDataChunk() requires matching formats');
×
355
    }
356
    if (data.byteStride !== this.byteStride || data.rowByteLength !== this.rowByteLength) {
×
357
      throw new Error('GPUVector.appendDataChunk() requires matching byte layout metadata');
×
358
    }
359
    this.data.push(data);
×
360
    this.length += data.length;
×
361
    this.valueLength += data.valueLength;
×
362
    this.appendableByteLength = appendedByteLength;
×
363
    return this;
×
364
  }
365

366
  /** Clears appendable logical rows and releases appended GPUData buffers. */
367
  resetLastBatch(): this {
368
    if (!this.isAppendable) {
×
369
      throw new Error('GPUVector.resetLastBatch() requires appendable vector storage');
×
370
    }
371
    for (const data of this.data.splice(0)) {
×
372
      data.destroy();
×
373
    }
374
    this.length = 0;
×
375
    this.valueLength = 0;
×
376
    this.appendableByteLength = 0;
×
377
    return this;
×
378
  }
379

380
  /** @internal Retains detached batch-local vector ownership under this aggregate vector. */
381
  retainOwnedVectors(vectors: GPUVector[]): this {
382
    this.ownedVectors.push(...vectors);
×
383
    return this;
×
384
  }
385

386
  /** Transfers same-buffer ownership to another vector view. */
387
  transferBufferOwnership(target: GPUVector): void {
388
    const sourceData = this.data[0];
1✔
389
    const targetData = target.data[0];
1✔
390
    if (!sourceData || !targetData || sourceData.buffer !== targetData.buffer) {
1!
391
      throw new Error('GPUVector ownership can only be transferred to the same buffer');
×
392
    }
393
    sourceData.transferBufferOwnership(targetData);
1✔
394
  }
395

396
  /** Releases owned GPU data and detached ownership handles. */
397
  destroy(): void {
398
    if (this.ownsDataChunks) {
448✔
399
      for (const data of this.data) {
428✔
400
        data.destroy();
439✔
401
      }
402
    }
403
    for (const vector of this.ownedVectors.splice(0)) {
448✔
404
      vector.destroy();
×
405
    }
406
  }
407
}
408

409
function getResolvedGPUVectorLayout<T extends GPUVectorFormat>(props: {
410
  format?: T;
411
  stride?: number;
412
  byteStride?: number;
413
  rowByteLength?: number;
414
}): {stride: number; byteStride: number; rowByteLength: number} {
415
  const formatInfo = props.format ? getGPUVectorFormatInfo(props.format) : undefined;
637✔
416
  const rowByteLength = props.rowByteLength ?? props.byteStride ?? formatInfo?.byteLength;
637✔
417
  if (rowByteLength === undefined) {
637!
418
    throw new Error('GPUVector requires format or explicit rowByteLength');
×
419
  }
420
  return {
637✔
421
    stride: props.stride ?? formatInfo?.components ?? 1,
680✔
422
    byteStride: props.byteStride ?? rowByteLength,
671✔
423
    rowByteLength
424
  };
425
}
426

427
function getFirstGPUVectorDataFormat<T extends GPUVectorFormat>(data: GPUData<T>[]): T | undefined {
428
  return data[0]?.format as T | undefined;
8✔
429
}
430

431
function validateGPUVectorDataFormats<T extends GPUVectorFormat>(
432
  data: GPUData<T>[],
433
  format: T
434
): void {
435
  const mismatchedChunk = data.find(chunk => chunk.format !== format);
609✔
436
  if (mismatchedChunk) {
535!
UNCOV
437
    throw new Error('GPUVector data chunks must share the declared format');
×
438
  }
439
}
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