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

visgl / luma.gl / 28112330114

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

push

github

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

10077 of 16053 branches covered (62.77%)

Branch coverage included in aggregate %.

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

14 existing lines in 5 files now uncovered.

20430 of 27128 relevant lines covered (75.31%)

4011.66 hits per line

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

61.93
/modules/engine/src/material/material.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {
6
  Binding,
7
  BindingsByGroup,
8
  CommandEncoder,
9
  ComputeShaderLayout,
10
  Device,
11
  ShaderLayout
12
} from '@luma.gl/core';
13
import {Buffer, ExternalTexture, Texture, TextureView, UniformStore} from '@luma.gl/core';
14
import type {ShaderModule} from '@luma.gl/shadertools';
15
import {
16
  DynamicBuffer,
17
  type DynamicBufferRange,
18
  getDynamicBufferFromBinding,
19
  isBufferRangeBinding,
20
  resolveBufferRangeBinding
21
} from '../dynamic-buffer/dynamic-buffer';
22
import {
23
  getTextureBindingLayout,
24
  isTextureBindingSource,
25
  type TextureBindingSource
26
} from '../dynamic-texture/texture-binding-source';
27
import {ShaderInputs} from '../shader-inputs';
28
import {shaderModuleHasUniforms} from '../utils/shader-module-utils';
29
import {uid} from '../utils/uid';
30
import {
31
  getModuleNameFromUniformBinding,
32
  MATERIAL_BIND_GROUP,
33
  MaterialFactory
34
} from './material-factory';
35

36
type MaterialModuleProps = Partial<Record<string, Record<string, unknown>>>;
37
/** Resource accepted by one material binding slot. */
38
type MaterialBinding = Binding | TextureBindingSource | DynamicBuffer | DynamicBufferRange;
39
type MaterialBindings = Record<string, MaterialBinding>;
40
/** Shader layout subset needed while resolving texture binding sources. */
41
type AnyShaderLayout = Pick<ShaderLayout | ComputeShaderLayout, 'bindings'>;
42
type MaterialPropsUpdate<TModuleProps extends MaterialModuleProps> = Partial<{
43
  [P in keyof TModuleProps]?: Partial<TModuleProps[P]>;
44
}>;
45

46
/** Construction props for one typed {@link Material}. */
47
export type MaterialProps<
48
  TModuleProps extends MaterialModuleProps = MaterialModuleProps,
49
  TBindings extends MaterialBindings = MaterialBindings
50
> = {
51
  /** Optional application-provided identifier. */
52
  id?: string;
53
  /** Factory that owns the material schema. */
54
  factory?: MaterialFactory<TModuleProps, TBindings>;
55
  /** Optional pre-created shader inputs for the material modules. */
56
  shaderInputs?: ShaderInputs<TModuleProps>;
57
  /** Shader modules used when a factory is not supplied. */
58
  modules?: ShaderModule[];
59
  /** Initial material-owned resource bindings. */
60
  bindings?: Partial<TBindings>;
61
};
62

63
/** Structural overrides applied when cloning a {@link Material}. */
64
export type MaterialCloneProps<
65
  TModuleProps extends MaterialModuleProps = MaterialModuleProps,
66
  TBindings extends MaterialBindings = MaterialBindings
67
> = {
68
  /** Optional identifier for the cloned material. */
69
  id?: string;
70
  /** Replacement material-owned resource bindings. */
71
  bindings?: Partial<TBindings>;
72
  /** Additional uniform/module props applied to the clone. */
73
  moduleProps?: MaterialPropsUpdate<TModuleProps>;
74
  /** Optional full replacement shader-input store. */
75
  shaderInputs?: ShaderInputs<TModuleProps>;
76
};
77

78
/**
79
 * Material owns bind group `3` resources and uniforms for one material instance.
80
 *
81
 * `setProps()` mutates uniform values in place. Structural resource changes are
82
 * expressed through `clone({...})`, which creates a new material identity.
83
 */
84
export class Material<
85
  TModuleProps extends MaterialModuleProps = MaterialModuleProps,
86
  TBindings extends MaterialBindings = MaterialBindings
