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

visgl / luma.gl / 23316217543

19 Mar 2026 08:47PM UTC coverage: 77.934% (+0.2%) from 77.702%
23316217543

push

github

web-flow
chore(core) composite shadertypes (structs, arrays), lighting module improvements (#2395)

3018 of 3855 branches covered (78.29%)

Branch coverage included in aggregate %.

1908 of 2636 new or added lines in 51 files covered. (72.38%)

38 existing lines in 3 files now uncovered.

33088 of 42474 relevant lines covered (77.9%)

116.67 hits per line

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

25.41
/modules/engine/src/compute/computation.ts
1
// luma.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import {
1✔
6
  type DeviceFeature,
1✔
7
  type ComputePipelineProps,
1✔
8
  type Shader,
1✔
9
  type Binding,
1✔
10
  Device,
1✔
11
  Buffer,
1✔
12
  ComputePipeline,
1✔
13
  ComputePass,
1✔
14
  UniformStore,
1✔
15
  log,
1✔
16
  dataTypeDecoder
1✔
17
} from '@luma.gl/core';
1✔
18
import {type ShaderModule, type PlatformInfo, ShaderAssembler} from '@luma.gl/shadertools';
1✔
19
import {type TypedArray, isNumericArray} from '@math.gl/types';
1✔
20
import {ShaderInputs} from '../shader-inputs';
1✔
21
import {PipelineFactory} from '../factories/pipeline-factory';
1✔
22
import {ShaderFactory} from '../factories/shader-factory';
1✔
23
import {uid} from '../utils/uid';
1✔
24
// import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
1✔
25

1✔
26
const LOG_DRAW_PRIORITY = 2;
1✔
27
const LOG_DRAW_TIMEOUT = 10000;
1✔
28

1✔
29
export type ComputationProps = Omit<ComputePipelineProps, 'shader'> & {
1✔
30
  source?: string;
1✔
31

1✔
32
  /** shadertool shader modules (added to shader code) */
1✔
33
  modules?: ShaderModule[];
1✔
34
  /** Shadertool module defines (configures shader code)*/
1✔
35
  defines?: Record<string, boolean>;
1✔
36
  // TODO - injections, hooks etc?
1✔
37

1✔
38
  /** Shader inputs, used to generated uniform buffers and bindings */
1✔
39
  shaderInputs?: ShaderInputs;
1✔
40

1✔
41
  /** Bindings */
1✔
42
  bindings?: Record<string, Binding>;
1✔
43

1✔
44
  /** Show shader source in browser? */
1✔
45
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
1✔
46

1✔
47
  /** Factory used to create a {@link ComputePipeline}. Defaults to {@link Device} default factory. */
1✔
48
  pipelineFactory?: PipelineFactory;
1✔
49
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
1✔
50
  shaderFactory?: ShaderFactory;
1✔
51
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
1✔
52
  shaderAssembler?: ShaderAssembler;
1✔
53
};
1✔
54

1✔
55
/**
1✔
56
 * v9 Model API
1✔
57
 * A model
1✔
58
 * - automatically reuses pipelines (programs) when possible
1✔
59
 * - automatically rebuilds pipelines if necessary to accommodate changed settings
1✔
60
 * shadertools integration
1✔
61
 * - accepts modules and performs shader transpilation
1✔
62
 */
