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

visgl / luma.gl / 25635395852

10 May 2026 05:41PM UTC coverage: 74.161% (-0.06%) from 74.218%
25635395852

push

github

web-flow
feat(engine) DynamicBuffer (#2587)

5267 of 8041 branches covered (65.5%)

Branch coverage included in aggregate %.

148 of 207 new or added lines in 5 files covered. (71.5%)

11853 of 15044 relevant lines covered (78.79%)

771.7 hits per line

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

71.05
/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
  getLogicalBufferSlots,
35
  normalizeBindingsByGroup
36
} from '@luma.gl/core';
37

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

41
import type {Geometry} from '../geometry/geometry';
42
import {GPUGeometry, makeGPUGeometry} from '../geometry/gpu-geometry';
43
import {getDebugTableForShaderLayout} from '../debug/debug-shader-layout';
44
import {debugFramebuffer} from '../debug/debug-framebuffer';
45
import {deepEqual} from '../utils/deep-equal';
46
import {BufferLayoutHelper} from '../utils/buffer-layout-helper';
47
import {sortedBufferLayoutByShaderSourceLocations} from '../utils/buffer-layout-order';
48
import {
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;
72✔
64
const LOG_DRAW_TIMEOUT = 10000;
72✔
65
const PIPELINE_INITIALIZATION_FAILED = 'render pipeline initialization failed';
72✔
66
const DEPTH_STENCIL_ATTACHMENT_FORMATS: TextureFormatDepthStencil[] = [
72✔
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> = {
72✔
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} = {};
104✔
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;
104✔
218
  /** instance count. `undefined` means not instanced */
219
  instanceCount: number = 0;
104✔
220
  /** Vertex count */
221
  vertexCount: number;
222

223
  /** Index buffer */
224
  indexBuffer: Buffer | null = null;
104✔
225
  /** Buffer-valued attributes */
226
  bufferAttributes: Record<string, Buffer> = {};
104✔
227
  /** Constant-valued attributes */
228
  constantAttributes: Record<string, TypedArray> = {};
104✔
229
  /** Bindings (textures, samplers, uniform buffers) */
230
  bindings: Record<string, ModelBinding> = {};
104✔
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;
104✔
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;
104✔
249
  // @ts-expect-error Assigned in function called by constructor
250
  _uniformStore: UniformStore;
251

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

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

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

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

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

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

285
    Object.assign(this.userData, props.userData);
104✔
286

287
    this.material = props.material || null;
104✔
288

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

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

300
    // Setup shader assembler
301
    const platformInfo = getPlatformInfo(device);
104✔
302

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

308
    this.props.shaderLayout =
104✔
309
      mergeShaderModuleBindingsIntoLayout(this.props.shaderLayout, modules) || null;
208✔
310

311
    const isWebGPU = this.device.type === 'webgpu';
104✔
312

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

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

351
    this.vertexCount = this.props.vertexCount;
104✔
352
    this.instanceCount = this.props.instanceCount;
104✔
353

354
    this.topology = this.props.topology;
104✔
355
    this.bufferLayout = this.props.bufferLayout;
104✔
356
    this.parameters = this.props.parameters;
104✔
357

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

363
    this.pipelineFactory =
104✔
364
      props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
208✔
365
    this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
104✔
366

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

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

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

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

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

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

425
  // Draw call
426

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

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

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

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

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

475
    this._syncAttachmentFormats(renderPass);
68✔
476

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

496
    let drawSuccess: boolean;
497
    let pipelineErrored = this.pipeline.isErrored;
68✔
498
    try {
68✔
499
      renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
68✔
500
      this._logDrawCallStart();
68✔
501

502
      // Update the pipeline if invalidated
503
      // TODO - inside RenderPass is likely the worst place to do this from performance perspective.
504
      // Application can call Model.predraw() to avoid this.
505
      this.pipeline = this._updatePipeline();
68✔
506
      pipelineErrored = this.pipeline.isErrored;
68✔
507

508
      if (pipelineErrored) {
68✔
509
        log.info(
2✔
510
          LOG_DRAW_PRIORITY,
511
          `>>> DRAWING ABORTED ${this.id}: ${PIPELINE_INITIALIZATION_FAILED}`
512
        )();
513
        drawSuccess = false;
2✔
514
      } else {
515
        const syncBindings = this._getBindings();
66✔
516
        const syncBindGroups = this._getBindGroups();
66✔
517

518
        const {indexBuffer} = this.vertexArray;
66✔
519
        const indexCount = indexBuffer
66✔
520
          ? indexBuffer.byteLength / (indexBuffer.indexType === 'uint32' ? 4 : 2)
12!
521
          : undefined;
522

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

551
    // Update needsRedraw flag
552
    if (drawSuccess) {
68✔
553
      this._lastDrawTimestamp = this.device.timestamp;
66✔
554
      this._needsRedraw = false;
66✔
555
    } else if (pipelineErrored) {
2!
556
      this._needsRedraw = PIPELINE_INITIALIZATION_FAILED;
2✔
557
    } else {
558
      this._needsRedraw = 'waiting for resource initialization';
×
559
    }
560
    return drawSuccess;
68✔
561
  }
562

563
  // Update fixed fields (can trigger pipeline rebuild)
564

565
  /**
566
   * Updates the optional geometry
567
   * Geometry, set topology and bufferLayout
568
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
569
   */
570
  setGeometry(geometry: GPUGeometry | Geometry | null): void {
571
    this._gpuGeometry?.destroy();
60✔
572
    const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
60✔
573
    if (gpuGeometry) {
60!
574
      this.setTopology(gpuGeometry.topology || 'triangle-list');
60!
575
      const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
60✔
576
      this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(
60✔
577
        gpuGeometry.bufferLayout,
578
        this.bufferLayout
579
      );
580
      if (this.vertexArray) {
60!
581
        this._setGeometryAttributes(gpuGeometry);
×
582
      }
583
    }
584
    this._gpuGeometry = gpuGeometry;
60✔
585
  }
586

587
  /**
588
   * Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
589
   * @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
590
   */
591
  setTopology(topology: PrimitiveTopology): void {
592
    if (topology !== this.topology) {
63✔
593
      this.topology = topology;
34✔
594
      this._setPipelineNeedsUpdate('topology');
34✔
595
    }
596
  }
597

598
  /**
599
   * Updates the buffer layout.
600
   * @note Triggers a pipeline rebuild / pipeline cache fetch
601
   */
602
  setBufferLayout(bufferLayout: BufferLayout[]): void {
603
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
×
604
    this.bufferLayout = this._gpuGeometry
×
605
      ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout)
606
      : bufferLayout;
607
    this._setPipelineNeedsUpdate('bufferLayout');
×
608

609
    // Recreate the pipeline
610
    this.pipeline = this._updatePipeline();
×
611

612
    // vertex array needs to be updated if we update buffer layout,
613
    // but not if we update parameters
614
    this.vertexArray = this.device.createVertexArray({
×
615
      shaderLayout: this.pipeline.shaderLayout,
616
      bufferLayout: this.pipeline.bufferLayout
617
    });
618

619
    // Reapply geometry attributes to the new vertex array
620
    if (this._gpuGeometry) {
×
621
      this._setGeometryAttributes(this._gpuGeometry);
×
622
    }
623
  }
624

625
  /**
626
   * Set GPU parameters.
627
   * @note Can trigger a pipeline rebuild / pipeline cache fetch.
628
   * @param parameters
629
   */
630
  setParameters(parameters: RenderPipelineParameters) {
631
    if (!deepEqual(parameters, this.parameters, 2)) {
×
632
      this.parameters = parameters;
×
633
      this._setPipelineNeedsUpdate('parameters');
×
634
    }
635
  }
636

637
  // Update dynamic fields
638

639
  /**
640
   * Updates the instance count (used in draw calls)
641
   * @note Any attributes with stepMode=instance need to be at least this big
642
   */
643
  setInstanceCount(instanceCount: number): void {
644
    this.instanceCount = instanceCount;
24✔
645
    // luma.gl examples don't set props.isInstanced and rely on auto-detection
646
    // but deck.gl sets instanceCount even for models that are not instanced.
647
    if (this.isInstanced === undefined && instanceCount > 0) {
24✔
648
      this.isInstanced = true;
17✔
649
    }
650
    this.setNeedsRedraw('instanceCount');
24✔
651
  }
652

653
  /**
654
   * Updates the vertex count (used in draw calls)
655
   * @note Any attributes with stepMode=vertex need to be at least this big
656
   */
657
  setVertexCount(vertexCount: number): void {
658
    this.vertexCount = vertexCount;
73✔
659
    this.setNeedsRedraw('vertexCount');
73✔
660
  }
661

662
  /** Set the shader inputs */
663
  setShaderInputs(shaderInputs: ShaderInputs): void {
664
    this.shaderInputs = shaderInputs;
104✔
665
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
104✔
666
    // Create uniform buffer bindings for all modules that actually have uniforms
667
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
104✔
668
      if (shaderModuleHasUniforms(module) && !this.material?.ownsModule(moduleName)) {
147✔
669
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
109✔
670
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
109✔
671
      }
672
    }
673
    this.setNeedsRedraw('shaderInputs');
104✔
674
  }
675

676
  setMaterial(material: Material | null): void {
677
    this.material = material;
×
678
    this.setNeedsRedraw('material');
×
679
  }
680

681
  /** Update uniform buffers from the model's shader inputs */
682
  /**
683
   * Flushes current shader-input values into managed uniform buffers and
684
   * non-material bindings.
685
   *
686
   * @param commandEncoder - Optional encoder used to order uniform uploads with
687
   * subsequent draw commands.
688
   */
689
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
690
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
94✔
691
    this.setBindings(this._getNonMaterialBindings(this.shaderInputs.getBindingValues()));
94✔
692
    // TODO - this is already tracked through buffer/texture update times?
693
    this.setNeedsRedraw('shaderInputs');
94✔
694
  }
