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

visgl / luma.gl / 27877205100

20 Jun 2026 04:32PM UTC coverage: 70.733% (+0.08%) from 70.652%
27877205100

push

github

web-flow
feat(engine) add portable video textures (#2677)

9660 of 15389 branches covered (62.77%)

Branch coverage included in aggregate %.

99 of 116 new or added lines in 2 files covered. (85.34%)

379 existing lines in 28 files now uncovered.

19717 of 26143 relevant lines covered (75.42%)

4107.12 hits per line

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

77.36
/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 ComputeShaderLayout,
21
  type PrimitiveTopology,
22
  type ShaderLayout,
23
  Device,
24
  DeviceFeature,
25
  Buffer,
26
  ExternalTexture,
27
  Texture,
28
  TextureView,
29
  RenderPipeline,
30
  RenderPass,
31
  PipelineFactory,
32
  ShaderFactory,
33
  UniformStore,
34
  log,
35
  dataTypeDecoder,
36
  getAttributeInfosFromLayouts,
37
  normalizeBindingsByGroup
38
} from '@luma.gl/core';
39

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

43
import type {Geometry} from '../geometry/geometry';
44
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
45
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
46
import {debugFramebuffer} from '../debug/debug-framebuffer';
47
import {deepEqual} from '../utils/deep-equal';
48
import {BufferLayoutHelper} from '../utils/buffer-layout-helper';
49
import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';
50
import {
51
  mergeInferredShaderLayout,
52
  mergeShaderModules,
53
  mergeShaderModuleBindingsIntoLayout,
54
  shaderModuleHasUniforms
55
} from '../utils/shader-module-utils';
56
import {uid} from '../utils/uid';
57
import {ShaderInputs} from '../shader-inputs';
58
import {
59
  DynamicBuffer,
60
  type DynamicBufferRange,
61
  isBufferRangeBinding,
62
  resolveBufferRangeBinding
63
} from '../dynamic-buffer/dynamic-buffer';
64
import {
65
  getTextureBindingLayout,
66
  isTextureBindingSource,
67
  type TextureBindingSource
68
} from '../dynamic-texture/texture-binding-source';
69
import {Material} from '../material/material';
70

71
const LOG_DRAW_PRIORITY = 2;
124✔
72
const LOG_DRAW_TIMEOUT = 10000;
124✔
73
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
124✔
74
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
124✔
75
  'stencil8',
76
  'depth16unorm',
77
  'depth24plus',
78
  'depth24plus-stencil8',
79
  'depth32float',
80
  'depth32float-stencil8'
81
];
82
/** Resource accepted by one model binding slot. */
83
type ModelBinding = Binding | TextureBindingSource | DynamicBuffer | DynamicBufferRange;
84
type ModelBuffer = Buffer | DynamicBuffer;
85
/** Shader layout subset needed while resolving texture binding sources. */
86
type AnyShaderLayout = Pick<ShaderLayout | ComputeShaderLayout, 'bindings'>;
87

88
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
89
  source?: string;
90
  vs?: string | null;
91
  fs?: string | null;
92

93
  /** Shadertools shader modules added to shader code. */
94
  modules?: ShaderModule[];
95
  /** Shadertools boolean or numeric preprocessor defines that configure shader code. */
96
  defines?: Record<string, boolean | number>;
97
  // TODO - injections, hooks etc?
98

99
  /** Shader inputs, used to generate uniform buffers and bindings. */
100
  shaderInputs?: ShaderInputs;
101
  /** Material-owned group-3 bindings */
102
  material?: Material;
103
  /** Shader resource bindings, including dynamic buffers and dynamic textures. */
104
  bindings?: Record<string, ModelBinding>;
105
  /** WebGL-only uniforms */
106
  uniforms?: Record<string, unknown>;
107
  /** Parameters that are built into the pipeline */
108
  parameters?: RenderPipelineParameters;
109

110
  /** Geometry */
111
  geometry?: GPUGeometry | Geometry | null;
112

113
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
114
  isInstanced?: boolean;
115
  /** instance count */
116
  instanceCount?: number;
117
  /** Vertex count */
118
  vertexCount?: number;
119

120
  /** Optional index buffer. Dynamic buffers are rebound when resized. */
121
  indexBuffer?: ModelBuffer | null;
122
  /** Optional indexed draw count. Defaults to the full bound index buffer length. */
123
  indexCount?: number;
124
  /** First vertex byte offset for WebGL indexed draws or first vertex for non-indexed draws. */
125
  firstVertex?: number;
126
  /** First index element for WebGPU indexed draws. */
127
  firstIndex?: number;
128
  /** Buffer-valued attributes. Dynamic buffers are rebound when resized. */
129
  attributes?: Record<string, ModelBuffer>;
130
  /**   */
131
  constantAttributes?: Record<string, TypedArray>;
132

133
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
134
  disableWarnings?: boolean;
135

136
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
137
  varyings?: string[];
138

139
  transformFeedback?: TransformFeedback;
140

141
  /** Show shader source in browser? */
142
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
143

144
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
145
  pipelineFactory?: PipelineFactory;
146
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
147
  shaderFactory?: ShaderFactory;
148
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
149
  shaderAssembler?: ShaderAssembler;
150
};
151

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

189
    isInstanced: undefined!,
190
    instanceCount: 0,
191
    vertexCount: 0,
192

193
    shaderInputs: undefined!,
194
    material: undefined!,
195
    pipelineFactory: undefined!,
196
    shaderFactory: undefined!,
197
    transformFeedback: undefined!,
198
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
199

200
    debugShaders: undefined!,
201
    disableWarnings: undefined!
202
  };
203

204
  /** Device that created this model */
205
  readonly device: Device;
