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

visgl / luma.gl / 16894224360

11 Aug 2025 11:00PM UTC coverage: 74.376% (+0.004%) from 74.372%
16894224360

Pull #2419

github

web-flow
Merge 7366b5d43 into f3d70fc91
Pull Request #2419: chore: Rename AsyncTexture to DynamicTexture

2082 of 2713 branches covered (76.74%)

Branch coverage included in aggregate %.

47 of 68 new or added lines in 8 files covered. (69.12%)

29 existing lines in 2 files now uncovered.

27316 of 36813 relevant lines covered (74.2%)

57.12 hits per line

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

80.51
/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
 * v9 Model API
1✔
107
 * A model
1✔
108
 * - automatically reuses pipelines (programs) when possible
1✔
109
 * - automatically rebuilds pipelines if necessary to accommodate changed settings
1✔
110
 * shadertools integration
1✔
111
 * - accepts modules and performs shader transpilation
1✔
112
 */
1✔
113
export class Model {
1✔
114
  static defaultProps: Required<ModelProps> = {
1✔
115
    ...RenderPipeline.defaultProps,
1✔
116
    source: undefined!,
1✔
117
    vs: null,
1✔
118
    fs: null,
1✔
119
    id: 'unnamed',
1✔
120
    handle: undefined,
1✔
121
    userData: {},
1✔
122
    defines: {},
1✔
123
    modules: [],
1✔
124
    geometry: null,
1✔
125
    indexBuffer: null,
1✔
126
    attributes: {},
1✔
127
    constantAttributes: {},
1✔
128
    varyings: [],
1✔
129

1✔
130
    isInstanced: undefined!,
1✔
131
    instanceCount: 0,
1✔
132
    vertexCount: 0,
1✔
133

1✔
134
    shaderInputs: undefined!,
1✔
135
    pipelineFactory: undefined!,
1✔
136
    shaderFactory: undefined!,
1✔
137
    transformFeedback: undefined!,
1✔
138
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
1✔
139

1✔
140
    debugShaders: undefined!,
1✔
141
    disableWarnings: undefined!
1✔
142
  };
1✔
143

1✔
144
  readonly device: Device;
1✔
145
  readonly id: string;
1✔
146
  // @ts-expect-error assigned in function called from constructor
1✔
147
  readonly source: string;
1✔
148
  // @ts-expect-error assigned in function called from constructor
1✔
149
  readonly vs: string;
1✔
150
  // @ts-expect-error assigned in function called from constructor
1✔
151
  readonly fs: string;
1✔
152
  readonly pipelineFactory: PipelineFactory;
1✔
153
  readonly shaderFactory: ShaderFactory;
1✔
154
  userData: {[key: string]: any} = {};
1✔
155

1✔
156
  // Fixed properties (change can trigger pipeline rebuild)
1✔
157

1✔
158
  /** The render pipeline GPU parameters, depth testing etc */
1✔
159
  parameters: RenderPipelineParameters;
1✔
160

1✔
161
  /** The primitive topology */
1✔
162
  topology: PrimitiveTopology;
1✔
163
  /** Buffer layout */
1✔
164
  bufferLayout: BufferLayout[];
1✔
165

1✔
166
  // Dynamic properties
1✔
167

1✔
168
  /** Use instanced rendering */
1✔
169
  isInstanced: boolean | undefined = undefined;
1✔
170
  /** instance count. `undefined` means not instanced */
1✔
171
  instanceCount: number = 0;
1✔
172
  /** Vertex count */
1✔
173
  vertexCount: number;
1✔
174

1✔
175
  /** Index buffer */
1✔
176
  indexBuffer: Buffer | null = null;
1✔
177
  /** Buffer-valued attributes */
1✔
178
  bufferAttributes: Record<string, Buffer> = {};
1✔
179
  /** Constant-valued attributes */
1✔
180
  constantAttributes: Record<string, TypedArray> = {};
1✔
181
  /** Bindings (textures, samplers, uniform buffers) */
1✔
182
  bindings: Record<string, Binding | DynamicTexture> = {};
1✔
183

1✔
184
  /**
1✔
185
   * VertexArray
1✔
186
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
1✔
187
   * @todo - allow application to define multiple vertex arrays?
1✔
188
   * */
1✔
189
  vertexArray: VertexArray;
1✔
190

1✔
191
  /** TransformFeedback, WebGL 2 only. */
1✔
192
  transformFeedback: TransformFeedback | null = null;
1✔
193

1✔
194
  /** The underlying GPU "program". @note May be recreated if parameters change */
1✔
195
  pipeline: RenderPipeline;
1✔
196

1✔
197
  /** ShaderInputs instance */
1✔
198
  // @ts-expect-error Assigned in function called by constructor
1✔
199
  shaderInputs: ShaderInputs;
1✔
200
  // @ts-expect-error Assigned in function called by constructor
1✔
201
  _uniformStore: UniformStore;
1✔
202

1✔
203
  _attributeInfos: Record<string, AttributeInfo> = {};
1✔
204
  _gpuGeometry: GPUGeometry | null = null;
1✔
205
  private props: Required<ModelProps>;
1✔
206

1✔
207
  _pipelineNeedsUpdate: string | false = 'newly created';
1✔
208
  private _needsRedraw: string | false = 'initializing';
1✔
209
  private _destroyed = false;
1✔
210

1✔
211
  /** "Time" of last draw. Monotonically increasing timestamp */
1✔
212
  _lastDrawTimestamp: number = -1;
1✔
213

1✔
214
  get [Symbol.toStringTag](): string {
1✔
215
    return 'Model';
×
216
  }
×
217

1✔
218
  toString(): string {
1✔
219
    return `Model(${this.id})`;
14✔
220
  }
14✔
221

1✔
222
  constructor(device: Device, props: ModelProps) {
1✔
223
    this.props = {...Model.defaultProps, ...props};
21✔
224
    props = this.props;
21✔
225
    this.id = props.id || uid('model');
21!
226
    this.device = device;
21✔
227

21✔
228
    Object.assign(this.userData, props.userData);
21✔
229

21✔
230
    // Setup shader module inputs
21✔
231
    const moduleMap = Object.fromEntries(
21✔
232
      this.props.modules?.map(module => [module.name, module]) || []
21!
233
    );
21✔
234

21✔
235
    const shaderInputs =
21✔
236
      props.shaderInputs ||
21✔
237
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
21✔
238
    // @ts-ignore
21✔
239
    this.setShaderInputs(shaderInputs);
21✔
240

21✔
241
    // Setup shader assembler
21✔
242
    const platformInfo = getPlatformInfo(device);
21✔
243

21✔
244
    // Extract modules from shader inputs if not supplied
21✔
245
    const modules =
21✔
246
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
21✔
247
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
21!
248

21✔
249
    const isWebGPU = this.device.type === 'webgpu';
21✔
250

21✔
251
    // WebGPU
21✔
252
    // TODO - hack to support unified WGSL shader
21✔
253
    // TODO - this is wrong, compile a single shader
21✔
254
    if (isWebGPU && this.props.source) {
21!
255
      // WGSL
×
256
      const {source, getUniforms} = this.props.shaderAssembler.assembleWGSLShader({
×
257
        platformInfo,
×
258
        ...this.props,
×
259
        modules
×
260
      });
×
261
      this.source = source;
×
262
      // @ts-expect-error
×
263
      this._getModuleUniforms = getUniforms;
×
264
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
×
265
      // @ts-expect-error Method on WebGPUDevice
×
UNCOV
266
      this.props.shaderLayout ||= device.getShaderLayout(this.source);
×
267
    } else {
21✔
268
      // GLSL
21✔
269
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
21✔
270
        platformInfo,
21✔
271
        ...this.props,
21✔
272
        modules
21✔
273
      });
21✔
274

21✔
275
      this.vs = vs;
21✔
276
      this.fs = fs;
21✔
277
      // @ts-expect-error
21✔
278
      this._getModuleUniforms = getUniforms;
21✔
279
    }