695

696
  /**
697
   * Sets bindings (textures, samplers, uniform buffers)
698
   */
699
  setBindings(bindings: Record<string, ModelBinding>): void {
700
    Object.assign(this.bindings, bindings);
239✔
701
    this.setNeedsRedraw('bindings');
239✔
702
  }
703

704
  /**
705
   * Updates optional transform feedback. WebGL only.
706
   */
707
  setTransformFeedback(transformFeedback: TransformFeedback | null): void {
708
    this.transformFeedback = transformFeedback;
20✔
709
    this.setNeedsRedraw('transformFeedback');
20✔
710
  }
711

712
  /**
713
   * Sets the index buffer
714
   * @todo - how to unset it if we change geometry?
715
   */
716
  setIndexBuffer(indexBuffer: ModelBuffer | null): void {
717
    const resolvedIndexBuffer =
718
      indexBuffer instanceof DynamicBuffer ? indexBuffer.buffer : indexBuffer;
60!
719
    this.indexBuffer = resolvedIndexBuffer;
60✔
720
    this._dynamicIndexBufferSource =
60✔
721
      indexBuffer instanceof DynamicBuffer
60!
722
        ? {source: indexBuffer, generation: indexBuffer.generation}
723
        : null;
724
    this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
60✔
725
    this.setNeedsRedraw('indexBuffer');
60✔
726
  }