87
> {
88
  /** Application-provided identifier. */
89
  readonly id: string;
90
  /** Device that owns the material resources. */
91
  readonly device: Device;
92
  /** Factory that defines the material schema. */
93
  readonly factory: MaterialFactory<TModuleProps, TBindings>;
94
  /** Shader inputs for the material-owned modules. */
95
  readonly shaderInputs: ShaderInputs<TModuleProps>;
96
  /** Internal binding store including uniform buffers and resource bindings. */
97
  readonly bindings: Record<string, MaterialBinding> = {};
17✔
98

99
  private _uniformStore: UniformStore;
100
  private _bindGroupCacheToken: object = {};
17✔
101
  private _dynamicResourceGenerations: Record<string, number> = {};
17✔
102

103
  constructor(device: Device, props: MaterialProps<TModuleProps, TBindings> = {}) {
17✔
104
    this.id = props.id || uid('material');
17✔
105
    this.device = device;
17✔
106

107
    this.factory =
17✔
108
      props.factory ||
17!
109
      new MaterialFactory<TModuleProps, TBindings>(device, {
110
        modules: props.modules || props.shaderInputs?.getModules() || []
×
111
      });
112

113
    const moduleMap = Object.fromEntries(
17✔
114
      (props.shaderInputs?.getModules() || this.factory.modules).map(module => [
17✔
115
        module.name,
116
        module
117
      ])
118
    ) as {[P in keyof TModuleProps]?: ShaderModule[] extends never ? never : any};
119
    this.shaderInputs = props.shaderInputs || new ShaderInputs<TModuleProps>(moduleMap);
17✔
120
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
17✔
121

122
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
17✔
123
      if (this.ownsModule(moduleName) && shaderModuleHasUniforms(module)) {
54✔
124
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
14✔
125
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
14✔
126
      }
127
    }
128

129
    this.updateShaderInputs();
17✔
130
    if (props.bindings) {
17✔
131
      this._replaceOwnedBindings(props.bindings);
15✔
132
    }
133
  }
134

135
  /** Destroys managed uniform-buffer resources owned by this material. */
136
  destroy(): void {
137
    this._uniformStore.destroy();
7✔
138
  }
139

140
  /** Creates a new material variant with optional structural and uniform overrides. */
141
  clone(
142
    props: MaterialCloneProps<TModuleProps, TBindings> = {}
×
143
  ): Material<TModuleProps, TBindings> {
144
    const material = this.factory.createMaterial({
×
145
      id: props.id,
146
      shaderInputs: props.shaderInputs,
147
      bindings: {
148
        ...this.getResourceBindings(),
149
        ...props.bindings
150
      }
151
    });
152

153
    if (!props.shaderInputs) {
×
154
      material.setProps(this.shaderInputs.getUniformValues() as MaterialPropsUpdate<TModuleProps>);
×
155
    }
156
    if (props.moduleProps) {
×
157
      material.setProps(props.moduleProps);
×
158
    }
159
    material.updateShaderInputs();
×
160
    return material;
×
161
  }
162

163
  /** Returns `true` if this material owns the supplied binding name. */
164
  ownsBinding(bindingName: string): boolean {
165
    return this.factory.ownsBinding(bindingName);
4✔
166
  }
167

168
  /** Returns `true` if this material owns the supplied shader module. */
169
  ownsModule(moduleName: string): boolean {
170
    return this.factory.ownsModule(moduleName);
150✔
171
  }
172

173
  /** Updates material uniform/module props in place without changing material identity. */
174
  setProps(props: MaterialPropsUpdate<TModuleProps>): void {
175
    this.shaderInputs.setProps(props);
16✔
176
  }
177

178
  /**
179
   * Updates managed uniform buffers and shader-input-owned bindings.
180
   *
181
   * @param commandEncoder - Optional encoder used to order material uniform
182
   * uploads with subsequent draw commands.
183
   */
184
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
185
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
17✔
186
    const didChange = this._setOwnedBindings(this.shaderInputs.getBindingValues());
17✔
187
    if (didChange) {
17!
188
      this._bindGroupCacheToken = {};
×
189
    }
190
  }
191

192
  /** Returns the material-owned resource bindings without internal uniform buffers. */
193
  getResourceBindings(): Partial<TBindings> {
194
    const resourceBindings = {} as Partial<TBindings>;
×
195

196
    for (const [name, binding] of Object.entries(this.bindings)) {
×
197
      if (!getModuleNameFromUniformBinding(name)) {
×
198
        (resourceBindings as Record<string, MaterialBinding>)[name] = binding;
×
199
      }
200
    }
201

202
    return resourceBindings;
×
203
  }
204

205
  /**
206
   * Returns resolved bindings, including internal uniform buffers and ready texture sources.
207
   * @param shaderLayout Reflected bindings used to select copied or external texture resolution.
208
   */
209
  getBindings(
210
    shaderLayout: AnyShaderLayout = {bindings: []}
11✔
211
  ): Partial<{[K in keyof TBindings]: Binding}> & Record<string, Binding> {
212
    this._syncDynamicResourceGenerations();
11✔
213

214
    const validBindings = {} as Partial<{[K in keyof TBindings]: Binding}> &
11✔
215
      Record<string, Binding>;
216
    const validBindingsMap = validBindings as Record<string, Binding>;
11✔
217
    for (const [name, binding] of Object.entries(this.bindings)) {
11✔
218
      if (isTextureBindingSource(binding)) {
12✔
219
        const bindingLayout = getTextureBindingLayout(shaderLayout, name, {
5✔
220
          fallbackGroup: MATERIAL_BIND_GROUP
221
        });
222
        const resolvedBinding = bindingLayout ? binding.resolveTextureBinding(bindingLayout) : null;
5!
223
        if (resolvedBinding) {
5!
224
          validBindingsMap[name] = resolvedBinding;
5✔
225
        }
226
      } else if (binding instanceof DynamicBuffer) {
7✔
227
        validBindingsMap[name] = binding.buffer;
3✔
228
      } else if (isBufferRangeBinding(binding)) {
4!
229
        validBindingsMap[name] = resolveBufferRangeBinding(binding);
×
230
      } else {
231
        validBindingsMap[name] = binding;
4✔
232
      }
233
    }
234

235
    this._syncDynamicResourceGenerations();
11✔
236
    return validBindings;
11✔
237
  }
