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

visgl / luma.gl / 23968747971

04 Apr 2026 01:48AM UTC coverage: 73.77% (+0.1%) from 73.637%
23968747971

push

github

web-flow
chore: Restore luma v6 globe example (#2575)

4945 of 7588 branches covered (65.17%)

Branch coverage included in aggregate %.

106 of 133 new or added lines in 12 files covered. (79.7%)

11069 of 14120 relevant lines covered (78.39%)

649.61 hits per line

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

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

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

38
import type {ShaderBindingDebugRow, ShaderModule, PlatformInfo} from '@luma.gl/shadertools';
39
import {ShaderAssembler} from '@luma.gl/shadertools';
40

41
import type {Geometry} from '../geometry/geometry';
42
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
43
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
44
import {debugFramebuffer} from '../debug/debug-framebuffer';
45
import {deepEqual} from '../utils/deep-equal';
46
import {BufferLayoutHelper} from '../utils/buffer-layout-helper';
47
import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';
48
import {
49
  mergeShaderModuleBindingsIntoLayout,
50
  shaderModuleHasUniforms
51
} from '../utils/shader-module-utils';
52
import {uid} from '../utils/uid';
53
import {ShaderInputs} from '../shader-inputs';
54
import {DynamicTexture} from '../dynamic-texture/dynamic-texture';
55
import {Material} from '../material/material';
56

57
const LOG_DRAW_PRIORITY = 2;
70✔
58
const LOG_DRAW_TIMEOUT = 10000;
70✔
59
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
70✔
60
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
70✔
61
  'stencil8',
62
  'depth16unorm',
63
  'depth24plus',
64
  'depth24plus-stencil8',
65
  'depth32float',
66
  'depth32float-stencil8'
67
];
68
type ModelBinding = Binding | DynamicTexture;
69

70
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
71
  source?: string;
72
  vs?: string | null;
73
  fs?: string | null;
74

75
  /** shadertool shader modules (added to shader code) */
76
  modules?: ShaderModule[];
77
  /** Shadertool module defines (configures shader code)*/
78
  defines?: Record<string, boolean>;
79
  // TODO - injections, hooks etc?
80

81
  /** Shader inputs, used to generated uniform buffers and bindings */
82
  shaderInputs?: ShaderInputs;
83
  /** Material-owned group-3 bindings */
84
  material?: Material;
85
  /** Bindings */
86
  bindings?: Record<string, Binding | DynamicTexture>;
87
  /** WebGL-only uniforms */
88
  uniforms?: Record<string, unknown>;
89
  /** Parameters that are built into the pipeline */
90
  parameters?: RenderPipelineParameters;
91

92
  /** Geometry */
93
  geometry?: GPUGeometry | Geometry | null;
94

95
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
96
  isInstanced?: boolean;
97
  /** instance count */
98
  instanceCount?: number;
99
  /** Vertex count */
100
  vertexCount?: number;
101

102
  indexBuffer?: Buffer | null;
103
  /** @note this is really a map of buffers, not a map of attributes */
104
  attributes?: Record<string, Buffer>;
105
  /**   */
106
  constantAttributes?: Record<string, TypedArray>;
107

108
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
109
  disableWarnings?: boolean;
110

111
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
112
  varyings?: string[];
113

114
  transformFeedback?: TransformFeedback;
115

116
  /** Show shader source in browser? */
117
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
118

119
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
120
  pipelineFactory?: PipelineFactory;
121
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
122
  shaderFactory?: ShaderFactory;
123
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
124
  shaderAssembler?: ShaderAssembler;
125
};
126

127
/**
128
 * High level draw API for luma.gl.
129
 *
130
 * A `Model` encapsulates shaders, geometry attributes, bindings and render
131
 * pipeline state into a single object. It automatically reuses and rebuilds
132
 * pipelines as render parameters change and exposes convenient hooks for
133
 * updating uniforms and attributes.
134
 *
135
 * Features:
136
 * - Reuses and lazily recompiles {@link RenderPipeline | pipelines} as needed.
137
 * - Integrates with `@luma.gl/shadertools` to assemble GLSL or WGSL from shader modules.
138
 * - Manages geometry attributes and buffer bindings.
139
 * - Accepts textures, samplers and uniform buffers as bindings, including `DynamicTexture`.
140
 * - Provides detailed debug logging and optional shader source inspection.
141
 */