21✔
280

21✔
281
    this.vertexCount = this.props.vertexCount;
21✔
282
    this.instanceCount = this.props.instanceCount;
21✔
283

21✔
284
    this.topology = this.props.topology;
21✔
285
    this.bufferLayout = this.props.bufferLayout;
21✔
286
    this.parameters = this.props.parameters;
21✔
287

21✔
288
    // Geometry, if provided, sets topology and vertex cound
21✔
289
    if (props.geometry) {
21✔
290
      this.setGeometry(props.geometry);
3✔
291
    }
3✔
292

21✔
293
    this.pipelineFactory =
21✔
294
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
21✔
295
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
21✔
296

21✔
297
    // Create the pipeline
21✔
298
    // @note order is important
21✔
299
    this.pipeline = this._updatePipeline();
21✔
300

21✔
301
    this.vertexArray = device.createVertexArray({
21✔
302
      shaderLayout: this.pipeline.shaderLayout,
21✔
303
      bufferLayout: this.pipeline.bufferLayout
21✔
304
    });
21✔
305

21✔
306
    // Now we can apply geometry attributes
21✔
307
    if (this._gpuGeometry) {
21✔
308
      this._setGeometryAttributes(this._gpuGeometry);
3✔
309
    }
3✔
310

21✔
311
    // Apply any dynamic settings that will not trigger pipeline change
