Skip to content

Commit d22313f

Browse files
authored
Merge branch 'main' into xr-navigation-arc-teleport
2 parents 83a0253 + f7c2b7e commit d22313f

6 files changed

Lines changed: 168 additions & 46 deletions

File tree

src/framework/components/gsplat/component.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ class GSplatComponent extends Component {
923923
setParameter(name, data) {
924924
const scopeId = this.system.app.graphicsDevice.scope.resolve(name);
925925
this._parameters.set(name, { scopeId, data });
926-
if (this._placement) this._placement.renderDirty = true;
926+
if (this._placement) this._placement.markDirty();
927927
}
928928

929929
/**
@@ -943,7 +943,7 @@ class GSplatComponent extends Component {
943943
*/
944944
deleteParameter(name) {
945945
this._parameters.delete(name);
946-
if (this._placement) this._placement.renderDirty = true;
946+
if (this._placement) this._placement.markDirty();
947947
}
948948

949949
/**

src/scene/gsplat-unified/gsplat-info.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BoundingBox } from '../../core/shape/bounding-box.js';
55
import { PIXELFORMAT_RGBA32U } from '../../platform/graphics/constants.js';
66
import { Texture } from '../../platform/graphics/texture.js';
77
import { 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
/**

src/scene/gsplat-unified/gsplat-placement.js

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Debug } from '../../core/debug.js';
22
import { GSplatStreams } from '../gsplat/gsplat-streams.js';
3-
import { WORKBUFFER_UPDATE_AUTO, WORKBUFFER_UPDATE_ALWAYS, WORKBUFFER_UPDATE_ONCE } from '../constants.js';
3+
import { WORKBUFFER_UPDATE_AUTO, WORKBUFFER_UPDATE_ONCE } from '../constants.js';
44
import { GsplatAllocId } from './gsplat-alloc-id.js';
55

66
/**
@@ -175,23 +175,25 @@ class GSplatPlacement {
175175
lodDirty = false;
176176

177177
/**
178-
* Flag indicating the splat needs to be re-rendered to work buffer.
179-
*/
180-
renderDirty = false;
181-
182-
/**
183-
* Work buffer update mode.
178+
* Monotonically increasing counter, bumped whenever this placement's splats need to be
179+
* re-copied to the work buffer (parameter or modifier changes, or an explicit one-shot update
180+
* request). Each consumer (a per-camera {@link GSplatInfo}) remembers the value it last
181+
* copied at, so a single request re-copies every consumer of a shared placement exactly once,
182+
* and child placements (octree files, environment) fan out from their parent's counter.
184183
*
185184
* @type {number}
185+
* @ignore
186186
*/
187-
workBufferUpdate = WORKBUFFER_UPDATE_AUTO;
187+
dirtyVersion = 0;
188188

189189
/**
190-
* Last seen format version for auto-detecting format changes.
190+
* Work buffer update mode (see WORKBUFFER_UPDATE_*). WORKBUFFER_UPDATE_ONCE is not stored as a
191+
* mode - it is converted into a single {@link dirtyVersion} bump.
191192
*
193+
* @type {number}
192194
* @private
193195
*/
194-
_lastFormatVersion = -1;
196+
_workBufferUpdate = WORKBUFFER_UPDATE_AUTO;
195197

196198
/**
197199
* Custom work buffer modifier code for this placement (object with code and pre-computed hash).
@@ -247,7 +249,7 @@ class GSplatPlacement {
247249
*/
248250
set workBufferModifier(value) {
249251
this._workBufferModifier = value;
250-
this.renderDirty = true;
252+
this.dirtyVersion++;
251253
}
252254

253255
/**
@@ -261,31 +263,35 @@ class GSplatPlacement {
261263
}
262264

263265
/**
264-
* Returns and clears the render dirty flag. Also checks for format version changes
265-
* and handles render mode.
266+
* Sets the work buffer update mode (see WORKBUFFER_UPDATE_*). WORKBUFFER_UPDATE_ONCE is turned
267+
* into a single {@link dirtyVersion} bump so every consumer re-copies once, rather than being
268+
* stored as a persistent mode.
266269
*
267-
* @returns {boolean} True if the splat needed re-rendering.
268-
*/
269-
consumeRenderDirty() {
270-
// Auto-detect format version changes
271-
// Cast to access format property (GSplatOctreeResource doesn't have format)
272-
const format = /** @type {GSplatResourceBase} */ (this.resource)?.format;
273-
if (format && this._lastFormatVersion !== format.extraStreamsVersion) {
274-
this._lastFormatVersion = format.extraStreamsVersion;
275-
this.renderDirty = true;
270+
* @type {number}
271+
*/
272+
set workBufferUpdate(value) {
273+
if (value === WORKBUFFER_UPDATE_ONCE) {
274+
this.dirtyVersion++;
275+
} else {
276+
this._workBufferUpdate = value;
276277
}
278+
}
277279

278-
// Handle work buffer update mode
279-
if (this.workBufferUpdate === WORKBUFFER_UPDATE_ALWAYS) {
280-
this.renderDirty = true;
281-
} else if (this.workBufferUpdate === WORKBUFFER_UPDATE_ONCE) {
282-
this.renderDirty = true;
283-
this.workBufferUpdate = WORKBUFFER_UPDATE_AUTO; // Auto-reset
284-
}
280+
/**
281+
* Gets the work buffer update mode.
282+
*
283+
* @type {number}
284+
*/
285+
get workBufferUpdate() {
286+
return this._workBufferUpdate;
287+
}
285288

286-
const dirty = this.renderDirty;
287-
this.renderDirty = false;
288-
return dirty;
289+
/**
290+
* Marks the placement as needing a one-time re-copy to the work buffer by all of its
291+
* consumers.
292+
*/
293+
markDirty() {
294+
this.dirtyVersion++;
289295
}
290296

291297
/**

src/scene/gsplat-unified/gsplat-world.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ class GSplatWorld {
613613
continue;
614614
}
615615
p.ensureInstanceStreams(this._device);
616-
const splatInfo = new GSplatInfo(this._device, p.resource, p, p.consumeRenderDirty.bind(p));
616+
const splatInfo = new GSplatInfo(this._device, p.resource, p);
617617
splats.push(splatInfo);
618618
}
619619

@@ -629,7 +629,7 @@ class GSplatWorld {
629629
p.ensureInstanceStreams(this._device);
630630
const octreeNodes = p.intervals.size > 0 ? inst.octree.nodes : null;
631631
const nodeInfos = octreeNodes ? inst.nodeInfos : null;
632-
const splatInfo = new GSplatInfo(this._device, p.resource, p, p.consumeRenderDirty.bind(p), octreeNodes, nodeInfos);
632+
const splatInfo = new GSplatInfo(this._device, p.resource, p, octreeNodes, nodeInfos);
633633
splats.push(splatInfo);
634634
}
635635
});

test/framework/components/gsplat/component.test.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from 'chai';
22

33
import { Entity } from '../../../../src/framework/entity.js';
4+
import { GSplatPlacement } from '../../../../src/scene/gsplat-unified/gsplat-placement.js';
45
import { createApp } from '../../../app.mjs';
56
import { jsdomSetup, jsdomTeardown } from '../../../jsdom.mjs';
67

@@ -70,6 +71,28 @@ describe('GSplatComponent', function () {
7071

7172
});
7273

74+
describe('#parameters', function () {
75+
76+
it('marks the placement dirty on setParameter and deleteParameter', function () {
77+
const e = new Entity();
78+
e.addComponent('gsplat');
79+
80+
const placement = new GSplatPlacement(null, e);
81+
e.gsplat._placement = placement;
82+
83+
const v0 = placement.dirtyVersion;
84+
e.gsplat.setParameter('uTest', 1);
85+
expect(e.gsplat.getParameter('uTest')).to.equal(1);
86+
expect(placement.dirtyVersion).to.be.above(v0);
87+
88+
const v1 = placement.dirtyVersion;
89+
e.gsplat.deleteParameter('uTest');
90+
expect(e.gsplat.getParameter('uTest')).to.be.undefined;
91+
expect(placement.dirtyVersion).to.be.above(v1);
92+
});
93+
94+
});
95+
7396
describe('#cloneComponent', function () {
7497

7598
it('copies LOD properties to the clone', function () {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect } from 'chai';
2+
3+
import { WORKBUFFER_UPDATE_ALWAYS, WORKBUFFER_UPDATE_AUTO, WORKBUFFER_UPDATE_ONCE } from '../../../src/scene/constants.js';
4+
import { GraphNode } from '../../../src/scene/graph-node.js';
5+
import { GSplatPlacement } from '../../../src/scene/gsplat-unified/gsplat-placement.js';
6+
7+
describe('GSplatPlacement', function () {
8+
9+
describe('dirty state', function () {
10+
11+
it('starts with dirtyVersion 0 and AUTO update mode', function () {
12+
const placement = new GSplatPlacement(null, new GraphNode());
13+
expect(placement.dirtyVersion).to.equal(0);
14+
expect(placement.workBufferUpdate).to.equal(WORKBUFFER_UPDATE_AUTO);
15+
});
16+
17+
it('markDirty increments dirtyVersion', function () {
18+
const placement = new GSplatPlacement(null, new GraphNode());
19+
placement.markDirty();
20+
expect(placement.dirtyVersion).to.equal(1);
21+
placement.markDirty();
22+
expect(placement.dirtyVersion).to.equal(2);
23+
});
24+
25+
it('WORKBUFFER_UPDATE_ONCE bumps dirtyVersion without changing the stored mode', function () {
26+
const placement = new GSplatPlacement(null, new GraphNode());
27+
placement.workBufferUpdate = WORKBUFFER_UPDATE_ONCE;
28+
expect(placement.dirtyVersion).to.equal(1);
29+
expect(placement.workBufferUpdate).to.equal(WORKBUFFER_UPDATE_AUTO);
30+
});
31+
32+
it('WORKBUFFER_UPDATE_ALWAYS sets the mode without bumping dirtyVersion', function () {
33+
const placement = new GSplatPlacement(null, new GraphNode());
34+
placement.workBufferUpdate = WORKBUFFER_UPDATE_ALWAYS;
35+
expect(placement.dirtyVersion).to.equal(0);
36+
expect(placement.workBufferUpdate).to.equal(WORKBUFFER_UPDATE_ALWAYS);
37+
});
38+
39+
it('setting workBufferModifier bumps dirtyVersion', function () {
40+
const placement = new GSplatPlacement(null, new GraphNode());
41+
placement.workBufferModifier = { code: 'void modifySplatCenter() {}', hash: 1 };
42+
expect(placement.dirtyVersion).to.equal(1);
43+
placement.workBufferModifier = null;
44+
expect(placement.dirtyVersion).to.equal(2);
45+
});
46+
47+
});
48+
});

0 commit comments

Comments
 (0)