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

visgl / luma.gl / 17192022436

24 Aug 2025 06:02PM UTC coverage: 62.079% (-13.2%) from 75.234%
17192022436

Pull #2437

github

web-flow
Merge 562c391b0 into 8314ecefa
Pull Request #2437: test(engine): add ShaderPassRenderer test

956 of 1559 branches covered (61.32%)

Branch coverage included in aggregate %.

491 of 666 new or added lines in 7 files covered. (73.72%)

5291 existing lines in 117 files now uncovered.

23238 of 37414 relevant lines covered (62.11%)

3.53 hits per line

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

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

1✔
5
// A lot of imports, but then Model is where it all comes together...
1✔
6
import type {TypedArray} from '@math.gl/types';
1✔
7
import type {
1✔
8
  RenderPipelineProps,
1✔
9
  RenderPipelineParameters,
1✔
10
  BufferLayout,
1✔
11
  Shader,
1✔
12
  VertexArray,
1✔
13
  TransformFeedback,
1✔
14
  AttributeInfo,
1✔
15
  Binding,
1✔
16
  PrimitiveTopology
1✔
17
} from '@luma.gl/core';
1✔
18
import {
1✔
19
  Device,
1✔
20
  DeviceFeature,
1✔
21
  Buffer,
1✔
22
  Texture,
1✔
23
  TextureView,
1✔
24
  Sampler,
1✔
25
  RenderPipeline,
1✔
26
  RenderPass,
1✔
27
  UniformStore,
1✔
28
  log,
1✔
29
  getTypedArrayConstructor,
1✔
30
  getAttributeInfosFromLayouts
1✔
31
} from '@luma.gl/core';
1✔
32

1✔
33
import type {ShaderModule, PlatformInfo} from '@luma.gl/shadertools';
1✔
34
import {ShaderAssembler} from '@luma.gl/shadertools';
1✔
35

1✔
36
import type {Geometry} from '../geometry/geometry';
1✔
37
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
1✔
38
import {PipelineFactory} from '../factories/pipeline-factory';
1✔
39
import {ShaderFactory} from '../factories/shader-factory';
1✔
40
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
1✔
41
import {debugFramebuffer} from '../debug/debug-framebuffer';
1✔
42
import {deepEqual} from '../utils/deep-equal';
1✔
43
import {BufferLayoutHelper} from '../utils/buffer-layout-helper';
1✔
44
import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';
1✔
45
import {uid} from '../utils/uid';
1✔
46
import {ShaderInputs} from '../shader-inputs';
1✔
47
import {DynamicTexture} from '../dynamic-texture/dynamic-texture';
1✔
48

1✔
49
const LOG_DRAW_PRIORITY = 2;
1✔
50
const LOG_DRAW_TIMEOUT = 10000;
1✔
51

1✔
52
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
1✔
53
  source?: string;
1✔
54
  vs?: string | null;
1✔
55
  fs?: string | null;
1✔
56

1✔
57
  /** shadertool shader modules (added to shader code) */
1✔
58
  modules?: ShaderModule[];
1✔
59
  /** Shadertool module defines (configures shader code)*/
1✔
60
  defines?: Record<string, boolean>;
1✔
61
  // TODO - injections, hooks etc?
1✔
62

1✔
63
  /** Shader inputs, used to generated uniform buffers and bindings */
1✔
64
  shaderInputs?: ShaderInputs;
1✔
65
  /** Bindings */
1✔
66
  bindings?: Record<string, Binding | DynamicTexture>;
1✔
67
  /** Parameters that are built into the pipeline */
1✔
68
  parameters?: RenderPipelineParameters;
1✔
69

1✔
70
  /** Geometry */
1✔
71
  geometry?: GPUGeometry | Geometry | null;
1✔
72

1✔
73
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
1✔
74
  isInstanced?: boolean;
1✔
75
  /** instance count */
1✔
76
  instanceCount?: number;
1✔
77
  /** Vertex count */
1✔
78
  vertexCount?: number;
1✔
79

1✔
80
  indexBuffer?: Buffer | null;
1✔
81
  /** @note this is really a map of buffers, not a map of attributes */
1✔
82
  attributes?: Record<string, Buffer>;
1✔
83
  /**   */
1✔
84
  constantAttributes?: Record<string, TypedArray>;
1✔
85

1✔
86
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
1✔
87
  disableWarnings?: boolean;
1✔
88

1✔
89
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
1✔
90
  varyings?: string[];
1✔
91

1✔
92
  transformFeedback?: TransformFeedback;
1✔
93

1✔
94
  /** Show shader source in browser? */
1✔
95
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
1✔
96

1✔
97
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
1✔
98
  pipelineFactory?: PipelineFactory;
1✔
99
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
1✔
100
  shaderFactory?: ShaderFactory;
1✔
101
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
1✔
102
  shaderAssembler?: ShaderAssembler;
1✔
103
};
1✔
104

