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

visgl / luma.gl / 26718723976

31 May 2026 04:56PM UTC coverage: 70.552% (+0.07%) from 70.487%
26718723976

Pull #2646

github

web-flow
Merge 61d366010 into c0e6b7370
Pull Request #2646: feat(tables) Align GPUVector types with luma.gl core types

8709 of 13961 branches covered (62.38%)

Branch coverage included in aggregate %.

400 of 550 new or added lines in 26 files covered. (72.73%)

10 existing lines in 5 files now uncovered.

17964 of 23845 relevant lines covered (75.34%)

4286.17 hits per line

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

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

5
import {type Binding, type ComputePass, Device} from '@luma.gl/core';
6
import {Computation, type ComputationProps} from '@luma.gl/engine';
7
import {DynamicBuffer} from '@luma.gl/engine';
8
import type {GPUData} from '../table/gpu-data';
9
import type {GPUVector} from '../table/gpu-vector';
10

11
/** Metadata supplied to one GPU table computation batch dispatch. */
12
export type GPUTableComputationBatch = {
13
  /** Zero-based batch index. */
14
  batchIndex: number;
15
  /** Shared logical row count for the current batched vector slice. */
16
  numRows: number;
17
};
18

19
/** Props for creating a WebGPU computation backed by GPU table vectors. */
20
export type GPUTableComputationProps = Omit<ComputationProps, 'bindings'> & {
21
  /** Ordinary non-table bindings forwarded to {@link Computation}. */
22
  bindings?: Record<string, Binding>;
23
  /** GPU vectors converted to storage-buffer bindings by name. */
24
  inputVectors?: Record<string, GPUVector>;
25
};
26

27
/**
28
 * A WebGPU computation that accepts {@link GPUVector} storage bindings.
29
 *
30
 * Direct vectors bind once during construction. Aggregate multi-buffer vectors
31
 * can be dispatched batch-by-batch with {@link dispatchBatches}.
32
 */
33
export class GPUTableComputation extends Computation {
34
  /** GPU vectors supplied when the computation was created. */
35
  readonly inputVectors: Record<string, GPUVector>;
36
  private readonly baseBindings: Record<string, Binding>;
37
  private readonly batchState: GPUTableComputationBatchState;
38

39
  constructor(device: Device, props: GPUTableComputationProps) {
40
    const {inputVectors = {}, bindings = {}, ...computationProps} = props;
×
41
    assertNoDuplicateBindingNames(inputVectors, bindings);
×
42

43
    const batchState = getGPUTableComputationBatchState(inputVectors);
×
44
    const baseBindings = {
×
45
      ...getDirectVectorBindings(inputVectors),
46
      ...bindings
47
    };
48

49
    super(device, {
×
50
      ...computationProps,
51
      bindings: baseBindings
52
    });
53

54
    this.inputVectors = {...inputVectors};
×
55
    this.baseBindings = baseBindings;
×
56
    this.batchState = batchState;
×
57
  }
58

59
  /** Dispatches once per vector batch, rebinding storage ranges before each dispatch. */
60
  dispatchBatches(
61
    computePass: ComputePass,
62
    getWorkgroupCount: number | ((batch: GPUTableComputationBatch) => number),
63
    y?: number,
64
    z?: number
65
  ): void {
66
    for (let batchIndex = 0; batchIndex < this.batchState.batchCount; batchIndex++) {
×
67
      const batch = {
×
68
        batchIndex,
69
        numRows: this.batchState.batchRowCounts[batchIndex]
70
      } satisfies GPUTableComputationBatch;
71
      this.setBindings({
×
72
        ...this.baseBindings,
73
        ...getBatchVectorBindings(this.inputVectors, batchIndex)
74
      });
75

76
      const workgroupCount =
77
        typeof getWorkgroupCount === 'function' ? getWorkgroupCount(batch) : getWorkgroupCount;
×
78
      super.dispatch(computePass, workgroupCount, y, z);
×
79
    }
80

81
    this.setBindings(this.baseBindings);
×
82
  }
83
}
84

85
type GPUTableComputationBatchState = {
86
  batchCount: number;
87
  batchRowCounts: number[];
88
};
89