727

728
  /**
729
   * Sets attributes (buffers)
730
   * @note Overrides any attributes previously set with the same name
731
   */
732
  setAttributes(buffers: Record<string, ModelBuffer>, options?: {disableWarnings?: boolean}): void {
733
    const disableWarnings = options?.disableWarnings ?? this.props.disableWarnings;
237✔
734
    if (buffers['indices']) {
237!
735
      log.warn(
×
736
        `Model:${this.id} setAttributes() - indexBuffer should be set using setIndexBuffer()`
737
      )();
738
    }
739

740
    // ensure bufferLayout order matches source layout so we bind
741
    // the correct buffers to the correct indices in webgpu.
742
    this.bufferLayout = sortedBufferLayoutByShaderSourceLocations(
237✔
743
      this.pipeline.shaderLayout,
744
      this.bufferLayout
745
    );
746
    const bufferLayoutHelper = new BufferLayoutHelper(this.bufferLayout);
237✔
747
    const logicalBufferSlots = getLogicalBufferSlots(this.pipeline.shaderLayout, this.bufferLayout);
237✔
748

749
    // Check if all buffers have a layout
750
    for (const [bufferName, buffer] of Object.entries(buffers)) {
237✔
751
      const resolvedBuffer = buffer instanceof DynamicBuffer ? buffer.buffer : buffer;
462!
752
      const bufferLayout = bufferLayoutHelper.getBufferLayout(bufferName);
462✔
753
      if (!bufferLayout) {
462!
754
        if (!disableWarnings) {
×
755
          log.warn(`Model(${this.id}): Missing layout for buffer "${bufferName}".`)();
×
756
        }
757
        continue; // eslint-disable-line no-continue
×
758
      }
759

760
      // In WebGL, for an interleaved attribute we may need to set multiple attributes
761
      // but in WebGPU, we set it according to the buffer's position in the vertexArray
762
      const attributeNames = bufferLayoutHelper.getAttributeNamesForBuffer(bufferLayout);
462✔
763
      let set = false;
462✔
764
      for (const attributeName of attributeNames) {
462✔
765
        const attributeInfo = this._attributeInfos[attributeName];
462✔
766
        if (attributeInfo) {
462✔
767
          const location =
768
            this.device.type === 'webgpu'
416✔
769
              ? logicalBufferSlots[attributeInfo.bufferName]
770
              : attributeInfo.location;
771

772
          this.vertexArray.setBuffer(location, resolvedBuffer);
416✔
773
          if (buffer instanceof DynamicBuffer) {
416!
NEW
774
            this._dynamicAttributeBufferSources[location] = {
×
775
              source: buffer,
776
              generation: buffer.generation
777
            };
778
          } else {
779
            delete this._dynamicAttributeBufferSources[location];
416✔
780
          }
781
          set = true;
416✔
782
        }
783
      }
784
      if (!set && !disableWarnings) {
462✔
785
        log.warn(
4✔
786
          `Model(${this.id}): Ignoring buffer "${resolvedBuffer.id}" for unknown attribute "${bufferName}"`
787
        )();
788
      }
789
    }
790
    this.setNeedsRedraw('attributes');
237✔
791
  }
