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

visgl / luma.gl / 25677315527

11 May 2026 02:45PM UTC coverage: 74.394% (+0.2%) from 74.161%
25677315527

push

github

web-flow
feat(arrow) ArrowGPUTable, ArrowModel (#2607)

Co-authored-by: Ib Green <ib.green.home@gmail.com>

5402 of 8198 branches covered (65.89%)

Branch coverage included in aggregate %.

193 of 228 new or added lines in 13 files covered. (84.65%)

1 existing line in 1 file now uncovered.

12047 of 15257 relevant lines covered (78.96%)

767.58 hits per line

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

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

37
import type {ShaderBindingDebugRow, ShaderModule, PlatformInfo} from '@luma.gl/shadertools';
38
import {ShaderAssembler} from '@luma.gl/shadertools';
39

40
import type {Geometry} from '../geometry/geometry';
41
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
42
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
43
import {debugFramebuffer} from '../debug/debug-framebuffer';
44
import {deepEqual} from '../utils/deep-equal';
45
import {BufferLayoutHelper} from '../utils/buffer-layout-helper';
46
import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';
47
import {
48
  mergeInferredShaderLayout,
49
  mergeShaderModuleBindingsIntoLayout,
50
  shaderModuleHasUniforms
51
} from '../utils/shader-module-utils';
52
import {uid} from '../utils/uid';
53
import {ShaderInputs} from '../shader-inputs';
54
import {
55
  DynamicBuffer,
56
  type DynamicBufferRange,
57
  isBufferRangeBinding,
58
  resolveBufferRangeBinding
59
} from '../dynamic-buffer/dynamic-buffer';
60
import {DynamicTexture} from '../dynamic-texture/dynamic-texture';
61
import {Material} from '../material/material';
62

63
const LOG_DRAW_PRIORITY = 2;
79✔
64
const LOG_DRAW_TIMEOUT = 10000;
79✔
65
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
79✔
66
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
79✔
67
  'stencil8',
68
  'depth16unorm',
69
  'depth24plus',
70
  'depth24plus-stencil8',
71
  'depth32float',
72
  'depth32float-stencil8'
73
];
74
type ModelBinding = Binding | DynamicTexture | DynamicBuffer | DynamicBufferRange;
75
type ModelBuffer = Buffer | DynamicBuffer;
76

77
export type ModelProps = Omit<RenderPipelineProps, 'vs' | 'fs' | 'bindings'> & {
78
  source?: string;
79
  vs?: string | null;
80
  fs?: string | null;
81

82
  /** shadertool shader modules (added to shader code) */
83
  modules?: ShaderModule[];
84
  /** Shadertool module defines (configures shader code)*/
85
  defines?: Record<string, boolean>;
86
  // TODO - injections, hooks etc?
87

88
  /** Shader inputs, used to generated uniform buffers and bindings */
89
  shaderInputs?: ShaderInputs;
90
  /** Material-owned group-3 bindings */
91
  material?: Material;
92
  /** Shader resource bindings, including dynamic buffers and dynamic textures. */
93
  bindings?: Record<string, ModelBinding>;
94
  /** WebGL-only uniforms */
95
  uniforms?: Record<string, unknown>;
96
  /** Parameters that are built into the pipeline */
97
  parameters?: RenderPipelineParameters;
98

99
  /** Geometry */
100
  geometry?: GPUGeometry | Geometry | null;
101

102
  /** @deprecated Use instanced rendering? Will be auto-detected in 9.1 */
103
  isInstanced?: boolean;
104
  /** instance count */
105
  instanceCount?: number;
106
  /** Vertex count */
107
  vertexCount?: number;
108

109
  /** Optional index buffer. Dynamic buffers are rebound when resized. */
110
  indexBuffer?: ModelBuffer | null;
111
  /** Buffer-valued attributes. Dynamic buffers are rebound when resized. */
112
  attributes?: Record<string, ModelBuffer>;
113
  /**   */
114
  constantAttributes?: Record<string, TypedArray>;
115

116
  /** Some applications intentionally supply unused attributes and bindings, and want to disable warnings */
117
  disableWarnings?: boolean;
118

119
  /** @internal For use with {@link TransformFeedback}, WebGL only. */
120
  varyings?: string[];
121

122
  transformFeedback?: TransformFeedback;
123

124
  /** Show shader source in browser? */
125
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
126

127
  /** Factory used to create a {@link RenderPipeline}. Defaults to {@link Device} default factory. */
128
  pipelineFactory?: PipelineFactory;
129
  /** Factory used to create a {@link Shader}. Defaults to {@link Device} default factory. */
130
  shaderFactory?: ShaderFactory;
131
  /** Shader assembler. Defaults to the ShaderAssembler.getShaderAssembler() */
132
  shaderAssembler?: ShaderAssembler;
133
};
134

135
/**
136
 * High level draw API for luma.gl.
137
 *
138
 * A `Model` encapsulates shaders, geometry attributes, bindings and render
139
 * pipeline state into a single object. It automatically reuses and rebuilds
140
 * pipelines as render parameters change and exposes convenient hooks for
141
 * updating uniforms and attributes.
142
 *
143
 * Features:
144
 * - Reuses and lazily recompiles {@link RenderPipeline | pipelines} as needed.
145
 * - Integrates with `@luma.gl/shadertools` to assemble GLSL or WGSL from shader modules.
146
 * - Manages geometry attributes and buffer bindings.
147
 * - Accepts textures, samplers and uniform buffers as bindings, including `DynamicTexture`.
148
 * - Provides detailed debug logging and optional shader source inspection.
149
 */
