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

visgl / luma.gl / 27647043079

16 Jun 2026 08:46PM UTC coverage: 70.644% (-0.05%) from 70.693%
27647043079

Pull #2679

github

web-flow
Merge fd2cbfbd8 into 530fcafa4
Pull Request #2679: feat(engine) add texture binding sources

9556 of 15252 branches covered (62.65%)

Branch coverage included in aggregate %.

35 of 58 new or added lines in 14 files covered. (60.34%)

5 existing lines in 3 files now uncovered.

19517 of 25902 relevant lines covered (75.35%)

4137.84 hits per line

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

75.75
/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
  mergeShaderModuleBindingsIntoLayout,
53
  shaderModuleHasUniforms
54
} from '../utils/shader-module-utils';
55
import {uid} from '../utils/uid';
56
import {ShaderInputs} from '../shader-inputs';
57
import {
58
  DynamicBuffer,
59
  type DynamicBufferRange,
60
  isBufferRangeBinding,
61
  resolveBufferRangeBinding
62
} from '../dynamic-buffer/dynamic-buffer';
63
import {
64
  getTextureBindingLayout,
65
  isTextureBindingSource,
66
  type TextureBindingSource
67
} from '../dynamic-texture/texture-binding-source';
68
import {Material} from '../material/material';
69

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

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

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

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

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

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

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

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

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

138
  transformFeedback?: TransformFeedback;
139

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

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

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

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

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

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

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

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

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

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

233
  // Dynamic properties
234

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

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

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

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

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

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

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

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

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

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

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

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

311
    Object.assign(this.userData, props.userData);
242✔
312

313
    this.material = props.material || null;
242✔
314

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

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

326
    // Setup shader assembler
327
    const platformInfo = getPlatformInfo(device);
242✔
328

329
    // Extract modules from shader inputs if not supplied
330
    const modules =
242!
331
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
332
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
242✔
333

334
    this.props.shaderLayout =
242✔
335
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
416✔
336

337
    const isWebGPU = this.device.type === 'webgpu';
242✔
338

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

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

375
    this.vertexCount = this.props.vertexCount;
242✔
376
    this.indexCount = this.props.indexCount;
242✔
377
    this.firstVertex = this.props.firstVertex;
242✔
378
    this.firstIndex = this.props.firstIndex;
242✔
379
    this.instanceCount = this.props.instanceCount;
242✔
380

381
    this.topology = this.props.topology;
242✔
382
    this.bufferLayout = this.props.bufferLayout;
242✔
383
    this.parameters = this.props.parameters;
242✔
384

385
    // Geometry, if provided, sets topology and vertex cound
386
    if (props.geometry) {
242✔
387
      this.setGeometry(props.geometry);
61✔
388
    }
389

390
    this.pipelineFactory =
242✔
391
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
484✔
392
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
242✔
393

394
    // Create the pipeline
395
    // @note order is important
396
    this.pipeline = this._updatePipeline();
242✔
397

398
    this.vertexArray = device.createVertexArray({
242✔
399
      shaderLayout: this.pipeline.shaderLayout,
400
      bufferLayout: this.pipeline.bufferLayout
401
    });
402

403
    // Now we can apply geometry attributes
404
    if (this._gpuGeometry) {
242✔
405
      this._setGeometryAttributes(this._gpuGeometry);
61✔
406
    }
407

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

413
    if (props.instanceCount) {
242✔
414
      this.setInstanceCount(props.instanceCount);
130✔
415
    }
416
    if (props.vertexCount) {
242✔
417
      this.setVertexCount(props.vertexCount);
187✔
418
    }
419
    if (props.indexBuffer) {
242!
420
      this.setIndexBuffer(props.indexBuffer);
×
421
    }
422
    if (props.attributes) {
242✔
423
      this.setAttributes(props.attributes);
241✔
424
    }
425
    if (props.constantAttributes) {
242!
426
      this.setConstantAttributes(props.constantAttributes);
242✔
427
    }
428
    if (props.bindings) {
242!
429
      this.setBindings(props.bindings);
242✔
430
    }
431
    if (props.transformFeedback) {
242!
432
      this.transformFeedback = props.transformFeedback;
×
433
    }
434
  }
435

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

452
  // Draw call
453

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

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

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

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

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

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

507
    this._syncAttachmentFormats(renderPass);
145✔
508

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

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

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

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

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

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

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

609
  // Update fixed fields (can trigger pipeline rebuild)
610

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

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

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

657
    this.bufferLayout = nextBufferLayout;
×
658
    this._setPipelineNeedsUpdate('bufferLayout');
×
659

660
    // Recreate the pipeline
661
    this.pipeline = this._updatePipeline();
