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

visgl / luma.gl / 28112330114

24 Jun 2026 04:07PM UTC coverage: 70.649% (-0.03%) from 70.68%
28112330114

push

github

web-flow
[codex] Move render draw state to render passes (#2689)

10077 of 16053 branches covered (62.77%)

Branch coverage included in aggregate %.

97 of 112 new or added lines in 8 files covered. (86.61%)

14 existing lines in 5 files now uncovered.

20430 of 27128 relevant lines covered (75.31%)

4011.66 hits per line

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

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

41
import type {
42
  ShaderBindingDebugRow,
43
  ShaderModule,
44
  ShaderPlugin,
45
  PlatformInfo
46
} from '@luma.gl/shadertools';
47
import {
48
  mergeShaderPluginModules,
49
  resolveShaderPlugins,
50
  ShaderAssembler
51
} from '@luma.gl/shadertools';
52

53
import type {Geometry} from '../geometry/geometry';
54
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
55
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
56
import {debugFramebuffer} from '../debug/debug-framebuffer';
57
import {deepEqual} from '../utils/deep-equal';
58
import {BufferLayoutHelper} from '../utils/buffer-layout-helper';
59
import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';
60
import {
61
  mergeInferredShaderLayout,
62
  mergeShaderModules,
63
  mergeShaderModuleBindingsIntoLayout,
64
  shaderModuleHasUniforms
65
} from '../utils/shader-module-utils';
66
import {uid} from '../utils/uid';
67
import {ShaderInputs} from '../shader-inputs';
68
import {
69
  DynamicBuffer,
70
  type DynamicBufferRange,
71
  isBufferRangeBinding,
72
  resolveBufferRangeBinding
73
} from '../dynamic-buffer/dynamic-buffer';
74
import {
75
  getTextureBindingLayout,
76
  isTextureBindingSource,
77
  type TextureBindingSource
78
} from '../dynamic-texture/texture-binding-source';
79
import {Material} from '../material/material';
80

81
const LOG_DRAW_PRIORITY = 2;
131✔
82
const LOG_DRAW_TIMEOUT = 10000;
131✔
83
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
131✔
84
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
131✔
85
  'stencil8',
86
  'depth16unorm',
87
  'depth24plus',
88
  'depth24plus-stencil8',
89
  'depth32float',
90
  'depth32float-stencil8'
91
];
92
/** Resource accepted by one model binding slot. */
93
type ModelBinding = Binding | TextureBindingSource | DynamicBuffer | DynamicBufferRange;
94
type ModelBuffer = Buffer | DynamicBuffer;
95
/** Shader layout subset needed while resolving texture binding sources. */
96
type AnyShaderLayout = Pick<ShaderLayout | ComputeShaderLayout, 'bindings'>;
97

98
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
99
  source?: string;
100
  vs?: string | null;
101
  fs?: string | null;
102

103
  /** Shadertools shader modules added to shader code. */
104
  modules?: ShaderModule[];
105
  /** Shadertools boolean or numeric preprocessor defines that configure shader code. */
106
  defines?: Record<string, boolean | number>;
107
  /** Reusable shader assembly plugins resolved for the active GLSL or WGSL backend. */
108
  plugins?: ShaderPlugin[];
109

110
  /** Shader inputs, used to generate uniform buffers and bindings. */
111
  shaderInputs?: ShaderInputs;
112
  /** Material-owned group-3 bindings */
113
  material?: Material;
114
  /** Shader resource bindings, including dynamic buffers and dynamic textures. */
115
  bindings?: Record<string, ModelBinding>;
116
  /** WebGL-only uniforms */
117
  uniforms?: Record<string, unknown>;
118
  /** Parameters that are built into the pipeline */
119
  parameters?: RenderPipelineParameters;
120

121
  /** Geometry */
122
  geometry?: GPUGeometry | Geometry | null;
123

124
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
125
  isInstanced?: boolean;
126
  /** instance count */
127
  instanceCount?: number;
128
  /** Vertex count */
129
  vertexCount?: number;
130

131
  /** Optional index buffer. Dynamic buffers are rebound when resized. */
132
  indexBuffer?: ModelBuffer | null;
133
  /** Optional indexed draw count. Defaults to the full bound index buffer length. */
134
  indexCount?: number;
135
  /** First vertex byte offset for WebGL indexed draws or first vertex for non-indexed draws. */
136
  firstVertex?: number;
137
  /** First index element for WebGPU indexed draws. */
138
  firstIndex?: number;
139
  /** Buffer-valued attributes. Dynamic buffers are rebound when resized. */
140
  attributes?: Record<string, ModelBuffer>;
141
  /**   */
142
  constantAttributes?: Record<string, TypedArray>;
143

144
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
145
  disableWarnings?: boolean;
146

147
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
148
  varyings?: string[];
149

150
  transformFeedback?: TransformFeedback;
151

152
  /** Show shader source in browser? */
153
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
154

155
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
156
  pipelineFactory?: PipelineFactory;
157
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
158
  shaderFactory?: ShaderFactory;
159
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
160
  shaderAssembler?: ShaderAssembler;
161
};
162

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

201
    isInstanced: undefined!,
202
    instanceCount: 0,
203
    vertexCount: 0,
204

205
    shaderInputs: undefined!,
206
    material: undefined!,
207
    pipelineFactory: undefined!,
208
    shaderFactory: undefined!,
209
    transformFeedback: undefined!,
210
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
211

212
    debugShaders: undefined!,
213
    disableWarnings: undefined!
214
  };
215

216
  /** Device that created this model */
217
  readonly device: Device;
218
  /** Application provided identifier */
219
  readonly id: string;
220
  /** WGSL shader source when using unified shader */
221
  // @ts-expect-error assigned in function called from constructor
222
  readonly source: string;
223
  /** GLSL vertex shader source */
224
  // @ts-expect-error assigned in function called from constructor
225
  readonly vs: string;
226
  /** GLSL fragment shader source */
227
  // @ts-expect-error assigned in function called from constructor
228
  readonly fs: string;
229
  /** Factory used to create render pipelines */
230
  readonly pipelineFactory: PipelineFactory;
