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

visgl / luma.gl / 23384903092

21 Mar 2026 05:30PM UTC coverage: 72.417% (+9.3%) from 63.136%
23384903092

push

github

web-flow
feat(devtool); Enable LLMs to automatically test website, package for devtools upstream (#2557)

4103 of 6350 branches covered (64.61%)

Branch coverage included in aggregate %.

215 of 271 new or added lines in 13 files covered. (79.34%)

45 existing lines in 6 files now uncovered.

9124 of 11915 relevant lines covered (76.58%)

277.34 hits per line

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

70.11
/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 PrimitiveTopology,
17
  Device,
18
  DeviceFeature,
19
  Buffer,
20
  Texture,
21
  TextureView,
22
  Sampler,
23
  RenderPipeline,
24
  RenderPass,
25
  PipelineFactory,
26
  ShaderFactory,
27
  UniformStore,
28
  log,
29
  dataTypeDecoder,
30
  getAttributeInfosFromLayouts
31
} from '@luma.gl/core';
32

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

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

47
const LOG_DRAW_PRIORITY = 2;
63✔
48
const LOG_DRAW_TIMEOUT = 10000;
63✔
49

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

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

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

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

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

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

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

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

92
  transformFeedback?: TransformFeedback;
93

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

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

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

139
    isInstanced: undefined!,
140
    instanceCount: 0,
141
    vertexCount: 0,
142

143
    shaderInputs: undefined!,
144
    pipelineFactory: undefined!,
145
    shaderFactory: undefined!,
146
    transformFeedback: undefined!,
147
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
148

149
    debugShaders: undefined!,
150
    disableWarnings: undefined!
151
  };
152

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

173
  // Fixed properties (change can trigger pipeline rebuild)
174

175
  /** The render pipeline GPU parameters, depth testing etc */
176
  parameters: RenderPipelineParameters;
177

178
  /** The primitive topology */
179
  topology: PrimitiveTopology;
180
  /** Buffer layout */
181
  bufferLayout: BufferLayout[];
182

183
  // Dynamic properties
184

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

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

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

208
  /** TransformFeedback, WebGL 2 only. */
209
  transformFeedback: TransformFeedback | null = null;
62✔
210

211
  /** The underlying GPU "program". @note May be recreated if parameters change */
212
  pipeline: RenderPipeline;
213

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

220
  _attributeInfos: Record<string, AttributeInfo> = {};
62✔
221
  _gpuGeometry: GPUGeometry | null = null;
62✔
222
  private props: Required<ModelProps>;
223

224
  _pipelineNeedsUpdate: string | false = 'newly created';
62✔
225
  private _needsRedraw: string | false = 'initializing';
62✔
226
  private _destroyed = false;
62✔
227

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

231
  get [Symbol.toStringTag](): string {
232
    return 'Model';
×
233
  }
234

235
  toString(): string {
236
    return `Model(${this.id})`;
74✔
237
  }
238

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

245
    Object.assign(this.userData, props.userData);
62✔
246

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

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

258
    // Setup shader assembler
259
    const platformInfo = getPlatformInfo(device);
62✔
260

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

266
    const isWebGPU = this.device.type === 'webgpu';
62✔
267

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

292
      this.vs = vs;
58✔
293
      this.fs = fs;
58✔
294
      // @ts-expect-error
295
      this._getModuleUniforms = getUniforms;
58✔
296
    }
297

298
    this.vertexCount = this.props.vertexCount;
62✔
299
    this.instanceCount = this.props.instanceCount;
62✔
300

301
    this.topology = this.props.topology;
62✔
302
    this.bufferLayout = this.props.bufferLayout;
62✔
303
    this.parameters = this.props.parameters;
62✔
304

305
    // Geometry, if provided, sets topology and vertex cound
306
    if (props.geometry) {
62✔
307
      this.setGeometry(props.geometry);
27✔
308
    }
309

310
    this.pipelineFactory =
62✔
311
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
124✔
312
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
62✔
313

314
    // Create the pipeline
315
    // @note order is important
316
    this.pipeline = this._updatePipeline();
62✔
317

318
    this.vertexArray = device.createVertexArray({
62✔
319
      shaderLayout: this.pipeline.shaderLayout,
320
      bufferLayout: this.pipeline.bufferLayout
321
    });
322

323
    // Now we can apply geometry attributes
324
    if (this._gpuGeometry) {
62✔
325
      this._setGeometryAttributes(this._gpuGeometry);
27✔
326
    }
327

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

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

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

372
  // Draw call
373

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

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

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

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

410
    try {
37✔
411
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
37✔
412
      this.predraw();
37✔
413
    } finally {
414
      renderPass.popDebugGroup();
37✔
415
    }
416

417
    let drawSuccess: boolean;
418
    try {
37✔
419
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
37✔
420
      this._logDrawCallStart();
37✔
421

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

427
      const syncBindings = this._getBindings();
37✔
428

429
      const {indexBuffer} = this.vertexArray;
37✔
430
      const indexCount = indexBuffer
37✔
431
        ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
12!
432
        : undefined;
433

434
      drawSuccess = this.pipeline.draw({
37✔
435
        renderPass,
436
        vertexArray: this.vertexArray,
437
        isInstanced: this.isInstanced,
438
        vertexCount: this.vertexCount,
439
        instanceCount: this.instanceCount,
440
        indexCount,
441
        transformFeedback: this.transformFeedback || undefined,
57✔
442
        // Pipelines may be shared across models when caching is enabled, so bindings
443
        // and WebGL uniforms must be supplied on every draw instead of being stored
444
        // on the pipeline instance.
445
        bindings: syncBindings,
446
        uniforms: this.props.uniforms,
447
        // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
448
        // so we must provide our unique parameters to each draw
449
        // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
450
        parameters: this.parameters,
451
        topology: this.topology
452
      });
453
    } finally {
454
      renderPass.popDebugGroup();
37✔
455
      this._logDrawCallEnd();
37✔
456
    }
457
    this._logFramebuffer(renderPass);
37✔
458

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

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

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

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

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

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

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

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

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

543
  // Update dynamic fields
544

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

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

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

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

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

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

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

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

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

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

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

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

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

695
  // INTERNAL METHODS
696

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

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

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

722
    return validBindings;
101✔
723
  }
724

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

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

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

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

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

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

791
      this._pipelineNeedsUpdate = false;
64✔
792

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

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

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

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

829
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
64✔
830
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
64✔
831
        this.shaderFactory.release(prevShaderFs);
1✔
832
      }
833
    }
834
    return this.pipeline;
136✔
835
  }
836

837
  /** Throttle draw call logging */
838
  _lastLogTime = 0;
62✔
839
  _logOpen = false;
62✔
840

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

848
    this._lastLogTime = Date.now();
×
849
    this._logOpen = true;
×
850

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

854
  _logDrawCallEnd(): void {
855
    if (this._logOpen) {
37!
856
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
857

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

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

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

869
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
870
      this._logOpen = false;
×
871
    }
872
  }
873

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

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

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

927
function shaderModuleHasUniforms(module: ShaderModule): boolean {
928
  return Boolean(module.uniformTypes && !isObjectEmpty(module.uniformTypes));
60✔
929
}
930

931
// HELPERS
932

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

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