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

visgl / luma.gl / 27202153897

09 Jun 2026 11:10AM UTC coverage: 69.763% (-0.7%) from 70.51%
27202153897

Pull #2669

github

web-flow
Merge 2f4faa434 into 70f8e2f4b
Pull Request #2669: chore(arrow): Improve split between GPUTables and Arrow tables

8943 of 14517 branches covered (61.6%)

Branch coverage included in aggregate %.

391 of 865 new or added lines in 32 files covered. (45.2%)

1 existing line in 1 file now uncovered.

18536 of 24872 relevant lines covered (74.53%)

4288.64 hits per line

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

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

5
import {
6
  Buffer,
7
  Device,
8
  type BufferLayout,
9
  type CommandEncoder,
10
  type RenderPass
11
} from '@luma.gl/core';
12
import {DynamicBuffer, Model, type ModelProps} from '@luma.gl/engine';
13
import type {GPURecordBatch} from '../table/gpu-record-batch';
14
import {GPU_TABLE_INDEX_COLUMN_NAME} from '../table/gpu-schema';
15
import {GPUTable} from '../table/gpu-table';
16

17
/** Controls which Model draw count mirrors the current GPU table row count. */
18
export type GPUTableModelCount = 'instance' | 'vertex' | 'none';
19

20
/** Props for rendering one GPU table through a luma.gl Model. */
21
export type GPUTableModelProps = ModelProps & {
22
  /** GPU table supplying model-ready attributes, bindings, and preserved batches. */
23
  table?: GPUTable;
24
  /** Controls whether table rows infer `instanceCount`, `vertexCount`, or neither. */
25
  tableCount?: GPUTableModelCount;
26
};
27

28
export type GPUTableModelDrawBatchesOptions = {
29
  /** Called immediately before drawing each preserved GPU record batch. */
30
  onBatch?: (batch: GPURecordBatch, batchIndex: number) => void;
31
};
32

33
type GPUTableModelState = {
34
  explicitAttributes: NonNullable<ModelProps['attributes']>;
35
  explicitBindings: NonNullable<ModelProps['bindings']>;
36
  explicitBufferLayout: BufferLayout[];
37
  explicitIndexBuffer: Buffer | DynamicBuffer | null;
38
  explicitIndexCount: number | undefined;
39
  explicitFirstVertex: number;
40
  explicitFirstIndex: number;
41
  explicitVertexCount: number;
42
  inferInstanceCount: boolean;
43
  inferVertexCount: boolean;
44
};
45

46
type GPUTableModelConstructorState = {
47
  table?: GPUTable;
48
  modelProps: ModelProps;
49
  state: GPUTableModelState;
50
};
51

52
type GPUTableDrawSource = GPUTable | GPURecordBatch;
53

54
type GPUTableIndexDrawState = {
55
  indexBuffer: Buffer | DynamicBuffer;
56
  indexCount: number;
57
  firstVertex: number;
58
  firstIndex: number;
59
};
60

61
/**
62
 * A luma.gl Model whose GPU attributes and bindings are sourced from a `GPUTable`.
63
 *
64
 * The table stays caller-owned. The model rebinds preserved table batches on
65
 * demand and mirrors table row counts into draw counts when requested.
66
 */
