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

visgl / luma.gl / 28183428603

25 Jun 2026 04:01PM UTC coverage: 70.395%. First build
28183428603

Pull #2695

github

web-flow
Merge 35d924ad1 into 53f8e0f7b
Pull Request #2695: feat(tables): add GPU constant columns

9740 of 15682 branches covered (62.11%)

Branch coverage included in aggregate %.

245 of 321 new or added lines in 6 files covered. (76.32%)

19474 of 25818 relevant lines covered (75.43%)

4230.7 hits per line

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

84.15
/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 {GPUInputSchema} from './gpu-input-schema';
14
import {GPUTableShaderBindings} from './gpu-table-shader-bindings';
15
import type {GPURecordBatch} from '../table/gpu-record-batch';
16
import {GPU_TABLE_INDEX_COLUMN_NAME} from '../table/gpu-schema';
17
import {GPUTable} from '../table/gpu-table';
18
import {getGPUDataBuffersForLayout} from '../table/gpu-vector-utils';
19

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

23
/** Props for rendering one GPU table through a luma.gl Model. */
24
export type GPUTableModelProps = ModelProps & {
25
  /** GPU table supplying model-ready attributes, bindings, and preserved batches. */
26
  table?: GPUTable;
27
  /** Controls whether table rows infer `instanceCount`, `vertexCount`, or neither. */
28
  tableCount?: GPUTableModelCount;
29
  /** Optional logical-column contract used to resolve constants and renamed shader inputs. */
30
  gpuInputSchema?: GPUInputSchema;
31
};
32

33
export type GPUTableModelDrawBatchesOptions = {
34
  /** Called immediately before drawing each preserved GPU record batch. */
35
  onBatch?: (batch: GPURecordBatch, batchIndex: number) => void;
36
};
37

38
type GPUTableModelState = {
39
  explicitAttributes: NonNullable<ModelProps['attributes']>;
40
  explicitBindings: NonNullable<ModelProps['bindings']>;
41
  tableBindingNames: string[];
42
  explicitBufferLayout: BufferLayout[];
43
  explicitIndexBuffer: Buffer | DynamicBuffer | null;
44
  explicitIndexCount: number | undefined;
45
  explicitFirstVertex: number;
46
  explicitFirstIndex: number;
47
  explicitVertexCount: number;
48
  inferInstanceCount: boolean;
49
  inferVertexCount: boolean;
50
  gpuInputSchema?: GPUInputSchema;
51
  shaderLayout?: ModelProps['shaderLayout'];
52
};
53

54
type GPUTableModelConstructorState = {
55
  table?: GPUTable;
56
  modelProps: ModelProps;
57
  state: GPUTableModelState;
58
  shaderBindings?: GPUTableShaderBindings;
59
};
60

61
type GPUTableDrawSource = GPUTable | GPURecordBatch;
62

63
type GPUTableIndexDrawState = {
64
  indexBuffer: Buffer | DynamicBuffer;
65
  indexCount: number;
66
  firstVertex: number;
67
  firstIndex: number;
68
};
69

70
/**
71
 * A luma.gl Model whose GPU attributes and bindings are sourced from a `GPUTable`.
72
 *
73
 * The table stays caller-owned. The model rebinds preserved table batches on
74
 * demand and mirrors table row counts into draw counts when requested.
75
 */
