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

visgl / luma.gl / 28098809096

24 Jun 2026 12:34PM UTC coverage: 70.667% (-0.001%) from 70.668%
28098809096

Pull #2687

github

web-flow
Merge d07ede553 into 1eb44740a
Pull Request #2687: [codex] Make GPU record batches own GPU data

10080 of 16035 branches covered (62.86%)

Branch coverage included in aggregate %.

151 of 188 new or added lines in 13 files covered. (80.32%)

15 existing lines in 4 files now uncovered.

20376 of 27063 relevant lines covered (75.29%)

4020.44 hits per line

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

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

5
import {Buffer, type BufferLayout, Device, type RenderPassProps} from '@luma.gl/core';
6
import {BufferTransform, DynamicBuffer, type BufferTransformProps} from '@luma.gl/engine';
7
import type {GPUData} from '../table/gpu-data';
8
import {GPUVector} from '../table/gpu-vector';
9
import {GPURecordBatch} from '../table/gpu-record-batch';
10
import {GPUTable} from '../table/gpu-table';
11
import {getGPUDataBuffersForLayout} from '../table/gpu-vector-utils';
12

13
type TableTransformBufferMap = NonNullable<Parameters<BufferTransform['run']>[0]>['outputBuffers'];
14
type TableTransformRunOptions = Parameters<BufferTransform['run']>[0];
15
type TableTransformInputVectors = Record<string, GPUVector> | GPUVector[];
16

17
/** Maps transform-feedback output varying names back to the input GPU vector they should update. */
18
export type TableTransformOutputCopyMap = Record<string, string>;
19

20
/** Options supplied for one {@link TableTransform.dispatchBatches} dispatch. */
21
export type TableTransformBatchOptions = RenderPassProps & {
22
  /** Output transform-feedback buffers for the current batch. */
23
  outputBuffers?:
24
    | TableTransformBufferMap
25
    | ((batch: GPURecordBatch, batchIndex: number) => TableTransformBufferMap);
26
};
27

28
/** Props for creating a WebGL transform backed by a GPU-resident table. */
29
export type TableTransformProps = BufferTransformProps & {
30
  /** Existing GPU table used as the source for transform input buffers. */
31
  table?: GPUTable;
32
  /** Existing GPU vectors converted into the transform input table. */
33
  inputVectors?: TableTransformInputVectors;
34
  /**
35
   * Allocates dense transform-feedback output vectors and copies them back into named input
36
   * vectors after each run.
37
   *
38
   * Output names are inferred as transform-feedback outputs when `outputs` is omitted.
39
   */
40
  copyOutputToInputVectors?: TableTransformOutputCopyMap;
41
  /** Controls whether GPU table row count is assigned to vertexCount. */
42
  tableCount?: 'vertex' | 'none';
43
};
44

45
type TableTransformState = {
46
  table: GPUTable;
47
  ownsTable: boolean;
48
  transformProps: BufferTransformProps;
49
  explicitAttributes: NonNullable<BufferTransformProps['attributes']>;
50
  explicitBufferLayout: BufferLayout[];
51
  inferVertexCount: boolean;
52
  copyOutputToInputVectors: TableTransformOutputCopyMap;
53
  outputVectors: Record<string, GPUVector>;
54
};
55

56
/**
57
 * A WebGL transform-feedback program whose input buffers come from a {@link GPUTable}.
58
 *
59
 * `TableTransform` keeps the regular {@link BufferTransform} execution model,
60
 * while adding table construction and batch-by-batch execution helpers.
61
 */