792

793
  /**
794
   * Sets constant attributes
795
   * @note Overrides any attributes previously set with the same name
796
   * Constant attributes are only supported in WebGL, not in WebGPU
797
   * Any attribute that is disabled in the current vertex array object
798
   * is read from the context's global constant value for that attribute location.
799
   * @param constantAttributes
800
   */
801
  setConstantAttributes(
802
    attributes: Record<string, TypedArray>,
803
    options?: {disableWarnings?: boolean}
804
  ): void {
805
    for (const [attributeName, value] of Object.entries(attributes)) {
104✔
806
      const attributeInfo = this._attributeInfos[attributeName];
×
807
      if (attributeInfo) {
×
808
        this.vertexArray.setConstantWebGL(attributeInfo.location, value);
×
809
      } else if (!(options?.disableWarnings ?? this.props.disableWarnings)) {
×
810
        log.warn(
×
811
          `Model "${this.id}: Ignoring constant supplied for unknown attribute "${attributeName}"`
812
        )();
813
      }
814
    }
815
    this.setNeedsRedraw('constants');
104✔
816
  }
817

818
  // INTERNAL METHODS
819

820
  /** Check that bindings are loaded. Returns id of first binding that is still loading. */
821
  _areBindingsLoading(): string | false {
822
    for (const binding of Object.values(this.bindings)) {
68✔
823
      if (binding instanceof DynamicTexture && !binding.isReady) {
100!
824
        return binding.id;
×
825
      }
826
    }
827
    for (const binding of Object.values(this.material?.bindings || {})) {
68✔
828
      if (binding instanceof DynamicTexture && !binding.isReady) {
×
829
        return binding.id;
×
830
      }
831
    }
832
    return false;
68✔
833
  }
