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

visgl / luma.gl / 27048947509

06 Jun 2026 01:36AM UTC coverage: 69.719% (-0.8%) from 70.51%
27048947509

Pull #2669

github

web-flow
Merge ec614bc63 into 70f8e2f4b
Pull Request #2669: chore(arrow): Improve split between GPUTables and Arrow tables

8935 of 14516 branches covered (61.55%)

Branch coverage included in aggregate %.

384 of 861 new or added lines in 31 files covered. (44.6%)

1 existing line in 1 file now uncovered.

18524 of 24869 relevant lines covered (74.49%)

4270.43 hits per line

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

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

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

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

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

63
const LOG_DRAW_PRIORITY = 2;
116✔
64
const LOG_DRAW_TIMEOUT = 10000;
116✔
65
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
116✔
66
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
116✔
67
  'stencil8',
68
  'depth16unorm',
69
  'depth24plus',
70
  'depth24plus-stencil8',
71
  'depth32float',
72
  'depth32float-stencil8'
73
];
74
type ModelBinding = Binding | DynamicTexture | DynamicBuffer | DynamicBufferRange;
75
type ModelBuffer = Buffer | DynamicBuffer;
76

77
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
78
  source?: string;
79
  vs?: string | null;
80
  fs?: string | null;
81

82
  /** Shadertools shader modules added to shader code. */
83
  modules?: ShaderModule[];
84
  /** Shadertools boolean or numeric preprocessor defines that configure shader code. */
85
  defines?: Record<string, boolean | number>;
86
  // TODO - injections, hooks etc?
87

88
  /** Shader inputs, used to generate uniform buffers and bindings. */
89
  shaderInputs?: ShaderInputs;
90
  /** Material-owned group-3 bindings */
91
  material?: Material;
92
  /** Shader resource bindings, including dynamic buffers and dynamic textures. */
93
  bindings?: Record<string, ModelBinding>;
94
  /** WebGL-only uniforms */
95
  uniforms?: Record<string, unknown>;
96
  /** Parameters that are built into the pipeline */
97
  parameters?: RenderPipelineParameters;
98

99
  /** Geometry */
100
  geometry?: GPUGeometry | Geometry | null;
101

102
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
103
  isInstanced?: boolean;
104
  /** instance count */
105
  instanceCount?: number;
106
  /** Vertex count */
107
  vertexCount?: number;
108

109
  /** Optional index buffer. Dynamic buffers are rebound when resized. */
110
  indexBuffer?: ModelBuffer | null;
111
  /** Optional indexed draw count. Defaults to the full bound index buffer length. */
112
  indexCount?: number;
113
  /** First vertex byte offset for WebGL indexed draws or first vertex for non-indexed draws. */
114
  firstVertex?: number;
115
  /** First index element for WebGPU indexed draws. */
116
  firstIndex?: number;
117
  /** Buffer-valued attributes. Dynamic buffers are rebound when resized. */
118
  attributes?: Record<string, ModelBuffer>;
119
  /**   */
120
  constantAttributes?: Record<string, TypedArray>;
121

122
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
123
  disableWarnings?: boolean;
124

125
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
126
  varyings?: string[];
127

128
  transformFeedback?: TransformFeedback;
129

130
  /** Show shader source in browser? */
131
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
132

133
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
134
  pipelineFactory?: PipelineFactory;
135
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
136
  shaderFactory?: ShaderFactory;
137
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
138
  shaderAssembler?: ShaderAssembler;
139
};
140

141
/**
142
 * High level draw API for luma.gl.
143
 *
144
 * A `Model` encapsulates shaders, geometry attributes, bindings and render
145
 * pipeline state into a single object. It automatically reuses and rebuilds
146
 * pipelines as render parameters change and exposes convenient hooks for
147
 * updating uniforms and attributes.
148
 *
149
 * Features:
150
 * - Reuses and lazily recompiles {@link RenderPipeline | pipelines} as needed.
151
 * - Integrates with `@luma.gl/shadertools` to assemble GLSL or WGSL from shader modules.
152
 * - Manages geometry attributes and buffer bindings.
153
 * - Accepts textures, samplers and uniform buffers as bindings, including `DynamicTexture`.
154
 * - Provides detailed debug logging and optional shader source inspection.
155
 */