21✔
312
    if ('isInstanced' in props) {
21✔
313
      this.isInstanced = props.isInstanced;
21✔
314
    }
21✔
315

21✔
316
    if (props.instanceCount) {
21!
317
      this.setInstanceCount(props.instanceCount);
×
UNCOV
318
    }
×
319
    if (props.vertexCount) {
21✔
320
      this.setVertexCount(props.vertexCount);
11✔
321
    }
11✔
322
    if (props.indexBuffer) {
21!
323
      this.setIndexBuffer(props.indexBuffer);
×
UNCOV
324
    }
×
325
    if (props.attributes) {
21✔
326
      this.setAttributes(props.attributes);
20✔
327
    }
20✔
328
    if (props.constantAttributes) {
21✔
329
      this.setConstantAttributes(props.constantAttributes);
21✔
330
    }
21✔
331
    if (props.bindings) {
21✔
332
      this.setBindings(props.bindings);
21✔
333
    }
21✔
334
    if (props.transformFeedback) {
21!
335
      this.transformFeedback = props.transformFeedback;
×
UNCOV
336
    }
×
337

21✔
338
    // Catch any access to non-standard props
21✔
339
    Object.seal(this);
21✔
340
  }
21✔
341

1✔
342
  destroy(): void {
1✔
343
    if (!this._destroyed) {
10✔
344
      // Release pipeline before we destroy the shaders used by the pipeline
9✔
345
      this.pipelineFactory.release(this.pipeline);
9✔
346
      // Release the shaders
9✔
347
      this.shaderFactory.release(this.pipeline.vs);
9✔
348
      if (this.pipeline.fs) {
9✔
349
        this.shaderFactory.release(this.pipeline.fs);
9✔
350
      }
9✔
351
      this._uniformStore.destroy();
9✔
352
      // TODO - mark resource as managed and destroyIfManaged() ?
9✔
353
      this._gpuGeometry?.destroy();
9!
354
      this._destroyed = true;
9✔
355
    }
9✔
356
  }
10✔
357

1✔
358
  // Draw call
1✔
359

1✔
360
  /** Query redraw status. Clears the status. */
1✔
361
  needsRedraw(): false | string {
1✔
362
    // Catch any writes to already bound resources
×
363
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
×
364
      this.setNeedsRedraw('contents of bound textures or buffers updated');
×
365
    }
×
366
    const needsRedraw = this._needsRedraw;
×
367
    this._needsRedraw = false;
×
368
    return needsRedraw;
×
UNCOV
369
  }
×
370

1✔
371
  /** Mark the model as needing a redraw */
1✔
372
  setNeedsRedraw(reason: string): void {
1✔
373
    this._needsRedraw ||= reason;
128✔
374
  }
128✔
375

1✔
376
  predraw(): void {
1✔
377
    // Update uniform buffers if needed
7✔
378
    this.updateShaderInputs();
7✔
379
    // Check if the pipeline is invalidated
7✔
380
    this.pipeline = this._updatePipeline();
7✔
381
  }
7✔
382

