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

visgl / luma.gl / 23981598546

04 Apr 2026 03:12PM UTC coverage: 74.234% (+0.02%) from 74.219%
23981598546

push

github

web-flow
feat(effects): Extract dof effect from dof example (#2591)

5176 of 7885 branches covered (65.64%)

Branch coverage included in aggregate %.

18 of 18 new or added lines in 4 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

11704 of 14854 relevant lines covered (78.79%)

777.34 hits per line

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

72.92
/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 CommandEncoder,
17
  type AttributeInfo,
18
  type Binding,
19
  type BindingsByGroup,
20
  type PrimitiveTopology,
21
  Device,
22
  DeviceFeature,
23
  Buffer,
24
  Texture,
25
  TextureView,
26
  Sampler,
27
  RenderPipeline,
28
  RenderPass,
29
  PipelineFactory,
30
  ShaderFactory,
31
  UniformStore,
32
  log,
33
  dataTypeDecoder,
34
  getAttributeInfosFromLayouts,
35
  getLogicalBufferSlots,
36
  normalizeBindingsByGroup
37
} from '@luma.gl/core';
38

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

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

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

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

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

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

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

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

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

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

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

115
  transformFeedback?: TransformFeedback;
116

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

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

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

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

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

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

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

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

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

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

207
  // Dynamic properties
208

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

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

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

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

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

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

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

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

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

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

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

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

273
    Object.assign(this.userData, props.userData);
103✔
274

275
    this.material = props.material || null;
103✔
276

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

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

288
    // Setup shader assembler
289
    const platformInfo = getPlatformInfo(device);
103✔
290

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

296
    this.props.shaderLayout =
103✔
297
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
206✔
298

299
    const isWebGPU = this.device.type === 'webgpu';
103✔
300

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

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

339
    this.vertexCount = this.props.vertexCount;
103✔
340
    this.instanceCount = this.props.instanceCount;
103✔
341

342
    this.topology = this.props.topology;
103✔
343
    this.bufferLayout = this.props.bufferLayout;
103✔
344
    this.parameters = this.props.parameters;
103✔
345

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

351
    this.pipelineFactory =
103✔
352
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
206✔
353
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
103✔
354

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

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

364
    // Now we can apply geometry attributes
365
    if (this._gpuGeometry) {
103✔
366
      this._setGeometryAttributes(this._gpuGeometry);
60✔
367
    }
368

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

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

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

413
  // Draw call
414

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

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

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

436
  /**
437
   * Updates uniforms and pipeline state before opening a render pass.
438
   *
439
   * @param commandEncoder - Encoder that should own any GPU uploads emitted
440
   * during draw preparation.
441
   */
442
  predraw(commandEncoder: CommandEncoder): void {
443
    // Update uniform buffers if needed
444
    this.updateShaderInputs(commandEncoder);
84✔
445
    this.material?.updateShaderInputs(commandEncoder);
84✔
446
    // Check if the pipeline is invalidated
447
    this.pipeline = this._updatePipeline();
84✔
448
  }
449

450
  /**
451
   * Issue one draw call.
452
   * @param renderPass - render pass to draw into
453
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
454
   */
