# How Splat State Tracking Works in Supersplat: CPU, GPU, and History Layers

> Discover how Supersplat efficiently tracks splat state using CPU uint8 arrays, GPU R8 textures, and edit history for seamless undo/redo functionality in your 3D applications.

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

---

**Supersplat manages splat state tracking through a three-layer architecture combining a CPU-side Uint8Array with bit flags, a GPU-side R8 texture for shader access, and edit-history operations for undo/redo support.**

The open-source Supersplat application (playcanvas/supersplat) handles interactive editing of Gaussian splats by maintaining synchronized state across the CPU-GPU boundary. Understanding how splat state tracking is implemented reveals why the editor can selectively transform, lock, or delete individual splats in real-time while preserving full undo capabilities.

## Understanding the Three-Layer State Architecture

Supersplat implements splat state tracking across three tightly-coupled layers:

- **CPU state array**: A `Uint8Array` where each byte holds three bit-flags (`selected = 1`, `locked = 2`, `deleted = 4`)
- **GPU state texture**: A 1-pixel-wide R8 texture that mirrors the CPU array each frame
- **Edit-history operations**: Transform operations that store old/new state for undo/redo functionality

This architecture ensures that UI interactions, shader rendering, and history management all reference the same authoritative state.

## CPU State Storage: The Uint8Array Bit-Flags

The authoritative source for splat state lives in a CPU-side typed array defined by the `State` enum in [`src/splat-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat-state.ts):

```ts
// src/splat-state.ts
enum State {
    selected = 1,
    locked   = 2,
    deleted  = 4
}
export { State };

```

When a `Splat` instance initializes, it ensures the vertex element contains a `state` property backed by this array. In [`src/splat.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat.ts), the constructor adds the storage channel if missing:

```ts
// src/splat.ts – constructor (excerpt)
if (!this.splatData.getProp('state')) {
    this.splatData.getElement('vertex').properties.push({
        type: 'uchar',
        name: 'state',
        storage: new Uint8Array(this.splatData.numSplats),
        byteSize: 1
    });
}

```

The resulting `Uint8Array` contains one byte per splat, with bits 0, 1, and 2 representing selected, locked, and deleted states respectively.

## GPU Synchronization: The State Texture Pipeline

To make state accessible to shaders, [`src/splat.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat.ts) allocates an R8 texture in the constructor:

```ts
this.stateTexture = createTexture('splatState', PIXELFORMAT_R8);

```

The `updateState()` method synchronizes CPU data to GPU memory and recomputes visibility statistics:

```ts
// src/splat.ts – updateState()
const state = this.splatData.getProp('state') as Uint8Array;
const data  = this.stateTexture.lock();
data.set(state);
this.stateTexture.unlock();

// count selected/locked/deleted
for (let i = 0; i < state.length; ++i) {
    const s = state[i];
    if (s & State.deleted) { numDeleted++; }
    else if (s & State.locked) { numLocked++; }
    else if (s & State.selected) { numSelected++; }
}

```

After unlocking, the texture is bound to the material via `material.setParameter('splatState', this.stateTexture)`, making it available to vertex and fragment shaders.

## Shader Implementation: Reading State in the Vertex Shader

The vertex shader in [`src/shaders/splat-shader.ts`](https://github.com/playcanvas/supersplat/blob/main/src/shaders/splat-shader.ts) samples the state texture to determine per-splat visibility and selection status:

```glsl
// src/shaders/splat-shader.ts – vertex shader (excerpt)
uint vertexState = uint(texelFetch(splatState, splat.uv, 0).r * 255.0 + 0.5) & 7u;

// discard deleted splats
#if !PICK_PASS
if ((vertexState & 4u) != 0u) {
    gl_Position = discardVec;
    return;
}
#endif

// pass selected / locked flags to fragment shader
texCoord_flags = vec4(
    corner.uv,
    (vertexState & 1u) != 0u ? 1.0 : 0.0,   // selected
    (vertexState & 2u) != 0u ? 1.0 : 0.0    // locked
);

```

The fragment shader uses these flags to apply visual tinting, rendering selected splats with highlight colors and locked splats with desaturated or warning colors.

## UI and Transform Integration

### Filtering in the Data Panel

The UI layer in [`src/ui/data-panel.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/data-panel.ts) queries the state array to determine which splats are editable or visible. The data panel uses filter functions that check against the `State` enum values:

```ts
// src/ui/data-panel.ts – example of filtering
valueFunc: i => ((state[i] === 0 || state[i] === State.selected) ? func(i) : undefined),
selectedFunc: i => state[i] === State.selected,

```

When users toggle selection through the interface, the corresponding byte in the `Uint8Array` updates immediately, triggering `updateState()` to refresh the GPU texture.