231
  /** Factory used to create shaders */
232
  readonly shaderFactory: ShaderFactory;
233
  /** User-supplied per-model data */
234
  userData: {[key: string]: any} = {};
261✔
235

236
  // Fixed properties (change can trigger pipeline rebuild)
237

238
  /** The render pipeline GPU parameters, depth testing etc */
239
  parameters: RenderPipelineParameters;
240

241
  /** The primitive topology */
242
  topology: PrimitiveTopology;
243
  /** Buffer layout */
244
  bufferLayout: BufferLayout[];
245

246
  // Dynamic properties
247

248
  /** Use instanced rendering */
249
  isInstanced: boolean | undefined = undefined;
261✔
250
  /** instance count. `undefined` means not instanced */
251
  instanceCount: number = 0;
261✔
252
  /** Vertex count */
253
  vertexCount: number;
254
  /** Indexed draw count override. Undefined draws the full bound index buffer. */
255
  indexCount: number | undefined;
256
  /** First vertex byte offset for WebGL indexed draws or first vertex for non-indexed draws. */
257
  firstVertex: number;
258
  /** First index element for WebGPU indexed draws. */
259
  firstIndex: number;
260

261
  /** Index buffer */
262
  indexBuffer: Buffer | null = null;
261✔
263
  /** Buffer-valued attributes */
264
  bufferAttributes: Record<string, Buffer> = {};
261✔
265
  /** Constant-valued attributes */
266
  constantAttributes: Record<string, TypedArray> = {};
261✔
267
  /** Bindings (textures, samplers, uniform buffers) */
268
  bindings: Record<string, ModelBinding> = {};
261✔
269

270
  /**
271
   * VertexArray
272
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
273
   * @todo - allow application to define multiple vertex arrays?
274
   * */
275
  vertexArray: VertexArray;
276

277
  /** TransformFeedback, WebGL 2 only. */
278
  transformFeedback: TransformFeedback | null = null;
261✔
279

280
  /** The underlying GPU "program". @note May be recreated if parameters change */
281
  pipeline: RenderPipeline;
282

283
  /** ShaderInputs instance */
284
  // @ts-expect-error Assigned in function called by constructor
285
  shaderInputs: ShaderInputs;
286
  material: Material | null = null;
261✔
287
  // @ts-expect-error Assigned in function called by constructor
288
  _uniformStore: UniformStore;
289

290
  _attributeInfos: Record<string, AttributeInfo> = {};
261✔
291
  _gpuGeometry: GPUGeometry | null = null;
261✔
292
  private props: Required<ModelProps>;
293
  private _dynamicIndexBufferSource: {source: DynamicBuffer; generation: number} | null = null;
261✔
294
  private _dynamicAttributeBufferSources: Record<
295
    number,
296
    {source: DynamicBuffer; generation: number}
297
  > = {};
261✔
298
  private _colorAttachmentFormats: (TextureFormatColor | null)[] | undefined;
299
  private _depthStencilAttachmentFormat: TextureFormatDepthStencil | undefined;
300

301
  _pipelineNeedsUpdate: string | false = 'newly created';
261✔
302
  private _needsRedraw: string | false = 'initializing';
261✔
303
  private _drawBlockedReason: string | false = false;
261✔
304
  private _destroyed = false;
261✔
305

306
  /** "Time" of last draw. Monotonically increasing timestamp */
307
  _lastDrawTimestamp: number = -1;
261✔
308
  private _bindingTable: ShaderBindingDebugRow[] = [];
261✔
309

310
  get [Symbol.toStringTag](): string {
311
    return 'Model';
×
312
  }
313

314
  toString(): string {
315
    return `Model(${this.id})`;
352✔
316
  }
317

318
  constructor(device: Device, props: ModelProps) {
319
    this.props = {...Model.defaultProps, ...props};
261✔
320
    props = this.props;
261✔
321
    this.id = props.id || uid('model');
261✔
322
    this.device = device;
261✔
323

324
    Object.assign(this.userData, props.userData);
261✔
325

326
    this.material = props.material || null;
261✔
327

328
    const platformInfo = getPlatformInfo(device);
261✔
329
    const resolvedPlugins = resolveShaderPlugins(this.props.plugins, platformInfo.shaderLanguage);
261✔
330

331
    // Setup shader module inputs
332
    const shaderInputModules = mergeShaderPluginModules(
261✔
333
      this.props.modules,
334
      resolvedPlugins.modules
335
    );
336
    const moduleMap = Object.fromEntries(shaderInputModules.map(module => [module.name, module]));
367✔
337

338
    const shaderInputs =
339
      props.shaderInputs ||
261✔
340
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
341
    if (props.shaderInputs && resolvedPlugins.modules.length > 0) {
261✔
342
      shaderInputs.addModules(resolvedPlugins.modules);
6✔
343
    }
344
    // @ts-ignore
345
    this.setShaderInputs(shaderInputs);
261✔
346

347
    // Setup shader assembler
348
    const modules = mergeShaderModules(this.props.modules, shaderInputs.getModules());
261✔
349
    const defines = {...resolvedPlugins.defines, ...this.props.defines};
261✔
350

351
    this.props.shaderLayout =
261✔
352
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
454✔
353

354
    const isWebGPU = this.device.type === 'webgpu';
261✔
355

356
    // WebGPU
357
    // TODO - hack to support unified WGSL shader
358
    // TODO - this is wrong, compile a single shader
359
    if (isWebGPU && this.props.source) {
261✔
360
      // WGSL
361
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
45✔
362
        platformInfo,
363
        ...this.props,
364
        modules,
365
        defines,
366
        pluginInjections: resolvedPlugins.injections,
367
        pluginVertexInputs: resolvedPlugins.vertexInputs,
368
        pluginVaryings: resolvedPlugins.varyings
369
      });
370
      this.source = source;
45✔
371
      // @ts-expect-error
372
      this._getModuleUniforms = getUniforms;
45✔
373
      this._bindingTable = bindingTable;
45✔
374
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
375
      const reflectedShaderLayout = (
376
        device as Device & {getShaderLayout?: (source: string) => any}
45✔
377
      ).getShaderLayout?.(this.source);
