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

visgl / luma.gl / 14683349798

26 Apr 2025 05:08PM UTC coverage: 74.055% (-0.9%) from 74.913%
14683349798

push

github

web-flow
feat(core): TextureFormat generics (#2377)

2019 of 2652 branches covered (76.13%)

Branch coverage included in aggregate %.

62 of 262 new or added lines in 15 files covered. (23.66%)

196 existing lines in 9 files now uncovered.

26575 of 35960 relevant lines covered (73.9%)

47.35 hits per line

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

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

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

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

1✔
38
import type {Geometry} from '../geometry/geometry';
1✔
39
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
1✔
40
import {PipelineFactory} from '../factories/pipeline-factory';
1✔
41
import {ShaderFactory} from '../factories/shader-factory';
1✔
42
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
1✔
43
import {debugFramebuffer} from '../debug/debug-framebuffer';
1✔
44
import {deepEqual} from '../utils/deep-equal';
1✔
45
import {uid} from '../utils/uid';
1✔
46
import {ShaderInputs} from '../shader-inputs';
1✔
47
import {AsyncTexture} from '../async-texture/async-texture';
1✔
48

1✔
49
const LOG_DRAW_PRIORITY = 2;
1✔
50
const LOG_DRAW_TIMEOUT = 10000;
1✔
51

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

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

1✔
63
  /** Shader inputs, used to generated uniform buffers and bindings */
1✔
64
  shaderInputs?: ShaderInputs;
1✔
65
  /** Bindings */
1✔
66
  bindings?: Record<string, Binding | AsyncTexture>;
1✔
67
  /** Parameters that are built into the pipeline */
1✔
68
  parameters?: RenderPipelineParameters;
1✔
69

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

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

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

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

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

1✔
92
  transformFeedback?: TransformFeedback;
1✔
93

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

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

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

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

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

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

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

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

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

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

1✔
166
  // Dynamic properties
1✔
167

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
357
  // Draw call
1✔
358

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
524
  // Update dynamic fields
1✔
525

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
676
  // INTERNAL METHODS
1✔
677

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
910
// HELPERS
1✔
911

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

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