156
export class Model {
157
  static defaultProps: Required<ModelProps> = {
116✔
158
    ...RenderPipeline.defaultProps,
159
    source: undefined!,
160
    vs: null,
161
    fs: null,
162
    id: 'unnamed',
163
    handle: undefined,
164
    userData: {},
165
    defines: {},
166
    modules: [],
167
    geometry: null,
168
    indexBuffer: null,
169
    indexCount: undefined!,
170
    firstVertex: 0,
171
    firstIndex: 0,
172
    attributes: {},
173
    constantAttributes: {},
174
    bindings: {},
175
    uniforms: {},
176
    varyings: [],
177

178
    isInstanced: undefined!,
179
    instanceCount: 0,
180
    vertexCount: 0,
181

182
    shaderInputs: undefined!,
183
    material: undefined!,
184
    pipelineFactory: undefined!,
185
    shaderFactory: undefined!,
186
    transformFeedback: undefined!,
187
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
188

189
    debugShaders: undefined!,
190
    disableWarnings: undefined!
191
  };
192

193
  /** Device that created this model */
194
  readonly device: Device;
195
  /** Application provided identifier */
196
  readonly id: string;
197
  /** WGSL shader source when using unified shader */
198
  // @ts-expect-error assigned in function called from constructor
199
  readonly source: string;
200
  /** GLSL vertex shader source */
201
  // @ts-expect-error assigned in function called from constructor
202
  readonly vs: string;
203
  /** GLSL fragment shader source */
204
  // @ts-expect-error assigned in function called from constructor
205
  readonly fs: string;
206
  /** Factory used to create render pipelines */
207
  readonly pipelineFactory: PipelineFactory;
208
  /** Factory used to create shaders */
209
  readonly shaderFactory: ShaderFactory;
210
  /** User-supplied per-model data */
211
  userData: {[key: string]: any} = {};
217✔
212

213
  // Fixed properties (change can trigger pipeline rebuild)
214

215
  /** The render pipeline GPU parameters, depth testing etc */
216
  parameters: RenderPipelineParameters;
217

218
  /** The primitive topology */
219
  topology: PrimitiveTopology;
220
  /** Buffer layout */
221
  bufferLayout: BufferLayout[];
222

223
  // Dynamic properties
224

225
  /** Use instanced rendering */
226
  isInstanced: boolean | undefined = undefined;
217✔
227
  /** instance count. `undefined` means not instanced */
228
  instanceCount: number = 0;
217✔
229
  /** Vertex count */
230
  vertexCount: number;
231
  /** Indexed draw count override. Undefined draws the full bound index buffer. */
232
  indexCount: number | undefined;
233
  /** First vertex byte offset for WebGL indexed draws or first vertex for non-indexed draws. */
234
  firstVertex: number;
235
  /** First index element for WebGPU indexed draws. */
236
  firstIndex: number;
237

238
  /** Index buffer */
239
  indexBuffer: Buffer | null = null;
217✔
240
  /** Buffer-valued attributes */
241
  bufferAttributes: Record<string, Buffer> = {};
217✔
242
  /** Constant-valued attributes */
243
  constantAttributes: Record<string, TypedArray> = {};
217✔
244
  /** Bindings (textures, samplers, uniform buffers) */
245
  bindings: Record<string, ModelBinding> = {};
217✔
246

247
  /**
248
   * VertexArray
249
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
250
   * @todo - allow application to define multiple vertex arrays?
251
   * */
252
  vertexArray: VertexArray;
253

254
  /** TransformFeedback, WebGL 2 only. */
255
  transformFeedback: TransformFeedback | null = null;
217✔
256

257
  /** The underlying GPU "program". @note May be recreated if parameters change */
258
  pipeline: RenderPipeline;
259

260
  /** ShaderInputs instance */
261
  // @ts-expect-error Assigned in function called by constructor
262
  shaderInputs: ShaderInputs;
263
  material: Material | null = null;
217✔
264
  // @ts-expect-error Assigned in function called by constructor
265
  _uniformStore: UniformStore;
266

267
  _attributeInfos: Record<string, AttributeInfo> = {};
217✔
268
  _gpuGeometry: GPUGeometry | null = null;
217✔
269
  private props: Required<ModelProps>;
270
  private _dynamicIndexBufferSource: {source: DynamicBuffer; generation: number} | null = null;
217✔
271
  private _dynamicAttributeBufferSources: Record<
272
    number,
273
    {source: DynamicBuffer; generation: number}
274
  > = {};
217✔
275
  private _colorAttachmentFormats: (TextureFormatColor | null)[] | undefined;
276
  private _depthStencilAttachmentFormat: TextureFormatDepthStencil | undefined;
277

278
  _pipelineNeedsUpdate: string | false = 'newly created';
217✔
279
  private _needsRedraw: string | false = 'initializing';
217✔
280
  private _drawBlockedReason: string | false = false;
217✔
281
  private _destroyed = false;
217✔
282

283
  /** "Time" of last draw. Monotonically increasing timestamp */
284
  _lastDrawTimestamp: number = -1;
217✔
285
  private _bindingTable: ShaderBindingDebugRow[] = [];
217✔
286

287
  get [Symbol.toStringTag](): string {
288
    return 'Model';
×
289
  }
290

291
  toString(): string {
292
    return `Model(${this.id})`;
278✔
293
  }
294

295
  constructor(device: Device, props: ModelProps) {
296
    this.props = {...Model.defaultProps, ...props};
217✔
297
    props = this.props;
217✔
298
    this.id = props.id || uid('model');
217!
299
    this.device = device;
217✔
300

301
    Object.assign(this.userData, props.userData);
217✔
302

303
    this.material = props.material || null;
217✔
304

305
    // Setup shader module inputs
306
    const moduleMap = Object.fromEntries(
217✔
307
      this.props.modules?.map(module => [module.name, module]) || []
341!
308
    );
309

310
    const shaderInputs =
311
      props.shaderInputs ||
217✔
312
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
313
    // @ts-ignore
314
    this.setShaderInputs(shaderInputs);
217✔
315

316
    // Setup shader assembler
317
    const platformInfo = getPlatformInfo(device);
217✔
318

319
    // Extract modules from shader inputs if not supplied
320
    const modules =
217!
321
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
322
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
217✔
323

324
    this.props.shaderLayout =
217✔
325
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
390✔
326

327
    const isWebGPU = this.device.type === 'webgpu';
217✔
328

329
    // WebGPU
330
    // TODO - hack to support unified WGSL shader
331
    // TODO - this is wrong, compile a single shader
332
    if (isWebGPU && this.props.source) {
217✔
333
      // WGSL
334
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
32✔
335
        platformInfo,
336
        ...this.props,
337
        modules
338
      });
339
      this.source = source;
32✔
340
      // @ts-expect-error
341
      this._getModuleUniforms = getUniforms;
32✔
342
      this._bindingTable = bindingTable;
32✔
343
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
344
      const inferredShaderLayout = (
345
        device as Device & {getShaderLayout?: (source: string) => any}
32✔
346
      ).getShaderLayout?.(this.source);
347
      const shaderLayout = mergeInferredShaderLayout(this.props.shaderLayout, inferredShaderLayout);
32✔
348
      this.props.shaderLayout =
32✔
349
        mergeShaderModuleBindingsIntoLayout(shaderLayout || null, modules) || null;
64!
350
    } else {
351
      // GLSL
352
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
185✔
353
        platformInfo,
354
        ...this.props,
355
        modules
356
      });
