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

visgl / luma.gl / 23981506010

04 Apr 2026 03:07PM UTC coverage: 74.219% (+0.03%) from 74.193%
23981506010

push

github

web-flow
fix: predraw() buffer overwrites (#2590)

5169 of 7877 branches covered (65.62%)

Branch coverage included in aggregate %.

45 of 59 new or added lines in 12 files covered. (76.27%)

1 existing line in 1 file now uncovered.

11692 of 14841 relevant lines covered (78.78%)

750.87 hits per line

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

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

5
import type {Binding, BindingsByGroup, CommandEncoder, Device} from '@luma.gl/core';
6
import {Buffer, Sampler, Texture, TextureView, UniformStore} from '@luma.gl/core';
7
import type {ShaderModule} from '@luma.gl/shadertools';
8
import {DynamicTexture} from '../dynamic-texture/dynamic-texture';
9
import {ShaderInputs} from '../shader-inputs';
10
import {shaderModuleHasUniforms} from '../utils/shader-module-utils';
11
import {uid} from '../utils/uid';
12
import {
13
  getModuleNameFromUniformBinding,
14
  MATERIAL_BIND_GROUP,
15
  MaterialFactory
16
} from './material-factory';
17

18
type MaterialModuleProps = Partial<Record<string, Record<string, unknown>>>;
19
type MaterialBindings = Record<string, Binding | DynamicTexture>;
20
type MaterialPropsUpdate<TModuleProps extends MaterialModuleProps> = Partial<{
21
  [P in keyof TModuleProps]?: Partial<TModuleProps[P]>;
22
}>;
23

24
/** Construction props for one typed {@link Material}. */
25
export type MaterialProps<
26
  TModuleProps extends MaterialModuleProps = MaterialModuleProps,
27
  TBindings extends MaterialBindings = MaterialBindings
28
> = {
29
  /** Optional application-provided identifier. */
30
  id?: string;
31
  /** Factory that owns the material schema. */
32
  factory?: MaterialFactory<TModuleProps, TBindings>;
33
  /** Optional pre-created shader inputs for the material modules. */
34
  shaderInputs?: ShaderInputs<TModuleProps>;
35
  /** Shader modules used when a factory is not supplied. */
36
  modules?: ShaderModule[];
37
  /** Initial material-owned resource bindings. */
38
  bindings?: Partial<TBindings>;
39
};
40

41
/** Structural overrides applied when cloning a {@link Material}. */
42
export type MaterialCloneProps<
43
  TModuleProps extends MaterialModuleProps = MaterialModuleProps,
44
  TBindings extends MaterialBindings = MaterialBindings
45
> = {
46
  /** Optional identifier for the cloned material. */
47
  id?: string;
48
  /** Replacement material-owned resource bindings. */
49
  bindings?: Partial<TBindings>;
50
  /** Additional uniform/module props applied to the clone. */
51
  moduleProps?: MaterialPropsUpdate<TModuleProps>;
52
  /** Optional full replacement shader-input store. */
53
  shaderInputs?: ShaderInputs<TModuleProps>;
54
};
55

56
/**
57
 * Material owns bind group `3` resources and uniforms for one material instance.
58
 *
59
 * `setProps()` mutates uniform values in place. Structural resource changes are
60
 * expressed through `clone({...})`, which creates a new material identity.
61
 */
62
export class Material<
63
  TModuleProps extends MaterialModuleProps = MaterialModuleProps,
64
  TBindings extends MaterialBindings = MaterialBindings
65
> {
66
  /** Application-provided identifier. */
67
  readonly id: string;
68
  /** Device that owns the material resources. */
69
  readonly device: Device;
70
  /** Factory that defines the material schema. */
71
  readonly factory: MaterialFactory<TModuleProps, TBindings>;
72
  /** Shader inputs for the material-owned modules. */
73
  readonly shaderInputs: ShaderInputs<TModuleProps>;
74
  /** Internal binding store including uniform buffers and resource bindings. */
75
  readonly bindings: Record<string, Binding | DynamicTexture> = {};
14✔
76

77
  private _uniformStore: UniformStore;
78
  private _bindGroupCacheToken: object = {};
14✔
79

80
  constructor(device: Device, props: MaterialProps<TModuleProps, TBindings> = {}) {
14✔
81
    this.id = props.id || uid('material');
14✔
82
    this.device = device;
14✔
83

84
    this.factory =
14✔
85
      props.factory ||
14!
86
      new MaterialFactory<TModuleProps, TBindings>(device, {
87
        modules: props.modules || props.shaderInputs?.getModules() || []
×
88
      });
89

90
    const moduleMap = Object.fromEntries(
14✔
91
      (props.shaderInputs?.getModules() || this.factory.modules).map(module => [
14✔
92
        module.name,
93
        module
94
      ])
95
    ) as {[P in keyof TModuleProps]?: ShaderModule[] extends never ? never : any};
96
    this.shaderInputs = props.shaderInputs || new ShaderInputs<TModuleProps>(moduleMap);
14✔
97
    this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
14✔
98

99
    for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
14✔
100
      if (this.ownsModule(moduleName) && shaderModuleHasUniforms(module)) {
51✔
101
        const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
14✔
102
        this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
14✔
103
      }
104
    }
105

106
    this.updateShaderInputs();
14✔
107
    if (props.bindings) {
14✔
108
      this._replaceOwnedBindings(props.bindings);
12✔
109
    }
110
  }
111

112
  /** Destroys managed uniform-buffer resources owned by this material. */
113
  destroy(): void {
114
    this._uniformStore.destroy();
4✔
115
  }
116

117
  /** Creates a new material variant with optional structural and uniform overrides. */
118
  clone(
119
    props: MaterialCloneProps<TModuleProps, TBindings> = {}
×
120
  ): Material<TModuleProps, TBindings> {
121
    const material = this.factory.createMaterial({
×
122
      id: props.id,
123
      shaderInputs: props.shaderInputs,
124
      bindings: {
125
        ...this.getResourceBindings(),
126
        ...props.bindings
127
      }
128
    });
129

130
    if (!props.shaderInputs) {
×
131
      material.setProps(this.shaderInputs.getUniformValues() as MaterialPropsUpdate<TModuleProps>);
×
132
    }
133
    if (props.moduleProps) {
×
134
      material.setProps(props.moduleProps);
×
135
    }
NEW
136
    material.updateShaderInputs();
×
UNCOV
137
    return material;
×
138
  }
139

140
  /** Returns `true` if this material owns the supplied binding name. */
141
  ownsBinding(bindingName: string): boolean {
142
    return this.factory.ownsBinding(bindingName);
1✔
143
  }
144

145
  /** Returns `true` if this material owns the supplied shader module. */
146
  ownsModule(moduleName: string): boolean {
147
    return this.factory.ownsModule(moduleName);
147✔
148
  }
149

150
  /** Updates material uniform/module props in place without changing material identity. */
151
  setProps(props: MaterialPropsUpdate<TModuleProps>): void {
152
    this.shaderInputs.setProps(props);
16✔
153
  }
154

155
  /**
156
   * Updates managed uniform buffers and shader-input-owned bindings.
157
   *
158
   * @param commandEncoder - Optional encoder used to order material uniform
159
   * uploads with subsequent draw commands.
160
   */
161
  updateShaderInputs(commandEncoder?: CommandEncoder): void {
162
    this._uniformStore.setUniforms(this.shaderInputs.getUniformValues(), commandEncoder);
14✔
163
    const didChange = this._setOwnedBindings(this.shaderInputs.getBindingValues());
14✔
164
    if (didChange) {
14!
165
      this._bindGroupCacheToken = {};
×
166
    }
167
  }
168

169
  /** Returns the material-owned resource bindings without internal uniform buffers. */
170
  getResourceBindings(): Partial<TBindings> {
171
    const resourceBindings = {} as Partial<TBindings>;
×
172

173
    for (const [name, binding] of Object.entries(this.bindings)) {
×
174
      if (!getModuleNameFromUniformBinding(name)) {
×
175
        (resourceBindings as Record<string, Binding | DynamicTexture>)[name] = binding;
×
176
      }
177
    }
178

179
    return resourceBindings;
×
180
  }
181

182
  /** Returns the resolved bindings, including internal uniform buffers and ready textures. */
183
  getBindings(): Partial<{[K in keyof TBindings]: Binding}> & Record<string, Binding> {
184
    const validBindings = {} as Partial<{[K in keyof TBindings]: Binding}> &
13✔
185
      Record<string, Binding>;
186
    const validBindingsMap = validBindings as Record<string, Binding>;
13✔
187

188
    for (const [name, binding] of Object.entries(this.bindings)) {
13✔
189
      if (binding instanceof DynamicTexture) {
15!
190
        if (binding.isReady) {
×
191
          validBindingsMap[name] = binding.texture;
×
192
        }
193
      } else {
194
        validBindingsMap[name] = binding;
15✔
195
      }
196
    }
197

198
    return validBindings;
13✔
199
  }
200

201
  /** Packages resolved material bindings into logical bind group `3`. */
202
  getBindingsByGroup(): BindingsByGroup {
203
    return this.factory.getBindingsByGroup(this.getBindings());
10✔
204
  }
205

206
  /** Returns the stable bind-group cache token for the requested bind group. */
207
  getBindGroupCacheKey(group: number): object | null {
208
    return group === MATERIAL_BIND_GROUP ? this._bindGroupCacheToken : null;
×
209
  }
210

211
  /** Returns the latest update timestamp across material-owned resources. */
212
  getBindingsUpdateTimestamp(): number {
213
    let timestamp = 0;
×
214
    for (const binding of Object.values(this.bindings)) {
×
215
      if (binding instanceof TextureView) {
×
216
        timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
×
217
      } else if (binding instanceof Buffer || binding instanceof Texture) {
×
218
        timestamp = Math.max(timestamp, binding.updateTimestamp);
×
219
      } else if (binding instanceof DynamicTexture) {
×
220
        timestamp = binding.texture
×
221
          ? Math.max(timestamp, binding.texture.updateTimestamp)
222
          : Infinity;
223
      } else if (!(binding instanceof Sampler)) {
×
224
        timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
×
225
      }
226
    }
227
    return timestamp;
×
228
  }
229

230
  /** Replaces owned resource bindings and invalidates the material cache identity when needed. */
231
  private _replaceOwnedBindings(bindings: Partial<TBindings>): void {
232
    const didChange = this._setOwnedBindings(bindings);
12✔
233
    if (didChange) {
12✔
234
      this._bindGroupCacheToken = {};
1✔
235
    }
236
  }
237

238
  private _setOwnedBindings(
239
    bindings: Partial<TBindings> | Record<string, Binding | DynamicTexture>
240
  ): boolean {
241
    let didChange = false;
26✔
242

243
    for (const [name, binding] of Object.entries(bindings)) {
26✔
244
      if (binding === undefined) {
1!
245
        continue;
×
246
      }
247
      if (!this.ownsBinding(name)) {
1!
248
        continue;
×
249
      }
250

251
      if (this.bindings[name] !== binding) {
1!
252
        this.bindings[name] = binding;
1✔
253
        didChange = true;
1✔
254
      }
255
    }
256

257
    return didChange;
26✔
258
  }
259
}
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