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

visgl / luma.gl / 23405224180

22 Mar 2026 02:32PM UTC coverage: 72.564% (+0.1%) from 72.417%
23405224180

Pull #2560

github

web-flow
Merge 7eeefee9e into 00727c0da
Pull Request #2560: feat(core, webgpu): Support WebGPU bind groups and WGSL `auto` binding syntax

4320 of 6694 branches covered (64.54%)

Branch coverage included in aggregate %.

470 of 560 new or added lines in 30 files covered. (83.93%)

2 existing lines in 2 files now uncovered.

9523 of 12383 relevant lines covered (76.9%)

271.85 hits per line

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

69.64
/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 Shader,
12
  type VertexArray,
13
  type TransformFeedback,
14
  type AttributeInfo,
15
  type Binding,
16
  type BindingsByGroup,
17
  type PrimitiveTopology,
18
  Device,
19
  DeviceFeature,
20
  Buffer,
21
  Texture,
22
  TextureView,
23
  Sampler,
24
  RenderPipeline,
25
  RenderPass,
26
  PipelineFactory,
27
  ShaderFactory,
28
  UniformStore,
29
  log,
30
  dataTypeDecoder,
31
  getAttributeInfosFromLayouts,
32
  normalizeBindingsByGroup
33
} from '@luma.gl/core';
34

35
import type {ShaderBindingDebugRow, ShaderModule, PlatformInfo} from '@luma.gl/shadertools';
36
import {ShaderAssembler} from '@luma.gl/shadertools';
37

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

54
const LOG_DRAW_PRIORITY = 2;
64✔
55
const LOG_DRAW_TIMEOUT = 10000;
64✔
56

57
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
58
  source?: string;
59
  vs?: string | null;
60
  fs?: string | null;
61

62
  /** shadertool shader modules (added to shader code) */
63
  modules?: ShaderModule[];
64
  /** Shadertool module defines (configures shader code)*/
65
  defines?: Record<string, boolean>;
66
  // TODO - injections, hooks etc?
67

68
  /** Shader inputs, used to generated uniform buffers and bindings */
69
  shaderInputs?: ShaderInputs;
70
  /** Material-owned group-3 bindings */
71
  material?: Material;
72
  /** Bindings */
73
  bindings?: Record<string, Binding | DynamicTexture>;
74
  /** WebGL-only uniforms */
75
  uniforms?: Record<string, unknown>;
76
  /** Parameters that are built into the pipeline */
77
  parameters?: RenderPipelineParameters;
78

79
  /** Geometry */
80
  geometry?: GPUGeometry | Geometry | null;
81

82
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
83
  isInstanced?: boolean;
84
  /** instance count */
85
  instanceCount?: number;
86
  /** Vertex count */
87
  vertexCount?: number;
88

89
  indexBuffer?: Buffer | null;
90
  /** @note this is really a map of buffers, not a map of attributes */
91
  attributes?: Record<string, Buffer>;
92
  /**   */
93
  constantAttributes?: Record<string, TypedArray>;
94

95
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
96
  disableWarnings?: boolean;
97

98
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
99
  varyings?: string[];
100

101
  transformFeedback?: TransformFeedback;
102

103
  /** Show shader source in browser? */
104
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
105

106
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
107
  pipelineFactory?: PipelineFactory;
108
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
109
  shaderFactory?: ShaderFactory;
110
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
111
  shaderAssembler?: ShaderAssembler;
112
};
113

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

148
    isInstanced: undefined!,
149
    instanceCount: 0,
150
    vertexCount: 0,
151

152
    shaderInputs: undefined!,
153
    material: undefined!,
154
    pipelineFactory: undefined!,
155
    shaderFactory: undefined!,
156
    transformFeedback: undefined!,
157
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
158

159
    debugShaders: undefined!,
160
    disableWarnings: undefined!
161
  };
162

163
  /** Device that created this model */
164
  readonly device: Device;
165
  /** Application provided identifier */
166
  readonly id: string;
167
  /** WGSL shader source when using unified shader */
168
  // @ts-expect-error assigned in function called from constructor
169
  readonly source: string;
170
  /** GLSL vertex shader source */
171
  // @ts-expect-error assigned in function called from constructor