1✔
383
  draw(renderPass: RenderPass): boolean {
1✔
384
    const loadingBinding = this._areBindingsLoading();
7✔
385
    if (loadingBinding) {
7!
386
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
387
      return false;
×
UNCOV
388
    }
×
389

7✔
390
    try {
7✔
391
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
7✔
392
      this.predraw();
7✔
393
    } finally {
7✔
394
      renderPass.popDebugGroup();
7✔
395
    }
7✔
396

7✔
397
    let drawSuccess: boolean;
7✔
398
    try {
7✔
399
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
7✔
400
      this._logDrawCallStart();
7✔
401

7✔
402
      // Update the pipeline if invalidated
7✔
403
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
7✔
404
      // Application can call Model.predraw() to avoid this.
7✔
405
      this.pipeline = this._updatePipeline();
7✔
406

7✔
407
      // Set pipeline state, we may be sharing a pipeline so we need to set all state on every draw
7✔
408
      // Any caching needs to be done inside the pipeline functions
7✔
409
      // TODO this is a busy initialized check for all bindings every frame
7✔
410

7✔
411
      const syncBindings = this._getBindings();
7✔
412
      this.pipeline.setBindings(syncBindings, {
7✔
413
        disableWarnings: this.props.disableWarnings
7✔
414
      });
7✔
415

7✔
416
      const {indexBuffer} = this.vertexArray;
7✔
417
      const indexCount = indexBuffer
7!
UNCOV
418
        ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
×
419
        : undefined;
7✔
420

7✔
421
      drawSuccess = this.pipeline.draw({
7✔
422
        renderPass,
7✔
423
        vertexArray: this.vertexArray,
7✔
424
        isInstanced: this.isInstanced,
7✔
425
        vertexCount: this.vertexCount,
7✔
426
        instanceCount: this.instanceCount,
7✔
427
        indexCount,
7✔
428
        transformFeedback: this.transformFeedback || undefined,
7✔
429
        // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
7✔
430
        // so we must provide our unique parameters to each draw
7✔
431
        // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
7✔
432
        parameters: this.parameters,
7✔
433
        topology: this.topology
7✔
434
      });
7✔
435
    } finally {
7✔
436
      renderPass.popDebugGroup();
7✔
437
      this._logDrawCallEnd();
7✔
438
    }
7✔
439
    this._logFramebuffer(renderPass);
7✔
440

7✔
441
    // Update needsRedraw flag
7✔
442
    if (drawSuccess) {
7✔
443
      this._lastDrawTimestamp = this.device.timestamp;
7✔
444
      this._needsRedraw = false;
7✔
445
    } else {
7!
446
      this._needsRedraw = 'waiting for resource initialization';
×
UNCOV
447
    }
×
448
    return drawSuccess;
7✔
449
  }
7✔
450

1✔
451
  // Update fixed fields (can trigger pipeline rebuild)
1✔
452

1✔
453
  /**
1✔
454
   * Updates the optional geometry
1✔
455
   * Geometry, set topology and bufferLayout
1✔
456
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
1✔
457
   */
1✔
458
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
1✔
459
    this._gpuGeometry?.destroy();
3!
460
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
3✔
461
    if (gpuGeometry) {
3✔
462
      this.setTopology(gpuGeometry.topology || 'triangle-list');
3!
463
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
3✔
464
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
3✔
465
        gpuGeometry.bufferLayout,
3✔
466
        this.bufferLayout
3✔
467
      );
3✔
468
      if (this.vertexArray) {
3!
469
        this._setGeometryAttributes(gpuGeometry);
×
UNCOV
470
      }
×
471
    }
3✔
472
    this._gpuGeometry = gpuGeometry;
3✔
473
  }
3✔
474

1✔
475
  /**
1✔
476
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
1✔
477
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
1✔
478
   */
1✔
479
  setTopology(topology: PrimitiveTopology): void {
1✔
480
    if (topology !== this.topology) {
5✔
481
      this.topology = topology;
2✔
482
      this._setPipelineNeedsUpdate('topology');
2✔
483
    }
2✔
484
  }
5✔
485

1✔
486
  /**
1✔
487
   * Updates the buffer layout.
1✔
488
   * @note Triggers a pipeline rebuild / pipeline cache fetch
1✔
489
   */
1✔
490
  setBufferLayout(bufferLayout: BufferLayout[]): void {
1✔
491
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
×
492
    this.bufferLayout = this._gpuGeometry
×
493
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
×
494
      : bufferLayout;
×
495
    this._setPipelineNeedsUpdate('bufferLayout');
×
496

×
497
    // Recreate the pipeline
×
498
    this.pipeline = this._updatePipeline();
×
499

×
500
    // vertex array needs to be updated if we update buffer layout,
×
501
    // but not if we update parameters
×
502
    this.vertexArray = this.device.createVertexArray({
×
503
      shaderLayout: this.pipeline.shaderLayout,
×
504
      bufferLayout: this.pipeline.bufferLayout
×
505
    });
×
506

×
507
    // Reapply geometry attributes to the new vertex array
×
508
    if (this._gpuGeometry) {
×
509
      this._setGeometryAttributes(this._gpuGeometry);
×
510
    }
×
UNCOV
511
  }