1✔
105
/**
1✔
106
 * High level draw API for luma.gl.
1✔
107
 *
1✔
108
 * A `Model` encapsulates shaders, geometry attributes, bindings and render
1✔
109
 * pipeline state into a single object. It automatically reuses and rebuilds
1✔
110
 * pipelines as render parameters change and exposes convenient hooks for
1✔
111
 * updating uniforms and attributes.
1✔
112
 *
1✔
113
 * Features:
1✔
114
 * - Reuses and lazily recompiles {@link RenderPipeline | pipelines} as needed.
1✔
115
 * - Integrates with `@luma.gl/shadertools` to assemble GLSL or WGSL from shader modules.
1✔
116
 * - Manages geometry attributes and buffer bindings.
1✔
117
 * - Accepts textures, samplers and uniform buffers as bindings, including `AsyncTexture`.
1✔
118
 * - Provides detailed debug logging and optional shader source inspection.
1✔
119
 */
1✔
120
export class Model {
1✔
121
  static defaultProps: Required<ModelProps> = {
1✔
122
    ...RenderPipeline.defaultProps,
1✔
123
    source: undefined!,
1✔
124
    vs: null,
1✔
125
    fs: null,
1✔
126
    id: 'unnamed',
1✔
127
    handle: undefined,
1✔
128
    userData: {},
1✔
129
    defines: {},
1✔
130
    modules: [],
1✔
131
    geometry: null,
1✔
132
    indexBuffer: null,
1✔
133
    attributes: {},
1✔
134
    constantAttributes: {},
1✔
135
    varyings: [],
1✔
136

1✔
137
    isInstanced: undefined!,
1✔
138
    instanceCount: 0,
1✔
139
    vertexCount: 0,
1✔
140

1✔
141
    shaderInputs: undefined!,
1✔
142
    pipelineFactory: undefined!,
1✔
143
    shaderFactory: undefined!,
1✔
144
    transformFeedback: undefined!,
1✔
145
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
1✔
146

1✔
147
    debugShaders: undefined!,
1✔
148
    disableWarnings: undefined!
1✔
149
  };
1✔
150

1✔
151
  /** Device that created this model */
1✔
152
  readonly device: Device;
1✔
153
  /** Application provided identifier */
1✔
154
  readonly id: string;
1✔
155
  /** WGSL shader source when using unified shader */
1✔
156
  // @ts-expect-error assigned in function called from constructor
1✔
157
  readonly source: string;
1✔
158
  /** GLSL vertex shader source */
1✔
159
  // @ts-expect-error assigned in function called from constructor
1✔
160
  readonly vs: string;
1✔
161
  /** GLSL fragment shader source */
1✔
162
  // @ts-expect-error assigned in function called from constructor
1✔
163
  readonly fs: string;
1✔
164
  /** Factory used to create render pipelines */
1✔
165
  readonly pipelineFactory: PipelineFactory;
1✔
166
  /** Factory used to create shaders */
1✔
167
  readonly shaderFactory: ShaderFactory;
1✔
168
  /** User-supplied per-model data */
1✔
169
  userData: {[key: string]: any} = {};
1✔
170

1✔
171
  // Fixed properties (change can trigger pipeline rebuild)
1✔
172

1✔
173
  /** The render pipeline GPU parameters, depth testing etc */
1✔
174
  parameters: RenderPipelineParameters;
1✔
175

1✔
176
  /** The primitive topology */
1✔
177
  topology: PrimitiveTopology;
1✔
178
  /** Buffer layout */
1✔
179
  bufferLayout: BufferLayout[];
1✔
180

1✔
181
  // Dynamic properties
1✔
182

1✔
183
  /** Use instanced rendering */
1✔
184
  isInstanced: boolean | undefined = undefined;
1✔
185
  /** instance count. `undefined` means not instanced */
1✔
186
  instanceCount: number = 0;
1✔
187
  /** Vertex count */
1✔
188
  vertexCount: number;
1✔
189

1✔
190
  /** Index buffer */
1✔
191
  indexBuffer: Buffer | null = null;
1✔
192
  /** Buffer-valued attributes */
1✔
193
  bufferAttributes: Record<string, Buffer> = {};
1✔
194
  /** Constant-valued attributes */
1✔
195
  constantAttributes: Record<string, TypedArray> = {};
1✔
196
  /** Bindings (textures, samplers, uniform buffers) */
1✔
197
  bindings: Record<string, Binding | DynamicTexture> = {};
1✔
198

1✔
199
  /**
1✔
200
   * VertexArray
1✔
201
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
1✔
202
   * @todo - allow application to define multiple vertex arrays?
1✔
203
   * */
1✔
204
  vertexArray: VertexArray;
1✔
205

1✔
206
  /** TransformFeedback, WebGL 2 only. */
1✔
207
  transformFeedback: TransformFeedback | null = null;
1✔
208

1✔
209
  /** The underlying GPU "program". @note May be recreated if parameters change */
1✔
210
  pipeline: RenderPipeline;
1✔
211

1✔
212
  /** ShaderInputs instance */
1✔
213
  // @ts-expect-error Assigned in function called by constructor
1✔
214
  shaderInputs: ShaderInputs;
1✔
215
  // @ts-expect-error Assigned in function called by constructor
1✔
216
  _uniformStore: UniformStore;
1✔
217

1✔
218
  _attributeInfos: Record<string, AttributeInfo> = {};
1✔
219
  _gpuGeometry: GPUGeometry | null = null;
1✔
220
  private props: Required<ModelProps>;
1✔
221

1✔
222
  _pipelineNeedsUpdate: string | false = 'newly created';
1✔
223
  private _needsRedraw: string | false = 'initializing';
1✔
224
  private _destroyed = false;
1✔
225

1✔
226
  /** "Time" of last draw. Monotonically increasing timestamp */
1✔
227
  _lastDrawTimestamp: number = -1;
1✔
228

1✔
229
  get [Symbol.toStringTag](): string {
1✔
230
    return 'Model';
×
231
  }
×
232

1✔
233
  toString(): string {
1✔
234
    return `Model(${this.id})`;
4✔
235
  }
4✔
236

1✔
237
  constructor(device: Device, props: ModelProps) {
1✔
238
    this.props = {...Model.defaultProps, ...props};
4✔
239
    props = this.props;
4✔
240
    this.id = props.id || uid('model');
4!
241
    this.device = device;
4✔
242

4✔
243
    Object.assign(this.userData, props.userData);
4✔
244

4✔
245
    // Setup shader module inputs
4✔
246
    const moduleMap = Object.fromEntries(
4✔
247
      this.props.modules?.map(module => [module.name, module]) || []
4!
248
    );
4✔
249

4✔
250
    const shaderInputs =
4✔
251
      props.shaderInputs ||
4✔
252
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
4✔
253
    // @ts-ignore
4✔
254
    this.setShaderInputs(shaderInputs);
4✔
255

4✔
256
    // Setup shader assembler
4✔
257
    const platformInfo = getPlatformInfo(device);
4✔
258

4✔
259
    // Extract modules from shader inputs if not supplied
4✔
260
    const modules =
4✔
261
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
4✔
262
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
4!
263

4✔
264
    const isWebGPU = this.device.type === 'webgpu';
4✔
265

4✔
266
    // WebGPU
4✔
267
    // TODO - hack to support unified WGSL shader
4✔
268
    // TODO - this is wrong, compile a single shader
4✔
269
    if (isWebGPU && this.props.source) {
4!
270
      // WGSL
×
271
      const {source, getUniforms} = this.props.shaderAssembler.assembleWGSLShader({
×
272
        platformInfo,
×
273
        ...this.props,
×
274
        modules
×
275
      });
×
276
      this.source = source;
×
277
      // @ts-expect-error
×
278
      this._getModuleUniforms = getUniforms;
×
279
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
×
280
      // @ts-expect-error Method on WebGPUDevice
×
281
      this.props.shaderLayout ||= device.getShaderLayout(this.source);
×
282
    } else {
4✔
283
      // GLSL
4✔
284
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
4✔
285
        platformInfo,
4✔
286
        ...this.props,
4✔
287
        modules
4✔
288
      });
4✔
289

4✔
290
      this.vs = vs;
4✔
291
      this.fs = fs;
4✔
292
      // @ts-expect-error
4✔
293
      this._getModuleUniforms = getUniforms;
4✔
294
    }