357

358
      this.vs = vs;
185✔
359
      this.fs = fs;
185✔
360
      // @ts-expect-error
361
      this._getModuleUniforms = getUniforms;
185✔
362
      this._bindingTable = [];
185✔
363
    }
364

365
    this.vertexCount = this.props.vertexCount;
217✔
366
    this.indexCount = this.props.indexCount;
217✔
367
    this.firstVertex = this.props.firstVertex;
217✔
368
    this.firstIndex = this.props.firstIndex;
217✔
369
    this.instanceCount = this.props.instanceCount;
217✔
370

371
    this.topology = this.props.topology;
217✔
372
    this.bufferLayout = this.props.bufferLayout;
217✔
373
    this.parameters = this.props.parameters;
217✔
374

375
    // Geometry, if provided, sets topology and vertex cound
376
    if (props.geometry) {
217✔
377
      this.setGeometry(props.geometry);
61✔
378
    }
379

380
    this.pipelineFactory =
217✔
381
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
434✔
382
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
217✔
383

384
    // Create the pipeline
385
    // @note order is important
386
    this.pipeline = this._updatePipeline();
217✔
387

388
    this.vertexArray = device.createVertexArray({
217✔
389
      shaderLayout: this.pipeline.shaderLayout,
390
      bufferLayout: this.pipeline.bufferLayout
391
    });
392

393
    // Now we can apply geometry attributes
394
    if (this._gpuGeometry) {
217✔
395
      this._setGeometryAttributes(this._gpuGeometry);
61✔
396
    }
397

398
    // Apply any dynamic settings that will not trigger pipeline change
399
    if ('isInstanced' in props) {
217!
400
      this.isInstanced = props.isInstanced;
217✔
401
    }
402

403
    if (props.instanceCount) {
217✔
404
      this.setInstanceCount(props.instanceCount);
117✔
405
    }
406
    if (props.vertexCount) {
217✔
407
      this.setVertexCount(props.vertexCount);
177✔
408
    }
409
    if (props.indexBuffer) {
217!
410
      this.setIndexBuffer(props.indexBuffer);
×
411
    }
412
    if (props.attributes) {
217✔
413
      this.setAttributes(props.attributes);
216✔
414
    }
415
    if (props.constantAttributes) {
217!
416
      this.setConstantAttributes(props.constantAttributes);
217✔
417
    }
418
    if (props.bindings) {
217!
419
      this.setBindings(props.bindings);
217✔
420
    }
421
    if (props.transformFeedback) {
217!
422
      this.transformFeedback = props.transformFeedback;
×
423
    }
424
  }
425

426
  destroy(): void {
427
    if (!this._destroyed) {
137✔
428
      // Release pipeline before we destroy the shaders used by the pipeline
429
      this.pipelineFactory.release(this.pipeline);
136✔
430
      // Release the shaders
431
      this.shaderFactory.release(this.pipeline.vs);
136✔
432
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
136✔
433
        this.shaderFactory.release(this.pipeline.fs);
104✔
434
      }
435
      this._uniformStore.destroy();
136✔
436
      // TODO - mark resource as managed and destroyIfManaged() ?
437
      this._gpuGeometry?.destroy();
136✔
438
      this._destroyed = true;
136✔
439
    }
440
  }
441

442
  // Draw call
443

444
  /** Query redraw status. Clears the status. */
445
  needsRedraw(): false | string {
446
    // Catch any writes to already bound resources
447
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
3!
448
      this.setNeedsRedraw('contents of bound textures or buffers updated');
3✔
449
    }
450
    const needsRedraw = this._needsRedraw;
3✔
451
    this._needsRedraw = false;
3✔
452
    return needsRedraw;
3✔
453
  }
454

455
  /** Mark the model as needing a redraw */
456
  setNeedsRedraw(reason: string): void {
457
    this._needsRedraw ||= reason;
2,026✔
458
  }
459

460
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
461
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
462
    return this._bindingTable;
2✔
463
  }
464

465
  /**
466
   * Updates uniforms and pipeline state before opening a render pass.
467
   *
468
   * @param commandEncoder - Encoder that should own any GPU uploads emitted
469
   * during draw preparation.
470
   */
471
  predraw(commandEncoder: CommandEncoder): void {
472
    // Update uniform buffers if needed
473
    this._syncDynamicBuffers();
157✔
474
    this.updateShaderInputs(commandEncoder);
157✔
475
    this.material?.updateShaderInputs(commandEncoder);
157✔
476
    // Check if the pipeline is invalidated
477
    this.pipeline = this._updatePipeline();
157✔
478
  }
479

480
  /**
481
   * Issue one draw call.
482
   * @param renderPass - render pass to draw into
483
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
484
   */
