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

visgl / luma.gl / 28705256675

04 Jul 2026 11:49AM UTC coverage: 72.243% (+0.06%) from 72.186%
28705256675

Pull #2732

github

web-flow
Merge 7119ee525 into 30d8c7ace
Pull Request #2732: [codex] Simplify text rendering behind TextRenderer

11259 of 17561 branches covered (64.11%)

Branch coverage included in aggregate %.

361 of 495 new or added lines in 17 files covered. (72.93%)

12 existing lines in 4 files now uncovered.

21824 of 28233 relevant lines covered (77.3%)

5613.36 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

414
    this.topology = this.props.topology;
323✔
415
    this.bufferLayout = this.props.bufferLayout;
323✔
416
    this.parameters = this.props.parameters;
323✔
417
    // Seed attachment formats before creating the initial WebGPU pipeline. This is required for
418
    // depth-only models whose intentional empty color target list must not fall back to the canvas
419
    // format before the first render pass can synchronize attachment state.
420
    this._colorAttachmentFormats = this.props.colorAttachmentFormats;
323✔
421
    this._depthStencilAttachmentFormat = this.props.depthStencilAttachmentFormat;
323✔
422

423
    // Geometry, if provided, sets topology and vertex cound
424
    if (props.geometry) {
323✔
425
      this.setGeometry(props.geometry);
101✔
426
    }
427

428
    this.pipelineFactory =
323✔
429
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
646✔
430
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
323✔
431

432
    // Create the pipeline
433
    // @note order is important
434
    this.pipeline = this._updatePipeline();
323✔
435

436
    this.vertexArray = device.createVertexArray({
323✔
437
      shaderLayout: this.pipeline.shaderLayout,
438
      bufferLayout: this.pipeline.bufferLayout
439
    });
440

441
    // Now we can apply geometry attributes
442
    if (this._gpuGeometry) {
323✔
443
      this._setGeometryAttributes(this._gpuGeometry);
101✔
444
    }
445

446
    // Apply any dynamic settings that will not trigger pipeline change
447
    if ('isInstanced' in props) {
323!
448
      this.isInstanced = props.isInstanced;
323✔
449
    }
450

451
    if (props.instanceCount) {
323✔
452
      this.setInstanceCount(props.instanceCount);
150✔
453
    }
454
    if (props.vertexCount) {
323✔
455
      this.setVertexCount(props.vertexCount);
263✔
456
    }
457
    if (props.indexBuffer) {
323!
458
      this.setIndexBuffer(props.indexBuffer);
×
459
    }
460
    if (props.attributes) {
323✔
461
      this.setAttributes(props.attributes);
322✔
462
    }
463
    if (props.constantAttributes) {
323!
464
      this.setConstantAttributes(props.constantAttributes);
323✔
465
    }
466
    if (props.bindings) {
323!
467
      this.setBindings(props.bindings);
323✔
468
    }
469
    if (props.transformFeedback) {
323!
470
      this.transformFeedback = props.transformFeedback;
×
471
    }
472
  }
473

474
  destroy(): void {
475
    if (!this._destroyed) {
242✔
476
      // Release pipeline before we destroy the shaders used by the pipeline
477
      this.pipelineFactory.release(this.pipeline);
241✔
478
      // Release the shaders
479
      this.shaderFactory.release(this.pipeline.vs);
241✔
480
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
241✔
481
        this.shaderFactory.release(this.pipeline.fs);
157✔
482
      }
483
      this._uniformStore.destroy();
241✔
484
      // TODO - mark resource as managed and destroyIfManaged() ?
485
      this._gpuGeometry?.destroy();
241✔
486
      this._destroyed = true;
241✔
487
    }
488
  }
489

490
  // Draw call
491

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

503
  /** Mark the model as needing a redraw */
504
  setNeedsRedraw(reason: string): void {
505
    this._needsRedraw ||= reason;
3,533✔
506
  }
507