4✔
295

4✔
296
    this.vertexCount = this.props.vertexCount;
4✔
297
    this.instanceCount = this.props.instanceCount;
4✔
298

4✔
299
    this.topology = this.props.topology;
4✔
300
    this.bufferLayout = this.props.bufferLayout;
4✔
301
    this.parameters = this.props.parameters;
4✔
302

4✔
303
    // Geometry, if provided, sets topology and vertex cound
4✔
304
    if (props.geometry) {
4✔
305
      this.setGeometry(props.geometry);
4✔
306
    }
4✔
307

4✔
308
    this.pipelineFactory =
4✔
309
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
4✔
310
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
4✔
311

4✔
312
    // Create the pipeline
4✔
313
    // @note order is important
4✔
314
    this.pipeline = this._updatePipeline();
4✔
315

4✔
316
    this.vertexArray = device.createVertexArray({
4✔
317
      shaderLayout: this.pipeline.shaderLayout,
4✔
318
      bufferLayout: this.pipeline.bufferLayout
4✔
319
    });
4✔
320

4✔
321
    // Now we can apply geometry attributes
4✔
322
    if (this._gpuGeometry) {
4✔
323
      this._setGeometryAttributes(this._gpuGeometry);
4✔
324
    }
4✔
325

4✔
326
    // Apply any dynamic settings that will not trigger pipeline change
4✔
327
    if ('isInstanced' in props) {
4✔
328
      this.isInstanced = props.isInstanced;
4✔
329
    }
4✔
330

4✔
331
    if (props.instanceCount) {
4!
332
      this.setInstanceCount(props.instanceCount);
×
333
    }