834

835
  /** Extracts texture view from loaded async textures. Returns null if any textures have not yet been loaded. */
836
  _getBindings(): Record<string, Binding> {
837
    const validBindings: Record<string, Binding> = {};
245✔
838

839
    for (const [name, binding] of Object.entries(this.bindings)) {
245✔
840
      const resolvedBinding = resolveModelBinding(binding);
314✔
841
      if (resolvedBinding) {
314!
842
        validBindings[name] = resolvedBinding;
314✔
843
      }
844
    }
845

846
    return validBindings;
245✔
847
  }
848

849
  _getBindGroups(): BindingsByGroup {
850
    const shaderLayout = this.pipeline?.shaderLayout || this.props.shaderLayout || {bindings: []};
177✔
851
    const bindGroups = shaderLayout.bindings.length
177✔
852
      ? normalizeBindingsByGroup(shaderLayout, this._getBindings())
853
      : {0: this._getBindings()};
854

855
    if (!this.material) {
177✔
856
      return bindGroups;
167✔
857
    }
858

859
    for (const [groupKey, groupBindings] of Object.entries(this.material.getBindingsByGroup())) {
10✔
860
      const group = Number(groupKey);
10✔
861
      bindGroups[group] = {
10✔
862
        ...(bindGroups[group] || {}),
20✔
863
        ...groupBindings
864
      };
865
    }
866

867
    return bindGroups;
10✔
868
  }
869

870
  _getBindGroupCacheKeys(): Partial<Record<number, object>> {
871
    const bindGroupCacheKey = this.material?.getBindGroupCacheKey(3);
66✔
872
    return bindGroupCacheKey ? {3: bindGroupCacheKey} : {};
66!
873
  }
874

875
  /** Get the timestamp of the latest updated bound GPU memory resource (buffer/texture). */
876
  _getBindingsUpdateTimestamp(): number {
877
    let timestamp = 0;
1✔
878
    for (const binding of Object.values(this.bindings)) {
1✔
879
      if (binding instanceof TextureView) {
×
880
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
881
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
882
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
NEW
883
      } else if (binding instanceof DynamicBuffer) {
×
NEW
884
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
885
      } else if (binding instanceof DynamicTexture) {
×
886
        timestamp = binding.texture
×
887
          ? Math.max(timestamp, binding.texture.updateTimestamp)
888
          : // The texture will become available in the future
889
            Infinity;
NEW
890
      } else if (isBufferRangeBinding(binding)) {
×
NEW
891
        timestamp = Math.max(
×
892
          timestamp,
893
          binding.buffer instanceof DynamicBuffer
×
894
            ? binding.buffer.updateTimestamp
895
            : binding.buffer.updateTimestamp
896
        );
897
      }
898
    }
899
    return Math.max(timestamp, this.material?.getBindingsUpdateTimestamp() || 0);
1✔
900
  }
901

902
  /**
903
   * Updates the optional geometry attributes
904
   * Geometry, sets several attributes, indexBuffer, and also vertex count
905
   * @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
906
   */
907
  _setGeometryAttributes(gpuGeometry: GPUGeometry): void {
908
    // Filter geometry attribute so that we don't issue warnings for unused attributes
909
    const attributes = {...gpuGeometry.attributes};
60✔
910
    for (const [attributeName] of Object.entries(attributes)) {
60✔
911
      if (
169✔
912
        !this.pipeline.shaderLayout.attributes.find(layout => layout.name === attributeName) &&
417✔
913
        attributeName !== 'positions'
914
      ) {
915
        delete attributes[attributeName];
40✔
916
      }
917
    }
918

919
    // TODO - delete previous geometry?
920
    this.vertexCount = gpuGeometry.vertexCount;
60✔
921
    this.setIndexBuffer(gpuGeometry.indices || null);
60✔
922
    this.setAttributes(gpuGeometry.attributes, {disableWarnings: true});
60✔
923
    this.setAttributes(attributes, {disableWarnings: this.props.disableWarnings});
60✔
924

925
    this.setNeedsRedraw('geometry attributes');
60✔
926
  }
