You're reading the 2.x docs. quantum-forge 3.0 is out: this page in 3.x / migration guide

Skip to content

Performance ​

Quantum simulation is computationally expensive. State space grows exponentially with the number of qudits. This page covers how to keep your game running at 60 FPS.

The One Rule: Pool Your Properties ​

Property pooling is the single most important optimization. Without pooling, every new property grows the tensor product. With pooling, measured properties are reset and reused within the existing shared state.

typescript
// After measurement, ALWAYS release to pool
const [value] = this.getModule().measure_properties([prop]);
this.deleteProperty(id);
this.releaseProperty(prop, value); // reset to |0⟩, return to pool

QuantumPropertyManager handles this automatically when you use removeProperty(id), which measures, deletes, and releases in one call.

Why Pooling Works ​

When you acquireProperty():

  1. Pool is not empty → reuses a property already in the shared state. No tensor product growth. Fast.
  2. Pool is empty → creates a fresh property. If this property later i_swaps with an existing one, the WASM module computes a tensor product to merge the state spaces. Expensive.

Pooled properties are already in the shared state, so entangling them (via i_swap) is cheap.

Shipped Limits ​

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

EditionMax DimensionMax QuditsState Space per Qudit
Qutrit (default)3122–3 basis states
Qubit2202 basis states

Choose your edition during npx quantum-forge-engine init (or with --edition qubit). The Qutrit edition gives you three-valued properties but fewer total qudits. The Qubit edition trades away qutrits for a higher qudit ceiling. See Quantum Setup: Editions.

Properties in separate (non-entangled) systems don't count against each other.

Performance Budget ​

60 FPS target guidelines (Qutrit edition: dimension ≤ 3, 12 max qudits; Qubit edition: dimension 2, 20 max qubits):

Active PropertiesPerformanceNotes
1–4No issuesComfortable headroom
4–8GoodMonitor frame times
8–10Watch carefullyOnly with good pooling
10–12At the limitAggressive pooling required

Expensive Operations ​

Ranked by cost:

  1. Tensor product: triggered by i_swap between properties in different shared states. Cost grows exponentially with the number of qudits in each state. This is the operation to minimize.

  2. Fresh property creation: when the pool is empty. The property itself is cheap, but the first i_swap with an existing property triggers a tensor product.

  3. Measurement: proportional to state vector size. Fast for small systems, noticeable for 15+ entangled qudits.

  4. Gate application: fast. Scales linearly with state vector size.

  5. Probability query: similar cost to measurement but no state change.

Optimization Strategies ​

1. Aggressive Pooling ​

Measure and release as soon as a property is no longer needed:

typescript
// Ball exits field → measure immediately, don't wait
const exists = registry.measureExistence(ballId);
// Property is now back in the pool

2. Limit Concurrent Quantum Objects ​

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

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

3. Choose the Right Dimension ​

Higher dimensions = exponentially larger state space:

DimensionStates per propertyMemory per entangled pair
2 (qubit)264 bytes
3 (qutrit)3144 bytes

The Qutrit edition supports dimensions 2 and 3. The Qubit edition is locked to dimension 2 but supports up to 20 qubits. If you only need binary states and want more quantum objects, consider the Qubit edition.

4. Separate Registries for Independent Systems ​

If your game has quantum objects that never interact, use separate registries:

typescript
const playerRegistry = new PlayerQuantumRegistry(logger);  // player abilities
const enemyRegistry = new EnemyQuantumRegistry(logger);    // enemy states

// These systems never entangle with each other,
// so they maintain independent (small) state spaces

5. Batch Operations ​

Batch gates: When applying many gates per frame (AI sweeps, circuit replay), use executeBatch() to send them all in one WASM call. Each individual gate call crosses the JS-WASM boundary, and for short gates that crossing cost is the thing you are paying for, not the gate math.

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

const ops: BatchOp[] = tape.map(instr => ({
  op: instr.gate,
  target: instr.property,
  predicates: instr.controls,
}));
const result = m.executeBatch(ops); // one WASM call instead of ~100

See Gates: Batch Gate Execution for the full API.

For maximum throughput, use executeBatchTape() with a pre-encoded Float64Array. See Gates: Tape-based batch. 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. The native C++ benchmarks in the repo measure the simulator, not the JS boundary, so they do not predict what you will see from a browser.

Batch measurement: Measure multiple properties at once instead of one at a time:

typescript
const values = this.getModule().measure_properties([prop1, prop2, prop3]);

6. Throttle Probability Queries ​

Reading probabilities for visualization doesn't need to happen every frame:

typescript
private probCache = new Map<string, number>();
private lastProbUpdate = 0;

getExistenceProbability(id: string): number {
  const now = performance.now();
  if (now - this.lastProbUpdate < 100) { // update at most 10x/sec
    return this.probCache.get(id) ?? 1.0;
  }
  this.lastProbUpdate = now;

  const prop = this.getProperty(id);
  if (!prop) return 1.0;
  const prob = /* query probabilities */;
  this.probCache.set(id, prob);
  return prob;
}

Destroying Unused Properties ​

When a property is no longer needed but you don't need its measurement value, call destroy() to remove it from the shared state vector entirely:

typescript
tempProp.destroy(); // factorizes qudit out of state vector

destroy() is a method on the property handle, not a module function. It is more aggressive than pooling, shrinking the state vector rather than keeping the qudit in |0⟩. Use it for temporary properties in replay/adapter scenarios. See Lifecycle & State Management for details.

Isolated Simulations ​

For search tree exploration or replay branches, use QuantumSimulation to create isolated quantum contexts. Each simulation has its own state vectors, preventing cross-contamination:

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

This prevents the common problem of replay branches growing a shared global state vector. 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, and returns null when the probe fails. Do not build a heap overlay on it. It will start reporting real numbers in a future build, and until then use the state budget queries below.

State budget queries ​

Query the state vector size for any property to monitor quantum state complexity. Both are methods on the property handle:

typescript
const sparseSize = prop.state_vector_size();
const numQudits = prop.num_active_qudits();

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

Use getMaxStateSize() to compare against the hard limit (100,000 basis amplitudes). See Lifecycle: State Budget for backpressure patterns.

Error Handling ​

WASM operations throw typed JavaScript errors when they fail. The most important for performance is [QuantumForgeStateSizeError], which fires when a tensor product would exceed the state size limit:

typescript
quantumSplit(originalId: string, newId: string): boolean {
  const prop1 = this.getProperty(originalId);
  if (!prop1) return false;

  const prop2 = this.acquireProperty();
  try {
    this.getModule().i_swap(prop1, prop2, 0.5);
  } catch (e) {
    if (e instanceof Error && e.message.startsWith("[QuantumForgeStateSizeError]")) {
      this.logger?.warn?.("State too large for split, falling back to classical");
    } else {
      this.logger?.warn?.("Quantum operation failed, falling back to classical");
    }
    this.releaseProperty(prop2, 0);
    return false;
  }

  this.setProperty(newId, prop2);
  return true;
}

The OOM guard estimates tensor product size before allocating, so the operation fails cleanly without corrupting state. Design your game so that hitting the limit is graceful, not fatal. See Error Handling for the full guide.

Powered by Quantum Forge