150
export class Model {
151
  static defaultProps: Required<ModelProps> = {
79✔
152
    ...RenderPipeline.defaultProps,
153
    source: undefined!,
154
    vs: null,
155
    fs: null,
156
    id: 'unnamed',
157
    handle: undefined,
158
    userData: {},
159
    defines: {},
160
    modules: [],
161
    geometry: null,
162
    indexBuffer: null,
163
    attributes: {},
164
    constantAttributes: {},
165
    bindings: {},
166
    uniforms: {},
167
    varyings: [],
168

169
    isInstanced: undefined!,
170
    instanceCount: 0,
171
    vertexCount: 0,
172

173
    shaderInputs: undefined!,
174
    material: undefined!,
175
    pipelineFactory: undefined!,
176
    shaderFactory: undefined!,
177
    transformFeedback: undefined!,
178
    shaderAssembler: ShaderAssembler.getDefaultShaderAssembler(),
179

180
    debugShaders: undefined!,
181
    disableWarnings: undefined!
182
  };
183

184
  /** Device that created this model */
185
  readonly device: Device;
186
  /** Application provided identifier */
187
  readonly id: string;
188
  /** WGSL shader source when using unified shader */
189
  // @ts-expect-error assigned in function called from constructor
190
  readonly source: string;
191
  /** GLSL vertex shader source */
192
  // @ts-expect-error assigned in function called from constructor
193
  readonly vs: string;
194
  /** GLSL fragment shader source */
195
  // @ts-expect-error assigned in function called from constructor
196
  readonly fs: string;
197
  /** Factory used to create render pipelines */
198
  readonly pipelineFactory: PipelineFactory;
199
  /** Factory used to create shaders */
200
  readonly shaderFactory: ShaderFactory;
201
  /** User-supplied per-model data */
202
  userData: {[key: string]: any} = {};
110✔
203

204
  // Fixed properties (change can trigger pipeline rebuild)
205

206
  /** The render pipeline GPU parameters, depth testing etc */
207
  parameters: RenderPipelineParameters;
208

209
  /** The primitive topology */
210
  topology: PrimitiveTopology;
211
  /** Buffer layout */
212
  bufferLayout: BufferLayout[];
213

214
  // Dynamic properties
215

216
  /** Use instanced rendering */
217
  isInstanced: boolean | undefined = undefined;
110✔
218
  /** instance count. `undefined` means not instanced */
219
  instanceCount: number = 0;
110✔
220
  /** Vertex count */
221
  vertexCount: number;
222

223
  /** Index buffer */
224
  indexBuffer: Buffer | null = null;
110✔
225
  /** Buffer-valued attributes */
226
  bufferAttributes: Record<string, Buffer> = {};
110✔
227
  /** Constant-valued attributes */
228
  constantAttributes: Record<string, TypedArray> = {};
110✔
229
  /** Bindings (textures, samplers, uniform buffers) */
230
  bindings: Record<string, ModelBinding> = {};
110✔
231

232
  /**
233
   * VertexArray
234
   * @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
235
   * @todo - allow application to define multiple vertex arrays?
236
   * */
237
  vertexArray: VertexArray;
238

239
  /** TransformFeedback, WebGL 2 only. */
240
  transformFeedback: TransformFeedback | null = null;
110✔
241

242
  /** The underlying GPU "program". @note May be recreated if parameters change */
243
  pipeline: RenderPipeline;
244

245
  /** ShaderInputs instance */
246
  // @ts-expect-error Assigned in function called by constructor
247
  shaderInputs: ShaderInputs;
248
  material: Material | null = null;
110✔
249
  // @ts-expect-error Assigned in function called by constructor
250
  _uniformStore: UniformStore;
251

252
  _attributeInfos: Record<string, AttributeInfo> = {};
110✔
253
  _gpuGeometry: GPUGeometry | null = null;
110✔
254
  private props: Required<ModelProps>;
255
  private _dynamicIndexBufferSource: {source: DynamicBuffer; generation: number} | null = null;
110✔
256
  private _dynamicAttributeBufferSources: Record<
257
    number,
258
    {source: DynamicBuffer; generation: number}
259
  > = {};
110✔
260
  private _colorAttachmentFormats: (TextureFormatColor | null)[] | undefined;
261
  private _depthStencilAttachmentFormat: TextureFormatDepthStencil | undefined;
262

263
  _pipelineNeedsUpdate: string | false = 'newly created';
110✔
264
  private _needsRedraw: string | false = 'initializing';
110✔
265
  private _drawBlockedReason: string | false = false;
110✔
266
  private _destroyed = false;
110✔
267

268
  /** "Time" of last draw. Monotonically increasing timestamp */
269
  _lastDrawTimestamp: number = -1;
110✔
270
  private _bindingTable: ShaderBindingDebugRow[] = [];
110✔
271

272
  get [Symbol.toStringTag](): string {
273
    return 'Model';
×
274
  }
275

276
  toString(): string {
277
    return `Model(${this.id})`;
134✔
278
  }
279

280
  constructor(device: Device, props: ModelProps) {
281
    this.props = {...Model.defaultProps, ...props};
110✔
282
    props = this.props;
110✔
283
    this.id = props.id || uid('model');
110!
284
    this.device = device;
110✔
285

286
    Object.assign(this.userData, props.userData);
110✔
287

288
    this.material = props.material || null;
110✔
289

290
    // Setup shader module inputs
291
    const moduleMap = Object.fromEntries(
110✔
292
      this.props.modules?.map(module => [module.name, module]) || []
87!
293
    );
294

295
    const shaderInputs =
296
      props.shaderInputs ||
110✔
297
      new ShaderInputs(moduleMap, {disableWarnings: this.props.disableWarnings});
298
    // @ts-ignore
299
    this.setShaderInputs(shaderInputs);
110✔
300

301
    // Setup shader assembler
302
    const platformInfo = getPlatformInfo(device);
110✔
303

304
    // Extract modules from shader inputs if not supplied
305
    const modules =
110!
306
      // @ts-ignore shaderInputs is assigned in setShaderInputs above.
307
      (this.props.modules?.length > 0 ? this.props.modules : this.shaderInputs?.getModules()) || [];
110✔
308

309
    this.props.shaderLayout =
110✔
310
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
215✔
311

312
    const isWebGPU = this.device.type === 'webgpu';
110✔
313

314
    // WebGPU
315
    // TODO - hack to support unified WGSL shader
316
    // TODO - this is wrong, compile a single shader
317
    if (isWebGPU && this.props.source) {
110✔
318
      // WGSL
319
      const {source, getUniforms, bindingTable} = this.props.shaderAssembler.assembleWGSLShader({
9✔
320
        platformInfo,
321
        ...this.props,
322
        modules
323
      });
324
      this.source = source;
9✔
325
      // @ts-expect-error
326
      this._getModuleUniforms = getUniforms;
9✔
327
      this._bindingTable = bindingTable;
9✔
328
      // Extract shader layout after modules have been added to WGSL source, to include any bindings added by modules
329
      const inferredShaderLayout = (
330
        device as Device & {getShaderLayout?: (source: string) => any}
9✔
331
      ).getShaderLayout?.(this.source);
332
      const shaderLayout = mergeInferredShaderLayout(this.props.shaderLayout, inferredShaderLayout);
9✔
333
      this.props.shaderLayout =
9✔
334
        mergeShaderModuleBindingsIntoLayout(shaderLayout || null, modules) || null;
18!
335
    } else {
336
      // GLSL
337
      const {vs, fs, getUniforms} = this.props.shaderAssembler.assembleGLSLShaderPair({
101✔
338
        platformInfo,
339
        ...this.props,
340
        modules
341
      });
342

343
      this.vs = vs;
101✔
344
      this.fs = fs;
101✔
345
      // @ts-expect-error
346
      this._getModuleUniforms = getUniforms;
101✔
347
      this._bindingTable = [];
101✔
348
    }
349

350
    this.vertexCount = this.props.vertexCount;
110✔
351
    this.instanceCount = this.props.instanceCount;
110✔
352

353
    this.topology = this.props.topology;
110✔
354
    this.bufferLayout = this.props.bufferLayout;
110✔
355
    this.parameters = this.props.parameters;
110✔
356

357
    // Geometry, if provided, sets topology and vertex cound
358
    if (props.geometry) {
110✔
359
      this.setGeometry(props.geometry);
60✔
360
    }
361

362
    this.pipelineFactory =
110✔
363
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
220✔
364
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
110✔
365

366
    // Create the pipeline
367
    // @note order is important
368
    this.pipeline = this._updatePipeline();
110✔
369

370
    this.vertexArray = device.createVertexArray({
110✔
371
      shaderLayout: this.pipeline.shaderLayout,
372
      bufferLayout: this.pipeline.bufferLayout
373
    });
374

375
    // Now we can apply geometry attributes
376
    if (this._gpuGeometry) {
110✔
377
      this._setGeometryAttributes(this._gpuGeometry);
60✔
378
    }
379

380
    // Apply any dynamic settings that will not trigger pipeline change
381
    if ('isInstanced' in props) {
110!
382
      this.isInstanced = props.isInstanced;
110✔
383
    }
384

385
    if (props.instanceCount) {
110✔
386
      this.setInstanceCount(props.instanceCount);
12✔
387
    }
388
    if (props.vertexCount) {
110✔
389
      this.setVertexCount(props.vertexCount);
74✔
390
    }
391
    if (props.indexBuffer) {
110!
392
      this.setIndexBuffer(props.indexBuffer);
×
393
    }
394
    if (props.attributes) {
110✔
395
      this.setAttributes(props.attributes);
109✔
396
    }
397
    if (props.constantAttributes) {
110!
398
      this.setConstantAttributes(props.constantAttributes);
110✔
399
    }
400
    if (props.bindings) {
110!
401
      this.setBindings(props.bindings);
110✔
402
    }
403
    if (props.transformFeedback) {
110!
404
      this.transformFeedback = props.transformFeedback;
×
405
    }
406
  }
407

408
  destroy(): void {
409
    if (!this._destroyed) {
87✔
410
      // Release pipeline before we destroy the shaders used by the pipeline
411
      this.pipelineFactory.release(this.pipeline);
86✔
412
      // Release the shaders
413
      this.shaderFactory.release(this.pipeline.vs);
86✔
414
      if (this.pipeline.fs && this.pipeline.fs !== this.pipeline.vs) {
86✔
415
        this.shaderFactory.release(this.pipeline.fs);
77✔
416
      }
417
      this._uniformStore.destroy();
86✔
418
      // TODO - mark resource as managed and destroyIfManaged() ?
419
      this._gpuGeometry?.destroy();
86✔
420
      this._destroyed = true;
86✔
421
    }
422
  }
423

424
  // Draw call
425

426
  /** Query redraw status. Clears the status. */
427
  needsRedraw(): false | string {
428
    // Catch any writes to already bound resources
429
    if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
1!
430
      this.setNeedsRedraw('contents of bound textures or buffers updated');
1✔
431
    }
432
    const needsRedraw = this._needsRedraw;
1✔
433
    this._needsRedraw = false;
1✔
434
    return needsRedraw;
1✔
435
  }
