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

visgl / luma.gl / 28178532665

25 Jun 2026 02:45PM UTC coverage: 70.237% (-0.2%) from 70.408%
28178532665

push

github

web-flow
chore(tables) Clean up GPU table inputs and constructors (#2693)

9607 of 15509 branches covered (61.94%)

Branch coverage included in aggregate %.

81 of 157 new or added lines in 9 files covered. (51.59%)

9 existing lines in 2 files now uncovered.

19235 of 25555 relevant lines covered (75.27%)

4233.77 hits per line

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

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

5
import {Buffer, type BufferLayout, type VertexFormat} from '@luma.gl/core';
6
import {DynamicBuffer} from '@luma.gl/engine';
7
import type {GPUField, GPUSchema, GPUTypeMap} from './gpu-schema';
8
import {GPUData} from './gpu-data';
9
import {GPUVector} from './gpu-vector';
10
import {GPURecordBatch, type GPURecordBatchSourceInfo} from './gpu-record-batch';
11
import {GPU_TABLE_INDEX_COLUMN_NAME, isGPUTableIndexColumnName} from './gpu-schema';
12
import {
13
  getGPUVectorElementFormat,
14
  isValueListGPUVectorFormat,
15
  isVertexListGPUVectorFormat
16
} from './gpu-vector-format';
17

18
type GPUVectorMap<T extends GPUTypeMap = GPUTypeMap> = {
19
  [Name in keyof T & string]: GPUVector<T[Name]>;
20
};
21

22
/** Options for constructing a GPU table from existing GPU vectors. */
23
export type GPUTableFromVectorsProps<T extends GPUTypeMap = GPUTypeMap> = {
24
  /** GPU vectors keyed by name, or a list of named GPU vectors. */
25
  vectors: GPUVectorMap<T> | Record<string, GPUVector> | GPUVector[];
26
  /** Optional table-level schema metadata. */
27
  metadata?: Map<string, string>;
28
  /** Optional source-row identity forwarded when vectors contain one GPUData chunk. */
29
  sourceInfo?: GPURecordBatchSourceInfo;
30
  /** Number of null rows forwarded when vectors contain one GPUData chunk. */
31
  nullCount?: number;
32
};
33

34
/** Options for constructing a GPU table from already-built record batches. */
35
export type GPUTableFromBatchesProps<T extends GPUTypeMap = GPUTypeMap> = {
36
  /** GPU batches preserved by the table. */
37
  batches: GPURecordBatch<T>[];
38
};
39

40
/** Options for constructing one typed table with no GPU record batches. */
41
export type GPUTableFromSchemaProps<T extends GPUTypeMap = GPUTypeMap> = {
42
  /** Selected schema retained by the empty table. */
43
  schema: GPUSchema<T>;
44
  /** Optional layout retained until the first batch is added. */
45
  bufferLayout?: BufferLayout[];
46
};
47

48
/** Generic GPU table construction props. */
49
export type GPUTableProps<T extends GPUTypeMap = GPUTypeMap> =
50
  | GPUTableFromVectorsProps<T>
51
  | GPUTableFromBatchesProps<T>
52
  | GPUTableFromSchemaProps<T>;
53

54
/** Options for replacing preserved GPU batches with larger packed batches. */
55
export type GPUTablePackBatchesOptions = {
56
  /** Greedily merge adjacent batches until each emitted batch reaches this row count. */
57
  minBatchSize?: number;
58
};
59

60
/** Half-open batch index range used by {@link GPUTable.detachBatches}. */
61
export type GPUTableDetachBatchesOptions = {
62
  /** First batch index to detach. Defaults to `0`. */
63
  first?: number;
64
  /** Batch index after the last detached batch. Defaults to `batches.length`. */
65
  last?: number;
66
};
67

68
/** GPU memory and schema metadata for one logical table. */
69
export class GPUTable<T extends GPUTypeMap = GPUTypeMap> {
70
  /** GPU-facing schema for the selected columns. */
71
  schema: GPUSchema<T>;
72
  /** Number of logical rows in the table. */
73
  numRows: number;
74
  /** Number of selected GPU columns in {@link schema}. */
75
  numCols: number;
76
  /** Number of null rows retained in table metadata. */
77
  nullCount: number;
78
  /** GPU vectors keyed by table/shader column name. */
79
  readonly gpuVectors: Record<string, GPUVector> = {};
78✔
80
  /** Preserved batch-local GPU storage. */
81
  readonly batches: GPURecordBatch[] = [];
78✔
82

83
  /** Buffer layout shared by preserved record batches. */
84
  readonly bufferLayout: BufferLayout[] = [];
78✔
85

86
  /** Creates one logical GPU table from a schema, batches, or batch-aligned vectors. */
87
  constructor(props: GPUTableProps<T>) {
88
    if ('batches' in props) {
78✔
89
      const firstBatch = props.batches[0];
65✔
90
      if (!firstBatch) {
65!
NEW
91
        throw new Error('GPUTable batches constructor requires at least one GPURecordBatch');
×
92
      }
93
      assertCompatibleGPURecordBatches(props.batches);
65✔
94
      this.schema = firstBatch.schema as GPUSchema<T>;
65✔
95
      this.numCols = firstBatch.schema.fields.length;
65✔
96
      this.batches.push(...(props.batches as GPURecordBatch[]));
65✔
97
      this.bufferLayout.push(...firstBatch.bufferLayout);
65✔
98
      this.numRows = props.batches.reduce((numRows, batch) => numRows + batch.numRows, 0);
81✔
99
      this.nullCount = props.batches.reduce((nullCount, batch) => nullCount + batch.nullCount, 0);
81✔
100
      this.rebuildAggregateVectors();
65✔
101
      return;
65✔
102
    }
103

104
    if ('schema' in props) {
13!
UNCOV
105
      this.schema = props.schema;
×
UNCOV
106
      this.numCols = props.schema.fields.length;
×
NEW
107
      this.numRows = 0;
×
NEW
108
      this.nullCount = 0;
×
NEW
109
      this.bufferLayout.push(...(props.bufferLayout ?? []));
×
UNCOV
110
      return;
×
111
    }
112

113
    const {vectors, metadata, sourceInfo, nullCount = 0} = props;
13✔
114
    const gpuVectors = normalizeGPUVectors(vectors);
13✔
115
    const hasGPUData = Object.values(gpuVectors).some(vector => vector.data.length > 0);
13✔
116
    const bufferLayout = getGPUVectorBufferLayout(gpuVectors, !hasGPUData);
13✔
117
    const fields = getGPUVectorFields(gpuVectors);
13✔
118
    const batches = createGPURecordBatchesFromVectors(
13✔
119
      gpuVectors,
120
      bufferLayout,
121
      fields,
122
      metadata,
123
      sourceInfo,
124
      nullCount
125
    );
126

127
    this.schema = {fields, metadata: metadata ?? new Map()};
13✔
128
    this.numCols = fields.length;
13✔
129
    this.numRows = batches.reduce((numRows, batch) => numRows + batch.numRows, 0);
13✔
130
    this.nullCount = batches.reduce((totalNullCount, batch) => totalNullCount + batch.nullCount, 0);
13✔
131
    this.bufferLayout.push(...bufferLayout);
13✔
132
    this.batches.push(...batches);
13✔
133
    this.rebuildAggregateVectors();
13✔
134
  }
135

136
  /** Replaces preserved GPU batches with fewer packed batches. */
137
  packBatches(options: GPUTablePackBatchesOptions = {}): this {
6✔
138
    if (this.batches.length <= 1) {
7!
139
      return this;
×
140
    }
141
    if (this.batches.some(batch => batch.gpuData[GPU_TABLE_INDEX_COLUMN_NAME])) {
14✔
142
      throw new Error('GPUTable.packBatches() does not support indexed tables');
1✔
143
    }
144

145
    const batchGroups = createGPUPackGroups(this.batches, options.minBatchSize);
6✔
146
    const nextBatches: GPURecordBatch[] = [];
6✔
147
    const supersededBatches: GPURecordBatch[] = [];
6✔
148

149
    for (const batchGroup of batchGroups) {
6✔
150
      if (batchGroup.length === 1) {
7✔
151
        nextBatches.push(batchGroup[0]);
1✔
152
        continue;
1✔
153
      }
154
      nextBatches.push(
6✔
155
        createPackedGPURecordBatch(batchGroup, this.bufferLayout, this.schema as GPUSchema)
156
      );
157
      supersededBatches.push(...batchGroup);
5✔
158
    }
159

160
    if (supersededBatches.length === 0) {
5!
161
      return this;
×
162
    }
163

164
    this.batches.splice(0, this.batches.length, ...nextBatches);
5✔
165
    this.refreshFromBatches();
5✔
166
    for (const batch of supersededBatches) {
5✔
167
      batch.destroy();
10✔
168
    }
169
    return this;
5✔
170
  }
171

172
  /** Adds one already-created GPU record batch to this table. */
173
  addBatch(batch: GPURecordBatch): this {
174
    if (this.batches.length === 0) {
10!
NEW
175
      assertMatchingGPURecordBatchSchema(this.schema, batch.schema, 'GPUTable.addBatch()');
×
NEW
176
      if (this.bufferLayout.length === 0) {
×
NEW
177
        this.bufferLayout.push(...batch.bufferLayout);
×
NEW
178
      } else if (!deepEqualBufferLayouts(this.bufferLayout, batch.bufferLayout)) {
×
NEW
179
        throw new Error('GPUTable.addBatch() requires matching buffer layouts');
×
180
      }
NEW
181
      this.batches.push(batch);
×
NEW
182
      return this.refreshFromBatches();
×
183
    }
184
    assertCompatibleGPURecordBatch(this, batch);
10✔
185
    this.batches.push(batch);
10✔
186
    return this.refreshFromBatches();
10✔
187
  }
188

189
  /** Recomputes aggregate row counts and vector views from preserved batches. */
190
  refreshFromBatches(): this {
191
    this.numRows = this.batches.reduce((numRows, batch) => numRows + batch.numRows, 0);
29✔
192
    this.nullCount = this.batches.reduce((nullCount, batch) => nullCount + batch.nullCount, 0);
29✔
193
    if (this.batches.length <= 1 || !this.trySynchronizeAggregateVectors()) {
17✔
194
      this.rebuildAggregateVectors();
7✔
195
    }
196
    return this;
17✔
197
  }
198

199
  /** Keeps only the requested columns and destroys the dropped batch-local data. */
200
  select(...columnNames: string[]): this {
201
    const selectedColumnNames = normalizeSelectedColumnNames(this, columnNames);
2✔
202
    const selectedColumnSet = new Set(selectedColumnNames);
2✔
203

204
    for (const batch of this.batches) {
2✔
205
      const droppedData = Object.entries(batch.gpuData)
2✔
206
        .filter(([name]) => !selectedColumnSet.has(name))
4✔
207
        .map(([, data]) => data);
2✔
208
      for (const data of droppedData) {
2✔
209
        data.destroy();
2✔
210
      }
211
      rebuildGPURecordBatchColumns(batch, selectedColumnNames);
2✔
212
    }
213

214
    rebuildGPUTableColumns(this, selectedColumnNames);
2✔
215
    this.rebuildAggregateVectors();
2✔
216
    return this;
2✔
217
  }
218

219
  /** Removes one column and returns an aggregate GPU vector that owns detached batch data. */
220
  detachVector(columnName: string): GPUVector {
221
    assertGPUTableColumn(this, columnName);
1✔
222
    const detachedData = this.batches.map(batch => batch.gpuData[columnName]);
2✔
223
    const firstData = detachedData[0];
1✔
224
    if (!firstData) {
1!
225
      throw new Error(`GPUTable.detachVector() column "${columnName}" has no GPU data`);
×
226
    }
227
    const detachedVector = new GPUVector({
1✔
228
      type: 'data',
229
      name: columnName,
230
      dataType: firstData.dataType,
231
      format: firstData.format,
232
      data: detachedData,
233
      stride: firstData.stride,
234
      byteStride: firstData.byteStride,
235
      rowByteLength: firstData.rowByteLength,
236
      bufferLayout: getColumnBufferLayout(this.bufferLayout, columnName),
237
      ownsData: true
238
    });
239
    const remainingColumnNames = this.schema.fields
1✔
240
      .map(field => field.name)
2✔
241
      .filter(name => name !== columnName);
2✔
242
    for (const batch of this.batches) {
1✔
243
      rebuildGPURecordBatchColumns(batch, remainingColumnNames);
2✔
244
    }
245
    rebuildGPUTableColumns(this, remainingColumnNames);
1✔
246
    this.rebuildAggregateVectors();
1✔
247
    return detachedVector;
1✔
248
  }
249

250
  /** Removes and returns a half-open range of GPU record batches. */
251
  detachBatches(options: GPUTableDetachBatchesOptions = {}): GPURecordBatch[] {
×
252
    const first = options.first ?? 0;
2!
253
    const last = options.last ?? this.batches.length;
2✔
254
    if (
2!
255
      !Number.isInteger(first) ||
10✔
256
      !Number.isInteger(last) ||
257
      first < 0 ||
258
      last < first ||
259
      last > this.batches.length
260
    ) {
261
      throw new Error('GPUTable.detachBatches() requires a valid half-open batch range');
×
262
    }
263

264
    const detachedBatches = this.batches.splice(first, last - first);
2✔
265
    if (detachedBatches.length === 0) {
2!
266
      return detachedBatches;
×
267
    }
268
    this.refreshFromBatches();
2✔
269
    return detachedBatches;
2✔
270
  }
271

272
  /** Destroys retained GPU batches and follows their vector-level ownership graphs. */
273
  destroy(): void {
274
    for (const batch of this.batches) {
55✔
275
      batch.destroy();
70✔
276
    }
277
  }
278

279
  private rebuildAggregateVectors(): void {
280
    for (const name of Object.keys(this.gpuVectors)) {
88✔
281
      delete this.gpuVectors[name];
14✔
282
    }
283
    const firstBatch = this.batches[0];
88✔
284
    if (!firstBatch) {
88!
285
      return;
×
286
    }
287
    for (const columnName of Object.keys(firstBatch.gpuData)) {
88✔
288
      const batchData = getBatchColumnData(this.batches, columnName);
136✔
289
      this.gpuVectors[columnName] = createAggregateGPUVector(
136✔
290
        columnName,
291
        batchData,
292
        getColumnBufferLayout(this.bufferLayout, columnName)
293
      );
294
    }
295
  }
296

297
  private trySynchronizeAggregateVectors(): boolean {
298
    const firstBatch = this.batches[0];
12✔
299
    if (!firstBatch) {
12!
300
      return false;
×
301
    }
302

303
    for (const columnName of Object.keys(firstBatch.gpuData)) {
12✔
304
      const aggregateVector = this.gpuVectors[columnName];
27✔
305
      const batchData = getBatchColumnData(this.batches, columnName);
27✔
306
      if (!aggregateVector || !canSynchronizeAggregateVector(aggregateVector, batchData)) {
27✔
307
        return false;
2✔
308
      }
309
    }
310

311
    for (const columnName of Object.keys(firstBatch.gpuData)) {
10✔
312
      const aggregateVector = this.gpuVectors[columnName];
25✔
313
      const batchData = getBatchColumnData(this.batches, columnName);
25✔
314
      for (const data of batchData.slice(aggregateVector.data.length)) {
25✔
315
        aggregateVector.addData(data);
25✔
316
      }
317
    }
318

319
    return true;
10✔
320
  }
321
}
322

323
function canSynchronizeAggregateVector(aggregateVector: GPUVector, batchData: GPUData[]): boolean {
324
  const firstData = batchData[0];
27✔
325
  if (!firstData) {
27!
326
    return false;
×
327
  }
328
  if (
27!
329
    aggregateVector.format !== firstData.format ||
108✔
330
    aggregateVector.stride !== firstData.stride ||
331
    aggregateVector.byteStride !== firstData.byteStride ||
332
    aggregateVector.rowByteLength !== firstData.rowByteLength
333
  ) {
334
    return false;
×
335
  }
336
  if (aggregateVector.data.length > batchData.length) {
27✔
337
    return false;
2✔
338
  }
339
  return aggregateVector.data.every((data, index) => data === batchData[index]);
25✔
340
}
341

342
function getBatchColumnData(batches: GPURecordBatch[], columnName: string): GPUData[] {
343
  return batches.map(batch => {
188✔
344
    const data = batch.gpuData[columnName];
271✔
345
    if (!data) {
271!
346
      throw new Error(`GPUTable batch is missing GPUData "${columnName}"`);
×
347
    }
348
    return data;
271✔
349
  });
350
}
351

352
function createAggregateGPUVector(
353
  columnName: string,
354
  data: GPUData[],
355
  bufferLayout?: BufferLayout
356
): GPUVector {
357
  const firstData = data[0];
136✔
358
  if (!firstData) {
136!
359
    throw new Error(`GPUTable aggregate vector "${columnName}" requires GPUData`);
×
360
  }
361
  return new GPUVector({
136✔
362
    type: 'data',
363
    name: columnName,
364
    dataType: firstData.dataType,
365
    format: firstData.format,
366
    data,
367
    stride: firstData.stride,
368
    byteStride: firstData.byteStride,
369
    rowByteLength: firstData.rowByteLength,
370
    bufferLayout
371
  });
372
}
373

374
function createGPUPackGroups(batches: GPURecordBatch[], minBatchSize?: number): GPURecordBatch[][] {
375
  if (minBatchSize === undefined) {
6✔
376
    return [batches];
5✔
377
  }
378
  if (!Number.isFinite(minBatchSize) || minBatchSize <= 0) {
1!
379
    throw new Error('GPUTable.packBatches() minBatchSize must be a positive number');
×
380
  }
381

382
  const batchGroups: GPURecordBatch[][] = [];
1✔
383
  let currentGroup: GPURecordBatch[] = [];
1✔
384
  let currentRowCount = 0;
1✔
385

386
  for (const batch of batches) {
1✔
387
    currentGroup.push(batch);
3✔
388
    currentRowCount += batch.numRows;
3✔
389
    if (currentRowCount >= minBatchSize) {
3✔
390
      batchGroups.push(currentGroup);
1✔
391
      currentGroup = [];
1✔
392
      currentRowCount = 0;
1✔
393
    }
394
  }
395

396
  if (currentGroup.length > 0) {
1!
397
    batchGroups.push(currentGroup);
1✔
398
  }
399
  return batchGroups;
1✔
400
}
401

402
function createPackedGPURecordBatch(
403
  batchGroup: GPURecordBatch[],
404
  bufferLayout: BufferLayout[],
405
  schema: GPUSchema
406
): GPURecordBatch {
407
  const firstBatch = batchGroup[0];
6✔
408
  const device = getGPURecordBatchDevice(firstBatch);
6✔
409
  const commandEncoder = device.createCommandEncoder();
6✔
410
  const packedData: Record<string, GPUData> = {};
6✔
411

412
  for (const columnName of Object.keys(firstBatch.gpuData)) {
6✔
413
    const sourceData = batchGroup.map(batch => batch.gpuData[columnName]);
20✔
414
    const firstData = sourceData[0];
10✔
415
    if (!firstData) {
10!
416
      throw new Error(`GPUTable batch is missing GPUData "${columnName}"`);
×
417
    }
418
    if (
10✔
419
      sourceData.some(
420
        data =>
421
          data.format &&
19✔
422
          (isValueListGPUVectorFormat(data.format) || isVertexListGPUVectorFormat(data.format))
423
      )
424
    ) {
425
      throw new Error(
1✔
426
        `GPUTable.packBatches() does not support variable-length GPUData "${columnName}"`
427
      );
428
    }
429

430
    const byteLength = sourceData.reduce(
9✔
431
      (totalByteLength, data) => totalByteLength + data.length * data.byteStride,
18✔
432
      0
433
    );
434
    const buffer = device.createBuffer({
9✔
435
      usage: Buffer.VERTEX | Buffer.STORAGE | Buffer.COPY_DST | Buffer.COPY_SRC,
436
      byteLength
437
    });
438
    let destinationOffset = 0;
9✔
439
    for (const data of sourceData) {
9✔
440
      const copyByteLength = getGPUDataCopyByteLength(data);
18✔
441
      if (copyByteLength === 0) {
18!
442
        continue;
×
443
      }
444
      commandEncoder.copyBufferToBuffer({
18✔
445
        sourceBuffer: getGPUDataConcreteBuffer(data),
446
        sourceOffset: data.byteOffset,
447
        destinationBuffer: buffer,
448
        destinationOffset,
449
        size: copyByteLength
450
      });
451
      destinationOffset += data.length * data.byteStride;
18✔
452
    }
453

454
    packedData[columnName] = new GPUData({
9✔
455
      buffer,
456
      dataType: firstData.dataType,
457
      format: firstData.format,
458
      length: sourceData.reduce((length, data) => length + data.length, 0),
18✔
459
      valueLength: sourceData.reduce((length, data) => length + data.valueLength, 0),
18✔
460
      stride: firstData.stride,
461
      byteStride: firstData.byteStride,
462
      rowByteLength: firstData.rowByteLength,
463
      ownsBuffer: true
464
    });
465
  }
466

467
  device.submit(commandEncoder.finish());
5✔
468
  return new GPURecordBatch({
5✔
469
    gpuData: packedData,
470
    bufferLayout,
471
    fields: schema.fields,
472
    metadata: new Map(schema.metadata),
473
    sourceInfo: getPackedGPURecordBatchSourceInfo(batchGroup),
474
    nullCount: batchGroup.reduce((nullCount, batch) => nullCount + batch.nullCount, 0)
10✔
475
  });
476
}
477

478
function getPackedGPURecordBatchSourceInfo(
479
  batchGroup: GPURecordBatch[]
480
): GPURecordBatchSourceInfo | undefined {
481
  const firstSourceInfo = batchGroup[0]?.sourceInfo;
5✔
482
  if (!firstSourceInfo) {
5!
483
    return undefined;
×
484
  }
485

486
  let sourceRowCount = firstSourceInfo.sourceRowCount;
5✔
487
  let nextSourceRowIndex = firstSourceInfo.sourceRowIndexOffset + firstSourceInfo.sourceRowCount;
5✔
488
  for (const batch of batchGroup.slice(1)) {
5✔
489
    const sourceInfo = batch.sourceInfo;
5✔
490
    if (
5✔
491
      !sourceInfo ||
11✔
492
      sourceInfo.sourceBatchIndex !== firstSourceInfo.sourceBatchIndex ||
493
      sourceInfo.sourceRowIndexOffset !== nextSourceRowIndex
494
    ) {
495
      return undefined;
4✔
496
    }
497
    sourceRowCount += sourceInfo.sourceRowCount;
1✔
498
    nextSourceRowIndex += sourceInfo.sourceRowCount;
1✔
499
  }
500

501
  return {
1✔
502
    sourceBatchIndex: firstSourceInfo.sourceBatchIndex,
503
    sourceRowIndexOffset: firstSourceInfo.sourceRowIndexOffset,
504
    sourceRowCount
505
  };
506
}
507

508
function getGPURecordBatchDevice<T extends GPUTypeMap>(batch: GPURecordBatch<T>) {
509
  const firstData = Object.values(batch.gpuData)[0];
6✔
510
  if (!firstData) {
6!
511
    throw new Error('GPUTable cannot pack an empty GPU record batch');
×
512
  }
513
  return firstData.buffer.device;
6✔
514
}
515

516
function getGPUDataCopyByteLength(data: GPUData): number {
517
  if (data.length === 0) {
18!
518
    return 0;
×
519
  }
520
  return (data.length - 1) * data.byteStride + data.rowByteLength;
18✔
521
}
522

523
function getGPUDataConcreteBuffer(data: GPUData): Buffer {
524
  return data.buffer instanceof DynamicBuffer ? data.buffer.buffer : data.buffer;
18✔
525
}
526

527
function assertCompatibleGPURecordBatch<T extends GPUTypeMap>(
528
  table: GPUTable<T>,
529
  batch: GPURecordBatch<T>
530
): void {
531
  if (!deepEqualBufferLayouts(table.bufferLayout, batch.bufferLayout)) {
10!
532
    throw new Error('GPUTable.addBatch() requires matching buffer layouts');
×
533
  }
534
  assertMatchingGPURecordBatchSchema(table.schema, batch.schema, 'GPUTable.addBatch()');
10✔
535
}
536

537
function assertCompatibleGPURecordBatches<T extends GPUTypeMap>(
538
  batches: GPURecordBatch<T>[]
539
): void {
540
  const firstBatch = batches[0];
65✔
541
  if (!firstBatch) {
65!
NEW
542
    return;
×
543
  }
544
  for (const batch of batches.slice(1)) {
65✔
545
    if (!deepEqualBufferLayouts(firstBatch.bufferLayout, batch.bufferLayout)) {
16!
NEW
546
      throw new Error('GPUTable batches constructor requires matching buffer layouts');
×
547
    }
548
    assertMatchingGPURecordBatchSchema(
16✔
549
      firstBatch.schema,
550
      batch.schema,
551
      'GPUTable batches constructor'
552
    );
553
  }
554
}
555

556
function assertMatchingGPURecordBatchSchema<T extends GPUTypeMap>(
557
  expectedSchema: GPUSchema<T>,
558
  candidateSchema: GPUSchema<T>,
559
  ownerName: string
560
): void {
561
  if (expectedSchema.fields.length !== candidateSchema.fields.length) {
26!
NEW
562
    throw new Error(ownerName + ' requires matching selected schema fields');
×
563
  }
564
  for (let fieldIndex = 0; fieldIndex < expectedSchema.fields.length; fieldIndex++) {
26✔
565
    const tableField = expectedSchema.fields[fieldIndex];
51✔
566
    const batchField = candidateSchema.fields[fieldIndex];
51✔
567
    if (
51!
568
      !batchField ||
153✔
569
      tableField.name !== batchField.name ||
570
      tableField.format !== batchField.format
571
    ) {
NEW
572
      throw new Error(ownerName + ' requires matching selected schema fields');
×
573
    }
574
  }
575
}
576

577
function deepEqualBufferLayouts(
578
  expectedBufferLayout: BufferLayout[],
579
  candidateBufferLayout: BufferLayout[]
580
): boolean {
581
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
26✔
582
}
583

584
function normalizeSelectedColumnNames<T extends GPUTypeMap>(
585
  table: GPUTable<T>,
586
  columnNames: string[]
587
): string[] {
588
  const knownColumnNames = new Set(table.schema.fields.map(field => field.name));
4✔
589
  const selectedColumnNames = Array.from(new Set(columnNames));
2✔
590
  for (const columnName of selectedColumnNames) {
2✔
591
    if (!knownColumnNames.has(columnName)) {
2!
592
      throw new Error(`GPUTable column "${columnName}" does not exist`);
×
593
    }
594
  }
595
  return selectedColumnNames;
2✔
596
}
597

598
function assertGPUTableColumn<T extends GPUTypeMap>(table: GPUTable<T>, columnName: string): void {
599
  if (!table.schema.fields.some(field => field.name === columnName)) {
2!
600
    throw new Error(`GPUTable column "${columnName}" does not exist`);
×
601
  }
602
}
603

604
function rebuildGPUTableColumns<T extends GPUTypeMap>(
605
  table: GPUTable<T>,
606
  columnNames: string[]
607
): void {
608
  const selectedColumnSet = new Set(columnNames);
3✔
609
  const selectedLayouts = columnNames
3✔
610
    .map(columnName => table.bufferLayout.find(layout => layout.name === columnName))
3✔
611
    .filter((layout): layout is BufferLayout => Boolean(layout));
3✔
612
  const selectedFields = columnNames
3✔
613
    .map(columnName => table.schema.fields.find(field => field.name === columnName))
3✔
614
    .filter((field): field is GPUField => Boolean(field));
3✔
615

616
  table.bufferLayout.splice(0, table.bufferLayout.length, ...selectedLayouts);
3✔
617
  table.schema = {
3✔
618
    fields: selectedFields,
619
    metadata: new Map(table.schema.metadata)
620
  };
621
  table.numCols = selectedFields.length;
3✔
622

623
  for (const name of Object.keys(table.gpuVectors)) {
3✔
624
    if (!selectedColumnSet.has(name)) {
6✔
625
      delete table.gpuVectors[name];
3✔
626
    }
627
  }
628
}
629

630
function rebuildGPURecordBatchColumns<T extends GPUTypeMap>(
631
  batch: GPURecordBatch<T>,
632
  columnNames: string[]
633
): void {
634
  const selectedColumnSet = new Set(columnNames);
4✔
635
  const selectedLayouts = columnNames
4✔
636
    .map(columnName => batch.bufferLayout.find(layout => layout.name === columnName))
4✔
637
    .filter((layout): layout is BufferLayout => Boolean(layout));
4✔
638
  const selectedFields = columnNames
4✔
639
    .map(columnName => batch.schema.fields.find(field => field.name === columnName))
4✔
640
    .filter((field): field is GPUField => Boolean(field));
4✔
641

642
  for (const name of Object.keys(batch.gpuData)) {
4✔
643
    if (!selectedColumnSet.has(name)) {
8✔
644
      delete batch.gpuData[name];
4✔
645
    }
646
  }
647
  batch.bufferLayout.splice(0, batch.bufferLayout.length, ...selectedLayouts);
4✔
648
  batch.schema = {
4✔
649
    fields: selectedFields,
650
    metadata: new Map(batch.schema.metadata)
651
  };
652
  batch.numCols = selectedFields.length;
4✔
653
}
654

655
function normalizeGPUVectors(
656
  vectors: Record<string, GPUVector> | GPUVector[]
657
): Record<string, GPUVector> {
658
  return Array.isArray(vectors)
13✔
659
    ? Object.fromEntries(vectors.map(vector => [vector.name, vector]))
1✔
660
    : vectors;
661
}
662

663
function getGPUVectorBufferLayout(
664
  gpuVectors: Record<string, GPUVector>,
665
  allowVariableLengthWithoutLayout = false
×
666
): BufferLayout[] {
667
  return Object.values(gpuVectors).flatMap(vector =>
13✔
668
    isGPUTableIndexColumnName(vector.name)
17✔
669
      ? []
670
      : synthesizeGPUVectorBufferLayout(vector, allowVariableLengthWithoutLayout)
671
  );
672
}
673

674
function synthesizeGPUVectorBufferLayout(
675
  vector: GPUVector,
676
  allowVariableLengthWithoutLayout: boolean
677
): BufferLayout[] {
678
  if (vector.bufferLayout) {
14✔
679
    return [vector.bufferLayout];
2✔
680
  }
681
  if (!vector.format) {
12!
NEW
682
    throw new Error(
×
683
      'GPUTable cannot synthesize a buffer layout for vector "' + vector.name + '" without a format'
684
    );
685
  }
686
  if (isVertexListGPUVectorFormat(vector.format)) {
12!
NEW
687
    if (allowVariableLengthWithoutLayout) {
×
NEW
688
      return [];
×
689
    }
NEW
690
    throw new Error(
×
691
      'GPUTable cannot synthesize a generic buffer layout for vertex-list vector "' +
692
        vector.name +
693
        '"'
694
    );
695
  }
696
  if (isValueListGPUVectorFormat(vector.format)) {
12!
NEW
697
    if (allowVariableLengthWithoutLayout) {
×
NEW
698
      return [];
×
699
    }
NEW
700
    throw new Error(
×
701
      'GPUTable cannot synthesize a generic buffer layout for value-list vector "' +
702
        vector.name +
703
        '"'
704
    );
705
  }
706
  return [
12✔
707
    {
708
      name: vector.name,
709
      byteStride: vector.byteStride,
710
      format: getGPUVectorElementFormat(vector.format) as VertexFormat
711
    }
712
  ];
713
}
714

715
function getGPUVectorFields(gpuVectors: Record<string, GPUVector>): GPUField[] {
716
  return Object.values(gpuVectors).map(vector => {
13✔
717
    if (!vector.format) {
17✔
718
      if (vector.bufferLayout) {
2!
719
        return {name: vector.name, nullable: false, metadata: new Map()};
2✔
720
      }
NEW
721
      throw new Error(
×
722
        'GPUTable cannot synthesize a schema field for vector "' +
723
          vector.name +
724
          '" without a format'
725
      );
726
    }
727
    return {
15✔
728
      name: vector.name,
729
      format: vector.format,
730
      nullable: false,
731
      metadata: new Map()
732
    };
733
  });
734
}
735

736
function createGPURecordBatchesFromVectors(
737
  gpuVectors: Record<string, GPUVector>,
738
  bufferLayout: BufferLayout[],
739
  fields: GPUField[],
740
  metadata: Map<string, string> | undefined,
741
  sourceInfo: GPURecordBatchSourceInfo | undefined,
742
  nullCount: number
743
): GPURecordBatch[] {
744
  const vectors = Object.values(gpuVectors);
13✔
745
  const batchCount = vectors[0]?.data.length ?? 0;
13!
746
  for (const vector of vectors) {
13✔
747
    validateGPUVectorChunks(vector);
17✔
748
    if (vector.data.length !== batchCount) {
17!
NEW
749
      throw new Error('GPUTable vectors constructor requires matching GPUData chunk counts');
×
750
    }
751
  }
752
  if (batchCount !== 1 && (sourceInfo || nullCount > 0)) {
13!
NEW
753
    throw new Error(
×
754
      'GPUTable vectors constructor supports sourceInfo and nullCount only with one GPUData chunk'
755
    );
756
  }
757

758
  const batches: GPURecordBatch[] = [];
13✔
759
  for (let batchIndex = 0; batchIndex < batchCount; batchIndex++) {
13✔
760
    const gpuData: Record<string, GPUData> = {};
13✔
761
    let numRows: number | undefined;
762
    for (const [columnName, vector] of Object.entries(gpuVectors)) {
13✔
763
      const data = vector.data[batchIndex];
17✔
764
      if (!data) {
17!
NEW
765
        throw new Error(
×
766
          'GPUTable vectors constructor is missing GPUData chunk for "' + columnName + '"'
767
        );
768
      }
769
      if (numRows === undefined) {
17✔
770
        numRows = data.length;
13✔
771
      } else if (data.length !== numRows) {
4!
NEW
772
        throw new Error(
×
773
          'GPUTable vectors constructor requires matching row counts in batch ' + batchIndex
774
        );
775
      }
776
      gpuData[columnName] = data;
17✔
777
    }
778
    batches.push(
13✔
779
      new GPURecordBatch({
780
        gpuData,
781
        bufferLayout,
782
        fields,
783
        metadata,
784
        ...(sourceInfo ? {sourceInfo} : {}),
13✔
785
        nullCount
786
      })
787
    );
788
  }
789
  return batches;
13✔
790
}
791

792
function validateGPUVectorChunks(vector: GPUVector): void {
793
  for (const data of vector.data) {
17✔
794
    if (
17!
795
      data.format !== vector.format ||
68✔
796
      data.stride !== vector.stride ||
797
      data.byteStride !== vector.byteStride ||
798
      data.rowByteLength !== vector.rowByteLength
799
    ) {
UNCOV
800
      throw new Error(
×
801
        'GPUTable vectors constructor requires compatible GPUData chunks for "' + vector.name + '"'
802
      );
803
    }
804
  }
805
}
806

807
function getColumnBufferLayout(
808
  bufferLayout: BufferLayout[],
809
  columnName: string
810
): BufferLayout | undefined {
811
  const layout = bufferLayout.find(candidateLayout => candidateLayout.name === columnName);
179✔
812
  return layout?.attributes ? layout : undefined;
137✔
813
}
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