62
export class TableTransform extends BufferTransform {
63
  /** GPU table backing the transform input attributes. */
64
  readonly table: GPUTable;
65
  /** GPU vectors backing the transform input attributes. */
66
  readonly inputVectors: Record<string, GPUVector>;
67
  /** Dense transform-feedback output vectors allocated for automatic input writeback. */
68
  readonly outputVectors: Record<string, GPUVector>;
69
  private readonly ownsTable: boolean;
70
  private readonly explicitAttributes: NonNullable<BufferTransformProps['attributes']>;
71
  private readonly explicitBufferLayout: BufferLayout[];
72
  private readonly inferVertexCount: boolean;
73
  private readonly copyOutputToInputVectors: TableTransformOutputCopyMap;
74
  private tableTransformDestroyed = false;
1✔
75

76
  constructor(device: Device, props: TableTransformProps) {
77
    const {
78
      table,
79
      ownsTable,
80
      transformProps,
81
      explicitAttributes,
82
      explicitBufferLayout,
83
      inferVertexCount,
84
      copyOutputToInputVectors,
85
      outputVectors
86
    } = getTableTransformState(device, props);
2✔
87

88
    try {
2✔
89
      super(device, transformProps);
2✔
90
    } catch (error) {
91
      destroyGPUVectors(outputVectors);
×
92
      if (ownsTable) {
×
93
        table.destroy();
×
94
      }
95
      throw error;
×
96
    }
97

98
    this.table = table;
1✔
99
    this.inputVectors = table.gpuVectors;
1✔
100
    this.outputVectors = outputVectors;
1✔
101
    this.ownsTable = ownsTable;
1✔
102
    this.explicitAttributes = explicitAttributes;
1✔
103
    this.explicitBufferLayout = explicitBufferLayout;
1✔
104
    this.inferVertexCount = inferVertexCount;
1✔
105
    this.copyOutputToInputVectors = copyOutputToInputVectors;
1✔
106
  }
107

108
  /** Runs the transform once and optionally copies transform outputs back into input vectors. */
109
  override run(options: TableTransformRunOptions = {}): void {
1✔
110
    if (!this.hasAutomaticOutputWriteback()) {
1!
111
      super.run(options);
×
112
      return;
×
113
    }
114
    assertNoExplicitOutputBuffers(
1✔
115
      options?.outputBuffers,
116
      'TableTransform.run() cannot combine outputBuffers with copyOutputToInputVectors'
117
    );
118
    super.run({...options, outputBuffers: this.getAutomaticOutputBuffers()});
1✔
119
    this.copyOutputsToInputVectors();
1✔
120
  }
121

122
  /** Runs the transform once per preserved GPU record batch. */
123
  dispatchBatches(options: TableTransformBatchOptions = {}): void {
×
124
    if (this.hasAutomaticOutputWriteback() && options.outputBuffers) {
×
125
      throw new Error(
×
126
        'TableTransform.dispatchBatches() cannot combine outputBuffers with copyOutputToInputVectors'
127
      );
128
    }
129
    assertMatchingBufferLayouts(
×
130
      this.table.bufferLayout,
131
      this.explicitBufferLayout,
132
      this.model.bufferLayout,
133
      'TableTransform.dispatchBatches() model buffer layout does not match its GPU table'
134
    );
135

136
    try {
×
137
      this.table.batches.forEach((batch, batchIndex) => {
×
138
        assertMatchingBufferLayouts(
×
139
          this.table.bufferLayout,
140
          [],
141
          batch.bufferLayout,
142
          'TableTransform.dispatchBatches() requires every GPU batch to use the table buffer layout'
143
        );
144
        this.model.setAttributes({
×
145
          ...this.explicitAttributes,
146
          ...getTableTransformAttributes(batch)
147
        });
148
        if (this.inferVertexCount) {
×
149
          this.model.setVertexCount(batch.numRows);
×
150
        }
151

152
        const outputBuffers =
153
          typeof options.outputBuffers === 'function'
×
154
            ? options.outputBuffers(batch, batchIndex)
155
            : options.outputBuffers;
156
        const {outputBuffers: _ignoredOutputBuffers, ...renderPassProps} = options;
×
157
        if (this.hasAutomaticOutputWriteback()) {
×
158
          super.run({...renderPassProps, outputBuffers: this.getAutomaticOutputBuffers()});
×
159
        } else {
160
          super.run({...renderPassProps, outputBuffers});
×
161
        }
162
      });
163
      if (this.hasAutomaticOutputWriteback()) {
×
164
        this.copyOutputsToInputVectors();
×
165
      }
166
    } finally {
167
      this.model.setAttributes({
×
168
        ...this.explicitAttributes,
169
        ...getTableTransformAttributes(this.table)
170
      });
171
      if (this.inferVertexCount) {
×
172
        this.model.setVertexCount(this.table.numRows);
×
173
      }
174
    }
175
  }
176

177
  override destroy(): void {
178
    if (this.tableTransformDestroyed) {
1!
179
      return;
×
180
    }
181
    super.destroy();
1✔
182
    destroyGPUVectors(this.outputVectors);
1✔
183
    if (this.ownsTable) {
1!
184
      this.table.destroy();
1✔
185
    }
186
    this.tableTransformDestroyed = true;
1✔
187
  }
188

189
  private hasAutomaticOutputWriteback(): boolean {
190
    return Object.keys(this.copyOutputToInputVectors).length > 0;
1✔
191
  }
192

193
  private getAutomaticOutputBuffers(): TableTransformBufferMap {
194
    return Object.fromEntries(
1✔
195
      Object.entries(this.outputVectors).map(([outputName, vector]) => [
1✔
196
        outputName,
197
        getConcreteGPUDataBuffer(getSingleGPUVectorData(vector))
198
      ])
199
    );
200
  }
201

202
  private copyOutputsToInputVectors(): void {
203
    const commandEncoder = this.device.createCommandEncoder();
1✔
204
    let copyCount = 0;
1✔
205

206
    for (const [outputName, inputName] of Object.entries(this.copyOutputToInputVectors)) {
1✔
207
      const outputVector = this.outputVectors[outputName];
1✔
208
      const inputVector = this.inputVectors[inputName];
1✔
209
      if (!outputVector || !inputVector) {
1!
210
        throw new Error(`TableTransform writeback mapping "${outputName}" is incomplete`);
×
211
      }
212
      const size = inputVector.length * inputVector.byteStride;
1✔
213
      if (size === 0) {
1!
214
        continue;
×
215
      }
216
      commandEncoder.copyBufferToBuffer({
1✔
217
        sourceBuffer: getConcreteGPUDataBuffer(getSingleGPUVectorData(outputVector)),
218
        destinationBuffer: getConcreteGPUDataBuffer(getSingleGPUVectorData(inputVector)),
219
        size
220
      });
221
      copyCount++;
1✔
222
    }
223

224
    if (copyCount > 0) {
1!
225
      this.device.submit(commandEncoder.finish());
1✔
226
      return;
1✔
227
    }
228
    commandEncoder.destroy();
×
229
  }
230
}
231