927

928
  /** Mark pipeline as needing update */
929
  _setPipelineNeedsUpdate(reason: string): void {
930
    this._pipelineNeedsUpdate ||= reason;
40✔
931
    this.setNeedsRedraw(reason);
40✔
932
  }
933

934
  /** Update pipeline if needed */
935
  _updatePipeline(): RenderPipeline {
936
    if (this._pipelineNeedsUpdate) {
266✔
937
      let prevShaderVs: Shader | null = null;
111✔
938
      let prevShaderFs: Shader | null = null;
111✔
939
      if (this.pipeline) {
111✔
940
        log.log(
7✔
941
          1,
942
          `Model ${this.id}: Recreating pipeline because "${this._pipelineNeedsUpdate}".`
943
        )();
944
        prevShaderVs = this.pipeline.vs;
7✔
945
        prevShaderFs = this.pipeline.fs;
7✔
946
      }
947

948
      this._pipelineNeedsUpdate = false;
111✔
949

950
      const vs = this.shaderFactory.createShader({
111✔
951
        id: `${this.id}-vertex`,
952
        stage: 'vertex',
953
        source: this.source || this.vs,
208✔
954
        debugShaders: this.props.debugShaders
955
      });
956

957
      let fs: Shader | null = null;
111✔
958
      if (this.source) {
111✔
959
        fs = vs;
14✔
960
      } else if (this.fs) {
97!
961
        fs = this.shaderFactory.createShader({
97✔
962
          id: `${this.id}-fragment`,
963
          stage: 'fragment',
964
          source: this.source || this.fs,
194✔
965
          debugShaders: this.props.debugShaders
966
        });
967
      }
968

969
      this.pipeline = this.pipelineFactory.createRenderPipeline({
111✔
970
        ...this.props,
971
        bindings: undefined,
972
        bufferLayout: this.bufferLayout,
973
        colorAttachmentFormats: this._colorAttachmentFormats,
974
        depthStencilAttachmentFormat: this._depthStencilAttachmentFormat,
975
        topology: this.topology,
976
        parameters: this.parameters,
977
        bindGroups: this._getBindGroups(),
978
        vs,
979
        fs
980
      });
981

982
      this._attributeInfos = getAttributeInfosFromLayouts(
111✔
983
        this.pipeline.shaderLayout,
984
        this.bufferLayout
985
      );
986

987
      if (prevShaderVs) this.shaderFactory.release(prevShaderVs);
111✔
988
      if (prevShaderFs && prevShaderFs !== prevShaderVs) {
111✔
989
        this.shaderFactory.release(prevShaderFs);
1✔
990
      }
991
    }
992
    return this.pipeline;
266✔
993
  }
994

995
  /** Throttle draw call logging */
996
  _lastLogTime = 0;
104✔
997
  _logOpen = false;
104✔
998