### Transform Handler Selection Logic

The `SplatsTransformHandler` in [`src/splats-transform-handler.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splats-transform-handler.ts) respects state flags when allocating transform operations. Only splats marked as `State.selected` receive palette entries for interactive manipulation:

```ts
// src/splats-transform-handler.ts – start()
for (let i = 0; i < state.length; ++i) {
    if (state[i] === State.selected) {
        const oldIdx = indices[i];
        // allocate a new palette index …
        indices[i] = newIdx;
    }
}

```

This ensures that locked or deleted splats remain immutable during bulk transform operations, while selected splats participate in moves, rotations, and scaling.

## Undo/Redo and Edit History

State changes integrate with the edit history system through operation objects. When transformations complete, `SplatsTransformOp` and `PlacePivotOp` instances capture the previous and current state values. These operations bundle into a `MultiOp` and fire via `events.fire('edit.add', …)`, preserving the entire `state` array, texture references, and transform palette.

On undo, the system restores the previous `Uint8Array` values and calls `updateState()` to resynchronize the GPU texture, reverting both internal logic and visual representation simultaneously.

## Practical Code Examples

### Toggling a Splat's Selection

To programmatically toggle the selected state of a specific splat:

```ts
import { State } from './splat-state';
import { Splat } from './splat';

// Assume `splat` is an existing Splat instance
function toggleSelection(splatId: number) {
    const state = splat.splatData.getProp('state') as Uint8Array;
    // Flip the selected flag (bit 1)
    state[splatId] ^= State.selected;
    // Push the change to GPU and refresh UI
    splat.updateState(State.selected);
}

```

### Deleting Selected Splats

To mark all currently selected splats as deleted:

```ts
function deleteSelection(splat: Splat) {
    const state = splat.splatData.getProp('state') as Uint8Array;
    for (let i = 0; i < state.length; ++i) {
        if (state[i] & State.selected) {
            state[i] |= State.deleted;   // mark as deleted
        }
    }
    // Re-compute sorting & bounds after deletion
    splat.updateState(State.deleted);
}

```

## Summary

- **Bit-packed storage**: Supersplat uses a `Uint8Array` with bit flags (`selected=1`, `locked=2`, `deleted=4`) defined in [`src/splat-state.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat-state.ts) for memory-efficient CPU-side state tracking.
- **GPU mirroring**: The `updateState()` method in [`src/splat.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat.ts) synchronizes the CPU array to an R8 texture, enabling shader access to selection and visibility states.
- **Shader-driven rendering**: The vertex shader in [`src/shaders/splat-shader.ts`](https://github.com/playcanvas/supersplat/blob/main/src/shaders/splat-shader.ts) discards deleted splats and passes selection flags to the fragment shader for visual feedback.
- **Selective transformation**: [`src/splats-transform-handler.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splats-transform-handler.ts) filters operations by checking `state[i] === State.selected`, ensuring only appropriate splats receive transform operations.
- **History integration**: Edit operations preserve state arrays in `SplatsTransformOp` instances, making undo/redo operations atomic and reversible across the entire application state.

## Frequently Asked Questions

### What data structure does Supersplat use for splat state tracking?

Supersplat stores splat state in a `Uint8Array` where each byte represents oneGaussian splat. The byte uses bit flags to track three states simultaneously: bit 0 for selected (value 1), bit 1 for locked (value 2), and bit 2 for deleted (value 4). This compact representation allows the application to store state for millions of splats with minimal memory overhead.

### How does the GPU access splat state information?

The GPU accesses state through a dedicated R8 texture created in [`src/splat.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat.ts). The `updateState()` method copies the CPU-side `Uint8Array` into this texture each frame, which shaders then sample using `texelFetch()`. The vertex shader converts the normalized float value back to integer flags by multiplying by 255.0 and applying a bitmask of 7 (binary 111) to isolate the three relevant bits.

### Can locked splats be transformed or deleted?

No. The transform handler in [`src/splats-transform-handler.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splats-transform-handler.ts) explicitly checks for `state[i] === State.selected` before allocating transform palette entries, preventing locked splats from participating in move, rotate, or scale operations. Similarly, UI panels filter out locked splats from selection operations. However, locked splats can still be rendered and viewed unless they are also marked as deleted.

### How does Supersplat handle undo/redo for state changes?

State changes are preserved through operation objects like `SplatsTransformOp` and `PlacePivotOp` that store both the old and new state values. When a user completes an action, these operations bundle into a `MultiOp` and push onto the edit history stack. Undo operations restore the previous `Uint8Array` values and trigger `updateState()` to resynchronize the GPU texture, ensuring visual and logical consistency.