172
  readonly vs: string;
173
  /** GLSL fragment shader source */
174
  // @ts-expect-error assigned in function called from constructor
175
  readonly fs: string;
176
  /** Factory used to create render pipelines */
177
  readonly pipelineFactory: PipelineFactory;
178
  /** Factory used to create shaders */
179
  readonly shaderFactory: ShaderFactory;
180
  /** User-supplied per-model data */
181
  userData: {[key: string]: any} = {};
64✔
182

183
  // Fixed properties (change can trigger pipeline rebuild)
184

185
  /** The render pipeline GPU parameters, depth testing etc */
186
  parameters: RenderPipelineParameters;
187

188
  /** The primitive topology */
189
  topology: PrimitiveTopology;
190
  /** Buffer layout */
191
  bufferLayout: BufferLayout[];
192

193
  // Dynamic properties
194

195
  /** Use instanced rendering */
196
  isInstanced: boolean | undefined = undefined;
64✔
197
  /** instance count. `undefined` means not instanced */
198
  instanceCount: number = 0;
64✔
199
  /** Vertex count */
200
  vertexCount: number;
201

202
  /** Index buffer */
203
  indexBuffer: Buffer | null = null;
64✔
204
  /** Buffer-valued attributes */
205
  bufferAttributes: Record<string, Buffer> = {};
64✔
206
  /** Constant-valued attributes */
207
  constantAttributes: Record<string, TypedArray> = {};
64✔
208
  /** Bindings (textures, samplers, uniform buffers) */
209
  bindings: Record<string, Binding | DynamicTexture> = {};
64✔
210

211
  /**
212
   * VertexArray
213
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
214
   * @todo - allow application to define multiple vertex arrays?
215
   * */
216
  vertexArray: VertexArray;
217

218
  /** TransformFeedback, WebGL 2 only. */
219
  transformFeedback: TransformFeedback | null = null;
64✔
220

221
  /** The underlying GPU "program". @note May be recreated if parameters change */
222
  pipeline: RenderPipeline;
223

224
  /** ShaderInputs instance */
225
  // @ts-expect-error Assigned in function called by constructor
226
  shaderInputs: ShaderInputs;
227
  material: Material | null = null;
64✔
228
  // @ts-expect-error Assigned in function called by constructor
229
  _uniformStore: UniformStore;
230

231
  _attributeInfos: Record<string, AttributeInfo> = {};
64✔
232
  _gpuGeometry: GPUGeometry | null = null;
64✔
233
  private props: Required<ModelProps>;
234

235
  _pipelineNeedsUpdate: string | false = 'newly created';
64✔
236
  private _needsRedraw: string | false = 'initializing';
64✔
237
  private _destroyed = false;
64✔
238

239
  /** "Time" of last draw. Monotonically increasing timestamp */
240
  _lastDrawTimestamp: number = -1;
64✔
241
  private _bindingTable: ShaderBindingDebugRow[] = [];
64✔
242

243
  get [Symbol.toStringTag](): string {
244
    return 'Model';
×
245
  }
246

247
  toString(): string {
248
    return `Model(${this.id})`;
74✔
249
  }
250

251
  constructor(device: Device, props: ModelProps) {
252
    this.props = {...Model.defaultProps, ...props};
64✔
253
    props = this.props;
64✔
254
    this.id = props.id || uid('model');
64!
255
    this.device = device;
64✔
256

257
    Object.assign(this.userData, props.userData);
64✔
258

259
    this.material = props.material || null;
64✔
260

261
    // Setup shader module inputs
262
    const moduleMap = Object.fromEntries(
64✔
263
      this.props.modules?.map(module => [module.name, module]) || []
26!
264
    );
265

266
    const shaderInputs =
267
      props.shaderInputs ||
64✔
268
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
269
    // @ts-ignore
270
    this.setShaderInputs(shaderInputs);
64✔
271

272
    // Setup shader assembler
273
    const platformInfo = getPlatformInfo(device);
64✔
274

275
    // Extract modules from shader inputs if not supplied
276
    const modules =
64!
277
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
278
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
64✔
279

280
    this.props.shaderLayout =
64✔
281
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
128✔
282

283
    const isWebGPU = this.device.type === 'webgpu';
64✔
284

285
    // WebGPU
286
    // TODO - hack to support unified WGSL shader
287
    // TODO - this is wrong, compile a single shader
288
    if (isWebGPU && this.props.source) {
64✔
289
      // WGSL
290
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
5✔
291
        platformInfo,
292
        ...this.props,
293
        modules
294
      });