1✔
63
export class Computation {
1✔
64
  static defaultProps: Required<ComputationProps> = {
1✔
65
    ...ComputePipeline.defaultProps,
1✔
66
    id: 'unnamed',
1✔
67
    handle: undefined,
1✔
68
    userData: {},
1✔
69

1✔
70
    source: '',
1✔
71
    modules: [],
1✔
72
    defines: {},
1✔
73

1✔
74
    bindings: undefined!,
1✔
75
    shaderInputs: undefined!,
1✔
76

1✔
77
    pipelineFactory: undefined!,
1✔
78
    shaderFactory: undefined!,
1✔
79
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
1✔
80

1✔
81
    debugShaders: undefined!
1✔
82
  };
1✔
83

1✔
84
  readonly device: Device;
1!
85
  readonly id: string;
×
86

×
87
  readonly pipelineFactory: PipelineFactory;
×
88
  readonly shaderFactory: ShaderFactory;
×
89

×
90
  userData: {[key: string]: any} = {};
×
91

×
92
  /** Bindings (textures, samplers, uniform buffers) */
×
93
  bindings: Record<string, Binding> = {};
×
94

×
95
  /** The underlying GPU pipeline. */
×
96
  pipeline: ComputePipeline;
×
97
  /** Assembled compute shader source */
×
98
  source: string;
×
99
  /** the underlying compiled compute shader */
×
100
  // @ts-ignore Set in function called from constructor
×
101
  shader: Shader;
×
102

×
103
  /** ShaderInputs instance */
×
104
  shaderInputs: ShaderInputs;
×
105

×
106
  // @ts-ignore Set in function called from constructor
×
107
  _uniformStore: UniformStore;
×
108

×
109
  _pipelineNeedsUpdate: string | false = 'newly created';
×
110

×
111
  private _getModuleUniforms: (props?: Record<string, Record<string, any>>) => Record<string, any>;
×
112
  private props: Required<ComputationProps>;
×
113

×
114
  private _destroyed = false;
×
115

×
116
  constructor(device: Device, props: ComputationProps) {
×
117
    if (device.type !== 'webgpu') {
×
118
      throw new Error('Computation is only supported in WebGPU');
×
119
    }
×
120

×
121
    this.props = {...Computation.defaultProps, ...props};
×
122
    props = this.props;
×
123
    this.id = props.id || uid('model');
×
124
    this.device = device;
×
125

×
126
    Object.assign(this.userData, props.userData);
×
127

×
128
    // Setup shader module inputs
×
129
    const moduleMap = Object.fromEntries(
×
130
      this.props.modules?.map(module => [module.name, module]) || []
×
131
    );
×
132
    // @ts-ignore TODO - fix up typing?
×
133
    this.shaderInputs = props.shaderInputs || new ShaderInputs(moduleMap);
×
134
    this.setShaderInputs(this.shaderInputs);
×
135

×
136
    // Support WGSL shader layout introspection
×
137
    // TODO - Don't modify props!!
×
138
    // @ts-expect-error method on WebGPUDevice
×
139
    this.props.shaderLayout ||= device.getShaderLayout(this.props.source);
×
140

×
141
    // Setup shader assembler
×
142
    const platformInfo = getPlatformInfo(device);
×
143

×
144
    // Extract modules from shader inputs if not supplied
×
145
    const modules =
×
146
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
×
147

×
148
    this.pipelineFactory =
×
149
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
×
150
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
×
151

×
152
    const {source, getUniforms} = this.props.shaderAssembler.assembleWGSLShader({
×
153
      platformInfo,
×
154
      ...this.props,
×
155
      modules
×
156
    });
×
157

×
158
    this.source = source;
×
159
    // @ts-ignore
×
160
    this._getModuleUniforms = getUniforms;
×
161

×
162
    // Create the pipeline
×
163
    // @note order is important
×
164
    this.pipeline = this._updatePipeline();
×
165

×
166
    // Apply any dynamic settings that will not trigger pipeline change
×
167
    if (props.bindings) {
×
168
      this.setBindings(props.bindings);
×
169
    }
×
170

×
171
    // Catch any access to non-standard props
×
172
    Object.seal(this);
×
173
  }
×
174

×
175
  destroy(): void {
×
176
    if (this._destroyed) return;
×
177
    this.pipelineFactory.release(this.pipeline);
×
178
    this.shaderFactory.release(this.shader);
×
179
    this._uniformStore.destroy();
×
180
    this._destroyed = true;
×
181
  }
×
182

×
183
  // Draw call
×
184

×
185
  predraw() {
×
186
    // Update uniform buffers if needed
×
187
    this.updateShaderInputs();
×
188
  }
×
189

×
190
  dispatch(computePass: ComputePass, x: number, y?: number, z?: number): void {
×
191
    try {
×
192
      this._logDrawCallStart();
×
193

×
194
      // Check if the pipeline is invalidated
×
195
      // TODO - this is likely the worst place to do this from performance perspective. Perhaps add a predraw()?
×
196
      this.pipeline = this._updatePipeline();
×
197

×
198
      // Set pipeline state, we may be sharing a pipeline so we need to set all state on every draw
×
199
      // Any caching needs to be done inside the pipeline functions
×
200
      this.pipeline.setBindings(this.bindings);
×
201
      computePass.setPipeline(this.pipeline);
×
202
      // @ts-expect-error
×
203
      computePass.setBindings([]);
×
204

×
205
      computePass.dispatch(x, y, z);
×
206
    } finally {
×
207
      this._logDrawCallEnd();
×
208
    }
×
209
  }
×
210

×
211
  // Update fixed fields (can trigger pipeline rebuild)
×
212

×
213
  // Update dynamic fields
×
214

×
215
  /**
×
216
   * Updates the vertex count (used in draw calls)
×
217
   * @note Any attributes with stepMode=vertex need to be at least this big
×
218
   */
×
219
  setVertexCount(vertexCount: number): void {
×
220
    // this.vertexCount = vertexCount;
×
221
  }
×
222

×
223
  /**
×
224
   * Updates the instance count (used in draw calls)
×
225
   * @note Any attributes with stepMode=instance need to be at least this big
×
226
   */
×
227
  setInstanceCount(instanceCount: number): void {
×
228
    // this.instanceCount = instanceCount;
×
229
  }
×
230

×
231
  setShaderInputs(shaderInputs: ShaderInputs): void {
×
232
    this.shaderInputs = shaderInputs;
×
233
    this._uniformStore = new UniformStore(this.shaderInputs.modules);
×
234
    // Create uniform buffer bindings for all modules
×
235
    for (const moduleName of Object.keys(this.shaderInputs.modules)) {
×
236
      const uniformBuffer = this._uniformStore.getManagedUniformBuffer(this.device, moduleName);
×
237
      this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
×
238
    }
×
239
  }
×
240

×
241
  /**
×
242
   * Updates shader module settings (which results in uniforms being set)
×
243
   */
×
244
  setShaderModuleProps(props: Record<string, any>): void {
×
245
    const uniforms = this._getModuleUniforms(props);
×
246

×
247
    // Extract textures & framebuffers set by the modules
×
248
    // TODO better way to extract bindings
×
249
    const keys = Object.keys(uniforms).filter(k => {
×
250
      const uniform = uniforms[k];
×
251
      return (
×
252
        !isNumericArray(uniform) && typeof uniform !== 'number' && typeof uniform !== 'boolean'
×
253
      );
×
254
    });
×
255
    const bindings: Record<string, Binding> = {};
×
256
    for (const k of keys) {
×
257
      bindings[k] = uniforms[k];
×
258
      delete uniforms[k];
×
259
    }
×
260
  }
×
261

×
262
  updateShaderInputs(): void {
×
263
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues());
×
264
  }