232
function getTableTransformState(device: Device, props: TableTransformProps): TableTransformState {
233
  const {
234
    table: explicitTable,
235
    inputVectors,
236
    copyOutputToInputVectors = {},
2✔
237
    tableCount = 'vertex',
2✔
238
    ...transformProps
239
  } = props;
2✔
240

241
  validateTableTransformSources({table: explicitTable, inputVectors});
2✔
242
  if (!transformProps.shaderLayout) {
2!
243
    throw new Error('TableTransform requires shaderLayout');
×
244
  }
245

246
  const {table, ownsTable} = getInitialTable({
2✔
247
    table: explicitTable,
248
    inputVectors
249
  });
250

251
  const explicitAttributes = transformProps.attributes || {};
2✔
252
  const explicitBufferLayout = transformProps.bufferLayout || [];
2✔
253
  const inferVertexCount = tableCount === 'vertex' && transformProps.vertexCount === undefined;
2✔
254
  let outputVectors: Record<string, GPUVector> = {};
2✔
255
  let transformOutputs: string[] | undefined;
256

257
  try {
2✔
258
    assertNoDuplicateNames(
2✔
259
      Object.keys(explicitAttributes),
260
      getBufferLayoutNames(table.bufferLayout),
261
      'attribute'
262
    );
263
    assertNoDuplicateNames(
2✔
264
      getBufferLayoutNames(explicitBufferLayout),
265
      getBufferLayoutNames(table.bufferLayout),
266
      'buffer layout'
267
    );
268
    outputVectors = createAutomaticOutputVectors(device, table, copyOutputToInputVectors);
2✔
269
    transformOutputs = getTableTransformOutputs(transformProps, copyOutputToInputVectors);
2✔
270
  } catch (error) {
271
    destroyGPUVectors(outputVectors);
1✔
272
    if (ownsTable) {
1!
273
      table.destroy();
1✔
274
    }
275
    throw error;
1✔
276
  }
277

278
  return {
1✔
279
    table,
280
    ownsTable,
281
    explicitAttributes,
282
    explicitBufferLayout,
283
    inferVertexCount,
284
    copyOutputToInputVectors,
285
    outputVectors,
286
    transformProps: {
287
      ...transformProps,
288
      ...(transformOutputs ? {outputs: transformOutputs} : {}),
1!
289
      attributes: {...explicitAttributes, ...getTableTransformAttributes(table)},
290
      bufferLayout: [...explicitBufferLayout, ...table.bufferLayout],
291
      ...(inferVertexCount ? {vertexCount: table.numRows} : {})
1!
292
    }
293
  };
294
}
295