295
      this.source = source;
5✔
296
      // @ts-expect-error
297
      this._getModuleUniforms = getUniforms;
5✔
298
      this._bindingTable = bindingTable;
5✔
299
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
300
      const inferredShaderLayout = (
301
        device as Device & {getShaderLayout?: (source: string) => any}
5✔
302
      ).getShaderLayout?.(this.source);
303
      this.props.shaderLayout =
5✔
304
        mergeShaderModuleBindingsIntoLayout(
5!
305
          this.props.shaderLayout || inferredShaderLayout || null,
10!
306
          modules
307
        ) || null;
308
    } else {
309
      // GLSL
310
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
59✔
311
        platformInfo,
312
        ...this.props,
313
        modules
314
      });
315

316
      this.vs = vs;
59✔
317
      this.fs = fs;
59✔
318
      // @ts-expect-error
319
      this._getModuleUniforms = getUniforms;
59✔
320
      this._bindingTable = [];
59✔
321
    }
322

323
    this.vertexCount = this.props.vertexCount;
64✔
324
    this.instanceCount = this.props.instanceCount;
64✔
325

326
    this.topology = this.props.topology;
64✔
327
    this.bufferLayout = this.props.bufferLayout;
64✔
328
    this.parameters = this.props.parameters;
64✔
329

330
    // Geometry, if provided, sets topology and vertex cound
331
    if (props.geometry) {
64✔
332
      this.setGeometry(props.geometry);
27✔
333
    }
334

335
    this.pipelineFactory =
64✔
336
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
128✔
337
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
64✔
338

339
    // Create the pipeline
340
    // @note order is important
341
    this.pipeline = this._updatePipeline();
64✔
342

343
    this.vertexArray = device.createVertexArray({
64✔
344
      shaderLayout: this.pipeline.shaderLayout,
345
      bufferLayout: this.pipeline.bufferLayout
346
    });
347

348
    // Now we can apply geometry attributes
349
    if (this._gpuGeometry) {
64✔
350
      this._setGeometryAttributes(this._gpuGeometry);
27✔
351
    }
352

353
    // Apply any dynamic settings that will not trigger pipeline change
354
    if ('isInstanced' in props) {
64!
355
      this.isInstanced = props.isInstanced;
64✔
356
    }
357

358
    if (props.instanceCount) {
64✔
359
      this.setInstanceCount(props.instanceCount);
10✔
360
    }
361
    if (props.vertexCount) {
64✔
362
      this.setVertexCount(props.vertexCount);
32✔
363
    }
364
    if (props.indexBuffer) {
64!
365
      this.setIndexBuffer(props.indexBuffer);
×
366
    }
367
    if (props.attributes) {
64✔
368
      this.setAttributes(props.attributes);
63✔
369
    }
370
    if (props.constantAttributes) {
64!
371
      this.setConstantAttributes(props.constantAttributes);
64✔
372
    }
373
    if (props.bindings) {
64!
374
      this.setBindings(props.bindings);
64✔
375
    }
376
    if (props.transformFeedback) {
64!
377
      this.transformFeedback = props.transformFeedback;
×
378
    }
379
  }
380

381
  destroy(): void {
382
    if (!this._destroyed) {
50✔
383
      // Release pipeline before we destroy the shaders used by the pipeline
384
      this.pipelineFactory.release(this.pipeline);
49✔
385
      // Release the shaders
386
      this.shaderFactory.release(this.pipeline.vs);
49✔
387
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
49✔
388
        this.shaderFactory.release(this.pipeline.fs);
44✔
389
      }
390
      this._uniformStore.destroy();
49✔
391
      // TODO - mark resource as managed and destroyIfManaged() ?
392
      this._gpuGeometry?.destroy();
49✔
393
      this._destroyed = true;
49✔
394
    }
395
  }
396

397
  // Draw call
398

399
  /** Query redraw status. Clears the status. */