76
export class GPUTableModel extends Model {
77
  /** Currently bound table, when table-backed rendering is active. */
78
  table?: GPUTable;
79
  private readonly tableState: GPUTableModelState;
80
  private tableShaderBindings?: GPUTableShaderBindings;
81
  private drawingTableBatches = false;
44✔
82

83
  /** Bytes owned by table-to-shader binding preparation, excluding caller-owned table buffers. */
84
  get tableBindingByteLength(): number {
85
    return this.tableShaderBindings?.ownedByteLength ?? 0;
1!
86
  }
87

88
  /** Creates a model whose table-backed attributes and bindings can be rebound by batch. */
89
  constructor(device: Device, props: GPUTableModelProps) {
90
    const {table, modelProps, state, shaderBindings} = getGPUTableModelConstructorState(
48✔
91
      device,
92
      props
93
    );
94
    super(device, modelProps);
44✔
95
    this.table = table;
44✔
96
    this.tableState = state;
44✔
97
    this.tableShaderBindings = shaderBindings;
44✔
98
    this.captureExplicitDrawState();
44✔
99
    if (table) {
44!
100
      this.setTableDrawState(table);
44✔
101
    }
102
  }
103

104
  override destroy(): void {
105
    this.tableShaderBindings?.destroy();
44✔
106
    this.tableShaderBindings = undefined;
44✔
107
    super.destroy();
44✔
108
  }
109

110
  /** Replaces the bound GPU table when one is supplied. */
111
  setProps(props: Partial<GPUTableModelProps>): void {
112
    if (props.table) {
2!
113
      this.setTable(props.table);
2✔
114
    }
115
  }
116

117
  /** Query redraw status after synchronizing inferred table draw state. */
118
  override needsRedraw(): false | string {
119
    this.syncTableDrawState();
5✔
120
    return super.needsRedraw();
5✔
121
  }
122

123
  /** Synchronizes inferred table draw state before opening a render pass. */
124
  override predraw(commandEncoder: CommandEncoder): void {
125
    this.syncTableDrawState();
10✔
126
    super.predraw(commandEncoder);
10✔
127
  }
128

129
  /**
130
   * Draws each preserved GPU record batch by rebinding batch-local buffers.
131
   *
132
   * The table-level attributes and bindings are restored before returning.
133
   */
134
  drawBatches(renderPass: RenderPass, options: GPUTableModelDrawBatchesOptions = {}): boolean {
8✔
135
    const table = this.table;
8✔
136
    if (!(table instanceof GPUTable)) {
8!
137
      throw new Error('GPUTableModel.drawBatches() requires a GPUTable');
×
138
    }
139

140
    const tableBufferLayout = this.tableShaderBindings?.bufferLayout ?? table.bufferLayout;
8✔
141
    assertMatchingBufferLayouts(
8✔
142
      tableBufferLayout,
143
      this.tableState.explicitBufferLayout,
144
      this.bufferLayout,
145
      'GPUTableModel.drawBatches() model buffer layout does not match its GPU table'
146
    );
147

148
    let drawSuccess = true;
8✔
149
    this.drawingTableBatches = true;
8✔
150
    try {
8✔
151
      for (const [batchIndex, batch] of table.batches.entries()) {
8✔
152
        const preparedBatch = this.tableShaderBindings?.batches[batchIndex];
11✔
153
        if (this.tableShaderBindings && !preparedBatch) {
11!
NEW
154
          throw new Error('GPUTableModel.drawBatches() is missing prepared batch bindings');
×
155
        }
156
        this.setAttributes({
11✔
157
          ...this.tableState.explicitAttributes,
158
          ...(preparedBatch?.attributes ?? getGPUTableDrawAttributes(batch))
18✔
159
        });
160
        this.setBindings({
11✔
161
          ...this.tableState.explicitBindings,
162
          ...(preparedBatch?.bindings ??
18✔
163
            getGPUTableDrawBindings(batch, this.tableState.tableBindingNames))
164
        });
165
        if (this.tableShaderBindings) {
11✔
166
          this.setConstantAttributes(this.tableShaderBindings.constantAttributes);
4✔
167
        }
168
        this.setTableDrawState(batch);
11✔
169
        options.onBatch?.(batch, batchIndex);
11✔
170
        drawSuccess = super.draw(renderPass) && drawSuccess;
11✔
171
      }
172
    } finally {
173
      this.drawingTableBatches = false;
8✔
174
      this.setAttributes({
8✔
175
        ...this.tableState.explicitAttributes,
176
        ...(this.tableShaderBindings?.batches[0]?.attributes ?? getGPUTableDrawAttributes(table))
12✔
177
      });
178
      this.setBindings({
8✔
179
        ...this.tableState.explicitBindings,
180
        ...(this.tableShaderBindings?.batches[0]?.bindings ??
12✔
181
          getGPUTableDrawBindings(table, this.tableState.tableBindingNames))
182
      });
183
      this.setTableDrawState(table);
8✔
184
    }
185

186
    return drawSuccess;
8✔
187
  }
188

189
  /** Rebinds the model to a replacement table while preserving explicit model state. */
190
  protected setTable(nextTable: GPUTable): void {
191
    if (this.tableState.gpuInputSchema) {
2!
NEW
192
      if (!this.tableState.shaderLayout) {
×
NEW
193
        throw new Error('GPUTableModel gpuInputSchema requires shaderLayout');
×
194
      }
NEW
195
      if (this.tableShaderBindings) {
×
NEW
196
        this.tableShaderBindings.updateBindings(nextTable);
×
197
      } else {
NEW
198
        this.tableShaderBindings = new GPUTableShaderBindings(this.device, {
×
199
          table: nextTable,
200
          gpuInputSchema: this.tableState.gpuInputSchema,
201
          shaderLayout: this.tableState.shaderLayout
202
        });
NEW
203
        if (this.tableShaderBindings.shaderModule) {
×
NEW
204
          this.tableShaderBindings.destroy();
×
NEW
205
          this.tableShaderBindings = undefined;
×
NEW
206
          throw new Error(
×
207
            'GPUTableModel storage gpuInputSchema requires a table during model construction'
208
          );
209
        }
210
      }
211
    }
212
    const tableBufferLayout = this.tableShaderBindings?.bufferLayout ?? nextTable.bufferLayout;
2✔
213
    const tableAttributes =
214
      this.tableShaderBindings?.batches[0]?.attributes ?? getGPUTableDrawAttributes(nextTable);
2✔
215
    const tableBindings =
216
      this.tableShaderBindings?.batches[0]?.bindings ??
2✔
217
      getGPUTableDrawBindings(nextTable, this.tableState.tableBindingNames);
218
    assertNoExplicitIndexBuffer(nextTable, this.tableState.explicitIndexBuffer);
2✔
219
    validateGPUTableIndexBatches(nextTable);
2✔
220
    assertNoDuplicateNames(
2✔
221
      Object.keys(this.tableState.explicitAttributes),
222
      getBufferLayoutNames(tableBufferLayout),
223
      'attribute'
224
    );
225
    assertNoDuplicateNames(
2✔
226
      getBufferLayoutNames(this.tableState.explicitBufferLayout),
227
      getBufferLayoutNames(tableBufferLayout),
228
      'buffer layout'
229
    );
230
    assertNoDuplicateNames(
2✔
231
      Object.keys(this.tableState.explicitBindings),
232
      Object.keys(tableBindings),
233
      'binding'
234
    );
235
    this.setBufferLayout([...this.tableState.explicitBufferLayout, ...tableBufferLayout]);
2✔
236
    this.setAttributes({
2✔
237
      ...this.tableState.explicitAttributes,
238
      ...tableAttributes
239
    });
240
    this.setBindings({
2✔
241
      ...this.tableState.explicitBindings,
242
      ...tableBindings
243
    });
244
    if (this.tableShaderBindings) {
2!
NEW
245
      this.setConstantAttributes(this.tableShaderBindings.constantAttributes);
×
246
    }
247
    this.setTableDrawState(nextTable);
2✔
248
    this.table = nextTable;
2✔
249
  }
250

251
  /** Disables table-backed draw state and restores any explicit index buffer. */
252
  protected clearTable(): void {
253
    this.table = undefined;
×
254
    this.restoreExplicitIndexDrawState();
×
255
  }
256

257
  private syncTableDrawState(): void {
258
    if (!this.table || this.drawingTableBatches) {
15✔
259
      return;
7✔
260
    }
261
    this.setTableDrawState(this.table);
8✔
262
  }
263

264
  private captureExplicitDrawState(): void {
265
    if (!this.tableState.explicitIndexBuffer && this.indexBuffer) {
44✔
266
      this.tableState.explicitIndexBuffer = this.indexBuffer;
1✔
267
    }
268
    if (this.tableState.explicitIndexCount === undefined && this.indexCount !== undefined) {
44!
269
      this.tableState.explicitIndexCount = this.indexCount;
×
270
    }
271
    if (this.tableState.explicitVertexCount === 0 && this.vertexCount > 0) {
44✔
272
      this.tableState.explicitVertexCount = this.vertexCount;
3✔
273
    }
274
  }
275

276
  private setTableDrawState(source: GPUTableDrawSource): void {
277
    this.setTableRowCount(source.numRows);
73✔
278
    const indexDrawState = getGPUTableIndexDrawState(source);
73✔
279
    if (indexDrawState) {
73✔
280
      this.setTableIndexDrawState(indexDrawState);
11✔
281
      return;
11✔
282
    }
283
    this.restoreExplicitIndexDrawState();
62✔
284
  }
285

286
  private setTableRowCount(rowCount: number): void {
287
    if (this.tableState.inferInstanceCount && this.instanceCount !== rowCount) {
73✔
288
      this.setInstanceCount(rowCount);
9✔
289
    }
290
    if (this.tableState.inferVertexCount && this.vertexCount !== rowCount) {
73!
291
      this.setVertexCount(rowCount);
×
292
    }
293
  }
294

295
  private setTableIndexDrawState({
296
    indexBuffer,
297
    indexCount,
298
    firstVertex,
299
    firstIndex
300
  }: GPUTableIndexDrawState): void {
301
    if (this.indexBuffer !== getConcreteIndexBuffer(indexBuffer)) {
11✔
302
      this.setIndexBuffer(indexBuffer);
9✔
303
    }
304
    if (this.indexCount !== indexCount) {
11✔
305
      this.setIndexCount(indexCount);
9✔
306
    }
307
    if (this.firstVertex !== firstVertex || this.firstIndex !== firstIndex) {
11✔
308
      this.setDrawOffsets({firstVertex, firstIndex});
1✔
309
    }
310
    if (this.vertexCount !== indexCount) {
11✔
311
      this.setVertexCount(indexCount);
9✔
312
    }
313
  }
314

315
  private restoreExplicitIndexDrawState(): void {
316
    if (this.indexBuffer !== getConcreteIndexBuffer(this.tableState.explicitIndexBuffer)) {
62✔
317
      this.setIndexBuffer(this.tableState.explicitIndexBuffer);
2✔
318
    }
319
    if (this.indexCount !== this.tableState.explicitIndexCount) {
62✔
320
      this.setIndexCount(this.tableState.explicitIndexCount);
2✔
321
    }
322
    if (
62!
323
      this.firstVertex !== this.tableState.explicitFirstVertex ||
124✔
324
      this.firstIndex !== this.tableState.explicitFirstIndex
325
    ) {
326
      this.setDrawOffsets({
×
327
        firstVertex: this.tableState.explicitFirstVertex,
328
        firstIndex: this.tableState.explicitFirstIndex
329
      });
330
    }
331
    if (
62✔
332
      !this.tableState.inferVertexCount &&
122✔
333
      this.vertexCount !== this.tableState.explicitVertexCount
334
    ) {
335
      this.setVertexCount(this.tableState.explicitVertexCount);
2✔
336
    }
337
  }
338
}
339