485
  draw(renderPass: RenderPass): boolean {
486
    if (this._drawBlockedReason && !this._pipelineNeedsUpdate) {
140✔
487
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${this._drawBlockedReason}`)();
1✔
488
      return false;
1✔
489
    }
490

491
    const loadingBinding = this._areBindingsLoading();
139✔
492
    if (loadingBinding) {
139!
493
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
494
      return false;
×
495
    }
496

497
    this._syncAttachmentFormats(renderPass);
139✔
498

499
    try {
139✔
500
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
139✔
501
      if (this.device.type === 'webgpu') {
139✔
502
        // WebGPU uploads cannot be encoded once the render pass is already open.
503
        // Keep the implicit draw() path working for existing callers by falling
504
        // back to immediate writes here; callers that need upload ordering
505
        // across multiple draws/viewports must call predraw(commandEncoder)
506
        // before beginRenderPass().
507
        this.updateShaderInputs();
9✔
508
        this.material?.updateShaderInputs();
9✔
509
        this._syncDynamicBuffers();
9✔
510
        this.pipeline = this._updatePipeline();
9✔
511
      } else {
512
        this.predraw(this.device.commandEncoder);
130✔
513
      }
514
    } finally {
515
      renderPass.popDebugGroup();
139✔
516
    }
517

518
    let drawSuccess: boolean;
519
    let pipelineErrored = this.pipeline.isErrored;
139✔
520
    try {
139✔
521
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
139✔
522
      this._logDrawCallStart();
139✔
523

524
      // Update the pipeline if invalidated
525
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
526
      // Application can call Model.predraw() to avoid this.
527
      this.pipeline = this._updatePipeline();
139✔
528
      pipelineErrored = this.pipeline.isErrored;
139✔
529

530
      if (pipelineErrored) {
139✔
531
        log.info(
1✔
532
          LOG_DRAW_PRIORITY,
533
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
534
        )();
535
        drawSuccess = false;
1✔
536
      } else {
537
        const drawValidationError = this.vertexArray.getDrawValidationError();
138✔
538
        if (drawValidationError) {
138!
539
          log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${drawValidationError}`)();
×
540
          this._drawBlockedReason = drawValidationError;
×
541
          drawSuccess = false;
×
542
        } else {
543
          const syncBindings = this._getBindings();
138✔
544
          const syncBindGroups = this._getBindGroups();
138✔
545

546
          const {indexBuffer} = this.vertexArray;
138✔
547
          const indexCount = indexBuffer
138✔
548
            ? (this.indexCount ??
24✔
549
              indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2))
12!
550
            : undefined;
551

552
          drawSuccess = this.pipeline.draw({
138✔
553
            renderPass,
554
            vertexArray: this.vertexArray,
555
            isInstanced: this.isInstanced,
556
            vertexCount: this.vertexCount,
557
            instanceCount: this.instanceCount,
558
            indexCount,
559
            firstVertex: this.firstVertex,
560
            firstIndex: this.firstIndex,
561
            transformFeedback: this.transformFeedback || undefined,
187✔
562
            // Pipelines may be shared across models when caching is enabled, so bindings
563
            // and WebGL uniforms must be supplied on every draw instead of being stored
564
            // on the pipeline instance.
565
            bindings: syncBindings,
566
            bindGroups: syncBindGroups,
567
            _bindGroupCacheKeys: this._getBindGroupCacheKeys(),
568
            uniforms: this.props.uniforms,
569
            // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
570
            // so we must provide our unique parameters to each draw
571
            // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
572
            parameters: this.parameters,
573
            topology: this.topology
574
          });
575
        }
576
      }
577
    } finally {
578
      renderPass.popDebugGroup();
139✔
579
      this._logDrawCallEnd();
139✔
580
    }
581
    this._logFramebuffer(renderPass);
139✔
582

583
    // Update needsRedraw flag
584
    if (drawSuccess) {
139✔
585
      this._lastDrawTimestamp = this.device.timestamp;
138✔
586
      this._needsRedraw = false;
138✔
587
    } else if (pipelineErrored) {
1!
588
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
1✔
589
      this._drawBlockedReason = PIPELINE_INITIALIZATION_FAILED;
1✔
590
    } else if (this._drawBlockedReason) {
×
591
      this._needsRedraw = this._drawBlockedReason;
×
592
    } else {
593
      this._needsRedraw = 'waiting for resource initialization';
×
594
    }
595
    return drawSuccess;
139✔
596
  }
597

598
  // Update fixed fields (can trigger pipeline rebuild)
599

600
  /**
601
   * Updates the optional geometry
602
   * Geometry, set topology and bufferLayout
603
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
604
   */
605
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
606
    this._gpuGeometry?.destroy();
61✔
607
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
61✔
608
    if (gpuGeometry) {
61!
609
      this.setTopology(gpuGeometry.topology || 'triangle-list');
61!
610
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
61✔
611
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
61✔
612
        gpuGeometry.bufferLayout,
613
        this.bufferLayout
614
      );
615
      if (this.vertexArray) {
61!
616
        this._setGeometryAttributes(gpuGeometry);
×
617
      }
618
    }
619
    this._gpuGeometry = gpuGeometry;
61✔
620
  }
621

622
  /**
623
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
624
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
625
   */
626
  setTopology(topology: PrimitiveTopology): void {
627
    if (topology !== this.topology) {
64✔
628
      this.topology = topology;
34✔
629
      this._setPipelineNeedsUpdate('topology');
34✔
630
    }
631
  }
