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

visgl / luma.gl / 27877741225

20 Jun 2026 04:53PM UTC coverage: 70.762% (+0.03%) from 70.733%
27877741225

push

github

web-flow
feat(experimental): HTMLTexture via HTML-in-Canvas (#2674)

9977 of 15874 branches covered (62.85%)

Branch coverage included in aggregate %.

76 of 136 new or added lines in 8 files covered. (55.88%)

131 existing lines in 5 files now uncovered.

20249 of 26841 relevant lines covered (75.44%)

4021.12 hits per line

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

77.94
/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;
127✔
82
const LOG_DRAW_TIMEOUT = 10000;
127✔
83
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
127✔
84
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
127✔
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> = {
127✔
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} = {};
251✔
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;
251✔
250
  /** instance count. `undefined` means not instanced */
251
  instanceCount: number = 0;
251✔
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;
251✔
263
  /** Buffer-valued attributes */
264
  bufferAttributes: Record<string, Buffer> = {};
251✔
265
  /** Constant-valued attributes */
266
  constantAttributes: Record<string, TypedArray> = {};
251✔
267
  /** Bindings (textures, samplers, uniform buffers) */
268
  bindings: Record<string, ModelBinding> = {};
251✔
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;
251✔
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;
251✔
287
  // @ts-expect-error Assigned in function called by constructor
288
  _uniformStore: UniformStore;
289

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

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

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

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

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

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

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

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

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

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

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

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

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

354
    const isWebGPU = this.device.type === 'webgpu';
251✔
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) {
251✔
360
      // WGSL
361
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
39✔
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;
39✔
371
      // @ts-expect-error
372
      this._getModuleUniforms = getUniforms;
39✔
373
      this._bindingTable = bindingTable;
39✔
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}
39✔
377
      ).getShaderLayout?.(this.source);
378
      const inferredShaderLayout = normalizeShaderPluginAttributeNames(
39✔
379
        reflectedShaderLayout,
380
        resolvedPlugins.vertexInputs
381
      );
382
      const shaderLayout = mergeInferredShaderLayout(
39✔
383
        this.props.shaderLayout,
384
        inferredShaderLayout,
385
        Object.keys(resolvedPlugins.vertexInputs)
386
      );
387
      this.props.shaderLayout =
39✔
388
        mergeShaderModuleBindingsIntoLayout(shaderLayout || null, modules) || null;
78!
389
    } else {
390
      // GLSL
391
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
212✔
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;
212✔
402
      this.fs = fs;
212✔
403
      // @ts-expect-error
404
      this._getModuleUniforms = getUniforms;
212✔
405
      this._bindingTable = [];
212✔
406
    }
407

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

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

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

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

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

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

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

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

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

469
  destroy(): void {
470
    if (!this._destroyed) {
171✔
471
      // Release pipeline before we destroy the shaders used by the pipeline
472
      this.pipelineFactory.release(this.pipeline);
170✔
473
      // Release the shaders
474
      this.shaderFactory.release(this.pipeline.vs);
170✔
475
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
170✔
476
        this.shaderFactory.release(this.pipeline.fs);
131✔
477
      }
478
      this._uniformStore.destroy();
170✔
479
      // TODO - mark resource as managed and destroyIfManaged() ?
480
      this._gpuGeometry?.destroy();
170✔
481
      this._destroyed = true;
170✔
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) {
7!
491
      this.setNeedsRedraw('contents of bound textures or buffers updated');
7✔
492
    }
493
    const needsRedraw = this._needsRedraw;
7✔
494
    this._needsRedraw = false;
7✔
495
    return needsRedraw;
7✔
496
  }
497

498
  /** Mark the model as needing a redraw */