142
export class Model {
143
  static defaultProps: Required<ModelProps> = {
70✔
144
    ...RenderPipeline.defaultProps,
145
    source: undefined!,
146
    vs: null,
147
    fs: null,
148
    id: 'unnamed',
149
    handle: undefined,
150
    userData: {},
151
    defines: {},
152
    modules: [],
153
    geometry: null,
154
    indexBuffer: null,
155
    attributes: {},
156
    constantAttributes: {},
157
    bindings: {},
158
    uniforms: {},
159
    varyings: [],
160

161
    isInstanced: undefined!,
162
    instanceCount: 0,
163
    vertexCount: 0,
164

165
    shaderInputs: undefined!,
166
    material: undefined!,
167
    pipelineFactory: undefined!,
168
    shaderFactory: undefined!,
169
    transformFeedback: undefined!,
170
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
171

172
    debugShaders: undefined!,
173
    disableWarnings: undefined!
174
  };
175

176
  /** Device that created this model */
177
  readonly device: Device;
178
  /** Application provided identifier */
179
  readonly id: string;
180
  /** WGSL shader source when using unified shader */
181
  // @ts-expect-error assigned in function called from constructor
182
  readonly source: string;
183
  /** GLSL vertex shader source */
184
  // @ts-expect-error assigned in function called from constructor
185
  readonly vs: string;
186
  /** GLSL fragment shader source */
187
  // @ts-expect-error assigned in function called from constructor
188
  readonly fs: string;
189
  /** Factory used to create render pipelines */
190
  readonly pipelineFactory: PipelineFactory;
191
  /** Factory used to create shaders */
192
  readonly shaderFactory: ShaderFactory;
193
  /** User-supplied per-model data */
194
  userData: {[key: string]: any} = {};
76✔
195

196
  // Fixed properties (change can trigger pipeline rebuild)
197

198
  /** The render pipeline GPU parameters, depth testing etc */
199
  parameters: RenderPipelineParameters;
200

201
  /** The primitive topology */
202
  topology: PrimitiveTopology;
203
  /** Buffer layout */
204
  bufferLayout: BufferLayout[];
205

206
  // Dynamic properties
207

208
  /** Use instanced rendering */
209
  isInstanced: boolean | undefined = undefined;
76✔
210
  /** instance count. `undefined` means not instanced */
211
  instanceCount: number = 0;
76✔
212
  /** Vertex count */
213
  vertexCount: number;
214

215
  /** Index buffer */
216
  indexBuffer: Buffer | null = null;
76✔
217
  /** Buffer-valued attributes */
218
  bufferAttributes: Record<string, Buffer> = {};
76✔
219
  /** Constant-valued attributes */
220
  constantAttributes: Record<string, TypedArray> = {};
76✔
221
  /** Bindings (textures, samplers, uniform buffers) */
222
  bindings: Record<string, Binding | DynamicTexture> = {};
76✔
223

224
  /**
225
   * VertexArray
226
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
227
   * @todo - allow application to define multiple vertex arrays?
228
   * */
229
  vertexArray: VertexArray;
230

231
  /** TransformFeedback, WebGL 2 only. */
232
  transformFeedback: TransformFeedback | null = null;
76✔
233

234
  /** The underlying GPU "program". @note May be recreated if parameters change */
235
  pipeline: RenderPipeline;
236

237
  /** ShaderInputs instance */
238
  // @ts-expect-error Assigned in function called by constructor
239
  shaderInputs: ShaderInputs;
240
  material: Material | null = null;
76✔
241
  // @ts-expect-error Assigned in function called by constructor
242
  _uniformStore: UniformStore;
243

244
  _attributeInfos: Record<string, AttributeInfo> = {};
76✔
245
  _gpuGeometry: GPUGeometry | null = null;
76✔
246
  private props: Required<ModelProps>;
247
  private _colorAttachmentFormats: (TextureFormatColor | null)[] | undefined;
248
  private _depthStencilAttachmentFormat: TextureFormatDepthStencil | undefined;
249

250
  _pipelineNeedsUpdate: string | false = 'newly created';
76✔
251
  private _needsRedraw: string | false = 'initializing';
76✔
252
  private _destroyed = false;
76✔
253

254
  /** "Time" of last draw. Monotonically increasing timestamp */
255
  _lastDrawTimestamp: number = -1;
76✔
256
  private _bindingTable: ShaderBindingDebugRow[] = [];
76✔
257

258
  get [Symbol.toStringTag](): string {
259
    return 'Model';
×
260
  }
261

262
  toString(): string {
263
    return `Model(${this.id})`;
86✔
264
  }
265

266
  constructor(device: Device, props: ModelProps) {
267
    this.props = {...Model.defaultProps, ...props};
76✔
268
    props = this.props;
76✔
269
    this.id = props.id || uid('model');
76!
270
    this.device = device;
76✔
271

272
    Object.assign(this.userData, props.userData);
76✔
273

274
    this.material = props.material || null;
76✔
275

276
    // Setup shader module inputs
277
    const moduleMap = Object.fromEntries(
76✔
278
      this.props.modules?.map(module => [module.name, module]) || []
44!
279
    );
280

281
    const shaderInputs =
282
      props.shaderInputs ||
76✔
283
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
284
    // @ts-ignore
285
    this.setShaderInputs(shaderInputs);
76✔
286

287
    // Setup shader assembler
288
    const platformInfo = getPlatformInfo(device);
76✔
289

290
    // Extract modules from shader inputs if not supplied
291
    const modules =
76!
292
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
293
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
76✔
294

295
    this.props.shaderLayout =
76✔
296
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
152✔
297

298
    const isWebGPU = this.device.type === 'webgpu';
76✔
299

300
    // WebGPU
301
    // TODO - hack to support unified WGSL shader
302
    // TODO - this is wrong, compile a single shader
303
    if (isWebGPU && this.props.source) {
76✔
304
      // WGSL
305
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
6✔
306
        platformInfo,
307
        ...this.props,
308
        modules
309
      });
310
      this.source = source;
6✔
311
      // @ts-expect-error
312
      this._getModuleUniforms = getUniforms;
6✔
313
      this._bindingTable = bindingTable;
6✔
314
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
315
      const inferredShaderLayout = (
316
        device as Device & {getShaderLayout?: (source: string) => any}
6✔
317
      ).getShaderLayout?.(this.source);
318
      this.props.shaderLayout =
6✔
319
        mergeShaderModuleBindingsIntoLayout(
6!
320
          this.props.shaderLayout || inferredShaderLayout || null,
12!
321
          modules
322
        ) || null;
323
    } else {
324
      // GLSL
325
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
70✔
326
        platformInfo,
327
        ...this.props,
328
        modules
329
      });