×
512

1✔
513
  /**
1✔
514
   * Set GPU parameters.
1✔
515
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
1✔
516
   * @param parameters
1✔
517
   */
1✔
518
  setParameters(parameters: RenderPipelineParameters) {
1✔
519
    if (!deepEqual(parameters, this.parameters, 2)) {
×
520
      this.parameters = parameters;
×
521
      this._setPipelineNeedsUpdate('parameters');
×
522
    }
×
UNCOV
523
  }
×
524

1✔
525
  // Update dynamic fields
1✔
526

1✔
527
  /**
1✔
528
   * Updates the instance count (used in draw calls)
1✔
529
   * @note Any attributes with stepMode=instance need to be at least this big
1✔
530
   */
1✔
531
  setInstanceCount(instanceCount: number): void {
1✔
532
    this.instanceCount = instanceCount;
1✔
533
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
1✔
534
    // but deck.gl sets instanceCount even for models that are not instanced.
1✔
535
    if (this.isInstanced === undefined && instanceCount > 0) {
1✔
536
      this.isInstanced = true;
1✔
537
    }
1✔
538
    this.setNeedsRedraw('instanceCount');
1✔
539
  }
1✔
540

1✔
541
  /**
1✔
542
   * Updates the vertex count (used in draw calls)
1✔
543
   * @note Any attributes with stepMode=vertex need to be at least this big
1✔
544
   */
1✔
545
  setVertexCount(vertexCount: number): void {
1✔
546
    this.vertexCount = vertexCount;
12✔
547
    this.setNeedsRedraw('vertexCount');
12✔
548
  }
12✔
549

1✔
550
  /** Set the shader inputs */
1✔
551
  setShaderInputs(shaderInputs: ShaderInputs): void {
1✔
552
    this.shaderInputs = shaderInputs;
21✔
553
    this._uniformStore = new UniformStore(this.shaderInputs.modules);
21✔
554
    // Create uniform buffer bindings for all modules that actually have uniforms
21✔
555
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
21✔
556
      if (shaderModuleHasUniforms(module)) {
9✔
557
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(this.device, moduleName);
9✔
558
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
9✔
559
      }
9✔
560
    }
9✔
561
    this.setNeedsRedraw('shaderInputs');
21✔
562
  }
21✔
563

1✔
564
  /** Update uniform buffers from the model's shader inputs */
1✔
565
  updateShaderInputs(): void {
1✔
566
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues());
7✔
567
    this.setBindings(this.shaderInputs.getBindingValues());
7✔
568
    // TODO - this is already tracked through buffer/texture update times?
7✔
569
    this.setNeedsRedraw('shaderInputs');
7✔
570
  }
7✔
571

1✔
572
  /**
1✔
573
   * Sets bindings (textures, samplers, uniform buffers)
1✔
574
   */
1✔
575
  setBindings(bindings: Record<string, Binding | DynamicTexture>): void {
1✔
576
    Object.assign(this.bindings, bindings);
28✔
577
    this.setNeedsRedraw('bindings');
28✔
578
  }
28✔
579

1✔
580
  /**
1✔
581
   * Updates optional transform feedback. WebGL only.
1✔
582
   */
1✔
583
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
1✔
584
    this.transformFeedback = transformFeedback;
3✔
585
    this.setNeedsRedraw('transformFeedback');
3✔
586
  }
3✔
587

1✔
588
  /**
1✔
589
   * Sets the index buffer
1✔
590
   * @todo - how to unset it if we change geometry?
1✔
591
   */
1✔
592
  setIndexBuffer(indexBuffer: Buffer | null): void {
1✔
593
    this.vertexArray.setIndexBuffer(indexBuffer);
3✔
594
    this.setNeedsRedraw('indexBuffer');
3✔
595
  }
3✔
596

1✔
597
  /**
1✔
598
   * Sets attributes (buffers)
1✔
599
   * @note Overrides any attributes previously set with the same name
1✔
600
   */