90
function getGPUTableComputationBatchState(
91
  inputVectors: Record<string, GPUVector>
92
): GPUTableComputationBatchState {
93
  const batchedVectorEntries = Object.entries(inputVectors).filter(([, vector]) =>
×
94
    requiresBatchBinding(vector)
×
95
  );
96
  if (batchedVectorEntries.length === 0) {
×
97
    const firstVector = Object.values(inputVectors)[0];
×
98
    return {
×
99
      batchCount: 1,
100
      batchRowCounts: [firstVector?.length ?? 0]
×
101
    };
102
  }
103

104
  const [referenceName, referenceVector] = batchedVectorEntries[0];
×
105
  const batchCount = referenceVector.data.length;
×
106
  const batchRowCounts = referenceVector.data.map(data => data.length);
×
107

108
  for (const [name, vector] of batchedVectorEntries.slice(1)) {
×
109
    if (vector.data.length !== batchCount) {
×
110
      throw new Error(
×
111
        `GPUTableComputation vector "${name}" batch count does not match "${referenceName}"`
112
      );
113
    }
114
    vector.data.forEach((data, batchIndex) => {
×
115
      if (data.length !== batchRowCounts[batchIndex]) {
×
116
        throw new Error(
×
117
          `GPUTableComputation vector "${name}" batch ${batchIndex} rows do not match "${referenceName}"`
118
        );
119
      }
120
    });
121
  }
122

123
  return {batchCount, batchRowCounts};
×
124
}
125

126
function requiresBatchBinding(vector: GPUVector): boolean {
NEW
127
  return vector.data.length !== 1;
×
128
}
129

130
function getDirectVectorBindings(inputVectors: Record<string, GPUVector>): Record<string, Binding> {
131
  const bindings: Record<string, Binding> = {};
×
132
  for (const [name, vector] of Object.entries(inputVectors)) {
×
133
    if (!requiresBatchBinding(vector)) {
×
134
      bindings[name] = getDirectVectorBinding(vector);
×
135
    }
136
  }
137
  return bindings;
×
138
}
139

140
function getDirectVectorBinding(vector: GPUVector): Binding {
NEW
141
  return getGPUDataBinding(getSingleGPUVectorData(vector));
×
142
}
143

144
function getBatchVectorBindings(
145
  inputVectors: Record<string, GPUVector>,
146
  batchIndex: number
147
): Record<string, Binding> {
148
  const bindings: Record<string, Binding> = {};
×
149
  for (const [name, vector] of Object.entries(inputVectors)) {
×
150
    if (!requiresBatchBinding(vector)) {
×
151
      continue;
×
152
    }
153
    const data = vector.data[batchIndex];
×
154
    if (!data) {
×
155
      throw new Error(`GPUTableComputation vector "${name}" is missing batch ${batchIndex}`);
×
156
    }
157
    bindings[name] = getGPUDataBinding(data);
×
158
  }
159
  return bindings;
×
160
}
161

162
function getGPUDataBinding(data: GPUData): Binding {
163
  return {
×
164
    buffer: getGPUDataBuffer(data),
165
    offset: data.byteOffset,
166
    size: data.length * data.byteStride
167
  };
168
}
169

170
function getGPUDataBuffer(data: GPUData) {
NEW
171
  return data.buffer instanceof DynamicBuffer ? data.buffer.buffer : data.buffer;
×
172
}
173

174
function getSingleGPUVectorData(vector: GPUVector): GPUData {
NEW
175
  const [data, ...remainingData] = vector.data;
×
NEW
176
  if (!data || remainingData.length > 0) {
×
NEW
177
    throw new Error(
×
178
      `GPUTableComputation vector "${vector.name}" requires exactly one GPUData chunk`
179
    );
180
  }
NEW
181
  return data;
×
182
}
183

184
function assertNoDuplicateBindingNames(
185
  inputVectors: Record<string, GPUVector>,
186
  bindings: Record<string, Binding>
187
): void {
188
  for (const name of Object.keys(inputVectors)) {
×
189
    if (name in bindings) {
×
190
      throw new Error(`GPUTableComputation binding "${name}" duplicates an explicit binding`);
×
191
    }
192
  }
193
}
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