455
  draw(renderPass: RenderPass): boolean {
456
    const loadingBinding = this._areBindingsLoading();
68✔
457
    if (loadingBinding) {
68!
458
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
459
      return false;
×
460
    }
461

462
    this._syncAttachmentFormats(renderPass);
68✔
463

464
    try {
68✔
465
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
68✔
466
      if (this.device.type === 'webgpu') {
68✔
467
        // WebGPU uploads cannot be encoded once the render pass is already open.
468
        // Keep the implicit draw() path working for existing callers by falling
469
        // back to immediate writes here; callers that need upload ordering
470
        // across multiple draws/viewports must call predraw(commandEncoder)
471
        // before beginRenderPass().
472
        this.updateShaderInputs();
10✔
473
        this.material?.updateShaderInputs();
10✔
474
        this.pipeline = this._updatePipeline();
10✔
475
      } else {
476
        this.predraw(this.device.commandEncoder);
58✔
477
      }
478
    } finally {
479
      renderPass.popDebugGroup();
68✔
480
    }
481

482
    let drawSuccess: boolean;
483
    let pipelineErrored = this.pipeline.isErrored;
68✔
484
    try {
68✔
485
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
68✔
486
      this._logDrawCallStart();
68✔
487

488
      // Update the pipeline if invalidated
489
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
490
      // Application can call Model.predraw() to avoid this.
491
      this.pipeline = this._updatePipeline();
68✔
492
      pipelineErrored = this.pipeline.isErrored;
68✔
493

494
      if (pipelineErrored) {
68✔
495
        log.info(
2✔
496
          LOG_DRAW_PRIORITY,
497
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
498
        )();
499
        drawSuccess = false;
2✔
500
      } else {
501
        const syncBindings = this._getBindings();
66✔
502
        const syncBindGroups = this._getBindGroups();
66✔
503

504
        const {indexBuffer} = this.vertexArray;
66✔
505
        const indexCount = indexBuffer
66✔
506
          ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
12!
507
          : undefined;
508

509
        drawSuccess = this.pipeline.draw({
66✔
510
          renderPass,
511
          vertexArray: this.vertexArray,
512
          isInstanced: this.isInstanced,
513
          vertexCount: this.vertexCount,
514
          instanceCount: this.instanceCount,
515
          indexCount,
516
          transformFeedback: this.transformFeedback || undefined,
111✔
517
          // Pipelines may be shared across models when caching is enabled, so bindings
518
          // and WebGL uniforms must be supplied on every draw instead of being stored
519
          // on the pipeline instance.
520
          bindings: syncBindings,
521
          bindGroups: syncBindGroups,
522
          _bindGroupCacheKeys: this._getBindGroupCacheKeys(),
523
          uniforms: this.props.uniforms,
524
          // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
525
          // so we must provide our unique parameters to each draw
526
          // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
527
          parameters: this.parameters,
528
          topology: this.topology
529
        });
530
      }
531
    } finally {
532
      renderPass.popDebugGroup();
68✔
533
      this._logDrawCallEnd();
68✔
534
    }
535
    this._logFramebuffer(renderPass);
68✔
536

537
    // Update needsRedraw flag
538
    if (drawSuccess) {
68✔
539
      this._lastDrawTimestamp = this.device.timestamp;
66✔
540
      this._needsRedraw = false;
66✔
541
    } else if (pipelineErrored) {
2!
542
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
2✔
543
    } else {
544
      this._needsRedraw = 'waiting for resource initialization';
×
545
    }
546
    return drawSuccess;
68✔
547
  }
548

549
  // Update fixed fields (can trigger pipeline rebuild)
550

551
  /**
552
   * Updates the optional geometry
553
   * Geometry, set topology and bufferLayout
554
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
555
   */
556
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
557
    this._gpuGeometry?.destroy();
60✔
558
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
60✔
559
    if (gpuGeometry) {
60!
560
      this.setTopology(gpuGeometry.topology || 'triangle-list');
60!
561
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
60✔
562
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
60✔
563
        gpuGeometry.bufferLayout,
564
        this.bufferLayout
565
      );
566
      if (this.vertexArray) {
60!
567
        this._setGeometryAttributes(gpuGeometry);
×
568
      }
569
    }
570
    this._gpuGeometry = gpuGeometry;
60✔
571
  }
572

573
  /**
574
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
575
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
576
   */
577
  setTopology(topology: PrimitiveTopology): void {
578
    if (topology !== this.topology) {
63✔
579
      this.topology = topology;
34✔
580
      this._setPipelineNeedsUpdate('topology');
34✔
581
    }
582
  }
583

584
  /**
585
   * Updates the buffer layout.
586
   * @note Triggers a pipeline rebuild / pipeline cache fetch
587
   */
588
  setBufferLayout(bufferLayout: BufferLayout[]): void {
589
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
×
590
    this.bufferLayout = this._gpuGeometry
×
591
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
592
      : bufferLayout;
593
    this._setPipelineNeedsUpdate('bufferLayout');
×
594

595
    // Recreate the pipeline
596
    this.pipeline = this._updatePipeline();
×
597

598
    // vertex array needs to be updated if we update buffer layout,
599
    // but not if we update parameters
600
    this.vertexArray = this.device.createVertexArray({
×
601
      shaderLayout: this.pipeline.shaderLayout,
602
      bufferLayout: this.pipeline.bufferLayout
603
    });
604

605
    // Reapply geometry attributes to the new vertex array
606
    if (this._gpuGeometry) {
×
607
      this._setGeometryAttributes(this._gpuGeometry);
×
608
    }
609
  }