400
  needsRedraw(): false | string {
401
    // Catch any writes to already bound resources
402
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
×
403
      this.setNeedsRedraw('contents of bound textures or buffers updated');
×
404
    }
405
    const needsRedraw = this._needsRedraw;
×
406
    this._needsRedraw = false;
×
407
    return needsRedraw;
×
408
  }
409

410
  /** Mark the model as needing a redraw */
411
  setNeedsRedraw(reason: string): void {
412
    this._needsRedraw ||= reason;
540✔
413
  }
414

415
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
416
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
417
    return this._bindingTable;
2✔
418
  }
419

420
  /** Update uniforms and pipeline state prior to drawing. */
421
  predraw(): void {
422
    // Update uniform buffers if needed
423
    this.updateShaderInputs();
37✔
424
    // Check if the pipeline is invalidated
425
    this.pipeline = this._updatePipeline();
37✔
426
  }
427

428
  /**
429
   * Issue one draw call.
430
   * @param renderPass - render pass to draw into
431
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
432
   */
433
  draw(renderPass: RenderPass): boolean {
434
    const loadingBinding = this._areBindingsLoading();
37✔
435
    if (loadingBinding) {
37!
436
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
437
      return false;
×
438
    }
439

440
    try {
37✔
441
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
37✔
442
      this.predraw();
37✔
443
    } finally {
444
      renderPass.popDebugGroup();
37✔
445
    }
446

447
    let drawSuccess: boolean;
448
    try {
37✔
449
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
37✔
450
      this._logDrawCallStart();
37✔
451

452
      // Update the pipeline if invalidated
453
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
454
      // Application can call Model.predraw() to avoid this.
455
      this.pipeline = this._updatePipeline();
37✔
456

457
      const syncBindings = this._getBindings();
37✔
458
      const syncBindGroups = this._getBindGroups();
37✔
459

460
      const {indexBuffer} = this.vertexArray;
37✔
461
      const indexCount = indexBuffer
37✔
462
        ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
12!
463
        : undefined;
464

465
      drawSuccess = this.pipeline.draw({
37✔
466
        renderPass,
467
        vertexArray: this.vertexArray,
468
        isInstanced: this.isInstanced,
469
        vertexCount: this.vertexCount,
470
        instanceCount: this.instanceCount,
471
        indexCount,
472
        transformFeedback: this.transformFeedback || undefined,
57✔
473
        // Pipelines may be shared across models when caching is enabled, so bindings
474
        // and WebGL uniforms must be supplied on every draw instead of being stored
475
        // on the pipeline instance.
476
        bindings: syncBindings,
477
        bindGroups: syncBindGroups,
478
        _bindGroupCacheKeys: this._getBindGroupCacheKeys(),
479
        uniforms: this.props.uniforms,
480
        // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
481
        // so we must provide our unique parameters to each draw
482
        // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
483
        parameters: this.parameters,
484
        topology: this.topology
485
      });
486
    } finally {
487
      renderPass.popDebugGroup();
37✔
488
      this._logDrawCallEnd();
37✔
489
    }
490
    this._logFramebuffer(renderPass);
37✔
491

492
    // Update needsRedraw flag
493
    if (drawSuccess) {
37!
494
      this._lastDrawTimestamp = this.device.timestamp;
37✔
495
      this._needsRedraw = false;
37✔
496
    } else {
497
      this._needsRedraw = 'waiting for resource initialization';
×
498
    }
499
    return drawSuccess;
37✔
500
  }
501

502
  // Update fixed fields (can trigger pipeline rebuild)
503

504
  /**
505
   * Updates the optional geometry
506
   * Geometry, set topology and bufferLayout
507
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
508
   */
509
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
510
    this._gpuGeometry?.destroy();
27✔
511
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
27✔
512
    if (gpuGeometry) {
27!
513
      this.setTopology(gpuGeometry.topology || 'triangle-list');
27!
514
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
27✔
515
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
27✔
516
        gpuGeometry.bufferLayout,
517
        this.bufferLayout
518
      );
519
      if (this.vertexArray) {
27!
520
        this._setGeometryAttributes(gpuGeometry);
×
521
      }
522
    }
523
    this._gpuGeometry = gpuGeometry;