×
334
    if (props.vertexCount) {
4✔
335
      this.setVertexCount(props.vertexCount);
4✔
336
    }
4✔
337
    if (props.indexBuffer) {
4!
338
      this.setIndexBuffer(props.indexBuffer);
×
339
    }
×
340
    if (props.attributes) {
4✔
341
      this.setAttributes(props.attributes);
4✔
342
    }
4✔
343
    if (props.constantAttributes) {
4✔
344
      this.setConstantAttributes(props.constantAttributes);
4✔
345
    }
4✔
346
    if (props.bindings) {
4✔
347
      this.setBindings(props.bindings);
4✔
348
    }
4✔
349
    if (props.transformFeedback) {
4!
350
      this.transformFeedback = props.transformFeedback;
×
351
    }
×
352
  }
4✔
353

1✔
354
  destroy(): void {
1✔
355
    if (!this._destroyed) {
3✔
356
      // Release pipeline before we destroy the shaders used by the pipeline
3✔
357
      this.pipelineFactory.release(this.pipeline);
3✔
358
      // Release the shaders
3✔
359
      this.shaderFactory.release(this.pipeline.vs);
3✔
360
      if (this.pipeline.fs) {
3✔
361
        this.shaderFactory.release(this.pipeline.fs);
3✔
362
      }
3✔
363
      this._uniformStore.destroy();
3✔
364
      // TODO - mark resource as managed and destroyIfManaged() ?
3✔
365
      this._gpuGeometry?.destroy();
3✔
366
      this._destroyed = true;
3✔
367
    }
3✔
368
  }
3✔
369

1✔
370
  // Draw call
1✔
371

1✔
372
  /** Query redraw status. Clears the status. */
1✔
373
  needsRedraw(): false | string {
1✔
374
    // Catch any writes to already bound resources
×
375
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
×
376
      this.setNeedsRedraw('contents of bound textures or buffers updated');
×
377
    }
×
378
    const needsRedraw = this._needsRedraw;
×
379
    this._needsRedraw = false;
×
380
    return needsRedraw;
×
381
  }
×
382

1✔
383
  /** Mark the model as needing a redraw */
1✔
384
  setNeedsRedraw(reason: string): void {
1✔
385
    this._needsRedraw ||= reason;
47✔
386
  }
47✔
387

1✔
388
  /** Update uniforms and pipeline state prior to drawing. */
1✔
389
  predraw(): void {
1✔
390
    // Update uniform buffers if needed
2✔
391
    this.updateShaderInputs();
2✔
392
    // Check if the pipeline is invalidated
2✔
393
    this.pipeline = this._updatePipeline();
2✔
394
  }
2✔
395

1✔
396
  /**
1✔
397
   * Issue one draw call.
1✔
398
   * @param renderPass - render pass to draw into
1✔
399
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
1✔
400
   */
1✔
401
  draw(renderPass: RenderPass): boolean {
1✔
402
    const loadingBinding = this._areBindingsLoading();
2✔
403
    if (loadingBinding) {
2!
404
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
405
      return false;
×
406
    }
×
407

2✔
408
    try {
2✔
409
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
2✔
410
      this.predraw();
2✔
411
    } finally {
2✔
412
      renderPass.popDebugGroup();
2✔
413
    }
2✔
414

2✔
415
    let drawSuccess: boolean;
2✔
416
    try {
2✔
417
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
2✔
418
      this._logDrawCallStart();
2✔
419

2✔
420
      // Update the pipeline if invalidated
2✔
421
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
2✔
422
      // Application can call Model.predraw() to avoid this.
2✔
423
      this.pipeline = this._updatePipeline();
2✔
424

2✔
425
      // Set pipeline state, we may be sharing a pipeline so we need to set all state on every draw
2✔
426
      // Any caching needs to be done inside the pipeline functions
2✔
427
      // TODO this is a busy initialized check for all bindings every frame
2✔
428

2✔
429
      const syncBindings = this._getBindings();
2✔
430
      this.pipeline.setBindings(syncBindings, {
2✔
431
        disableWarnings: this.props.disableWarnings
2✔
432
      });
2✔
433

2✔
434
      const {indexBuffer} = this.vertexArray;
2✔
435
      const indexCount = indexBuffer
2!
436
        ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
×
437
        : undefined;
2✔
438

2✔
439
      drawSuccess = this.pipeline.draw({
2✔
440
        renderPass,
2✔
441
        vertexArray: this.vertexArray,
2✔
442
        isInstanced: this.isInstanced,
2✔
443
        vertexCount: this.vertexCount,
2✔
444
        instanceCount: this.instanceCount,
2✔
445
        indexCount,
2✔
446
        transformFeedback: this.transformFeedback || undefined,
2✔
447
        // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
2✔
448
        // so we must provide our unique parameters to each draw
2✔
449
        // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
2✔
450
        parameters: this.parameters,
2✔
451
        topology: this.topology
2✔
452
      });
2✔
453
    } finally {
2✔
454
      renderPass.popDebugGroup();
2✔
455
      this._logDrawCallEnd();
2✔
456
    }
2✔
457
    this._logFramebuffer(renderPass);
2✔
458

2✔
459
    // Update needsRedraw flag
2✔
460
    if (drawSuccess) {
2✔
461
      this._lastDrawTimestamp = this.device.timestamp;
2✔
462
      this._needsRedraw = false;
2✔
463
    } else {
2!
464
      this._needsRedraw = 'waiting for resource initialization';
×
465
    }
×
466
    return drawSuccess;
2✔
467
  }