340
function getGPUTableModelConstructorState(
341
  device: Device,
342
  props: GPUTableModelProps
343
): GPUTableModelConstructorState {
344
  const {table, tableCount = 'instance', gpuInputSchema, ...modelProps} = props;
48✔
345
  const explicitAttributes = modelProps.attributes || {};
48✔
346
  const explicitBindings = modelProps.bindings || {};
48✔
347
  const tableBindingNames = (modelProps.shaderLayout?.bindings || [])
48!
348
    .filter(binding => binding.type === 'storage' || binding.type === 'read-only-storage')
13✔
349
    .map(binding => binding.name);
13✔
350
  const explicitBufferLayout = modelProps.bufferLayout || [];
48✔
351
  const explicitIndexBuffer = modelProps.indexBuffer ?? null;
48✔
352
  const explicitIndexCount = modelProps.indexCount;
48✔
353
  const explicitFirstVertex = modelProps.firstVertex ?? 0;
48✔
354
  const explicitFirstIndex = modelProps.firstIndex ?? 0;
48✔
355
  const explicitVertexCount = modelProps.vertexCount ?? 0;
48✔
356
  const inferInstanceCount =
357
    Boolean(table) && tableCount === 'instance' && modelProps.instanceCount === undefined;
48✔
358
  const inferVertexCount =
359
    Boolean(table) && tableCount === 'vertex' && modelProps.vertexCount === undefined;
48✔
360

361
  if (!table) {
48!
362
    return {
×
363
      table,
364
      modelProps,
365
      state: {
366
        explicitAttributes,
367
        explicitBindings,
368
        tableBindingNames,
369
        explicitBufferLayout,
370
        explicitIndexBuffer,
371
        explicitIndexCount,
372
        explicitFirstVertex,
373
        explicitFirstIndex,
374
        explicitVertexCount,
375
        inferInstanceCount,
376
        inferVertexCount,
377
        gpuInputSchema,
378
        shaderLayout: modelProps.shaderLayout
379
      }
380
    };
381
  }
382

383
  assertNoExplicitIndexBuffer(table, explicitIndexBuffer);
48✔
384
  validateGPUTableIndexBatches(table);
48✔
385

386
  let shaderBindings: GPUTableShaderBindings | undefined;
387
  if (gpuInputSchema) {
47✔
388
    if (!modelProps.shaderLayout) {
4!
NEW
389
      throw new Error('GPUTableModel gpuInputSchema requires shaderLayout');
×
390
    }
391
    shaderBindings = new GPUTableShaderBindings(device, {
4✔
392
      table,
393
      gpuInputSchema,
394
      shaderLayout: modelProps.shaderLayout
395
    });
396
  } else if (Object.keys(table.gpuConstants).length > 0) {
43!
NEW
397
    throw new Error('GPUTableModel constant columns require gpuInputSchema');
×
398
  }
399

400
  const tableBufferLayout = shaderBindings?.bufferLayout ?? table.bufferLayout;
47✔
401
  const tableAttributes =
402
    shaderBindings?.batches[0]?.attributes ?? getGPUTableDrawAttributes(table);
47✔
403
  const tableBindings =
404
    shaderBindings?.batches[0]?.bindings ?? getGPUTableDrawBindings(table, tableBindingNames);
47✔
405

406
  assertNoDuplicateNames(
47✔
407
    Object.keys(explicitAttributes),
408
    getBufferLayoutNames(tableBufferLayout),
409
    'attribute'
410
  );
411
  assertNoDuplicateNames(
45✔
412
    getBufferLayoutNames(explicitBufferLayout),
413
    getBufferLayoutNames(tableBufferLayout),
414
    'buffer layout'
415
  );
416
  assertNoDuplicateNames(Object.keys(explicitBindings), Object.keys(tableBindings), 'binding');
45✔
417

418
  return {
44✔
419
    table,
420
    shaderBindings,
421
    state: {
422
      explicitAttributes,
423
      explicitBindings,
424
      tableBindingNames,
425
      explicitBufferLayout,
426
      explicitIndexBuffer,
427
      explicitIndexCount,
428
      explicitFirstVertex,
429
      explicitFirstIndex,
430
      explicitVertexCount,
431
      inferInstanceCount,
432
      inferVertexCount,
433
      gpuInputSchema,
434
      shaderLayout: modelProps.shaderLayout
435
    },
436
    modelProps: {
437
      ...modelProps,
438
      modules: shaderBindings?.shaderModule
44✔
439
        ? [...(modelProps.modules ?? []), shaderBindings.shaderModule]
3✔
440
        : modelProps.modules,
441
      bufferLayout: [...explicitBufferLayout, ...tableBufferLayout],
442
      attributes: {...explicitAttributes, ...tableAttributes},
443
      constantAttributes: shaderBindings?.constantAttributes,
444
      bindings: {
445
        ...explicitBindings,
446
        ...tableBindings
447
      },
448
      ...(inferInstanceCount ? {instanceCount: table.numRows} : {}),
44✔
449
      ...(inferVertexCount ? {vertexCount: table.numRows} : {})
44✔
450
    }
451
  };
452
}
453