206
  /** Application provided identifier */
207
  readonly id: string;
208
  /** WGSL shader source when using unified shader */
209
  // @ts-expect-error assigned in function called from constructor
210
  readonly source: string;
211
  /** GLSL vertex shader source */
212
  // @ts-expect-error assigned in function called from constructor
213
  readonly vs: string;
214
  /** GLSL fragment shader source */
215
  // @ts-expect-error assigned in function called from constructor
216
  readonly fs: string;
217
  /** Factory used to create render pipelines */
218
  readonly pipelineFactory: PipelineFactory;
219
  /** Factory used to create shaders */
220
  readonly shaderFactory: ShaderFactory;
221
  /** User-supplied per-model data */
222
  userData: {[key: string]: any} = {};
245✔
223

224
  // Fixed properties (change can trigger pipeline rebuild)
225

226
  /** The render pipeline GPU parameters, depth testing etc */
227
  parameters: RenderPipelineParameters;
228

229
  /** The primitive topology */
230
  topology: PrimitiveTopology;
231
  /** Buffer layout */
232
  bufferLayout: BufferLayout[];
233

234
  // Dynamic properties
235

236
  /** Use instanced rendering */
237
  isInstanced: boolean | undefined = undefined;
245✔
238
  /** instance count. `undefined` means not instanced */
239
  instanceCount: number = 0;
245✔
240
  /** Vertex count */
241
  vertexCount: number;
242
  /** Indexed draw count override. Undefined draws the full bound index buffer. */
243
  indexCount: number | undefined;
244
  /** First vertex byte offset for WebGL indexed draws or first vertex for non-indexed draws. */
245
  firstVertex: number;
246
  /** First index element for WebGPU indexed draws. */
247
  firstIndex: number;
248

249
  /** Index buffer */
250
  indexBuffer: Buffer | null = null;
245✔
251
  /** Buffer-valued attributes */
252
  bufferAttributes: Record<string, Buffer> = {};
245✔
253
  /** Constant-valued attributes */
254
  constantAttributes: Record<string, TypedArray> = {};
245✔
255
  /** Bindings (textures, samplers, uniform buffers) */
256
  bindings: Record<string, ModelBinding> = {};
245✔
257

258
  /**
259
   * VertexArray
260
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
261
   * @todo - allow application to define multiple vertex arrays?
262
   * */
263
  vertexArray: VertexArray;
264

265
  /** TransformFeedback, WebGL 2 only. */
266
  transformFeedback: TransformFeedback | null = null;
245✔
267

268
  /** The underlying GPU "program". @note May be recreated if parameters change */
269
  pipeline: RenderPipeline;
270

271
  /** ShaderInputs instance */
272
  // @ts-expect-error Assigned in function called by constructor
273
  shaderInputs: ShaderInputs;
274
  material: Material | null = null;
245✔
275
  // @ts-expect-error Assigned in function called by constructor
276
  _uniformStore: UniformStore;
277

278
  _attributeInfos: Record<string, AttributeInfo> = {};
245✔
279
  _gpuGeometry: GPUGeometry | null = null;
245✔
280
  private props: Required<ModelProps>;
281
  private _dynamicIndexBufferSource: {source: DynamicBuffer; generation: number} | null = null;
245✔
282
  private _dynamicAttributeBufferSources: Record<
283
    number,
284
    {source: DynamicBuffer; generation: number}
285
  > = {};
245✔
286
  private _colorAttachmentFormats: (TextureFormatColor | null)[] | undefined;
287
  private _depthStencilAttachmentFormat: TextureFormatDepthStencil | undefined;
288

289
  _pipelineNeedsUpdate: string | false = 'newly created';
245✔
290
  private _needsRedraw: string | false = 'initializing';
245✔
291
  private _drawBlockedReason: string | false = false;
245✔
292
  private _destroyed = false;
245✔
293

294
  /** "Time" of last draw. Monotonically increasing timestamp */
295
  _lastDrawTimestamp: number = -1;
245✔
296
  private _bindingTable: ShaderBindingDebugRow[] = [];
245✔
297

298
  get [Symbol.toStringTag](): string {
UNCOV
299
    return 'Model';
×
300
  }
301

302
  toString(): string {
303
    return `Model(${this.id})`;
292✔
304
  }
305

306
  constructor(device: Device, props: ModelProps) {
307
    this.props = {...Model.defaultProps, ...props};
245✔
308
    props = this.props;
245✔
309
    this.id = props.id || uid('model');
245✔
310
    this.device = device;
245✔
311

312
    Object.assign(this.userData, props.userData);
245✔
313

314
    this.material = props.material || null;
245✔
315

316
    // Setup shader module inputs
317
    const moduleMap = Object.fromEntries(
245✔
318
      this.props.modules?.map(module => [module.name, module]) || []
355!
319
    );
320

321
    const shaderInputs =
322
      props.shaderInputs ||
245✔
323
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
324
    // @ts-ignore
325
    this.setShaderInputs(shaderInputs);
245✔
326

327
    // Setup shader assembler
328
    const platformInfo = getPlatformInfo(device);
245✔
329

330
    const modules = mergeShaderModules(this.props.modules, shaderInputs.getModules());
245✔
331

332
    this.props.shaderLayout =
245✔
333
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
421✔
334

335
    const isWebGPU = this.device.type === 'webgpu';
245✔
336

337
    // WebGPU
338
    // TODO - hack to support unified WGSL shader
339
    // TODO - this is wrong, compile a single shader
340
    if (isWebGPU && this.props.source) {
245✔
341
      // WGSL
342
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
36✔
343
        platformInfo,
344
        ...this.props,
345
        modules
346
      });
347
      this.source = source;