2✔
468

1✔
469
  // Update fixed fields (can trigger pipeline rebuild)
1✔
470

1✔
471
  /**
1✔
472
   * Updates the optional geometry
1✔
473
   * Geometry, set topology and bufferLayout
1✔
474
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
1✔
475
   */
1✔
476
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
1✔
477
    this._gpuGeometry?.destroy();
4!
478
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
4✔
479
    if (gpuGeometry) {
4✔
480
      this.setTopology(gpuGeometry.topology || 'triangle-list');
4!
481
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
4✔
482
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
4✔
483
        gpuGeometry.bufferLayout,
4✔
484
        this.bufferLayout
4✔
485
      );
4✔
486
      if (this.vertexArray) {
4!
487
        this._setGeometryAttributes(gpuGeometry);
×
488
      }
×
489
    }
4✔
490
    this._gpuGeometry = gpuGeometry;
4✔
491
  }
4✔
492

1✔
493
  /**
1✔
494
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
1✔
495
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
1✔
496
   */
1✔
497
  setTopology(topology: PrimitiveTopology): void {
1✔
498
    if (topology !== this.topology) {
4✔
499
      this.topology = topology;
4✔
500
      this._setPipelineNeedsUpdate('topology');
4✔
501
    }
4✔
502
  }
4✔
503

1✔
504
  /**
1✔
505
   * Updates the buffer layout.
1✔
506
   * @note Triggers a pipeline rebuild / pipeline cache fetch
1✔
507
   */
1✔
508
  setBufferLayout(bufferLayout: BufferLayout[]): void {
1✔
509
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
×
510
    this.bufferLayout = this._gpuGeometry
×
511
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
×
512
      : bufferLayout;
×
513
    this._setPipelineNeedsUpdate('bufferLayout');
×
514

×
515
    // Recreate the pipeline
×
516
    this.pipeline = this._updatePipeline();
×
517

×
518
    // vertex array needs to be updated if we update buffer layout,
×
519
    // but not if we update parameters
×
520
    this.vertexArray = this.device.createVertexArray({
×
521
      shaderLayout: this.pipeline.shaderLayout,
×
522
      bufferLayout: this.pipeline.bufferLayout
×
523
    });
×
524

×
525
    // Reapply geometry attributes to the new vertex array
×
526
    if (this._gpuGeometry) {
×
527
      this._setGeometryAttributes(this._gpuGeometry);
×
528
    }
×
529
  }
×
530

1✔
531
  /**
1✔
532
   * Set GPU parameters.
1✔
533
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
1✔
534
   * @param parameters
1✔
535
   */
1✔
536
  setParameters(parameters: RenderPipelineParameters) {
1✔
537
    if (!deepEqual(parameters, this.parameters, 2)) {
×
538
      this.parameters = parameters;
×
539
      this._setPipelineNeedsUpdate('parameters');
×
540
    }
×
541
  }
×
542

1✔
543
  // Update dynamic fields
1✔
544

1✔
545
  /**
1✔
546
   * Updates the instance count (used in draw calls)
1✔
547
   * @note Any attributes with stepMode=instance need to be at least this big
1✔
548
   */
1✔
549
  setInstanceCount(instanceCount: number): void {
1✔
UNCOV
550
    this.instanceCount = instanceCount;
×
UNCOV
551
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
×
UNCOV
552
    // but deck.gl sets instanceCount even for models that are not instanced.
×
UNCOV
553
    if (this.isInstanced === undefined && instanceCount > 0) {
×
UNCOV
554
      this.isInstanced = true;
×
UNCOV
555
    }
×
UNCOV
556
    this.setNeedsRedraw('instanceCount');
×
UNCOV
557
  }
×
558

1✔
559
  /**
1✔
560
   * Updates the vertex count (used in draw calls)
1✔
561
   * @note Any attributes with stepMode=vertex need to be at least this big
1✔
562
   */
1✔
563
  setVertexCount(vertexCount: number): void {
1✔
564
    this.vertexCount = vertexCount;
4✔
565
    this.setNeedsRedraw('vertexCount');
4✔
566
  }
4✔
567

1✔
568
  /** Set the shader inputs */
1✔
569
  setShaderInputs(shaderInputs: ShaderInputs): void {
1✔
570
    this.shaderInputs = shaderInputs;
4✔
571
    this._uniformStore = new UniformStore(this.shaderInputs.modules);
4✔
572
    // Create uniform buffer bindings for all modules that actually have uniforms
4✔
573
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
4✔
574
      if (shaderModuleHasUniforms(module)) {
3✔
575
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(this.device, moduleName);
2✔
576
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
2✔
577
      }
2✔
578
    }