436

437
  /** Mark the model as needing a redraw */
438
  setNeedsRedraw(reason: string): void {
439
    this._needsRedraw ||= reason;
1,085✔
440
  }
441

442
  /** Returns WGSL binding debug rows for the assembled shader. Returns an empty array for GLSL models. */
443
  getBindingDebugTable(): readonly ShaderBindingDebugRow[] {
444
    return this._bindingTable;
2✔
445
  }
446

447
  /**
448
   * Updates uniforms and pipeline state before opening a render pass.
449
   *
450
   * @param commandEncoder - Encoder that should own any GPU uploads emitted
451
   * during draw preparation.
452
   */
453
  predraw(commandEncoder: CommandEncoder): void {
454
    // Update uniform buffers if needed
455
    this._syncDynamicBuffers();
84✔
456
    this.updateShaderInputs(commandEncoder);
84✔
457
    this.material?.updateShaderInputs(commandEncoder);
84✔
458
    // Check if the pipeline is invalidated
459
    this.pipeline = this._updatePipeline();
84✔
460
  }
461

462
  /**
463
   * Issue one draw call.
464
   * @param renderPass - render pass to draw into
465
   * @returns `true` if the draw call was executed, `false` if resources were not ready.
466
   */
467
  draw(renderPass: RenderPass): boolean {
468
    if (this._drawBlockedReason && !this._pipelineNeedsUpdate) {
68✔
469
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${this._drawBlockedReason}`)();
1✔
470
      return false;
1✔
471
    }
472

473
    const loadingBinding = this._areBindingsLoading();
67✔
474
    if (loadingBinding) {
67!
475
      log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
×
476
      return false;
×
477
    }
478

479
    this._syncAttachmentFormats(renderPass);
67✔
480

481
    try {
67✔
482
      renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
67✔
483
      if (this.device.type === 'webgpu') {
67✔
484
        // WebGPU uploads cannot be encoded once the render pass is already open.
485
        // Keep the implicit draw() path working for existing callers by falling
486
        // back to immediate writes here; callers that need upload ordering
487
        // across multiple draws/viewports must call predraw(commandEncoder)
488
        // before beginRenderPass().
489
        this.updateShaderInputs();
9✔
490
        this.material?.updateShaderInputs();
9✔
491
        this._syncDynamicBuffers();
9✔
492
        this.pipeline = this._updatePipeline();
9✔
493
      } else {
494
        this.predraw(this.device.commandEncoder);
58✔
495
      }
496
    } finally {
497
      renderPass.popDebugGroup();
67✔
498
    }
499

500
    let drawSuccess: boolean;
501
    let pipelineErrored = this.pipeline.isErrored;
67✔
502
    try {
67✔
503
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
67✔
504
      this._logDrawCallStart();
67✔
505

506
      // Update the pipeline if invalidated
507
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
508
      // Application can call Model.predraw() to avoid this.
509
      this.pipeline = this._updatePipeline();
67✔
510
      pipelineErrored = this.pipeline.isErrored;
67✔
511

512
      if (pipelineErrored) {
67✔
513
        log.info(
1✔
514
          LOG_DRAW_PRIORITY,
515
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
516
        )();
517
        drawSuccess = false;
1✔
518
      } else {
519
        const drawValidationError = this.vertexArray.getDrawValidationError();
66✔
520
        if (drawValidationError) {
66!
NEW
521
          log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${drawValidationError}`)();
×
NEW
522
          this._drawBlockedReason = drawValidationError;
×
NEW
523
          drawSuccess = false;
×
524
        } else {
525
          const syncBindings = this._getBindings();
66✔
526
          const syncBindGroups = this._getBindGroups();
66✔
527

528
          const {indexBuffer} = this.vertexArray;
66✔
529
          const indexCount = indexBuffer
66✔
530
            ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
12!
531
            : undefined;
532

533
          drawSuccess = this.pipeline.draw({
66✔
534
            renderPass,
535
            vertexArray: this.vertexArray,
536
            isInstanced: this.isInstanced,
537
            vertexCount: this.vertexCount,
538
            instanceCount: this.instanceCount,
539
            indexCount,
540
            transformFeedback: this.transformFeedback || undefined,
111✔
541
            // Pipelines may be shared across models when caching is enabled, so bindings
542
            // and WebGL uniforms must be supplied on every draw instead of being stored
543
            // on the pipeline instance.
544
            bindings: syncBindings,
545
            bindGroups: syncBindGroups,
546
            _bindGroupCacheKeys: this._getBindGroupCacheKeys(),
547
            uniforms: this.props.uniforms,
548
            // WebGL shares underlying cached pipelines even for models that have different parameters and topology,
549
            // so we must provide our unique parameters to each draw
550
            // (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
551
            parameters: this.parameters,
552
            topology: this.topology
553
          });
554
        }