×
265

×
266
  /**
×
267
   * Sets bindings (textures, samplers, uniform buffers)
×
268
   */
×
269
  setBindings(bindings: Record<string, Binding>): void {
×
270
    Object.assign(this.bindings, bindings);
×
271
  }
×
272

×
273
  _setPipelineNeedsUpdate(reason: string): void {
×
274
    this._pipelineNeedsUpdate = this._pipelineNeedsUpdate || reason;
×
275
  }
×
276

×
277
  _updatePipeline(): ComputePipeline {
×
278
    if (this._pipelineNeedsUpdate) {
×
279
      let prevShader: Shader | null = null;
×
280
      if (this.pipeline) {
×
281
        log.log(
×
282
          1,
×
283
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
×
284
        )();
×
285
        prevShader = this.shader;
×
286
      }
×
287

×
288
      this._pipelineNeedsUpdate = false;
×
289

×
290
      this.shader = this.shaderFactory.createShader({
×
291
        id: `${this.id}-fragment`,
×
292
        stage: 'compute',
×
293
        source: this.source,
×
294
        debugShaders: this.props.debugShaders
×
295
      });
×
296

×
297
      this.pipeline = this.pipelineFactory.createComputePipeline({
×
298
        ...this.props,
×
299
        shader: this.shader
×
300
      });
×
301

×
302
      if (prevShader) {
×
303
        this.shaderFactory.release(prevShader);
×
304
      }
×
305
    }
×
306
    return this.pipeline;
×
307
  }
×
308

×
309
  /** Throttle draw call logging */
×
310
  _lastLogTime = 0;
×
311
  _logOpen = false;
×
312

×
313
  _logDrawCallStart(): void {
×
314
    // IF level is 4 or higher, log every frame.
×
315
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
×
316
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
×
317
      return;
×
318
    }
×
319

×
320
    this._lastLogTime = Date.now();
×
321
    this._logOpen = true;
×
322

×
323
    log.group(LOG_DRAW_PRIORITY, `>>> DRAWING MODEL ${this.id}`, {collapsed: log.level <= 2})();
×
324
  }
×
325

×
326
  _logDrawCallEnd(): void {
×
327
    if (this._logOpen) {
×
328
      // const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.props.shaderLayout, this.id);
×
329

×
330
      // log.table(logLevel, attributeTable)();
×
331
      // log.table(logLevel, uniformTable)();
×
332
      // log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
333

×
334
      const uniformTable = this.shaderInputs.getDebugTable();
×
335
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
336

×
337
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
338
      this._logOpen = false;
×
339
    }
×
340
  }
×
341

×
342
  protected _drawCount = 0;
×
343

1✔
344
  // TODO - fix typing of luma data types
1✔
345
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1✔
NEW
346
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
347
    const typedArray =
×
348
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
349
    return typedArray.toString();
×
350
  }
×
351
}
1✔
352

1✔
353
/** Create a shadertools platform info from the Device */
1✔
354
export function getPlatformInfo(device: Device): PlatformInfo {
1✔
355
  return {
×
356
    type: device.type,
×
357
    shaderLanguage: device.info.shadingLanguage,
×
358
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
×
359
    gpu: device.info.gpu,
×
360
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
×
361
    features: device.features as unknown as Set<DeviceFeature>
×
362
  };
×
363
}
×
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