330

331
      this.vs = vs;
70✔
332
      this.fs = fs;
70✔
333
      // @ts-expect-error
334
      this._getModuleUniforms = getUniforms;
70✔
335
      this._bindingTable = [];
70✔
336
    }
337

338
    this.vertexCount = this.props.vertexCount;
76✔
339
    this.instanceCount = this.props.instanceCount;
76✔
340

341
    this.topology = this.props.topology;
76✔
342
    this.bufferLayout = this.props.bufferLayout;
76✔
343
    this.parameters = this.props.parameters;
76✔
344

345
    // Geometry, if provided, sets topology and vertex cound
346
    if (props.geometry) {
76✔
347
      this.setGeometry(props.geometry);
34✔
348
    }
349

350
    this.pipelineFactory =
76✔
351
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
152✔
352
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
76✔
353

354
    // Create the pipeline
355
    // @note order is important
356
    this.pipeline = this._updatePipeline();
76✔
357

358
    this.vertexArray = device.createVertexArray({
76✔
359
      shaderLayout: this.pipeline.shaderLayout,
360
      bufferLayout: this.pipeline.bufferLayout
361
    });
362

363
    // Now we can apply geometry attributes
364
    if (this._gpuGeometry) {
76✔
365
      this._setGeometryAttributes(this._gpuGeometry);
34✔
366
    }
367

368
    // Apply any dynamic settings that will not trigger pipeline change
369
    if ('isInstanced' in props) {
76!
370
      this.isInstanced = props.isInstanced;
76✔
371
    }
372

373
    if (props.instanceCount) {
76✔
374
      this.setInstanceCount(props.instanceCount);
10✔
375
    }
376
    if (props.vertexCount) {
76✔
377
      this.setVertexCount(props.vertexCount);
44✔
378
    }
379
    if (props.indexBuffer) {
76!
380
      this.setIndexBuffer(props.indexBuffer);
×
381
    }
382
    if (props.attributes) {
76✔
383
      this.setAttributes(props.attributes);
75✔
384
    }
385
    if (props.constantAttributes) {
76!
386
      this.setConstantAttributes(props.constantAttributes);
76✔
387
    }
388
    if (props.bindings) {
76!
389
      this.setBindings(props.bindings);
76✔
390
    }
391
    if (props.transformFeedback) {
76!
392
      this.transformFeedback = props.transformFeedback;
×
393
    }
394
  }
395

396
  destroy(): void {
397
    if (!this._destroyed) {
56✔
398
      // Release pipeline before we destroy the shaders used by the pipeline
399
      this.pipelineFactory.release(this.pipeline);
55✔
400
      // Release the shaders
401
      this.shaderFactory.release(this.pipeline.vs);
55✔
402
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
55✔
403
        this.shaderFactory.release(this.pipeline.fs);
49✔
404
      }
405
      this._uniformStore.destroy();
55✔
406
      // TODO - mark resource as managed and destroyIfManaged() ?
407
      this._gpuGeometry?.destroy();
55✔
408
      this._destroyed = true;
55✔
409
    }