555
      }
556
    } finally {
557
      renderPass.popDebugGroup();
67✔
558
      this._logDrawCallEnd();
67✔
559
    }
560
    this._logFramebuffer(renderPass);
67✔
561

562
    // Update needsRedraw flag
563
    if (drawSuccess) {
67✔
564
      this._lastDrawTimestamp = this.device.timestamp;
66✔
565
      this._needsRedraw = false;
66✔
566
    } else if (pipelineErrored) {
1!
567
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
1✔
568
      this._drawBlockedReason = PIPELINE_INITIALIZATION_FAILED;
1✔
NEW
569
    } else if (this._drawBlockedReason) {
×
NEW
570
      this._needsRedraw = this._drawBlockedReason;
×
571
    } else {
572
      this._needsRedraw = 'waiting for resource initialization';
×
573
    }
574
    return drawSuccess;
67✔
575
  }
576

577
  // Update fixed fields (can trigger pipeline rebuild)
578

579
  /**
580
   * Updates the optional geometry
581
   * Geometry, set topology and bufferLayout
582
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
583
   */
584
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
585
    this._gpuGeometry?.destroy();
60✔
586
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
60✔
587
    if (gpuGeometry) {
60!
588
      this.setTopology(gpuGeometry.topology || 'triangle-list');
60!
589
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
60✔
590
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
60✔
591
        gpuGeometry.bufferLayout,
592
        this.bufferLayout
593
      );