67
export class GPUTableModel extends Model {
68
  /** Currently bound table, when table-backed rendering is active. */
69
  table?: GPUTable;
70
  private readonly tableState: GPUTableModelState;
71
  private drawingTableBatches = false;
18✔
72

73
  /** Creates a model whose table-backed attributes and bindings can be rebound by batch. */
74
  constructor(device: Device, props: GPUTableModelProps) {
75
    const {table, modelProps, state} = getGPUTableModelConstructorState(props);
20✔
76
    super(device, modelProps);
20✔
77
    this.table = table;
20✔
78
    this.tableState = state;
20✔
79
    if (table) {
20✔
80
      this.setTableDrawState(table);
18✔
81
    }
82
  }
83

84
  /** Replaces the bound GPU table when one is supplied. */
85
  setProps(props: Partial<GPUTableModelProps>): void {
86
    if (props.table) {
1!
87
      this.setTable(props.table);
1✔
88
    }
89
  }
90

91
  /** Query redraw status after synchronizing inferred table draw state. */
92
  override needsRedraw(): false | string {
93
    this.syncTableDrawState();
2✔
94
    return super.needsRedraw();
2✔
95
  }
96

97
  /** Synchronizes inferred table draw state before opening a render pass. */
98
  override predraw(commandEncoder: CommandEncoder): void {
99
    this.syncTableDrawState();
3✔
100
    super.predraw(commandEncoder);
3✔
101
  }
102

103
  /**
104
   * Draws each preserved GPU record batch by rebinding batch-local buffers.
105
   *
106
   * The table-level attributes and bindings are restored before returning.
107
   */
108
  drawBatches(renderPass: RenderPass, options: GPUTableModelDrawBatchesOptions = {}): boolean {
2✔
109
    const table = this.table;
2✔
110
    if (!(table instanceof GPUTable)) {
2!
111
      throw new Error('GPUTableModel.drawBatches() requires a GPUTable');
×
112
    }
113

114
    assertMatchingBufferLayouts(
2✔
115
      table.bufferLayout,
116
      this.tableState.explicitBufferLayout,
117
      this.bufferLayout,
118
      'GPUTableModel.drawBatches() model buffer layout does not match its GPU table'
119
    );
120

121
    let drawSuccess = true;
2✔
122
    this.drawingTableBatches = true;
2✔
123
    try {
2✔
124
      for (const [batchIndex, batch] of table.batches.entries()) {
2✔
125
        assertMatchingBufferLayouts(
3✔
126
          table.bufferLayout,
127
          [],
128
          batch.bufferLayout,
129
          'GPUTableModel.drawBatches() requires every batch to use the table buffer layout'
130
        );
131
        this.setAttributes({
3✔
132
          ...this.tableState.explicitAttributes,
133
          ...batch.attributes
134
        });
135
        this.setBindings({
3✔
136
          ...this.tableState.explicitBindings,
137
          ...batch.bindings
138
        });
139
        this.setTableDrawState(batch);
3✔
140
        options.onBatch?.(batch, batchIndex);
3✔
141
        drawSuccess = super.draw(renderPass) && drawSuccess;
3✔
142
      }
143
    } finally {
144
      this.drawingTableBatches = false;
2✔
145
      this.setAttributes({
2✔
146
        ...this.tableState.explicitAttributes,
147
        ...table.attributes
148
      });
149
      this.setBindings({
2✔
150
        ...this.tableState.explicitBindings,
151
        ...table.bindings
152
      });
153
      this.setTableDrawState(table);
2✔
154
    }
155

156
    return drawSuccess;
2✔
157
  }
158

159
  /** Rebinds the model to a replacement table while preserving explicit model state. */
160
  protected setTable(nextTable: GPUTable): void {
161
    assertNoExplicitIndexBuffer(nextTable, this.tableState.explicitIndexBuffer);
1✔
162
    validateGPUTableIndexBatches(nextTable);
1✔
163
    assertNoDuplicateNames(
1✔
164
      Object.keys(this.tableState.explicitAttributes),
165
      Object.keys(nextTable.attributes),
166
      'attribute'
167
    );
168
    assertNoDuplicateNames(
1✔
169
      getBufferLayoutNames(this.tableState.explicitBufferLayout),
170
      getBufferLayoutNames(nextTable.bufferLayout),
171
      'buffer layout'
172
    );
173
    assertNoDuplicateNames(
1✔
174
      Object.keys(this.tableState.explicitBindings),
175
      Object.keys(nextTable.bindings),
176
      'binding'
177
    );
178

179
    this.setBufferLayout([...this.tableState.explicitBufferLayout, ...nextTable.bufferLayout]);
1✔
180
    this.setAttributes({
1✔
181
      ...this.tableState.explicitAttributes,
182
      ...nextTable.attributes
183
    });
184
    this.setBindings({
1✔
185
      ...this.tableState.explicitBindings,
186
      ...nextTable.bindings
187
    });
188
    this.setTableDrawState(nextTable);
1✔
189
    this.table = nextTable;
1✔
190
  }
191

192
  /** Disables table-backed draw state and restores any explicit index buffer. */
193
  protected clearTable(): void {
194
    this.table = undefined;
×
195
    this.restoreExplicitIndexDrawState();
×
196
  }
197

198
  private syncTableDrawState(): void {
199
    if (!this.table || this.drawingTableBatches) {
5✔
200
      return;
3✔
201
    }
202
    this.setTableDrawState(this.table);
2✔
203
  }
204

205
  private setTableDrawState(source: GPUTableDrawSource): void {
206
    this.setTableRowCount(source.numRows);
26✔
207
    const indexDrawState = getGPUTableIndexDrawState(source);
26✔
208
    if (indexDrawState) {
26!
209
      this.setTableIndexDrawState(indexDrawState);
×
210
      return;
×
211
    }
212
    this.restoreExplicitIndexDrawState();
26✔
213
  }
214

215
  private setTableRowCount(rowCount: number): void {
216
    if (this.tableState.inferInstanceCount && this.instanceCount !== rowCount) {
26✔
217
      this.setInstanceCount(rowCount);
5✔
218
    }
219
    if (this.tableState.inferVertexCount && this.vertexCount !== rowCount) {
26!
220
      this.setVertexCount(rowCount);
×
221
    }
222
  }
223

224
  private setTableIndexDrawState({
225
    indexBuffer,
226
    indexCount,
227
    firstVertex,
228
    firstIndex
229
  }: GPUTableIndexDrawState): void {
230
    if (this.indexBuffer !== getConcreteIndexBuffer(indexBuffer)) {
×
231
      this.setIndexBuffer(indexBuffer);
×
232
    }
NEW
233
    if (this.indexCount !== indexCount) {
×
NEW
234
      this.setIndexCount(indexCount);
×
235
    }
NEW
236
    if (this.firstVertex !== firstVertex || this.firstIndex !== firstIndex) {
×
NEW
237
      this.setDrawOffsets({firstVertex, firstIndex});
×
238
    }
239
    if (this.vertexCount !== indexCount) {
×
240
      this.setVertexCount(indexCount);
×
241
    }
242
  }
243

244
  private restoreExplicitIndexDrawState(): void {
245
    if (this.indexBuffer !== getConcreteIndexBuffer(this.tableState.explicitIndexBuffer)) {
26!
246
      this.setIndexBuffer(this.tableState.explicitIndexBuffer);
×
247
    }
248
    if (this.indexCount !== this.tableState.explicitIndexCount) {
26!
NEW
249
      this.setIndexCount(this.tableState.explicitIndexCount);
×
250
    }
251
    if (
26!
252
      this.firstVertex !== this.tableState.explicitFirstVertex ||
52✔
253
      this.firstIndex !== this.tableState.explicitFirstIndex
254
    ) {
NEW
255
      this.setDrawOffsets({
×
256
        firstVertex: this.tableState.explicitFirstVertex,
257
        firstIndex: this.tableState.explicitFirstIndex
258
      });
259
    }
260
    if (
26!
261
      !this.tableState.inferVertexCount &&
51✔
262
      this.vertexCount !== this.tableState.explicitVertexCount
263
    ) {
264
      this.setVertexCount(this.tableState.explicitVertexCount);
×
265
    }
266
  }
267
}
268