27✔
524
  }
525

526
  /**
527
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
528
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
529
   */
530
  setTopology(topology: PrimitiveTopology): void {
531
    if (topology !== this.topology) {
30✔
532
      this.topology = topology;
8✔
533
      this._setPipelineNeedsUpdate('topology');
8✔
534
    }
535
  }
536

537
  /**
538
   * Updates the buffer layout.
539
   * @note Triggers a pipeline rebuild / pipeline cache fetch
540
   */
541
  setBufferLayout(bufferLayout: BufferLayout[]): void {
542
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
×
543
    this.bufferLayout = this._gpuGeometry
×
544
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
545
      : bufferLayout;
546
    this._setPipelineNeedsUpdate('bufferLayout');
×
547

548
    // Recreate the pipeline
549
    this.pipeline = this._updatePipeline();
×
550

551
    // vertex array needs to be updated if we update buffer layout,
552
    // but not if we update parameters
553
    this.vertexArray = this.device.createVertexArray({
×
554
      shaderLayout: this.pipeline.shaderLayout,
555
      bufferLayout: this.pipeline.bufferLayout
556
    });
557

558
    // Reapply geometry attributes to the new vertex array
559
    if (this._gpuGeometry) {
×
560
      this._setGeometryAttributes(this._gpuGeometry);
×
561
    }
562
  }
563

564
  /**
565
   * Set GPU parameters.
566
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
567
   * @param parameters
568
   */
569
  setParameters(parameters: RenderPipelineParameters) {
570
    if (!deepEqual(parameters, this.parameters, 2)) {
×
571
      this.parameters = parameters;
×
572
      this._setPipelineNeedsUpdate('parameters');
×
573
    }
574
  }
575

576
  // Update dynamic fields
577

578
  /**
579
   * Updates the instance count (used in draw calls)
580
   * @note Any attributes with stepMode=instance need to be at least this big
581
   */
582
  setInstanceCount(instanceCount: number): void {
583
    this.instanceCount = instanceCount;
24✔
584
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
585
    // but deck.gl sets instanceCount even for models that are not instanced.
586
    if (this.isInstanced === undefined && instanceCount > 0) {
24✔
587
      this.isInstanced = true;
17✔
588
    }
589
    this.setNeedsRedraw('instanceCount');
24✔
590
  }
591

592
  /**
593
   * Updates the vertex count (used in draw calls)
594
   * @note Any attributes with stepMode=vertex need to be at least this big
595
   */
596
  setVertexCount(vertexCount: number): void {
597
    this.vertexCount = vertexCount;
33✔
598
    this.setNeedsRedraw('vertexCount');
33✔
599
  }
600

601
  /** Set the shader inputs */
602
  setShaderInputs(shaderInputs: ShaderInputs): void {
603
    this.shaderInputs = shaderInputs;
64✔
604
    this._uniformStore = new UniformStore(this.shaderInputs.modules);
64✔
605
    // Create uniform buffer bindings for all modules that actually have uniforms
606
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
64✔
607
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
65✔
608
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(this.device, moduleName);
47✔
609
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
47✔
610
      }
611
    }
612
    this.setNeedsRedraw('shaderInputs');
64✔
613
  }
614

615
  setMaterial(material: Material | null): void {
NEW
616
    this.material = material;
×
NEW
617
    this.setNeedsRedraw('material');
×
618
  }
619

620
  /** Update uniform buffers from the model's shader inputs */
621
  updateShaderInputs(): void {
622
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues());
37✔
623
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
37✔
624
    // TODO - this is already tracked through buffer/texture update times?
625
    this.setNeedsRedraw('shaderInputs');
37✔
626
  }
627

628
  /**
629
   * Sets bindings (textures, samplers, uniform buffers)
630
   */
631
  setBindings(bindings: Record<string, Binding | DynamicTexture>): void {
632
    Object.assign(this.bindings, bindings);
108✔
633
    this.setNeedsRedraw('bindings');
108✔
634
  }
635

636
  /**
637
   * Updates optional transform feedback. WebGL only.
638
   */
639
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
640
    this.transformFeedback = transformFeedback;
16✔
641
    this.setNeedsRedraw('transformFeedback');
16✔
642
  }