594
      if (this.vertexArray) {
60!
595
        this._setGeometryAttributes(gpuGeometry);
×
596
      }
597
    }
598
    this._gpuGeometry = gpuGeometry;
60✔
599
  }
600

601
  /**
602
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
603
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
604
   */
605
  setTopology(topology: PrimitiveTopology): void {
606
    if (topology !== this.topology) {
63✔
607
      this.topology = topology;
34✔
608
      this._setPipelineNeedsUpdate('topology');
34✔
609
    }
610
  }
611

612
  /**
613
   * Updates the buffer layout.
614
   * @note Triggers a pipeline rebuild / pipeline cache fetch
615
   */
616
  setBufferLayout(bufferLayout: BufferLayout[]): void {
617
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
2✔
618
    const nextBufferLayout = this._gpuGeometry
2!
619
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
620
      : bufferLayout;
621
    if (deepEqual(nextBufferLayout, this.bufferLayout, -1)) {
2!
622
      return;
2✔
623
    }
624

NEW
625
    this.bufferLayout = nextBufferLayout;
×
UNCOV
626
    this._setPipelineNeedsUpdate('bufferLayout');
×
627

628
    // Recreate the pipeline
629
    this.pipeline = this._updatePipeline();
×
630

631
    // vertex array needs to be updated if we update buffer layout,
632
    // but not if we update parameters
633
    this.vertexArray = this.device.createVertexArray({
×
634
      shaderLayout: this.pipeline.shaderLayout,
635
      bufferLayout: this.pipeline.bufferLayout
636
    });
637

638
    // Reapply geometry attributes to the new vertex array
639
    if (this._gpuGeometry) {
×
640
      this._setGeometryAttributes(this._gpuGeometry);
×
641
    }
642
  }
643

644
  /**
645
   * Set GPU parameters.
646
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
647
   * @param parameters
648
   */
649
  setParameters(parameters: RenderPipelineParameters) {
650
    if (!deepEqual(parameters, this.parameters, 2)) {
×
651
      this.parameters = parameters;
×
652
      this._setPipelineNeedsUpdate('parameters');
×
653
    }
654
  }
655

656
  // Update dynamic fields
657

658
  /**
659
   * Updates the instance count (used in draw calls)
660
   * @note Any attributes with stepMode=instance need to be at least this big
661
   */
662
  setInstanceCount(instanceCount: number): void {
663
    this.instanceCount = instanceCount;
27✔
664
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
665
    // but deck.gl sets instanceCount even for models that are not instanced.
666
    if (this.isInstanced === undefined && instanceCount > 0) {
27✔
667
      this.isInstanced = true;
19✔
668
    }
669
    this.setNeedsRedraw('instanceCount');
27✔
670
  }
671

672
  /**
673
   * Updates the vertex count (used in draw calls)
674
   * @note Any attributes with stepMode=vertex need to be at least this big
675
   */
676
  setVertexCount(vertexCount: number): void {
677
    this.vertexCount = vertexCount;
75✔
678
    this.setNeedsRedraw('vertexCount');
75✔
679
  }
680

681
  /** Set the shader inputs */
682
  setShaderInputs(shaderInputs: ShaderInputs): void {
683
    this.shaderInputs = shaderInputs;
110✔
684
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
110✔
685
    // Create uniform buffer bindings for all modules that actually have uniforms
686
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
110✔
687
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
147✔
688
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
109✔
689
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
109✔
690
      }
691
    }
692
    this.setNeedsRedraw('shaderInputs');
110✔
693
  }
694

695
  setMaterial(material: Material | null): void {
696
    this.material = material;
×
697
    this.setNeedsRedraw('material');
×
698
  }
699

700
  /** Update uniform buffers from the model's shader inputs */
701
  /**
702
   * Flushes current shader-input values into managed uniform buffers and
703
   * non-material bindings.
704
   *
705
   * @param commandEncoder - Optional encoder used to order uniform uploads with
706
   * subsequent draw commands.
707
   */
708
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
709
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
93✔
710
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
93✔
711
    // TODO - this is already tracked through buffer/texture update times?
712
    this.setNeedsRedraw('shaderInputs');
93✔
713
  }
714

715
  /**
716
   * Sets bindings (textures, samplers, uniform buffers)
717
   */
718
  setBindings(bindings: Record<string, ModelBinding>): void {
719
    Object.assign(this.bindings, bindings);
244✔
720
    this.setNeedsRedraw('bindings');
244✔
721
  }
722

723
  /**
724
   * Updates optional transform feedback. WebGL only.
725
   */
726
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
727
    this.transformFeedback = transformFeedback;
20✔
728
    this.setNeedsRedraw('transformFeedback');