610

611
  /**
612
   * Set GPU parameters.
613
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
614
   * @param parameters
615
   */
616
  setParameters(parameters: RenderPipelineParameters) {
617
    if (!deepEqual(parameters, this.parameters, 2)) {
×
618
      this.parameters = parameters;
×
619
      this._setPipelineNeedsUpdate('parameters');
×
620
    }
621
  }
622

623
  // Update dynamic fields
624

625
  /**
626
   * Updates the instance count (used in draw calls)
627
   * @note Any attributes with stepMode=instance need to be at least this big
628
   */
629
  setInstanceCount(instanceCount: number): void {
630
    this.instanceCount = instanceCount;
24✔
631
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
632
    // but deck.gl sets instanceCount even for models that are not instanced.
633
    if (this.isInstanced === undefined && instanceCount > 0) {
24✔
634
      this.isInstanced = true;
17✔
635
    }
636
    this.setNeedsRedraw('instanceCount');
24✔
637
  }
638

639
  /**
640
   * Updates the vertex count (used in draw calls)
641
   * @note Any attributes with stepMode=vertex need to be at least this big
642
   */
643
  setVertexCount(vertexCount: number): void {
644
    this.vertexCount = vertexCount;
72✔
645
    this.setNeedsRedraw('vertexCount');
72✔
646
  }
647

648
  /** Set the shader inputs */
649
  setShaderInputs(shaderInputs: ShaderInputs): void {
650
    this.shaderInputs = shaderInputs;
103✔
651
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
103✔
652
    // Create uniform buffer bindings for all modules that actually have uniforms
653
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
103✔
654
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
147✔
655
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
109✔
656
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
109✔
657
      }
658
    }
659
    this.setNeedsRedraw('shaderInputs');
103✔
660
  }
661

662
  setMaterial(material: Material | null): void {
663
    this.material = material;
×
664
    this.setNeedsRedraw('material');
×
665
  }
666

667
  /** Update uniform buffers from the model's shader inputs */
668
  /**
669
   * Flushes current shader-input values into managed uniform buffers and
670
   * non-material bindings.
671
   *
672
   * @param commandEncoder - Optional encoder used to order uniform uploads with
673
   * subsequent draw commands.
674
   */
675
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
676
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
94✔
677
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
94✔
678
    // TODO - this is already tracked through buffer/texture update times?
679
    this.setNeedsRedraw('shaderInputs');
94✔
680
  }
681

682
  /**
683
   * Sets bindings (textures, samplers, uniform buffers)
684
   */
685
  setBindings(bindings: Record<string, Binding | DynamicTexture>): void {
686
    Object.assign(this.bindings, bindings);
238✔
687
    this.setNeedsRedraw('bindings');
238✔
688
  }
689

690
  /**
691
   * Updates optional transform feedback. WebGL only.
692
   */
693
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
694
    this.transformFeedback = transformFeedback;
20✔
695
    this.setNeedsRedraw('transformFeedback');
20✔
696
  }
697

698
  /**
699
   * Sets the index buffer
700
   * @todo - how to unset it if we change geometry?
701
   */
702
  setIndexBuffer(indexBuffer: Buffer | null): void {
703
    this.vertexArray.setIndexBuffer(indexBuffer);
60✔
704
    this.setNeedsRedraw('indexBuffer');
60✔
705
  }
706

707
  /**
708
   * Sets attributes (buffers)
709
   * @note Overrides any attributes previously set with the same name
710
   */
711
  setAttributes(buffers: Record<string, Buffer>, options?: {disableWarnings?: boolean}): void {
712
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
236✔
713
    if (buffers['indices']) {
236!
714
      log.warn(
×
715
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
716
      )();
717
    }
718

719
    // ensure bufferLayout order matches source layout so we bind
720
    // the correct buffers to the correct indices in webgpu.
721
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
236✔
722
      this.pipeline.shaderLayout,
723
      this.bufferLayout
724
    );
725
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
236✔
726
    const logicalBufferSlots = getLogicalBufferSlots(this.pipeline.shaderLayout, this.bufferLayout);
236✔
727

728
    // Check if all buffers have a layout