296
function getTableTransformAttributes(
297
  source: GPUTable | GPURecordBatch
298
): Record<string, Buffer | DynamicBuffer> {
299
  const attributeSource = source instanceof GPUTable ? source.batches[0] : source;
1!
300
  if (!attributeSource) {
1!
NEW
301
    return {};
×
302
  }
303
  return getGPUDataBuffersForLayout(attributeSource.bufferLayout, attributeSource.gpuData);
1✔
304
}
305

306
function getInitialTable(props: {table?: GPUTable; inputVectors?: TableTransformInputVectors}): {
307
  table: GPUTable;
308
  ownsTable: boolean;
309
} {
310
  if (props.table) {
2!
311
    return {table: props.table, ownsTable: false};
×
312
  }
313
  if (props.inputVectors) {
2!
314
    return {table: new GPUTable({vectors: props.inputVectors}), ownsTable: true};
2✔
315
  }
316
  throw new Error('TableTransform requires table or inputVectors');
×
317
}
318

319
function validateTableTransformSources(props: {
320
  table?: GPUTable;
321
  inputVectors?: TableTransformInputVectors;
322
}): void {
323
  const sourceCount = Number(Boolean(props.table)) + Number(Boolean(props.inputVectors));
2✔
324
  if (sourceCount > 1) {
2!
325
    throw new Error('TableTransform requires only one of table or inputVectors');
×
326
  }
327
  if (sourceCount === 0) {
2!
328
    throw new Error('TableTransform requires table or inputVectors');
×
329
  }
330
}
331

332
function createAutomaticOutputVectors(
333
  device: Device,
334
  table: GPUTable,
335
  copyOutputToInputVectors: TableTransformOutputCopyMap
336
): Record<string, GPUVector> {
337
  const outputEntries = Object.entries(copyOutputToInputVectors);
2✔
338
  if (outputEntries.length === 0) {
2!
339
    return {};
×
340
  }
341
  if (table.batches.length !== 1) {
2!
342
    throw new Error(
×
343
      'TableTransform copyOutputToInputVectors currently requires one directly bindable GPU batch'
344
    );
345
  }
346

347
  const copiedInputNames = new Set<string>();
2✔
348
  const outputVectors: Record<string, GPUVector> = {};
2✔
349

350
  try {
2✔
351
    for (const [outputName, inputName] of outputEntries) {
2✔
352
      if (copiedInputNames.has(inputName)) {
2!
353
        throw new Error(
×
354
          `TableTransform copyOutputToInputVectors maps more than one output to "${inputName}"`
355
        );
356
      }
357
      copiedInputNames.add(inputName);
2✔
358

359
      const inputVector = table.gpuVectors[inputName];
2✔
360
      if (!inputVector) {
2!
361
        throw new Error(
×
362
          `TableTransform copyOutputToInputVectors references missing input vector "${inputName}"`
363
        );
364
      }
365
      validateAutomaticWritebackVector(inputName, inputVector);
2✔
366
      if (!inputVector.format) {
2!
367
        throw new Error(
×
368
          `TableTransform copyOutputToInputVectors requires input vector "${inputName}" to have a format`
369
        );
370
      }
371

372
      const byteLength = inputVector.length * inputVector.byteStride;
1✔
373
      const outputBuffer = device.createBuffer({
1✔
374
        usage: Buffer.VERTEX | Buffer.STORAGE | Buffer.COPY_SRC | Buffer.COPY_DST,
375
        byteLength: Math.max(1, byteLength)
376
      });
377
      outputVectors[outputName] = new GPUVector({
1✔
378
        type: 'buffer',
379
        name: outputName,
380
        buffer: outputBuffer,
381
        format: inputVector.format,
382
        length: inputVector.length,
383
        stride: inputVector.stride,
384
        byteStride: inputVector.byteStride,
385
        rowByteLength: inputVector.rowByteLength,
386
        ownsBuffer: true
387
      });
388
    }
389
  } catch (error) {
390
    destroyGPUVectors(outputVectors);
1✔
391
    throw error;
1✔
392
  }
393

394
  return outputVectors;
1✔
395
}
396