36✔
348
      // @ts-expect-error
349
      this._getModuleUniforms = getUniforms;
36✔
350
      this._bindingTable = bindingTable;
36✔
351
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
352
      const inferredShaderLayout = (
353
        device as Device & {getShaderLayout?: (source: string) => any}
36✔
354
      ).getShaderLayout?.(this.source);
355
      const shaderLayout = mergeInferredShaderLayout(this.props.shaderLayout, inferredShaderLayout);
36✔
356
      this.props.shaderLayout =
36✔
357
        mergeShaderModuleBindingsIntoLayout(shaderLayout || null, modules) || null;
72!
358
    } else {
359
      // GLSL
360
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
209✔
361
        platformInfo,
362
        ...this.props,
363
        modules
364
      });
365

366
      this.vs = vs;
209✔
367
      this.fs = fs;
209✔
368
      // @ts-expect-error
369
      this._getModuleUniforms = getUniforms;
209✔
370
      this._bindingTable = [];
209✔
371
    }
372

373
    this.vertexCount = this.props.vertexCount;
245✔
374
    this.indexCount = this.props.indexCount;
245✔
375
    this.firstVertex = this.props.firstVertex;
245✔
376
    this.firstIndex = this.props.firstIndex;
245✔
377
    this.instanceCount = this.props.instanceCount;
245✔
378

379
    this.topology = this.props.topology;
245✔
380
    this.bufferLayout = this.props.bufferLayout;
245✔
381
    this.parameters = this.props.parameters;
245✔
382

383
    // Geometry, if provided, sets topology and vertex cound
384
    if (props.geometry) {
245✔
385
      this.setGeometry(props.geometry);
62✔
386
    }
387

388
    this.pipelineFactory =
245✔
389
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
490✔
390
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
245✔
391

392
    // Create the pipeline
393
    // @note order is important
394
    this.pipeline = this._updatePipeline();
245✔
395

396
    this.vertexArray = device.createVertexArray({
245✔
397
      shaderLayout: this.pipeline.shaderLayout,
398
      bufferLayout: this.pipeline.bufferLayout
399
    });
400

401
    // Now we can apply geometry attributes
402
    if (this._gpuGeometry) {
245✔
403
      this._setGeometryAttributes(this._gpuGeometry);
62✔
404
    }
405

406
    // Apply any dynamic settings that will not trigger pipeline change
407
    if ('isInstanced' in props) {
245!
408
      this.isInstanced = props.isInstanced;
245✔
409
    }
410

411
    if (props.instanceCount) {
245✔
412
      this.setInstanceCount(props.instanceCount);
131✔
413
    }
414
    if (props.vertexCount) {
245✔
415
      this.setVertexCount(props.vertexCount);
189✔
416
    }
417
    if (props.indexBuffer) {
245!
UNCOV
418
      this.setIndexBuffer(props.indexBuffer);
×
419
    }
420
    if (props.attributes) {
245✔
421
      this.setAttributes(props.attributes);
244✔
422
    }
423
    if (props.constantAttributes) {
245!
424
      this.setConstantAttributes(props.constantAttributes);
245✔
425
    }
426
    if (props.bindings) {
245!
427
      this.setBindings(props.bindings);
245✔
428
    }
429
    if (props.transformFeedback) {
245!
UNCOV
430
      this.transformFeedback = props.transformFeedback;
×
431
    }
432
  }
433

434
  destroy(): void {
435
    if (!this._destroyed) {
165✔
436
      // Release pipeline before we destroy the shaders used by the pipeline
437
      this.pipelineFactory.release(this.pipeline);
164✔
438
      // Release the shaders
439
      this.shaderFactory.release(this.pipeline.vs);
164✔
440
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
164✔
441
        this.shaderFactory.release(this.pipeline.fs);
128✔
442
      }
443
      this._uniformStore.destroy();
164✔
444
      // TODO - mark resource as managed and destroyIfManaged() ?
445
      this._gpuGeometry?.destroy();
164✔
446
      this._destroyed = true;
164✔
447
    }
448
  }
449

450
  // Draw call
451

452
  /** Query redraw status. Clears the status. */
453
  needsRedraw(): false | string {
454
    // Catch any writes to already bound resources
455
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
7!
456
      this.setNeedsRedraw('contents of bound textures or buffers updated');
7✔
457
    }
458
    const needsRedraw = this._needsRedraw;
7✔
459
    this._needsRedraw = false;
7✔
460
    return needsRedraw;
7✔
461
  }
462

463
  /** Mark the model as needing a redraw */
464
  setNeedsRedraw(reason: string): void {
465
    this._needsRedraw ||= reason;
2,264✔
466
  }
467

468
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
469
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
470
    return this._bindingTable;
2✔
471
  }
472

473
  /**
474
   * Updates uniforms and pipeline state before opening a render pass.
475
   *
476
   * @param commandEncoder - Encoder that should own any GPU uploads emitted
477
   * during draw preparation.
478
   */
479
  predraw(commandEncoder: CommandEncoder): void {
480
    // Update uniform buffers if needed
481
    this._syncDynamicBuffers();
164✔
482
    this.updateShaderInputs(commandEncoder);
164✔
483
    this.material?.updateShaderInputs(commandEncoder);
164✔
484
    // Check if the pipeline is invalidated
485
    this.pipeline = this._updatePipeline();
164✔
486
  }
487

488
  /**
489
   * Issue one draw call.
490
   * @param renderPass - render pass to draw into
491
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
492
   */