×
662

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

670
    // Reapply geometry attributes to the new vertex array
671
    if (this._gpuGeometry) {
×
672
      this._setGeometryAttributes(this._gpuGeometry);
×
673
    }
674
  }
675

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

688
  // Update dynamic fields
689

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

890
  // INTERNAL METHODS
891

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

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

916
    for (const [name, binding] of Object.entries(this.bindings)) {
404✔
917
      const resolvedBinding = resolveModelBinding(name, binding, shaderLayout);
336✔
918
      if (resolvedBinding) {
336!
919
        validBindings[name] = resolvedBinding;
336✔
920
      }
921
    }
922

923
    return validBindings;
404✔
924
  }
925

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

939
    if (!this.material) {
397✔
940
      return bindGroups;
387✔
941
    }
942

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

953
    return bindGroups;
10✔
954
  }
955

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

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

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

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

1021
    this.setNeedsRedraw('geometry attributes');
61✔
1022
  }
1023

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

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

1045
      this._pipelineNeedsUpdate = false;
249✔
1046

1047
      const vs = this.shaderFactory.createShader({
249✔
1048
        id: `${this.id}-vertex`,
1049
        stage: 'vertex',
1050
        source: this.source || this.vs,
458✔
1051
        debugShaders: this.props.debugShaders
1052
      });
1053

1054
      let fs: Shader | null = null;
249✔
1055
      if (this.source) {
249✔
1056
        fs = vs;
40✔
1057
      } else if (this.fs) {
209!
1058
        fs = this.shaderFactory.createShader({
209✔
1059
          id: `${this.id}-fragment`,
1060
          stage: 'fragment',
1061
          source: this.source || this.fs,
418✔
1062
          debugShaders: this.props.debugShaders
1063
        });
1064
      }
1065

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

1079
      this._attributeInfos = getAttributeInfosFromLayouts(
249✔
1080
        this.pipeline.shaderLayout,
1081
        this.bufferLayout
1082
      );
1083

1084
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
249✔
1085
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
249✔
1086
        this.shaderFactory.release(prevShaderFs);
1✔
1087
      }
1088
    }
1089
    return this.pipeline;
563✔
1090
  }
1091

1092
  /** Throttle draw call logging */
1093
  _lastLogTime = 0;
242✔
1094
  _logOpen = false;
242✔
1095

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

1103
    this._lastLogTime = Date.now();
×
1104
    this._logOpen = true;
×
1105

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

1109
  _logDrawCallEnd(): void {
1110
    if (this._logOpen) {
145!
1111
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1112

1113
      // log.table(logLevel, attributeTable)();
1114
      // log.table(logLevel, uniformTable)();
1115
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1116

1117
      const uniformTable = this.shaderInputs.getDebugTable();
×
1118
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1119

1120
      const attributeTable = this._getAttributeDebugTable();
×
1121
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1122
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1123

1124
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1125
      this._logOpen = false;
×
1126
    }
1127
  }
1128

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

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

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

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

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

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

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

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

1226
    const framebuffer =
1227
      (
1228
        renderPass as RenderPass & {
9!
1229
          framebuffer?: {
1230
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1231
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1232
          };
1233
        }
1234
      ).framebuffer || renderPass.props.framebuffer;
1235

1236
    const nextColorAttachmentFormats = framebuffer?.colorAttachments?.map(colorAttachment =>
145✔
1237
      asColorAttachmentFormat(colorAttachment?.texture?.format)
9✔
1238
    );
1239
    const nextDepthStencilAttachmentFormat = asDepthStencilAttachmentFormat(
145✔
1240
      framebuffer?.depthStencilAttachment?.texture?.format
1241
    );
1242

1243
    if (
145✔
1244
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
148✔
1245
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1246
    ) {
1247
      this._colorAttachmentFormats = nextColorAttachmentFormats;
6✔
1248
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
6✔
1249
      this._setPipelineNeedsUpdate('attachment formats');
6✔
1250
    }
1251
  }
1252
}
1253

1254
// HELPERS
1255

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

1275
  if (binding instanceof DynamicBuffer) {
335✔
1276
    return binding.buffer;
23✔
1277
  }
1278

1279
  if (isBufferRangeBinding(binding)) {
312✔
1280
    return resolveBufferRangeBinding(binding);
7✔
1281
  }
1282

1283
  return binding;
305✔
1284
}
1285
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1286
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
9!
1287
}
1288

1289
function asDepthStencilAttachmentFormat(
1290
  format?: string | null
1291
): TextureFormatDepthStencil | undefined {
1292
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
9!
1293
}
1294

1295
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1296
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
9✔
1297
}
1298

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