@@ -5,6 +5,7 @@ import { BoundingBox } from '../../core/shape/bounding-box.js';
55import { PIXELFORMAT_RGBA32U } from '../../platform/graphics/constants.js' ;
66import { Texture } from '../../platform/graphics/texture.js' ;
77import { TextureUtils } from '../../platform/graphics/texture-utils.js' ;
8+ import { WORKBUFFER_UPDATE_ALWAYS } from '../constants.js' ;
89
910/**
1011 * @import { GraphicsDevice } from "../../platform/graphics/graphics-device.js";
@@ -29,7 +30,10 @@ const _fullRangeInterval = [0, 0];
2930/**
3031 * Represents a snapshot of gsplat state for rendering. This class captures all necessary data
3132 * at a point in time and should not hold references back to the source placement. All required
32- * data should be copied or referenced, allowing placement to be modified without affecting the info.
33+ * data should be copied or referenced, allowing placement to be modified without affecting the
34+ * info. The only exception is shader configuration and dirty state, which are deliberately read
35+ * live through narrow accessor closures (see the fields documented as 'retrieved live'), so that
36+ * changes raised after the snapshot was taken are still observed.
3337 *
3438 * @ignore
3539 */
@@ -183,24 +187,45 @@ class GSplatInfo {
183187 getInstanceStreams = null ;
184188
185189 /**
186- * Callback to consume render dirty flag from the source placement.
190+ * Function to get the placement that owns the dirty state - the parent placement for child
191+ * placements (octree files, environment), the placement itself otherwise. Retrieved live (not
192+ * snapshotted) so dirty requests raised after this info was created are still observed.
187193 *
188- * @type {Function |null }
194+ * @type {(() => GSplatPlacement) |null }
189195 * @private
190196 */
191- _consumeRenderDirty = null ;
197+ _getDirtySource = null ;
198+
199+ /**
200+ * The last {@link GSplatPlacement#dirtyVersion} this consumer re-copied at. Tracked here (per
201+ * camera) rather than on the shared placement, so a single dirty request re-copies every
202+ * consumer of the placement exactly once.
203+ *
204+ * @type {number }
205+ * @private
206+ */
207+ _lastDirtyVersion = - 1 ;
208+
209+ /**
210+ * The last resource format extra-streams version this consumer re-copied at.
211+ *
212+ * @type {number }
213+ * @private
214+ */
215+ _lastFormatVersion = - 1 ;
192216
193217 /**
194218 * Create a new GSplatInfo.
195219 *
196220 * @param {GraphicsDevice } device - The graphics device.
197221 * @param {GSplatResourceBase } resource - The splat resource.
198- * @param {GSplatPlacement } placement - The placement of the splat.
199- * @param {Function|null } [consumeRenderDirty] - Callback to consume render dirty flag.
222+ * @param {GSplatPlacement } placement - The placement of the splat. Do not store it - snapshot
223+ * its data instead, as the placement is mutated for future world states while this info still
224+ * renders. Only shader configuration and dirty state may be read live, via accessor closures.
200225 * @param {GSplatOctreeNode[]|null } [octreeNodes] - Octree nodes for bounds lookup.
201226 * @param {NodeInfo[]|null } [nodeInfos] - Per-node info array from octree instance.
202227 */
203- constructor ( device , resource , placement , consumeRenderDirty = null , octreeNodes = null , nodeInfos = null ) {
228+ constructor ( device , resource , placement , octreeNodes = null , nodeInfos = null ) {
204229 Debug . assert ( resource ) ;
205230 Debug . assert ( placement ) ;
206231
@@ -221,7 +246,7 @@ class GSplatInfo {
221246 this . parameters = placement . parameters ;
222247 this . getWorkBufferModifier = ( ) => placement . workBufferModifier ;
223248 this . getInstanceStreams = ( ) => placement . streams ;
224- this . _consumeRenderDirty = consumeRenderDirty ;
249+ this . _getDirtySource = ( ) => placement . parentPlacement ?? placement ;
225250 this . octreeNodes = octreeNodes ;
226251 this . nodeInfos = nodeInfos ;
227252
@@ -439,9 +464,29 @@ class GSplatInfo {
439464 this . previousWorldTransform . copy ( worldMatrix ) ;
440465 }
441466
442- const renderDirty = this . _consumeRenderDirty ? this . _consumeRenderDirty ( ) : false ;
467+ let dirty = worldMatrixChanged ;
468+
469+ // One-shot re-copy requests (parameter or modifier changes, or an explicit update) and
470+ // the continuous update mode come from the placement - or its parent for child placements
471+ // (octree files, environment). The last-seen version is tracked here (per consumer), so a
472+ // single request re-copies every consumer of a shared placement exactly once.
473+ const source = this . _getDirtySource ( ) ;
474+ if ( this . _lastDirtyVersion !== source . dirtyVersion ) {
475+ this . _lastDirtyVersion = source . dirtyVersion ;
476+ dirty = true ;
477+ }
478+ if ( source . workBufferUpdate === WORKBUFFER_UPDATE_ALWAYS ) {
479+ dirty = true ;
480+ }
481+
482+ // Auto-detect resource format (extra streams) changes, also tracked per consumer.
483+ const format = this . resource ?. format ;
484+ if ( format && this . _lastFormatVersion !== format . extraStreamsVersion ) {
485+ this . _lastFormatVersion = format . extraStreamsVersion ;
486+ dirty = true ;
487+ }
443488
444- return worldMatrixChanged || renderDirty ;
489+ return dirty ;
445490 }
446491
447492 /**
0 commit comments