643

644
  /**
645
   * Sets the index buffer
646
   * @todo - how to unset it if we change geometry?
647
   */
648
  setIndexBuffer(indexBuffer: Buffer | null): void {
649
    this.vertexArray.setIndexBuffer(indexBuffer);
27✔
650
    this.setNeedsRedraw('indexBuffer');
27✔
651
  }
652

653
  /**
654
   * Sets attributes (buffers)
655
   * @note Overrides any attributes previously set with the same name
656
   */
657
  setAttributes(buffers: Record<string, Buffer>, options?: {disableWarnings?: boolean}): void {
658
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
131✔
659
    if (buffers['indices']) {
131!
660
      log.warn(
×
661
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
662
      )();
663
    }
664

665
    // ensure bufferLayout order matches source layout so we bind
666
    // the correct buffers to the correct indices in webgpu.
667
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
131✔
668
      this.pipeline.shaderLayout,
669
      this.bufferLayout
670
    );
671
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
131✔
672

673
    // Check if all buffers have a layout
674
    for (const [bufferName, buffer] of Object.entries(buffers)) {
131✔
675
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
279✔
676
      if (!bufferLayout) {
279!
677
        if (!disableWarnings) {
×
678
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
679
        }
680
        continue; // eslint-disable-line no-continue
×
681
      }
682

683
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
684
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
685
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
279✔
686
      let set = false;
279✔
687
      for (const attributeName of attributeNames) {
279✔
688
        const attributeInfo = this._attributeInfos[attributeName];
279✔
689
        if (attributeInfo) {
279✔
690
          const location =
691
            this.device.type === 'webgpu'
239✔
692
              ? bufferLayoutHelper.getBufferIndex(attributeInfo.bufferName)
693
              : attributeInfo.location;
694

695
          this.vertexArray.setBuffer(location, buffer);
239✔
696
          set = true;
239✔
697
        }
698
      }
699
      if (!set && !disableWarnings) {
279✔
700
        log.warn(
2✔
701
          `Model(${this.id}): Ignoring buffer "${buffer.id}" for unknown attribute "${bufferName}"`
702
        )();
703
      }
704
    }
705
    this.setNeedsRedraw('attributes');
131✔
706
  }
707

708
  /**
709
   * Sets constant attributes
710
   * @note Overrides any attributes previously set with the same name
711
   * Constant attributes are only supported in WebGL, not in WebGPU
712
   * Any attribute that is disabled in the current vertex array object
713
   * is read from the context's global constant value for that attribute location.
714
   * @param constantAttributes
715
   */
716
  setConstantAttributes(
717
    attributes: Record<string, TypedArray>,
718
    options?: {disableWarnings?: boolean}
719
  ): void {
720
    for (const [attributeName, value] of Object.entries(attributes)) {
64✔
721
      const attributeInfo = this._attributeInfos[attributeName];
×
722
      if (attributeInfo) {
×
723
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
724
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
725
        log.warn(
×
726
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
727
        )();
728
      }
729
    }
730
    this.setNeedsRedraw('constants');
64✔
731
  }
732

733
  // INTERNAL METHODS
734

735
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
736
  _areBindingsLoading(): string | false {
737
    for (const binding of Object.values(this.bindings)) {
37✔
738
      if (binding instanceof DynamicTexture && !binding.isReady) {
32!
739
        return binding.id;
×
740
      }
741
    }
742
    for (const binding of Object.values(this.material?.bindings || {})) {
37✔
NEW
743
      if (binding instanceof DynamicTexture && !binding.isReady) {
×
NEW
744
        return binding.id;
×
745
      }
746
    }
747
    return false;
37✔
748
  }
749

750
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
751
  _getBindings(): Record<string, Binding> {
752
    const validBindings: Record<string, Binding> = {};
140✔
753

754
    for (const [name, binding] of Object.entries(this.bindings)) {
140✔
755
      if (binding instanceof DynamicTexture) {
111✔
756
        // Check that async textures are loaded
757
        if (binding.isReady) {
2!
758
          validBindings[name] = binding.texture;
2✔
759
        }
760
      } else {
761
        validBindings[name] = binding;
109✔
762
      }
763
    }
764

765
    return validBindings;
140✔
766
  }
