@threenative/core 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  diff --git a/build/three.cjs b/build/three.cjs
2
- index 8939a67dd76325ccd25ee8806fc4d22b1caea88a..b0f917241f1f2cd25baa21dec20edda32176ef6a 100644
2
+ index 8939a67..b0f9172 100644
3
3
  --- a/build/three.cjs
4
4
  +++ b/build/three.cjs
5
5
  @@ -26116,6 +26116,8 @@ function copyArrayContents( src, target ) {
@@ -12,7 +12,7 @@ index 8939a67dd76325ccd25ee8806fc4d22b1caea88a..b0f917241f1f2cd25baa21dec20edda3
12
12
  * Constructs a new batched mesh.
13
13
  *
14
14
  diff --git a/build/three.core.js b/build/three.core.js
15
- index 0bb9262cd029f411933d077fd44197a51ec5e8e9..868a634b4294be5f32664912a50813d55d6dd6e7 100644
15
+ index 0bb9262..868a634 100644
16
16
  --- a/build/three.core.js
17
17
  +++ b/build/three.core.js
18
18
  @@ -26136,6 +26136,8 @@ function copyArrayContents( src, target ) {
@@ -25,10 +25,277 @@ index 0bb9262cd029f411933d077fd44197a51ec5e8e9..868a634b4294be5f32664912a50813d5
25
25
  * Constructs a new batched mesh.
26
26
  *
27
27
  diff --git a/build/three.webgpu.js b/build/three.webgpu.js
28
- index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a18392c88f865 100644
28
+ index 2e49079..7aa62ad 100644
29
29
  --- a/build/three.webgpu.js
30
30
  +++ b/build/three.webgpu.js
31
- @@ -18500,7 +18500,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
31
+ @@ -141,6 +141,15 @@ class NodeMaterialObserver {
32
+ */
33
+ this.renderId = 0;
34
+
35
+ + /**
36
+ + * Whether the current render produces motion vectors. Objects keep refreshing
37
+ + * fully while this is the case.
38
+ + *
39
+ + * @type {boolean}
40
+ + * @default false
41
+ + */
42
+ + this.renderingVelocity = false;
43
+ +
44
+ }
45
+
46
+ /**
47
+ @@ -240,6 +249,88 @@ class NodeMaterialObserver {
48
+
49
+ }
50
+
51
+ + /**
52
+ + * Returns `true` if the given render object is a static object whose per-object state - world
53
+ + * matrix, material, geometry and the scene environment - did not change since the last
54
+ + * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still
55
+ + * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node
56
+ + * updates, its object uniform group upload and its per-draw attribute scan.
57
+ + *
58
+ + * @param {RenderObject} renderObject - The render object.
59
+ + * @return {boolean} Whether the given render object is settled or not.
60
+ + */
61
+ + isSettled( renderObject ) {
62
+ +
63
+ + const data = this.renderObjects.get( renderObject );
64
+ +
65
+ + return data !== undefined && data.settledState === true;
66
+ +
67
+ + }
68
+ +
69
+ + /**
70
+ + * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw,
71
+ + * so every step of that draw reads the same answer.
72
+ + *
73
+ + * @param {RenderObject} renderObject - The render object.
74
+ + * @return {boolean} Whether the given render object is settled or not.
75
+ + */
76
+ + updateSettled( renderObject ) {
77
+ +
78
+ + const { object, material, geometry, scene } = renderObject;
79
+ + const data = this.getRenderObjectData( renderObject );
80
+ +
81
+ + if ( object.static !== true || this.renderingVelocity === true ) {
82
+ +
83
+ + data.settledState = false;
84
+ +
85
+ + return false;
86
+ +
87
+ + }
88
+ +
89
+ + let settled = data.settled;
90
+ +
91
+ + if ( settled === undefined ) {
92
+ +
93
+ + data.settled = settled = {
94
+ + matrix: object.matrixWorld.clone(),
95
+ + environmentRotation: scene.environmentRotation.clone(),
96
+ + environmentIntensity: scene.environmentIntensity,
97
+ + materialVersion: material.version,
98
+ + geometryVersion: geometry.version
99
+ + };
100
+ +
101
+ + data.settledState = false;
102
+ +
103
+ + return false;
104
+ +
105
+ + }
106
+ +
107
+ + if ( settled.matrix.equals( object.matrixWorld ) !== true ||
108
+ + settled.environmentRotation.equals( scene.environmentRotation ) !== true ||
109
+ + settled.environmentIntensity !== scene.environmentIntensity ||
110
+ + settled.materialVersion !== material.version ||
111
+ + settled.geometryVersion !== geometry.version ) {
112
+ +
113
+ + // re-arm with the current state, so the object settles again once it stops changing
114
+ +
115
+ + settled.matrix.copy( object.matrixWorld );
116
+ + settled.environmentRotation.copy( scene.environmentRotation );
117
+ + settled.environmentIntensity = scene.environmentIntensity;
118
+ + settled.materialVersion = material.version;
119
+ + settled.geometryVersion = geometry.version;
120
+ +
121
+ + data.settledState = false;
122
+ +
123
+ + return false;
124
+ +
125
+ + }
126
+ +
127
+ + data.settledState = true;
128
+ +
129
+ + return true;
130
+ +
131
+ + }
132
+ +
133
+ /**
134
+ * Returns an attribute data structure holding the attributes versions for
135
+ * monitoring.
136
+ @@ -306,7 +397,6 @@ class NodeMaterialObserver {
137
+
138
+ data = {
139
+ _renderId: -1,
140
+ - _equal: false,
141
+
142
+ attributes: this.getAttributesData( geometry.attributes ),
143
+ indexId: geometry.index ? geometry.index.id : null,
144
+ @@ -335,7 +425,7 @@ class NodeMaterialObserver {
145
+
146
+ if ( data === undefined ) {
147
+
148
+ - data = { _renderId: -1, _equal: false };
149
+ + data = { _renderId: -1 };
150
+
151
+ for ( const property of this.refreshUniforms ) {
152
+
153
+ @@ -419,7 +509,6 @@ class NodeMaterialObserver {
154
+
155
+ value.copy( mtlValue );
156
+
157
+ - materialData._equal = false;
158
+ return false;
159
+
160
+ }
161
+ @@ -431,7 +520,6 @@ class NodeMaterialObserver {
162
+ value.id = mtlValue.id;
163
+ value.version = mtlValue.version;
164
+
165
+ - materialData._equal = false;
166
+ return false;
167
+
168
+ }
169
+ @@ -440,7 +528,6 @@ class NodeMaterialObserver {
170
+
171
+ materialData[ property ] = mtlValue;
172
+
173
+ - materialData._equal = false;
174
+ return false;
175
+
176
+ }
177
+ @@ -456,18 +543,12 @@ class NodeMaterialObserver {
178
+ renderObjectData.bufferWidth = width;
179
+ renderObjectData.bufferHeight = height;
180
+
181
+ - materialData._equal = false;
182
+ return false;
183
+
184
+ }
185
+
186
+ }
187
+
188
+ - materialData._equal = true;
189
+ -
190
+ - } else {
191
+ -
192
+ - if ( materialData._equal === false ) return false;
193
+
194
+ }
195
+
196
+ @@ -510,7 +591,6 @@ class NodeMaterialObserver {
197
+ // attribute was removed
198
+ delete storedAttributes[ name ];
199
+
200
+ - geometryData._equal = false;
201
+ return false;
202
+
203
+ }
204
+ @@ -523,7 +603,6 @@ class NodeMaterialObserver {
205
+ storedAttributeData.id = id;
206
+ storedAttributeData.version = version;
207
+
208
+ - geometryData._equal = false;
209
+ return false;
210
+
211
+ }
212
+ @@ -534,7 +613,6 @@ class NodeMaterialObserver {
213
+
214
+ geometryData.attributes = this.getAttributesData( attributes );
215
+
216
+ - geometryData._equal = false;
217
+ return false;
218
+
219
+ }
220
+ @@ -552,7 +630,6 @@ class NodeMaterialObserver {
221
+ geometryData.indexId = currentIndexId;
222
+ geometryData.indexVersion = currentIndexVersion;
223
+
224
+ - geometryData._equal = false;
225
+ return false;
226
+
227
+ }
228
+ @@ -564,16 +641,10 @@ class NodeMaterialObserver {
229
+ geometryData.drawRange.start = geometry.drawRange.start;
230
+ geometryData.drawRange.count = geometry.drawRange.count;
231
+
232
+ - geometryData._equal = false;
233
+ return false;
234
+
235
+ }
236
+
237
+ - geometryData._equal = true;
238
+ -
239
+ - } else {
240
+ -
241
+ - if ( geometryData._equal === false ) return false;
242
+
243
+ }
244
+
245
+ @@ -724,7 +795,14 @@ class NodeMaterialObserver {
246
+ */
247
+ needsRefresh( renderObject, nodeFrame ) {
248
+
249
+ - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
250
+ + this.renderingVelocity = this.needsVelocity( nodeFrame.renderer );
251
+ +
252
+ + // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and
253
+ + // the object uniform upload of this object all skip or all refresh.
254
+ +
255
+ + this.updateSettled( renderObject );
256
+ +
257
+ + if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity )
258
+ return true;
259
+
260
+ const { renderId } = nodeFrame;
261
+ @@ -737,10 +815,14 @@ class NodeMaterialObserver {
262
+
263
+ }
264
+
265
+ - const isStatic = renderObject.object.static === true;
266
+ const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
267
+
268
+ - if ( isStatic || isBundle )
269
+ + // `object.static` is not the question - it is a switch, and a brand new object has it set
270
+ + // before it has been drawn once, so answering from it skips the draw that creates this
271
+ + // object's bind groups and leaves the pipeline reading a layout that was never built. A
272
+ + // static object that moved, or whose material or geometry was edited, has to refresh like
273
+ + // any other; only a settled one may skip, and nothing is settled before its first refresh.
274
+ + if ( this.isSettled( renderObject ) === true || isBundle )
275
+ return false;
276
+
277
+ const lightsData = this.getLights( renderObject.lightsNode, renderId );
278
+ @@ -18320,7 +18402,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) {
279
+
280
+ const uniformBufferSize = matrixCount * 16 * 4;
281
+
282
+ - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
283
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
284
+ + if ( false ) {
285
+
286
+ instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
287
+
288
+ @@ -18414,7 +18497,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
289
+
290
+ const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4;
291
+
292
+ - if ( uniformBufferSize > builder.getUniformBufferLimit() ) {
293
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
294
+ + if ( true ) {
295
+
296
+ interleavedMatrix = _matrixBuffers.get( matrices );
297
+
298
+ @@ -18500,7 +18584,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
32
299
 
33
300
  const previousInstanceData = _previousInstanceMatrices.get( object );
34
301
 
@@ -38,7 +305,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
38
305
 
39
306
  } );
40
307
 
41
- @@ -18599,6 +18600,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
308
+ @@ -18599,6 +18684,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
42
309
  const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) );
43
310
 
44
311
  const matricesTexture = batchMesh._matricesTexture;
@@ -46,7 +313,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
46
313
 
47
314
  const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst();
48
315
  const j = float( indirectId ).mul( 4 ).toInt().toConst();
49
- @@ -18626,6 +18628,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
316
+ @@ -18626,6 +18712,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
50
317
 
51
318
  positionLocal.assign( batchingMatrix.mul( positionLocal ) );
52
319
 
@@ -66,7 +333,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
66
333
  const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) );
67
334
 
68
335
  const batchingNormal = bm.mul( transformedNormal ).xyz;
69
- @@ -18777,7 +18792,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
336
+ @@ -18777,7 +18876,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
70
337
 
71
338
  if ( skeletonData !== undefined ) {
72
339
 
@@ -76,7 +343,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
76
343
 
77
344
  }
78
345
 
79
- @@ -18844,7 +18860,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ],
346
+ @@ -18844,7 +18944,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ],
80
347
 
81
348
  if ( state !== undefined ) {
82
349
 
@@ -86,7 +353,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
86
353
 
87
354
  }
88
355
 