378
      const inferredShaderLayout = normalizeShaderPluginAttributeNames(
45✔
379
        reflectedShaderLayout,
380
        resolvedPlugins.vertexInputs
381
      );
382
      const shaderLayout = mergeInferredShaderLayout(
45✔
383
        this.props.shaderLayout,
384
        inferredShaderLayout,
385
        Object.keys(resolvedPlugins.vertexInputs)
386
      );
387
      this.props.shaderLayout =
45✔
388
        mergeShaderModuleBindingsIntoLayout(shaderLayout || null, modules) || null;
90!
389
    } else {
390
      // GLSL
391
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
216✔
392
        platformInfo,
393
        ...this.props,
394
        modules,
395
        defines,
396
        pluginInjections: resolvedPlugins.injections,
397
        pluginVertexInputs: resolvedPlugins.vertexInputs,
398
        pluginVaryings: resolvedPlugins.varyings
399
      });
400

401
      this.vs = vs;
216✔
402
      this.fs = fs;
216✔
403
      // @ts-expect-error
404
      this._getModuleUniforms = getUniforms;
216✔
405
      this._bindingTable = [];
216✔
406
    }
407

408
    this.vertexCount = this.props.vertexCount;
261✔
409
    this.indexCount = this.props.indexCount;
261✔
410
    this.firstVertex = this.props.firstVertex;
261✔
411
    this.firstIndex = this.props.firstIndex;
261✔
412
    this.instanceCount = this.props.instanceCount;
261✔
413

414
    this.topology = this.props.topology;
261✔
415
    this.bufferLayout = this.props.bufferLayout;
261✔
416
    this.parameters = this.props.parameters;
261✔
417

418
    // Geometry, if provided, sets topology and vertex cound
419
    if (props.geometry) {
261✔
420
      this.setGeometry(props.geometry);
64✔
421
    }
422

423
    this.pipelineFactory =
261✔
424
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
522✔
425
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
261✔
426

427
    // Create the pipeline
428
    // @note order is important
429
    this.pipeline = this._updatePipeline();
261✔
430

431
    this.vertexArray = device.createVertexArray({
261✔
432
      shaderLayout: this.pipeline.shaderLayout,
433
      bufferLayout: this.pipeline.bufferLayout
434
    });
435

436
    // Now we can apply geometry attributes
437
    if (this._gpuGeometry) {
261✔
438
      this._setGeometryAttributes(this._gpuGeometry);
64✔
439
    }
440

441
    // Apply any dynamic settings that will not trigger pipeline change
442
    if ('isInstanced' in props) {
261!
443
      this.isInstanced = props.isInstanced;
261✔
444
    }
445

446
    if (props.instanceCount) {
261✔
447
      this.setInstanceCount(props.instanceCount);
134✔
448
    }
449
    if (props.vertexCount) {
261✔
450
      this.setVertexCount(props.vertexCount);
204✔
451
    }
452
    if (props.indexBuffer) {
261!
453
      this.setIndexBuffer(props.indexBuffer);
×
454
    }
455
    if (props.attributes) {
261✔
456
      this.setAttributes(props.attributes);
260✔
457
    }
458
    if (props.constantAttributes) {
261!
459
      this.setConstantAttributes(props.constantAttributes);
261✔
460
    }
461
    if (props.bindings) {
261!
462
      this.setBindings(props.bindings);
261✔
463
    }
464
    if (props.transformFeedback) {
261!
465
      this.transformFeedback = props.transformFeedback;
×
466
    }
467
  }
468

469
  destroy(): void {
470
    if (!this._destroyed) {
181✔
471
      // Release pipeline before we destroy the shaders used by the pipeline
472
      this.pipelineFactory.release(this.pipeline);
180✔
473
      // Release the shaders
474
      this.shaderFactory.release(this.pipeline.vs);
180✔
475
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
180✔
476
        this.shaderFactory.release(this.pipeline.fs);
135✔
477
      }
478
      this._uniformStore.destroy();
180✔
479
      // TODO - mark resource as managed and destroyIfManaged() ?
480
      this._gpuGeometry?.destroy();
180✔
481
      this._destroyed = true;
180✔
482
    }
483
  }
484

485
  // Draw call
486

487
  /** Query redraw status. Clears the status. */
488
  needsRedraw(): false | string {
489
    // Catch any writes to already bound resources
490
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
6!
491
      this.setNeedsRedraw('contents of bound textures or buffers updated');
6✔
492
    }
493
    const needsRedraw = this._needsRedraw;
6✔
494
    this._needsRedraw = false;
6✔
495
    return needsRedraw;
6✔
496
  }
497

498
  /** Mark the model as needing a redraw */
499
  setNeedsRedraw(reason: string): void {
500
    this._needsRedraw ||= reason;
2,478✔
501
  }
502

503
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
504
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
505
    return this._bindingTable;
2✔
506
  }
507

508
  /**
509
   * Updates uniforms and pipeline state before opening a render pass.
510
   *
511
   * @param commandEncoder - Encoder that should own any GPU uploads emitted
512
   * during draw preparation.
513
   */
514
  predraw(commandEncoder: CommandEncoder): void {
515
    // Update uniform buffers if needed
516
    this._syncDynamicBuffers();
199✔
517
    this.updateShaderInputs(commandEncoder);
199✔
518
    this.material?.updateShaderInputs(commandEncoder);
199✔
519
    // Check if the pipeline is invalidated
520
    this.pipeline = this._updatePipeline();
199✔
521
  }
522

523
  /**
524
   * Issue one draw call.
525
   * @param renderPass - render pass to draw into
526
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
527
   */