729
    for (const [bufferName, buffer] of Object.entries(buffers)) {
236✔
730
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
462✔
731
      if (!bufferLayout) {
462!
732
        if (!disableWarnings) {
×
733
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
734
        }
735
        continue; // eslint-disable-line no-continue
×
736
      }
737

738
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
739
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
740
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
462✔
741
      let set = false;
462✔
742
      for (const attributeName of attributeNames) {
462✔
743
        const attributeInfo = this._attributeInfos[attributeName];
462✔
744
        if (attributeInfo) {
462✔
745
          const location =
746
            this.device.type === 'webgpu'
416✔
747
              ? logicalBufferSlots[attributeInfo.bufferName]
748
              : attributeInfo.location;
749

750
          this.vertexArray.setBuffer(location, buffer);
416✔
751
          set = true;
416✔
752
        }
753
      }
754
      if (!set && !disableWarnings) {
462✔
755
        log.warn(
4✔
756
          `Model(${this.id}): Ignoring buffer "${buffer.id}" for unknown attribute "${bufferName}"`
757
        )();
758
      }
759
    }
760
    this.setNeedsRedraw('attributes');
236✔
761
  }
762

763
  /**
764
   * Sets constant attributes
765
   * @note Overrides any attributes previously set with the same name
766
   * Constant attributes are only supported in WebGL, not in WebGPU
767
   * Any attribute that is disabled in the current vertex array object
768
   * is read from the context's global constant value for that attribute location.
769
   * @param constantAttributes
770
   */
771
  setConstantAttributes(
772
    attributes: Record<string, TypedArray>,
773
    options?: {disableWarnings?: boolean}
774
  ): void {
775
    for (const [attributeName, value] of Object.entries(attributes)) {
103✔
776
      const attributeInfo = this._attributeInfos[attributeName];
×
777
      if (attributeInfo) {
×
778
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
779
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
780
        log.warn(
×
781
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
782
        )();
783
      }
784
    }
785
    this.setNeedsRedraw('constants');
103✔
786
  }
787

788
  // INTERNAL METHODS
789

790
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
791
  _areBindingsLoading(): string | false {
792
    for (const binding of Object.values(this.bindings)) {
68✔
793
      if (binding instanceof DynamicTexture && !binding.isReady) {
100!
794
        return binding.id;
×
795
      }
796
    }
797
    for (const binding of Object.values(this.material?.bindings || {})) {
68✔
798
      if (binding instanceof DynamicTexture && !binding.isReady) {
×
799
        return binding.id;
×
800
      }
801
    }
802
    return false;
68✔
803
  }
804

805
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
806
  _getBindings(): Record<string, Binding> {
807
    const validBindings: Record<string, Binding> = {};
242✔
808

809
    for (const [name, binding] of Object.entries(this.bindings)) {
242✔
810
      if (binding instanceof DynamicTexture) {
312!
811
        // Check that async textures are loaded
UNCOV
812
        if (binding.isReady) {
×
UNCOV
813
          validBindings[name] = binding.texture;
×
814
        }
815
      } else {
816
        validBindings[name] = binding;
312✔
817
      }
818
    }
819

820
    return validBindings;
242✔
821
  }
822

823
  _getBindGroups(): BindingsByGroup {
824
    const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
176✔
825
    const bindGroups = shaderLayout.bindings.length
176✔
826
      ? normalizeBindingsByGroup(shaderLayout, this._getBindings())
827
      : {0: this._getBindings()};
828

829
    if (!this.material) {
176✔
830
      return bindGroups;
166✔
831
    }
832

833
    for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) {
10✔
834
      const group = Number(groupKey);
10✔
835
      bindGroups[group] = {
10✔
836
        ...(bindGroups[group] || {}),
20✔
837
        ...groupBindings
838
      };
839
    }
840

841
    return bindGroups;
10✔
842
  }
843

844
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
845
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
66✔
846
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
66!
847
  }
848

849
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
850
  _getBindingsUpdateTimestamp(): number {
851
    let timestamp = 0;
1✔
852
    for (const binding of Object.values(this.bindings)) {
1✔
853
      if (binding instanceof TextureView) {
×
854
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
855
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
856
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
857
      } else if (binding instanceof DynamicTexture) {
×
858
        timestamp = binding.texture
×
859
          ? Math.max(timestamp, binding.texture.updateTimestamp)
860
          : // The texture will become available in the future
861
            Infinity;
862
      } else if (!(binding instanceof Sampler)) {
×
863
        timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
×
864
      }
865
    }
866
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
1✔
867
  }
