Skip to content

H-5770, H-6115: Add PlaybackContext for real-time simulation playback and WebWorker#8295

Open
kube wants to merge 44 commits intomainfrom
cf/h-5770-simulation-computeplayback
Open

H-5770, H-6115: Add PlaybackContext for real-time simulation playback and WebWorker#8295
kube wants to merge 44 commits intomainfrom
cf/h-5770-simulation-computeplayback

Conversation

@kube
Copy link
Collaborator

@kube kube commented Jan 23, 2026

🌟 What is the purpose of this PR?

Introduce a PlaybackContext that handles viewing simulation frames at controlled speeds, separate from simulation computation. Simulation computes frames as fast as possible (or buffered) while playback shows them at the configured speed.

🔗 Related links

🚫 Blocked by

Nothing

🔍 What does this change?

Architecture

  • SimulationContext: Handles simulation lifecycle, configuration, and frame computation via WebWorker
  • PlaybackContext: Handles visualization playback using requestAnimationFrame
  • Backpressure control: PlaybackProvider controls worker computation rate via ack() and setBackpressure()

Play Modes

Mode maxFramesAhead batchSize Behavior
viewOnly 0 0 Only plays existing frames, no computation
computeBuffer 40 10 Computes minimally ahead of playback
computeMax 10000 500 Computes as fast as possible

Playback Speeds

  • Finite speeds (1x, 2x, 5x, 10x, 30x, 60x, 120x): Frame advancement based on elapsed time
  • "Max" (Infinity): Jumps directly to latest available frame each tick

Key Changes

  • Playback pauses automatically when reaching end of available frames (if simulation complete)
  • initialize() returns Promise for proper async handling
  • Play mode auto-switches to viewOnly when simulation completes

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

None

🐾 Next steps

None

🛡 What tests cover this?

  • Unit tests in src/playback/provider.test.tsx (28 tests)

❓ How to test this?

  1. Checkout the branch
  2. Run the petrinaut editor with yarn dev
  3. Create a simulation and run it
  4. Confirm playback shows frames at correct speed based on dt and playback speed
  5. Test different play modes (viewOnly, computeBuffer, computeMax)
  6. Test different playback speeds including "Max"
  7. Verify playback pauses at end when simulation completes

📹 Demo

N/A

@vercel
Copy link

vercel bot commented Jan 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
petrinaut Ready Ready Preview Feb 5, 2026 9:28am
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
hash Ignored Ignored Preview Feb 5, 2026 9:28am
hashdotdesign Ignored Ignored Preview Feb 5, 2026 9:28am
hashdotdesign-tokens Ignored Ignored Preview Feb 5, 2026 9:28am

@github-actions github-actions bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Jan 23, 2026
Copy link
Collaborator Author

kube commented Jan 23, 2026

@github-actions github-actions bot added the area/infra Relates to version control, CI, CD or IaC (area) label Jan 23, 2026
@kube kube force-pushed the cf/h-5770-simulation-computeplayback branch from a676139 to 2c55788 Compare January 27, 2026 01:24
@kube kube force-pushed the cf/h-5770-simulation-computeplayback branch from 2c55788 to 558a7bb Compare January 27, 2026 11:27
@kube kube changed the base branch from main to graphite-base/8295 January 27, 2026 23:10
@kube kube force-pushed the cf/h-5770-simulation-computeplayback branch from 558a7bb to da55643 Compare January 27, 2026 23:10
@kube kube changed the base branch from graphite-base/8295 to cf/h-5763-have-a-minimap-for-nets-when-not-fully-in-view January 27, 2026 23:10
@kube kube changed the base branch from cf/h-5763-have-a-minimap-for-nets-when-not-fully-in-view to graphite-base/8295 January 28, 2026 00:58
@kube kube force-pushed the cf/h-5770-simulation-computeplayback branch from da55643 to 1ea2c35 Compare January 28, 2026 01:24
@kube kube changed the base branch from graphite-base/8295 to cf/h-5763-have-a-minimap-for-nets-when-not-fully-in-view January 28, 2026 01:24
@github-actions github-actions bot added the area/deps Relates to third-party dependencies (area) label Jan 28, 2026
@graphite-app graphite-app bot changed the base branch from cf/h-5763-have-a-minimap-for-nets-when-not-fully-in-view to graphite-base/8295 January 29, 2026 00:26
@kube kube changed the base branch from graphite-base/8295 to main January 29, 2026 10:48
kube and others added 22 commits February 5, 2026 02:04
Remove Promise<void> union from play and setPlayMode function types,
keeping only void returns for simpler synchronous API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace async arrow functions with Promise.resolve for mock accessors
- Use globalThis instead of self in web worker
- Use underscore prefix for intentionally unused simulation variables
- Fix dot notation for property access in tests
- Remove unnecessary void operator and conditional
- Remove constructor return statement in mock Worker class

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…mulationInstance definitions