410
  }
411

412
  // Draw call
413

414
  /** Query redraw status. Clears the status. */
415
  needsRedraw(): false | string {
416
    // Catch any writes to already bound resources
417
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
1!
418
      this.setNeedsRedraw('contents of bound textures or buffers updated');
1✔
419
    }
420
    const needsRedraw = this._needsRedraw;
1✔
421
    this._needsRedraw = false;
1✔
422
    return needsRedraw;
1✔
423
  }
424

425
  /** Mark the model as needing a redraw */
426
  setNeedsRedraw(reason: string): void {
427
    this._needsRedraw ||= reason;
650✔
428
  }
429

430
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
431
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
432
    return this._bindingTable;
2✔
433
  }
434

435
  /** Update uniforms and pipeline state prior to drawing. */
436
  predraw(): void {
437
    // Update uniform buffers if needed
438
    this.updateShaderInputs();
43✔
439
    // Check if the pipeline is invalidated
440
    this.pipeline = this._updatePipeline();
43✔
441
  }
442

443
  /**
444
   * Issue one draw call.
445
   * @param renderPass - render pass to draw into
446
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
447
   */
448
  draw(renderPass: RenderPass): boolean {
449
    const loadingBinding = this._areBindingsLoading();
43✔
450
    if (loadingBinding) {
43!
451
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
452
      return false;
×
453
    }
454

455
    this._syncAttachmentFormats(renderPass);
43✔
456

457
    try {
43✔
458
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
43✔
459
      this.predraw();
43✔
460
    } finally {
461
      renderPass.popDebugGroup();
43✔
462
    }
463

464
    let drawSuccess: boolean;
465
    let pipelineErrored = this.pipeline.isErrored;
43✔
466
    try {
43✔
467
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
43✔
468
      this._logDrawCallStart();
43✔
469

470
      // Update the pipeline if invalidated
471
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
472
      // Application can call Model.predraw() to avoid this.
473
      this.pipeline = this._updatePipeline();
43✔
474
      pipelineErrored = this.pipeline.isErrored;
43✔
475

476
      if (pipelineErrored) {
43✔
477
        log.info(
2✔
478
          LOG_DRAW_PRIORITY,
479
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
480
        )();
481
        drawSuccess = false;
2✔
482
      } else {
483
        const syncBindings = this._getBindings();
41✔
484
        const syncBindGroups = this._getBindGroups();
41✔
485

486
        const {indexBuffer} = this.vertexArray;
41✔
487
        const indexCount = indexBuffer
41✔
488
          ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
12!
489
          : undefined;
490

491
        drawSuccess = this.pipeline.draw({
41✔
492
          renderPass,
493
          vertexArray: this.vertexArray,
494
          isInstanced: this.isInstanced,
495
          vertexCount: this.vertexCount,
496
          instanceCount: this.instanceCount,
497
          indexCount,
498
          transformFeedback: this.transformFeedback || undefined,
61✔
499
          // Pipelines may be shared across models when caching is enabled, so bindings
500
          // and WebGL uniforms must be supplied on every draw instead of being stored
501
          // on the pipeline instance.
502
          bindings: syncBindings,
503
          bindGroups: syncBindGroups,
504
          _bindGroupCacheKeys: this._getBindGroupCacheKeys(),
505
          uniforms: this.props.uniforms,
506
          // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
507
          // so we must provide our unique parameters to each draw
508
          // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
509
          parameters: this.parameters,
510
          topology: this.topology
511
        });
512
      }
513
    } finally {
514
      renderPass.popDebugGroup();
43✔
515
      this._logDrawCallEnd();
43✔
516
    }
517
    this._logFramebuffer(renderPass);
43✔
518

519
    // Update needsRedraw flag
520
    if (drawSuccess) {
43✔
521
      this._lastDrawTimestamp = this.device.timestamp;
41✔
522
      this._needsRedraw = false;
41✔
523
    } else if (pipelineErrored) {
2!
524
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
2✔
525
    } else {
526
      this._needsRedraw = 'waiting for resource initialization';
×
527
    }
528
    return drawSuccess;
43✔
529
  }
530

531
  // Update fixed fields (can trigger pipeline rebuild)
532

533
  /**
534
   * Updates the optional geometry
535
   * Geometry, set topology and bufferLayout
536
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
537
   */
538
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
539
    this._gpuGeometry?.destroy();