493
  draw(renderPass: RenderPass): boolean {
494
    if (this._drawBlockedReason && !this._pipelineNeedsUpdate) {
148✔
495
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${this._drawBlockedReason}`)();
1✔
496
      return false;
1✔
497
    }
498

499
    const loadingBinding = this._areBindingsLoading();
147✔
500
    if (loadingBinding) {
147✔
501
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
1✔
502
      return false;
1✔
503
    }
504

505
    this._syncAttachmentFormats(renderPass);
146✔
506

507
    try {
146✔
508
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
146✔
509
      if (this.device.type === 'webgpu') {
146✔
510
        // WebGPU uploads cannot be encoded once the render pass is already open.
511
        // Keep the implicit draw() path working for existing callers by falling
512
        // back to immediate writes here; callers that need upload ordering
513
        // across multiple draws/viewports must call predraw(commandEncoder)
514
        // before beginRenderPass().
515
        this.updateShaderInputs();
10✔
516
        this.material?.updateShaderInputs();
10✔
517
        this._syncDynamicBuffers();
10✔
518
        this.pipeline = this._updatePipeline();
10✔
519
      } else {
520
        this.predraw(this.device.commandEncoder);
136✔
521
      }
522
    } finally {
523
      renderPass.popDebugGroup();
146✔
524
    }
525

526
    let drawSuccess: boolean;
527
    let pipelineErrored = this.pipeline.isErrored;
146✔
528
    try {
146✔
529
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
146✔
530
      this._logDrawCallStart();
146✔
531

532
      // Update the pipeline if invalidated
533
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
534
      // Application can call Model.predraw() to avoid this.
535
      this.pipeline = this._updatePipeline();
146✔
536
      pipelineErrored = this.pipeline.isErrored;
146✔
537

538
      if (pipelineErrored) {
146✔
539
        log.info(
1✔
540
          LOG_DRAW_PRIORITY,
541
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
542
        )();
543
        drawSuccess = false;
1✔
544
      } else {
545
        const drawValidationError = this.vertexArray.getDrawValidationError();
145✔
546
        if (drawValidationError) {
145!
UNCOV
547
          log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${drawValidationError}`)();
×
UNCOV
548
          this._drawBlockedReason = drawValidationError;
×
549
          drawSuccess = false;
×
550
        } else {
551
          const shaderLayout = this._getCurrentShaderLayout();
145✔
552
          const syncBindings = this._getBindings(shaderLayout);
145✔
553
          const syncBindGroups = this._getBindGroups(shaderLayout, syncBindings);
145✔
554

555
          const {indexBuffer} = this.vertexArray;
145✔
556
          const indexCount = indexBuffer
145✔
557
            ? (this.indexCount ??
28✔
558
              indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2))
12!
559
            : undefined;
560

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

592
    // Update needsRedraw flag
593
    if (drawSuccess) {
146✔
594
      this._lastDrawTimestamp = this.device.timestamp;
145✔
595
      this._needsRedraw = false;
145✔
596
    } else if (pipelineErrored) {
1!
597
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
1✔
598
      this._drawBlockedReason = PIPELINE_INITIALIZATION_FAILED;
1✔
UNCOV
599
    } else if (this._drawBlockedReason) {
×
UNCOV
600
      this._needsRedraw = this._drawBlockedReason;
×
601
    } else {
602
      this._needsRedraw = 'waiting for resource initialization';
×
603
    }
604
    return drawSuccess;
146✔
605
  }
606

607
  // Update fixed fields (can trigger pipeline rebuild)
608

609
  /**
610
   * Updates the optional geometry
611
   * Geometry, set topology and bufferLayout
612
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
613
   */
614
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
615
    this._gpuGeometry?.destroy();
62✔
616
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
62✔
617
    if (gpuGeometry) {
62!
618
      this.setTopology(gpuGeometry.topology || 'triangle-list');
62!
619
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
62✔
620
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
62✔
621
        gpuGeometry.bufferLayout,
622
        this.bufferLayout
623
      );
624
      if (this.vertexArray) {
62!
UNCOV
625
        this._setGeometryAttributes(gpuGeometry);
×
626
      }
627
    }
628
    this._gpuGeometry = gpuGeometry;
62✔
629
  }
630

631
  /**
632
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
633
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
634
   */
635
  setTopology(topology: PrimitiveTopology): void {
636
    if (topology !== this.topology) {
65✔
637
      this.topology = topology;
34✔
638
      this._setPipelineNeedsUpdate('topology');
34✔
639
    }
640
  }
641

642
  /**
643
   * Updates the buffer layout.
644
   * @note Triggers a pipeline rebuild / pipeline cache fetch
645
   */
646
  setBufferLayout(bufferLayout: BufferLayout[]): void {
647
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
3✔
648
    const nextBufferLayout = this._gpuGeometry
3✔
649
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
650
      : bufferLayout;
651
    if (deepEqual(nextBufferLayout, this.bufferLayout, -1)) {
3✔
652
      return;
2✔
653
    }
654

655
    this.bufferLayout = nextBufferLayout;
1✔
656
    this._setPipelineNeedsUpdate('bufferLayout');
1✔
657

658
    // Recreate the pipeline
659
    this.pipeline = this._updatePipeline();
1✔
660

661
    // vertex array needs to be updated if we update buffer layout,
662
    // but not if we update parameters
663
    this.vertexArray = this.device.createVertexArray({
1✔
664
      shaderLayout: this.pipeline.shaderLayout,
665
      bufferLayout: this.pipeline.bufferLayout
666
    });
667

668
    // Reapply geometry attributes to the new vertex array
669
    if (this._gpuGeometry) {
1!
670
      this._setGeometryAttributes(this._gpuGeometry);
1✔
671
    }
672
  }
673

674
  /**
675
   * Set GPU parameters.
676
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
677
   * @param parameters
678
   */