499
  setNeedsRedraw(reason: string): void {
500
    this._needsRedraw ||= reason;
2,310✔
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();
168✔
517
    this.updateShaderInputs(commandEncoder);
168✔
518
    this.material?.updateShaderInputs(commandEncoder);
168✔
519
    // Check if the pipeline is invalidated
520
    this.pipeline = this._updatePipeline();
168✔
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) {
156✔
530
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${this._drawBlockedReason}`)();
1✔
531
      return false;
1✔
532
    }
533

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

540
    this._syncAttachmentFormats(renderPass);
154✔
541

542
    try {
154✔
543
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
154✔
544
      if (this.device.type === 'webgpu') {
154✔
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();
14✔
551
        this.material?.updateShaderInputs();
14✔
552
        this._syncDynamicBuffers();
14✔
553
        this.pipeline = this._updatePipeline();
14✔
554
      } else {
555
        this.predraw(this.device.commandEncoder);
140✔
556
      }
557
    } finally {
558
      renderPass.popDebugGroup();
154✔
559
    }
560

561
    let drawSuccess: boolean;
562
    let pipelineErrored = this.pipeline.isErrored;
154✔
563
    try {
154✔
564
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
154✔
565
      this._logDrawCallStart();
154✔
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();
154✔
571
      pipelineErrored = this.pipeline.isErrored;
154✔
572

573
      if (pipelineErrored) {
154✔
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();
153✔
581
        if (drawValidationError) {
153!
UNCOV
582
          log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${drawValidationError}`)();
×
UNCOV
583
          this._drawBlockedReason = drawValidationError;
×
UNCOV
584
          drawSuccess = false;
×
585
        } else {
586
          const shaderLayout = this._getCurrentShaderLayout();
153✔
587
          const syncBindings = this._getBindings(shaderLayout);
153✔
588
          const syncBindGroups = this._getBindGroups(shaderLayout, syncBindings);
153✔
589

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

596
          drawSuccess = this.pipeline.draw({
153✔
597
            renderPass,
598
            vertexArray: this.vertexArray,
599
            isInstanced: this.isInstanced,
600
            vertexCount: this.vertexCount,
601
            instanceCount: this.instanceCount,
602
            indexCount,
603
            firstVertex: this.firstVertex,
604
            firstIndex: this.firstIndex,
605
            transformFeedback: this.transformFeedback || undefined,
217✔
606
            // Pipelines may be shared across models when caching is enabled, so bindings
607
            // and WebGL uniforms must be supplied on every draw instead of being stored
608
            // on the pipeline instance.
609
            bindings: syncBindings,
610
            bindGroups: syncBindGroups,
611
            _bindGroupCacheKeys: this._getBindGroupCacheKeys(),
612
            uniforms: this.props.uniforms,
613
            // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
614
            // so we must provide our unique parameters to each draw
615
            // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
616
            parameters: this.parameters,
617
            topology: this.topology
618
          });
619
        }
620
      }
621
    } finally {
622
      renderPass.popDebugGroup();
154✔
623
      this._logDrawCallEnd();
154✔
624
    }
625
    this._logFramebuffer(renderPass);
154✔
626

627
    // Update needsRedraw flag
628
    if (drawSuccess) {
154✔
629
      this._lastDrawTimestamp = this.device.timestamp;
153✔
630
      this._needsRedraw = false;
153✔
631
    } else if (pipelineErrored) {
1!
632
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
1✔
633
      this._drawBlockedReason = PIPELINE_INITIALIZATION_FAILED;
1✔
UNCOV
634
    } else if (this._drawBlockedReason) {
×
UNCOV
635
      this._needsRedraw = this._drawBlockedReason;
×
636
    } else {
UNCOV
637
      this._needsRedraw = 'waiting for resource initialization';
×
638
    }
639
    return drawSuccess;
154✔
640
  }
641

642
  // Update fixed fields (can trigger pipeline rebuild)
643

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

666
  /**
667
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
668
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
669
   */
670
  setTopology(topology: PrimitiveTopology): void {
671
    if (topology !== this.topology) {
65✔
672
      this.topology = topology;
34✔
673
      this._setPipelineNeedsUpdate('topology');
34✔
674
    }
675
  }