868

869
  /**
870
   * Updates the optional geometry attributes
871
   * Geometry, sets several attributes, indexBuffer, and also vertex count
872
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
873
   */
874
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
875
    // Filter geometry attribute so that we don't issue warnings for unused attributes
876
    const attributes = {...gpuGeometry.attributes};
60✔
877
    for (const [attributeName] of Object.entries(attributes)) {
60✔
878
      if (
169✔
879
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
417✔
880
        attributeName !== 'positions'
881
      ) {
882
        delete attributes[attributeName];
40✔
883
      }
884
    }
885

886
    // TODO - delete previous geometry?
887
    this.vertexCount = gpuGeometry.vertexCount;
60✔
888
    this.setIndexBuffer(gpuGeometry.indices || null);
60✔
889
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
60✔
890
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
60✔
891

892
    this.setNeedsRedraw('geometry attributes');
60✔
893
  }
894

895
  /** Mark pipeline as needing update */
896
  _setPipelineNeedsUpdate(reason: string): void {
897
    this._pipelineNeedsUpdate ||= reason;
40✔
898
    this.setNeedsRedraw(reason);
40✔
899
  }
900

901
  /** Update pipeline if needed */
902
  _updatePipeline(): RenderPipeline {
903
    if (this._pipelineNeedsUpdate) {
265✔
904
      let prevShaderVs: Shader | null = null;
110✔
905
      let prevShaderFs: Shader | null = null;
110✔
906
      if (this.pipeline) {
110✔
907
        log.log(
7✔
908
          1,
909
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
910
        )();
911
        prevShaderVs = this.pipeline.vs;
7✔
912
        prevShaderFs = this.pipeline.fs;
7✔
913
      }
914

915
      this._pipelineNeedsUpdate = false;
110✔
916

917
      const vs = this.shaderFactory.createShader({
110✔
918
        id: `${this.id}-vertex`,
919
        stage: 'vertex',
920
        source: this.source || this.vs,
207✔
921
        debugShaders: this.props.debugShaders
922
      });
923

924
      let fs: Shader | null = null;
110✔
925
      if (this.source) {
110✔
926
        fs = vs;
13✔
927
      } else if (this.fs) {
97!
928
        fs = this.shaderFactory.createShader({
97✔
929
          id: `${this.id}-fragment`,
930
          stage: 'fragment',
931
          source: this.source || this.fs,
194✔
932
          debugShaders: this.props.debugShaders
933
        });
934
      }
935

936
      this.pipeline = this.pipelineFactory.createRenderPipeline({
110✔
937
        ...this.props,
938
        bindings: undefined,
939
        bufferLayout: this.bufferLayout,
940
        colorAttachmentFormats: this._colorAttachmentFormats,
941
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
942
        topology: this.topology,
943
        parameters: this.parameters,
944
        bindGroups: this._getBindGroups(),
945
        vs,
946
        fs
947
      });
948

949
      this._attributeInfos = getAttributeInfosFromLayouts(
110✔
950
        this.pipeline.shaderLayout,
951
        this.bufferLayout
952
      );
953

954
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
110✔
955
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
110✔
956
        this.shaderFactory.release(prevShaderFs);
1✔
957
      }
958
    }
959
    return this.pipeline;
265✔
960
  }
961

962
  /** Throttle draw call logging */
963
  _lastLogTime = 0;
103✔
964
  _logOpen = false;
103✔
965

966
  _logDrawCallStart(): void {
967
    // IF level is 4 or higher, log every frame.
968
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
68!
969
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
68!
970
      return;
68✔
971
    }
972

973
    this._lastLogTime = Date.now();
×
974
    this._logOpen = true;
×
975

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

979
  _logDrawCallEnd(): void {
980
    if (this._logOpen) {
68!
981
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
982

983
      // log.table(logLevel, attributeTable)();
984
      // log.table(logLevel, uniformTable)();
985
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
986

987
      const uniformTable = this.shaderInputs.getDebugTable();
×
988
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
989

990
      const attributeTable = this._getAttributeDebugTable();
×
991
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
992
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
993

994
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
995
      this._logOpen = false;
×
996
    }
997
  }