3✔
579
    this.setNeedsRedraw('shaderInputs');
4✔
580
  }
4✔
581

1✔
582
  /** Update uniform buffers from the model's shader inputs */
1✔
583
  updateShaderInputs(): void {
1✔
584
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues());
2✔
585
    this.setBindings(this.shaderInputs.getBindingValues());
2✔
586
    // TODO - this is already tracked through buffer/texture update times?
2✔
587
    this.setNeedsRedraw('shaderInputs');
2✔
588
  }
2✔
589

1✔
590
  /**
1✔
591
   * Sets bindings (textures, samplers, uniform buffers)
1✔
592
   */
1✔
593
  setBindings(bindings: Record<string, Binding | DynamicTexture>): void {
1✔
594
    Object.assign(this.bindings, bindings);
9✔
595
    this.setNeedsRedraw('bindings');
9✔
596
  }
9✔
597

1✔
598
  /**
1✔
599
   * Updates optional transform feedback. WebGL only.
1✔
600
   */
1✔
601
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
1✔
UNCOV
602
    this.transformFeedback = transformFeedback;
×
UNCOV
603
    this.setNeedsRedraw('transformFeedback');
×
UNCOV
604
  }
×
605

1✔
606
  /**
1✔
607
   * Sets the index buffer
1✔
608
   * @todo - how to unset it if we change geometry?
1✔
609
   */
1✔
610
  setIndexBuffer(indexBuffer: Buffer | null): void {
1✔
611
    this.vertexArray.setIndexBuffer(indexBuffer);
4✔
612
    this.setNeedsRedraw('indexBuffer');
4✔
613
  }
4✔
614

1✔
615
  /**
1✔
616
   * Sets attributes (buffers)
1✔
617
   * @note Overrides any attributes previously set with the same name
1✔
618
   */
1✔
619
  setAttributes(buffers: Record<string, Buffer>, options?: {disableWarnings?: boolean}): void {
1✔
620
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
12✔
621
    if (buffers['indices']) {
12!
622
      log.warn(
×
623
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
×
624
      )();
×
625
    }
×
626

12✔
627
    // ensure bufferLayout order matches source layout so we bind
12✔
628
    // the correct buffers to the correct indices in webgpu.
12✔
629
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
12✔
630
      this.pipeline.shaderLayout,
12✔
631
      this.bufferLayout
12✔
632
    );
12✔
633
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
12✔
634

12✔
635
    // Check if all buffers have a layout
12✔
636
    for (const [bufferName, buffer] of Object.entries(buffers)) {
12✔
637
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
24✔
638
      if (!bufferLayout) {
24!
639
        if (!disableWarnings) {
×
640
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
641
        }
×
642
        continue; // eslint-disable-line no-continue
×
643
      }
×
644

24✔
645
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
24✔
646
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
24✔
647
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
24✔
648
      let set = false;
24✔
649
      for (const attributeName of attributeNames) {
24✔
650
        const attributeInfo = this._attributeInfos[attributeName];
24✔
651
        if (attributeInfo) {
24✔
652
          const location =
24✔
653
            this.device.type === 'webgpu'
24!
654
              ? bufferLayoutHelper.getBufferIndex(attributeInfo.bufferName)
×
655
              : attributeInfo.location;
24✔
656

24✔
657
          this.vertexArray.setBuffer(location, buffer);
24✔
658
          set = true;
24✔
659
        }
24✔
660
      }
24✔
661
      if (!set && !disableWarnings) {
24!
UNCOV
662
        log.warn(
×
UNCOV
663
          `Model(${this.id}): Ignoring buffer "${buffer.id}" for unknown attribute "${bufferName}"`
×
UNCOV
664
        )();
×
UNCOV
665
      }
×
666
    }
24✔
667
    this.setNeedsRedraw('attributes');
12✔
668
  }
12✔
669

1✔
670
  /**
1✔
671
   * Sets constant attributes
1✔
672
   * @note Overrides any attributes previously set with the same name
1✔
673
   * Constant attributes are only supported in WebGL, not in WebGPU
1✔
674
   * Any attribute that is disabled in the current vertex array object
1✔
675
   * is read from the context's global constant value for that attribute location.
1✔
676
   * @param constantAttributes
1✔
677
   */
1✔
678
  setConstantAttributes(
1✔
679
    attributes: Record<string, TypedArray>,
4✔
680
    options?: {disableWarnings?: boolean}
4✔
681
  ): void {
4✔
682
    for (const [attributeName, value] of Object.entries(attributes)) {
4!
683
      const attributeInfo = this._attributeInfos[attributeName];
×
684
      if (attributeInfo) {
×
685
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
686
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
687
        log.warn(
×
688
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
×
689
        )();
×
690
      }
×
691
    }
×
692
    this.setNeedsRedraw('constants');
4✔
693
  }
4✔
694

1✔
695
  // INTERNAL METHODS
1✔
696