20✔
729
  }
730

731
  /**
732
   * Sets the index buffer
733
   * @todo - how to unset it if we change geometry?
734
   */
735
  setIndexBuffer(indexBuffer: ModelBuffer | null): void {
736
    const resolvedIndexBuffer =
737
      indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
60!
738
    this.indexBuffer = resolvedIndexBuffer;
60✔
739
    this._dynamicIndexBufferSource =
60✔
740
      indexBuffer instanceof DynamicBuffer
60!
741
        ? {source: indexBuffer, generation: indexBuffer.generation}
742
        : null;
743
    this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
60✔
744
    this.setNeedsRedraw('indexBuffer');
60✔
745
  }
746

747
  /**
748
   * Sets attributes (buffers)
749
   * @note Overrides any attributes previously set with the same name
750
   */
751
  setAttributes(buffers: Record<string, ModelBuffer>, options?: {disableWarnings?: boolean}): void {
752
    this._drawBlockedReason = false;
244✔
753
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
244✔
754
    if (buffers['indices']) {
244!
755
      log.warn(
×
756
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
757
      )();
758
    }
759

760
    // ensure bufferLayout order matches source layout so we bind
761
    // the correct buffers to the correct indices in webgpu.
762
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
244✔
763
      this.pipeline.shaderLayout,
764
      this.bufferLayout
765
    );
766
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
244✔
767

768
    // Check if all buffers have a layout
769
    for (const [bufferName, buffer] of Object.entries(buffers)) {
244✔
770
      const resolvedBuffer = buffer instanceof DynamicBuffer ? buffer.buffer : buffer;
472!
771
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
472✔
772
      if (!bufferLayout) {
472!
773
        if (!disableWarnings) {
×
774
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
775
        }
776
        continue; // eslint-disable-line no-continue
×
777
      }
778

779
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
780
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
781
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
472✔
782
      let set = false;
472✔
783
      for (const attributeName of attributeNames) {
472✔
784
        const attributeInfo = this._attributeInfos[attributeName];
472✔
785
        if (attributeInfo) {
472✔
786
          const bufferSlot =
787
            this.device.type === 'webgpu'
426✔
788
              ? this.vertexArray.getBufferSlot(attributeInfo.bufferName)
789
              : attributeInfo.location;
790
          if (bufferSlot === null) {
426!
NEW
791
            if (!disableWarnings) {
×
NEW
792
              log.warn(
×
793
                `Model(${this.id}): Missing vertex array slot for buffer "${attributeInfo.bufferName}".`
794
              )();
795
            }
NEW
796
            continue; // eslint-disable-line no-continue
×
797
          }
798

799
          this.vertexArray.setBuffer(bufferSlot, resolvedBuffer);
426✔
800
          if (buffer instanceof DynamicBuffer) {
426!
NEW
801
            this._dynamicAttributeBufferSources[bufferSlot] = {
×
802
              source: buffer,
803
              generation: buffer.generation
804
            };
805
          } else {
806
            delete this._dynamicAttributeBufferSources[bufferSlot];
426✔
807
          }
808
          set = true;
426✔
809
        }
810
      }
811
      if (!set && !disableWarnings) {
472✔
812
        log.warn(
4✔
813
          `Model(${this.id}): Ignoring buffer "${resolvedBuffer.id}" for unknown attribute "${bufferName}"`
814
        )();
815
      }
816
    }
817
    this.setNeedsRedraw('attributes');
244✔
818
  }
819

820
  /**
821
   * Sets constant attributes
822
   * @note Overrides any attributes previously set with the same name
823
   * Constant attributes are only supported in WebGL, not in WebGPU
824
   * Any attribute that is disabled in the current vertex array object
825
   * is read from the context's global constant value for that attribute location.
826
   * @param constantAttributes
827
   */
828
  setConstantAttributes(
829
    attributes: Record<string, TypedArray>,
830
    options?: {disableWarnings?: boolean}
831
  ): void {
832
    for (const [attributeName, value] of Object.entries(attributes)) {
110✔
833
      const attributeInfo = this._attributeInfos[attributeName];
×
834
      if (attributeInfo) {
×
835
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
836
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
837
        log.warn(
×
838
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
839
        )();
840
      }
841
    }
842
    this.setNeedsRedraw('constants');
110✔
843
  }
844

845
  // INTERNAL METHODS
846

847
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
848
  _areBindingsLoading(): string | false {
849
    for (const binding of Object.values(this.bindings)) {
67✔
850
      if (binding instanceof DynamicTexture && !binding.isReady) {
100!
851
        return binding.id;
×
852
      }
853
    }
854
    for (const binding of Object.values(this.material?.bindings || {})) {
67✔
855
      if (binding instanceof DynamicTexture && !binding.isReady) {
×
856
        return binding.id;
×
857
      }
858
    }
859
    return false;
67✔
860
  }
861

862
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
863
  _getBindings(): Record<string, Binding> {
864
    const validBindings: Record<string, Binding> = {};
251✔
865

866
    for (const [name, binding] of Object.entries(this.bindings)) {
251✔
867
      const resolvedBinding = resolveModelBinding(binding);
314✔
868
      if (resolvedBinding) {
314!
869
        validBindings[name] = resolvedBinding;
314✔
870
      }
871
    }
872

873
    return validBindings;
251✔
874
  }
875