632

633
  /**
634
   * Updates the buffer layout.
635
   * @note Triggers a pipeline rebuild / pipeline cache fetch
636
   */
637
  setBufferLayout(bufferLayout: BufferLayout[]): void {
638
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
2✔
639
    const nextBufferLayout = this._gpuGeometry
2!
640
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
641
      : bufferLayout;
642
    if (deepEqual(nextBufferLayout, this.bufferLayout, -1)) {
2!
643
      return;
2✔
644
    }
645

646
    this.bufferLayout = nextBufferLayout;
×
647
    this._setPipelineNeedsUpdate('bufferLayout');
×
648

649
    // Recreate the pipeline
650
    this.pipeline = this._updatePipeline();
×
651

652
    // vertex array needs to be updated if we update buffer layout,
653
    // but not if we update parameters
654
    this.vertexArray = this.device.createVertexArray({
×
655
      shaderLayout: this.pipeline.shaderLayout,
656
      bufferLayout: this.pipeline.bufferLayout
657
    });
658

659
    // Reapply geometry attributes to the new vertex array
660
    if (this._gpuGeometry) {
×
661
      this._setGeometryAttributes(this._gpuGeometry);
×
662
    }
663
  }
664

665
  /**
666
   * Set GPU parameters.
667
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
668
   * @param parameters
669
   */
670
  setParameters(parameters: RenderPipelineParameters) {
671
    if (!deepEqual(parameters, this.parameters, 2)) {
×
672
      this.parameters = parameters;
×
673
      this._setPipelineNeedsUpdate('parameters');
×
674
    }
675
  }
676

677
  // Update dynamic fields
678

679
  /**
680
   * Updates the instance count (used in draw calls)
681
   * @note Any attributes with stepMode=instance need to be at least this big
682
   */
683
  setInstanceCount(instanceCount: number): void {
684
    this.instanceCount = instanceCount;
140✔
685
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
686
    // but deck.gl sets instanceCount even for models that are not instanced.
687
    if (this.isInstanced === undefined && instanceCount > 0) {
140✔
688
      this.isInstanced = true;
124✔
689
    }
690
    this.setNeedsRedraw('instanceCount');
140✔
691
  }
692

693
  /**
694
   * Updates the vertex count (used in draw calls)
695
   * @note Any attributes with stepMode=vertex need to be at least this big
696
   */
697
  setVertexCount(vertexCount: number): void {
698
    this.vertexCount = vertexCount;
178✔
699
    this.setNeedsRedraw('vertexCount');
178✔
700
  }
701

702
  /** Updates the indexed draw count override. */
703
  setIndexCount(indexCount: number | undefined): void {
NEW
704
    this.indexCount = indexCount;
×
NEW
705
    this.setNeedsRedraw('indexCount');
×
706
  }
707

708
  /** Updates the first indexed/non-indexed draw offsets. */
709
  setDrawOffsets({firstVertex, firstIndex}: {firstVertex: number; firstIndex: number}): void {
NEW
710
    this.firstVertex = firstVertex;
×
NEW
711
    this.firstIndex = firstIndex;
×
NEW
712
    this.setNeedsRedraw('drawOffsets');
×
713
  }
714

715
  /** Set the shader inputs */
716
  setShaderInputs(shaderInputs: ShaderInputs): void {
717
    this.shaderInputs = shaderInputs;
217✔
718
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
217✔
719
    // Create uniform buffer bindings for all modules that actually have uniforms
720
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
217✔
721
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
437✔
722
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
109✔
723
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
109✔
724
      }
725
    }
726
    this.setNeedsRedraw('shaderInputs');
217✔
727
  }
728

729
  setMaterial(material: Material | null): void {
730
    this.material = material;
×
731
    this.setNeedsRedraw('material');
×
732
  }
733

734
  /** Update uniform buffers from the model's shader inputs */
735
  /**
736
   * Flushes current shader-input values into managed uniform buffers and
737
   * non-material bindings.
738
   *
739
   * @param commandEncoder - Optional encoder used to order uniform uploads with
740
   * subsequent draw commands.
741
   */
742
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
743
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
166✔
744
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
166✔
745
    // TODO - this is already tracked through buffer/texture update times?
746
    this.setNeedsRedraw('shaderInputs');
166✔
747
  }
748

749
  /**
750
   * Sets bindings (textures, samplers, uniform buffers)
751
   */
752
  setBindings(bindings: Record<string, ModelBinding>): void {
753
    Object.assign(this.bindings, bindings);
435✔
754
    this.setNeedsRedraw('bindings');
435✔
755
  }
756

757
  /**
758
   * Updates optional transform feedback. WebGL only.
759
   */
760
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
761
    this.transformFeedback = transformFeedback;
88✔
762
    this.setNeedsRedraw('transformFeedback');
88✔
763
  }
764

765
  /**
766
   * Sets the index buffer
767
   * @todo - how to unset it if we change geometry?
768
   */
769
  setIndexBuffer(indexBuffer: ModelBuffer | null): void {
770
    const resolvedIndexBuffer =
771
      indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
61!
772
    this.indexBuffer = resolvedIndexBuffer;
61✔
773
    this._dynamicIndexBufferSource =
61✔
774
      indexBuffer instanceof DynamicBuffer
61!
775
        ? {source: indexBuffer, generation: indexBuffer.generation}
776
        : null;
777
    this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
61✔
778
    this.setNeedsRedraw('indexBuffer');
61✔
779
  }
780