528
  draw(renderPass: RenderPass): boolean {
529
    if (this._drawBlockedReason && !this._pipelineNeedsUpdate) {
178✔
530
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${this._drawBlockedReason}`)();
1✔
531
      return false;
1✔
532
    }
533

534
    const loadingBinding = this._areBindingsLoading();
177✔
535
    if (loadingBinding) {
177✔
536
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
1✔
537
      return false;
1✔
538
    }
539

540
    this._syncAttachmentFormats(renderPass);
176✔
541

542
    try {
176✔
543
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
176✔
544
      if (this.device.type === 'webgpu') {
176✔
545
        // WebGPU uploads cannot be encoded once the render pass is already open.
546
        // Keep the implicit draw() path working for existing callers by falling
547
        // back to immediate writes here; callers that need upload ordering
548
        // across multiple draws/viewports must call predraw(commandEncoder)
549
        // before beginRenderPass().
550
        this.updateShaderInputs();
25✔
551
        this.material?.updateShaderInputs();
25✔
552
        this._syncDynamicBuffers();
25✔
553
        this.pipeline = this._updatePipeline();
25✔
554
      } else {
555
        this.predraw(this.device.commandEncoder);
151✔
556
      }
557
    } finally {
558
      renderPass.popDebugGroup();
176✔
559
    }
560

561
    let drawSuccess: boolean;
562
    let pipelineErrored = this.pipeline.isErrored;
176✔
563
    try {
176✔
564
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
176✔
565
      this._logDrawCallStart();
176✔
566

567
      // Update the pipeline if invalidated
568
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
569
      // Application can call Model.predraw() to avoid this.
570
      this.pipeline = this._updatePipeline();
176✔
571
      pipelineErrored = this.pipeline.isErrored;
176✔
572

573
      if (pipelineErrored) {
176✔
574
        log.info(
1✔
575
          LOG_DRAW_PRIORITY,
576
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
577
        )();
578
        drawSuccess = false;
1✔
579
      } else {
580
        const drawValidationError = this.vertexArray.getDrawValidationError();
175✔
581
        if (drawValidationError) {
175!
582
          log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${drawValidationError}`)();
×
583
          this._drawBlockedReason = drawValidationError;
×
584
          drawSuccess = false;
×
585
        } else {
586
          const shaderLayout = this._getCurrentShaderLayout();
175✔
587
          const syncBindings = this._getBindings(shaderLayout);
175✔
588
          const syncBindGroups = this._getBindGroups(shaderLayout, syncBindings);
175✔
589

590
          const {indexBuffer} = this.vertexArray;
175✔
591
          const indexCount = indexBuffer
175✔
592
            ? (this.indexCount ??
28✔
593
              indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2))
12!
594
            : undefined;
595

596
          renderPass.setPipeline(this.pipeline);
175✔
597
          renderPass.setBindings(syncBindGroups, {
175✔
598
            _bindGroupCacheKeys: this._getBindGroupCacheKeys()
599
          });
600
          renderPass.setVertexArray(this.vertexArray);
175✔
601
          drawSuccess = renderPass.draw({
175✔
602
            isInstanced: this.isInstanced,
603
            vertexCount: this.vertexCount,
604
            instanceCount: this.instanceCount,
605
            indexCount,
606
            firstVertex: this.firstVertex,
607
            firstIndex: this.firstIndex,
608
            transformFeedback: this.transformFeedback || undefined,
261✔
609
            uniforms: this.props.uniforms,
610
            // WebGL shares underlying cached programs even for models that have different
611
            // parameters and topology, so those compatibility overrides remain per draw.
612
            parameters: this.parameters,
613
            topology: this.topology
614
          });
615
        }
616
      }
617
    } finally {
618
      renderPass.popDebugGroup();
176✔
619
      this._logDrawCallEnd();
176✔
620
    }
621
    this._logFramebuffer(renderPass);
176✔
622

623
    // Update needsRedraw flag
624
    if (drawSuccess) {
176✔
625
      this._lastDrawTimestamp = this.device.timestamp;
175✔
626
      this._needsRedraw = false;
175✔
627
    } else if (pipelineErrored) {
1!
628
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
1✔
629
      this._drawBlockedReason = PIPELINE_INITIALIZATION_FAILED;
1✔
630
    } else if (this._drawBlockedReason) {
×
631
      this._needsRedraw = this._drawBlockedReason;
×
632
    } else {
633
      this._needsRedraw = 'waiting for resource initialization';
×
634
    }
635
    return drawSuccess;
176✔
636
  }
637

638
  // Update fixed fields (can trigger pipeline rebuild)
639

640
  /**
641
   * Updates the optional geometry
642
   * Geometry, set topology and bufferLayout
643
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
644
   */
645
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
646
    this._gpuGeometry?.destroy();
64✔
647
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
64✔
648
    if (gpuGeometry) {
64!
649
      this.setTopology(gpuGeometry.topology || 'triangle-list');
64!
650
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
64✔
651
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
64✔
652
        gpuGeometry.bufferLayout,
653
        this.bufferLayout
654
      );
655
      if (this.vertexArray) {
64!
656
        this._setGeometryAttributes(gpuGeometry);
×
657
      }
658
    }
659
    this._gpuGeometry = gpuGeometry;
64✔
660
  }
661

662
  /**
663
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
664
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
665
   */
666
  setTopology(topology: PrimitiveTopology): void {
667
    if (topology !== this.topology) {
67✔
668
      this.topology = topology;
36✔
669
      this._setPipelineNeedsUpdate('topology');
36✔
670
    }
671
  }
672

673
  /**
674
   * Updates the buffer layout.
675
   * @note Triggers a pipeline rebuild / pipeline cache fetch
676
   */
677
  setBufferLayout(bufferLayout: BufferLayout[]): void {
678
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
3✔
679
    const nextBufferLayout = this._gpuGeometry
3✔
680
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
681
      : bufferLayout;
682
    if (deepEqual(nextBufferLayout, this.bufferLayout, -1)) {
3✔
683
      return;
2✔
684
    }
685

686
    this.bufferLayout = nextBufferLayout;
1✔
687
    this._setPipelineNeedsUpdate('bufferLayout');
1✔
688

689
    // Recreate the pipeline
690
    this.pipeline = this._updatePipeline();
1✔
691

692
    // vertex array needs to be updated if we update buffer layout,
693
    // but not if we update parameters
694
    this.vertexArray = this.device.createVertexArray({
1✔
695
      shaderLayout: this.pipeline.shaderLayout,
696
      bufferLayout: this.pipeline.bufferLayout
697
    });
698

699
    // Reapply geometry attributes to the new vertex array
700
    if (this._gpuGeometry) {
1!
701
      this._setGeometryAttributes(this._gpuGeometry);
1✔
702
    }
703
  }
