Skip to content

Performance ​

Quantum simulation is expensive: the state space grows exponentially with the number of qudits that share a state. This page covers how to keep your game running at 60 FPS.

The one rule: dispose what you are done with ​

Qudits that share a state multiply its size. Two properties join one shared state the first time they interact, and that state can grow as large as dimension ^ qudits entries. A disposed property stops counting:

  • A qudit that was alone in its state goes to an internal cache, and the next quantum() with the same number of values reuses it instead of allocating.
  • A qudit that still shared a state is destroyed, which takes it out of that state.
typescript
import { ensureLoaded, quantum } from "quantum-forge/quantum";

await ensureLoaded();

const ball = quantum([false, true]).superpose();
// ... the ball leaves the field ...
const scored = ball.measure();
ball.dispose(); // frees the qudit for the next quantum()

This is why a game that keeps spawning and removing quantum objects stays within the limit, as long as it disposes them when they leave. If you keep handles on entities managed by the engine's EntityManager, removing the entity disposes them for you. See Lifecycle.

Shipped limits ​

The published package ships two editions with different trade-offs:

EditionValues per propertyMax qudits in one shared stateMax amplitudes in one state
Qutrit (default)2 or 312100,000
Qubit220100,000

Pick the edition with useQuantumForgeBuild("qubit") before ensureLoaded(), or during npx quantum-forge-engine init (--edition qubit) if you scaffold with the engine. The Qutrit Edition gives you three-valued properties but fewer qudits. The Qubit Edition trades away qutrits for a higher qudit ceiling. See Quantum Setup: Editions.

Properties that never interacted live in separate states and don't count against each other's limits.

Performance budget ​

60 FPS guidelines, per shared state:

Qudits in one shared statePerformanceNotes
1 to 4No issuesComfortable headroom
4 to 8GoodMonitor frame times
8 to 10Watch carefullyOnly with prompt disposal
10 to 12At the limitCheck capacity before every interaction

Expensive operations ​

Ranked by cost:

  1. Tensor product: triggered the first time two properties in different shared states interact, whether through a two-property gate like iSwap() or a gate with a when predicate on the other property. Cost grows exponentially with the number of qudits in each state. This is the operation to minimize.
  2. Measurement: proportional to state vector size. Fast for small states, noticeable for large entangled groups.
  3. Gate application: fast. Scales linearly with state vector size.
  4. Probability query: similar cost to measurement, but no state change.

Creating a property with quantum() is cheap on its own. The cost arrives with its first interaction.

Optimization strategies ​

1. Dispose promptly ​

Dispose a property as soon as its object is gone. Waiting keeps its qudit in the shared state, and every gate and measurement on its partners pays for it:

typescript
// Ball exits the field: measure and dispose now, not at the end of the round
const exists = ball.exists.measure();
ball.exists.dispose();

2. Limit concurrent quantum objects ​

Design your game so that only a few objects are quantum at any time:

  • Quantum Pong: at most 4 to 6 balls quantum at once (2 or 3 pairs)
  • Quantris: the current piece plus at most 2 or 3 recently placed pieces
  • Bloch Invaders: only the targeted invader is actively quantum

3. Declare only the values you need ​

More values per property means an exponentially larger state space:

ValuesStates per propertyMemory per entangled pair
2, like quantum([false, true])264 bytes
3, like quantum(["rock", "paper", "scissors"])3144 bytes

A qubit is cheaper than a qutrit. If you only need two values and want more quantum objects, consider the Qubit Edition.

4. Keep independent systems apart ​

Properties that never interact stay in separate, small states. You don't need to do anything to keep them apart: just don't write gates or predicates that connect them. A player's abilities and the enemies' states can each stay small as long as no gate links the two.

5. Measure together ​

Measure several properties with one call instead of one at a time:

typescript
import { measure } from "quantum-forge/quantum";

const [va, vb, vc] = measure(a, b, c);

6. Batch gates through .raw ​

When you apply many gates per frame (AI sweeps, circuit replay), each gate call crosses the JS-WASM boundary, and for short gates that crossing is most of the cost. The batch APIs, executeBatch() and executeBatchTape(), send a whole sequence in one WASM call. They work on raw WASM properties, which a handle exposes as handle.raw:

typescript
import { getModule, type BatchOp } from "quantum-forge/quantum";

const m = getModule();
const ops: BatchOp[] = [
  { op: "hadamard", target: a.raw },
  { op: "cycle", target: b.raw, predicates: [a.raw.is(1)] },
];
const result = m.executeBatch(ops); // one WASM call

Batch operations bypass the handle, so observers and QuantumRecorder never see them, and a recorded session that includes them will not replay correctly. See Gates: batch gate execution for the full API.

For maximum throughput, executeBatchTape() takes a pre-encoded Float64Array. The source notes a sequence of 3000-plus operations copying to WASM in roughly 0.1 ms as a tape, against roughly 300 ms of embind marshaling for the same ops as JS objects. Treat that as an order-of-magnitude guide, not a benchmark on your hardware.

7. Throttle probability queries ​

Reading probabilities never collapses anything, so it is safe in a render loop. For large states, you still don't need to read them every frame:

typescript
import type { Quantum } from "quantum-forge/quantum";

const probCache = new Map<number, number>();
let lastProbUpdate = 0;

function existenceProbability(prop: Quantum<boolean>): number {
  const now = performance.now();
  if (now - lastProbUpdate >= 100) { // refresh at most 10 times a second
    probCache.clear();
    lastProbUpdate = now;
  }
  let p = probCache.get(prop.id);
  if (p === undefined) {
    p = prop.probability(true);
    probCache.set(prop.id, p);
  }
  return p;
}

Isolated simulations ​

For search tree exploration or replay branches, QuantumSimulation creates isolated quantum contexts on raw WASM properties, so branches never grow a shared global state:

typescript
import { getQuantumForge } from "quantum-forge/quantum";

const sim = getQuantumForge().createSimulation();
const prop = sim.createProperty(2);
// ... explore this branch ...
sim.destroy(); // releases everything at once

See Lifecycle: QuantumSimulation.

Monitoring ​

WASM heap usage ​

getWasmMemoryBytes() is exported, but in the currently shipped builds it always returns null. It probes the WASM module for a getMemoryBytes() binding that the module does not yet export. Do not build a heap overlay on it. Until it reports real numbers, use the state budget queries below.

State budget queries ​

Two methods on the handle report the size of the state it belongs to:

typescript
const sparseSize = prop.stateVectorSize();
const numQudits = prop.numActiveQudits();

debugText(`State: ${sparseSize} amplitudes, ${numQudits} qudits`);

Compare them against getMaxStateSize() and getMaxQudits(). See Lifecycle: state budget queries.

When the limit is reached ​

An interaction that would pass the qudit limit or the state size limit throws. Check capacity before the interaction rather than catching the throw: each caught throw leaks WASM memory. See Error handling for the messages and the check.

Powered by Quantum Forge