1✔
601
  setAttributes(buffers: Record<string, Buffer>, options?: {disableWarnings?: boolean}): void {
1✔
602
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
27✔
603
    if (buffers['indices']) {
27!
604
      log.warn(
×
605
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
×
606
      )();
×
UNCOV
607
    }
×
608

27✔
609
    // ensure bufferLayout order matches source layout so we bind
27✔
610
    // the correct buffers to the correct indices in webgpu.
27✔
611
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
27✔
612
      this.pipeline.shaderLayout,
27✔
613
      this.bufferLayout
27✔
614
    );
27✔
615
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
27✔
616

27✔
617
    // Check if all buffers have a layout
27✔
618
    for (const [bufferName, buffer] of Object.entries(buffers)) {
27✔
619
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
23✔
620
      if (!bufferLayout) {
23!
621
        if (!disableWarnings) {
×
622
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
623
        }
×
624
        continue; // eslint-disable-line no-continue
×
UNCOV
625
      }
×
626

23✔
627
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
23✔
628
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
23✔
629
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
23✔
630
      let set = false;
23✔
631
      for (const attributeName of attributeNames) {
23✔
632
        const attributeInfo = this._attributeInfos[attributeName];
23✔
633
        if (attributeInfo) {
23✔
634
          const location =
21✔
635
            this.device.type === 'webgpu'
21!
UNCOV
636
              ? bufferLayoutHelper.getBufferIndex(attributeInfo.bufferName)
×
637
              : attributeInfo.location;
21✔
638

21✔
639
          this.vertexArray.setBuffer(location, buffer);
21✔
640
          set = true;
21✔
641
        }
21✔
642
      }
23✔
643
      if (!set && !disableWarnings) {
23✔
644
        log.warn(
2✔
645
          `Model(${this.id}): Ignoring buffer "${buffer.id}" for unknown attribute "${bufferName}"`
2✔
646
        )();
2✔
647
      }
2✔
648
    }
23✔
649
    this.setNeedsRedraw('attributes');
27✔
650
  }
27✔
651

1✔
652
  /**
1✔
653
   * Sets constant attributes
1✔
654
   * @note Overrides any attributes previously set with the same name
1✔
655
   * Constant attributes are only supported in WebGL, not in WebGPU
1✔
656
   * Any attribute that is disabled in the current vertex array object
1✔
657
   * is read from the context's global constant value for that attribute location.
1✔
658
   * @param constantAttributes
1✔
659
   */
1✔
660
  setConstantAttributes(
1✔
661
    attributes: Record<string, TypedArray>,
21✔
662
    options?: {disableWarnings?: boolean}
21✔
663
  ): void {
21✔
664
    for (const [attributeName, value] of Object.entries(attributes)) {
21!
665
      const attributeInfo = this._attributeInfos[attributeName];
×
666
      if (attributeInfo) {
×
667
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
668
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
669
        log.warn(
×
670
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
×
671
        )();
×
672
      }
×
UNCOV
673
    }
×
674
    this.setNeedsRedraw('constants');
21✔
675
  }
21✔
676

1✔
677
  // INTERNAL METHODS
1✔
678

1✔
679
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
1✔
680
  _areBindingsLoading(): string | false {
1✔
681
    for (const binding of Object.values(this.bindings)) {
7✔
682
      if (binding instanceof DynamicTexture && !binding.isReady) {
2!
683
        return binding.id;
×
UNCOV
684
      }
×
685
    }
2✔
686
    return false;
7✔
687
  }
7✔
688

1✔
689
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
1✔
690
  _getBindings(): Record<string, Binding> {
1✔
691
    const validBindings: Record<string, Binding> = {};
29✔
692

29✔
693
    for (const [name, binding] of Object.entries(this.bindings)) {
29✔
694
      if (binding instanceof DynamicTexture) {
11!
695
        // Check that async textures are loaded
×
696
        if (binding.isReady) {
×
697
          validBindings[name] = binding.texture;
×
UNCOV
698
        }
×
699
      } else {
11✔
700
        validBindings[name] = binding;
11✔
701
      }
11✔
702
    }
11✔
703

29✔
704
    return validBindings;
29✔
705
  }
29✔
706

1✔
707
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
1✔
708
  _getBindingsUpdateTimestamp(): number {
1✔
709
    let timestamp = 0;
×
710
    for (const binding of Object.values(this.bindings)) {
×
711
      if (binding instanceof TextureView) {
×
712
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
713
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
NEW
714
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
715
      } else if (binding instanceof DynamicTexture) {
×
716
        timestamp = binding.texture
×
717
          ? Math.max(timestamp, binding.texture.updateTimestamp)
×
718
          : // The texture will become available in the future
×
719
            Infinity;
×
720
      } else if (!(binding instanceof Sampler)) {
×
721
        timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
×
722
      }
×
723
    }
×
724
    return timestamp;
×
UNCOV
725
  }