704

705
  /**
706
   * Set GPU parameters.
707
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
708
   * @param parameters
709
   */
710
  setParameters(parameters: RenderPipelineParameters) {
711
    if (!deepEqual(parameters, this.parameters, 2)) {
12!
712
      this.parameters = parameters;
12✔
713
      this._setPipelineNeedsUpdate('parameters');
12✔
714
    }
715
  }
716

717
  // Update dynamic fields
718

719
  /**
720
   * Updates the instance count (used in draw calls)
721
   * @note Any attributes with stepMode=instance need to be at least this big
722
   */
723
  setInstanceCount(instanceCount: number): void {
724
    this.instanceCount = instanceCount;
164✔
725
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
726
    // but deck.gl sets instanceCount even for models that are not instanced.
727
    if (this.isInstanced === undefined && instanceCount > 0) {
164✔
728
      this.isInstanced = true;
141✔
729
    }
730
    this.setNeedsRedraw('instanceCount');
164✔
731
  }
732

733
  /**
734
   * Updates the vertex count (used in draw calls)
735
   * @note Any attributes with stepMode=vertex need to be at least this big
736
   */
737
  setVertexCount(vertexCount: number): void {
738
    this.vertexCount = vertexCount;
216✔
739
    this.setNeedsRedraw('vertexCount');
216✔
740
  }
741

742
  /** Updates the indexed draw count override. */
743
  setIndexCount(indexCount: number | undefined): void {
744
    this.indexCount = indexCount;
11✔
745
    this.setNeedsRedraw('indexCount');
11✔
746
  }
747

748
  /** Updates the first indexed/non-indexed draw offsets. */
749
  setDrawOffsets({firstVertex, firstIndex}: {firstVertex: number; firstIndex: number}): void {
750
    this.firstVertex = firstVertex;
1✔
751
    this.firstIndex = firstIndex;
1✔
752
    this.setNeedsRedraw('drawOffsets');
1✔
753
  }
754

755
  /** Set the shader inputs */
756
  setShaderInputs(shaderInputs: ShaderInputs): void {
757
    this.shaderInputs = shaderInputs;
261✔
758
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
261✔
759
    // Create uniform buffer bindings for all modules that actually have uniforms
760
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
261✔
761
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
476✔
762
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
147✔
763
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
147✔
764
      }
765
    }
766
    this.setNeedsRedraw('shaderInputs');
261✔
767
  }
768

769
  setMaterial(material: Material | null): void {
770
    this.material = material;
×
771
    this.setNeedsRedraw('material');
×
772
  }
773

774
  /** Update uniform buffers from the model's shader inputs */
775
  /**
776
   * Flushes current shader-input values into managed uniform buffers and
777
   * non-material bindings.
778
   *
779
   * @param commandEncoder - Optional encoder used to order uniform uploads with
780
   * subsequent draw commands.
781
   */
782
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
783
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
224✔
784
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
224✔
785
    // TODO - this is already tracked through buffer/texture update times?
786
    this.setNeedsRedraw('shaderInputs');
224✔
787
  }
788

789
  /**
790
   * Sets bindings (textures, samplers, uniform buffers)
791
   */
792
  setBindings(bindings: Record<string, ModelBinding>): void {
793
    Object.assign(this.bindings, bindings);
550✔
794
    this.setNeedsRedraw('bindings');
550✔
795
  }
796

797
  /**
798
   * Updates optional transform feedback. WebGL only.
799
   */
800
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
801
    this.transformFeedback = transformFeedback;
88✔
802
    this.setNeedsRedraw('transformFeedback');
88✔
803
  }
804

805
  /**
806
   * Sets the index buffer
807
   * @todo - how to unset it if we change geometry?
808
   */
809
  setIndexBuffer(indexBuffer: ModelBuffer | null): void {
810
    const resolvedIndexBuffer =
811
      indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
76!
812
    this.indexBuffer = resolvedIndexBuffer;
76✔
813
    this._dynamicIndexBufferSource =
76✔
814
      indexBuffer instanceof DynamicBuffer
76!
815
        ? {source: indexBuffer, generation: indexBuffer.generation}
816
        : null;
817
    this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
76✔
818
    this.setNeedsRedraw('indexBuffer');
76✔
819
  }
820

821
  /**
822
   * Sets attributes (buffers)
823
   * @note Overrides any attributes previously set with the same name
824
   */
825
  setAttributes(buffers: Record<string, ModelBuffer>, options?: {disableWarnings?: boolean}): void {
826
    this._drawBlockedReason = false;
480✔
827
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
480✔
828
    if (buffers['indices']) {
480!
829
      log.warn(
×
830
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
831
      )();
832
    }
833

834
    // ensure bufferLayout order matches source layout so we bind
835
    // the correct buffers to the correct indices in webgpu.
836
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
480✔
837
      this.pipeline.shaderLayout,
838
      this.bufferLayout
839
    );
840
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
480✔
841

842
    // Check if all buffers have a layout