238

239
  /**
240
   * Packages resolved material bindings into logical bind group `3`.
241
   * @param shaderLayout Reflected bindings used to select copied or external texture resolution.
242
   */
243
  getBindingsByGroup(shaderLayout: AnyShaderLayout = {bindings: []}): BindingsByGroup {
×
UNCOV
244
    return this.factory.getBindingsByGroup(this.getBindings(shaderLayout));
×
245
  }
246

247
  /** Returns the stable bind-group cache token for the requested bind group. */
248
  getBindGroupCacheKey(group: number): object | null {
249
    this._syncDynamicResourceGenerations();
6✔
250
    return group === MATERIAL_BIND_GROUP ? this._bindGroupCacheToken : null;
6!
251
  }
252

253
  /** Returns the latest update timestamp across material-owned resources. */
254
  getBindingsUpdateTimestamp(): number {
255
    let timestamp = 0;
×
256
    for (const binding of Object.values(this.bindings)) {
×
257
      if (binding instanceof TextureView) {
×
258
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
259
      } else if (
260
        binding instanceof Buffer ||
×
261
        binding instanceof Texture ||
262
        binding instanceof ExternalTexture
263
      ) {
264
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
265
      } else if (binding instanceof DynamicBuffer) {
×
266
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
267
      } else if (isTextureBindingSource(binding)) {
×
268
        timestamp = binding.isReady ? Math.max(timestamp, binding.updateTimestamp) : Infinity;
×
269
      } else if (isBufferRangeBinding(binding)) {
×
270
        timestamp = Math.max(
×
271
          timestamp,
272
          binding.buffer instanceof DynamicBuffer
×
273
            ? binding.buffer.updateTimestamp
274
            : binding.buffer.updateTimestamp
275
        );
276
      }
277
    }
278
    return timestamp;
×
279
  }
280

281
  /** Replaces owned resource bindings and invalidates the material cache identity when needed. */
282
  private _replaceOwnedBindings(bindings: Partial<TBindings>): void {
283
    const didChange = this._setOwnedBindings(bindings);
15✔
284
    if (didChange) {
15✔
285
      this._bindGroupCacheToken = {};
4✔
286
    }
287
  }
288

289
  private _setOwnedBindings(
290
    bindings: Partial<TBindings> | Record<string, MaterialBinding>
291
  ): boolean {
292
    let didChange = false;
32✔
293

294
    for (const [name, binding] of Object.entries(bindings)) {
32✔
295
      if (binding === undefined) {
4!
296
        continue;
×
297
      }
298
      if (!this.ownsBinding(name)) {
4!
299
        continue;
×
300
      }
301

302
      if (this.bindings[name] !== binding) {
4!
303
        this.bindings[name] = binding;
4✔
304
        didChange = true;
4✔
305
      }
306
    }
307

308
    return didChange;
32✔
309
  }
310

311
  /** Invalidates the material bind-group key when deferred binding generations change. */
312
  private _syncDynamicResourceGenerations(): void {
313
    const nextGenerations: Record<string, number> = {};
28✔
314
    let didChange = false;
28✔
315

316
    for (const [name, binding] of Object.entries(this.bindings)) {
28✔
317
      const dynamicResourceGeneration = getDynamicResourceGeneration(binding);
30✔
318
      if (dynamicResourceGeneration !== null) {
30✔
319
        nextGenerations[name] = dynamicResourceGeneration;
22✔
320
        if (this._dynamicResourceGenerations[name] !== dynamicResourceGeneration) {
22✔
321
          didChange = true;
7✔
322
        }
323
      }
324
    }
325

326
    if (
28✔
327
      Object.keys(nextGenerations).length !== Object.keys(this._dynamicResourceGenerations).length
328
    ) {
329
      didChange = true;
3✔
330
    }
331

332
    this._dynamicResourceGenerations = nextGenerations;
28✔
333
    if (didChange) {
28✔
334
      this._bindGroupCacheToken = {};
7✔
335
    }
336
  }
337
}
338

339
/**
340
 * Returns the generation tracked for one deferred material binding.
341
 * @param binding Material binding candidate.
342
 * @returns Deferred binding generation, or `null` for stable concrete bindings.
343
 */
344
function getDynamicResourceGeneration(binding: MaterialBinding): number | null {
345
  if (isTextureBindingSource(binding)) {
30✔
346
    return binding.generation;
14✔
347
  }
348

349
  return getDynamicBufferFromBinding(binding)?.generation ?? null;
16✔
350
}
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