34✔
540
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
34✔
541
    if (gpuGeometry) {
34!
542
      this.setTopology(gpuGeometry.topology || 'triangle-list');
34!
543
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
34✔
544
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
34✔
545
        gpuGeometry.bufferLayout,
546
        this.bufferLayout
547
      );
548
      if (this.vertexArray) {
34!
549
        this._setGeometryAttributes(gpuGeometry);
×
550
      }
551
    }
552
    this._gpuGeometry = gpuGeometry;
34✔
553
  }
554

555
  /**
556
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
557
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
558
   */
559
  setTopology(topology: PrimitiveTopology): void {
560
    if (topology !== this.topology) {
37✔
561
      this.topology = topology;
8✔
562
      this._setPipelineNeedsUpdate('topology');
8✔
563
    }
564
  }
565

566
  /**
567
   * Updates the buffer layout.
568
   * @note Triggers a pipeline rebuild / pipeline cache fetch
569
   */
570
  setBufferLayout(bufferLayout: BufferLayout[]): void {
571
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
×
572
    this.bufferLayout = this._gpuGeometry
×
573
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
574
      : bufferLayout;
575
    this._setPipelineNeedsUpdate('bufferLayout');
×
576

577
    // Recreate the pipeline
578
    this.pipeline = this._updatePipeline();
×
579

580
    // vertex array needs to be updated if we update buffer layout,
581
    // but not if we update parameters
582
    this.vertexArray = this.device.createVertexArray({
×
583
      shaderLayout: this.pipeline.shaderLayout,
584
      bufferLayout: this.pipeline.bufferLayout
585
    });
586

587
    // Reapply geometry attributes to the new vertex array
588
    if (this._gpuGeometry) {
×
589
      this._setGeometryAttributes(this._gpuGeometry);
×
590
    }
591
  }
592

593
  /**
594
   * Set GPU parameters.
595
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
596
   * @param parameters
597
   */
598
  setParameters(parameters: RenderPipelineParameters) {
599
    if (!deepEqual(parameters, this.parameters, 2)) {
×
600
      this.parameters = parameters;
×
601
      this._setPipelineNeedsUpdate('parameters');
×
602
    }
603
  }
604

605
  // Update dynamic fields
606

607
  /**
608
   * Updates the instance count (used in draw calls)
609
   * @note Any attributes with stepMode=instance need to be at least this big
610
   */
611
  setInstanceCount(instanceCount: number): void {
612
    this.instanceCount = instanceCount;
24✔
613
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
614
    // but deck.gl sets instanceCount even for models that are not instanced.
615
    if (this.isInstanced === undefined && instanceCount > 0) {
24✔
616
      this.isInstanced = true;
17✔
617
    }
618
    this.setNeedsRedraw('instanceCount');
24✔
619
  }
620

621
  /**
622
   * Updates the vertex count (used in draw calls)
623
   * @note Any attributes with stepMode=vertex need to be at least this big
624
   */
625
  setVertexCount(vertexCount: number): void {
626
    this.vertexCount = vertexCount;
45✔
627
    this.setNeedsRedraw('vertexCount');
45✔
628
  }
629

630
  /** Set the shader inputs */
631
  setShaderInputs(shaderInputs: ShaderInputs): void {
632
    this.shaderInputs = shaderInputs;
76✔
633
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
76✔
634
    // Create uniform buffer bindings for all modules that actually have uniforms
635
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
76✔
636
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
104✔
637
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
72✔
638
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
72✔
639
      }
640
    }
641
    this.setNeedsRedraw('shaderInputs');
76✔
642
  }
643

644
  setMaterial(material: Material | null): void {
645
    this.material = material;
×
646
    this.setNeedsRedraw('material');
×
647
  }
648

649
  /** Update uniform buffers from the model's shader inputs */
650
  updateShaderInputs(): void {
651
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues());
43✔
652
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
43✔
653
    // TODO - this is already tracked through buffer/texture update times?
654
    this.setNeedsRedraw('shaderInputs');
43✔
655
  }
656

657
  /**
658
   * Sets bindings (textures, samplers, uniform buffers)
659
   */
660
  setBindings(bindings: Record<string, Binding | DynamicTexture>): void {
661
    Object.assign(this.bindings, bindings);
126✔
662
    this.setNeedsRedraw('bindings');
126✔
663
  }
664

665
  /**
666
   * Updates optional transform feedback. WebGL only.
667
   */
668
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
669
    this.transformFeedback = transformFeedback;
20✔
670
    this.setNeedsRedraw('transformFeedback');
20✔
671
  }
672