1✔
697
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
1✔
698
  _areBindingsLoading(): string | false {
1✔
699
    for (const binding of Object.values(this.bindings)) {
2✔
700
      if (binding instanceof DynamicTexture && !binding.isReady) {
3!
701
        return binding.id;
×
702
      }
×
703
    }
3✔
704
    return false;
2✔
705
  }
2✔
706

1✔
707
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
1✔
708
  _getBindings(): Record<string, Binding> {
1✔
709
    const validBindings: Record<string, Binding> = {};
6✔
710

6✔
711
    for (const [name, binding] of Object.entries(this.bindings)) {
6✔
712
      if (binding instanceof DynamicTexture) {
5✔
713
        // Check that async textures are loaded
1✔
714
        if (binding.isReady) {
1✔
715
          validBindings[name] = binding.texture;
1✔
716
        }
1✔
717
      } else {
5✔
718
        validBindings[name] = binding;
4✔
719
      }
4✔
720
    }
5✔
721

6✔
722
    return validBindings;
6✔
723
  }
6✔
724

1✔
725
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
1✔
726
  _getBindingsUpdateTimestamp(): number {
1✔
727
    let timestamp = 0;
×
728
    for (const binding of Object.values(this.bindings)) {
×
729
      if (binding instanceof TextureView) {
×
730
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
731
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
732
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
733
      } else if (binding instanceof DynamicTexture) {
×
734
        timestamp = binding.texture
×
735
          ? Math.max(timestamp, binding.texture.updateTimestamp)
×
736
          : // The texture will become available in the future
×
737
            Infinity;
×
738
      } else if (!(binding instanceof Sampler)) {
×
739
        timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
×
740
      }
×
741
    }
×
742
    return timestamp;
×
743
  }
×
744

1✔
745
  /**
1✔
746
   * Updates the optional geometry attributes
1✔
747
   * Geometry, sets several attributes, indexBuffer, and also vertex count
1✔
748
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
1✔
749
   */
1✔
750
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
1✔
751
    // Filter geometry attribute so that we don't issue warnings for unused attributes
4✔
752
    const attributes = {...gpuGeometry.attributes};
4✔
753
    for (const [attributeName] of Object.entries(attributes)) {
4✔
754
      if (
12✔
755
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
12!
756
        attributeName !== 'positions'
×
757
      ) {
12!
758
        delete attributes[attributeName];
×
759
      }
×
760
    }
12✔
761

4✔
762
    // TODO - delete previous geometry?
4✔
763
    this.vertexCount = gpuGeometry.vertexCount;
4✔
764
    this.setIndexBuffer(gpuGeometry.indices || null);
4✔
765
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
4✔
766
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
4✔
767

4✔
768
    this.setNeedsRedraw('geometry attributes');
4✔
769
  }
4✔
770

1✔
771
  /** Mark pipeline as needing update */
1✔
772
  _setPipelineNeedsUpdate(reason: string): void {
1✔
773
    this._pipelineNeedsUpdate ||= reason;
4✔
774
    this.setNeedsRedraw(reason);
4✔
775
  }
4✔
776

1✔
777
  /** Update pipeline if needed */
1✔
778
  _updatePipeline(): RenderPipeline {
1✔
779
    if (this._pipelineNeedsUpdate) {
8✔
780
      let prevShaderVs: Shader | null = null;
4✔
781
      let prevShaderFs: Shader | null = null;
4✔
782
      if (this.pipeline) {
4!
UNCOV
783
        log.log(
×
UNCOV
784
          1,
×
UNCOV
785
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
×
UNCOV
786
        )();
×
UNCOV
787
        prevShaderVs = this.pipeline.vs;
×
UNCOV
788
        prevShaderFs = this.pipeline.fs;
×
UNCOV
789
      }
×
790

4✔
791
      this._pipelineNeedsUpdate = false;
4✔
792

4✔
793
      const vs = this.shaderFactory.createShader({
4✔
794
        id: `${this.id}-vertex`,
4✔
795
        stage: 'vertex',
4✔
796
        source: this.source || this.vs,
4✔
797
        debugShaders: this.props.debugShaders
4✔
798
      });
4✔
799

4✔
800
      let fs: Shader | null = null;
4✔
801
      if (this.source) {
4!
802
        fs = vs;
×
803
      } else if (this.fs) {
4✔
804
        fs = this.shaderFactory.createShader({
4✔
805
          id: `${this.id}-fragment`,
4✔
806
          stage: 'fragment',
4✔
807
          source: this.source || this.fs,
4✔
808
          debugShaders: this.props.debugShaders
4✔
809
        });
4✔
810
      }
4✔
811

4✔
812
      this.pipeline = this.pipelineFactory.createRenderPipeline({
4✔
813
        ...this.props,
4✔
814
        bufferLayout: this.bufferLayout,
4✔
815
        topology: this.topology,
4✔
816
        parameters: this.parameters,
4✔
817
        // TODO - why set bindings here when we reset them every frame?
4✔
818
        // Should we expose a BindGroup abstraction?
4✔
819
        bindings: this._getBindings(),
4✔
820
        vs,
4✔
821
        fs
4✔
822
      });
4✔
823

4✔
824
      this._attributeInfos = getAttributeInfosFromLayouts(
4✔
825
        this.pipeline.shaderLayout,
4✔
826
        this.bufferLayout
4✔
827
      );
4✔
828

4✔
829
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
4!
830
      if (prevShaderFs) this.shaderFactory.release(prevShaderFs);
4!
831
    }
4✔
832
    return this.pipeline;
8✔
833
  }
8✔
834

1✔
835
  /** Throttle draw call logging */
1✔
836
  _lastLogTime = 0;
1✔
837
  _logOpen = false;
1✔
838

1✔
839
  _logDrawCallStart(): void {
1✔
840
    // IF level is 4 or higher, log every frame.
2✔
841
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
2!
842
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
2!
843
      return;
2✔
844
    }
2!
845

×
846
    this._lastLogTime = Date.now();
×
847
    this._logOpen = true;
×
848

×
849
    log.group(LOG_DRAW_PRIORITY, `>>> DRAWING MODEL ${this.id}`, {collapsed: log.level <= 2})();
×
850
  }
2✔
851

1✔
852
  _logDrawCallEnd(): void {
1✔
853
    if (this._logOpen) {
2!
854
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
855

×
856
      // log.table(logLevel, attributeTable)();
×
857
      // log.table(logLevel, uniformTable)();
×
858
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
859

×
860
      const uniformTable = this.shaderInputs.getDebugTable();
×
861
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
862

×
863
      const attributeTable = this._getAttributeDebugTable();
×
864
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
865
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
866

×
867
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
868
      this._logOpen = false;
×
869
    }
×
870
  }
2✔
871

1✔
872
  protected _drawCount = 0;
1✔
873
  _logFramebuffer(renderPass: RenderPass): void {
1✔
874
    const debugFramebuffers = this.device.props.debugFramebuffers;
2✔
875
    this._drawCount++;
2✔
876
    // Update first 3 frames and then every 60 frames
2✔
877
    if (!debugFramebuffers) {
2✔
878
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
2✔
879
      return;
2✔
880
    }
2!
881
    // TODO - display framebuffer output in debug window
×
882
    const framebuffer = renderPass.props.framebuffer;
×
883
    if (framebuffer) {
×
884
      debugFramebuffer(framebuffer, {id: framebuffer.id, minimap: true});
×
885
      // log.image({logLevel: LOG_DRAW_PRIORITY, message: `${framebuffer.id} %c sup?`, image})();
×
886
    }
×
887
  }
2✔
888

1✔
889
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1✔
890
    const table: Record<string, Record<string, unknown>> = {};
×
891
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
892
      const values = this.vertexArray.attributes[attributeInfo.location];
×
893
      table[attributeInfo.location] = {
×
894
        name,
×
895
        type: attributeInfo.shaderType,
×
896
        values: values
×
897
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
×
898
          : 'null'
×
899
      };
×
900
    }
×
901
    if (this.vertexArray.indexBuffer) {
×
902
      const {indexBuffer} = this.vertexArray;
×
903
      const values =
×
904
        indexBuffer.indexType === 'uint32'
×
905
          ? new Uint32Array(indexBuffer.debugData)
×
906
          : new Uint16Array(indexBuffer.debugData);
×
907
      table['indices'] = {
×
908
        name: 'indices',
×
909
        type: indexBuffer.indexType,
×
910
        values: values.toString()
×
911
      };
×
912
    }
×
913
    return table;
×
914
  }
×
915

1✔
916
  // TODO - fix typing of luma data types
1✔
917
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1✔
918
    const TypedArrayConstructor = getTypedArrayConstructor(dataType);
×
919
    const typedArray =
×
920
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
921
    return typedArray.toString();
×
922
  }
×
923
}
1✔
924

1✔
925
function shaderModuleHasUniforms(module: ShaderModule): boolean {
3✔
926
  return Boolean(module.uniformTypes && !isObjectEmpty(module.uniformTypes));
3✔
927
}
3✔
928

1✔
929
// HELPERS
1✔
930

1✔
931
/** Create a shadertools platform info from the Device */
1✔
932
export function getPlatformInfo(device: Device): PlatformInfo {
1✔
933
  return {
4✔
934
    type: device.type,
4✔
935
    shaderLanguage: device.info.shadingLanguage,
4✔
936
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
4✔
937
    gpu: device.info.gpu,
4✔
938
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
4✔
939
    features: device.features as unknown as Set<DeviceFeature>
4✔
940
  };
4✔
941
}
4✔
942

1✔
943
/** Returns true if given object is empty, false otherwise. */
1✔
944
function isObjectEmpty(obj: object): boolean {
2✔
945
  // @ts-ignore key is unused
2✔
946
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2✔
947
  for (const key in obj) {
2✔
948
    return false;
2✔
949
  }
2!
950
  return true;
×
951
}
×
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