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

visgl / luma.gl / 26178691674

20 May 2026 05:24PM UTC coverage: 74.942% (+0.09%) from 74.85%
26178691674

push

github

web-flow
feat(gpgpu): Consolidate arithmetic operations (#2630)

7494 of 11282 branches covered (66.42%)

Branch coverage included in aggregate %.

281 of 344 new or added lines in 30 files covered. (81.69%)

26 existing lines in 5 files now uncovered.

16166 of 20289 relevant lines covered (79.68%)

1206.12 hits per line

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

84.13
/modules/gpgpu/src/operation/gpu-table.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4
import {getDataTypeFromTypedArray, getTypedArrayFromDataType} from '../utils/vertex-data-types';
5
import {Device, Buffer, SignedDataType} from '@luma.gl/core';
6
import type {TypedArray, TypedArrayConstructor} from '@math.gl/types';
7
import {bufferPool} from '../utils/buffer-pool';
8
import type {Operation} from './operation';
9

10
/** Properties used to construct a {@link GPUTableEvaluator}. */
11
export type GPUTableEvaluatorProps = {
12
  /** Optional debug name used by {@link GPUTableEvaluator.toString}. */
13
  id?: string;
14
  /** Scalar element type for every stored value. */
15
  type: SignedDataType;
16
  /** Number of scalar elements in each logical row. */
17
  size: number;
18
  /** Number of bytes to skip before reading the first element.
19
   * @default 0
20
   */
21
  offset?: number;
22
  /** Number of bytes between the starts of adjacent rows.
23
   * @default ValueType.BYTES_PER_ELEMENT * size
24
   */
25
  stride?: number;
26
  /** Whether integer values should be normalized when read as float vertex attributes. */
27
  normalized?: boolean;
28
  /** CPU buffer that initializes the table, required unless `source` is supplied. */
29
  value?: TypedArray;
30
  /** External GPU buffer that backs this table and is not owned by the evaluator. */
31
  buffer?: Buffer;
32
  /** Lazy operation or table whose output initializes this table, required unless `value` is supplied. */
33
  source?: Operation | GPUTableEvaluator | null;
34
  /** Whether every row should read the same value. */
35
  isConstant?: boolean;
36
  /** Number of logical rows, inferred for constants and CPU-backed tables when omitted. */
37
  length?: number;
38
};
39

40
/**
41
 * Device-agnostic, immutable 2D numeric table used as input and output for lazy GPGPU operations.
42
 *
43
 * A table describes row layout and a data source, but does not allocate or run GPU work until
44
 * {@link GPUTableEvaluator.evaluate} is called. Operation functions such as `add()` return new tables whose
45
 * `source` points at the deferred operation.
46
 */
47
export class GPUTableEvaluator {
48
  /** Scalar element type for each stored value. */
49
  readonly type: SignedDataType;
50
  /** Number of scalar elements in each logical row. */
51
  readonly size: number;
52
  /** Number of bytes to skip before reading the first element. */
53
  readonly offset: number;
54
  /** Number of bytes between the starts of adjacent rows. */
55
  readonly stride: number;
56
  /** Whether integer values should be normalized when read as float vertex attributes. */
57
  readonly normalized: boolean;
58
  /** Whether all rows share the same value. */
59
  readonly isConstant: boolean;
60
  /** Number of logical rows. */
61
  readonly length: number;
62
  /** Total bytes needed for the table storage. */
63
  readonly byteLength: number;
64
  /** TypedArray constructor for CPU representation, derived from {@link GPUTableEvaluator.type}. */
65
  readonly ValueType: TypedArrayConstructor;
66
  /** Operation whose output is used to fill the vector, required unless `value` is supplied */
67
  readonly source: Operation | GPUTableEvaluator | null = null;
434✔
68

69
  /** User-assigned id for easy debugging */
70
  protected _id?: string;
71
  /** destroy() has been called and no more resources should be created */
72
  protected _destroyed: boolean = false;
434✔
73

74
  /** CPU buffer, either provided by the user or read back from the GPU */
75
  protected _value?: TypedArray;
76
  /** GPU buffer */
77
  private _buffer?: Buffer;
78
  /** Whether the GPU buffer is externally owned and must not be recycled by the evaluator. */
79
  private _hasExternalBuffer: boolean = false;
434✔
80

81
  /**
82
   * Constructs a table from a CPU array.
83
   *
84
   * Plain JavaScript arrays are converted to typed arrays using `type` or `float32` by default.
85
   * `Float64Array` inputs are reinterpreted as `uint32` pairs so they can be consumed by GPU
86
   * operations such as {@link fround}.
87
   */
88
  static fromArray(
89
    value: TypedArray | number[],
90
    {
91
      type,
92
      size = 1,
210✔
93
      offset = 0,
210✔
94
      stride = 0,
210✔
95
      normalized = false
210✔
96
    }: Partial<Pick<GPUTableEvaluatorProps, 'type' | 'size' | 'offset' | 'stride' | 'normalized'>>
97
  ): GPUTableEvaluator {
98
    if (Array.isArray(value)) {
210✔
99
      type = type || 'float32';
186✔
100
      const ArrayType = getTypedArrayFromDataType(type);
186✔
101
      value = new ArrayType(value);
186✔
102
    } else if (value instanceof Float64Array) {
24✔
103
      // This is not really supported by GPU buffer, treat it as 2 uints
104
      type = 'uint32';
9✔
105
      size *= 2;
9✔
106
      offset *= 2;
9✔
107
      stride *= 2;
9✔
108
      value = new Uint32Array(value.buffer);
9✔
109
    } else {
110
      type = type || getDataTypeFromTypedArray(value);
15✔
111
    }
112
    const id = `<${type} * ${size}>`;
210✔
113
    return new GPUTableEvaluator({
210✔
114
      id,
115
      type,
116
      size,
117
      offset,
118
      stride,
119
      normalized,
120
      value
121
    });
122
  }
123

124
  /**
125
   * Constructs a constant table whose single row is broadcast across non-constant inputs.
126
   *
127
   * @param value - Scalar or row value.
128
   * @param type - Scalar element type used for the CPU representation.
129
   */
130
  static fromConstant(
131
    value: number | number[],
132
    type: SignedDataType = 'float32'
12✔
133
  ): GPUTableEvaluator {
134
    const ArrayType = getTypedArrayFromDataType(type);
12✔
135
    let id: string;
136
    if (Array.isArray(value)) {
12✔
137
      id = `[${value.join(',')}]`;
9✔
138
    } else {
139
      id = String(value);
3✔
140
      value = [value];
3✔
141
    }
142
    return new GPUTableEvaluator({
12✔
143
      id,
144
      isConstant: true,
145
      type,
146
      size: value.length,
147
      value: new ArrayType(value)
148
    });
149
  }
150

151
  /** TODO - Construct a new GPUTableEvaluator from a loaders.gl Table/BatchedTable. */
152
  // static from(table: Table, columnName: string | number): GPUTableEvaluator
153

154
  /**
155
   * Creates a table from explicit row layout and source information.
156
   *
157
   * Prefer {@link GPUTableEvaluator.fromArray} or {@link GPUTableEvaluator.fromConstant} for CPU-backed tables.
158
   */
159
  constructor(props: GPUTableEvaluatorProps) {
160
    const {id, value, buffer, source = null, isConstant = false} = props;
434✔
161
    if (!source && !value && !buffer) {
434!
162
      throw new Error('OperationResource must have a value source');
×
163
    }
164
    let {type, size, offset, stride, normalized, length} = props;
434✔
165
    if (source instanceof GPUTableEvaluator) {
434✔
166
      type = type ?? source.type;
6!
167
      size = size ?? source.size;
6!
168
      offset = offset ?? source.offset;
6!
169
      stride = stride ?? source.stride;
6✔
170
      normalized = normalized ?? source.normalized;
6✔
171
      length = length ?? source.length;
6✔
172
    } else {
173
      size = size ?? 1;
428!
174
      offset = offset ?? 0;
428✔
175
      normalized = normalized ?? false;
428✔
176
      length = isConstant ? 1 : length;
428✔
177
    }
178

179
    this._id = id;
434✔
180
    this.type = type;
434✔
181
    this.size = size;
434✔
182
    this.ValueType = getTypedArrayFromDataType(this.type);
434✔
183
    this.offset = offset;
434✔
184
    this.stride = stride || this.ValueType.BYTES_PER_ELEMENT * size;
434✔
185
    this.normalized = normalized;
434✔
186
    this.source = source;
434✔
187
    this._value = value;
434✔
188
    this._buffer = buffer;
434✔
189
    this._hasExternalBuffer = source instanceof GPUTableEvaluator || Boolean(buffer);
434✔
190

191
    if (length === undefined) {
434✔
192
      if (isConstant) {
217!
UNCOV
193
        length = 1;
×
194
      } else {
195
        if (!value) {
217!
196
          throw new Error('GPUTableEvaluator: length not defined');
×
197
        }
198
        length = Math.ceil(value.byteLength / this.stride);
217✔
199
      }
200
    }
201
    this.isConstant = isConstant;
434✔
202
    this.length = length;
434✔
203
    this.byteLength = this.stride * length;
434✔
204
  }
205

206
  /** CPU-side typed array, when available. */
207
  get value(): TypedArray | undefined {
208
    return (
1,651✔
209
      this._value || (this.source instanceof GPUTableEvaluator ? this.source.value : undefined)
1,679!
210
    );
211
  }
212

213
  get evaluated(): boolean {
214
    return Boolean(this._buffer);
5✔
215
  }
216

217
  /** GPU buffer for the table. Only available after {@link GPUTableEvaluator.evaluate} resolves. */
218
  get buffer(): Buffer {
219
    if (!this._buffer) {
561!
220
      throw new Error(`${this} not evaluated`);
×
221
    }
222
    return this._buffer;
561✔
223
  }
224

225
  /**
226
   * Materializes the table on a device.
227
   *
228
   * If the table is operation-backed, dependencies are evaluated first and then the backend
229
   * operation writes into a cached GPU buffer.
230
   */
231
  async evaluate(device: Device): Promise<void> {
232
    if (this._destroyed) {
356!
233
      throw new Error(`GPUTableEvaluator ${this} already destroyed`);
×
234
    }
235
    if (!this._buffer) {
356✔
236
      let buffer: Buffer;
237
      if (this.source instanceof GPUTableEvaluator) {
352✔
238
        await this.source.evaluate(device);
6✔
239
        buffer = this.source.buffer;
6✔
240
      } else {
241
        buffer = bufferPool.createOrReuse(device, this.byteLength);
346✔
242
        if (this._value) {
346✔
243
          buffer.write(this._value);
203✔
244
        } else {
245
          const result = await this.source!.execute(device, buffer);
143✔
246
          if (!result.success) {
143!
NEW
247
            throw result.error || new Error(`${this.source} evaluation failed`);
×
248
          }
249
          if (result.value) {
143✔
250
            this._value = result.value;
66✔
251
          }
252
        }
253
      }
254
      // cache the result when successful
255
      this._buffer = buffer;
352✔
256
    }
257
  }
258

259
  /**
260
   * Reads table data back to the CPU.
261
   *
262
   * This is intended for debugging and validation. Tightly packed rows return a typed-array view;
263
   * strided rows are copied into a compact typed array.
264
   */
265
  async readValue(startRow: number = 0, endRow?: number): Promise<TypedArray> {
133✔
266
    const {ValueType} = this;
133✔
267
    if (!this._value) {
133✔
268
      const bytes = await this.buffer.readAsync(this.offset, this.byteLength);
70✔
269
      this._value = new ValueType(bytes.buffer as ArrayBuffer);
70✔
270
    }
271

272
    const {size, offset, stride, length} = this;
133✔
273
    const width = ValueType.BYTES_PER_ELEMENT * size;
133✔
274
    endRow = endRow ?? length;
133✔
275

276
    if (stride === width) {
133!
277
      const buffer = this._value!.buffer as ArrayBuffer;
133✔
278
      return new ValueType(buffer, offset + stride * startRow, (endRow - startRow) * size);
133✔
279
    }
280

281
    const bytes = new Uint8Array(width * (endRow - startRow));
×
282
    let i0 = offset + startRow * stride,
×
283
      i1 = 0;
×
284
    for (let y = startRow; y < endRow; y++) {
×
285
      for (let x = 0; x < width; x++) {
×
286
        bytes[i1++] = bytes[i0 + x];
×
287
      }
288
      i0 += stride;
×
289
    }
290
    return new ValueType(bytes.buffer);
×
291
  }
292

293
  /** Returns the debug id, source description, or class name. */
294
  toString(): string {
295
    return this._id ?? this.source?.toString() ?? this.constructor.name;
362!
296
  }
297

298
  /** Releases cached GPU storage and prevents future evaluation. */
299
  destroy() {
300
    if (this._buffer) {
352!
301
      if (!this._hasExternalBuffer) {
352✔
302
        bufferPool.recycle(this._buffer);
346✔
303
      }
304
      this._buffer = undefined;
352✔
305
    }
306
    this._destroyed = true;
352✔
307
  }
308
}
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