679
  setParameters(parameters: RenderPipelineParameters) {
UNCOV
680
    if (!deepEqual(parameters, this.parameters, 2)) {
×
UNCOV
681
      this.parameters = parameters;
×
682
      this._setPipelineNeedsUpdate('parameters');
×
683
    }
684
  }
685

686
  // Update dynamic fields
687

688
  /**
689
   * Updates the instance count (used in draw calls)
690
   * @note Any attributes with stepMode=instance need to be at least this big
691
   */
692
  setInstanceCount(instanceCount: number): void {
693
    this.instanceCount = instanceCount;
162✔
694
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
695
    // but deck.gl sets instanceCount even for models that are not instanced.
696
    if (this.isInstanced === undefined && instanceCount > 0) {
162✔
697
      this.isInstanced = true;
138✔
698
    }
699
    this.setNeedsRedraw('instanceCount');
162✔
700
  }
701

702
  /**
703
   * Updates the vertex count (used in draw calls)
704
   * @note Any attributes with stepMode=vertex need to be at least this big
705
   */
706
  setVertexCount(vertexCount: number): void {
707
    this.vertexCount = vertexCount;
201✔
708
    this.setNeedsRedraw('vertexCount');
201✔
709
  }
710

711
  /** Updates the indexed draw count override. */
712
  setIndexCount(indexCount: number | undefined): void {
713
    this.indexCount = indexCount;
11✔
714
    this.setNeedsRedraw('indexCount');
11✔
715
  }
716

717
  /** Updates the first indexed/non-indexed draw offsets. */
718
  setDrawOffsets({firstVertex, firstIndex}: {firstVertex: number; firstIndex: number}): void {
719
    this.firstVertex = firstVertex;
1✔
720
    this.firstIndex = firstIndex;
1✔
721
    this.setNeedsRedraw('drawOffsets');
1✔
722
  }
723

724
  /** Set the shader inputs */
725
  setShaderInputs(shaderInputs: ShaderInputs): void {
726
    this.shaderInputs = shaderInputs;
245✔
727
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
245✔
728
    // Create uniform buffer bindings for all modules that actually have uniforms
729
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
245✔
730
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
464✔
731
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
136✔
732
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
136✔
733
      }
734
    }
735
    this.setNeedsRedraw('shaderInputs');
245✔
736
  }
737

738
  setMaterial(material: Material | null): void {
UNCOV
739
    this.material = material;
×
UNCOV
740
    this.setNeedsRedraw('material');
×
741
  }
742

743
  /** Update uniform buffers from the model's shader inputs */
744
  /**
745
   * Flushes current shader-input values into managed uniform buffers and
746
   * non-material bindings.
747
   *
748
   * @param commandEncoder - Optional encoder used to order uniform uploads with
749
   * subsequent draw commands.
750
   */
751
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
752
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
174✔
753
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
174✔
754
    // TODO - this is already tracked through buffer/texture update times?
755
    this.setNeedsRedraw('shaderInputs');
174✔
756
  }
757

758
  /**
759
   * Sets bindings (textures, samplers, uniform buffers)
760
   */
761
  setBindings(bindings: Record<string, ModelBinding>): void {
762
    Object.assign(this.bindings, bindings);
480✔
763
    this.setNeedsRedraw('bindings');
480✔
764
  }
765

766
  /**
767
   * Updates optional transform feedback. WebGL only.
768
   */
769
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
770
    this.transformFeedback = transformFeedback;
88✔
771
    this.setNeedsRedraw('transformFeedback');
88✔
772
  }
773

774
  /**
775
   * Sets the index buffer
776
   * @todo - how to unset it if we change geometry?
777
   */
778
  setIndexBuffer(indexBuffer: ModelBuffer | null): void {
779
    const resolvedIndexBuffer =
780
      indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
74!
781
    this.indexBuffer = resolvedIndexBuffer;
74✔
782
    this._dynamicIndexBufferSource =
74✔
783
      indexBuffer instanceof DynamicBuffer
74!
784
        ? {source: indexBuffer, generation: indexBuffer.generation}
785
        : null;
786
    this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
74✔
787
    this.setNeedsRedraw('indexBuffer');
74✔
788
  }
789

790
  /**
791
   * Sets attributes (buffers)
792
   * @note Overrides any attributes previously set with the same name
793
   */
794
  setAttributes(buffers: Record<string, ModelBuffer>, options?: {disableWarnings?: boolean}): void {
795
    this._drawBlockedReason = false;
460✔
796
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
460✔
797
    if (buffers['indices']) {
460!
UNCOV
798
      log.warn(
×
799
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
800
      )();
801
    }
802

803
    // ensure bufferLayout order matches source layout so we bind
804
    // the correct buffers to the correct indices in webgpu.
805
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
460✔
806
      this.pipeline.shaderLayout,
807
      this.bufferLayout
808
    );
809
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
460✔
810

811
    // Check if all buffers have a layout
812
    for (const [bufferName, buffer] of Object.entries(buffers)) {
460✔
813
      const resolvedBuffer = buffer instanceof DynamicBuffer ? buffer.buffer : buffer;
577✔
814
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
577✔
815
      if (!bufferLayout) {
577!
UNCOV
816
        if (!disableWarnings) {
×
UNCOV
817
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
818
        }
819
        continue; // eslint-disable-line no-continue
×
820
      }
821

822
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
823
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
824
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
577✔
825
      let set = false;
577✔
826
      for (const attributeName of attributeNames) {
577✔
827
        const attributeInfo = this._attributeInfos[attributeName];
784✔
828
        if (attributeInfo) {
784✔
829
          const bufferSlot =
830
            this.device.type === 'webgpu'
740✔
831
              ? this.vertexArray.getBufferSlot(attributeInfo.bufferName)
832
              : attributeInfo.location;
833
          if (bufferSlot === null) {
740!
UNCOV
834
            if (!disableWarnings) {
×
UNCOV
835
              log.warn(
×
836
                `Model(${this.id}): Missing vertex array slot for buffer "${attributeInfo.bufferName}".`
837
              )();
838
            }
UNCOV
839
            continue; // eslint-disable-line no-continue
×
840
          }
841

842
          this.vertexArray.setBuffer(bufferSlot, resolvedBuffer);
740✔
843
          if (buffer instanceof DynamicBuffer) {
740✔
844
            this._dynamicAttributeBufferSources[bufferSlot] = {
41✔
845
              source: buffer,
846
              generation: buffer.generation
847
            };
848
          } else {
849
            delete this._dynamicAttributeBufferSources[bufferSlot];
699✔
850
          }
851
          set = true;
740✔
852
        }
853
      }
854
      if (!set && !disableWarnings) {
577✔
855
        log.warn(
2✔
856
          `Model(${this.id}): Ignoring buffer "${resolvedBuffer.id}" for unknown attribute "${bufferName}"`
857
        )();
858
      }
859
    }
860
    this.setNeedsRedraw('attributes');
460✔
861
  }
862

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

888
  // INTERNAL METHODS
889

890
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
891
  _areBindingsLoading(): string | false {
892
    for (const binding of Object.values(this.bindings)) {
150✔
893
      if (isTextureBindingSource(binding) && !binding.isReady) {
146✔
894
        return binding.id;
1✔
895
      }
896
    }
897
    for (const binding of Object.values(this.material?.bindings || {})) {
149✔
UNCOV
898
      if (isTextureBindingSource(binding) && !binding.isReady) {
×
UNCOV
899
        return binding.id;
×
900
      }
901
    }
902
    return false;
149✔
903
  }
904

905
  /**
906
   * Resolves ready model bindings for the current shader layout.
907
   * @param shaderLayout Reflected bindings used to select copied or external texture resolution.
908
   */
909
  _getBindings(
910
    shaderLayout: AnyShaderLayout = this._getCurrentShaderLayout()
410✔
911
  ): Record<string, Binding> {
912
    const validBindings: Record<string, Binding> = {};
410✔
913

914
    for (const [name, binding] of Object.entries(this.bindings)) {
410✔
915
      const resolvedBinding = resolveModelBinding(name, binding, shaderLayout);
337✔
916
      if (resolvedBinding) {
337!
917
        validBindings[name] = resolvedBinding;
337✔
918
      }
919
    }
920

921
    return validBindings;
410✔
922
  }
923

924
  /**
925
   * Groups resolved model and material bindings for the current shader layout.
926
   * @param shaderLayout Reflected bindings used to group logical binding names.
927
   * @param bindings Model bindings already resolved for this draw preparation.
928
   */
929
  _getBindGroups(
930
    shaderLayout: AnyShaderLayout = this._getCurrentShaderLayout(),
403✔
931
    bindings: Record<string, Binding> = this._getBindings(shaderLayout)
403✔
932
  ): BindingsByGroup {
933
    const bindGroups = shaderLayout.bindings.length
403✔
934
      ? normalizeBindingsByGroup(shaderLayout, bindings)
935
      : {0: bindings};
936

937
    if (!this.material) {
403✔
938
      return bindGroups;
393✔
939
    }
940

941
    for (const [groupKey, groupBindings] of Object.entries(
10✔
942
      this.material.getBindingsByGroup(shaderLayout)
943
    )) {
944
      const group = Number(groupKey);
10✔
945
      bindGroups[group] = {
10✔
946
        ...(bindGroups[group] || {}),
20✔
947
        ...groupBindings
948
      };
949
    }
950

951
    return bindGroups;
10✔
952
  }
953

954
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
955
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
148✔
956
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
148!
957
  }
958

959
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
960
  _getBindingsUpdateTimestamp(): number {
961
    let timestamp = 0;
7✔
962
    if (this._dynamicIndexBufferSource) {
7!
UNCOV
963
      timestamp = Math.max(timestamp, this._dynamicIndexBufferSource.source.updateTimestamp);
×
964
    }
965
    for (const entry of Object.values(this._dynamicAttributeBufferSources)) {
7✔
966
      timestamp = Math.max(timestamp, entry.source.updateTimestamp);
6✔
967
    }
968
    for (const binding of Object.values(this.bindings)) {
7✔
969
      if (binding instanceof TextureView) {
8!
UNCOV
970
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
971
      } else if (
972
        binding instanceof Buffer ||
8✔
973
        binding instanceof Texture ||
974
        binding instanceof ExternalTexture
975
      ) {
976
        timestamp = Math.max(timestamp, binding.updateTimestamp);
6✔
977
      } else if (binding instanceof DynamicBuffer) {
2!
UNCOV
978
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
979
      } else if (isTextureBindingSource(binding)) {
2!
980
        timestamp = binding.isReady
2!
981
          ? Math.max(timestamp, binding.updateTimestamp)
982
          : // The texture will become available in the future
983
            Infinity;
UNCOV
984
      } else if (isBufferRangeBinding(binding)) {
×
UNCOV
985
        timestamp = Math.max(
×
986
          timestamp,
987
          binding.buffer instanceof DynamicBuffer
×
988
            ? binding.buffer.updateTimestamp
989
            : binding.buffer.updateTimestamp
990
        );
991
      }
992
    }
993
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
7✔
994
  }
995

996
  /**
997
   * Updates the optional geometry attributes
998
   * Geometry, sets several attributes, indexBuffer, and also vertex count
999
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
1000
   */
1001
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
1002
    // Filter geometry attribute so that we don't issue warnings for unused attributes
1003
    const attributes = {...gpuGeometry.attributes};
63✔
1004
    for (const [attributeName] of Object.entries(attributes)) {
63✔
1005
      if (
63!
1006
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
209✔
1007
        attributeName !== 'positions'
1008
      ) {
1009
        delete attributes[attributeName];
63✔
1010
      }
1011
    }
1012

1013
    // TODO - delete previous geometry?
1014
    this.vertexCount = gpuGeometry.vertexCount;
63✔
1015
    this.setIndexBuffer(gpuGeometry.indices || null);
63✔
1016
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
63✔
1017
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
63✔
1018

1019
    this.setNeedsRedraw('geometry attributes');
63✔
1020
  }
1021

1022
  /** Mark pipeline as needing update */
1023
  _setPipelineNeedsUpdate(reason: string): void {
1024
    this._pipelineNeedsUpdate ||= reason;
42✔
1025
    this._drawBlockedReason = false;
42✔
1026
    this.setNeedsRedraw(reason);
42✔
1027
  }
1028

1029
  /** Update pipeline if needed */
1030
  _updatePipeline(): RenderPipeline {
1031
    if (this._pipelineNeedsUpdate) {
569✔
1032
      let prevShaderVs: Shader | null = null;
254✔
1033
      let prevShaderFs: Shader | null = null;
254✔
1034
      if (this.pipeline) {
254✔
1035
        log.log(
9✔
1036
          1,
1037
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
1038
        )();
1039
        prevShaderVs = this.pipeline.vs;
9✔
1040
        prevShaderFs = this.pipeline.fs;
9✔
1041
      }
1042

1043
      this._pipelineNeedsUpdate = false;
254✔
1044

1045
      const vs = this.shaderFactory.createShader({
254✔
1046
        id: `${this.id}-vertex`,
1047
        stage: 'vertex',
1048
        source: this.source || this.vs,
465✔
1049
        debugShaders: this.props.debugShaders
1050
      });
1051

1052
      let fs: Shader | null = null;
254✔
1053
      if (this.source) {
254✔
1054
        fs = vs;
43✔
1055
      } else if (this.fs) {
211!
1056
        fs = this.shaderFactory.createShader({
211✔
1057
          id: `${this.id}-fragment`,
1058
          stage: 'fragment',
1059
          source: this.source || this.fs,
422✔
1060
          debugShaders: this.props.debugShaders
1061
        });
1062
      }
1063

1064
      this.pipeline = this.pipelineFactory.createRenderPipeline({
254✔
1065
        ...this.props,
1066
        bindings: undefined,
1067
        bufferLayout: this.bufferLayout,
1068
        colorAttachmentFormats: this._colorAttachmentFormats,
1069
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
1070
        topology: this.topology,
1071
        parameters: this.parameters,
1072
        bindGroups: this._getBindGroups(),
1073
        vs,
1074
        fs
1075
      });
1076

1077
      this._attributeInfos = getAttributeInfosFromLayouts(
254✔
1078
        this.pipeline.shaderLayout,
1079
        this.bufferLayout
1080
      );
1081

1082
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
254✔
1083
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
254✔
1084
        this.shaderFactory.release(prevShaderFs);
2✔
1085
      }
1086
    }
1087
    return this.pipeline;
569✔
1088
  }
1089

1090
  /** Throttle draw call logging */
1091
  _lastLogTime = 0;
245✔
1092
  _logOpen = false;
245✔
1093

1094
  _logDrawCallStart(): void {
1095
    // IF level is 4 or higher, log every frame.
1096
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
146!
1097
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
146!
1098
      return;
146✔
1099
    }
1100

UNCOV
1101
    this._lastLogTime = Date.now();
×
UNCOV
1102
    this._logOpen = true;
×
1103

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

1107
  _logDrawCallEnd(): void {
1108
    if (this._logOpen) {
146!
UNCOV
1109
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1110

1111
      // log.table(logLevel, attributeTable)();
1112
      // log.table(logLevel, uniformTable)();
UNCOV
1113
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1114

1115
      const uniformTable = this.shaderInputs.getDebugTable();
×
UNCOV
1116
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1117

1118
      const attributeTable = this._getAttributeDebugTable();
×
UNCOV
1119
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1120
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1121

1122
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
UNCOV
1123
      this._logOpen = false;
×
1124
    }
1125
  }
1126

1127
  protected _drawCount = 0;
245✔
1128
  _logFramebuffer(renderPass: RenderPass): void {
1129
    const debugFramebuffers = this.device.props.debugFramebuffers;
146✔
1130
    this._drawCount++;
146✔
1131
    // Update first 3 frames and then every 60 frames
1132
    if (!debugFramebuffers) {
146!
1133
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
1134
      return;
146✔
1135
    }
UNCOV
1136
    const framebuffer = renderPass.props.framebuffer;
×
UNCOV
1137
    debugFramebuffer(renderPass, framebuffer, {
×
1138
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
1139
      minimap: true
1140
    });
1141
  }
1142

1143
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
UNCOV
1144
    const table: Record<string, Record<string, unknown>> = {};
×
UNCOV
1145
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
1146
      const values = this.vertexArray.attributes[attributeInfo.location];
×
1147
      table[attributeInfo.location] = {
×
1148
        name,
1149
        type: attributeInfo.shaderType,
1150
        values: values
×
1151
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
1152
          : 'null'
1153
      };
1154
    }
UNCOV
1155
    if (this.vertexArray.indexBuffer) {
×
UNCOV
1156
      const {indexBuffer} = this.vertexArray;
×
1157
      const values =
1158
        indexBuffer.indexType === 'uint32'
×
1159
          ? new Uint32Array(indexBuffer.debugData)
1160
          : new Uint16Array(indexBuffer.debugData);
UNCOV
1161
      table['indices'] = {
×
1162
        name: 'indices',
1163
        type: indexBuffer.indexType,
1164
        values: values.toString()
1165
      };
1166
    }
UNCOV
1167
    return table;
×
1168
  }
1169

1170
  // TODO - fix typing of luma data types
1171
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
UNCOV
1172
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1173
    const typedArray =
1174
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
UNCOV
1175
    return typedArray.toString();
×
1176
  }
1177

1178
  private _getNonMaterialBindings(
1179
    bindings: Record<string, ModelBinding>
1180
  ): Record<string, ModelBinding> {
1181
    if (!this.material) {
174!
1182
      return bindings;
174✔
1183
    }
1184

UNCOV
1185
    const filteredBindings: Record<string, ModelBinding> = {};
×
UNCOV
1186
    for (const [name, binding] of Object.entries(bindings)) {
×
1187
      if (!this.material.ownsBinding(name)) {
×
1188
        filteredBindings[name] = binding;
×
1189
      }
1190
    }
UNCOV
1191
    return filteredBindings;
×
1192
  }
1193

1194
  /** Returns the current reflected shader layout or the pre-reflection empty layout. */
1195
  private _getCurrentShaderLayout(): AnyShaderLayout {
1196
    return this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
409✔
1197
  }
1198

1199
  private _syncDynamicBuffers(): void {
1200
    if (
174!
1201
      this._dynamicIndexBufferSource &&
174!
1202
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1203
    ) {
UNCOV
1204
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
UNCOV
1205
      this.indexBuffer = resolvedIndexBuffer;
×
1206
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
1207
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
1208
      this.setNeedsRedraw('dynamic index buffer');
×
1209
    }
1210

1211
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
174✔
1212
      if (entry.generation !== entry.source.generation) {
6!
UNCOV
1213
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
UNCOV
1214
        entry.generation = entry.source.generation;
×
1215
        this.setNeedsRedraw('dynamic attribute buffer');
×
1216
      }
1217
    }
1218
  }
1219
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1220
    if (this.device.type !== 'webgpu') {
146✔
1221
      return;
136✔
1222
    }
1223

1224
    const framebuffer =
1225
      (
1226
        renderPass as RenderPass & {
10✔
1227
          framebuffer?: {
1228
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1229
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1230
          };
1231
        }
1232
      ).framebuffer || renderPass.props.framebuffer;
1233
    const renderBundleProps = renderPass.props as RenderPass['props'] & {
146✔
1234
      colorAttachmentFormats?: (TextureFormatColor | null)[];
1235
      depthStencilAttachmentFormat?: TextureFormatDepthStencil | false;
1236
    };
1237

1238
    const nextColorAttachmentFormats =
1239
      renderBundleProps.colorAttachmentFormats ??
146✔
1240
      framebuffer?.colorAttachments?.map(colorAttachment =>
1241
        asColorAttachmentFormat(colorAttachment?.texture?.format)
9✔
1242
      );
1243
    const nextDepthStencilAttachmentFormat =
1244
      renderBundleProps.depthStencilAttachmentFormat === false
146!
1245
        ? undefined
1246
        : (renderBundleProps.depthStencilAttachmentFormat ??
19✔
1247
          asDepthStencilAttachmentFormat(framebuffer?.depthStencilAttachment?.texture?.format));
1248

1249
    if (
146✔
1250
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
149✔
1251
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1252
    ) {
1253
      this._colorAttachmentFormats = nextColorAttachmentFormats;
7✔
1254
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
7✔
1255
      this._setPipelineNeedsUpdate('attachment formats');
7✔
1256
    }
1257
  }
1258
}
1259

1260
// HELPERS
1261

1262
/**
1263
 * Resolves one model binding against the current shader layout.
1264
 * @param bindingName Logical model binding name.
1265
 * @param binding Model binding or deferred engine binding source.
1266
 * @param shaderLayout Reflected bindings used to select copied or external texture resolution.
1267
 * @returns Concrete core binding, or `null` while a deferred source is unavailable.
1268
 */
1269
function resolveModelBinding(
1270
  bindingName: string,
1271
  binding: ModelBinding,
1272
  shaderLayout: AnyShaderLayout
1273
): Binding | null {
1274
  if (isTextureBindingSource(binding)) {
337✔
1275
    const bindingLayout = getTextureBindingLayout(shaderLayout, bindingName, {
1✔
1276
      fallbackGroup: 0
1277
    });
1278
    return bindingLayout ? binding.resolveTextureBinding(bindingLayout) : null;
1!
1279
  }
1280

1281
  if (binding instanceof DynamicBuffer) {
336✔
1282
    return binding.buffer;
23✔
1283
  }
1284

1285
  if (isBufferRangeBinding(binding)) {
313✔
1286
    return resolveBufferRangeBinding(binding);
7✔
1287
  }
1288

1289
  return binding;
306✔
1290
}
1291
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1292
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
9!
1293
}
1294

1295
function asDepthStencilAttachmentFormat(
1296
  format?: string | null
1297
): TextureFormatDepthStencil | undefined {
1298
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
9!
1299
}
1300

1301
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1302
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
9✔
1303
}
1304

1305
/** Create a shadertools platform info from the Device */
1306
export function getPlatformInfo(device: Device): PlatformInfo {
1307
  return {
245✔
1308
    type: device.type,
1309
    shaderLanguage: device.info.shadingLanguage,
1310
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1311
    gpu: device.info.gpu,
1312
    limits: device.limits as unknown as Record<string, number | undefined>,
1313
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1314
    features: device.features as unknown as Set<DeviceFeature>
1315
  };
1316
}
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