676

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

690
    this.bufferLayout = nextBufferLayout;
1✔
691
    this._setPipelineNeedsUpdate('bufferLayout');
1✔
692

693
    // Recreate the pipeline
694
    this.pipeline = this._updatePipeline();
1✔
695

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

703
    // Reapply geometry attributes to the new vertex array
704
    if (this._gpuGeometry) {
1!
705
      this._setGeometryAttributes(this._gpuGeometry);
1✔
706
    }
707
  }
708

709
  /**
710
   * Set GPU parameters.
711
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
712
   * @param parameters
713
   */
714
  setParameters(parameters: RenderPipelineParameters) {
UNCOV
715
    if (!deepEqual(parameters, this.parameters, 2)) {
×
UNCOV
716
      this.parameters = parameters;
×
UNCOV
717
      this._setPipelineNeedsUpdate('parameters');
×
718
    }
719
  }
720

721
  // Update dynamic fields
722

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

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

746
  /** Updates the indexed draw count override. */
747
  setIndexCount(indexCount: number | undefined): void {
748
    this.indexCount = indexCount;
11✔
749
    this.setNeedsRedraw('indexCount');
11✔
750
  }
751

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

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

773
  setMaterial(material: Material | null): void {
UNCOV
774
    this.material = material;
×
UNCOV
775
    this.setNeedsRedraw('material');
×
776
  }
777

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

793
  /**
794
   * Sets bindings (textures, samplers, uniform buffers)
795
   */
796
  setBindings(bindings: Record<string, ModelBinding>): void {
797
    Object.assign(this.bindings, bindings);
494✔
798
    this.setNeedsRedraw('bindings');
494✔
799
  }
800

801
  /**
802
   * Updates optional transform feedback. WebGL only.
803
   */
804
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
805
    this.transformFeedback = transformFeedback;
88✔
806
    this.setNeedsRedraw('transformFeedback');
88✔
807
  }
808

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

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

838
    // ensure bufferLayout order matches source layout so we bind
839
    // the correct buffers to the correct indices in webgpu.
840
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
466✔
841
      this.pipeline.shaderLayout,
842
      this.bufferLayout
843
    );
844
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
466✔
845

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

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

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

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

923
  // INTERNAL METHODS
924

925
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
926
  _areBindingsLoading(): string | false {
927
    for (const binding of Object.values(this.bindings)) {
158✔
928
      if (isTextureBindingSource(binding) && !binding.isReady) {
154✔
929
        return binding.id;
1✔
930
      }
931
    }
932
    for (const binding of Object.values(this.material?.bindings || {})) {
157✔
UNCOV
933
      if (isTextureBindingSource(binding) && !binding.isReady) {
×
UNCOV
934
        return binding.id;
×
935
      }
936
    }
937
    return false;
157✔
938
  }
939

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

949
    for (const [name, binding] of Object.entries(this.bindings)) {
426✔
950
      const resolvedBinding = resolveModelBinding(name, binding, shaderLayout);
351✔
951
      if (resolvedBinding) {
351!
952
        validBindings[name] = resolvedBinding;
351✔
953
      }
954
    }
955

956
    return validBindings;
426✔
957
  }
958

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

972
    if (!this.material) {
419✔
973
      return bindGroups;
409✔
974
    }
975

976
    for (const [groupKey, groupBindings] of Object.entries(
10✔
977
      this.material.getBindingsByGroup(shaderLayout)
978
    )) {
979
      const group = Number(groupKey);
10✔
980
      bindGroups[group] = {
10✔
981
        ...(bindGroups[group] || {}),
20✔
982
        ...groupBindings
983
      };
984
    }
985

986
    return bindGroups;
10✔
987
  }
988

989
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
990
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
156✔
991
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
156!
992
  }
993

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

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

1048
    // TODO - delete previous geometry?
1049
    this.vertexCount = gpuGeometry.vertexCount;