89
- @@ -29911,6 +29928,18 @@ class RenderObject {
356
+ @@ -29911,6 +30012,18 @@ class RenderObject {
90
357
  */
91
358
  this._sourceMaterial = renderer._currentSourceMaterial;
92
359
 
@@ -105,7 +372,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
105
372
  /**
106
373
  * An event listener which is defined by `RenderObjects`. It performs
107
374
  * clean up tasks when `dispose()` on this render object.
108
- @@ -30098,7 +30127,7 @@ class RenderObject {
375
+ @@ -30098,7 +30211,7 @@ class RenderObject {
109
376
  */
110
377
  getChainArray() {
111
378
 
@@ -114,7 +381,15 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
114
381
 
115
382
  }
116
383
 
117
- @@ -30680,6 +30709,8 @@ class RenderObjects {
384
+ @@ -30548,6 +30661,7 @@ class RenderObject {
385
+ }
386
+
387
+ cacheKey = hash$1( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version );
388
+ + cacheKey = hash$1( cacheKey, this.context.sampleCount );
389
+
390
+ return cacheKey;
391
+
392
+ @@ -30680,6 +30794,8 @@ class RenderObjects {
118
393
  _chainKeys$3[ 1 ] = material;
119
394
  _chainKeys$3[ 2 ] = renderContext;
120
395
  _chainKeys$3[ 3 ] = lightsNode;
@@ -123,7 +398,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
123
398
 
124
399
  //
125
400
 
126
- @@ -30707,9 +30738,18 @@ class RenderObjects {
401
+ @@ -30707,9 +30823,18 @@ class RenderObjects {
127
402
 
128
403
  }
129
404
 
@@ -144,7 +419,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
144
419
 
145
420
  renderObject.dispose();
146
421
 
147
- @@ -30719,6 +30759,8 @@ class RenderObjects {
422
+ @@ -30719,6 +30844,8 @@ class RenderObjects {
148
423
 
149
424
  renderObject.version = material.version;
150
425
 
@@ -153,7 +428,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
153
428
  }
154
429
 
155
430
  }
156
- @@ -30731,6 +30773,7 @@ class RenderObjects {
431
+ @@ -30731,6 +30858,7 @@ class RenderObjects {
157
432
  _chainKeys$3[ 1 ] = null;
158
433
  _chainKeys$3[ 2 ] = null;
159
434
  _chainKeys$3[ 3 ] = null;
@@ -161,7 +436,187 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
161
436
 
162
437
  //
163
438
 
164
- @@ -39725,7 +39768,8 @@ class VelocityNode extends TempNode {
439
+ @@ -31165,6 +31293,14 @@ class Geometries extends DataMap {
440
+ */
441
+ this._geometryDisposeListeners = new Map();
442
+
443
+ + /**
444
+ + * This Weak Map holds the attribute versions a settled render object was last drawn with,
445
+ + * so a write into one of them is what brings the object back out of the settled path.
446
+ + *
447
+ + * @type {WeakMap<RenderObject,Map<BufferAttribute,number>>}
448
+ + */
449
+ + this._settledAttributes = new WeakMap();
450
+ +
451
+ }
452
+
453
+ /**
454
+ @@ -31190,10 +31326,69 @@ class Geometries extends DataMap {
455
+
456
+ if ( this.has( renderObject ) === false ) this.initGeometry( renderObject );
457
+
458
+ + // A settled static object has no attribute that could have changed since the last refresh,
459
+ + // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of
460
+ + // this draw, and a BufferGeometry carries no version of its own to announce the write, so
461
+ + // the attributes' own versions are the signal - and the only one there is.
462
+ +
463
+ + const monitor = renderObject.getMonitor();
464
+ +
465
+ + if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return;
466
+ +
467
+ this.updateAttributes( renderObject );
468
+
469
+ }
470
+
471
+ + /**
472
+ + * Returns `true` if an attribute of the given render object - or its index - was written to
473
+ + * since the last time its versions were read, and records the versions it holds now.
474
+ + *
475
+ + * @param {RenderObject} renderObject - The render object.
476
+ + * @return {boolean} Whether any of the render object's versions moved.
477
+ + */
478
+ + attributesChanged( renderObject ) {
479
+ +
480
+ + const attributes = renderObject.getAttributes();
481
+ + const index = this.getIndex( renderObject );
482
+ + const settled = this._settledAttributes.get( renderObject );
483
+ +
484
+ + if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) {
485
+ +
486
+ + let changed = false;
487
+ +
488
+ + for ( const attribute of attributes ) {
489
+ +
490
+ + if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) {
491
+ +
492
+ + changed = true;
493
+ + break;
494
+ +
495
+ + }
496
+ +
497
+ + }
498
+ +
499
+ + if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false;
500
+ +
501
+ + }
502
+ +
503
+ + const versions = new Map();
504
+ +
505
+ + for ( const attribute of attributes ) {
506
+ +
507
+ + const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
508
+ +
509
+ + versions.set( data, data.version );
510
+ +
511
+ + }
512
+ +
513
+ + if ( index !== null ) versions.set( index, index.version );
514
+ +
515
+ + this._settledAttributes.set( renderObject, versions );
516
+ +
517
+ + return true;
518
+ +
519
+ + }
520
+ +
521
+ /**
522
+ * Initializes the geometry of the given render object.
523
+ *
524
+ @@ -32771,7 +32966,7 @@ class Bindings extends DataMap {
525
+ */
526
+ updateForRender( renderObject ) {
527
+
528
+ - this._updateBindings( this.getForRender( renderObject ) );
529
+ + this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) );
530
+
531
+ }
532
+
533
+ @@ -32918,12 +33113,13 @@ class Bindings extends DataMap {
534
+ * Updates the given array of bindings.
535
+ *
536
+ * @param {Array<BindGroup>} bindings - The bind groups.
537
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
538
+ */
539
+ - _updateBindings( bindings ) {
540
+ + _updateBindings( bindings, settled ) {
541
+
542
+ for ( const bindGroup of bindings ) {
543
+
544
+ - this._update( bindGroup, bindings );
545
+ + this._update( bindGroup, bindings, settled );
546
+
547
+ }
548
+
549
+ @@ -32934,11 +33130,27 @@ class Bindings extends DataMap {
550
+ *
551
+ * @param {BindGroup} bindGroup - The bind group to update.
552
+ * @param {Array<BindGroup>} bindings - The bind groups.
553
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
554
+ */
555
+ - _update( bindGroup, bindings ) {
556
+ + _update( bindGroup, bindings, settled ) {
557
+
558
+ const { backend } = this;
559
+
560
+ + // A group a node added after this object's first draw has no buffers and no layout yet, and
561
+ + // the pipeline reads that layout whatever this draw decides to do with it - so it is built
562
+ + // here, on every draw. A group that did not exist cannot have been written either, so it is
563
+ + // written now: `settled` is what tells the loop below to write it, not to skip it.
564
+ + if ( this.get( bindGroup ).bindGroup === undefined ) {
565
+ +
566
+ + this._createBindings( [ bindGroup ] );
567
+ +
568
+ + settled = false;
569
+ +
570
+ + }
571
+ +
572
+ + // a settled object keeps the values it uploaded the last time it refreshed, so an object
573
+ + // group it has already written is the one thing in here it may skip
574
+ +
575
+ let needsBindingsUpdate = false;
576
+ let cacheBindings = true;
577
+ let cacheIndex = 0;
578
+ @@ -32948,7 +33160,7 @@ class Bindings extends DataMap {
579
+
580
+ for ( const binding of bindGroup.bindings ) {
581
+
582
+ - const updatedGroup = this.nodes.updateGroup( binding );
583
+ + const updatedGroup = this.nodes.updateGroup( binding, settled );
584
+
585
+ // every uniforms group is a uniform buffer. So if no update is required,
586
+ // we move one with the next binding. Otherwise the next if block will update the group.
587
+ @@ -33952,7 +34164,8 @@ class RenderContexts {
588
+
589
+ }
590
+
591
+ - if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
592
+ + const sampleCount = renderTarget !== null ? renderTarget.samples : this.renderer.currentSamples;
593
+ + renderState.sampleCount = sampleCount === 0 ? 1 : sampleCount;
594
+
595
+ renderState.clearDepthValue = this.renderer.getClearDepth();
596
+ renderState.clearStencilValue = this.renderer.getClearStencil();
597
+ @@ -34181,6 +34394,21 @@ class Textures extends DataMap {
598
+
599
+ backend.destroyTexture( texture );
600
+
601
+ + // delete cached bind groups so they don't point to the destroyed texture
602
+ +
603
+ + if ( textureData.bindGroups ) {
604
+ +
605
+ + for ( const bindGroup of textureData.bindGroups ) {
606
+ +
607
+ + const bindingsData = this.backend.get( bindGroup );
608
+ +
609
+ + bindingsData.groups = undefined;
610
+ + bindingsData.versions = undefined;
611
+ +
612
+ + }
613
+ +
614
+ + }
615
+ +
616
+ }
617
+
618
+ //
619
+ @@ -39725,7 +39953,8 @@ class VelocityNode extends TempNode {
165
620
 
166
621
  const previousModelMatrix = getPreviousMatrix( object );
167
622
 
@@ -171,7 +626,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
171
626
 
172
627
  //
173
628
 
174
- @@ -53841,7 +53885,7 @@ class NodeBuilder {
629
+ @@ -53841,7 +54070,7 @@ class NodeBuilder {
175
630
 
176
631
  const mrt = this.renderer.getMRT();
177
632
 
@@ -180,7 +635,93 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
180
635
 
181
636
  }
182
637
 
183
- @@ -62731,6 +62775,7 @@ class Renderer {
638
+ @@ -55244,15 +55473,17 @@ class NodeManager extends DataMap {
639
+ * Returns `true` if the given node uniforms group must be updated or not.
640
+ *
641
+ * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
642
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
643
+ * @return {boolean} Whether the node uniforms group requires an update or not.
644
+ */
645
+ - updateGroup( nodeUniformsGroup ) {
646
+ + updateGroup( nodeUniformsGroup, settled ) {
647
+
648
+ const groupNode = nodeUniformsGroup.groupNode;
649
+
650
+ - // groups that are updated per object always require an update so no further checks are needed
651
+ + // groups that are updated per object always require an update so no further checks are needed.
652
+ + // A settled static object keeps the values it uploaded the last time it was refreshed.
653
+
654
+ - if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
655
+ + if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true;
656
+
657
+ // check for update
658
+
659
+ @@ -56056,6 +56287,21 @@ class NodeManager extends DataMap {
660
+
661
+ }
662
+
663
+ + /**
664
+ + * Returns `true` if the per-object updates of the given render object can be skipped
665
+ + * because the object is static and nothing it depends on changed since its last refresh.
666
+ + *
667
+ + * @param {RenderObject} renderObject - The render object.
668
+ + * @return {boolean} Whether the render object is settled or not.
669
+ + */
670
+ + isSettled( renderObject ) {
671
+ +
672
+ + const monitor = renderObject.getMonitor();
673
+ +
674
+ + return monitor !== undefined ? monitor.isSettled( renderObject ) : false;
675
+ +
676
+ + }
677
+ +
678
+ /**
679
+ * Triggers the call of `updateBefore()` methods
680
+ * for all nodes of the given render object.
681
+ @@ -56065,9 +56311,14 @@ class NodeManager extends DataMap {
682
+ updateBefore( renderObject ) {
683
+
684
+ const nodeBuilder = renderObject.getNodeBuilderState();
685
+ + const settled = this.isSettled( renderObject );
686
+
687
+ for ( const node of nodeBuilder.updateBeforeNodes ) {
688
+
689
+ + // a settled static object has nothing to update per object
690
+ +
691
+ + if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue;
692
+ +
693
+ // update frame state for each node
694
+
695
+ this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
696
+ @@ -56085,10 +56336,11 @@ class NodeManager extends DataMap {
697
+ updateAfter( renderObject ) {
698
+
699
+ const nodeBuilder = renderObject.getNodeBuilderState();
700
+ + const settled = this.isSettled( renderObject );
701
+
702
+ for ( const node of nodeBuilder.updateAfterNodes ) {
703
+
704
+ - // update frame state for each node
705
+ + if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue;
706
+
707
+ this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
708
+
709
+ @@ -56125,9 +56377,14 @@ class NodeManager extends DataMap {
710
+
711
+ const nodeFrame = this.getNodeFrameForRender( renderObject );
712
+ const nodeBuilder = renderObject.getNodeBuilderState();
713
+ + const settled = this.isSettled( renderObject );
714
+
715
+ for ( const node of nodeBuilder.updateNodes ) {
716
+
717
+ + // a settled static object keeps the per-object values it uploaded last time
718
+ +
719
+ + if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue;
720
+ +
721
+ nodeFrame.updateNode( node );
722
+
723
+ }
724
+ @@ -62731,6 +62988,7 @@ class Renderer {
184
725
  let materialDisplacementMap;
185
726
  let materialDisplacementScale;
186
727
  let materialDisplacementBias;
@@ -188,7 +729,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
188
729
 
189
730
  const previousSourceMaterial = this._currentSourceMaterial;
190
731
 
191
- @@ -62756,6 +62801,7 @@ class Renderer {
732
+ @@ -62756,6 +63014,7 @@ class Renderer {
192
733
  materialDisplacementMap = overrideMaterial.displacementMap;
193
734
  materialDisplacementScale = overrideMaterial.displacementScale;
194
735
  materialDisplacementBias = overrideMaterial.displacementBias;
@@ -196,109 +737,1100 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a1839
196
737
 
197
738
  if ( material.positionNode && material.positionNode.isNode ) {
198
739
 
199
- @@ -62763,7 +62809,15 @@ class Renderer {
740
+ @@ -62763,7 +63022,15 @@ class Renderer {
741
+
742
+ }
743
+
744
+ - overrideMaterial.alphaTest = material.alphaTest;
745
+ + // `Material.alphaTest`'s setter bumps `version` whenever alpha-test-ness flips, and this
746
+ + // override material is shared by every caster in the pass. A scene that mixes alpha-tested
747
+ + // casters with opaque ones therefore moves that shared version several times per frame,
748
+ + // and every OTHER render object drawn with it then fails the version gate in
749
+ + // `RenderObjects.get` and recomputes a full material cache key that always matches.
750
+ + // Writing the backing field leaves the `alphaTest` getter — and so the cache key — still
751
+ + // reporting this caster's value, while each render object tracks the staleness signal
752
+ + // that belongs to it: its own source material's version.
753
+ + overrideMaterial._alphaTest = material.alphaTest;
754
+ overrideMaterial.alphaMap = material.alphaMap;
755
+ overrideMaterial.displacementMap = material.displacementMap;
756
+ overrideMaterial.displacementScale = material.displacementScale;
757
+ @@ -62825,6 +63092,7 @@ class Renderer {
758
+ scene.overrideMaterial.displacementMap = materialDisplacementMap;
759
+ scene.overrideMaterial.displacementScale = materialDisplacementScale;
760
+ scene.overrideMaterial.displacementBias = materialDisplacementBias;
761
+ + scene.overrideMaterial._alphaTest = materialAlphaTest;
762
+
763
+ }
764
+
765
+ @@ -62886,18 +63154,17 @@ class Renderer {
766
+
767
+ //
768
+
769
+ - const needsRefresh = this._nodes.needsRefresh( renderObject );
770
+ + this._nodes.needsRefresh( renderObject );
771
+
772
+ - if ( needsRefresh ) {
773
+ -
774
+ - this._nodes.updateBefore( renderObject );
775
+ -
776
+ - this._geometries.updateForRender( renderObject );
777
+ -
778
+ - this._nodes.updateForRender( renderObject );
779
+ - this._bindings.updateForRender( renderObject );
780
+ -
781
+ - }
782
+ + // Every step of a draw runs, settled or not. What a settled static object skips is decided
783
+ + // inside each of them - its OBJECT node updates, its object uniform group write and its
784
+ + // attribute scan - because each of those answers for itself, and a step left out here
785
+ + // cannot: a group a node added after this object's first draw still has to be created, and
786
+ + // the pipeline reads that group's layout whatever this draw decided to do with it.
787
+ + this._geometries.updateForRender( renderObject );
788
+ + this._nodes.updateBefore( renderObject );
789
+ + this._nodes.updateForRender( renderObject );
790
+ + this._bindings.updateForRender( renderObject );
791
+
792
+ this._pipelines.updateForRender( renderObject );
793
+
794
+ @@ -62907,7 +63174,7 @@ class Renderer {
795
+
796
+ this.backend.draw( renderObject, this.info );
797
+
798
+ - if ( needsRefresh ) this._nodes.updateAfter( renderObject );
799
+ + this._nodes.updateAfter( renderObject );
800
+
801
+ }
802
+
803
+ @@ -79004,7 +79271,14 @@ class WGSLNodeBuilder extends NodeBuilder {
804
+
805
+ } else {
806
+
807
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
808
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
809
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
810
+ + // operands (including the array form above) stay u32.
811
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
812
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
813
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
814
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
815
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
816
+
817
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
818
+
819
+ @@ -79586,7 +79860,8 @@ class WGSLNodeBuilder extends NodeBuilder {
820
+
821
+ uniformGPU = buffer;
822
+
823
+ - uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id;
824
+ + // The base builder supplies a unique name within this program. A process-wide
825
+ + // allocation id changes otherwise identical shader source for every buffer.
826
+
827
+ } else {
828
+
829
+ @@ -80329,9 +80604,12 @@ ${ flowData.code }
830
+ const textureNode = uniform.node;
831
+ const texture = textureNode.value;
832
+
833
+ - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
834
+ - // Also textureGather always need sampler.
835
+ - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
836
+ + // The bind group layout freezes sampler inclusion once, in getUniformFromNode
837
+ + // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can
838
+ + // disagree with that layout and shift every later binding index.
839
+ + const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache );
840
+ + const uniformGPU = nodeData.uniformGPU;
841
+ + const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true;
842
+
843
+ if ( needsSampler ) {
844
+
845
+ @@ -81295,9 +81573,15 @@ class WebGPUAttributeUtils {
846
+ destroyAttribute( attribute ) {
847
+
848
+ const backend = this.backend;
849
+ - const data = backend.get( this._getBufferAttribute( attribute ) );
850
+ + const bufferAttribute = this._getBufferAttribute( attribute );
851
+ + const data = backend.get( bufferAttribute );
852
+
853
+ - data.buffer.destroy();
854
+ + // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes
855
+ + // share it: destroy it once and forget it there too, as the WebGL backend does, or the next
856
+ + // createAttribute() finds the destroyed buffer and submits it (threenative patch).
857
+ + if ( data.buffer !== undefined ) data.buffer.destroy();
858
+ +
859
+ + if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute );
860
+
861
+ backend.delete( attribute );
862
+
863
+ @@ -81598,18 +81882,16 @@ class WebGPUBindingUtils {
864
+
865
+ const bindingsData = backend.get( bindGroup );
866
+
867
+ - // check if the the bind group already has a layout
868
+ + const entries = this._createLayoutEntries( bindGroup );
869
+ + const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
870
+
871
+ if ( bindingsData.layout ) {
872
+
873
+ - return bindingsData.layout.layoutGPU;
874
+ + if ( bindingsData.layoutKey === bindGroupLayoutKey ) return bindingsData.layout.layoutGPU;
875
+
876
+ - }
877
+ -
878
+ - // if not, assing one
879
+ + this.deleteBindGroupData( bindGroup );
880
+
881
+ - const entries = this._createLayoutEntries( bindGroup );
882
+ - const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
883
+ + }
884
+
885
+ // try to find an existing layout in the cache
886
+
887
+ @@ -82131,6 +82413,9 @@ class WebGPUBindingUtils {
888
+
889
+ }
890
+
891
+ + bindingsData.groups = [];
892
+ + bindingsData.versions = [];
893
+ +
894
+ }
895
+
896
+ /**
897
+ @@ -83814,6 +84099,14 @@ class WebGPUBackend extends Backend {
898
+ */
899
+ this.compatibilityMode = null;
900
+
901
+ + /**
902
+ + * A reference to the WebGPU instance that creates adapters.
903
+ + *
904
+ + * @type {?GPU}
905
+ + * @default null
906
+ + */
907
+ + this.gpu = null;
908
+ +
909
+ /**
910
+ * A reference to the device.
911
+ *
912
+ @@ -83926,7 +84219,9 @@ class WebGPUBackend extends Backend {
913
+ xrCompatible: renderer.xr.enabled
914
+ };
915
+
916
+ - const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
917
+ + this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null;
918
+ +
919
+ + const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null;
920
+
921
+ if ( adapter === null ) {
922
+
923
+ @@ -85461,13 +85756,12 @@ class WebGPUBackend extends Backend {
924
+ for ( let i = 0, l = bindings.length; i < l; i ++ ) {
925
+
926
+ const bindGroup = bindings[ i ];
927
+ + const bindingsData = this.get( bindGroup );
928
+
929
+ - if ( currentBindingGroups[ i ] !== bindGroup.id ) {
930
+ -
931
+ - const bindingsData = this.get( bindGroup );
932
+ + if ( currentBindingGroups[ i ] !== bindingsData.group ) {
933
+
934
+ passEncoderGPU.setBindGroup( i, bindingsData.group );
935
+ - currentBindingGroups[ i ] = bindGroup.id;
936
+ + currentBindingGroups[ i ] = bindingsData.group;
937
+
938
+ }
939
+
940
+ @@ -85766,6 +86060,7 @@ class WebGPUBackend extends Backend {
941
+ const utils = this.utils;
942
+
943
+ const sampleCount = utils.getSampleCountRenderContext( renderObject.context );
944
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
945
+ const colorSpace = utils.getCurrentColorSpace( renderObject.context );
946
+ const colorFormat = utils.getCurrentColorFormat( renderObject.context );
947
+ const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
948
+ @@ -85787,7 +86082,8 @@ class WebGPUBackend extends Backend {
949
+ data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat ||
950
+ data.primitiveTopology !== primitiveTopology ||
951
+ data.frontFaceCW !== frontFaceCW ||
952
+ - data.clippingContextCacheKey !== renderObject.clippingContextCacheKey
953
+ + data.clippingContextCacheKey !== renderObject.clippingContextCacheKey ||
954
+ + data.bindGroupLayoutKeys !== bindGroupLayoutKeys
955
+ ) {
956
+
957
+ data.material = material; data.materialVersion = material.version;
958
+ @@ -85807,6 +86103,7 @@ class WebGPUBackend extends Backend {
959
+ data.primitiveTopology = primitiveTopology;
960
+ data.frontFaceCW = frontFaceCW;
961
+ data.clippingContextCacheKey = renderObject.clippingContextCacheKey;
962
+ + data.bindGroupLayoutKeys = bindGroupLayoutKeys;
963
+
964
+ needsUpdate = true;
965
+
966
+ @@ -85828,6 +86125,7 @@ class WebGPUBackend extends Backend {
967
+
968
+ const utils = this.utils;
969
+ const renderContext = renderObject.context;
970
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
971
+
972
+ // meshes with negative scale have a different frontFace render pipeline
973
+ // descriptor value so the following must be honored in the cache key
974
+ @@ -85846,6 +86144,7 @@ class WebGPUBackend extends Backend {
975
+ material.side,
976
+ frontFaceCW,
977
+ utils.getSampleCountRenderContext( renderContext ),
978
+ + bindGroupLayoutKeys,
979
+ utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ),
980
+ utils.getPrimitiveTopology( object, material ),
981
+ renderObject.getGeometryCacheKey(),
982
+ diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js
983
+ index f707acc..b8a7b2f 100644
984
+ --- a/build/three.webgpu.nodes.js
985
+ +++ b/build/three.webgpu.nodes.js
986
+ @@ -141,6 +141,15 @@ class NodeMaterialObserver {
987
+ */
988
+ this.renderId = 0;
989
+
990
+ + /**
991
+ + * Whether the current render produces motion vectors. Objects keep refreshing
992
+ + * fully while this is the case.
993
+ + *
994
+ + * @type {boolean}
995
+ + * @default false
996
+ + */
997
+ + this.renderingVelocity = false;
998
+ +
999
+ }
1000
+
1001
+ /**
1002
+ @@ -240,6 +249,88 @@ class NodeMaterialObserver {
1003
+
1004
+ }
1005
+
1006
+ + /**
1007
+ + * Returns `true` if the given render object is a static object whose per-object state - world
1008
+ + * matrix, material, geometry and the scene environment - did not change since the last
1009
+ + * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still
1010
+ + * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node
1011
+ + * updates, its object uniform group upload and its per-draw attribute scan.
1012
+ + *
1013
+ + * @param {RenderObject} renderObject - The render object.
1014
+ + * @return {boolean} Whether the given render object is settled or not.
1015
+ + */
1016
+ + isSettled( renderObject ) {
1017
+ +
1018
+ + const data = this.renderObjects.get( renderObject );
1019
+ +
1020
+ + return data !== undefined && data.settledState === true;
1021
+ +
1022
+ + }
1023
+ +
1024
+ + /**
1025
+ + * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw,
1026
+ + * so every step of that draw reads the same answer.
1027
+ + *
1028
+ + * @param {RenderObject} renderObject - The render object.
1029
+ + * @return {boolean} Whether the given render object is settled or not.
1030
+ + */
1031
+ + updateSettled( renderObject ) {
1032
+ +
1033
+ + const { object, material, geometry, scene } = renderObject;
1034
+ + const data = this.getRenderObjectData( renderObject );
1035
+ +
1036
+ + if ( object.static !== true || this.renderingVelocity === true ) {
1037
+ +
1038
+ + data.settledState = false;
1039
+ +
1040
+ + return false;
1041
+ +
1042
+ + }
1043
+ +
1044
+ + let settled = data.settled;
1045
+ +
1046
+ + if ( settled === undefined ) {
1047
+ +
1048
+ + data.settled = settled = {
1049
+ + matrix: object.matrixWorld.clone(),
1050
+ + environmentRotation: scene.environmentRotation.clone(),
1051
+ + environmentIntensity: scene.environmentIntensity,
1052
+ + materialVersion: material.version,
1053
+ + geometryVersion: geometry.version
1054
+ + };
1055
+ +
1056
+ + data.settledState = false;
1057
+ +
1058
+ + return false;
1059
+ +
1060
+ + }
1061
+ +
1062
+ + if ( settled.matrix.equals( object.matrixWorld ) !== true ||
1063
+ + settled.environmentRotation.equals( scene.environmentRotation ) !== true ||
1064
+ + settled.environmentIntensity !== scene.environmentIntensity ||
1065
+ + settled.materialVersion !== material.version ||
1066
+ + settled.geometryVersion !== geometry.version ) {
1067
+ +
1068
+ + // re-arm with the current state, so the object settles again once it stops changing
1069
+ +
1070
+ + settled.matrix.copy( object.matrixWorld );
1071
+ + settled.environmentRotation.copy( scene.environmentRotation );
1072
+ + settled.environmentIntensity = scene.environmentIntensity;
1073
+ + settled.materialVersion = material.version;
1074
+ + settled.geometryVersion = geometry.version;
1075
+ +
1076
+ + data.settledState = false;
1077
+ +
1078
+ + return false;
1079
+ +
1080
+ + }
1081
+ +
1082
+ + data.settledState = true;
1083
+ +
1084
+ + return true;
1085
+ +
1086
+ + }
1087
+ +
1088
+ /**
1089
+ * Returns an attribute data structure holding the attributes versions for
1090
+ * monitoring.
1091
+ @@ -306,7 +397,6 @@ class NodeMaterialObserver {
1092
+
1093
+ data = {
1094
+ _renderId: -1,
1095
+ - _equal: false,
1096
+
1097
+ attributes: this.getAttributesData( geometry.attributes ),
1098
+ indexId: geometry.index ? geometry.index.id : null,
1099
+ @@ -335,7 +425,7 @@ class NodeMaterialObserver {
1100
+
1101
+ if ( data === undefined ) {
1102
+
1103
+ - data = { _renderId: -1, _equal: false };
1104
+ + data = { _renderId: -1 };
1105
+
1106
+ for ( const property of this.refreshUniforms ) {
1107
+
1108
+ @@ -419,7 +509,6 @@ class NodeMaterialObserver {
1109
+
1110
+ value.copy( mtlValue );
1111
+
1112
+ - materialData._equal = false;
1113
+ return false;
1114
+
1115
+ }
1116
+ @@ -431,7 +520,6 @@ class NodeMaterialObserver {
1117
+ value.id = mtlValue.id;
1118
+ value.version = mtlValue.version;
1119
+
1120
+ - materialData._equal = false;
1121
+ return false;
1122
+
1123
+ }
1124
+ @@ -440,7 +528,6 @@ class NodeMaterialObserver {
1125
+
1126
+ materialData[ property ] = mtlValue;
1127
+
1128
+ - materialData._equal = false;
1129
+ return false;
1130
+
1131
+ }
1132
+ @@ -456,18 +543,12 @@ class NodeMaterialObserver {
1133
+ renderObjectData.bufferWidth = width;
1134
+ renderObjectData.bufferHeight = height;
1135
+
1136
+ - materialData._equal = false;
1137
+ return false;
1138
+
1139
+ }
1140
+
1141
+ }
1142
+
1143
+ - materialData._equal = true;
1144
+ -
1145
+ - } else {
1146
+ -
1147
+ - if ( materialData._equal === false ) return false;
1148
+
1149
+ }
1150
+
1151
+ @@ -510,7 +591,6 @@ class NodeMaterialObserver {
1152
+ // attribute was removed
1153
+ delete storedAttributes[ name ];
1154
+
1155
+ - geometryData._equal = false;
1156
+ return false;
1157
+
1158
+ }
1159
+ @@ -523,7 +603,6 @@ class NodeMaterialObserver {
1160
+ storedAttributeData.id = id;
1161
+ storedAttributeData.version = version;
1162
+
1163
+ - geometryData._equal = false;
1164
+ return false;
1165
+
1166
+ }
1167
+ @@ -534,7 +613,6 @@ class NodeMaterialObserver {
1168
+
1169
+ geometryData.attributes = this.getAttributesData( attributes );
1170
+
1171
+ - geometryData._equal = false;
1172
+ return false;
1173
+
1174
+ }
1175
+ @@ -552,7 +630,6 @@ class NodeMaterialObserver {
1176
+ geometryData.indexId = currentIndexId;
1177
+ geometryData.indexVersion = currentIndexVersion;
1178
+
1179
+ - geometryData._equal = false;
1180
+ return false;
1181
+
1182
+ }
1183
+ @@ -564,16 +641,10 @@ class NodeMaterialObserver {
1184
+ geometryData.drawRange.start = geometry.drawRange.start;
1185
+ geometryData.drawRange.count = geometry.drawRange.count;
1186
+
1187
+ - geometryData._equal = false;
1188
+ return false;
1189
+
1190
+ }
1191
+
1192
+ - geometryData._equal = true;
1193
+ -
1194
+ - } else {
1195
+ -
1196
+ - if ( geometryData._equal === false ) return false;
1197
+
1198
+ }
1199
+
1200
+ @@ -724,7 +795,14 @@ class NodeMaterialObserver {
1201
+ */
1202
+ needsRefresh( renderObject, nodeFrame ) {
1203
+
1204
+ - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
1205
+ + this.renderingVelocity = this.needsVelocity( nodeFrame.renderer );
1206
+ +
1207
+ + // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and
1208
+ + // the object uniform upload of this object all skip or all refresh.
1209
+ +
1210
+ + this.updateSettled( renderObject );
1211
+ +
1212
+ + if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity )
1213
+ return true;
1214
+
1215
+ const { renderId } = nodeFrame;
1216
+ @@ -737,10 +815,14 @@ class NodeMaterialObserver {
1217
+
1218
+ }
1219
+
1220
+ - const isStatic = renderObject.object.static === true;
1221
+ const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
1222
+
1223
+ - if ( isStatic || isBundle )
1224
+ + // `object.static` is not the question - it is a switch, and a brand new object has it set
1225
+ + // before it has been drawn once, so answering from it skips the draw that creates this
1226
+ + // object's bind groups and leaves the pipeline reading a layout that was never built. A
1227
+ + // static object that moved, or whose material or geometry was edited, has to refresh like
1228
+ + // any other; only a settled one may skip, and nothing is settled before its first refresh.
1229
+ + if ( this.isSettled( renderObject ) === true || isBundle )
1230
+ return false;
1231
+
1232
+ const lightsData = this.getLights( renderObject.lightsNode, renderId );
1233
+ @@ -18320,7 +18402,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) {
1234
+
1235
+ const uniformBufferSize = matrixCount * 16 * 4;
1236
+
1237
+ - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
1238
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1239
+ + if ( false ) {
1240
+
1241
+ instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
1242
+
1243
+ @@ -18414,7 +18497,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
1244
+
1245
+ const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4;
1246
+
1247
+ - if ( uniformBufferSize > builder.getUniformBufferLimit() ) {
1248
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1249
+ + if ( true ) {
1250
+
1251
+ interleavedMatrix = _matrixBuffers.get( matrices );
1252
+
1253
+ @@ -31165,6 +31249,14 @@ class Geometries extends DataMap {
1254
+ */
1255
+ this._geometryDisposeListeners = new Map();
1256
+
1257
+ + /**
1258
+ + * This Weak Map holds the attribute versions a settled render object was last drawn with,
1259
+ + * so a write into one of them is what brings the object back out of the settled path.
1260
+ + *
1261
+ + * @type {WeakMap<RenderObject,Map<BufferAttribute,number>>}
1262
+ + */
1263
+ + this._settledAttributes = new WeakMap();
1264
+ +
1265
+ }
1266
+
1267
+ /**
1268
+ @@ -31190,10 +31282,69 @@ class Geometries extends DataMap {
1269
+
1270
+ if ( this.has( renderObject ) === false ) this.initGeometry( renderObject );
1271
+
1272
+ + // A settled static object has no attribute that could have changed since the last refresh,
1273
+ + // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of
1274
+ + // this draw, and a BufferGeometry carries no version of its own to announce the write, so
1275
+ + // the attributes' own versions are the signal - and the only one there is.
1276
+ +
1277
+ + const monitor = renderObject.getMonitor();
1278
+ +
1279
+ + if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return;
1280
+ +
1281
+ this.updateAttributes( renderObject );
1282
+
1283
+ }
1284
+
1285
+ + /**
1286
+ + * Returns `true` if an attribute of the given render object - or its index - was written to
1287
+ + * since the last time its versions were read, and records the versions it holds now.
1288
+ + *
1289
+ + * @param {RenderObject} renderObject - The render object.
1290
+ + * @return {boolean} Whether any of the render object's versions moved.
1291
+ + */
1292
+ + attributesChanged( renderObject ) {
1293
+ +
1294
+ + const attributes = renderObject.getAttributes();
1295
+ + const index = this.getIndex( renderObject );
1296
+ + const settled = this._settledAttributes.get( renderObject );
1297
+ +
1298
+ + if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) {
1299
+ +
1300
+ + let changed = false;
1301
+ +
1302
+ + for ( const attribute of attributes ) {
1303
+ +
1304
+ + if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) {
1305
+ +
1306
+ + changed = true;
1307
+ + break;
1308
+ +
1309
+ + }
1310
+ +
1311
+ + }
1312
+ +
1313
+ + if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false;
1314
+ +
1315
+ + }
1316
+ +
1317
+ + const versions = new Map();
1318
+ +
1319
+ + for ( const attribute of attributes ) {
1320
+ +
1321
+ + const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
1322
+ +
1323
+ + versions.set( data, data.version );
1324
+ +
1325
+ + }
1326
+ +
1327
+ + if ( index !== null ) versions.set( index, index.version );
1328
+ +
1329
+ + this._settledAttributes.set( renderObject, versions );
1330
+ +
1331
+ + return true;
1332
+ +
1333
+ + }
1334
+ +
1335
+ /**
1336
+ * Initializes the geometry of the given render object.
1337
+ *
1338
+ @@ -32771,7 +32922,7 @@ class Bindings extends DataMap {
1339
+ */
1340
+ updateForRender( renderObject ) {
1341
+
1342
+ - this._updateBindings( this.getForRender( renderObject ) );
1343
+ + this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) );
1344
+
1345
+ }
1346
+
1347
+ @@ -32918,12 +33069,13 @@ class Bindings extends DataMap {
1348
+ * Updates the given array of bindings.
1349
+ *
1350
+ * @param {Array<BindGroup>} bindings - The bind groups.
1351
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
1352
+ */
1353
+ - _updateBindings( bindings ) {
1354
+ + _updateBindings( bindings, settled ) {
1355
+
1356
+ for ( const bindGroup of bindings ) {
1357
+
1358
+ - this._update( bindGroup, bindings );
1359
+ + this._update( bindGroup, bindings, settled );
1360
+
1361
+ }
1362
+
1363
+ @@ -32934,11 +33086,27 @@ class Bindings extends DataMap {
1364
+ *
1365
+ * @param {BindGroup} bindGroup - The bind group to update.
1366
+ * @param {Array<BindGroup>} bindings - The bind groups.
1367
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
1368
+ */
1369
+ - _update( bindGroup, bindings ) {
1370
+ + _update( bindGroup, bindings, settled ) {
1371
+
1372
+ const { backend } = this;
1373
+
1374
+ + // A group a node added after this object's first draw has no buffers and no layout yet, and
1375
+ + // the pipeline reads that layout whatever this draw decides to do with it - so it is built
1376
+ + // here, on every draw. A group that did not exist cannot have been written either, so it is
1377
+ + // written now: `settled` is what tells the loop below to write it, not to skip it.
1378
+ + if ( this.get( bindGroup ).bindGroup === undefined ) {
1379
+ +
1380
+ + this._createBindings( [ bindGroup ] );
1381
+ +
1382
+ + settled = false;
1383
+ +
1384
+ + }
1385
+ +
1386
+ + // a settled object keeps the values it uploaded the last time it refreshed, so an object
1387
+ + // group it has already written is the one thing in here it may skip
1388
+ +
1389
+ let needsBindingsUpdate = false;
1390
+ let cacheBindings = true;
1391
+ let cacheIndex = 0;
1392
+ @@ -32948,7 +33116,7 @@ class Bindings extends DataMap {
1393
+
1394
+ for ( const binding of bindGroup.bindings ) {
1395
+
1396
+ - const updatedGroup = this.nodes.updateGroup( binding );
1397
+ + const updatedGroup = this.nodes.updateGroup( binding, settled );
1398
+
1399
+ // every uniforms group is a uniform buffer. So if no update is required,
1400
+ // we move one with the next binding. Otherwise the next if block will update the group.
1401
+ @@ -34181,6 +34349,21 @@ class Textures extends DataMap {
1402
+
1403
+ backend.destroyTexture( texture );
1404
+
1405
+ + // delete cached bind groups so they don't point to the destroyed texture
1406
+ +
1407
+ + if ( textureData.bindGroups ) {
1408
+ +
1409
+ + for ( const bindGroup of textureData.bindGroups ) {
1410
+ +
1411
+ + const bindingsData = this.backend.get( bindGroup );
1412
+ +
1413
+ + bindingsData.groups = undefined;
1414
+ + bindingsData.versions = undefined;
1415
+ +
1416
+ + }
1417
+ +
1418
+ + }
1419
+ +
1420
+ }
1421
+
1422
+ //
1423
+ @@ -55244,15 +55427,17 @@ class NodeManager extends DataMap {
1424
+ * Returns `true` if the given node uniforms group must be updated or not.
1425
+ *
1426
+ * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
1427
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
1428
+ * @return {boolean} Whether the node uniforms group requires an update or not.
1429
+ */
1430
+ - updateGroup( nodeUniformsGroup ) {
1431
+ + updateGroup( nodeUniformsGroup, settled ) {
1432
+
1433
+ const groupNode = nodeUniformsGroup.groupNode;
1434
+
1435
+ - // groups that are updated per object always require an update so no further checks are needed
1436
+ + // groups that are updated per object always require an update so no further checks are needed.
1437
+ + // A settled static object keeps the values it uploaded the last time it was refreshed.
1438
+
1439
+ - if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
1440
+ + if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true;
1441
+
1442
+ // check for update
1443
+
1444
+ @@ -56056,6 +56241,21 @@ class NodeManager extends DataMap {
1445
+
1446
+ }
1447
+
1448
+ + /**
1449
+ + * Returns `true` if the per-object updates of the given render object can be skipped
1450
+ + * because the object is static and nothing it depends on changed since its last refresh.
1451
+ + *
1452
+ + * @param {RenderObject} renderObject - The render object.
1453
+ + * @return {boolean} Whether the render object is settled or not.
1454
+ + */
1455
+ + isSettled( renderObject ) {
1456
+ +
1457
+ + const monitor = renderObject.getMonitor();
1458
+ +
1459
+ + return monitor !== undefined ? monitor.isSettled( renderObject ) : false;
1460
+ +
1461
+ + }
1462
+ +
1463
+ /**
1464
+ * Triggers the call of `updateBefore()` methods
1465
+ * for all nodes of the given render object.
1466
+ @@ -56065,9 +56265,14 @@ class NodeManager extends DataMap {
1467
+ updateBefore( renderObject ) {
1468
+
1469
+ const nodeBuilder = renderObject.getNodeBuilderState();
1470
+ + const settled = this.isSettled( renderObject );
1471
+
1472
+ for ( const node of nodeBuilder.updateBeforeNodes ) {
1473
+
1474
+ + // a settled static object has nothing to update per object
1475
+ +
1476
+ + if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue;
1477
+ +
1478
+ // update frame state for each node
1479
+
1480
+ this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
1481
+ @@ -56085,10 +56290,11 @@ class NodeManager extends DataMap {
1482
+ updateAfter( renderObject ) {
1483
+
1484
+ const nodeBuilder = renderObject.getNodeBuilderState();
1485
+ + const settled = this.isSettled( renderObject );
1486
+
1487
+ for ( const node of nodeBuilder.updateAfterNodes ) {
1488
+
1489
+ - // update frame state for each node
1490
+ + if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue;
1491
+
1492
+ this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
1493
+
1494
+ @@ -56125,9 +56331,14 @@ class NodeManager extends DataMap {
1495
+
1496
+ const nodeFrame = this.getNodeFrameForRender( renderObject );
1497
+ const nodeBuilder = renderObject.getNodeBuilderState();
1498
+ + const settled = this.isSettled( renderObject );
1499
+
1500
+ for ( const node of nodeBuilder.updateNodes ) {
1501
+
1502
+ + // a settled static object keeps the per-object values it uploaded last time
1503
+ +
1504
+ + if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue;
1505
+ +
1506
+ nodeFrame.updateNode( node );
1507
+
1508
+ }
1509
+ @@ -62886,18 +63097,17 @@ class Renderer {
1510
+
1511
+ //
1512
+
1513
+ - const needsRefresh = this._nodes.needsRefresh( renderObject );
1514
+ -
1515
+ - if ( needsRefresh ) {
1516
+ -
1517
+ - this._nodes.updateBefore( renderObject );
1518
+ -
1519
+ - this._geometries.updateForRender( renderObject );
1520
+ -
1521
+ - this._nodes.updateForRender( renderObject );
1522
+ - this._bindings.updateForRender( renderObject );
1523
+ + this._nodes.needsRefresh( renderObject );
1524
+
1525
+ - }
1526
+ + // Every step of a draw runs, settled or not. What a settled static object skips is decided
1527
+ + // inside each of them - its OBJECT node updates, its object uniform group write and its
1528
+ + // attribute scan - because each of those answers for itself, and a step left out here
1529
+ + // cannot: a group a node added after this object's first draw still has to be created, and
1530
+ + // the pipeline reads that group's layout whatever this draw decided to do with it.
1531
+ + this._geometries.updateForRender( renderObject );
1532
+ + this._nodes.updateBefore( renderObject );
1533
+ + this._nodes.updateForRender( renderObject );
1534
+ + this._bindings.updateForRender( renderObject );
1535
+
1536
+ this._pipelines.updateForRender( renderObject );
1537
+
1538
+ @@ -62907,7 +63117,7 @@ class Renderer {
1539
+
1540
+ this.backend.draw( renderObject, this.info );
1541
+
1542
+ - if ( needsRefresh ) this._nodes.updateAfter( renderObject );
1543
+ + this._nodes.updateAfter( renderObject );
1544
+
1545
+ }
1546
+
1547
+ @@ -79004,7 +79214,14 @@ class WGSLNodeBuilder extends NodeBuilder {
1548
+
1549
+ } else {
1550
+
1551
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
1552
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
1553
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
1554
+ + // operands (including the array form above) stay u32.
1555
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
1556
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
1557
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
1558
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
1559
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
1560
+
1561
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
1562
+
1563
+ @@ -81295,9 +81512,15 @@ class WebGPUAttributeUtils {
1564
+ destroyAttribute( attribute ) {
1565
+
1566
+ const backend = this.backend;
1567
+ - const data = backend.get( this._getBufferAttribute( attribute ) );
1568
+ + const bufferAttribute = this._getBufferAttribute( attribute );
1569
+ + const data = backend.get( bufferAttribute );
1570
+ +
1571
+ + // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes
1572
+ + // share it: destroy it once and forget it there too, as the WebGL backend does, or the next
1573
+ + // createAttribute() finds the destroyed buffer and submits it (threenative patch).
1574
+ + if ( data.buffer !== undefined ) data.buffer.destroy();
1575
+
1576
+ - data.buffer.destroy();
1577
+ + if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute );
1578
+
1579
+ backend.delete( attribute );
1580
+
1581
+ diff --git a/src/materials/nodes/manager/NodeMaterialObserver.js b/src/materials/nodes/manager/NodeMaterialObserver.js
1582
+ index 1f4c254..7e2cb99 100644
1583
+ --- a/src/materials/nodes/manager/NodeMaterialObserver.js
1584
+ +++ b/src/materials/nodes/manager/NodeMaterialObserver.js
1585
+ @@ -133,6 +133,15 @@ class NodeMaterialObserver {
1586
+ */
1587
+ this.renderId = 0;
1588
+
1589
+ + /**
1590
+ + * Whether the current render produces motion vectors. Objects keep refreshing
1591
+ + * fully while this is the case.
1592
+ + *
1593
+ + * @type {boolean}
1594
+ + * @default false
1595
+ + */
1596
+ + this.renderingVelocity = false;
1597
+ +
1598
+ }
1599
+
1600
+ /**
1601
+ @@ -232,6 +241,88 @@ class NodeMaterialObserver {
1602
+
1603
+ }
1604
+
1605
+ + /**
1606
+ + * Returns `true` if the given render object is a static object whose per-object state - world
1607
+ + * matrix, material, geometry and the scene environment - did not change since the last
1608
+ + * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still
1609
+ + * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node
1610
+ + * updates, its object uniform group upload and its per-draw attribute scan.
1611
+ + *
1612
+ + * @param {RenderObject} renderObject - The render object.
1613
+ + * @return {boolean} Whether the given render object is settled or not.
1614
+ + */
1615
+ + isSettled( renderObject ) {
1616
+ +
1617
+ + const data = this.renderObjects.get( renderObject );
1618
+ +
1619
+ + return data !== undefined && data.settledState === true;
1620
+ +
1621
+ + }
1622
+ +
1623
+ + /**
1624
+ + * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw,
1625
+ + * so every step of that draw reads the same answer.
1626
+ + *
1627
+ + * @param {RenderObject} renderObject - The render object.
1628
+ + * @return {boolean} Whether the given render object is settled or not.
1629
+ + */
1630
+ + updateSettled( renderObject ) {
1631
+ +
1632
+ + const { object, material, geometry, scene } = renderObject;
1633
+ + const data = this.getRenderObjectData( renderObject );
1634
+ +
1635
+ + if ( object.static !== true || this.renderingVelocity === true ) {
1636
+ +
1637
+ + data.settledState = false;
1638
+ +
1639
+ + return false;
1640
+ +
1641
+ + }
1642
+ +
1643
+ + let settled = data.settled;
1644
+ +
1645
+ + if ( settled === undefined ) {
1646
+ +
1647
+ + data.settled = settled = {
1648
+ + matrix: object.matrixWorld.clone(),
1649
+ + environmentRotation: scene.environmentRotation.clone(),
1650
+ + environmentIntensity: scene.environmentIntensity,
1651
+ + materialVersion: material.version,
1652
+ + geometryVersion: geometry.version
1653
+ + };
1654
+ +
1655
+ + data.settledState = false;
1656
+ +
1657
+ + return false;
1658
+ +
1659
+ + }
1660
+ +
1661
+ + if ( settled.matrix.equals( object.matrixWorld ) !== true ||
1662
+ + settled.environmentRotation.equals( scene.environmentRotation ) !== true ||
1663
+ + settled.environmentIntensity !== scene.environmentIntensity ||
1664
+ + settled.materialVersion !== material.version ||
1665
+ + settled.geometryVersion !== geometry.version ) {
1666
+ +
1667
+ + // re-arm with the current state, so the object settles again once it stops changing
1668
+ +
1669
+ + settled.matrix.copy( object.matrixWorld );
1670
+ + settled.environmentRotation.copy( scene.environmentRotation );
1671
+ + settled.environmentIntensity = scene.environmentIntensity;
1672
+ + settled.materialVersion = material.version;
1673
+ + settled.geometryVersion = geometry.version;
1674
+ +
1675
+ + data.settledState = false;
1676
+ +
1677
+ + return false;
1678
+ +
1679
+ + }
1680
+ +
1681
+ + data.settledState = true;
1682
+ +
1683
+ + return true;
1684
+ +
1685
+ + }
1686
+ +
1687
+ /**
1688
+ * Returns an attribute data structure holding the attributes versions for
1689
+ * monitoring.
1690
+ @@ -298,7 +389,6 @@ class NodeMaterialObserver {
1691
+
1692
+ data = {
1693
+ _renderId: - 1,
1694
+ - _equal: false,
1695
+
1696
+ attributes: this.getAttributesData( geometry.attributes ),
1697
+ indexId: geometry.index ? geometry.index.id : null,
1698
+ @@ -327,7 +417,7 @@ class NodeMaterialObserver {
1699
+
1700
+ if ( data === undefined ) {
1701
+
1702
+ - data = { _renderId: - 1, _equal: false };
1703
+ + data = { _renderId: - 1 };
1704
+
1705
+ for ( const property of this.refreshUniforms ) {
1706
+
1707
+ @@ -411,7 +501,6 @@ class NodeMaterialObserver {
1708
+
1709
+ value.copy( mtlValue );
1710
+
1711
+ - materialData._equal = false;
1712
+ return false;
1713
+
1714
+ }
1715
+ @@ -423,7 +512,6 @@ class NodeMaterialObserver {
1716
+ value.id = mtlValue.id;
1717
+ value.version = mtlValue.version;
1718
+
1719
+ - materialData._equal = false;
1720
+ return false;
1721
+
1722
+ }
1723
+ @@ -432,7 +520,6 @@ class NodeMaterialObserver {
1724
+
1725
+ materialData[ property ] = mtlValue;
1726
+
1727
+ - materialData._equal = false;
1728
+ return false;
1729
+
1730
+ }
1731
+ @@ -448,18 +535,12 @@ class NodeMaterialObserver {
1732
+ renderObjectData.bufferWidth = width;
1733
+ renderObjectData.bufferHeight = height;
1734
+
1735
+ - materialData._equal = false;
1736
+ return false;
1737
+
1738
+ }
200
1739
 
201
1740
  }
202
1741
 
203
- - overrideMaterial.alphaTest = material.alphaTest;
204
- + // `Material.alphaTest`'s setter bumps `version` whenever alpha-test-ness flips, and this
205
- + // override material is shared by every caster in the pass. A scene that mixes alpha-tested
206
- + // casters with opaque ones therefore moves that shared version several times per frame,
207
- + // and every OTHER render object drawn with it then fails the version gate in
208
- + // `RenderObjects.get` and recomputes a full material cache key that always matches.
209
- + // Writing the backing field leaves the `alphaTest` getter — and so the cache key — still
210
- + // reporting this caster's value, while each render object tracks the staleness signal
211
- + // that belongs to it: its own source material's version.
212
- + overrideMaterial._alphaTest = material.alphaTest;
213
- overrideMaterial.alphaMap = material.alphaMap;
214
- overrideMaterial.displacementMap = material.displacementMap;
215
- overrideMaterial.displacementScale = material.displacementScale;
216
- @@ -62825,6 +62879,7 @@ class Renderer {
217
- scene.overrideMaterial.displacementMap = materialDisplacementMap;
218
- scene.overrideMaterial.displacementScale = materialDisplacementScale;
219
- scene.overrideMaterial.displacementBias = materialDisplacementBias;
220
- + scene.overrideMaterial._alphaTest = materialAlphaTest;
1742
+ - materialData._equal = true;
1743
+ -
1744
+ - } else {
1745
+ -
1746
+ - if ( materialData._equal === false ) return false;
221
1747
 
222
1748
  }
223
1749
 
224
- @@ -78999,15 +78999,22 @@ class WGSLNodeBuilder extends NodeBuilder {
225
- let snippet;
1750
+ @@ -502,7 +583,6 @@ class NodeMaterialObserver {
1751
+ // attribute was removed
1752
+ delete storedAttributes[ name ];
226
1753
 
227
- if ( depthSnippet ) {
1754
+ - geometryData._equal = false;
1755
+ return false;
228
1756
 
229
- snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
1757
+ }
1758
+ @@ -515,7 +595,6 @@ class NodeMaterialObserver {
1759
+ storedAttributeData.id = id;
1760
+ storedAttributeData.version = version;
230
1761
 
231
- } else {
1762
+ - geometryData._equal = false;
1763
+ return false;
232
1764
 
233
- - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
234
- + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
235
- + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
236
- + // operands (including the array form above) stay u32.
237
- + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
238
- + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
239
- + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
240
- + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
241
- + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
1765
+ }
1766
+ @@ -526,7 +605,6 @@ class NodeMaterialObserver {
242
1767
 
243
- if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
1768
+ geometryData.attributes = this.getAttributesData( attributes );
244
1769
 
245
- snippet += '.x';
1770
+ - geometryData._equal = false;
1771
+ return false;
246
1772
 
247
1773
  }
248
- @@ -79586,7 +79641,8 @@ class WGSLNodeBuilder extends NodeBuilder {
1774
+ @@ -544,7 +622,6 @@ class NodeMaterialObserver {
1775
+ geometryData.indexId = currentIndexId;
1776
+ geometryData.indexVersion = currentIndexVersion;
249
1777
 
250
- uniformGPU = buffer;
1778
+ - geometryData._equal = false;
1779
+ return false;
251
1780
 
252
- - uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id;
253
- + // The base builder supplies a unique name within this program. A process-wide
254
- + // allocation id changes otherwise identical shader source for every buffer.
1781
+ }
1782
+ @@ -556,16 +633,10 @@ class NodeMaterialObserver {
1783
+ geometryData.drawRange.start = geometry.drawRange.start;
1784
+ geometryData.drawRange.count = geometry.drawRange.count;
255
1785
 
256
- } else {
1786
+ - geometryData._equal = false;
1787
+ return false;
257
1788
 
258
- @@ -80329,9 +80385,12 @@ ${ flowData.code }
259
- const textureNode = uniform.node;
260
- const texture = textureNode.value;
1789
+ }
261
1790
 
262
- - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
263
- - // Also textureGather always need sampler.
264
- - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
265
- + // The bind group layout freezes sampler inclusion once, in getUniformFromNode
266
- + // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can
267
- + // disagree with that layout and shift every later binding index.
268
- + const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache );
269
- + const uniformGPU = nodeData.uniformGPU;
270
- + const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true;
1791
+ - geometryData._equal = true;
1792
+ -
1793
+ - } else {
1794
+ -
1795
+ - if ( geometryData._equal === false ) return false;
271
1796
 
272
- if ( needsSampler ) {
1797
+ }
273
1798
 
274
- @@ -83814,6 +83873,14 @@ class WebGPUBackend extends Backend {
275
- */
276
- this.compatibilityMode = null;
1799
+ @@ -716,7 +787,14 @@ class NodeMaterialObserver {
1800
+ */
1801
+ needsRefresh( renderObject, nodeFrame ) {
277
1802
 
278
- + /**
279
- + * A reference to the WebGPU instance that creates adapters.
280
- + *
281
- + * @type {?GPU}
282
- + * @default null
283
- + */
284
- + this.gpu = null;
1803
+ - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
1804
+ + this.renderingVelocity = this.needsVelocity( nodeFrame.renderer );
285
1805
  +
286
- /**
287
- * A reference to the device.
288
- *
289
- @@ -83926,7 +83993,9 @@ class WebGPUBackend extends Backend {
290
- xrCompatible: renderer.xr.enabled
291
- };
292
-
293
- - const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
294
- + this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null;
1806
+ + // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and
1807
+ + // the object uniform upload of this object all skip or all refresh.
295
1808
  +
296
- + const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null;
1809
+ + this.updateSettled( renderObject );
1810
+ +
1811
+ + if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity )
1812
+ return true;
297
1813
 
298
- if ( adapter === null ) {
1814
+ const { renderId } = nodeFrame;
1815
+ @@ -729,10 +807,14 @@ class NodeMaterialObserver {
1816
+
1817
+ }
1818
+
1819
+ - const isStatic = renderObject.object.static === true;
1820
+ const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
299
1821
 
1822
+ - if ( isStatic || isBundle )
1823
+ + // `object.static` is not the question - it is a switch, and a brand new object has it set
1824
+ + // before it has been drawn once, so answering from it skips the draw that creates this
1825
+ + // object's bind groups and leaves the pipeline reading a layout that was never built. A
1826
+ + // static object that moved, or whose material or geometry was edited, has to refresh like
1827
+ + // any other; only a settled one may skip, and nothing is settled before its first refresh.
1828
+ + if ( this.isSettled( renderObject ) === true || isBundle )
1829
+ return false;
1830
+
1831
+ const lightsData = this.getLights( renderObject.lightsNode, renderId );
300
1832
  diff --git a/src/nodes/accessors/Batch.js b/src/nodes/accessors/Batch.js
301
- index ab62e98dadab2539d7e1d663fd42267bbc8ebcaa..2cf4bb6039853d5c922e89041419250b243694ef 100644
1833
+ index ab62e98..2cf4bb6 100644
302
1834
  --- a/src/nodes/accessors/Batch.js
303
1835
  +++ b/src/nodes/accessors/Batch.js
304
1836
  @@ -1,6 +1,6 @@
@@ -344,10 +1876,30 @@ index ab62e98dadab2539d7e1d663fd42267bbc8ebcaa..2cf4bb6039853d5c922e89041419250b
344
1876
  -
345
1877
  -
346
1878
  diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js
347
- index b89834b9166c75fd946ade24db265bf8c367d244..c7b7bdd7a31d852a581d4bc75a1e1afc1d214e5e 100644
1879
+ index b89834b..ed00e75 100644
348
1880
  --- a/src/nodes/accessors/Instance.js
349
1881
  +++ b/src/nodes/accessors/Instance.js
350
- @@ -220,7 +220,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
1882
+ @@ -40,7 +40,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) {
1883
+
1884
+ const uniformBufferSize = matrixCount * 16 * 4;
1885
+
1886
+ - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
1887
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1888
+ + if ( false ) {
1889
+
1890
+ instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
1891
+
1892
+ @@ -134,7 +135,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
1893
+
1894
+ const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4;
1895
+
1896
+ - if ( uniformBufferSize > builder.getUniformBufferLimit() ) {
1897
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1898
+ + if ( true ) {
1899
+
1900
+ interleavedMatrix = _matrixBuffers.get( matrices );
1901
+
1902
+ @@ -220,7 +222,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
351
1903
 
352
1904
  const previousInstanceData = _previousInstanceMatrices.get( object );
353
1905
 
@@ -358,7 +1910,7 @@ index b89834b9166c75fd946ade24db265bf8c367d244..c7b7bdd7a31d852a581d4bc75a1e1afc
358
1910
  } );
359
1911
 
360
1912
  diff --git a/src/nodes/accessors/Skinning.js b/src/nodes/accessors/Skinning.js
361
- index 2356aed8656dceb1ea74b73ea2e739371a960484..aaeea633a8ece291be5faf868e312867ddad01b6 100644
1913
+ index 2356aed..aaeea63 100644
362
1914
  --- a/src/nodes/accessors/Skinning.js
363
1915
  +++ b/src/nodes/accessors/Skinning.js
364
1916
  @@ -149,7 +149,8 @@ export const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
@@ -382,7 +1934,7 @@ index 2356aed8656dceb1ea74b73ea2e739371a960484..aaeea633a8ece291be5faf868e312867
382
1934
  }
383
1935
 
384
1936
  diff --git a/src/nodes/accessors/VelocityNode.js b/src/nodes/accessors/VelocityNode.js
385
- index 373548b0b587e7da3ef0993bfe316af420e7f66c..c48002abbce20ee8b29908cf314d7b03afe0db03 100644
1937
+ index 373548b..c48002a 100644
386
1938
  --- a/src/nodes/accessors/VelocityNode.js
387
1939
  +++ b/src/nodes/accessors/VelocityNode.js
388
1940
  @@ -106,7 +106,8 @@ class VelocityNode extends TempNode {
@@ -396,7 +1948,7 @@ index 373548b0b587e7da3ef0993bfe316af420e7f66c..c48002abbce20ee8b29908cf314d7b03
396
1948
  //
397
1949
 
398
1950
  diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js
399
- index be00b81ef36e1af83b0066603e616f8f1f81a115..1990da8fe8f1f29a7b53030756dfd966d8de3cb6 100644
1951
+ index be00b81..1990da8 100644
400
1952
  --- a/src/nodes/core/NodeBuilder.js
401
1953
  +++ b/src/nodes/core/NodeBuilder.js
402
1954
  @@ -3450,7 +3450,7 @@ class NodeBuilder {
@@ -409,7 +1961,7 @@ index be00b81ef36e1af83b0066603e616f8f1f81a115..1990da8fe8f1f29a7b53030756dfd966
409
1961
  }
410
1962
 
411
1963
  diff --git a/src/objects/BatchedMesh.js b/src/objects/BatchedMesh.js
412
- index 2ef7cb257438826059eb2408b3756c4891f06a10..35cc0ffe83a69ea9fa5fca6dec2d820ca193f88d 100644
1964
+ index 2ef7cb2..35cc0ff 100644
413
1965
  --- a/src/objects/BatchedMesh.js
414
1966
  +++ b/src/objects/BatchedMesh.js
415
1967
  @@ -181,6 +181,8 @@ function copyArrayContents( src, target ) {
@@ -422,9 +1974,18 @@ index 2ef7cb257438826059eb2408b3756c4891f06a10..35cc0ffe83a69ea9fa5fca6dec2d820c
422
1974
  * Constructs a new batched mesh.
423
1975
  *
424
1976
  diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js
425
- index 5f3d213a016b37db3b2e5d022da3dd0234d86f6d..7a9a168f29773f37ef9a51ef9f07ade9f3ea95ef 100644
1977
+ index 5f3d213..a3cec4e 100644
426
1978
  --- a/src/renderers/common/Bindings.js
427
1979
  +++ b/src/renderers/common/Bindings.js
1980
+ @@ -145,7 +145,7 @@ class Bindings extends DataMap {
1981
+ */
1982
+ updateForRender( renderObject ) {
1983
+
1984
+ - this._updateBindings( this.getForRender( renderObject ) );
1985
+ + this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) );
1986
+
1987
+ }
1988
+
428
1989
  @@ -206,10 +206,14 @@ class Bindings extends DataMap {
429
1990
 
430
1991
  } else if ( binding.isSampledTexture ) {
@@ -440,8 +2001,165 @@ index 5f3d213a016b37db3b2e5d022da3dd0234d86f6d..7a9a168f29773f37ef9a51ef9f07ade9
440
2001
  this.textures.updateSampler( binding );
441
2002
 
442
2003
  } else if ( binding.isStorageBuffer ) {
2004
+ @@ -292,12 +296,13 @@ class Bindings extends DataMap {
2005
+ * Updates the given array of bindings.
2006
+ *
2007
+ * @param {Array<BindGroup>} bindings - The bind groups.
2008
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
2009
+ */
2010
+ - _updateBindings( bindings ) {
2011
+ + _updateBindings( bindings, settled ) {
2012
+
2013
+ for ( const bindGroup of bindings ) {
2014
+
2015
+ - this._update( bindGroup, bindings );
2016
+ + this._update( bindGroup, bindings, settled );
2017
+
2018
+ }
2019
+
2020
+ @@ -308,11 +313,27 @@ class Bindings extends DataMap {
2021
+ *
2022
+ * @param {BindGroup} bindGroup - The bind group to update.
2023
+ * @param {Array<BindGroup>} bindings - The bind groups.
2024
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
2025
+ */
2026
+ - _update( bindGroup, bindings ) {
2027
+ + _update( bindGroup, bindings, settled ) {
2028
+
2029
+ const { backend } = this;
2030
+
2031
+ + // A group a node added after this object's first draw has no buffers and no layout yet, and
2032
+ + // the pipeline reads that layout whatever this draw decides to do with it - so it is built
2033
+ + // here, on every draw. A group that did not exist cannot have been written either, so it is
2034
+ + // written now: `settled` is what tells the loop below to write it, not to skip it.
2035
+ + if ( this.get( bindGroup ).bindGroup === undefined ) {
2036
+ +
2037
+ + this._createBindings( [ bindGroup ] );
2038
+ +
2039
+ + settled = false;
2040
+ +
2041
+ + }
2042
+ +
2043
+ + // a settled object keeps the values it uploaded the last time it refreshed, so an object
2044
+ + // group it has already written is the one thing in here it may skip
2045
+ +
2046
+ let needsBindingsUpdate = false;
2047
+ let cacheBindings = true;
2048
+ let cacheIndex = 0;
2049
+ @@ -322,7 +343,7 @@ class Bindings extends DataMap {
2050
+
2051
+ for ( const binding of bindGroup.bindings ) {
2052
+
2053
+ - const updatedGroup = this.nodes.updateGroup( binding );
2054
+ + const updatedGroup = this.nodes.updateGroup( binding, settled );
2055
+
2056
+ // every uniforms group is a uniform buffer. So if no update is required,
2057
+ // we move one with the next binding. Otherwise the next if block will update the group.
2058
+ diff --git a/src/renderers/common/Geometries.js b/src/renderers/common/Geometries.js
2059
+ index eb332c4..979da2f 100644
2060
+ --- a/src/renderers/common/Geometries.js
2061
+ +++ b/src/renderers/common/Geometries.js
2062
+ @@ -139,6 +139,14 @@ class Geometries extends DataMap {
2063
+ */
2064
+ this._geometryDisposeListeners = new Map();
2065
+
2066
+ + /**
2067
+ + * This Weak Map holds the attribute versions a settled render object was last drawn with,
2068
+ + * so a write into one of them is what brings the object back out of the settled path.
2069
+ + *
2070
+ + * @type {WeakMap<RenderObject,Map<BufferAttribute,number>>}
2071
+ + */
2072
+ + this._settledAttributes = new WeakMap();
2073
+ +
2074
+ }
2075
+
2076
+ /**
2077
+ @@ -164,10 +172,69 @@ class Geometries extends DataMap {
2078
+
2079
+ if ( this.has( renderObject ) === false ) this.initGeometry( renderObject );
2080
+
2081
+ + // A settled static object has no attribute that could have changed since the last refresh,
2082
+ + // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of
2083
+ + // this draw, and a BufferGeometry carries no version of its own to announce the write, so
2084
+ + // the attributes' own versions are the signal - and the only one there is.
2085
+ +
2086
+ + const monitor = renderObject.getMonitor();
2087
+ +
2088
+ + if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return;
2089
+ +
2090
+ this.updateAttributes( renderObject );
2091
+
2092
+ }
2093
+
2094
+ + /**
2095
+ + * Returns `true` if an attribute of the given render object - or its index - was written to
2096
+ + * since the last time its versions were read, and records the versions it holds now.
2097
+ + *
2098
+ + * @param {RenderObject} renderObject - The render object.
2099
+ + * @return {boolean} Whether any of the render object's versions moved.
2100
+ + */
2101
+ + attributesChanged( renderObject ) {
2102
+ +
2103
+ + const attributes = renderObject.getAttributes();
2104
+ + const index = this.getIndex( renderObject );
2105
+ + const settled = this._settledAttributes.get( renderObject );
2106
+ +
2107
+ + if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) {
2108
+ +
2109
+ + let changed = false;
2110
+ +
2111
+ + for ( const attribute of attributes ) {
2112
+ +
2113
+ + if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) {
2114
+ +
2115
+ + changed = true;
2116
+ + break;
2117
+ +
2118
+ + }
2119
+ +
2120
+ + }
2121
+ +
2122
+ + if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false;
2123
+ +
2124
+ + }
2125
+ +
2126
+ + const versions = new Map();
2127
+ +
2128
+ + for ( const attribute of attributes ) {
2129
+ +
2130
+ + const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
2131
+ +
2132
+ + versions.set( data, data.version );
2133
+ +
2134
+ + }
2135
+ +
2136
+ + if ( index !== null ) versions.set( index, index.version );
2137
+ +
2138
+ + this._settledAttributes.set( renderObject, versions );
2139
+ +
2140
+ + return true;
2141
+ +
2142
+ + }
2143
+ +
2144
+ /**
2145
+ * Initializes the geometry of the given render object.
2146
+ *
2147
+ diff --git a/src/renderers/common/RenderContexts.js b/src/renderers/common/RenderContexts.js
2148
+ index 64ecdee..7319e25 100644
2149
+ --- a/src/renderers/common/RenderContexts.js
2150
+ +++ b/src/renderers/common/RenderContexts.js
2151
+ @@ -77,7 +77,8 @@ class RenderContexts {
2152
+
2153
+ }
2154
+
2155
+ - if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
2156
+ + const sampleCount = renderTarget !== null ? renderTarget.samples : this.renderer.currentSamples;
2157
+ + renderState.sampleCount = sampleCount === 0 ? 1 : sampleCount;
2158
+
2159
+ renderState.clearDepthValue = this.renderer.getClearDepth();
2160
+ renderState.clearStencilValue = this.renderer.getClearStencil();
443
2161
  diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js
444
- index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..3a8b0012abf054f50ab66466bab7efa16fe1bc09 100644
2162
+ index c900fa4..a3dcba2 100644
445
2163
  --- a/src/renderers/common/RenderObject.js
446
2164
  +++ b/src/renderers/common/RenderObject.js
447
2165
  @@ -299,6 +299,18 @@ class RenderObject {
@@ -472,8 +2190,16 @@ index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..3a8b0012abf054f50ab66466bab7efa1
472
2190
 
473
2191
  }
474
2192
 
2193
+ @@ -936,6 +948,7 @@ class RenderObject {
2194
+ }
2195
+
2196
+ cacheKey = hash( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version );
2197
+ + cacheKey = hash( cacheKey, this.context.sampleCount );
2198
+
2199
+ return cacheKey;
2200
+
475
2201
  diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js
476
- index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..72569d0339314d26eb7a35ff2a8462a803c852a6 100644
2202
+ index 676cc2b..72569d0 100644
477
2203
  --- a/src/renderers/common/RenderObjects.js
478
2204
  +++ b/src/renderers/common/RenderObjects.js
479
2205
  @@ -97,6 +97,8 @@ class RenderObjects {
@@ -524,7 +2250,7 @@ index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..72569d0339314d26eb7a35ff2a8462a8
524
2250
  //
525
2251
 
526
2252
  diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js
527
- index 8dfc2ae055ad7b9d4c00e91f7d52a39e747caebe..e9d27b3ddad3b1f080a717d1f6683b60afd2b259 100644
2253
+ index 8dfc2ae..89b33ef 100644
528
2254
  --- a/src/renderers/common/Renderer.js
529
2255
  +++ b/src/renderers/common/Renderer.js
530
2256
  @@ -3550,6 +3550,7 @@ class Renderer {
@@ -568,8 +2294,162 @@ index 8dfc2ae055ad7b9d4c00e91f7d52a39e747caebe..e9d27b3ddad3b1f080a717d1f6683b60
568
2294
 
569
2295
  }
570
2296
 
2297
+ @@ -3705,18 +3716,17 @@ class Renderer {
2298
+
2299
+ //
2300
+
2301
+ - const needsRefresh = this._nodes.needsRefresh( renderObject );
2302
+ + this._nodes.needsRefresh( renderObject );
2303
+
2304
+ - if ( needsRefresh ) {
2305
+ -
2306
+ - this._nodes.updateBefore( renderObject );
2307
+ -
2308
+ - this._geometries.updateForRender( renderObject );
2309
+ -
2310
+ - this._nodes.updateForRender( renderObject );
2311
+ - this._bindings.updateForRender( renderObject );
2312
+ -
2313
+ - }
2314
+ + // Every step of a draw runs, settled or not. What a settled static object skips is decided
2315
+ + // inside each of them - its OBJECT node updates, its object uniform group write and its
2316
+ + // attribute scan - because each of those answers for itself, and a step left out here
2317
+ + // cannot: a group a node added after this object's first draw still has to be created, and
2318
+ + // the pipeline reads that group's layout whatever this draw decided to do with it.
2319
+ + this._geometries.updateForRender( renderObject );
2320
+ + this._nodes.updateBefore( renderObject );
2321
+ + this._nodes.updateForRender( renderObject );
2322
+ + this._bindings.updateForRender( renderObject );
2323
+
2324
+ this._pipelines.updateForRender( renderObject );
2325
+
2326
+ @@ -3726,7 +3736,7 @@ class Renderer {
2327
+
2328
+ this.backend.draw( renderObject, this.info );
2329
+
2330
+ - if ( needsRefresh ) this._nodes.updateAfter( renderObject );
2331
+ + this._nodes.updateAfter( renderObject );
2332
+
2333
+ }
2334
+
2335
+ diff --git a/src/renderers/common/Textures.js b/src/renderers/common/Textures.js
2336
+ index f42b90e..be47645 100644
2337
+ --- a/src/renderers/common/Textures.js
2338
+ +++ b/src/renderers/common/Textures.js
2339
+ @@ -215,6 +215,21 @@ class Textures extends DataMap {
2340
+
2341
+ backend.destroyTexture( texture );
2342
+
2343
+ + // delete cached bind groups so they don't point to the destroyed texture
2344
+ +
2345
+ + if ( textureData.bindGroups ) {
2346
+ +
2347
+ + for ( const bindGroup of textureData.bindGroups ) {
2348
+ +
2349
+ + const bindingsData = this.backend.get( bindGroup );
2350
+ +
2351
+ + bindingsData.groups = undefined;
2352
+ + bindingsData.versions = undefined;
2353
+ +
2354
+ + }
2355
+ +
2356
+ + }
2357
+ +
2358
+ }
2359
+
2360
+ //
2361
+ diff --git a/src/renderers/common/nodes/NodeManager.js b/src/renderers/common/nodes/NodeManager.js
2362
+ index f6d4f0b..033128c 100644
2363
+ --- a/src/renderers/common/nodes/NodeManager.js
2364
+ +++ b/src/renderers/common/nodes/NodeManager.js
2365
+ @@ -109,15 +109,17 @@ class NodeManager extends DataMap {
2366
+ * Returns `true` if the given node uniforms group must be updated or not.
2367
+ *
2368
+ * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
2369
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
2370
+ * @return {boolean} Whether the node uniforms group requires an update or not.
2371
+ */
2372
+ - updateGroup( nodeUniformsGroup ) {
2373
+ + updateGroup( nodeUniformsGroup, settled ) {
2374
+
2375
+ const groupNode = nodeUniformsGroup.groupNode;
2376
+
2377
+ - // groups that are updated per object always require an update so no further checks are needed
2378
+ + // groups that are updated per object always require an update so no further checks are needed.
2379
+ + // A settled static object keeps the values it uploaded the last time it was refreshed.
2380
+
2381
+ - if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
2382
+ + if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true;
2383
+
2384
+ // check for update
2385
+
2386
+ @@ -921,6 +923,21 @@ class NodeManager extends DataMap {
2387
+
2388
+ }
2389
+
2390
+ + /**
2391
+ + * Returns `true` if the per-object updates of the given render object can be skipped
2392
+ + * because the object is static and nothing it depends on changed since its last refresh.
2393
+ + *
2394
+ + * @param {RenderObject} renderObject - The render object.
2395
+ + * @return {boolean} Whether the render object is settled or not.
2396
+ + */
2397
+ + isSettled( renderObject ) {
2398
+ +
2399
+ + const monitor = renderObject.getMonitor();
2400
+ +
2401
+ + return monitor !== undefined ? monitor.isSettled( renderObject ) : false;
2402
+ +
2403
+ + }
2404
+ +
2405
+ /**
2406
+ * Triggers the call of `updateBefore()` methods
2407
+ * for all nodes of the given render object.
2408
+ @@ -930,9 +947,14 @@ class NodeManager extends DataMap {
2409
+ updateBefore( renderObject ) {
2410
+
2411
+ const nodeBuilder = renderObject.getNodeBuilderState();
2412
+ + const settled = this.isSettled( renderObject );
2413
+
2414
+ for ( const node of nodeBuilder.updateBeforeNodes ) {
2415
+
2416
+ + // a settled static object has nothing to update per object
2417
+ +
2418
+ + if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue;
2419
+ +
2420
+ // update frame state for each node
2421
+
2422
+ this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
2423
+ @@ -950,10 +972,11 @@ class NodeManager extends DataMap {
2424
+ updateAfter( renderObject ) {
2425
+
2426
+ const nodeBuilder = renderObject.getNodeBuilderState();
2427
+ + const settled = this.isSettled( renderObject );
2428
+
2429
+ for ( const node of nodeBuilder.updateAfterNodes ) {
2430
+
2431
+ - // update frame state for each node
2432
+ + if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue;
2433
+
2434
+ this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
2435
+
2436
+ @@ -990,9 +1013,14 @@ class NodeManager extends DataMap {
2437
+
2438
+ const nodeFrame = this.getNodeFrameForRender( renderObject );
2439
+ const nodeBuilder = renderObject.getNodeBuilderState();
2440
+ + const settled = this.isSettled( renderObject );
2441
+
2442
+ for ( const node of nodeBuilder.updateNodes ) {
2443
+
2444
+ + // a settled static object keeps the per-object values it uploaded last time
2445
+ +
2446
+ + if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue;
2447
+ +
2448
+ nodeFrame.updateNode( node );
2449
+
2450
+ }
571
2451
  diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js
572
- index 06e203693ed9b168a2bddf773cd8ce8dc23196e4..37b5e0be09967e8acbd38e570b71eb201fc9bad7 100644
2452
+ index 06e2036..69b8b67 100644
573
2453
  --- a/src/renderers/webgpu/WebGPUBackend.js
574
2454
  +++ b/src/renderers/webgpu/WebGPUBackend.js
575
2455
  @@ -102,6 +102,14 @@ class WebGPUBackend extends Backend {
@@ -598,16 +2478,70 @@ index 06e203693ed9b168a2bddf773cd8ce8dc23196e4..37b5e0be09967e8acbd38e570b71eb20
598
2478
 
599
2479
  if ( adapter === null ) {
600
2480
 
2481
+ @@ -1749,13 +1759,12 @@ class WebGPUBackend extends Backend {
2482
+ for ( let i = 0, l = bindings.length; i < l; i ++ ) {
2483
+
2484
+ const bindGroup = bindings[ i ];
2485
+ + const bindingsData = this.get( bindGroup );
2486
+
2487
+ - if ( currentBindingGroups[ i ] !== bindGroup.id ) {
2488
+ -
2489
+ - const bindingsData = this.get( bindGroup );
2490
+ + if ( currentBindingGroups[ i ] !== bindingsData.group ) {
2491
+
2492
+ passEncoderGPU.setBindGroup( i, bindingsData.group );
2493
+ - currentBindingGroups[ i ] = bindGroup.id;
2494
+ + currentBindingGroups[ i ] = bindingsData.group;
2495
+
2496
+ }
2497
+
2498
+ @@ -2054,6 +2063,7 @@ class WebGPUBackend extends Backend {
2499
+ const utils = this.utils;
2500
+
2501
+ const sampleCount = utils.getSampleCountRenderContext( renderObject.context );
2502
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
2503
+ const colorSpace = utils.getCurrentColorSpace( renderObject.context );
2504
+ const colorFormat = utils.getCurrentColorFormat( renderObject.context );
2505
+ const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
2506
+ @@ -2075,7 +2085,8 @@ class WebGPUBackend extends Backend {
2507
+ data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat ||
2508
+ data.primitiveTopology !== primitiveTopology ||
2509
+ data.frontFaceCW !== frontFaceCW ||
2510
+ - data.clippingContextCacheKey !== renderObject.clippingContextCacheKey
2511
+ + data.clippingContextCacheKey !== renderObject.clippingContextCacheKey ||
2512
+ + data.bindGroupLayoutKeys !== bindGroupLayoutKeys
2513
+ ) {
2514
+
2515
+ data.material = material; data.materialVersion = material.version;
2516
+ @@ -2095,6 +2106,7 @@ class WebGPUBackend extends Backend {
2517
+ data.primitiveTopology = primitiveTopology;
2518
+ data.frontFaceCW = frontFaceCW;
2519
+ data.clippingContextCacheKey = renderObject.clippingContextCacheKey;
2520
+ + data.bindGroupLayoutKeys = bindGroupLayoutKeys;
2521
+
2522
+ needsUpdate = true;
2523
+
2524
+ @@ -2116,6 +2128,7 @@ class WebGPUBackend extends Backend {
2525
+
2526
+ const utils = this.utils;
2527
+ const renderContext = renderObject.context;
2528
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
2529
+
2530
+ // meshes with negative scale have a different frontFace render pipeline
2531
+ // descriptor value so the following must be honored in the cache key
2532
+ @@ -2134,6 +2147,7 @@ class WebGPUBackend extends Backend {
2533
+ material.side,
2534
+ frontFaceCW,
2535
+ utils.getSampleCountRenderContext( renderContext ),
2536
+ + bindGroupLayoutKeys,
2537
+ utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ),
2538
+ utils.getPrimitiveTopology( object, material ),
2539
+ renderObject.getGeometryCacheKey(),
601
2540
  diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
602
- index be07536301cd47ef2544e4a54f6abe17407f52af..3b064a25a73fb35bcaed653794bda420bb004dec 100644
2541
+ index be07536..2acc832 100644
603
2542
  --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
604
2543
  +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
605
- @@ -736,15 +736,22 @@ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
606
- let snippet;
607
-
608
- if ( depthSnippet ) {
609
-
610
- snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
2544
+ @@ -741,7 +741,14 @@ class WGSLNodeBuilder extends NodeBuilder {
611
2545
 
612
2546
  } else {
613
2547
 
@@ -623,10 +2557,7 @@ index be07536301cd47ef2544e4a54f6abe17407f52af..3b064a25a73fb35bcaed653794bda420
623
2557
 
624
2558
  if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
625
2559
 
626
- snippet += '.x';
627
-
628
- }
629
- @@ -1323,7 +1323,8 @@ class WGSLNodeBuilder extends NodeBuilder {
2560
+ @@ -1323,7 +1330,8 @@ class WGSLNodeBuilder extends NodeBuilder {
630
2561
 
631
2562
  uniformGPU = buffer;
632
2563
 
@@ -636,7 +2567,7 @@ index be07536301cd47ef2544e4a54f6abe17407f52af..3b064a25a73fb35bcaed653794bda420
636
2567
 
637
2568
  } else {
638
2569
 
639
- @@ -2066,9 +2067,12 @@ ${ flowData.code }
2570
+ @@ -2066,9 +2074,12 @@ ${ flowData.code }
640
2571
  const textureNode = uniform.node;
641
2572
  const texture = textureNode.value;
642
2573
 
@@ -652,31 +2583,63 @@ index be07536301cd47ef2544e4a54f6abe17407f52af..3b064a25a73fb35bcaed653794bda420
652
2583
 
653
2584
  if ( needsSampler ) {
654
2585
 
655
- diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js
656
- index f707acc24f5711fdae065ce920b08435b0869c1b..4c679d417f79af7994f296c005fcfaf3416c3f7a 100644
657
- --- a/build/three.webgpu.nodes.js
658
- +++ b/build/three.webgpu.nodes.js
659
- @@ -78999,15 +78999,22 @@ class WGSLNodeBuilder extends NodeBuilder {
660
- let snippet;
2586
+ diff --git a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
2587
+ index f683d74..83bbe18 100644
2588
+ --- a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
2589
+ +++ b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
2590
+ @@ -361,9 +361,15 @@ class WebGPUAttributeUtils {
2591
+ destroyAttribute( attribute ) {
2592
+
2593
+ const backend = this.backend;
2594
+ - const data = backend.get( this._getBufferAttribute( attribute ) );
2595
+ + const bufferAttribute = this._getBufferAttribute( attribute );
2596
+ + const data = backend.get( bufferAttribute );
2597
+ +
2598
+ + // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes
2599
+ + // share it: destroy it once and forget it there too, as the WebGL backend does, or the next
2600
+ + // createAttribute() finds the destroyed buffer and submits it (threenative patch).
2601
+ + if ( data.buffer !== undefined ) data.buffer.destroy();
661
2602
 
662
- if ( depthSnippet ) {
2603
+ - data.buffer.destroy();
2604
+ + if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute );
663
2605
 
664
- snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
2606
+ backend.delete( attribute );
665
2607
 
666
- } else {
2608
+ diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js
2609
+ index da4bd47..a5da7ca 100644
2610
+ --- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js
2611
+ +++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js
2612
+ @@ -97,18 +97,16 @@ class WebGPUBindingUtils {
667
2613
 
668
- - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
669
- + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
670
- + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
671
- + // operands (including the array form above) stay u32.
672
- + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
673
- + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
674
- + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
675
- + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
676
- + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
2614
+ const bindingsData = backend.get( bindGroup );
677
2615
 
678
- if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
2616
+ - // check if the the bind group already has a layout
2617
+ + const entries = this._createLayoutEntries( bindGroup );
2618
+ + const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
679
2619
 
680
- snippet += '.x';
2620
+ if ( bindingsData.layout ) {
681
2621
 
682
- }
2622
+ - return bindingsData.layout.layoutGPU;
2623
+ -
2624
+ - }
2625
+ + if ( bindingsData.layoutKey === bindGroupLayoutKey ) return bindingsData.layout.layoutGPU;
2626
+
2627
+ - // if not, assing one
2628
+ + this.deleteBindGroupData( bindGroup );
2629
+
2630
+ - const entries = this._createLayoutEntries( bindGroup );
2631
+ - const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
2632
+ + }
2633
+
2634
+ // try to find an existing layout in the cache
2635
+
2636
+ @@ -630,6 +628,9 @@ class WebGPUBindingUtils {
2637
+
2638
+ }
2639
+
2640
+ + bindingsData.groups = [];
2641
+ + bindingsData.versions = [];
2642
+ +
2643
+ }
2644
+
2645
+ /**