767

768
  _getBindGroups(): BindingsByGroup {
769
    const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
103✔
770
    const bindGroups = shaderLayout.bindings.length
103✔
771
      ? normalizeBindingsByGroup(shaderLayout, this._getBindings())
772
      : {0: this._getBindings()};
773

774
    if (!this.material) {
103✔
775
      return bindGroups;
100✔
776
    }
777

778
    for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) {
3✔
779
      const group = Number(groupKey);
3✔
780
      bindGroups[group] = {
3✔
781
        ...(bindGroups[group] || {}),
6✔
782
        ...groupBindings
783
      };
784
    }
785

786
    return bindGroups;
3✔
787
  }
788

789
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
790
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
37✔
791
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
37!
792
  }
793

794
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
795
  _getBindingsUpdateTimestamp(): number {
796
    let timestamp = 0;
×
797
    for (const binding of Object.values(this.bindings)) {
×
798
      if (binding instanceof TextureView) {
×
799
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
800
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
801
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
802
      } else if (binding instanceof DynamicTexture) {
×
803
        timestamp = binding.texture
×
804
          ? Math.max(timestamp, binding.texture.updateTimestamp)
805
          : // The texture will become available in the future
806
            Infinity;
807
      } else if (!(binding instanceof Sampler)) {
×
808
        timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
×
809
      }
810
    }
NEW
811
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
×
812
  }
813

814
  /**
815
   * Updates the optional geometry attributes
816
   * Geometry, sets several attributes, indexBuffer, and also vertex count
817
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
818
   */
819
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
820
    // Filter geometry attribute so that we don't issue warnings for unused attributes
821
    const attributes = {...gpuGeometry.attributes};
27✔
822
    for (const [attributeName] of Object.entries(attributes)) {
27✔
823
      if (
78✔
824
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
248✔
825
        attributeName !== 'positions'
826
      ) {
827
        delete attributes[attributeName];
38✔
828
      }
829
    }
830

831
    // TODO - delete previous geometry?
832
    this.vertexCount = gpuGeometry.vertexCount;
27✔
833
    this.setIndexBuffer(gpuGeometry.indices || null);
27✔
834
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
27✔
835
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
27✔
836

837
    this.setNeedsRedraw('geometry attributes');
27✔
838
  }
839

840
  /** Mark pipeline as needing update */
841
  _setPipelineNeedsUpdate(reason: string): void {
842
    this._pipelineNeedsUpdate ||= reason;
8✔
843
    this.setNeedsRedraw(reason);
8✔
844
  }
845

846
  /** Update pipeline if needed */
847
  _updatePipeline(): RenderPipeline {
848
    if (this._pipelineNeedsUpdate) {
138✔
849
      let prevShaderVs: Shader | null = null;
66✔
850
      let prevShaderFs: Shader | null = null;
66✔
851
      if (this.pipeline) {
66✔
852
        log.log(
2✔
853
          1,
854
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
855
        )();
856
        prevShaderVs = this.pipeline.vs;
2✔
857
        prevShaderFs = this.pipeline.fs;
2✔
858
      }
859

860
      this._pipelineNeedsUpdate = false;
66✔
861

862
      const vs = this.shaderFactory.createShader({
66✔
863
        id: `${this.id}-vertex`,
864
        stage: 'vertex',
865
        source: this.source || this.vs,
126✔
866
        debugShaders: this.props.debugShaders
867
      });
868

869
      let fs: Shader | null = null;
66✔
870
      if (this.source) {
66✔
871
        fs = vs;
6✔
872
      } else if (this.fs) {
60!
873
        fs = this.shaderFactory.createShader({
60✔
874
          id: `${this.id}-fragment`,
875
          stage: 'fragment',
876
          source: this.source || this.fs,
120✔
877
          debugShaders: this.props.debugShaders
878
        });
879
      }
880

881
      this.pipeline = this.pipelineFactory.createRenderPipeline({
66✔
882
        ...this.props,
883
        bindings: undefined,
884
        bufferLayout: this.bufferLayout,
885
        topology: this.topology,
886
        parameters: this.parameters,
887
        bindGroups: this._getBindGroups(),
888
        vs,
889
        fs
890
      });
891

892
      this._attributeInfos = getAttributeInfosFromLayouts(
66✔
893
        this.pipeline.shaderLayout,
894
        this.bufferLayout
895
      );
896

897
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
66✔
898
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
66✔
899
        this.shaderFactory.release(prevShaderFs);
1✔
900
      }
901
    }