- Removed redundant SimulationInstance definitions from test cases.
- Simplified tests by directly using SimulationFrame instances.
- Improved readability and maintainability of the test suite.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Place instances are now accessed via SimulationInstance.places Map
instead of being duplicated in each frame's place state. Functions
that need Place data now look it up from simulation.places.get(placeId).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rewrite simulation/worker/README.md with worker states, messages, and hook docs
- Add simulation/simulator/README.md explaining types and computation flow
- Add simulation/README.md documenting SimulationProvider context
- Add playback/README.md documenting PlaybackContext

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…igurable

- Move maxTime from worker state to SimulationInstance (immutable once set)
- Check maxTime in computeNextFrame instead of worker
- Add SimulationCompletionReason type ("maxTime" | "deadlock")
- Make backpressure configurable via InitMessage (maxFramesAhead, batchSize)
- Add setBackpressure message for runtime reconfiguration
- Remove setMaxTime message from worker protocol
- Update READMEs with new architecture documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase timeout duration from 0 to 50 milliseconds to allow for better simulation initialization.
- Add comments to clarify the need for improvement in handling simulation initialization completion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove computeBufferDuration from SimulationContext and derive it
from playMode in PlaybackProvider:
- viewOnly: 0s (no buffering needed)
- computeBuffer: 0.5s ahead
- computeMax: 10s ahead (large buffer for continuous computation)

This simplifies the API by making buffer duration an internal
implementation detail of PlaybackProvider.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify play(), setPlayMode(), and tick() by removing manual maxTime
management. The backpressure mechanism via ack now handles all
computation flow control automatically.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Make PlaybackProvider.play() the single entry point for starting
playback/simulation. This consolidates initialization logic and ensures
backpressure params are always passed correctly.

Key changes:
- Remove console.log debug statements
- Replace fragile setTimeout(50) with state-based coordination
- Remove duplicate initialization logic from simulation-controls
- Remove unused COMPUTE_BUFFER_THRESHOLD constant

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- SimulationContext.initialize() now returns Promise<void> that resolves
  when worker is ready or rejects on error
- PlaybackProvider.play() awaits initialize() before calling runSimulation()
- Worker tracks simulationStatus (ready/running/complete/error) for proper
  state validation
- Moved run guard logic from Provider to worker where it has authoritative state
- Updated tests to handle Promise-returning functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update useLatest hook to set ref synchronously during render instead of
in useEffect. This ensures the ref is immediately available to other
effects in the same render cycle, fixing an issue where the last batch
of simulation frames wasn't visible in the Timeline.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When playback speed is Infinity, skip time-based frame advancement
(which produces Infinity math) and instead jump directly to the
latest available frame. Pauses playback when simulation completes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sync documentation with current implementation:
- Fix incorrect backpressure values (computeBuffer: 40/10)
- Simplify and condense documentation
- Add playback speed behavior details (including Max/Infinity)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address AI review feedback:
- Pass current RNG state to computePossibleTransition() to ensure
  deterministic random sequencing across multiple transitions per frame
- Fix off-by-one in ack() calls: use totalFrames - 1 since indices are 0-based
- Remove unused runSimulation from effect dependency array

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add getFramesInRange(start, end?) method to SimulationContext for
  fetching frames in a range instead of all frames
- Update useCompartmentData hook to use incremental frame fetching,
  only processing new frames instead of re-processing all frames
- Cache place colors computation with useMemo
- Track processed frame count to enable incremental updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix issues identified by AI reviewers and human feedback:
- Add race condition guards after async getFrame() in animation loop
- Make stop() pause simulation worker like pause() does
- Fix stale frame count in play() and setCurrentViewedFrame()
- Remove unnecessary exports (PlayModeBackpressure, PLAY_MODES)
- Update changeset description to be user-facing
- Optimize useCompartmentData from O(p*f) to O(p+f)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

const reset: WorkerActions["reset"] = () => {
postMessage({ type: "stop" });
setState(initialState);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated stop and reset implementations

Low Severity

The stop and reset actions have identical implementations - both post a { type: "stop" } message and reset state to initialState. The reset function should call stop() instead of duplicating the logic.

Fix in Cursor Fix in Web

@kube kube requested a review from CiaranMn February 5, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants