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

visgl / luma.gl / 26033117099

18 May 2026 12:20PM UTC coverage: 74.769% (-0.1%) from 74.887%
26033117099

push

github

web-flow
feat(arrow) Streaming ArrowTextLayer (#2620)

6882 of 10396 branches covered (66.2%)

Branch coverage included in aggregate %.

194 of 250 new or added lines in 9 files covered. (77.6%)

13 existing lines in 7 files now uncovered.

15038 of 18921 relevant lines covered (79.48%)

914.25 hits per line

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

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

5
import {
6
  Device,
7
  type BufferLayout,
8
  type CommandEncoder,
9
  type RenderPass,
10
  type ShaderLayout
11
} from '@luma.gl/core';
12
import {Model, type ModelProps} from '@luma.gl/engine';
13
import * as arrow from 'apache-arrow';
14
import {type ArrowVertexFormatOptions} from './arrow-shader-layout';
15
import {GPUTable, type GPUTableProps} from './plain-gpu-table';
16
import type {GPUVectorProps} from './arrow-gpu-vector';
17
import {ArrowGeometry, type ArrowGeometryProps} from './arrow-geometry';
18
import type {ArrowMeshTable} from './arrow-mesh-types';
19

20
/** GPU table source accepted by ArrowModel. */
21
export type ArrowModelGPUTable = GPUTable;
22

23
/** Props for creating a Model whose attributes are derived from an Arrow table. */
24
export type ArrowModelProps = ModelProps &
25
  ArrowVertexFormatOptions & {
26
    /**
27
     * Mesh Arrow table used as the construction source for GPU geometry buffers.
28
     *
29
     * Mesh input is converted through {@link ArrowGeometry}. It is mutually
30
     * exclusive with `arrowTable`, `arrowGPUTable`, and `geometry`.
31
     */
32
    arrowMesh?: ArrowMeshTable | arrow.Table;
33
    /** Options applied when converting Mesh Arrow input into GPU geometry. */
34
    arrowMeshOptions?: Omit<ArrowGeometryProps, 'arrowMesh'>;
35
    /** Arrow table used as the construction source for GPU attribute buffers. */
36
    arrowTable?: arrow.Table;
37
    /** Existing non-streaming GPU table used as the source for model attributes. */
38
    arrowGPUTable?: GPUTable;
39
    /** Maps shader attribute names to Arrow column paths. Defaults to using attribute names. */
40
    arrowPaths?: Record<string, string>;
41
    /** Buffer props applied to each Arrow-derived GPU vector. */
42
    arrowBufferProps?: GPUVectorProps;
43
    /** Controls whether row count is assigned to instanceCount, vertexCount, or neither. */
44
    arrowCount?: 'instance' | 'vertex' | 'none';
45
  };
46

47
type ArrowModelState = {
48
  arrowGPUTable?: ArrowModelGPUTable;
49
  ownsArrowGPUTable: boolean;
50
  arrowGeometry?: ArrowGeometry;
51
  modelProps: ModelProps;
52
  arrowState: ArrowModelArrowState;
53
};
54

55
type ArrowModelExplicitAttributes = NonNullable<ModelProps['attributes']>;
56
type ArrowModelExplicitBindings = NonNullable<ModelProps['bindings']>;
57

58
type ArrowModelArrowState = {
59
  shaderLayout: ShaderLayout;
60
  arrowPaths?: Record<string, string>;
61
  arrowMeshOptions?: Omit<ArrowGeometryProps, 'arrowMesh'>;
62
  arrowBufferProps?: GPUVectorProps;
63
  arrowCount: 'instance' | 'vertex' | 'none';
64
  allowWebGLOnlyFormats?: boolean;
65
  explicitAttributes: ArrowModelExplicitAttributes;
66
  explicitBindings: ArrowModelExplicitBindings;
67
  explicitBufferLayout: BufferLayout[];
68
  inferInstanceCount: boolean;
69
  inferVertexCount: boolean;
70
};
71

72
/** A luma.gl Model with GPU attributes backed by Arrow table columns. */
73
export class ArrowModel extends Model {
74
  /** GPU representation of the currently active Arrow table attributes. */
75
  arrowGPUTable?: ArrowModelGPUTable;
76
  /** GPU representation of the currently active Mesh Arrow geometry. */
77
  arrowGeometry?: ArrowGeometry;
78
  private arrowState: ArrowModelArrowState;
79
  private ownsArrowGPUTable: boolean;
80
  private arrowModelDestroyed = false;
15✔
81
  private drawingArrowBatches = false;
15✔
82

83
  constructor(device: Device, props: ArrowModelProps) {
84
    const {arrowGPUTable, ownsArrowGPUTable, arrowGeometry, modelProps, arrowState} =
85
      getArrowModelState(device, props);
19✔
86
    try {
19✔
87
      super(device, modelProps);
19✔
88
    } catch (error) {
89
      if (ownsArrowGPUTable) {
×
90
        arrowGPUTable?.destroy();
×
91
      }
92
      arrowGeometry?.destroy();
×
93
      throw error;
×
94
    }
95
    this.arrowGPUTable = arrowGPUTable;
15✔
96
    this.arrowGeometry = arrowGeometry;
15✔
97
    this.ownsArrowGPUTable = ownsArrowGPUTable;
15✔
98
    this.arrowState = arrowState;
15✔
99
  }
100

101
  /** Updates the model when a replacement Arrow table is supplied. */
102
  setProps(props: Partial<ArrowModelProps>): void {
103
    if (props.arrowMesh) {
2!
104
      this.setArrowMesh(props.arrowMesh, props.arrowMeshOptions);
×
105
    }
106
    if (props.arrowTable) {
2!
107
      this.setArrowTable(props.arrowTable);
2✔
108
    }
109
    if (props.arrowGPUTable) {
2!
110
      this.setArrowGPUTable(props.arrowGPUTable, false);
×
111
    }
112
  }
113

114
  /** Query redraw status. Clears the status. */
115
  override needsRedraw(): false | string {
116
    this.syncArrowGPUTableCount();
2✔
117
    return super.needsRedraw();
2✔
118
  }
119

120
  /** Updates uniforms, dynamic buffers, and inferred Arrow counts before opening a render pass. */
121
  override predraw(commandEncoder: CommandEncoder): void {
122
    this.syncArrowGPUTableCount();
3✔
123
    super.predraw(commandEncoder);
3✔
124
  }
125

126
  /**
127
   * Draws each preserved Arrow GPU record batch through the model's existing pipeline.
128
   *
129
   * Batch drawing reuses the current buffer layout and only swaps batch attribute buffers
130
   * plus inferred Arrow row counts between draw calls.
131
   */
132
  drawBatches(renderPass: RenderPass): boolean {
133
    const arrowGPUTable = this.arrowGPUTable;
2✔
134
    if (!(arrowGPUTable instanceof GPUTable)) {
2!
135
      throw new Error('ArrowModel.drawBatches() requires a GPUTable');
×
136
    }
137

138
    assertMatchingBufferLayouts(
2✔
139
      arrowGPUTable.bufferLayout,
140
      this.arrowState.explicitBufferLayout,
141
      this.bufferLayout,
142
      'ArrowModel.drawBatches() model buffer layout does not match its Arrow table'
143
    );
144

145
    let drawSuccess = true;
2✔
146
    this.drawingArrowBatches = true;
2✔
147
    try {
2✔
148
      for (const batch of arrowGPUTable.batches) {
2✔
149
        assertMatchingBufferLayouts(
3✔
150
          arrowGPUTable.bufferLayout,
151
          [],
152
          batch.bufferLayout,
153
          'ArrowModel.drawBatches() requires every Arrow batch to use the table buffer layout'
154
        );
155
        this.setAttributes({
3✔
156
          ...this.arrowState.explicitAttributes,
157
          ...batch.attributes
158
        });
159
        this.setBindings({
3✔
160
          ...this.arrowState.explicitBindings,
161
          ...batch.bindings
162
        });
163
        this.setArrowRowCount(batch.numRows);
3✔
164
        drawSuccess = super.draw(renderPass) && drawSuccess;
3✔
165
      }
166
    } finally {
167
      this.drawingArrowBatches = false;
2✔
168
      this.setAttributes({
2✔
169
        ...this.arrowState.explicitAttributes,
170
        ...arrowGPUTable.attributes
171
      });
172
      this.setBindings({
2✔
173
        ...this.arrowState.explicitBindings,
174
        ...arrowGPUTable.bindings
175
      });
176
      this.setArrowRowCount(arrowGPUTable.numRows);
2✔
177
    }
178

179
    return drawSuccess;
2✔
180
  }
181

182
  override destroy(): void {
183
    if (!this.arrowModelDestroyed) {
15!
184
      super.destroy();
15✔
185
      if (this.ownsArrowGPUTable) {
15✔
186
        this.arrowGPUTable?.destroy();
12✔
187
      }
188
      this.arrowModelDestroyed = true;
15✔
189
    }
190
  }
191

192
  private setArrowMesh(
193
    arrowMesh: ArrowMeshTable | arrow.Table,
194
    arrowMeshOptions?: Omit<ArrowGeometryProps, 'arrowMesh'>
195
  ): void {
196
    const arrowGeometry = new ArrowGeometry(this.device, {
×
197
      arrowMesh,
198
      ...this.arrowState.arrowMeshOptions,
199
      ...arrowMeshOptions
200
    });
201

202
    try {
×
203
      this.setGeometry(arrowGeometry);
×
204
    } catch (error) {
205
      arrowGeometry.destroy();
×
206
      throw error;
×
207
    }
208

209
    if (this.ownsArrowGPUTable) {
×
210
      this.arrowGPUTable?.destroy();
×
211
    }
212
    this.arrowGPUTable = undefined;
×
213
    this.arrowGeometry = arrowGeometry;
×
214
    this.ownsArrowGPUTable = false;
×
215
  }
216

217
  private setArrowTable(arrowTable: arrow.Table): void {
218
    const nextArrowGPUTable = new GPUTable(this.device, arrowTable, {
2✔
219
      shaderLayout: this.arrowState.shaderLayout,
220
      arrowPaths: this.arrowState.arrowPaths,
221
      bufferProps: this.arrowState.arrowBufferProps,
222
      allowWebGLOnlyFormats: this.arrowState.allowWebGLOnlyFormats
223
    });
224

225
    this.setArrowGPUTable(nextArrowGPUTable, true);
2✔
226
  }
227

228
  private setArrowGPUTable(
229
    nextArrowGPUTable: ArrowModelGPUTable,
230
    ownsNextArrowGPUTable: boolean
231
  ): void {
232
    try {
2✔
233
      assertNoDuplicateNames(
2✔
234
        Object.keys(this.arrowState.explicitAttributes),
235
        Object.keys(nextArrowGPUTable.attributes),
236
        'attribute'
237
      );
238
      assertNoDuplicateNames(
2✔
239
        getBufferLayoutNames(this.arrowState.explicitBufferLayout),
240
        getBufferLayoutNames(nextArrowGPUTable.bufferLayout),
241
        'buffer layout'
242
      );
243
      assertNoDuplicateNames(
2✔
244
        Object.keys(this.arrowState.explicitBindings),
245
        Object.keys(nextArrowGPUTable.bindings),
246
        'binding'
247
      );
248

249
      this.setBufferLayout([
2✔
250
        ...this.arrowState.explicitBufferLayout,
251
        ...nextArrowGPUTable.bufferLayout
252
      ]);
253
      this.setAttributes({
2✔
254
        ...this.arrowState.explicitAttributes,
255
        ...nextArrowGPUTable.attributes
256
      });
257
      this.setBindings({
2✔
258
        ...this.arrowState.explicitBindings,
259
        ...nextArrowGPUTable.bindings
260
      });
261

262
      if (this.arrowState.inferInstanceCount) {
2!
263
        this.setArrowRowCount(nextArrowGPUTable.numRows);
2✔
264
      }
265
      if (this.arrowState.inferVertexCount) {
2!
266
        this.setArrowRowCount(nextArrowGPUTable.numRows);
×
267
      }
268
    } catch (error) {
269
      if (ownsNextArrowGPUTable) {
×
270
        nextArrowGPUTable.destroy();
×
271
      }
272
      throw error;
×
273
    }
274

275
    const previousArrowGPUTable = this.arrowGPUTable;
2✔
276
    const ownsPreviousArrowGPUTable = this.ownsArrowGPUTable;
2✔
277
    this.arrowGPUTable = nextArrowGPUTable;
2✔
278
    this.ownsArrowGPUTable = ownsNextArrowGPUTable;
2✔
279
    if (ownsPreviousArrowGPUTable) {
2!
280
      previousArrowGPUTable?.destroy();
2✔
281
    }
282
  }
283

284
  private syncArrowGPUTableCount(): void {
285
    if (!this.arrowGPUTable || this.drawingArrowBatches) {
5✔
286
      return;
3✔
287
    }
288
    if (this.arrowState.inferInstanceCount && this.instanceCount !== this.arrowGPUTable.numRows) {
2✔
289
      this.setArrowRowCount(this.arrowGPUTable.numRows);
1✔
290
    }
291
    if (this.arrowState.inferVertexCount && this.vertexCount !== this.arrowGPUTable.numRows) {
2!
292
      this.setArrowRowCount(this.arrowGPUTable.numRows);
×
293
    }
294
  }
295

296
  private setArrowRowCount(rowCount: number): void {
297
    if (this.arrowState.inferInstanceCount) {
8!
298
      this.setInstanceCount(rowCount);
8✔
299
    }
300
    if (this.arrowState.inferVertexCount) {
8!
301
      this.setVertexCount(rowCount);
×
302
    }
303
  }
304
}
305

306
function getArrowModelState(device: Device, props: ArrowModelProps): ArrowModelState {
307
  const {
308
    arrowMesh,
309
    arrowMeshOptions,
310
    arrowTable,
311
    arrowGPUTable: explicitArrowGPUTable,
312
    arrowPaths,
313
    arrowBufferProps,
314
    arrowCount = 'instance',
19✔
315
    allowWebGLOnlyFormats,
316
    ...modelProps
317
  } = props;
19✔
318

319
  validateArrowModelSources({
19✔
320
    arrowMesh,
321
    arrowTable,
322
    arrowGPUTable: explicitArrowGPUTable
323
  });
324

325
  if (!modelProps.shaderLayout) {
19✔
326
    throw new Error('ArrowModel requires shaderLayout');
1✔
327
  }
328
  if (arrowMesh && modelProps.geometry) {
17!
329
    throw new Error('ArrowModel requires only one of arrowMesh or geometry');
×
330
  }
331

332
  const explicitAttributes = modelProps.attributes || {};
17✔
333
  const explicitBindings = modelProps.bindings || {};
19✔
334
  const explicitBufferLayout = modelProps.bufferLayout || [];
19✔
335
  const inferInstanceCount =
336
    !arrowMesh && arrowCount === 'instance' && modelProps.instanceCount === undefined;
19✔
337
  const inferVertexCount =
338
    !arrowMesh && arrowCount === 'vertex' && modelProps.vertexCount === undefined;
19✔
339

340
  if (arrowMesh) {
19✔
341
    const arrowGeometry = new ArrowGeometry(device, {arrowMesh, ...arrowMeshOptions});
1✔
342
    return {
1✔
343
      arrowGPUTable: undefined,
344
      ownsArrowGPUTable: false,
345
      arrowGeometry,
346
      arrowState: {
347
        shaderLayout: modelProps.shaderLayout,
348
        arrowPaths,
349
        arrowMeshOptions,
350
        arrowBufferProps,
351
        arrowCount,
352
        allowWebGLOnlyFormats,
353
        explicitAttributes,
354
        explicitBindings,
355
        explicitBufferLayout,
356
        inferInstanceCount,
357
        inferVertexCount
358
      },
359
      modelProps: {
360
        ...modelProps,
361
        geometry: arrowGeometry
362
      }
363
    };
364
  }
365

366
  const {arrowGPUTable, ownsArrowGPUTable} = getInitialArrowGPUTable({
16✔
367
    device,
368
    arrowTable,
369
    arrowGPUTable: explicitArrowGPUTable,
370
    shaderLayout: modelProps.shaderLayout,
371
    arrowPaths,
372
    arrowBufferProps,
373
    allowWebGLOnlyFormats
374
  });
375

376
  try {
16✔
377
    assertNoDuplicateNames(
16✔
378
      Object.keys(explicitAttributes),
379
      Object.keys(arrowGPUTable.attributes),
380
      'attribute'
381
    );
382
    assertNoDuplicateNames(
16✔
383
      getBufferLayoutNames(explicitBufferLayout),
384
      getBufferLayoutNames(arrowGPUTable.bufferLayout),
385
      'buffer layout'
386
    );
387
    assertNoDuplicateNames(
16✔
388
      Object.keys(explicitBindings),
389
      Object.keys(arrowGPUTable.bindings),
390
      'binding'
391
    );
392
  } catch (error) {
393
    arrowGPUTable.destroy();
2✔
394
    throw error;
2✔
395
  }
396

397
  return {
14✔
398
    arrowGPUTable,
399
    ownsArrowGPUTable,
400
    arrowGeometry: undefined,
401
    arrowState: {
402
      shaderLayout: modelProps.shaderLayout,
403
      arrowPaths,
404
      arrowMeshOptions,
405
      arrowBufferProps,
406
      arrowCount,
407
      allowWebGLOnlyFormats,
408
      explicitAttributes,
409
      explicitBindings,
410
      explicitBufferLayout,
411
      inferInstanceCount,
412
      inferVertexCount
413
    },
414
    modelProps: {
415
      ...modelProps,
416
      bufferLayout: [...explicitBufferLayout, ...arrowGPUTable.bufferLayout],
417
      attributes: {...explicitAttributes, ...arrowGPUTable.attributes},
418
      bindings: {...explicitBindings, ...arrowGPUTable.bindings},
419
      ...(inferInstanceCount ? {instanceCount: arrowGPUTable.numRows} : {}),
14✔
420
      ...(inferVertexCount ? {vertexCount: arrowGPUTable.numRows} : {})
14✔
421
    }
422
  };
423
}
424

425
function getInitialArrowGPUTable(props: {
426
  device: Device;
427
  arrowTable?: arrow.Table;
428
  arrowGPUTable?: GPUTable;
429
  shaderLayout: ShaderLayout;
430
  arrowPaths?: Record<string, string>;
431
  arrowBufferProps?: GPUVectorProps;
432
  allowWebGLOnlyFormats?: boolean;
433
}): {arrowGPUTable: ArrowModelGPUTable; ownsArrowGPUTable: boolean} {
434
  if (props.arrowGPUTable) {
16✔
435
    return {arrowGPUTable: props.arrowGPUTable, ownsArrowGPUTable: false};
2✔
436
  }
437

438
  return {
14✔
439
    arrowGPUTable: new GPUTable(props.device, props.arrowTable!, {
440
      shaderLayout: props.shaderLayout,
441
      arrowPaths: props.arrowPaths,
442
      bufferProps: props.arrowBufferProps,
443
      allowWebGLOnlyFormats: props.allowWebGLOnlyFormats
444
    } satisfies GPUTableProps),
445
    ownsArrowGPUTable: true
446
  };
447
}
448

449
function validateArrowModelSources(props: {
450
  arrowMesh?: ArrowMeshTable | arrow.Table;
451
  arrowTable?: arrow.Table;
452
  arrowGPUTable?: GPUTable;
453
}): void {
454
  const sourceCount =
455
    Number(Boolean(props.arrowMesh)) +
19✔
456
    Number(Boolean(props.arrowTable)) +
457
    Number(Boolean(props.arrowGPUTable));
458
  if (sourceCount > 1) {
19✔
459
    throw new Error('ArrowModel requires only one of arrowMesh, arrowTable, or arrowGPUTable');
1✔
460
  }
461
  if (sourceCount === 0) {
18!
NEW
462
    throw new Error('ArrowModel requires arrowMesh, arrowTable, or arrowGPUTable');
×
463
  }
464
}
465

466
function getBufferLayoutNames(bufferLayout: BufferLayout[]): string[] {
467
  return bufferLayout.map(layout => layout.name);
34✔
468
}
469

470
function assertNoDuplicateNames(
471
  explicitNames: string[],
472
  arrowNames: string[],
473
  nameType: string
474
): void {
475
  const explicitNameSet = new Set(explicitNames);
52✔
476
  for (const arrowName of arrowNames) {
52✔
477
    if (explicitNameSet.has(arrowName)) {
53✔
478
      throw new Error(`ArrowModel ${nameType} "${arrowName}" duplicates an explicit ${nameType}`);
2✔
479
    }
480
  }
481
}
482

483
function assertMatchingBufferLayouts(
484
  arrowBufferLayout: BufferLayout[],
485
  explicitBufferLayout: BufferLayout[],
486
  candidateBufferLayout: BufferLayout[],
487
  errorMessage: string
488
): void {
489
  const expectedBufferLayout = [...explicitBufferLayout, ...arrowBufferLayout];
5✔
490
  if (!deepEqualBufferLayouts(expectedBufferLayout, candidateBufferLayout)) {
5!
491
    throw new Error(errorMessage);
×
492
  }
493
}
494

495
function deepEqualBufferLayouts(
496
  expectedBufferLayout: BufferLayout[],
497
  candidateBufferLayout: BufferLayout[]
498
): boolean {
499
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
5✔
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