673
  /**
674
   * Sets the index buffer
675
   * @todo - how to unset it if we change geometry?
676
   */
677
  setIndexBuffer(indexBuffer: Buffer | null): void {
678
    this.vertexArray.setIndexBuffer(indexBuffer);
34✔
679
    this.setNeedsRedraw('indexBuffer');
34✔
680
  }
681

682
  /**
683
   * Sets attributes (buffers)
684
   * @note Overrides any attributes previously set with the same name
685
   */
686
  setAttributes(buffers: Record<string, Buffer>, options?: {disableWarnings?: boolean}): void {
687
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
157✔
688
    if (buffers['indices']) {
157!
689
      log.warn(
×
690
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
691
      )();
692
    }
693

694
    // ensure bufferLayout order matches source layout so we bind
695
    // the correct buffers to the correct indices in webgpu.
696
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
157✔
697
      this.pipeline.shaderLayout,
698
      this.bufferLayout
699
    );
700
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
157✔
701
    const logicalBufferSlots = getLogicalBufferSlots(this.pipeline.shaderLayout, this.bufferLayout);
157✔
702

703
    // Check if all buffers have a layout
704
    for (const [bufferName, buffer] of Object.entries(buffers)) {
157✔
705
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
306✔
706
      if (!bufferLayout) {
306!
707
        if (!disableWarnings) {
×
708
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
709
        }
710
        continue; // eslint-disable-line no-continue
×
711
      }
712

713
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
714
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
715
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
306✔
716
      let set = false;
306✔
717
      for (const attributeName of attributeNames) {
306✔
718
        const attributeInfo = this._attributeInfos[attributeName];
306✔
719
        if (attributeInfo) {
306✔
720
          const location =
721
            this.device.type === 'webgpu'
260✔
722
              ? logicalBufferSlots[attributeInfo.bufferName]
723
              : attributeInfo.location;
724

725
          this.vertexArray.setBuffer(location, buffer);
260✔
726
          set = true;
260✔
727
        }
728
      }
729
      if (!set && !disableWarnings) {
306✔
730
        log.warn(
4✔
731
          `Model(${this.id}): Ignoring buffer "${buffer.id}" for unknown attribute "${bufferName}"`
732
        )();
733
      }
734
    }
735
    this.setNeedsRedraw('attributes');
157✔
736
  }
737

738
  /**
739
   * Sets constant attributes
740
   * @note Overrides any attributes previously set with the same name
741
   * Constant attributes are only supported in WebGL, not in WebGPU
742
   * Any attribute that is disabled in the current vertex array object
743
   * is read from the context's global constant value for that attribute location.
744
   * @param constantAttributes
745
   */
746
  setConstantAttributes(
747
    attributes: Record<string, TypedArray>,
748
    options?: {disableWarnings?: boolean}
749
  ): void {
750
    for (const [attributeName, value] of Object.entries(attributes)) {
76✔
751
      const attributeInfo = this._attributeInfos[attributeName];
×
752
      if (attributeInfo) {
×
753
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
754
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
755
        log.warn(
×
756
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
757
        )();
758
      }
759
    }
760
    this.setNeedsRedraw('constants');
76✔
761
  }
762

763
  // INTERNAL METHODS
764

765
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
766
  _areBindingsLoading(): string | false {
767
    for (const binding of Object.values(this.bindings)) {
43✔
768
      if (binding instanceof DynamicTexture && !binding.isReady) {
36!
769
        return binding.id;
×
770
      }
771
    }
772
    for (const binding of Object.values(this.material?.bindings || {})) {
43✔
773
      if (binding instanceof DynamicTexture && !binding.isReady) {
×
774
        return binding.id;
×
775
      }
776
    }
777
    return false;
43✔
778
  }
779

780
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
781
  _getBindings(): Record<string, Binding> {
782
    const validBindings: Record<string, Binding> = {};
164✔
783

784
    for (const [name, binding] of Object.entries(this.bindings)) {
164✔
785
      if (binding instanceof DynamicTexture) {
147✔
786
        // Check that async textures are loaded
787
        if (binding.isReady) {
2!
788
          validBindings[name] = binding.texture;
2✔
789
        }
790
      } else {
791
        validBindings[name] = binding;
145✔
792
      }
793
    }
794

795
    return validBindings;
164✔
796
  }
797

798
  _getBindGroups(): BindingsByGroup {
799
    const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
123✔
800
    const bindGroups = shaderLayout.bindings.length
123✔
801
      ? normalizeBindingsByGroup(shaderLayout, this._getBindings())
802
      : {0: this._getBindings()};
803

804
    if (!this.material) {
123✔
805
      return bindGroups;
113✔
806
    }
807

808
    for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) {
10✔
809
      const group = Number(groupKey);
10✔
810
      bindGroups[group] = {
10✔
811
        ...(bindGroups[group] || {}),
20✔
812
        ...groupBindings
813
      };
814
    }