902
    return this.pipeline;
138✔
903
  }
904

905
  /** Throttle draw call logging */
906
  _lastLogTime = 0;
64✔
907
  _logOpen = false;
64✔
908

909
  _logDrawCallStart(): void {
910
    // IF level is 4 or higher, log every frame.
911
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
37!
912
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
37!
913
      return;
37✔
914
    }
915

916
    this._lastLogTime = Date.now();
×
917
    this._logOpen = true;
×
918

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

922
  _logDrawCallEnd(): void {
923
    if (this._logOpen) {
37!
924
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
925

926
      // log.table(logLevel, attributeTable)();
927
      // log.table(logLevel, uniformTable)();
928
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
929

930
      const uniformTable = this.shaderInputs.getDebugTable();
×
931
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
932

933
      const attributeTable = this._getAttributeDebugTable();
×
934
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
935
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
936

937
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
938
      this._logOpen = false;
×
939
    }
940
  }
941

942
  protected _drawCount = 0;
64✔
943
  _logFramebuffer(renderPass: RenderPass): void {
944
    const debugFramebuffers = this.device.props.debugFramebuffers;
37✔
945
    this._drawCount++;
37✔
946
    // Update first 3 frames and then every 60 frames
947
    if (!debugFramebuffers) {
37!
948
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
949
      return;
37✔
950
    }
951
    // TODO - display framebuffer output in debug window
952
    const framebuffer = renderPass.props.framebuffer;
×
953
    if (framebuffer) {
×
954
      debugFramebuffer(framebuffer, {id: framebuffer.id, minimap: true});
×
955
      // log.image({logLevel: LOG_DRAW_PRIORITY, message: `${framebuffer.id} %c sup?`, image})();
956
    }
957
  }
958

959
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
960
    const table: Record<string, Record<string, unknown>> = {};
×
961
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
962
      const values = this.vertexArray.attributes[attributeInfo.location];
×
963
      table[attributeInfo.location] = {
×
964
        name,
965
        type: attributeInfo.shaderType,
966
        values: values
×
967
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
968
          : 'null'
969
      };
970
    }
971
    if (this.vertexArray.indexBuffer) {
×
972
      const {indexBuffer} = this.vertexArray;
×
973
      const values =
974
        indexBuffer.indexType === 'uint32'
×
975
          ? new Uint32Array(indexBuffer.debugData)
976
          : new Uint16Array(indexBuffer.debugData);
977
      table['indices'] = {
×
978
        name: 'indices',
979
        type: indexBuffer.indexType,
980
        values: values.toString()
981
      };
982
    }
983
    return table;
×
984
  }
985

986
  // TODO - fix typing of luma data types
987
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
988
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
989
    const typedArray =
990
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
991
    return typedArray.toString();
×
992
  }
993

994
  private _getNonMaterialBindings(
995
    bindings: Record<string, Binding | DynamicTexture>
996
  ): Record<string, Binding | DynamicTexture> {
997
    if (!this.material) {
37!
998
      return bindings;
37✔
999
    }
1000

NEW
1001
    const filteredBindings: Record<string, Binding | DynamicTexture> = {};
×
NEW
1002
    for (const [name, binding] of Object.entries(bindings)) {
×
NEW
1003
      if (!this.material.ownsBinding(name)) {
×
NEW
1004
        filteredBindings[name] = binding;
×
1005
      }
1006
    }
NEW
1007
    return filteredBindings;
×
1008
  }
1009
}
1010

1011
// HELPERS
1012

1013
/** Create a shadertools platform info from the Device */
1014
export function getPlatformInfo(device: Device): PlatformInfo {
1015
  return {
64✔
1016
    type: device.type,
1017
    shaderLanguage: device.info.shadingLanguage,
1018
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1019
    gpu: device.info.gpu,
1020
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1021
    features: device.features as unknown as Set<DeviceFeature>
1022
  };
1023
}
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