876
  _getBindGroups(): BindingsByGroup {
877
    const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
183✔
878
    const bindGroups = shaderLayout.bindings.length
183✔
879
      ? normalizeBindingsByGroup(shaderLayout, this._getBindings())
880
      : {0: this._getBindings()};
881

882
    if (!this.material) {
183✔
883
      return bindGroups;
173✔
884
    }
885

886
    for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) {
10✔
887
      const group = Number(groupKey);
10✔
888
      bindGroups[group] = {
10✔
889
        ...(bindGroups[group] || {}),
20✔
890
        ...groupBindings
891
      };
892
    }
893

894
    return bindGroups;
10✔
895
  }
896

897
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
898
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
66✔
899
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
66!
900
  }
901

902
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
903
  _getBindingsUpdateTimestamp(): number {
904
    let timestamp = 0;
1✔
905
    for (const binding of Object.values(this.bindings)) {
1✔
906
      if (binding instanceof TextureView) {
×
907
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
908
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
909
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
910
      } else if (binding instanceof DynamicBuffer) {
×
911
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
912
      } else if (binding instanceof DynamicTexture) {
×
913
        timestamp = binding.texture
×
914
          ? Math.max(timestamp, binding.texture.updateTimestamp)
915
          : // The texture will become available in the future
916
            Infinity;
917
      } else if (isBufferRangeBinding(binding)) {
×
918
        timestamp = Math.max(
×
919
          timestamp,
920
          binding.buffer instanceof DynamicBuffer
×
921
            ? binding.buffer.updateTimestamp
922
            : binding.buffer.updateTimestamp
923
        );
924
      }
925
    }
926
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
1✔
927
  }
928

929
  /**
930
   * Updates the optional geometry attributes
931
   * Geometry, sets several attributes, indexBuffer, and also vertex count
932
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
933
   */
934
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
935
    // Filter geometry attribute so that we don't issue warnings for unused attributes
936
    const attributes = {...gpuGeometry.attributes};
60✔
937
    for (const [attributeName] of Object.entries(attributes)) {
60✔
938
      if (
169✔
939
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
417✔
940
        attributeName !== 'positions'
941
      ) {
942
        delete attributes[attributeName];
40✔
943
      }
944
    }
945

946
    // TODO - delete previous geometry?
947
    this.vertexCount = gpuGeometry.vertexCount;
60✔
948
    this.setIndexBuffer(gpuGeometry.indices || null);
60✔
949
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
60✔
950
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
60✔
951

952
    this.setNeedsRedraw('geometry attributes');
60✔
953
  }
954

955
  /** Mark pipeline as needing update */
956
  _setPipelineNeedsUpdate(reason: string): void {
957
    this._pipelineNeedsUpdate ||= reason;
40✔
958
    this._drawBlockedReason = false;
40✔
959
    this.setNeedsRedraw(reason);
40✔
960
  }
961

962
  /** Update pipeline if needed */
963
  _updatePipeline(): RenderPipeline {
964
    if (this._pipelineNeedsUpdate) {
270✔
965
      let prevShaderVs: Shader | null = null;
117✔
966
      let prevShaderFs: Shader | null = null;
117✔
967
      if (this.pipeline) {
117✔
968
        log.log(
7✔
969
          1,
970
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
971
        )();
972
        prevShaderVs = this.pipeline.vs;
7✔
973
        prevShaderFs = this.pipeline.fs;
7✔
974
      }
975

976
      this._pipelineNeedsUpdate = false;
117✔
977

978
      const vs = this.shaderFactory.createShader({
117✔
979
        id: `${this.id}-vertex`,
980
        stage: 'vertex',
981
        source: this.source || this.vs,
219✔
982
        debugShaders: this.props.debugShaders
983
      });
984

985
      let fs: Shader | null = null;
117✔
986
      if (this.source) {
117✔
987
        fs = vs;
15✔
988
      } else if (this.fs) {
102!
989
        fs = this.shaderFactory.createShader({
102✔
990
          id: `${this.id}-fragment`,
991
          stage: 'fragment',
992
          source: this.source || this.fs,
204✔
993
          debugShaders: this.props.debugShaders
994
        });
995
      }
996

997
      this.pipeline = this.pipelineFactory.createRenderPipeline({
117✔
998
        ...this.props,
999
        bindings: undefined,
1000
        bufferLayout: this.bufferLayout,
1001
        colorAttachmentFormats: this._colorAttachmentFormats,
1002
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
1003
        topology: this.topology,
1004
        parameters: this.parameters,
1005
        bindGroups: this._getBindGroups(),
1006
        vs,
1007
        fs
1008
      });
1009

1010
      this._attributeInfos = getAttributeInfosFromLayouts(
117✔
1011
        this.pipeline.shaderLayout,
1012
        this.bufferLayout
1013
      );
1014

1015
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
117✔
1016
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
117✔
1017
        this.shaderFactory.release(prevShaderFs);
1✔
1018
      }
1019
    }
1020
    return this.pipeline;
270✔
1021
  }
1022

1023
  /** Throttle draw call logging */
1024
  _lastLogTime = 0;
110✔
1025
  _logOpen = false;
110✔
1026

1027
  _logDrawCallStart(): void {
1028
    // IF level is 4 or higher, log every frame.
1029
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
67!
1030
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
67!
1031
      return;
67✔
1032
    }
1033

1034
    this._lastLogTime = Date.now();
×
1035
    this._logOpen = true;
×
1036

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

1040
  _logDrawCallEnd(): void {
1041
    if (this._logOpen) {
67!
1042
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1043

1044
      // log.table(logLevel, attributeTable)();
1045
      // log.table(logLevel, uniformTable)();
1046
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1047

1048
      const uniformTable = this.shaderInputs.getDebugTable();
×
1049
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1050

1051
      const attributeTable = this._getAttributeDebugTable();
×
1052
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1053
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1054

1055
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1056
      this._logOpen = false;
×
1057
    }
1058
  }