815

816
    return bindGroups;
10✔
817
  }
818

819
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
820
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
41✔
821
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
41!
822
  }
823

824
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
825
  _getBindingsUpdateTimestamp(): number {
826
    let timestamp = 0;
1✔
827
    for (const binding of Object.values(this.bindings)) {
1✔
828
      if (binding instanceof TextureView) {
×
829
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
830
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
831
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
832
      } else if (binding instanceof DynamicTexture) {
×
833
        timestamp = binding.texture
×
834
          ? Math.max(timestamp, binding.texture.updateTimestamp)
835
          : // The texture will become available in the future
836
            Infinity;
837
      } else if (!(binding instanceof Sampler)) {
×
838
        timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
×
839
      }
840
    }
841
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
1✔
842
  }
843

844
  /**
845
   * Updates the optional geometry attributes
846
   * Geometry, sets several attributes, indexBuffer, and also vertex count
847
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
848
   */
849
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
850
    // Filter geometry attribute so that we don't issue warnings for unused attributes
851
    const attributes = {...gpuGeometry.attributes};
34✔
852
    for (const [attributeName] of Object.entries(attributes)) {
34✔
853
      if (
91✔
854
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
261✔
855
        attributeName !== 'positions'
856
      ) {
857
        delete attributes[attributeName];
40✔
858
      }
859
    }
860

861
    // TODO - delete previous geometry?
862
    this.vertexCount = gpuGeometry.vertexCount;
34✔
863
    this.setIndexBuffer(gpuGeometry.indices || null);
34✔
864
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
34✔
865
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
34✔
866

867
    this.setNeedsRedraw('geometry attributes');
34✔
868
  }
869

870
  /** Mark pipeline as needing update */
871
  _setPipelineNeedsUpdate(reason: string): void {
872
    this._pipelineNeedsUpdate ||= reason;
13✔
873
    this.setNeedsRedraw(reason);
13✔
874
  }
875

876
  /** Update pipeline if needed */
877
  _updatePipeline(): RenderPipeline {
878
    if (this._pipelineNeedsUpdate) {
162✔
879
      let prevShaderVs: Shader | null = null;
82✔
880
      let prevShaderFs: Shader | null = null;
82✔
881
      if (this.pipeline) {
82✔
882
        log.log(
6✔
883
          1,
884
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
885
        )();
886
        prevShaderVs = this.pipeline.vs;
6✔
887
        prevShaderFs = this.pipeline.fs;
6✔
888
      }
889

890
      this._pipelineNeedsUpdate = false;
82✔
891

892
      const vs = this.shaderFactory.createShader({
82✔
893
        id: `${this.id}-vertex`,
894
        stage: 'vertex',
895
        source: this.source || this.vs,
153✔
896
        debugShaders: this.props.debugShaders
897
      });
898

899
      let fs: Shader | null = null;
82✔
900
      if (this.source) {
82✔
901
        fs = vs;
11✔
902
      } else if (this.fs) {
71!
903
        fs = this.shaderFactory.createShader({
71✔
904
          id: `${this.id}-fragment`,
905
          stage: 'fragment',
906
          source: this.source || this.fs,
142✔
907
          debugShaders: this.props.debugShaders
908
        });
909
      }
910

911
      this.pipeline = this.pipelineFactory.createRenderPipeline({
82✔
912
        ...this.props,
913
        bindings: undefined,
914
        bufferLayout: this.bufferLayout,
915
        colorAttachmentFormats: this._colorAttachmentFormats,
916
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
917
        topology: this.topology,
918
        parameters: this.parameters,
919
        bindGroups: this._getBindGroups(),
920
        vs,
921
        fs
922
      });
923

924
      this._attributeInfos = getAttributeInfosFromLayouts(
82✔
925
        this.pipeline.shaderLayout,
926
        this.bufferLayout
927
      );
928

929
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
82✔
930
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
82✔
931
        this.shaderFactory.release(prevShaderFs);
1✔
932
      }
933
    }
934
    return this.pipeline;
162✔
935
  }
936

937
  /** Throttle draw call logging */
938
  _lastLogTime = 0;
76✔
939
  _logOpen = false;
76✔
940

941
  _logDrawCallStart(): void {
942
    // IF level is 4 or higher, log every frame.
943
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
43!
944
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
43!
945
      return;
43✔
946
    }
947

948
    this._lastLogTime = Date.now();