63✔
1050
    this.setIndexBuffer(gpuGeometry.indices || null);
63✔
1051
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
63✔
1052
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
63✔
1053

1054
    this.setNeedsRedraw('geometry attributes');
63✔
1055
  }
1056

1057
  /** Mark pipeline as needing update */
1058
  _setPipelineNeedsUpdate(reason: string): void {
1059
    this._pipelineNeedsUpdate ||= reason;
44✔
1060
    this._drawBlockedReason = false;
44✔
1061
    this.setNeedsRedraw(reason);
44✔
1062
  }
1063

1064
  /** Update pipeline if needed */
1065
  _updatePipeline(): RenderPipeline {
1066
    if (this._pipelineNeedsUpdate) {
591✔
1067
      let prevShaderVs: Shader | null = null;
262✔
1068
      let prevShaderFs: Shader | null = null;
262✔
1069
      if (this.pipeline) {
262✔
1070
        log.log(
11✔
1071
          1,
1072
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
1073
        )();
1074
        prevShaderVs = this.pipeline.vs;
11✔
1075
        prevShaderFs = this.pipeline.fs;
11✔
1076
      }
1077

1078
      this._pipelineNeedsUpdate = false;
262✔
1079

1080
      const vs = this.shaderFactory.createShader({
262✔
1081
        id: `${this.id}-vertex`,
1082
        stage: 'vertex',
1083
        source: this.source || this.vs,
476✔
1084
        debugShaders: this.props.debugShaders
1085
      });
1086

1087
      let fs: Shader | null = null;
262✔
1088
      if (this.source) {
262✔
1089
        fs = vs;
48✔
1090
      } else if (this.fs) {
214!
1091
        fs = this.shaderFactory.createShader({
214✔
1092
          id: `${this.id}-fragment`,
1093
          stage: 'fragment',
1094
          source: this.source || this.fs,
428✔
1095
          debugShaders: this.props.debugShaders
1096
        });
1097
      }
1098

1099
      this.pipeline = this.pipelineFactory.createRenderPipeline({
262✔
1100
        ...this.props,
1101
        bindings: undefined,
1102
        bufferLayout: this.bufferLayout,
1103
        colorAttachmentFormats: this._colorAttachmentFormats,
1104
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
1105
        topology: this.topology,
1106
        parameters: this.parameters,
1107
        bindGroups: this._getBindGroups(),
1108
        vs,
1109
        fs
1110
      });
1111

1112
      this._attributeInfos = getAttributeInfosFromLayouts(
262✔
1113
        this.pipeline.shaderLayout,
1114
        this.bufferLayout
1115
      );
1116

1117
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
262✔
1118
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
262✔
1119
        this.shaderFactory.release(prevShaderFs);
2✔
1120
      }
1121
    }
1122
    return this.pipeline;
591✔
1123
  }
1124

1125
  /** Throttle draw call logging */
1126
  _lastLogTime = 0;
251✔
1127
  _logOpen = false;
251✔
1128

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

1136
    this._lastLogTime = Date.now();
×
1137
    this._logOpen = true;
×
1138

UNCOV
1139
    log.group(LOG_DRAW_PRIORITY, `>>> DRAWING MODEL ${this.id}`, {collapsed: log.level <= 2})();
×
1140
  }
1141

1142
  _logDrawCallEnd(): void {
1143
    if (this._logOpen) {
154!
1144
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1145

1146
      // log.table(logLevel, attributeTable)();
1147
      // log.table(logLevel, uniformTable)();
UNCOV
1148
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1149

UNCOV
1150
      const uniformTable = this.shaderInputs.getDebugTable();
×
UNCOV
1151
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1152

UNCOV
1153
      const attributeTable = this._getAttributeDebugTable();
×
UNCOV
1154
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1155
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1156

UNCOV
1157
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1158
      this._logOpen = false;
×
1159
    }
1160
  }
1161

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

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

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