454
function getGPUTableDrawBindings(
455
  source: GPUTableDrawSource,
456
  bindingNames: string[]
457
): Record<string, Buffer | DynamicBuffer> {
458
  const batch = source instanceof GPUTable ? source.batches[0] : source;
56✔
459
  if (!batch) {
56!
460
    return {};
×
461
  }
462
  const bindings: Record<string, Buffer | DynamicBuffer> = {};
56✔
463
  for (const bindingName of bindingNames) {
56✔
464
    const data = batch.gpuData[bindingName];
6✔
465
    if (data) {
6✔
466
      bindings[bindingName] = data.buffer;
3✔
467
    }
468
  }
469
  return bindings;
56✔
470
}
471

472
function getGPUTableDrawAttributes(
473
  source: GPUTableDrawSource
474
): Record<string, Buffer | DynamicBuffer> {
475
  const attributeSource = source instanceof GPUTable ? source.batches[0] : source;
56✔
476
  if (!attributeSource) {
56!
477
    return {};
×
478
  }
479
  return getGPUDataBuffersForLayout(attributeSource.bufferLayout, attributeSource.gpuData);
56✔
480
}
481

482
function getBufferLayoutNames(bufferLayout: BufferLayout[]): string[] {
483
  return bufferLayout.map(layout => layout.name);
145✔
484
}
485