×
949
    this._logOpen = true;
×
950

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

954
  _logDrawCallEnd(): void {
955
    if (this._logOpen) {
43!
956
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
957

958
      // log.table(logLevel, attributeTable)();
959
      // log.table(logLevel, uniformTable)();
960
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
961

962
      const uniformTable = this.shaderInputs.getDebugTable();
×
963
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
964

965
      const attributeTable = this._getAttributeDebugTable();
×
966
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
967
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
968

969
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
970
      this._logOpen = false;
×
971
    }
972
  }
973

974
  protected _drawCount = 0;
76✔
975
  _logFramebuffer(renderPass: RenderPass): void {
976
    const debugFramebuffers = this.device.props.debugFramebuffers;
43✔
977
    this._drawCount++;
43✔
978
    // Update first 3 frames and then every 60 frames
979
    if (!debugFramebuffers) {
43!
980
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
981
      return;
43✔
982
    }
983
    const framebuffer = renderPass.props.framebuffer;
×
984
    debugFramebuffer(renderPass, framebuffer, {
×
985
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
986
      minimap: true
987
    });
988
  }
989

990
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
991
    const table: Record<string, Record<string, unknown>> = {};
×
992
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
993
      const values = this.vertexArray.attributes[attributeInfo.location];
×
994
      table[attributeInfo.location] = {
×
995
        name,
996
        type: attributeInfo.shaderType,
997
        values: values
×
998
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
999
          : 'null'
1000
      };
1001
    }
1002
    if (this.vertexArray.indexBuffer) {
×
1003
      const {indexBuffer} = this.vertexArray;
×
1004
      const values =
1005
        indexBuffer.indexType === 'uint32'
×
1006
          ? new Uint32Array(indexBuffer.debugData)
1007
          : new Uint16Array(indexBuffer.debugData);
1008
      table['indices'] = {
×
1009
        name: 'indices',
1010
        type: indexBuffer.indexType,
1011
        values: values.toString()
1012
      };
1013
    }
1014
    return table;
×
1015
  }
1016

1017
  // TODO - fix typing of luma data types
1018
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1019
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1020
    const typedArray =
1021
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
1022
    return typedArray.toString();
×
1023
  }
1024

1025
  private _getNonMaterialBindings(
1026
    bindings: Record<string, ModelBinding>
1027
  ): Record<string, ModelBinding> {
1028
    if (!this.material) {
43!
1029
      return bindings;
43✔
1030
    }
1031

NEW
1032
    const filteredBindings: Record<string, ModelBinding> = {};
×
1033
    for (const [name, binding] of Object.entries(bindings)) {
×
1034
      if (!this.material.ownsBinding(name)) {
×
1035
        filteredBindings[name] = binding;
×
1036
      }
1037
    }
1038
    return filteredBindings;
×
1039
  }
1040

1041
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1042
    if (this.device.type !== 'webgpu') {
43✔
1043
      return;
34✔
1044
    }
1045

1046
    const framebuffer =
1047
      (
1048
        renderPass as RenderPass & {
9!
1049
          framebuffer?: {
1050
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1051
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1052
          };
1053
        }
1054
      ).framebuffer || renderPass.props.framebuffer;
1055

1056
    const nextColorAttachmentFormats = framebuffer?.colorAttachments?.map(colorAttachment =>
43✔
1057
      asColorAttachmentFormat(colorAttachment?.texture?.format)
9✔
1058
    );
1059
    const nextDepthStencilAttachmentFormat = asDepthStencilAttachmentFormat(
43✔
1060
      framebuffer?.depthStencilAttachment?.texture?.format
1061
    );
1062

1063
    if (
43✔
1064
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
47✔
1065
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1066
    ) {
1067
      this._colorAttachmentFormats = nextColorAttachmentFormats;
5✔
1068
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
5✔
1069
      this._setPipelineNeedsUpdate('attachment formats');
5✔
1070
    }
1071
  }
1072
}
1073

1074
// HELPERS
1075

1076
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1077
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
9!
1078
}
1079

1080
function asDepthStencilAttachmentFormat(
1081
  format?: string | null
1082
): TextureFormatDepthStencil | undefined {
1083
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
9!
1084
}
1085

1086
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1087
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
9✔
1088
}
1089

1090
/** Create a shadertools platform info from the Device */
1091
export function getPlatformInfo(device: Device): PlatformInfo {
1092
  return {
76✔
1093
    type: device.type,
1094
    shaderLanguage: device.info.shadingLanguage,
1095
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1096
    gpu: device.info.gpu,
1097
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1098
    features: device.features as unknown as Set<DeviceFeature>
1099
  };
1100
}
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