781
  /**
782
   * Sets attributes (buffers)
783
   * @note Overrides any attributes previously set with the same name
784
   */
785
  setAttributes(buffers: Record<string, ModelBuffer>, options?: {disableWarnings?: boolean}): void {
786
    this._drawBlockedReason = false;
418✔
787
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
418✔
788
    if (buffers['indices']) {
418!
789
      log.warn(
×
790
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
791
      )();
792
    }
793

794
    // ensure bufferLayout order matches source layout so we bind
795
    // the correct buffers to the correct indices in webgpu.
796
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
418✔
797
      this.pipeline.shaderLayout,
798
      this.bufferLayout
799
    );
800
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
418✔
801

802
    // Check if all buffers have a layout
803
    for (const [bufferName, buffer] of Object.entries(buffers)) {
418✔
804
      const resolvedBuffer = buffer instanceof DynamicBuffer ? buffer.buffer : buffer;
522✔
805
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
522✔
806
      if (!bufferLayout) {
522!
807
        if (!disableWarnings) {
×
808
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
809
        }
810
        continue; // eslint-disable-line no-continue
×
811
      }
812

813
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
814
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
815
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
522✔
816
      let set = false;
522✔
817
      for (const attributeName of attributeNames) {
522✔
818
        const attributeInfo = this._attributeInfos[attributeName];
704✔
819
        if (attributeInfo) {
704✔
820
          const bufferSlot =
821
            this.device.type === 'webgpu'
660✔
822
              ? this.vertexArray.getBufferSlot(attributeInfo.bufferName)
823
              : attributeInfo.location;
824
          if (bufferSlot === null) {
660!
825
            if (!disableWarnings) {
×
826
              log.warn(
×
827
                `Model(${this.id}): Missing vertex array slot for buffer "${attributeInfo.bufferName}".`
828
              )();
829
            }
830
            continue; // eslint-disable-line no-continue
×
831
          }
832

833
          this.vertexArray.setBuffer(bufferSlot, resolvedBuffer);
660✔
834
          if (buffer instanceof DynamicBuffer) {
660✔
835
            this._dynamicAttributeBufferSources[bufferSlot] = {
31✔
836
              source: buffer,
837
              generation: buffer.generation
838
            };
839
          } else {
840
            delete this._dynamicAttributeBufferSources[bufferSlot];
629✔
841
          }
842
          set = true;
660✔
843
        }
844
      }
845
      if (!set && !disableWarnings) {
522✔
846
        log.warn(
2✔
847
          `Model(${this.id}): Ignoring buffer "${resolvedBuffer.id}" for unknown attribute "${bufferName}"`
848
        )();
849
      }
850
    }
851
    this.setNeedsRedraw('attributes');
418✔
852
  }
853

854
  /**
855
   * Sets constant attributes
856
   * @note Overrides any attributes previously set with the same name
857
   * Constant attributes are only supported in WebGL, not in WebGPU
858
   * Any attribute that is disabled in the current vertex array object
859
   * is read from the context's global constant value for that attribute location.
860
   * @param constantAttributes
861
   */
862
  setConstantAttributes(
863
    attributes: Record<string, TypedArray>,
864
    options?: {disableWarnings?: boolean}
865
  ): void {
866
    for (const [attributeName, value] of Object.entries(attributes)) {
217✔
867
      const attributeInfo = this._attributeInfos[attributeName];
×
868
      if (attributeInfo) {
×
869
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
870
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
871
        log.warn(
×
872
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
873
        )();
874
      }
875
    }
876
    this.setNeedsRedraw('constants');
217✔
877
  }
878

879
  // INTERNAL METHODS
880

881
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
882
  _areBindingsLoading(): string | false {
883
    for (const binding of Object.values(this.bindings)) {
142✔
884
      if (binding instanceof DynamicTexture && !binding.isReady) {
143!
885
        return binding.id;
×
886
      }
887
    }
888
    for (const binding of Object.values(this.material?.bindings || {})) {
142✔
889
      if (binding instanceof DynamicTexture && !binding.isReady) {
×
890
        return binding.id;
×
891
      }
892
    }
893
    return false;
142✔
894
  }
895

896
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
897
  _getBindings(): Record<string, Binding> {
898
    const validBindings: Record<string, Binding> = {};
510✔
899

900
    for (const [name, binding] of Object.entries(this.bindings)) {
510✔
901
      const resolvedBinding = resolveModelBinding(binding);
416✔
902
      if (resolvedBinding) {
416!
903
        validBindings[name] = resolvedBinding;
416✔
904
      }
905
    }
906

907
    return validBindings;
510✔
908
  }
909

910
  _getBindGroups(): BindingsByGroup {
911
    const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
365✔
912
    const bindGroups = shaderLayout.bindings.length
365✔
913
      ? normalizeBindingsByGroup(shaderLayout, this._getBindings())
914
      : {0: this._getBindings()};
915

916
    if (!this.material) {
365✔
917
      return bindGroups;
355✔
918
    }
919

920
    for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) {
10✔
921
      const group = Number(groupKey);
10✔
922
      bindGroups[group] = {
10✔
923
        ...(bindGroups[group] || {}),
20✔
924
        ...groupBindings
925
      };
926
    }
927

928
    return bindGroups;
10✔
929
  }
930

931
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
932
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
141✔
933
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
141!
934
  }
935

