# How Scene State Management Works in Supersplat: A Deep Dive into the Diffing Architecture

> Discover how Supersplat manages scene state with efficient diffing and snapshots. Learn about selective rendering and UI updates in this deep dive.

- Repository: [PlayCanvas/supersplat](https://github.com/playcanvas/supersplat)
- Tags: deep-dive
- Published: 2026-05-10

---

**Supersplat implements scene state management through a lightweight double-buffered snapshot system that diffs serialized element states each frame to trigger selective rendering and UI updates.**

The Supersplat project by PlayCanvas uses an innovative approach to **scene state management** that minimizes unnecessary renders by tracking exactly what changed between frames. Instead of comparing complex object graphs, the system captures flattened snapshots of scene elements in [`src/scene-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene-state.ts) and runs a diff algorithm to detect additions, removals, moves, and value changes. This architecture integrates tightly with the main loop in [`src/scene.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene.ts), providing deterministic change detection for splat rendering and UI synchronization.

## The State Snapshot Architecture

The **`SceneState`** class in [`src/scene-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene-state.ts) serves as the container for frame snapshots. It organizes data by `ElementType` using a structure that maps elements to indices while maintaining flat arrays of serialized values, avoiding the overhead of deep object comparisons.

### Per-Type Storage Structure

For each element type (splat, camera, etc.), the state maintains dedicated storage structures. The constructor populates these for every type listed in `ElementTypeList` according to the implementation in lines 19-26:

- **`elements: Map<Element, number>`** – Maps an element instance to its index in the value arrays.
- **`valueStart: number[]`** – Tracks the start offset of each element's serialized values.
- **`valueCount: number[]`** – Records the number of serialized values for each element.
- **`values: any[]`** – A flat list containing all serialized values for the type.

This flat structure ensures that comparisons happen on primitive arrays rather than complex objects, making the diff operation significantly faster.

### Serializing Elements with pack()

When a frame begins processing, the system serializes each element using the `pack()` method. This method utilizes a **`Serializer`** helper that pushes each value into the `activeValues` array (set to the current type's `values` array) as shown in lines 39-48:

```typescript
// Inside SceneState.pack(element)
state.pack(element) // Serializes via Serializer
// Records start offset in valueStart
// Records length in valueCount  
// Updates element-to-index map

```

The serialization captures only the essential primitive data needed to detect changes, keeping memory usage minimal.

## Change Detection Algorithm

The core **change detection** logic resides in the `compare()` method, which performs a structural diff between the current frame's state and the previous frame's state.

### Detecting Added, Removed, Moved, and Changed Elements

The `compare(previousState)` implementation (lines 86-146 in [`src/scene-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene-state.ts)) walks every `ElementType` and produces a comprehensive diff:

1. **Builds an intersection set** (`common`) of elements present in both the current and previous frames.
2. **Detects added elements** by identifying entries in the current map missing from the intersection.
3. **Detects removed elements** by identifying entries in the previous map missing from the intersection.
4. **Detects moved elements** when an element's index changes between frames, indicating reordering.
5. **Detects changed elements** by comparing the serialized value arrays (count or individual values) for elements present in both frames.

The method returns an object containing four arrays (`added`, `removed`, `moved`, `changed`) of `ElementType`, allowing the system to react precisely to specific categories of changes.

## Integration with the Render Loop

The **`Scene`** class in [`src/scene.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene.ts) orchestrates the state capture and comparison each frame, driving both rendering decisions and event dispatch.

### Double-Buffering Strategy

The system uses a double-buffering approach alternating between two `SceneState` instances based on the frame counter. Inside `onUpdate` (lines 24-31), the logic follows this pattern:

```typescript
const i = this.app.frame % 2;                     // double-buffered states
const state = this.sceneState[i];
state.reset();
this.forEachElement(e => state.pack(e));          // record current frame
const result = state.compare(this.sceneState[1 - i]); // diff with previous
const all = new Set([...result.added, ...result.removed,
                     ...result.moved, ...result.changed]);

```

This ensures that the system always has access to both the current and previous frame states without allocating new objects each frame.

### Render Triggering and Events

The diff results directly control the render cycle. As implemented in lines 38-40:

```typescript
if (this.lockedRenderMode) { /* ... */ }
else if (!this.app.renderNextFrame) {
    this.app.renderNextFrame = this.forceRender || all.size > 0;
}

```

If any element type changed (`all.size > 0`), the engine schedules a render for the next frame. Additionally, the scene fires specific `updated:<type>` events for each changed type (lines 44-48), allowing UI components and gizmos to react only to relevant changes rather than processing every frame.

## Practical Implementation Example

You can manually invoke the state management system to debug or monitor changes. This example demonstrates capturing state and logging the diff results:

```typescript
import { Scene } from './scene';
import { ElementType } from './element';

// Assume `scene` is an instantiated Scene
function logChanges() {
    // Force a state capture for the current frame
    const i = scene.app.frame % 2;
    const cur = scene.sceneState[i];
    cur.reset();
    scene.elements.forEach(e => cur.pack(e));

    // Compare with the previous frame
    const prev = scene.sceneState[1 - i];
    const diff = cur.compare(prev);

    console.log('Added:', diff.added.map(t => ElementType[t]));
    console.log('Removed:', diff.removed.map(t => ElementType[t]));
    console.log('Moved:', diff.moved.map(t => ElementType[t]));
    console.log('Changed:', diff.changed.map(t => ElementType[t]));
}

```

Running `logChanges()` after a frame outputs which element types were added, removed, moved, or changed, providing visibility into the **scene state management** system's operation.

## Summary

- **Supersplat** uses double-buffered `SceneState` snapshots in [`src/scene-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene-state.ts) to track changes incrementally.
- The **`compare()`** method detects added, removed, moved, and changed elements by type using flat array comparisons.
- **Flat serialization** avoids expensive deep object comparisons while maintaining deterministic ordering for move detection.
- Integration with **`Scene.onUpdate`** drives selective rendering and dispatches type-specific update events to minimize UI recomputation.

## Frequently Asked Questions

### How does Supersplat detect which scene elements changed between frames?

The system serializes each element into flat primitive arrays via the `SceneState.pack()` method, then runs `compare()` against the previous frame's snapshot. It detects changes by comparing value counts and individual serialized values for elements present in both frames, as implemented in [`src/scene-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/scene-state.ts) lines 86-146.

### What is the performance benefit of the SceneState diffing approach?

By storing only minimal per-element data (flat arrays of primitive values) and avoiding deep object traversal, the system performs change detection with minimal memory allocation. The deterministic ordering also allows the system to detect moves without expensive tree diffing algorithms.

### Where does the serialization logic live for different element types?

The base serialization interface is defined in [`src/element.ts`](https://github.com/playcanvas/supersplat/blob/main/src/element.ts), while the actual value packing happens through the `Serializer` class in [`src/serializer.ts`](https://github.com/playcanvas/supersplat/blob/main/src/serializer.ts). The `SceneState` class coordinates serialization by setting the active values array and invoking `pack()` on each element during the frame update cycle.

### Can the scene state management system track element reordering?

Yes, the system explicitly tracks **moved** elements by comparing the index of each element between the current and previous frames. When an element's position in the `elements` Map changes relative to the prior snapshot, it is categorized as moved in the diff results, allowing the renderer to update spatial relationships without full rebuilds.