843
    for (const [bufferName, buffer] of Object.entries(buffers)) {
480✔
844
      const resolvedBuffer = buffer instanceof DynamicBuffer ? buffer.buffer : buffer;
580✔
845
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
580✔
846
      if (!bufferLayout) {
580!
847
        if (!disableWarnings) {
×
848
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
849
        }
850
        continue; // eslint-disable-line no-continue
×
851
      }
852

853
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
854
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
855
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
580✔
856
      let set = false;
580✔
857
      for (const attributeName of attributeNames) {
580✔
858
        const attributeInfo = this._attributeInfos[attributeName];
791✔
859
        if (attributeInfo) {
791✔
860
          const bufferSlot =
861
            this.device.type === 'webgpu'
747✔
862
              ? this.vertexArray.getBufferSlot(attributeInfo.bufferName)
863
              : attributeInfo.location;
864
          if (bufferSlot === null) {
747!
865
            if (!disableWarnings) {
×
866
              log.warn(
×
867
                `Model(${this.id}): Missing vertex array slot for buffer "${attributeInfo.bufferName}".`
868
              )();
869
            }
870
            continue; // eslint-disable-line no-continue
×
871
          }
872

873
          this.vertexArray.setBuffer(bufferSlot, resolvedBuffer);
747✔
874
          if (buffer instanceof DynamicBuffer) {
747✔
875
            this._dynamicAttributeBufferSources[bufferSlot] = {
41✔
876
              source: buffer,
877
              generation: buffer.generation
878
            };
879
          } else {
880
            delete this._dynamicAttributeBufferSources[bufferSlot];
706✔
881
          }
882
          set = true;
747✔
883
        }
884
      }
885
      if (!set && !disableWarnings) {
580✔
886
        log.warn(
2✔
887
          `Model(${this.id}): Ignoring buffer "${resolvedBuffer.id}" for unknown attribute "${bufferName}"`
888
        )();
889
      }
890
    }
891
    this.setNeedsRedraw('attributes');
480✔
892
  }
893

894
  /**
895
   * Sets constant attributes
896
   * @note Overrides any attributes previously set with the same name
897
   * Constant attributes are only supported in WebGL, not in WebGPU
898
   * Any attribute that is disabled in the current vertex array object
899
   * is read from the context's global constant value for that attribute location.
900
   * @param constantAttributes
901
   */
902
  setConstantAttributes(
903
    attributes: Record<string, TypedArray>,
904
    options?: {disableWarnings?: boolean}
905
  ): void {
906
    for (const [attributeName, value] of Object.entries(attributes)) {
261✔
907
      const attributeInfo = this._attributeInfos[attributeName];
×
908
      if (attributeInfo) {
×
909
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
910
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
911
        log.warn(
×
912
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
913
        )();
914
      }
915
    }
916
    this.setNeedsRedraw('constants');
261✔
917
  }
918

919
  // INTERNAL METHODS
920

921
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
922
  _areBindingsLoading(): string | false {
923
    for (const binding of Object.values(this.bindings)) {
180✔
924
      if (isTextureBindingSource(binding) && !binding.isReady) {
178✔
925
        return binding.id;
1✔
926
      }
927
    }
928
    for (const binding of Object.values(this.material?.bindings || {})) {
179✔
929
      if (isTextureBindingSource(binding) && !binding.isReady) {
×
930
        return binding.id;
×
931
      }
932
    }
933
    return false;
179✔
934
  }
935

936
  /**
937
   * Resolves ready model bindings for the current shader layout.
938
   * @param shaderLayout Reflected bindings used to select copied or external texture resolution.
939
   */
940
  _getBindings(
941
    shaderLayout: AnyShaderLayout = this._getCurrentShaderLayout()
186✔
942
  ): Record<string, Binding> {
943
    const validBindings: Record<string, Binding> = {};
186✔
944

945
    for (const [name, binding] of Object.entries(this.bindings)) {
186✔
946
      const resolvedBinding = resolveModelBinding(name, binding, shaderLayout);
230✔
947
      if (resolvedBinding) {
230!
948
        validBindings[name] = resolvedBinding;
230✔
949
      }
950
    }
951

952
    return validBindings;
186✔
953
  }
954

955
  /**
956
   * Groups resolved model and material bindings for the current shader layout.
957
   * @param shaderLayout Reflected bindings used to group logical binding names.
958
   * @param bindings Model bindings already resolved for this draw preparation.
959
   */
960
  _getBindGroups(
961
    shaderLayout: AnyShaderLayout = this._getCurrentShaderLayout(),
179✔
962
    bindings: Record<string, Binding> = this._getBindings(shaderLayout)
179✔
963
  ): BindingsByGroup {
964
    const bindGroups = shaderLayout.bindings.length
179✔
965
      ? normalizeBindingsByGroup(shaderLayout, bindings)
966
      : {0: bindings};
967

968
    if (!this.material) {
179!
969
      return bindGroups;
179✔
970
    }
971

UNCOV
972
    for (const [groupKey, groupBindings] of Object.entries(
×
973
      this.material.getBindingsByGroup(shaderLayout)
974
    )) {
UNCOV
975
      const group = Number(groupKey);
×
UNCOV
976
      bindGroups[group] = {
×
977
        ...(bindGroups[group] || {}),
×
978
        ...groupBindings
979
      };
980
    }
981

UNCOV
982
    return bindGroups;
×
983
  }
984

985
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
986
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
178✔
987
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
178!
988
  }
989

990
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
991
  _getBindingsUpdateTimestamp(): number {
992
    let timestamp = 0;
6✔
993
    if (this._dynamicIndexBufferSource) {
6!
994
      timestamp = Math.max(timestamp, this._dynamicIndexBufferSource.source.updateTimestamp);
×
995
    }
996
    for (const entry of Object.values(this._dynamicAttributeBufferSources)) {
6✔
997
      timestamp = Math.max(timestamp, entry.source.updateTimestamp);
6✔
998
    }
999
    for (const binding of Object.values(this.bindings)) {
6✔
1000
      if (binding instanceof TextureView) {
8!
1001
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
1002
      } else if (
1003
        binding instanceof Buffer ||
8✔
1004
        binding instanceof Texture ||
1005
        binding instanceof ExternalTexture
1006
      ) {
1007
        timestamp = Math.max(timestamp, binding.updateTimestamp);
6✔
1008
      } else if (binding instanceof DynamicBuffer) {
2!
1009
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
1010
      } else if (isTextureBindingSource(binding)) {
2!
1011
        timestamp = binding.isReady
2!
1012
          ? Math.max(timestamp, binding.updateTimestamp)
1013
          : // The texture will become available in the future
1014
            Infinity;
1015
      } else if (isBufferRangeBinding(binding)) {
×
1016
        timestamp = Math.max(
×
1017
          timestamp,
1018
          binding.buffer instanceof DynamicBuffer
×
1019
            ? binding.buffer.updateTimestamp
1020
            : binding.buffer.updateTimestamp
1021
        );
1022
      }
1023
    }
1024
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
6✔
1025
  }