×
726

1✔
727
  /**
1✔
728
   * Updates the optional geometry attributes
1✔
729
   * Geometry, sets several attributes, indexBuffer, and also vertex count
1✔
730
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
1✔
731
   */
1✔
732
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
1✔
733
    // Filter geometry attribute so that we don't issue warnings for unused attributes
3✔
734
    const attributes = {...gpuGeometry.attributes};
3✔
735
    for (const [attributeName] of Object.entries(attributes)) {
3✔
736
      if (
6✔
737
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
6!
UNCOV
738
        attributeName !== 'positions'
×
739
      ) {
6!
740
        delete attributes[attributeName];
×
UNCOV
741
      }
×
742
    }
6✔
743

3✔
744
    // TODO - delete previous geometry?
3✔
745
    this.vertexCount = gpuGeometry.vertexCount;
3✔
746
    this.setIndexBuffer(gpuGeometry.indices || null);
3!
747
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
3✔
748
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
3✔
749

3✔
750
    this.setNeedsRedraw('geometry attributes');
3✔
751
  }
3✔
752

1✔
753
  /** Mark pipeline as needing update */
1✔
754
  _setPipelineNeedsUpdate(reason: string): void {
1✔
755
    this._pipelineNeedsUpdate ||= reason;
2✔
756
    this.setNeedsRedraw(reason);
2✔
757
  }
2✔
758

1✔
759
  /** Update pipeline if needed */
1✔
760
  _updatePipeline(): RenderPipeline {
1✔
761
    if (this._pipelineNeedsUpdate) {
35✔
762
      let prevShaderVs: Shader | null = null;
22✔
763
      let prevShaderFs: Shader | null = null;
22✔
764
      if (this.pipeline) {
22✔
765
        log.log(
1✔
766
          1,
1✔
767
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
1✔
768
        )();
1✔
769
        prevShaderVs = this.pipeline.vs;
1✔
770
        prevShaderFs = this.pipeline.fs;
1✔
771
      }
1✔
772

22✔
773
      this._pipelineNeedsUpdate = false;
22✔
774

22✔
775
      const vs = this.shaderFactory.createShader({
22✔
776
        id: `${this.id}-vertex`,
22✔
777
        stage: 'vertex',
22✔
778
        source: this.source || this.vs,
22✔
779
        debugShaders: this.props.debugShaders
22✔
780
      });
22✔
781

22✔
782
      let fs: Shader | null = null;
22✔
783
      if (this.source) {
22!
UNCOV
784
        fs = vs;
×
785
      } else if (this.fs) {
22✔
786
        fs = this.shaderFactory.createShader({
22✔
787
          id: `${this.id}-fragment`,
22✔
788
          stage: 'fragment',
22✔
789
          source: this.source || this.fs,
22✔
790
          debugShaders: this.props.debugShaders
22✔
791
        });
22✔
792
      }
22✔
793

22✔
794
      this.pipeline = this.pipelineFactory.createRenderPipeline({
22✔
795
        ...this.props,
22✔
796
        bufferLayout: this.bufferLayout,
22✔
797
        topology: this.topology,
22✔
798
        parameters: this.parameters,
22✔
799
        // TODO - why set bindings here when we reset them every frame?
22✔
800
        // Should we expose a BindGroup abstraction?
22✔
801
        bindings: this._getBindings(),
22✔
802
        vs,
22✔
803
        fs
22✔
804
      });
22✔
805

22✔
806
      this._attributeInfos = getAttributeInfosFromLayouts(
22✔
807
        this.pipeline.shaderLayout,
22✔
808
        this.bufferLayout
22✔
809
      );
22✔
810

22✔
811
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
22✔
812
      if (prevShaderFs) this.shaderFactory.release(prevShaderFs);
22✔
813
    }
22✔
814
    return this.pipeline;
35✔
815
  }
35✔
816

1✔
817
  /** Throttle draw call logging */
1✔
818
  _lastLogTime = 0;
1✔
819
  _logOpen = false;
1✔
820

