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

visgl / luma.gl / 27039396583

05 Jun 2026 08:49PM UTC coverage: 70.51% (-0.2%) from 70.723%
27039396583

push

github

web-flow
chore: Arrow picking (#2668)

8921 of 14308 branches covered (62.35%)

Branch coverage included in aggregate %.

53 of 134 new or added lines in 8 files covered. (39.55%)

1 existing line in 1 file now uncovered.

18439 of 24495 relevant lines covered (75.28%)

4334.27 hits per line

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

70.2
/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
  explicitVertexCount: number;
39
  inferInstanceCount: boolean;
40
  inferVertexCount: boolean;
41
};
42

43
type GPUTableModelConstructorState = {
44
  table?: GPUTable;
45
  modelProps: ModelProps;
46
  state: GPUTableModelState;
47
};
48

49
type GPUTableDrawSource = GPUTable | GPURecordBatch;
50

51
type GPUTableIndexDrawState = {
52
  indexBuffer: Buffer | DynamicBuffer;
53
  indexCount: number;
54
};
55

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

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

79
  /** Replaces the bound GPU table when one is supplied. */
80
  setProps(props: Partial<GPUTableModelProps>): void {
81
    if (props.table) {
1!
82
      this.setTable(props.table);
1✔
83
    }
84
  }
85

86
  /** Query redraw status after synchronizing inferred table draw state. */
87
  override needsRedraw(): false | string {
88
    this.syncTableDrawState();
2✔
89
    return super.needsRedraw();
2✔
90
  }
91

92
  /** Synchronizes inferred table draw state before opening a render pass. */
93
  override predraw(commandEncoder: CommandEncoder): void {
94
    this.syncTableDrawState();
3✔
95
    super.predraw(commandEncoder);
3✔
96
  }
97

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

109
    assertMatchingBufferLayouts(
2✔
110
      table.bufferLayout,
111
      this.tableState.explicitBufferLayout,
112
      this.bufferLayout,
113
      'GPUTableModel.drawBatches() model buffer layout does not match its GPU table'
114
    );
115

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

151
    return drawSuccess;
2✔
152
  }
153

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

174
    this.setBufferLayout([...this.tableState.explicitBufferLayout, ...nextTable.bufferLayout]);
1✔
175
    this.setAttributes({
1✔
176
      ...this.tableState.explicitAttributes,
177
      ...nextTable.attributes
178
    });
179
    this.setBindings({
1✔
180
      ...this.tableState.explicitBindings,
181
      ...nextTable.bindings
182
    });
183
    this.setTableDrawState(nextTable);
1✔
184
    this.table = nextTable;
1✔
185
  }
186

187
  /** Disables table-backed draw state and restores any explicit index buffer. */
188
  protected clearTable(): void {
189
    this.table = undefined;
×
NEW
190
    this.restoreExplicitIndexDrawState();
×
191
  }
192

193
  private syncTableDrawState(): void {
194
    if (!this.table || this.drawingTableBatches) {
5✔
195
      return;
3✔
196
    }
197
    this.setTableDrawState(this.table);
2✔
198
  }
199

200
  private setTableDrawState(source: GPUTableDrawSource): void {
201
    this.setTableRowCount(source.numRows);
26✔
202
    const indexDrawState = getGPUTableIndexDrawState(source);
26✔
203
    if (indexDrawState) {
26!
NEW
204
      this.setTableIndexDrawState(indexDrawState);
×
NEW
205
      return;
×
206
    }
207
    this.restoreExplicitIndexDrawState();
26✔
208
  }
209

210
  private setTableRowCount(rowCount: number): void {
211
    if (this.tableState.inferInstanceCount && this.instanceCount !== rowCount) {
26✔
212
      this.setInstanceCount(rowCount);
5✔
213
    }
214
    if (this.tableState.inferVertexCount && this.vertexCount !== rowCount) {
26!
215
      this.setVertexCount(rowCount);
×
216
    }
217
  }