998

999
  protected _drawCount = 0;
103✔
1000
  _logFramebuffer(renderPass: RenderPass): void {
1001
    const debugFramebuffers = this.device.props.debugFramebuffers;
68✔
1002
    this._drawCount++;
68✔
1003
    // Update first 3 frames and then every 60 frames
1004
    if (!debugFramebuffers) {
68!
1005
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
1006
      return;
68✔
1007
    }
1008
    const framebuffer = renderPass.props.framebuffer;
×
1009
    debugFramebuffer(renderPass, framebuffer, {
×
1010
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
1011
      minimap: true
1012
    });
1013
  }
1014

1015
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1016
    const table: Record<string, Record<string, unknown>> = {};
×
1017
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
1018
      const values = this.vertexArray.attributes[attributeInfo.location];
×
1019
      table[attributeInfo.location] = {
×
1020
        name,
1021
        type: attributeInfo.shaderType,
1022
        values: values
×
1023
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
1024
          : 'null'
1025
      };
1026
    }
1027
    if (this.vertexArray.indexBuffer) {
×
1028
      const {indexBuffer} = this.vertexArray;
×
1029
      const values =
1030
        indexBuffer.indexType === 'uint32'
×
1031
          ? new Uint32Array(indexBuffer.debugData)
1032
          : new Uint16Array(indexBuffer.debugData);
1033
      table['indices'] = {
×
1034
        name: 'indices',
1035
        type: indexBuffer.indexType,
1036
        values: values.toString()
1037
      };
1038
    }
1039
    return table;
×
1040
  }
1041

1042
  // TODO - fix typing of luma data types
1043
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1044
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1045
    const typedArray =
1046
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
1047
    return typedArray.toString();
×
1048
  }
1049

1050
  private _getNonMaterialBindings(
1051
    bindings: Record<string, ModelBinding>
1052
  ): Record<string, ModelBinding> {
1053
    if (!this.material) {
94!
1054
      return bindings;
94✔
1055
    }
1056

1057
    const filteredBindings: Record<string, ModelBinding> = {};
×
1058
    for (const [name, binding] of Object.entries(bindings)) {
×
1059
      if (!this.material.ownsBinding(name)) {
×
1060
        filteredBindings[name] = binding;
×
1061
      }
1062
    }
1063
    return filteredBindings;
×
1064
  }
1065

1066
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1067
    if (this.device.type !== 'webgpu') {
68✔
1068
      return;
58✔
1069
    }
1070

1071
    const framebuffer =
1072
      (
1073
        renderPass as RenderPass & {
10!
1074
          framebuffer?: {
1075
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1076
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1077
          };
1078
        }
1079
      ).framebuffer || renderPass.props.framebuffer;
1080

1081
    const nextColorAttachmentFormats = framebuffer?.colorAttachments?.map(colorAttachment =>
68✔
1082
      asColorAttachmentFormat(colorAttachment?.texture?.format)
10✔
1083
    );
1084
    const nextDepthStencilAttachmentFormat = asDepthStencilAttachmentFormat(
68✔
1085
      framebuffer?.depthStencilAttachment?.texture?.format
1086
    );
1087

1088
    if (
68✔
1089
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
72✔
1090
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1091
    ) {
1092
      this._colorAttachmentFormats = nextColorAttachmentFormats;
6✔
1093
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
6✔
1094
      this._setPipelineNeedsUpdate('attachment formats');
6✔
1095
    }
1096
  }
1097
}
1098

1099
// HELPERS
1100

1101
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1102
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
10!
1103
}
1104

1105
function asDepthStencilAttachmentFormat(
1106
  format?: string | null
1107
): TextureFormatDepthStencil | undefined {
1108
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
10!
1109
}
1110

1111
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1112
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
10✔
1113
}
1114

1115
/** Create a shadertools platform info from the Device */
1116
export function getPlatformInfo(device: Device): PlatformInfo {
1117
  return {
103✔
1118
    type: device.type,
1119
    shaderLanguage: device.info.shadingLanguage,
1120
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1121
    gpu: device.info.gpu,
1122
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1123
    features: device.features as unknown as Set<DeviceFeature>
1124
  };
1125
}
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