1026

1027
  /**
1028
   * Updates the optional geometry attributes
1029
   * Geometry, sets several attributes, indexBuffer, and also vertex count
1030
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
1031
   */
1032
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
1033
    // Filter geometry attribute so that we don't issue warnings for unused attributes
1034
    const attributes = {...gpuGeometry.attributes};
65✔
1035
    for (const [attributeName] of Object.entries(attributes)) {
65✔
1036
      if (
65!
1037
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
215✔
1038
        attributeName !== 'positions'
1039
      ) {
1040
        delete attributes[attributeName];
65✔
1041
      }
1042
    }
1043

1044
    // TODO - delete previous geometry?
1045
    this.vertexCount = gpuGeometry.vertexCount;
65✔
1046
    this.setIndexBuffer(gpuGeometry.indices || null);
65✔
1047
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
65✔
1048
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
65✔
1049

1050
    this.setNeedsRedraw('geometry attributes');
65✔
1051
  }
1052

1053
  /** Mark pipeline as needing update */
1054
  _setPipelineNeedsUpdate(reason: string): void {
1055
    this._pipelineNeedsUpdate ||= reason;
64✔
1056
    this._drawBlockedReason = false;
64✔
1057
    this.setNeedsRedraw(reason);
64✔
1058
  }
1059

1060
  /** Update pipeline if needed */
1061
  _updatePipeline(): RenderPipeline {
1062
    if (this._pipelineNeedsUpdate) {
665✔
1063
      let prevShaderVs: Shader | null = null;
290✔
1064
      let prevShaderFs: Shader | null = null;
290✔
1065
      if (this.pipeline) {
290✔
1066
        log.log(
29✔
1067
          1,
1068
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
1069
        )();
1070
        prevShaderVs = this.pipeline.vs;
29✔
1071
        prevShaderFs = this.pipeline.fs;
29✔
1072
      }
1073

1074
      this._pipelineNeedsUpdate = false;
290✔
1075

1076
      const vs = this.shaderFactory.createShader({
290✔
1077
        id: `${this.id}-vertex`,
1078
        stage: 'vertex',
1079
        source: this.source || this.vs,
514✔
1080
        debugShaders: this.props.debugShaders
1081
      });
1082

1083
      let fs: Shader | null = null;
290✔
1084
      if (this.source) {
290✔
1085
        fs = vs;
66✔
1086
      } else if (this.fs) {
224!
1087
        fs = this.shaderFactory.createShader({
224✔
1088
          id: `${this.id}-fragment`,
1089
          stage: 'fragment',
1090
          source: this.source || this.fs,
448✔
1091
          debugShaders: this.props.debugShaders
1092
        });
1093
      }
1094

1095
      this.pipeline = this.pipelineFactory.createRenderPipeline({
290✔
1096
        ...this.props,
1097
        bindings: undefined,
1098
        bufferLayout: this.bufferLayout,
1099
        colorAttachmentFormats: this._colorAttachmentFormats,
1100
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
1101
        topology: this.topology,
1102
        parameters: this.parameters,
1103
        bindGroups: undefined,
1104
        vs,
1105
        fs
1106
      });
1107

1108
      this._attributeInfos = getAttributeInfosFromLayouts(
290✔
1109
        this.pipeline.shaderLayout,
1110
        this.bufferLayout
1111
      );
1112

1113
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
290✔
1114
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
290✔
1115
        this.shaderFactory.release(prevShaderFs);
8✔
1116
      }
1117
    }
1118
    return this.pipeline;
665✔
1119
  }
1120

1121
  /** Throttle draw call logging */
1122
  _lastLogTime = 0;
261✔
1123
  _logOpen = false;
261✔
1124

1125
  _logDrawCallStart(): void {
1126
    // IF level is 4 or higher, log every frame.
1127
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
176!
1128
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
176!
1129
      return;
176✔
1130
    }
1131

1132
    this._lastLogTime = Date.now();
×
1133
    this._logOpen = true;
×
1134

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

1138
  _logDrawCallEnd(): void {
1139
    if (this._logOpen) {
176!
1140
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1141

1142
      // log.table(logLevel, attributeTable)();
1143
      // log.table(logLevel, uniformTable)();
1144
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1145

1146
      const uniformTable = this.shaderInputs.getDebugTable();
×
1147
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1148

1149
      const attributeTable = this._getAttributeDebugTable();
×
1150
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1151
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1152

1153
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1154
      this._logOpen = false;
×
1155
    }
1156
  }
1157

1158
  protected _drawCount = 0;
261✔
1159
  _logFramebuffer(renderPass: RenderPass): void {
1160
    const debugFramebuffers = this.device.props.debugFramebuffers;
176✔
1161
    this._drawCount++;
176✔
1162
    // Update first 3 frames and then every 60 frames
1163
    if (!debugFramebuffers) {
176!
1164
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
1165
      return;
176✔
1166
    }
1167
    const framebuffer = renderPass.props.framebuffer;
×
1168
    debugFramebuffer(renderPass, framebuffer, {
×
1169
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
1170
      minimap: true
1171
    });
1172
  }
1173

1174
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1175
    const table: Record<string, Record<string, unknown>> = {};
×
1176
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
1177
      const values = this.vertexArray.attributes[attributeInfo.location];
×
1178
      table[attributeInfo.location] = {
×
1179
        name,
1180
        type: attributeInfo.shaderType,
1181
        values: values
×
1182
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
1183
          : 'null'
1184
      };
1185
    }
1186
    if (this.vertexArray.indexBuffer) {
×
1187
      const {indexBuffer} = this.vertexArray;
×
1188
      const values =
1189
        indexBuffer.indexType === 'uint32'
×
1190
          ? new Uint32Array(indexBuffer.debugData)
1191
          : new Uint16Array(indexBuffer.debugData);
1192
      table['indices'] = {
×
1193
        name: 'indices',
1194
        type: indexBuffer.indexType,
1195
        values: values.toString()
1196
      };
1197
    }
1198
    return table;