999
  _logDrawCallStart(): void {
1000
    // IF level is 4 or higher, log every frame.
1001
    const logDrawTimeout = log.level > 3 ? 0 : LOG_DRAW_TIMEOUT;
68!
1002
    if (log.level < 2 || Date.now() - this._lastLogTime < logDrawTimeout) {
68!
1003
      return;
68✔
1004
    }
1005

1006
    this._lastLogTime = Date.now();
×
1007
    this._logOpen = true;
×
1008

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

1012
  _logDrawCallEnd(): void {
1013
    if (this._logOpen) {
68!
1014
      const shaderLayoutTable = getDebugTableForShaderLayout(this.pipeline.shaderLayout, this.id);
×
1015

1016
      // log.table(logLevel, attributeTable)();
1017
      // log.table(logLevel, uniformTable)();
1018
      log.table(LOG_DRAW_PRIORITY, shaderLayoutTable)();
×
1019

1020
      const uniformTable = this.shaderInputs.getDebugTable();
×
1021
      log.table(LOG_DRAW_PRIORITY, uniformTable)();
×
1022

1023
      const attributeTable = this._getAttributeDebugTable();
×
1024
      log.table(LOG_DRAW_PRIORITY, this._attributeInfos)();
×
1025
      log.table(LOG_DRAW_PRIORITY, attributeTable)();
×
1026

1027
      log.groupEnd(LOG_DRAW_PRIORITY)();
×
1028
      this._logOpen = false;
×
1029
    }
1030
  }
1031

1032
  protected _drawCount = 0;
104✔
1033
  _logFramebuffer(renderPass: RenderPass): void {
1034
    const debugFramebuffers = this.device.props.debugFramebuffers;
68✔
1035
    this._drawCount++;
68✔
1036
    // Update first 3 frames and then every 60 frames
1037
    if (!debugFramebuffers) {
68!
1038
      // } || (this._drawCount++ > 3 && this._drawCount % 60)) {
1039
      return;
68✔
1040
    }
1041
    const framebuffer = renderPass.props.framebuffer;
×
1042
    debugFramebuffer(renderPass, framebuffer, {
×
1043
      id: framebuffer?.id || `${this.id}-framebuffer`,
×
1044
      minimap: true
1045
    });
1046
  }
1047

1048
  _getAttributeDebugTable(): Record<string, Record<string, unknown>> {
1049
    const table: Record<string, Record<string, unknown>> = {};
×
1050
    for (const [name, attributeInfo] of Object.entries(this._attributeInfos)) {
×
1051
      const values = this.vertexArray.attributes[attributeInfo.location];
×
1052
      table[attributeInfo.location] = {
×
1053
        name,
1054
        type: attributeInfo.shaderType,
1055
        values: values
×
1056
          ? this._getBufferOrConstantValues(values, attributeInfo.bufferDataType)
1057
          : 'null'
1058
      };
1059
    }
1060
    if (this.vertexArray.indexBuffer) {
×
1061
      const {indexBuffer} = this.vertexArray;
×
1062
      const values =
1063
        indexBuffer.indexType === 'uint32'
×
1064
          ? new Uint32Array(indexBuffer.debugData)
1065
          : new Uint16Array(indexBuffer.debugData);
1066
      table['indices'] = {
×
1067
        name: 'indices',
1068
        type: indexBuffer.indexType,
1069
        values: values.toString()
1070
      };
1071
    }
1072
    return table;
×
1073
  }
1074

1075
  // TODO - fix typing of luma data types
1076
  _getBufferOrConstantValues(attribute: Buffer | TypedArray, dataType: any): string {
1077
    const TypedArrayConstructor = dataTypeDecoder.getTypedArrayConstructor(dataType);
×
1078
    const typedArray =
1079
      attribute instanceof Buffer ? new TypedArrayConstructor(attribute.debugData) : attribute;
×
1080
    return typedArray.toString();
×
1081
  }
1082

1083
  private _getNonMaterialBindings(
1084
    bindings: Record<string, ModelBinding>
1085
  ): Record<string, ModelBinding> {
1086
    if (!this.material) {
94!
1087
      return bindings;
94✔
1088
    }
1089

1090
    const filteredBindings: Record<string, ModelBinding> = {};
×
1091
    for (const [name, binding] of Object.entries(bindings)) {
×
1092
      if (!this.material.ownsBinding(name)) {
×
1093
        filteredBindings[name] = binding;
×
1094
      }
1095
    }
1096
    return filteredBindings;
×
1097
  }
1098

1099
  private _syncDynamicBuffers(): void {
1100
    if (
94!
1101
      this._dynamicIndexBufferSource &&
94!
1102
      this._dynamicIndexBufferSource.generation !== this._dynamicIndexBufferSource.source.generation
1103
    ) {
NEW
1104
      const resolvedIndexBuffer = this._dynamicIndexBufferSource.source.buffer;
×
NEW
1105
      this.indexBuffer = resolvedIndexBuffer;
×
NEW
1106
      this.vertexArray.setIndexBuffer(resolvedIndexBuffer);
×
NEW
1107
      this._dynamicIndexBufferSource.generation = this._dynamicIndexBufferSource.source.generation;
×
NEW
1108
      this.setNeedsRedraw('dynamic index buffer');
×
1109
    }
1110

1111
    for (const [locationKey, entry] of Object.entries(this._dynamicAttributeBufferSources)) {
94✔
NEW
1112
      if (entry.generation !== entry.source.generation) {
×
NEW
1113
        this.vertexArray.setBuffer(Number(locationKey), entry.source.buffer);
×
NEW
1114
        entry.generation = entry.source.generation;
×
NEW
1115
        this.setNeedsRedraw('dynamic attribute buffer');
×
1116
      }
1117
    }
1118
  }
1119
  private _syncAttachmentFormats(renderPass: RenderPass): void {
1120
    if (this.device.type !== 'webgpu') {
68✔
1121
      return;
58✔
1122
    }
1123

1124
    const framebuffer =
1125
      (
1126
        renderPass as RenderPass & {
10!
1127
          framebuffer?: {
1128
            colorAttachments?: Array<{texture?: {format?: TextureFormatColor}} | null>;
1129
            depthStencilAttachment?: {texture?: {format?: TextureFormatDepthStencil}} | null;
1130
          };
1131
        }
1132
      ).framebuffer || renderPass.props.framebuffer;
1133

1134
    const nextColorAttachmentFormats = framebuffer?.colorAttachments?.map(colorAttachment =>
68✔
1135
      asColorAttachmentFormat(colorAttachment?.texture?.format)
10✔
1136
    );
1137
    const nextDepthStencilAttachmentFormat = asDepthStencilAttachmentFormat(
68✔
1138
      framebuffer?.depthStencilAttachment?.texture?.format
1139
    );
1140

1141
    if (
68✔
1142
      !deepEqual(this._colorAttachmentFormats, nextColorAttachmentFormats, 1) ||
72✔
1143
      this._depthStencilAttachmentFormat !== nextDepthStencilAttachmentFormat
1144
    ) {
1145
      this._colorAttachmentFormats = nextColorAttachmentFormats;
6✔
1146
      this._depthStencilAttachmentFormat = nextDepthStencilAttachmentFormat;
6✔
1147
      this._setPipelineNeedsUpdate('attachment formats');
6✔
1148
    }
1149
  }
1150
}
1151