269
function getGPUTableModelConstructorState(
270
  props: GPUTableModelProps
271
): GPUTableModelConstructorState {
272
  const {table, tableCount = 'instance', ...modelProps} = props;
20✔
273
  const explicitAttributes = modelProps.attributes || {};
20✔
274
  const explicitBindings = modelProps.bindings || {};
20✔
275
  const explicitBufferLayout = modelProps.bufferLayout || [];
20✔
276
  const explicitIndexBuffer = modelProps.indexBuffer ?? null;
20✔
277
  const explicitIndexCount = modelProps.indexCount;
20✔
278
  const explicitFirstVertex = modelProps.firstVertex ?? 0;
20✔
279
  const explicitFirstIndex = modelProps.firstIndex ?? 0;
20✔
280
  const explicitVertexCount = modelProps.vertexCount ?? 0;
20✔
281
  const inferInstanceCount =
282
    Boolean(table) && tableCount === 'instance' && modelProps.instanceCount === undefined;
20✔
283
  const inferVertexCount =
284
    Boolean(table) && tableCount === 'vertex' && modelProps.vertexCount === undefined;
20✔
285

286
  if (!table) {
20!
287
    return {
×
288
      table,
289
      modelProps,
290
      state: {
291
        explicitAttributes,
292
        explicitBindings,
293
        explicitBufferLayout,
294
        explicitIndexBuffer,
295
        explicitIndexCount,
296
        explicitFirstVertex,
297
        explicitFirstIndex,
298
        explicitVertexCount,
299
        inferInstanceCount,
300
        inferVertexCount
301
      }
302
    };
303
  }
304

305
  assertNoDuplicateNames(
20✔
306
    Object.keys(explicitAttributes),
307
    Object.keys(table.attributes),
308
    'attribute'
309
  );
310
  assertNoDuplicateNames(
20✔
311
    getBufferLayoutNames(explicitBufferLayout),
312
    getBufferLayoutNames(table.bufferLayout),
313
    'buffer layout'
314
  );
315
  assertNoDuplicateNames(Object.keys(explicitBindings), Object.keys(table.bindings), 'binding');
20✔
316
  assertNoExplicitIndexBuffer(table, explicitIndexBuffer);
20✔
317
  validateGPUTableIndexBatches(table);
20✔
318

319
  return {
20✔
320
    table,
321
    state: {
322
      explicitAttributes,
323
      explicitBindings,
324
      explicitBufferLayout,
325
      explicitIndexBuffer,
326
      explicitIndexCount,
327
      explicitFirstVertex,
328
      explicitFirstIndex,
329
      explicitVertexCount,
330
      inferInstanceCount,
331
      inferVertexCount
332
    },
333
    modelProps: {
334
      ...modelProps,
335
      bufferLayout: [...explicitBufferLayout, ...table.bufferLayout],
336
      attributes: {...explicitAttributes, ...table.attributes},
337
      bindings: {...explicitBindings, ...table.bindings},
338
      ...(inferInstanceCount ? {instanceCount: table.numRows} : {}),
18✔
339
      ...(inferVertexCount ? {vertexCount: table.numRows} : {})
18✔
340
    }
341
  };
342
}
343