×
1199
  }
1200

1201
  // TODO - fix typing of luma data types
1202
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1203
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1204
    const typedArray =
1205
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
1206
    return typedArray.toString();
×
1207
  }
1208

1209
  private _getNonMaterialBindings(
1210
    bindings: Record<string, ModelBinding>
1211
  ): Record<string, ModelBinding> {
1212
    if (!this.material) {
224!
1213
      return bindings;
224✔
1214
    }
1215

1216
    const filteredBindings: Record<string, ModelBinding> = {};
×
1217
    for (const [name, binding] of Object.entries(bindings)) {
×
1218
      if (!this.material.ownsBinding(name)) {
×
1219
        filteredBindings[name] = binding;
×
1220
      }
1221
    }
1222
    return filteredBindings;
×
1223
  }
1224

1225
  /** Returns the current reflected shader layout or the pre-reflection empty layout. */
1226
  private _getCurrentShaderLayout(): AnyShaderLayout {
1227
    return this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
185!
1228
  }
1229

1230
  private _syncDynamicBuffers(): void {
1231
    if (
224!
1232
      this._dynamicIndexBufferSource &&
224!
1233
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1234
    ) {
1235
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
1236
      this.indexBuffer = resolvedIndexBuffer;
×
1237
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
1238
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
1239
      this.setNeedsRedraw('dynamic index buffer');
×
1240
    }
1241

1242
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
224✔
1243
      if (entry.generation !== entry.source.generation) {
6!
1244
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
1245
        entry.generation = entry.source.generation;
×
1246
        this.setNeedsRedraw('dynamic attribute buffer');
×
1247
      }
1248
    }
1249
  }
1250
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1251
    if (this.device.type !== 'webgpu') {
176✔
1252
      return;
151✔
1253
    }
1254

1255
    const framebuffer =
1256
      (
1257
        renderPass as RenderPass & {
25✔
1258
          framebuffer?: {
1259
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1260
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1261
          };
1262
        }
1263
      ).framebuffer || renderPass.props.framebuffer;
1264
    const renderBundleProps = renderPass.props as RenderPass['props'] & {
176✔
1265
      colorAttachmentFormats?: (TextureFormatColor | null)[];
1266
      depthStencilAttachmentFormat?: TextureFormatDepthStencil | false;
1267
    };
1268

1269
    const nextColorAttachmentFormats =
1270
      renderBundleProps.colorAttachmentFormats ??
176✔
1271
      framebuffer?.colorAttachments?.map(colorAttachment =>
1272
        asColorAttachmentFormat(colorAttachment?.texture?.format)
24✔
1273
      );
1274
    const nextDepthStencilAttachmentFormat =
1275
      renderBundleProps.depthStencilAttachmentFormat === false
176!
1276
        ? undefined
1277
        : (renderBundleProps.depthStencilAttachmentFormat ??
49✔
1278
          asDepthStencilAttachmentFormat(framebuffer?.depthStencilAttachment?.texture?.format));
1279

1280
    if (
176✔
1281
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
186✔
1282
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1283
    ) {
1284
      this._colorAttachmentFormats = nextColorAttachmentFormats;
15✔
1285
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
15✔
1286
      this._setPipelineNeedsUpdate('attachment formats');
15✔
1287
    }
1288
  }
1289
}
1290

1291
// HELPERS
1292

1293
function normalizeShaderPluginAttributeNames(
1294
  shaderLayout: ShaderLayout | null | undefined,
1295
  vertexInputs: Record<string, AttributeShaderType>
1296
): ShaderLayout | null | undefined {
1297
  if (!shaderLayout || Object.keys(vertexInputs).length === 0) {
45✔
1298
    return shaderLayout;
44✔
1299
  }
1300

1301
  return {
1✔
1302
    ...shaderLayout,
1303
    attributes: shaderLayout.attributes.map(attribute => {
1304
      const publicName = attribute.name.startsWith('_luma_')
1!
1305
        ? attribute.name.slice('_luma_'.length)
1306
        : null;
1307
      return publicName && vertexInputs[publicName] ? {...attribute, name: publicName} : attribute;
1!
1308
    })
1309
  };
1310
}
1311

1312
/**
1313
 * Resolves one model binding against the current shader layout.
1314
 * @param bindingName Logical model binding name.
1315
 * @param binding Model binding or deferred engine binding source.
1316
 * @param shaderLayout Reflected bindings used to select copied or external texture resolution.
1317
 * @returns Concrete core binding, or `null` while a deferred source is unavailable.
1318
 */
1319
function resolveModelBinding(
1320
  bindingName: string,
1321
  binding: ModelBinding,
1322
  shaderLayout: AnyShaderLayout
1323
): Binding | null {
1324
  if (isTextureBindingSource(binding)) {
230✔
1325
    const bindingLayout = getTextureBindingLayout(shaderLayout, bindingName, {
1✔
1326
      fallbackGroup: 0
1327
    });
1328
    return bindingLayout ? binding.resolveTextureBinding(bindingLayout) : null;
1!
1329
  }
1330

1331
  if (binding instanceof DynamicBuffer) {
229✔
1332
    return binding.buffer;
23✔
1333
  }
1334

1335
  if (isBufferRangeBinding(binding)) {
206✔
1336
    return resolveBufferRangeBinding(binding);
7✔
1337
  }
1338

1339
  return binding;
199✔
1340
}
1341
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1342
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
24!
1343
}
1344

1345
function asDepthStencilAttachmentFormat(
1346
  format?: string | null
1347
): TextureFormatDepthStencil | undefined {
1348
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
24✔
1349
}
1350

1351
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1352
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
36✔
1353
}
1354

1355
/** Create a shadertools platform info from the Device */
1356
export function getPlatformInfo(device: Device): PlatformInfo {
1357
  return {
261✔
1358
    type: device.type,
1359
    shaderLanguage: device.info.shadingLanguage,
1360
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1361
    gpu: device.info.gpu,
1362
    limits: device.limits as unknown as Record<string, number | undefined>,
1363
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1364
    features: device.features as unknown as Set<DeviceFeature>
1365
  };
1366
}
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