936
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
937
  _getBindingsUpdateTimestamp(): number {
938
    let timestamp = 0;
3✔
939
    if (this._dynamicIndexBufferSource) {
3!
940
      timestamp = Math.max(timestamp, this._dynamicIndexBufferSource.source.updateTimestamp);
×
941
    }
942
    for (const entry of Object.values(this._dynamicAttributeBufferSources)) {
3✔
943
      timestamp = Math.max(timestamp, entry.source.updateTimestamp);
4✔
944
    }
945
    for (const binding of Object.values(this.bindings)) {
3✔
946
      if (binding instanceof TextureView) {
×
947
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
948
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
949
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
950
      } else if (binding instanceof DynamicBuffer) {
×
951
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
952
      } else if (binding instanceof DynamicTexture) {
×
953
        timestamp = binding.isReady
×
954
          ? Math.max(timestamp, binding.updateTimestamp)
955
          : // The texture will become available in the future
956
            Infinity;
957
      } else if (isBufferRangeBinding(binding)) {
×
958
        timestamp = Math.max(
×
959
          timestamp,
960
          binding.buffer instanceof DynamicBuffer
×
961
            ? binding.buffer.updateTimestamp
962
            : binding.buffer.updateTimestamp
963
        );
964
      }
965
    }
966
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
3✔
967
  }
968

969
  /**
970
   * Updates the optional geometry attributes
971
   * Geometry, sets several attributes, indexBuffer, and also vertex count
972
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
973
   */
974
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
975
    // Filter geometry attribute so that we don't issue warnings for unused attributes
976
    const attributes = {...gpuGeometry.attributes};
61✔
977
    for (const [attributeName] of Object.entries(attributes)) {
61✔
978
      if (
61!
979
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
205✔
980
        attributeName !== 'positions'
981
      ) {
982
        delete attributes[attributeName];
61✔
983
      }
984
    }
985

986
    // TODO - delete previous geometry?
987
    this.vertexCount = gpuGeometry.vertexCount;
61✔
988
    this.setIndexBuffer(gpuGeometry.indices || null);
61✔
989
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
61✔
990
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
61✔
991

992
    this.setNeedsRedraw('geometry attributes');
61✔
993
  }
994

995
  /** Mark pipeline as needing update */
996
  _setPipelineNeedsUpdate(reason: string): void {
997
    this._pipelineNeedsUpdate ||= reason;
40✔
998
    this._drawBlockedReason = false;
40✔
999
    this.setNeedsRedraw(reason);
40✔
1000
  }
1001

1002
  /** Update pipeline if needed */
1003
  _updatePipeline(): RenderPipeline {
1004
    if (this._pipelineNeedsUpdate) {
525✔
1005
      let prevShaderVs: Shader | null = null;
224✔
1006
      let prevShaderFs: Shader | null = null;
224✔
1007
      if (this.pipeline) {
224✔
1008
        log.log(
7✔
1009
          1,
1010
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
1011
        )();
1012
        prevShaderVs = this.pipeline.vs;
7✔
1013
        prevShaderFs = this.pipeline.fs;
7✔
1014
      }
1015

1016
      this._pipelineNeedsUpdate = false;
224✔
1017

1018
      const vs = this.shaderFactory.createShader({
224✔
1019
        id: `${this.id}-vertex`,
1020
        stage: 'vertex',
1021
        source: this.source || this.vs,
410✔
1022
        debugShaders: this.props.debugShaders
1023
      });
1024

1025
      let fs: Shader | null = null;
224✔
1026
      if (this.source) {
224✔
1027
        fs = vs;
38✔
1028
      } else if (this.fs) {
186!
1029
        fs = this.shaderFactory.createShader({
186✔
1030
          id: `${this.id}-fragment`,
1031
          stage: 'fragment',
1032
          source: this.source || this.fs,
372✔
1033
          debugShaders: this.props.debugShaders
1034
        });
1035
      }
1036

1037
      this.pipeline = this.pipelineFactory.createRenderPipeline({
224✔
1038
        ...this.props,
1039
        bindings: undefined,
1040
        bufferLayout: this.bufferLayout,
1041
        colorAttachmentFormats: this._colorAttachmentFormats,
1042
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
1043
        topology: this.topology,
1044
        parameters: this.parameters,
1045
        bindGroups: this._getBindGroups(),
1046
        vs,
1047
        fs
1048
      });
1049

1050
      this._attributeInfos = getAttributeInfosFromLayouts(
224✔
1051
        this.pipeline.shaderLayout,
1052
        this.bufferLayout
1053
      );
1054

1055
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
224✔
1056
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
224✔
1057
        this.shaderFactory.release(prevShaderFs);
1✔
1058
      }
1059
    }
1060
    return this.pipeline;
525✔
1061
  }
1062

1063
  /** Throttle draw call logging */
1064
  _lastLogTime = 0;
217✔
1065
  _logOpen = false;
217✔
1066

1067
  _logDrawCallStart(): void {
1068
    // IF level is 4 or higher, log every frame.
1069
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
139!
1070
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
139!
1071
      return;
139✔
1072
    }
1073

1074
    this._lastLogTime = Date.now();
×
1075
    this._logOpen = true;
×
1076

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

1080
  _logDrawCallEnd(): void {
1081
    if (this._logOpen) {
139!
1082
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1083

1084
      // log.table(logLevel, attributeTable)();
1085
      // log.table(logLevel, uniformTable)();
1086
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1087

1088
      const uniformTable = this.shaderInputs.getDebugTable();
×
1089
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1090

1091
      const attributeTable = this._getAttributeDebugTable();
×
1092
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1093
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1094

1095
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1096
      this._logOpen = false;
×
1097
    }
1098
  }