1213
  private _getNonMaterialBindings(
1214
    bindings: Record<string, ModelBinding>
1215
  ): Record<string, ModelBinding> {
1216
    if (!this.material) {
182!
1217
      return bindings;
182✔
1218
    }
1219

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

1229
  /** Returns the current reflected shader layout or the pre-reflection empty layout. */
1230
  private _getCurrentShaderLayout(): AnyShaderLayout {
1231
    return this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
425✔
1232
  }
1233

1234
  private _syncDynamicBuffers(): void {
1235
    if (
182!
1236
      this._dynamicIndexBufferSource &&
182!
1237
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1238
    ) {
UNCOV
1239
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
UNCOV
1240
      this.indexBuffer = resolvedIndexBuffer;
×
UNCOV
1241
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
UNCOV
1242
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
UNCOV
1243
      this.setNeedsRedraw('dynamic index buffer');
×
1244
    }
1245

1246
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
182✔
1247
      if (entry.generation !== entry.source.generation) {
6!
UNCOV
1248
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
UNCOV
1249
        entry.generation = entry.source.generation;
×
UNCOV
1250
        this.setNeedsRedraw('dynamic attribute buffer');
×
1251
      }
1252
    }
1253
  }
1254
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1255
    if (this.device.type !== 'webgpu') {
154✔
1256
      return;
140✔
1257
    }
1258

1259
    const framebuffer =
1260
      (
1261
        renderPass as RenderPass & {
14✔
1262
          framebuffer?: {
1263
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1264
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1265
          };
1266
        }
1267
      ).framebuffer || renderPass.props.framebuffer;
1268
    const renderBundleProps = renderPass.props as RenderPass['props'] & {
154✔
1269
      colorAttachmentFormats?: (TextureFormatColor | null)[];
1270
      depthStencilAttachmentFormat?: TextureFormatDepthStencil | false;
1271
    };
1272

1273
    const nextColorAttachmentFormats =
1274
      renderBundleProps.colorAttachmentFormats ??
154✔
1275
      framebuffer?.colorAttachments?.map(colorAttachment =>
1276
        asColorAttachmentFormat(colorAttachment?.texture?.format)
13✔
1277
      );
1278
    const nextDepthStencilAttachmentFormat =
1279
      renderBundleProps.depthStencilAttachmentFormat === false
154!
1280
        ? undefined
1281
        : (renderBundleProps.depthStencilAttachmentFormat ??
27✔
1282
          asDepthStencilAttachmentFormat(framebuffer?.depthStencilAttachment?.texture?.format));
1283

1284
    if (
154✔
1285
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
159✔
1286
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1287
    ) {
1288
      this._colorAttachmentFormats = nextColorAttachmentFormats;
9✔
1289
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
9✔
1290
      this._setPipelineNeedsUpdate('attachment formats');
9✔
1291
    }
1292
  }
1293
}
1294

1295
// HELPERS
1296

1297
function normalizeShaderPluginAttributeNames(
1298
  shaderLayout: ShaderLayout | null | undefined,
1299
  vertexInputs: Record<string, AttributeShaderType>
1300
): ShaderLayout | null | undefined {
1301
  if (!shaderLayout || Object.keys(vertexInputs).length === 0) {
39✔
1302
    return shaderLayout;
38✔
1303
  }
1304

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

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

1335
  if (binding instanceof DynamicBuffer) {
350✔
1336
    return binding.buffer;
23✔
1337
  }
1338

1339
  if (isBufferRangeBinding(binding)) {
327✔
1340
    return resolveBufferRangeBinding(binding);
7✔
1341
  }
1342

1343
  return binding;
320✔
1344
}
1345
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1346
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
13!
1347
}
1348

1349
function asDepthStencilAttachmentFormat(
1350
  format?: string | null
1351
): TextureFormatDepthStencil | undefined {
1352
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
13✔
1353
}
1354

1355
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1356
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
14✔
1357
}
1358

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