1059

1060
  protected _drawCount = 0;
110✔
1061
  _logFramebuffer(renderPass: RenderPass): void {
1062
    const debugFramebuffers = this.device.props.debugFramebuffers;
67✔
1063
    this._drawCount++;
67✔
1064
    // Update first 3 frames and then every 60 frames
1065
    if (!debugFramebuffers) {
67!
1066
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
1067
      return;
67✔
1068
    }
1069
    const framebuffer = renderPass.props.framebuffer;
×
1070
    debugFramebuffer(renderPass, framebuffer, {
×
1071
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
1072
      minimap: true
1073
    });
1074
  }
1075

1076
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1077
    const table: Record<string, Record<string, unknown>> = {};
×
1078
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
1079
      const values = this.vertexArray.attributes[attributeInfo.location];
×
1080
      table[attributeInfo.location] = {
×
1081
        name,
1082
        type: attributeInfo.shaderType,
1083
        values: values
×
1084
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
1085
          : 'null'
1086
      };
1087
    }
1088
    if (this.vertexArray.indexBuffer) {
×
1089
      const {indexBuffer} = this.vertexArray;
×
1090
      const values =
1091
        indexBuffer.indexType === 'uint32'
×
1092
          ? new Uint32Array(indexBuffer.debugData)
1093
          : new Uint16Array(indexBuffer.debugData);
1094
      table['indices'] = {
×
1095
        name: 'indices',
1096
        type: indexBuffer.indexType,
1097
        values: values.toString()
1098
      };
1099
    }
1100
    return table;
×
1101
  }
1102

1103
  // TODO - fix typing of luma data types
1104
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1105
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1106
    const typedArray =
1107
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
1108
    return typedArray.toString();
×
1109
  }
1110

1111
  private _getNonMaterialBindings(
1112
    bindings: Record<string, ModelBinding>
1113
  ): Record<string, ModelBinding> {
1114
    if (!this.material) {
93!
1115
      return bindings;
93✔
1116
    }
1117

1118
    const filteredBindings: Record<string, ModelBinding> = {};
×
1119
    for (const [name, binding] of Object.entries(bindings)) {
×
1120
      if (!this.material.ownsBinding(name)) {
×
1121
        filteredBindings[name] = binding;
×
1122
      }
1123
    }
1124
    return filteredBindings;
×
1125
  }
1126

1127
  private _syncDynamicBuffers(): void {
1128
    if (
93!
1129
      this._dynamicIndexBufferSource &&
93!
1130
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1131
    ) {
1132
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
1133
      this.indexBuffer = resolvedIndexBuffer;
×
1134
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
1135
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
1136
      this.setNeedsRedraw('dynamic index buffer');
×
1137
    }
1138

1139
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
93✔
1140
      if (entry.generation !== entry.source.generation) {
×
1141
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
1142
        entry.generation = entry.source.generation;
×
1143
        this.setNeedsRedraw('dynamic attribute buffer');
×
1144
      }
1145
    }
1146
  }
1147
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1148
    if (this.device.type !== 'webgpu') {
67✔
1149
      return;
58✔
1150
    }
1151

1152
    const framebuffer =
1153
      (
1154
        renderPass as RenderPass & {
9!
1155
          framebuffer?: {
1156
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1157
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1158
          };
1159
        }
1160
      ).framebuffer || renderPass.props.framebuffer;
1161

1162
    const nextColorAttachmentFormats = framebuffer?.colorAttachments?.map(colorAttachment =>
67✔
1163
      asColorAttachmentFormat(colorAttachment?.texture?.format)
9✔
1164
    );
1165
    const nextDepthStencilAttachmentFormat = asDepthStencilAttachmentFormat(
67✔
1166
      framebuffer?.depthStencilAttachment?.texture?.format
1167
    );
1168

1169
    if (
67✔
1170
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
70✔
1171
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1172
    ) {
1173
      this._colorAttachmentFormats = nextColorAttachmentFormats;
6✔
1174
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
6✔
1175
      this._setPipelineNeedsUpdate('attachment formats');
6✔
1176
    }
1177
  }
1178
}
1179

1180
// HELPERS
1181

1182
function resolveModelBinding(binding: ModelBinding): Binding | null {
1183
  if (binding instanceof DynamicTexture) {
314!
1184
    return binding.isReady ? binding.texture : null;
×
1185
  }
1186

1187
  if (binding instanceof DynamicBuffer) {
314✔
1188
    return binding.buffer;
2✔
1189
  }
1190

1191
  if (isBufferRangeBinding(binding)) {
312!
1192
    return resolveBufferRangeBinding(binding);
×
1193
  }
1194

1195
  return binding;
312✔
1196
}
1197
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1198
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
9!
1199
}
1200

1201
function asDepthStencilAttachmentFormat(
1202
  format?: string | null
1203
): TextureFormatDepthStencil | undefined {
1204
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
9!
1205
}
1206

1207
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1208
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
9✔
1209
}
1210

1211
/** Create a shadertools platform info from the Device */
1212
export function getPlatformInfo(device: Device): PlatformInfo {
1213
  return {
110✔
1214
    type: device.type,
1215
    shaderLanguage: device.info.shadingLanguage,
1216
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1217
    gpu: device.info.gpu,
1218
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1219
    features: device.features as unknown as Set<DeviceFeature>
1220
  };
1221
}
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