218

219
  private setTableIndexDrawState({indexBuffer, indexCount}: GPUTableIndexDrawState): void {
NEW
220
    if (this.indexBuffer !== getConcreteIndexBuffer(indexBuffer)) {
×
NEW
221
      this.setIndexBuffer(indexBuffer);
×
222
    }
NEW
223
    if (this.vertexCount !== indexCount) {
×
NEW
224
      this.setVertexCount(indexCount);
×
225
    }
226
  }
227

228
  private restoreExplicitIndexDrawState(): void {
229
    if (this.indexBuffer !== getConcreteIndexBuffer(this.tableState.explicitIndexBuffer)) {
26!
NEW
230
      this.setIndexBuffer(this.tableState.explicitIndexBuffer);
×
231
    }
232
    if (
26!
233
      !this.tableState.inferVertexCount &&
51✔
234
      this.vertexCount !== this.tableState.explicitVertexCount
235
    ) {
NEW
236
      this.setVertexCount(this.tableState.explicitVertexCount);
×
237
    }
238
  }
239
}
240

241
function getGPUTableModelConstructorState(
242
  props: GPUTableModelProps
243
): GPUTableModelConstructorState {
244
  const {table, tableCount = 'instance', ...modelProps} = props;
20✔
245
  const explicitAttributes = modelProps.attributes || {};
20✔
246
  const explicitBindings = modelProps.bindings || {};
20✔
247
  const explicitBufferLayout = modelProps.bufferLayout || [];
20✔
248
  const explicitIndexBuffer = modelProps.indexBuffer ?? null;
20✔
249
  const explicitVertexCount = modelProps.vertexCount ?? 0;
20✔
250
  const inferInstanceCount =
251
    Boolean(table) && tableCount === 'instance' && modelProps.instanceCount === undefined;
20✔
252
  const inferVertexCount =
253
    Boolean(table) && tableCount === 'vertex' && modelProps.vertexCount === undefined;
20✔
254

255
  if (!table) {
20!
256
    return {
×
257
      table,
258
      modelProps,
259
      state: {
260
        explicitAttributes,
261
        explicitBindings,
262
        explicitBufferLayout,
263
        explicitIndexBuffer,
264
        explicitVertexCount,
265
        inferInstanceCount,
266
        inferVertexCount
267
      }
268
    };
269
  }
270

271
  assertNoDuplicateNames(
20✔
272
    Object.keys(explicitAttributes),
273
    Object.keys(table.attributes),
274
    'attribute'
275
  );
276
  assertNoDuplicateNames(
20✔
277
    getBufferLayoutNames(explicitBufferLayout),
278
    getBufferLayoutNames(table.bufferLayout),
279
    'buffer layout'
280
  );
281
  assertNoDuplicateNames(Object.keys(explicitBindings), Object.keys(table.bindings), 'binding');
20✔
282
  assertNoExplicitIndexBuffer(table, explicitIndexBuffer);
20✔
283
  validateGPUTableIndexBatches(table);
20✔
284

285
  return {
20✔
286
    table,
287
    state: {
288
      explicitAttributes,
289
      explicitBindings,
290
      explicitBufferLayout,
291
      explicitIndexBuffer,
292
      explicitVertexCount,
293
      inferInstanceCount,
294
      inferVertexCount
295
    },
296
    modelProps: {
297
      ...modelProps,
298
      bufferLayout: [...explicitBufferLayout, ...table.bufferLayout],
299
      attributes: {...explicitAttributes, ...table.attributes},
300
      bindings: {...explicitBindings, ...table.bindings},
301
      ...(inferInstanceCount ? {instanceCount: table.numRows} : {}),
18✔
302
      ...(inferVertexCount ? {vertexCount: table.numRows} : {})
18✔
303
    }
304
  };
305
}
306

