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

visgl / luma.gl / 26365832078

24 May 2026 03:53PM UTC coverage: 74.573% (+0.06%) from 74.51%
26365832078

push

github

web-flow
chore: Simplify arrow examples (#2636)

8463 of 12834 branches covered (65.94%)

Branch coverage included in aggregate %.

281 of 357 new or added lines in 3 files covered. (78.71%)

733 existing lines in 26 files now uncovered.

17709 of 22262 relevant lines covered (79.55%)

4350.11 hits per line

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

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

5
import {Device, type BufferLayout, type CommandEncoder, type RenderPass} from '@luma.gl/core';
6
import {Model, type ModelProps} from '@luma.gl/engine';
7
import {GPUTable} from './gpu-table';
8

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

12
/** Props for rendering one GPU table through a luma.gl Model. */
13
export type GPUTableModelProps = ModelProps & {
14
  /** GPU table supplying model-ready attributes, bindings, and preserved batches. */
15
  table?: GPUTable;
16
  /** Controls whether table rows infer `instanceCount`, `vertexCount`, or neither. */
17
  tableCount?: GPUTableModelCount;
18
};
19

20
type GPUTableModelState = {
21
  explicitAttributes: NonNullable<ModelProps['attributes']>;
22
  explicitBindings: NonNullable<ModelProps['bindings']>;
23
  explicitBufferLayout: BufferLayout[];
24
  inferInstanceCount: boolean;
25
  inferVertexCount: boolean;
26
};
27

28
type GPUTableModelConstructorState = {
29
  table?: GPUTable;
30
  modelProps: ModelProps;
31
  state: GPUTableModelState;
32
};
33

34
/**
35
 * A luma.gl Model whose GPU attributes and bindings are sourced from a `GPUTable`.
36
 *
37
 * The table stays caller-owned. The model rebinds preserved table batches on
38
 * demand and mirrors table row counts into draw counts when requested.
39
 */
40
export class GPUTableModel extends Model {
41
  /** Currently bound table, when table-backed rendering is active. */
42
  table?: GPUTable;
43
  private readonly tableState: GPUTableModelState;
44
  private drawingTableBatches = false;
18✔
45

46
  /** Creates a model whose table-backed attributes and bindings can be rebound by batch. */
47
  constructor(device: Device, props: GPUTableModelProps) {
48
    const {table, modelProps, state} = getGPUTableModelConstructorState(props);
20✔
49
    super(device, modelProps);
20✔
50
    this.table = table;
20✔
51
    this.tableState = state;
20✔
52
  }
53

54
  /** Replaces the bound GPU table when one is supplied. */
55
  setProps(props: Partial<GPUTableModelProps>): void {
56
    if (props.table) {
×
UNCOV
57
      this.setTable(props.table);
×
58
    }
59
  }
60

61
  /** Query redraw status after synchronizing inferred table row counts. */
62
  override needsRedraw(): false | string {
63
    this.syncTableCount();
2✔
64
    return super.needsRedraw();
2✔
65
  }
66

67
  /** Synchronizes inferred table row counts before opening a render pass. */
68
  override predraw(commandEncoder: CommandEncoder): void {
69
    this.syncTableCount();
3✔
70
    super.predraw(commandEncoder);
3✔
71
  }
72

73
  /**
74
   * Draws each preserved GPU record batch by rebinding batch-local buffers.
75
   *
76
   * The table-level attributes and bindings are restored before returning.
77
   */
78
  drawBatches(renderPass: RenderPass): boolean {
79
    const table = this.table;
2✔
80
    if (!(table instanceof GPUTable)) {
2!
UNCOV
81
      throw new Error('GPUTableModel.drawBatches() requires a GPUTable');
×
82
    }
83

84
    assertMatchingBufferLayouts(
2✔
85
      table.bufferLayout,
86
      this.tableState.explicitBufferLayout,
87
      this.bufferLayout,
88
      'GPUTableModel.drawBatches() model buffer layout does not match its GPU table'
89
    );
90

91
    let drawSuccess = true;
2✔
92
    this.drawingTableBatches = true;
2✔
93
    try {
2✔
94
      for (const batch of table.batches) {
2✔
95
        assertMatchingBufferLayouts(
3✔
96
          table.bufferLayout,
97
          [],
98
          batch.bufferLayout,
99
          'GPUTableModel.drawBatches() requires every batch to use the table buffer layout'
100
        );
101
        this.setAttributes({
3✔
102
          ...this.tableState.explicitAttributes,
103
          ...batch.attributes
104
        });
105
        this.setBindings({
3✔
106
          ...this.tableState.explicitBindings,
107
          ...batch.bindings
108
        });
109
        this.setTableRowCount(batch.numRows);
3✔
110
        drawSuccess = super.draw(renderPass) && drawSuccess;
3✔
111
      }
112
    } finally {
113
      this.drawingTableBatches = false;
2✔
114
      this.setAttributes({
2✔
115
        ...this.tableState.explicitAttributes,
116
        ...table.attributes
117
      });
118
      this.setBindings({
2✔
119
        ...this.tableState.explicitBindings,
120
        ...table.bindings
121
      });
122
      this.setTableRowCount(table.numRows);
2✔
123
    }
124

125
    return drawSuccess;
2✔
126
  }
127

128
  /** Rebinds the model to a replacement table while preserving explicit model state. */
129
  protected setTable(nextTable: GPUTable): void {
130
    assertNoDuplicateNames(
2✔
131
      Object.keys(this.tableState.explicitAttributes),
132
      Object.keys(nextTable.attributes),
133
      'attribute'
134
    );
135
    assertNoDuplicateNames(
2✔
136
      getBufferLayoutNames(this.tableState.explicitBufferLayout),
137
      getBufferLayoutNames(nextTable.bufferLayout),
138
      'buffer layout'
139
    );
140
    assertNoDuplicateNames(
2✔
141
      Object.keys(this.tableState.explicitBindings),
142
      Object.keys(nextTable.bindings),
143
      'binding'
144
    );
145

146
    this.setBufferLayout([...this.tableState.explicitBufferLayout, ...nextTable.bufferLayout]);
2✔
147
    this.setAttributes({
2✔
148
      ...this.tableState.explicitAttributes,
149
      ...nextTable.attributes
150
    });
151
    this.setBindings({
2✔
152
      ...this.tableState.explicitBindings,
153
      ...nextTable.bindings
154
    });
155
    this.setTableRowCount(nextTable.numRows);
2✔
156
    this.table = nextTable;
2✔
157
  }
158

159
  /** Disables table-backed count synchronization without disturbing current model buffers. */
160
  protected clearTable(): void {
UNCOV
161
    this.table = undefined;
×
162
  }
163

164
  private syncTableCount(): void {
165
    if (!this.table || this.drawingTableBatches) {
5✔
166
      return;
3✔
167
    }
168
    if (this.tableState.inferInstanceCount && this.instanceCount !== this.table.numRows) {
2✔
169
      this.setTableRowCount(this.table.numRows);
1✔
170
    }
171
    if (this.tableState.inferVertexCount && this.vertexCount !== this.table.numRows) {
2!
UNCOV
172
      this.setTableRowCount(this.table.numRows);
×
173
    }
174
  }
175

176
  private setTableRowCount(rowCount: number): void {
177
    if (this.tableState.inferInstanceCount) {
8!
178
      this.setInstanceCount(rowCount);
8✔
179
    }
180
    if (this.tableState.inferVertexCount) {
8!
UNCOV
181
      this.setVertexCount(rowCount);
×
182
    }
183
  }
184
}
185

186
function getGPUTableModelConstructorState(
187
  props: GPUTableModelProps
188
): GPUTableModelConstructorState {
189
  const {table, tableCount = 'instance', ...modelProps} = props;
20✔
190
  const explicitAttributes = modelProps.attributes || {};
20✔
191
  const explicitBindings = modelProps.bindings || {};
20✔
192
  const explicitBufferLayout = modelProps.bufferLayout || [];
20✔
193
  const inferInstanceCount =
194
    Boolean(table) && tableCount === 'instance' && modelProps.instanceCount === undefined;
20✔
195
  const inferVertexCount =
196
    Boolean(table) && tableCount === 'vertex' && modelProps.vertexCount === undefined;
20✔
197

198
  if (!table) {
20✔
199
    return {
1✔
200
      table,
201
      modelProps,
202
      state: {
203
        explicitAttributes,
204
        explicitBindings,
205
        explicitBufferLayout,
206
        inferInstanceCount,
207
        inferVertexCount
208
      }
209
    };
210
  }
211

212
  assertNoDuplicateNames(
19✔
213
    Object.keys(explicitAttributes),
214
    Object.keys(table.attributes),
215
    'attribute'
216
  );
217
  assertNoDuplicateNames(
19✔
218
    getBufferLayoutNames(explicitBufferLayout),
219
    getBufferLayoutNames(table.bufferLayout),
220
    'buffer layout'
221
  );
222
  assertNoDuplicateNames(Object.keys(explicitBindings), Object.keys(table.bindings), 'binding');
19✔
223

224
  return {
19✔
225
    table,
226
    state: {
227
      explicitAttributes,
228
      explicitBindings,
229
      explicitBufferLayout,
230
      inferInstanceCount,
231
      inferVertexCount
232
    },
233
    modelProps: {
234
      ...modelProps,
235
      bufferLayout: [...explicitBufferLayout, ...table.bufferLayout],
236
      attributes: {...explicitAttributes, ...table.attributes},
237
      bindings: {...explicitBindings, ...table.bindings},
238
      ...(inferInstanceCount ? {instanceCount: table.numRows} : {}),
17✔
239
      ...(inferVertexCount ? {vertexCount: table.numRows} : {})
17✔
240
    }
241
  };
242
}
243

244
function getBufferLayoutNames(bufferLayout: BufferLayout[]): string[] {
245
  return bufferLayout.map(layout => layout.name);
40✔
246
}
247

248
function assertNoDuplicateNames(
249
  explicitNames: string[],
250
  tableNames: string[],
251
  nameType: string
252
): void {
253
  const explicitNameSet = new Set(explicitNames);
61✔
254
  for (const tableName of tableNames) {
61✔
255
    if (explicitNameSet.has(tableName)) {
55✔
256
      throw new Error(
2✔
257
        `GPUTableModel ${nameType} "${tableName}" duplicates an explicit ${nameType}`
258
      );
259
    }
260
  }
261
}
262

263
function assertMatchingBufferLayouts(
264
  tableBufferLayout: BufferLayout[],
265
  explicitBufferLayout: BufferLayout[],
266
  candidateBufferLayout: BufferLayout[],
267
  errorMessage: string
268
): void {
269
  const expectedBufferLayout = [...explicitBufferLayout, ...tableBufferLayout];
5✔
270
  if (!deepEqualBufferLayouts(expectedBufferLayout, candidateBufferLayout)) {
5!
UNCOV
271
    throw new Error(errorMessage);
×
272
  }
273
}
274

275
function deepEqualBufferLayouts(
276
  expectedBufferLayout: BufferLayout[],
277
  candidateBufferLayout: BufferLayout[]
278
): boolean {
279
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
5✔
280
}
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