1099

1100
  protected _drawCount = 0;
217✔
1101
  _logFramebuffer(renderPass: RenderPass): void {
1102
    const debugFramebuffers = this.device.props.debugFramebuffers;
139✔
1103
    this._drawCount++;
139✔
1104
    // Update first 3 frames and then every 60 frames
1105
    if (!debugFramebuffers) {
139!
1106
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
1107
      return;
139✔
1108
    }
1109
    const framebuffer = renderPass.props.framebuffer;
×
1110
    debugFramebuffer(renderPass, framebuffer, {
×
1111
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
1112
      minimap: true
1113
    });
1114
  }
1115

1116
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1117
    const table: Record<string, Record<string, unknown>> = {};
×
1118
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
1119
      const values = this.vertexArray.attributes[attributeInfo.location];
×
1120
      table[attributeInfo.location] = {
×
1121
        name,
1122
        type: attributeInfo.shaderType,
1123
        values: values
×
1124
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
1125
          : 'null'
1126
      };
1127
    }
1128
    if (this.vertexArray.indexBuffer) {
×
1129
      const {indexBuffer} = this.vertexArray;
×
1130
      const values =
1131
        indexBuffer.indexType === 'uint32'
×
1132
          ? new Uint32Array(indexBuffer.debugData)
1133
          : new Uint16Array(indexBuffer.debugData);
1134
      table['indices'] = {
×
1135
        name: 'indices',
1136
        type: indexBuffer.indexType,
1137
        values: values.toString()
1138
      };
1139
    }
1140
    return table;
×
1141
  }
1142

1143
  // TODO - fix typing of luma data types
1144
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1145
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1146
    const typedArray =
1147
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
1148
    return typedArray.toString();
×
1149
  }
1150

1151
  private _getNonMaterialBindings(
1152
    bindings: Record<string, ModelBinding>
1153
  ): Record<string, ModelBinding> {
1154
    if (!this.material) {
166!
1155
      return bindings;
166✔
1156
    }
1157

1158
    const filteredBindings: Record<string, ModelBinding> = {};
×
1159
    for (const [name, binding] of Object.entries(bindings)) {
×
1160
      if (!this.material.ownsBinding(name)) {
×
1161
        filteredBindings[name] = binding;
×
1162
      }
1163
    }
1164
    return filteredBindings;
×
1165
  }
1166

1167
  private _syncDynamicBuffers(): void {
1168
    if (
166!
1169
      this._dynamicIndexBufferSource &&
166!
1170
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1171
    ) {
1172
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
1173
      this.indexBuffer = resolvedIndexBuffer;
×
1174
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
1175
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
1176
      this.setNeedsRedraw('dynamic index buffer');
×
1177
    }
1178

1179
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
166✔
1180
      if (entry.generation !== entry.source.generation) {
5!
1181
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
1182
        entry.generation = entry.source.generation;
×
1183
        this.setNeedsRedraw('dynamic attribute buffer');
×
1184
      }
1185
    }
1186
  }
1187
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1188
    if (this.device.type !== 'webgpu') {
139✔
1189
      return;
130✔
1190
    }
1191

1192
    const framebuffer =
1193
      (
1194
        renderPass as RenderPass & {
9!
1195
          framebuffer?: {
1196
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1197
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1198
          };
1199
        }
1200
      ).framebuffer || renderPass.props.framebuffer;
1201

1202
    const nextColorAttachmentFormats = framebuffer?.colorAttachments?.map(colorAttachment =>
139✔
1203
      asColorAttachmentFormat(colorAttachment?.texture?.format)
9✔
1204
    );
1205
    const nextDepthStencilAttachmentFormat = asDepthStencilAttachmentFormat(
139✔
1206
      framebuffer?.depthStencilAttachment?.texture?.format
1207
    );
1208

1209
    if (
139✔
1210
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
142✔
1211
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1212
    ) {
1213
      this._colorAttachmentFormats = nextColorAttachmentFormats;
6✔
1214
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
6✔
1215
      this._setPipelineNeedsUpdate('attachment formats');
6✔
1216
    }
1217
  }
1218
}
1219

1220
// HELPERS
1221

1222
function resolveModelBinding(binding: ModelBinding): Binding | null {
1223
  if (binding instanceof DynamicTexture) {
416!
1224
    return binding.isReady ? binding.texture : null;
×
1225
  }
1226

1227
  if (binding instanceof DynamicBuffer) {
416✔
1228
    return binding.buffer;
23✔
1229
  }
1230

1231
  if (isBufferRangeBinding(binding)) {
393✔
1232
    return resolveBufferRangeBinding(binding);
7✔
1233
  }
1234

1235
  return binding;
386✔
1236
}
1237
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1238
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
9!
1239
}
1240

1241
function asDepthStencilAttachmentFormat(
1242
  format?: string | null
1243
): TextureFormatDepthStencil | undefined {
1244
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
9!
1245
}
1246

1247
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1248
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
9✔
1249
}
1250

1251
/** Create a shadertools platform info from the Device */
1252
export function getPlatformInfo(device: Device): PlatformInfo {
1253
  return {
217✔
1254
    type: device.type,
1255
    shaderLanguage: device.info.shadingLanguage,
1256
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1257
    gpu: device.info.gpu,
1258
    limits: device.limits as unknown as Record<string, number | undefined>,
1259
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1260
    features: device.features as unknown as Set<DeviceFeature>
1261
  };
1262
}
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