486
function assertNoDuplicateNames(
487
  explicitNames: string[],
488
  tableNames: string[],
489
  nameType: string
490
): void {
491
  const explicitNameSet = new Set(explicitNames);
143✔
492
  for (const tableName of tableNames) {
143✔
493
    if (explicitNameSet.has(tableName)) {
135✔
494
      throw new Error(
3✔
495
        `GPUTableModel ${nameType} "${tableName}" duplicates an explicit ${nameType}`
496
      );
497
    }
498
  }
499
}
500

501
function assertNoExplicitIndexBuffer(
502
  table: Pick<GPUTable, 'gpuVectors'>,
503
  explicitIndexBuffer: Buffer | DynamicBuffer | null
504
): void {
505
  if (explicitIndexBuffer && table.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]) {
50!
506
    throw new Error('GPUTableModel indices column duplicates an explicit indexBuffer');
×
507
  }
508
}
509

510
function validateGPUTableIndexBatches(table: GPUTable): void {
511
  const tableHasIndices = Boolean(table.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]);
50✔
512
  for (const batch of table.batches) {
50✔
513
    const batchHasIndices = Boolean(batch.gpuData[GPU_TABLE_INDEX_COLUMN_NAME]);
55✔
514
    if (batchHasIndices !== tableHasIndices) {
55!
515
      throw new Error('GPUTableModel indexed tables require every batch to include indices');
×
516
    }
517
    getGPUTableIndexDrawState(batch);
55✔
518
  }