344
function getBufferLayoutNames(bufferLayout: BufferLayout[]): string[] {
345
  return bufferLayout.map(layout => layout.name);
40✔
346
}
347

348
function assertNoDuplicateNames(
349
  explicitNames: string[],
350
  tableNames: string[],
351
  nameType: string
352
): void {
353
  const explicitNameSet = new Set(explicitNames);
61✔
354
  for (const tableName of tableNames) {
61✔
355
    if (explicitNameSet.has(tableName)) {
53✔
356
      throw new Error(
2✔
357
        `GPUTableModel ${nameType} "${tableName}" duplicates an explicit ${nameType}`
358
      );
359
    }
360
  }
361
}
362

363
function assertNoExplicitIndexBuffer(
364
  table: Pick<GPUTable, 'gpuVectors'>,
365
  explicitIndexBuffer: Buffer | DynamicBuffer | null
366
): void {
367
  if (explicitIndexBuffer && table.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]) {
19!
368
    throw new Error('GPUTableModel indices column duplicates an explicit indexBuffer');
×
369
  }
370
}
371

372
function validateGPUTableIndexBatches(table: GPUTable): void {
373
  const tableHasIndices = Boolean(table.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]);
19✔
374
  for (const batch of table.batches) {
19✔
375
    const batchHasIndices = Boolean(batch.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]);
22✔
376
    if (batchHasIndices !== tableHasIndices) {
22!
377
      throw new Error('GPUTableModel indexed tables require every batch to include indices');
×
378
    }
379
    getGPUTableIndexDrawState(batch);
22✔
380
  }
381
}
382

383
function getGPUTableIndexDrawState(source: GPUTableDrawSource): GPUTableIndexDrawState | null {
384
  const indexVector = source.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME];
48✔
385
  if (!indexVector) {
48!
386
    return null;
48✔
387
  }
388
  if (source instanceof GPUTable && source.batches.length > 1) {
×
389
    return null;
×
390
  }
391
  if (indexVector.format !== 'vertex-list<uint32>') {
×
392
    throw new Error('GPUTableModel indices column requires vertex-list<uint32> format');
×
393
  }
394

395
  const [indexData, ...remainingIndexData] = indexVector.data;
×
396
  if (!indexData || remainingIndexData.length > 0) {
×
397
    throw new Error('GPUTableModel indices column requires exactly one GPUData chunk');
×
398
  }
399
  const indexBuffer = indexData.buffer;
×
400
  const concreteIndexBuffer = getConcreteIndexBuffer(indexBuffer);
×
401
  if (!concreteIndexBuffer || !(concreteIndexBuffer.usage & Buffer.INDEX)) {
×
402
    throw new Error('GPUTableModel indices column requires Buffer.INDEX usage');
×
403
  }
NEW
404
  const indexByteStride = concreteIndexBuffer.indexType === 'uint32' ? 4 : 2;
×
405
  if (indexData.byteOffset % indexByteStride !== 0) {
48!
NEW
406
    throw new Error('GPUTableModel indices column byteOffset must align with its index type');
×
407
  }
UNCOV
408
  return {
×
409
    indexBuffer,
410
    indexCount: indexVector.valueLength,
411
    firstVertex: indexData.byteOffset,
412
    firstIndex: indexData.byteOffset / indexByteStride
413
  };
414
}
415

416
function getConcreteIndexBuffer(indexBuffer: Buffer | DynamicBuffer | null): Buffer | null {
417
  return indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
26!
418
}
419

420
function assertMatchingBufferLayouts(
421
  tableBufferLayout: BufferLayout[],
422
  explicitBufferLayout: BufferLayout[],
423
  candidateBufferLayout: BufferLayout[],
424
  errorMessage: string
425
): void {
426
  const expectedBufferLayout = [...explicitBufferLayout, ...tableBufferLayout];
5✔
427
  if (!deepEqualBufferLayouts(expectedBufferLayout, candidateBufferLayout)) {
5!
428
    throw new Error(errorMessage);
×
429
  }
430
}
431

432
function deepEqualBufferLayouts(
433
  expectedBufferLayout: BufferLayout[],
434
  candidateBufferLayout: BufferLayout[]
435
): boolean {
436
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
5✔
437
}
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