397
function validateAutomaticWritebackVector(name: string, vector: GPUVector): void {
398
  if (vector.bufferLayout) {
2!
399
    throw new Error(
×
400
      `TableTransform copyOutputToInputVectors does not support interleaved input vector "${name}"`
401
    );
402
  }
403
  if (vector.byteOffset !== 0) {
2!
404
    throw new Error(
×
405
      `TableTransform copyOutputToInputVectors requires zero byteOffset for input vector "${name}"`
406
    );
407
  }
408
  if (vector.byteStride !== vector.rowByteLength) {
2✔
409
    throw new Error(
1✔
410
      `TableTransform copyOutputToInputVectors requires tightly packed input vector "${name}"`
411
    );
412
  }
413
  getConcreteGPUDataBuffer(getSingleGPUVectorData(vector));
1✔
414
}
415

416
function getTableTransformOutputs(
417
  transformProps: BufferTransformProps,
418
  copyOutputToInputVectors: TableTransformOutputCopyMap
419
): string[] | undefined {
420
  const inferredOutputs = Object.keys(copyOutputToInputVectors);
1✔
421
  if (inferredOutputs.length === 0) {
1!
422
    return transformProps.outputs;
×
423
  }
424

425
  const declaredOutputs = transformProps.outputs || transformProps.varyings;
1✔
426
  if (!declaredOutputs) {
1!
427
    return inferredOutputs;
1✔
428
  }
429
  assertSameOutputNames(declaredOutputs, inferredOutputs);
×
430
  return transformProps.outputs;
×
431
}
432

433
function assertSameOutputNames(declaredOutputs: string[], inferredOutputs: string[]): void {
434
  if (
×
435
    declaredOutputs.length !== inferredOutputs.length ||
×
436
    declaredOutputs.some(outputName => !inferredOutputs.includes(outputName))
×
437
  ) {
438
    throw new Error(
×
439
      'TableTransform outputs must match copyOutputToInputVectors output names when both are supplied'
440
    );
441
  }
442
}
443

444
function assertNoExplicitOutputBuffers(
445
  outputBuffers: TableTransformBufferMap | undefined,
446
  errorMessage: string
447
): void {
448
  if (outputBuffers) {
1!
449
    throw new Error(errorMessage);
×
450
  }
451
}
452

453
function getConcreteGPUDataBuffer(data: GPUData): Buffer {
454
  return data.buffer instanceof DynamicBuffer ? data.buffer.buffer : data.buffer;
4✔
455
}
456

457
function getSingleGPUVectorData(vector: GPUVector): GPUData {
458
  const [data, ...remainingData] = vector.data;
4✔
459
  if (!data || remainingData.length > 0) {
4!
460
    throw new Error(`TableTransform vector "${vector.name}" requires exactly one GPUData chunk`);
×
461
  }
462
  return data;
4✔
463
}
464

465
function destroyGPUVectors(vectors: Record<string, GPUVector>): void {
466
  for (const vector of Object.values(vectors)) {
3✔
467
    vector.destroy();
1✔
468
  }
469
}
470

471
function getBufferLayoutNames(bufferLayout: BufferLayout[]): string[] {
472
  return bufferLayout.map(layout => layout.name);
6✔
473
}
474

475
function assertNoDuplicateNames(
476
  explicitNames: string[],
477
  tableNames: string[],
478
  nameType: string
479
): void {
480
  const explicitNameSet = new Set(explicitNames);
4✔
481
  for (const tableName of tableNames) {
4✔
482
    if (explicitNameSet.has(tableName)) {
4!
483
      throw new Error(
×
484
        `TableTransform ${nameType} "${tableName}" duplicates an explicit ${nameType}`
485
      );
486
    }
487
  }
488
}
489

490
function assertMatchingBufferLayouts(
491
  tableBufferLayout: BufferLayout[],
492
  explicitBufferLayout: BufferLayout[],
493
  candidateBufferLayout: BufferLayout[],
494
  errorMessage: string
495
): void {
496
  const expectedBufferLayout = [...explicitBufferLayout, ...tableBufferLayout];
×
497
  if (JSON.stringify(expectedBufferLayout) !== JSON.stringify(candidateBufferLayout)) {
×
498
    throw new Error(errorMessage);
×
499
  }
500
}
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