1152
// HELPERS
1153

1154
function resolveModelBinding(binding: ModelBinding): Binding | null {
1155
  if (binding instanceof DynamicTexture) {
314!
NEW
1156
    return binding.isReady ? binding.texture : null;
×
1157
  }
1158

1159
  if (binding instanceof DynamicBuffer) {
314✔
1160
    return binding.buffer;
2✔
1161
  }
1162

1163
  if (isBufferRangeBinding(binding)) {
312!
NEW
1164
    return resolveBufferRangeBinding(binding);
×
1165
  }
1166

1167
  return binding;
312✔
1168
}
1169
function asColorAttachmentFormat(format?: string | null): TextureFormatColor | null {
1170
  return format && !isDepthStencilAttachmentFormat(format) ? (format as TextureFormatColor) : null;
10!
1171
}
1172

1173
function asDepthStencilAttachmentFormat(
1174
  format?: string | null
1175
): TextureFormatDepthStencil | undefined {
1176
  return format && isDepthStencilAttachmentFormat(format) ? format : undefined;
10!
1177
}
1178

1179
function isDepthStencilAttachmentFormat(format: string): format is TextureFormatDepthStencil {
1180
  return DEPTH_STENCIL_ATTACHMENT_FORMATS.includes(format as TextureFormatDepthStencil);
10✔
1181
}
1182

1183
/** Create a shadertools platform info from the Device */
1184
export function getPlatformInfo(device: Device): PlatformInfo {
1185
  return {
104✔
1186
    type: device.type,
1187
    shaderLanguage: device.info.shadingLanguage,
1188
    shaderLanguageVersion: device.info.shadingLanguageVersion as 100 | 300,
1189
    gpu: device.info.gpu,
1190
    // HACK - we pretend that the DeviceFeatures is a Set, it has a similar API
1191
    features: device.features as unknown as Set<DeviceFeature>
1192
  };
1193
}
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