1✔
821
  _logDrawCallStart(): void {
1✔
822
    // IF level is 4 or higher, log every frame.
7✔
823
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
7!
824
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
7!
825
      return;
7✔
826
    }
7!
827

×
828
    this._lastLogTime = Date.now();
×
829
    this._logOpen = true;
×
830

×
UNCOV
831
    log.group(LOG_DRAW_PRIORITY, `>>> DRAWING MODEL ${this.id}`, {collapsed: log.level <= 2})();
×
832
  }
7✔
833

1✔
834
  _logDrawCallEnd(): void {
1✔
835
    if (this._logOpen) {
7!
836
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
837

×
838
      // log.table(logLevel, attributeTable)();
×
839
      // log.table(logLevel, uniformTable)();
×
840
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
841

×
842
      const uniformTable = this.shaderInputs.getDebugTable();
×
843
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
844

×
845
      const attributeTable = this._getAttributeDebugTable();
×
846
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
847
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
848

×
849
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
850
      this._logOpen = false;
×
UNCOV
851
    }
×
852
  }
7✔
853

1✔
854
  protected _drawCount = 0;
1✔
855
  _logFramebuffer(renderPass: RenderPass): void {
1✔
856
    const debugFramebuffers = this.device.props.debugFramebuffers;
7✔
857
    this._drawCount++;
7✔
858
    // Update first 3 frames and then every 60 frames
7✔
859
    if (!debugFramebuffers) {
7✔
860
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
7✔
861
      return;
7✔
862
    }
7!
863
    // TODO - display framebuffer output in debug window
×
864
    const framebuffer = renderPass.props.framebuffer;
×
865
    if (framebuffer) {
×
866
      debugFramebuffer(framebuffer, {id: framebuffer.id, minimap: true});
×
867
      // log.image({logLevel: LOG_DRAW_PRIORITY, message: `${framebuffer.id} %c sup?`, image})();
×
UNCOV
868
    }
×
869
  }
7✔
870

1✔
871
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1✔
872
    const table: Record<string, Record<string, unknown>> = {};
×
873
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
874
      const values = this.vertexArray.attributes[attributeInfo.location];
×
875
      table[attributeInfo.location] = {
×
876
        name,
×
877
        type: attributeInfo.shaderType,
×
878
        values: values
×
879
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
×
880
          : 'null'
×
881
      };
×
882
    }
×
883
    if (this.vertexArray.indexBuffer) {
×
884
      const {indexBuffer} = this.vertexArray;
×
885
      const values =
×
886
        indexBuffer.indexType === 'uint32'
×
887
          ? new Uint32Array(indexBuffer.debugData)
×
888
          : new Uint16Array(indexBuffer.debugData);
×
889
      table['indices'] = {
×
890
        name: 'indices',
×
891
        type: indexBuffer.indexType,
×
892
        values: values.toString()
×
893
      };
×
894
    }
×
895
    return table;
×
UNCOV
896
  }
×
897

1✔
898
  // TODO - fix typing of luma data types
1✔
899
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1✔
900
    const TypedArrayConstructor = getTypedArrayConstructor(dataType);
×
901
    const typedArray =
×
902
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
903
    return typedArray.toString();
×
UNCOV
904
  }
×
905
}
1✔
906

1✔
907
function shaderModuleHasUniforms(module: ShaderModule): boolean {
9✔
908
  return Boolean(module.uniformTypes && !isObjectEmpty(module.uniformTypes));
9✔
909
}
9✔
910

1✔
911
// HELPERS
1✔
912

1✔
913
/** Create a shadertools platform info from the Device */
1✔
914
export function getPlatformInfo(device: Device): PlatformInfo {
1✔
915
  return {
21✔
916
    type: device.type,
21✔
917
    shaderLanguage: device.info.shadingLanguage,
21✔
918
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
21✔
919
    gpu: device.info.gpu,
21✔
920
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
21✔
921
    features: device.features as unknown as Set<DeviceFeature>
21✔
922
  };
21✔
923
}
21✔
924

1✔
925
/** Returns true if given object is empty, false otherwise. */
1✔
926
function isObjectEmpty(obj: object): boolean {
9✔
927
  // @ts-ignore key is unused
9✔
928
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
9✔
929
  for (const key in obj) {
9✔
930
    return false;
9✔
931
  }
9!
932
  return true;
×
UNCOV
933
}
×
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