508
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
509
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
510
    return this._bindingTable;
2✔
511
  }
512

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

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

539
    const loadingBinding = this._areBindingsLoading();
274✔
540
    if (loadingBinding) {
274!
UNCOV
541
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
UNCOV
542
      return false;
×
543
    }
544

545
    this._syncAttachmentFormats(renderPass);
274✔
546

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

566
    let drawSuccess: boolean;
567
    let pipelineErrored = this.pipeline.isErrored;
274✔
568
    try {
274✔
569
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
274✔
570
      this._logDrawCallStart();
274✔
571

572
      // Update the pipeline if invalidated
573
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
574
      // Application can call Model.predraw() to avoid this.
575
      this.pipeline = this._updatePipeline();
274✔
576
      pipelineErrored = this.pipeline.isErrored;
274✔
577

578
      if (pipelineErrored) {
274✔
579
        log.info(
1✔
580
          LOG_DRAW_PRIORITY,
581
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
582
        )();
583
        drawSuccess = false;
1✔
584
      } else {
585
        const drawValidationError = this.vertexArray.getDrawValidationError();
273✔
586
        if (drawValidationError) {
273!
587
          log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${drawValidationError}`)();
×
588
          this._drawBlockedReason = drawValidationError;
×
589
          drawSuccess = false;
×
590
        } else {
591
          const shaderLayout = this._getCurrentShaderLayout();
273✔
592
          const syncBindings = this._getBindings(shaderLayout);
273✔
593
          const syncBindGroups = this._getBindGroups(shaderLayout, syncBindings);
273✔
594

595
          const {indexBuffer} = this.vertexArray;
273✔
596
          const indexCount = indexBuffer
273✔
597
            ? (this.indexCount ??
34✔
598
              indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2))
12!
599
            : undefined;
600

601
          renderPass.setPipeline(this.pipeline);
273✔
602
          renderPass.setBindings(syncBindGroups, {
273✔
603
            _bindGroupCacheKeys: this._getBindGroupCacheKeys()
604
          });
605
          renderPass.setVertexArray(this.vertexArray);
273✔
606
          const hasNoInstances = this.isInstanced === true && this.instanceCount === 0;
273✔
607
          drawSuccess = hasNoInstances
273✔
608
            ? true
609
            : renderPass.draw({
610
                isInstanced: this.isInstanced,
611
                vertexCount: this.vertexCount,
612
                instanceCount: this.isInstanced ? this.instanceCount : undefined,
272✔
613
                indexCount,
614
                firstVertex: this.firstVertex,
615
                firstIndex: this.firstIndex,
616
                transformFeedback: this.transformFeedback || undefined,
453✔
617
                uniforms: this.props.uniforms,
618
                // WebGL shares underlying cached programs even for models that have different
619
                // parameters and topology, so those compatibility overrides remain per draw.
620
                parameters: this.parameters,
621
                topology: this.topology
622
              });
623
        }
624
      }
625
    } finally {
626
      renderPass.popDebugGroup();
274✔
627
      this._logDrawCallEnd();
274✔
628
    }
629
    this._logFramebuffer(renderPass);
274✔
630

631
    // Update needsRedraw flag
632
    if (drawSuccess) {
274✔
633
      this._lastDrawTimestamp = this.device.timestamp;
273✔
634
      this._needsRedraw = false;
273✔
635
    } else if (pipelineErrored) {
1!
636
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
1✔
637
      this._drawBlockedReason = PIPELINE_INITIALIZATION_FAILED;
1✔
638
    } else if (this._drawBlockedReason) {
×
639
      this._needsRedraw = this._drawBlockedReason;
×
640
    } else {
641
      this._needsRedraw = 'waiting for resource initialization';
×
642
    }
643
    return drawSuccess;
274✔
644
  }
645

646
  // Update fixed fields (can trigger pipeline rebuild)
647

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

670
  /**
671
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
672
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
673
   */
674
  setTopology(topology: PrimitiveTopology): void {
675
    if (topology !== this.topology) {
104✔
676
      this.topology = topology;
73✔
677
      this._setPipelineNeedsUpdate('topology');
73✔
678
    }
679
  }
680

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

694
    this.bufferLayout = nextBufferLayout;
1✔
695
    this._setPipelineNeedsUpdate('bufferLayout');
1✔
696

697
    // Recreate the pipeline
698
    this.pipeline = this._updatePipeline();
1✔
699

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

707
    // Reapply geometry attributes to the new vertex array
708
    if (this._gpuGeometry) {
1!
709
      this._setGeometryAttributes(this._gpuGeometry);
1✔
710
    }
711
  }
712

713
  /**
714
   * Set GPU parameters.
715
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
716
   * @param parameters
717
   */
718
  setParameters(parameters: RenderPipelineParameters) {
719
    if (!deepEqual(parameters, this.parameters, 2)) {
42✔
720
      this.parameters = parameters;
27✔
721
      this._setPipelineNeedsUpdate('parameters');
27✔
722
    }
723
  }
724

725
  // Update dynamic fields
726

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

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

750
  /** Updates the indexed draw count override. */
751
  setIndexCount(indexCount: number | undefined): void {
752
    this.indexCount = indexCount;
15✔
753
    this.setNeedsRedraw('indexCount');
15✔
754
  }
755

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

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

777
  setMaterial(material: Material | null): void {
778
    this.material = material;
×
779
    this.setNeedsRedraw('material');
×
780
  }
781

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

797
  /**
798
   * Sets bindings (textures, samplers, uniform buffers)
799
   */
800
  setBindings(bindings: Record<string, ModelBinding>): void {
801
    Object.assign(this.bindings, bindings);
853✔
802
    this.setNeedsRedraw('bindings');
853✔
803
  }
804

805
  /**
806
   * Updates optional transform feedback. WebGL only.
807
   */
808
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
809
    this.transformFeedback = transformFeedback;
90✔
810
    this.setNeedsRedraw('transformFeedback');
90✔
811
  }
812

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

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

842
    // ensure bufferLayout order matches source layout so we bind
843
    // the correct buffers to the correct indices in webgpu.
844
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
668✔
845
      this.pipeline.shaderLayout,
846
      this.bufferLayout
847
    );
848
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
668✔
849

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

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

881
          this.vertexArray.setBuffer(bufferSlot, resolvedBuffer);
1,144✔
882
          if (buffer instanceof DynamicBuffer) {
1,144✔
883
            this._dynamicAttributeBufferSources[bufferSlot] = {
85✔
884
              source: buffer,
885
              generation: buffer.generation
886
            };
887
          } else {
888
            delete this._dynamicAttributeBufferSources[bufferSlot];
1,059✔
889
          }
890
          set = true;
1,144✔
891
        }
892
      }
893
      if (!set && !disableWarnings) {
772✔
894
        log.warn(
2✔
895
          `Model(${this.id}): Ignoring buffer "${resolvedBuffer.id}" for unknown attribute "${bufferName}"`
896
        )();
897
      }
898
    }
899
    this.setNeedsRedraw('attributes');
668✔
900
  }
901

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

927
  // INTERNAL METHODS
928

929
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
930
  _areBindingsLoading(): string | false {
931
    for (const binding of Object.values(this.bindings)) {
277✔
932
      if (isTextureBindingSource(binding) && !binding.isReady) {
427!
UNCOV
933
        return binding.id;
×
934
      }
935
    }
936
    for (const binding of Object.values(this.material?.bindings || {})) {
277✔
937
      if (isTextureBindingSource(binding) && !binding.isReady) {
×
938
        return binding.id;
×
939
      }
940
    }
941
    return false;
277✔
942
  }
943

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

953
    for (const [name, binding] of Object.entries(this.bindings)) {
284✔
954
      const resolvedBinding = resolveModelBinding(name, binding, shaderLayout);
489✔
955
      if (resolvedBinding) {
489!
956
        validBindings[name] = resolvedBinding;
489✔
957
      }
958
    }
959

960
    return validBindings;
284✔
961
  }
962

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

976
    if (!this.material) {
277!
977
      return bindGroups;
277✔
978
    }
979

980
    for (const [groupKey, groupBindings] of Object.entries(
×
981
      this.material.getBindingsByGroup(shaderLayout)
982
    )) {
983
      const group = Number(groupKey);
×
984
      bindGroups[group] = {
×
985
        ...(bindGroups[group] || {}),
×
986
        ...groupBindings
987
      };
988
    }
989

990
    return bindGroups;
×
991
  }
992

993
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
994
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
276✔
995
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
276!
996
  }
997

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

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

1052
    // TODO - delete previous geometry?
1053
    this.vertexCount = gpuGeometry.vertexCount;
102✔
1054
    this.setIndexBuffer(gpuGeometry.indices || null);
102✔
1055
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
102✔
1056
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
102✔
1057

1058
    this.setNeedsRedraw('geometry attributes');
102✔
1059
  }
1060

1061
  /** Mark pipeline as needing update */
1062
  _setPipelineNeedsUpdate(reason: string): void {
1063
    this._pipelineNeedsUpdate ||= reason;
146✔
1064
    this._drawBlockedReason = false;
146✔
1065
    this.setNeedsRedraw(reason);
146✔
1066
  }
1067

1068
  /** Update pipeline if needed */
1069
  _updatePipeline(): RenderPipeline {
1070
    if (this._pipelineNeedsUpdate) {
977✔
1071
      let prevShaderVs: Shader | null = null;
397✔
1072
      let prevShaderFs: Shader | null = null;
397✔
1073
      if (this.pipeline) {
397✔
1074
        log.log(
74✔
1075
          1,
1076
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
1077
        )();
1078
        prevShaderVs = this.pipeline.vs;
74✔
1079
        prevShaderFs = this.pipeline.fs;
74✔
1080
      }
1081

1082
      this._pipelineNeedsUpdate = false;
397✔
1083

1084
      const vs = this.shaderFactory.createShader({
397✔
1085
        id: `${this.id}-vertex`,
1086
        stage: 'vertex',
1087
        source: this.source || this.vs,
658✔
1088
        debugShaders: this.props.debugShaders
1089
      });
1090

1091
      let fs: Shader | null = null;
397✔
1092
      if (this.source) {
397✔
1093
        fs = vs;
136✔
1094
      } else if (this.fs) {
261!
1095
        fs = this.shaderFactory.createShader({
261✔
1096
          id: `${this.id}-fragment`,
1097
          stage: 'fragment',
1098
          source: this.source || this.fs,
522✔
1099
          debugShaders: this.props.debugShaders
1100
        });
1101
      }
1102

1103
      this.pipeline = this.pipelineFactory.createRenderPipeline({
397✔
1104
        ...this.props,
1105
        bindings: undefined,
1106
        bufferLayout: this.bufferLayout,
1107
        colorAttachmentFormats: this._colorAttachmentFormats,
1108
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
1109
        topology: this.topology,
1110
        parameters: this.parameters,
1111
        bindGroups: undefined,
1112
        vs,
1113
        fs
1114
      });
1115

1116
      this._attributeInfos = getAttributeInfosFromLayouts(
397✔
1117
        this.pipeline.shaderLayout,
1118
        this.bufferLayout
1119
      );
1120

1121
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
397✔
1122
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
397✔
1123
        this.shaderFactory.release(prevShaderFs);
22✔
1124
      }
1125
    }
1126
    return this.pipeline;
977✔
1127
  }
1128

1129
  /** Throttle draw call logging */
1130
  _lastLogTime = 0;
323✔
1131
  _logOpen = false;
323✔
1132

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

1140
    this._lastLogTime = Date.now();
×
1141
    this._logOpen = true;
×
1142

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

1146
  _logDrawCallEnd(): void {
1147
    if (this._logOpen) {
274!
1148
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1149

1150
      // log.table(logLevel, attributeTable)();
1151
      // log.table(logLevel, uniformTable)();
1152
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1153

1154
      const uniformTable = this.shaderInputs.getDebugTable();
×
1155
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1156

1157
      const attributeTable = this._getAttributeDebugTable();
×
1158
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1159
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1160

1161
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1162
      this._logOpen = false;
×
1163
    }
1164
  }
1165

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

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

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

1217
  private _getNonMaterialBindings(
1218
    bindings: Record<string, ModelBinding>
1219
  ): Record<string, ModelBinding> {
1220
    if (!this.material) {
377!
1221
      return bindings;
377✔
1222
    }
1223

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

1233
  /** Returns the current reflected shader layout or the pre-reflection empty layout. */
1234
  private _getCurrentShaderLayout(): AnyShaderLayout {
1235
    return this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
283!
1236
  }
1237

1238
  private _syncDynamicBuffers(): void {
1239
    if (
376!
1240
      this._dynamicIndexBufferSource &&
376!
1241
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1242
    ) {
1243
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
1244
      this.indexBuffer = resolvedIndexBuffer;
×
1245
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
1246
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
1247
      this.setNeedsRedraw('dynamic index buffer');
×
1248
    }
1249

1250
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
376✔
1251
      if (entry.generation !== entry.source.generation) {
23!
1252
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
1253
        entry.generation = entry.source.generation;
×
1254
        this.setNeedsRedraw('dynamic attribute buffer');
×
1255
      }
1256
    }
1257
  }
1258
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1259
    if (this.device.type !== 'webgpu') {
274✔
1260
      return;
198✔
1261
    }
1262

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

1277
    const nextColorAttachmentFormats =
1278
      renderBundleProps.colorAttachmentFormats ??
76✔
1279
      framebuffer?.colorAttachments?.map(colorAttachment =>
1280
        asColorAttachmentFormat(colorAttachment?.texture?.format)
69✔
1281
      );
1282
    const nextDepthStencilAttachmentFormat =
1283
      renderBundleProps.depthStencilAttachmentFormat === false
76!
1284
        ? undefined
1285
        : (renderBundleProps.depthStencilAttachmentFormat ??
151✔
1286
          asDepthStencilAttachmentFormat(framebuffer?.depthStencilAttachment?.texture?.format));
1287

1288
    if (
76✔
1289
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
107✔
1290
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1291
    ) {
1292
      this._colorAttachmentFormats = nextColorAttachmentFormats;
45✔
1293
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
45✔
1294
      this._setPipelineNeedsUpdate('attachment formats');
45✔
1295
    }
1296
  }
1297
}
1298

1299
// HELPERS
1300

1301
function normalizeShaderPluginAttributeNames(
1302
  shaderLayout: ShaderLayout | null | undefined,
1303
  vertexInputs: Record<string, AttributeShaderType>
1304
): ShaderLayout | null | undefined {
1305
  if (!shaderLayout || Object.keys(vertexInputs).length === 0) {
84✔
1306
    return shaderLayout;
83✔
1307
  }
1308

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

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

1339
  if (binding instanceof DynamicBuffer) {
475✔
1340
    return binding.buffer;
23✔
1341
  }
1342

1343
  if (isBufferRangeBinding(binding)) {
452✔
1344
    return resolveBufferRangeBinding(binding);
14✔
1345
  }
1346

1347
  return binding;
438✔
1348
}
1349
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1350
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
69!
1351
}
1352

1353
function asDepthStencilAttachmentFormat(
1354
  format?: string | null
1355
): TextureFormatDepthStencil | undefined {
1356
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
75✔
1357
}
1358

1359
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1360
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
92✔
1361
}
1362

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