307
function getBufferLayoutNames(bufferLayout: BufferLayout[]): string[] {
308
  return bufferLayout.map(layout => layout.name);
40✔
309
}
310

311
function assertNoDuplicateNames(
312
  explicitNames: string[],
313
  tableNames: string[],
314
  nameType: string
315
): void {
316
  const explicitNameSet = new Set(explicitNames);
61✔
317
  for (const tableName of tableNames) {
61✔
318
    if (explicitNameSet.has(tableName)) {
53✔
319
      throw new Error(
2✔
320
        `GPUTableModel ${nameType} "${tableName}" duplicates an explicit ${nameType}`
321
      );
322
    }
323
  }
324
}
325

326
function assertNoExplicitIndexBuffer(
327
  table: Pick<GPUTable, 'gpuVectors'>,
328
  explicitIndexBuffer: Buffer | DynamicBuffer | null
329
): void {
330
  if (explicitIndexBuffer && table.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]) {
19!
NEW
331
    throw new Error('GPUTableModel indices column duplicates an explicit indexBuffer');
×
332
  }
333
}
334

335
function validateGPUTableIndexBatches(table: GPUTable): void {
336
  const tableHasIndices = Boolean(table.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]);
19✔
337
  for (const batch of table.batches) {
19✔
338
    const batchHasIndices = Boolean(batch.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]);
22✔
339
    if (batchHasIndices !== tableHasIndices) {
22!
NEW
340
      throw new Error('GPUTableModel indexed tables require every batch to include indices');
×
341
    }
342
    getGPUTableIndexDrawState(batch);
22✔
343
  }
344
}
345

346
function getGPUTableIndexDrawState(source: GPUTableDrawSource): GPUTableIndexDrawState | null {
347
  const indexVector = source.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME];
48✔
348
  if (!indexVector) {
48!
349
    return null;
48✔
350
  }
NEW
351
  if (source instanceof GPUTable && source.batches.length > 1) {
×
NEW
352
    return null;
×
353
  }
NEW
354
  if (indexVector.format !== 'vertex-list<uint32>') {
×
NEW
355
    throw new Error('GPUTableModel indices column requires vertex-list<uint32> format');
×
356
  }
357

NEW
358
  const [indexData, ...remainingIndexData] = indexVector.data;
×
NEW
359
  if (!indexData || remainingIndexData.length > 0) {
×
NEW
360
    throw new Error('GPUTableModel indices column requires exactly one GPUData chunk');
×
361
  }
NEW
362
  const indexBuffer = indexData.buffer;
×
NEW
363
  const concreteIndexBuffer = getConcreteIndexBuffer(indexBuffer);
×
NEW
364
  if (!concreteIndexBuffer || !(concreteIndexBuffer.usage & Buffer.INDEX)) {
×
NEW
365
    throw new Error('GPUTableModel indices column requires Buffer.INDEX usage');
×
366
  }
NEW
367
  return {
×
368
    indexBuffer,
369
    indexCount: indexVector.valueLength
370
  };
371
}
372

373
function getConcreteIndexBuffer(indexBuffer: Buffer | DynamicBuffer | null): Buffer | null {
374
  return indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
26!
375
}
376

377
function assertMatchingBufferLayouts(
378
  tableBufferLayout: BufferLayout[],
379
  explicitBufferLayout: BufferLayout[],
380
  candidateBufferLayout: BufferLayout[],
381
  errorMessage: string
382
): void {
383
  const expectedBufferLayout = [...explicitBufferLayout, ...tableBufferLayout];
5✔
384
  if (!deepEqualBufferLayouts(expectedBufferLayout, candidateBufferLayout)) {
5!
385
    throw new Error(errorMessage);
×
386
  }
387
}
388

389
function deepEqualBufferLayouts(
390
  expectedBufferLayout: BufferLayout[],
391
  candidateBufferLayout: BufferLayout[]
392
): boolean {
393
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
5✔
394
}
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