519
}
520

521
function getGPUTableIndexDrawState(source: GPUTableDrawSource): GPUTableIndexDrawState | null {
522
  const indexData =
523
    source instanceof GPUTable
128✔
524
      ? source.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME]?.data[0]
525
      : source.gpuData[GPU_TABLE_INDEX_COLUMN_NAME];
526
  if (!indexData) {
128✔
527
    return null;
104✔
528
  }
529
  if (source instanceof GPUTable && source.batches.length > 1) {
24✔
530
    return null;
3✔
531
  }
532
  if (indexData.format !== 'vertex-list<uint32>') {
21!
533
    throw new Error('GPUTableModel indices column requires vertex-list<uint32> format');
×
534
  }
535

536
  if (
21!
537
    source instanceof GPUTable &&
30✔
538
    source.gpuVectors[GPU_TABLE_INDEX_COLUMN_NAME].data.length !== 1
539
  ) {
540
    throw new Error('GPUTableModel indices column requires exactly one GPUData chunk');
×
541
  }
542
  const indexBuffer = indexData.buffer;
21✔
543
  const concreteIndexBuffer = getConcreteIndexBuffer(indexBuffer);
21✔
544
  if (!concreteIndexBuffer || !(concreteIndexBuffer.usage & Buffer.INDEX)) {
21✔
545
    throw new Error('GPUTableModel indices column requires Buffer.INDEX usage');
1✔
546
  }
547
  const indexByteStride = concreteIndexBuffer.indexType === 'uint32' ? 4 : 2;
20!
548
  if (indexData.byteOffset % indexByteStride !== 0) {
20!
549
    throw new Error('GPUTableModel indices column byteOffset must align with its index type');
×
550
  }
551
  return {
20✔
552
    indexBuffer,
553
    indexCount: indexData.valueLength,
554
    firstVertex: indexData.byteOffset,
555
    firstIndex: indexData.byteOffset / indexByteStride
556
  };
557
}
558

559
function getConcreteIndexBuffer(indexBuffer: Buffer | DynamicBuffer | null): Buffer | null {
560
  return indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
94!
561
}
562

563
function assertMatchingBufferLayouts(
564
  tableBufferLayout: BufferLayout[],
565
  explicitBufferLayout: BufferLayout[],
566
  candidateBufferLayout: BufferLayout[],
567
  errorMessage: string
568
): void {
569
  const expectedBufferLayout = [...explicitBufferLayout, ...tableBufferLayout];
8✔
570
  if (!deepEqualBufferLayouts(expectedBufferLayout, candidateBufferLayout)) {
8!
571
    throw new Error(errorMessage);
×
572
  }
573
}
574

575
function deepEqualBufferLayouts(
576
  expectedBufferLayout: BufferLayout[],
577
  candidateBufferLayout: BufferLayout[]
578
): boolean {
579
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
8✔
580
}
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