Skip to content

Refactor: move animation playback and timing into Manager - #5004

Open
behackl wants to merge 7 commits into
refactor/manager-lifecyclefrom
refactor/manager-execution
Open

behackl wants to merge 7 commits into
refactor/manager-lifecyclefrom
refactor/manager-execution

Conversation

@behackl

@behackl behackl commented Sep 8, 2026

Copy link
Copy Markdown
Member

Overview: What does this pull request change?

Stacked on #5002.

This PR moves animation playback and the animation clock into Manager, using one play loop for Cairo and OpenGL. Scene prepares animations and updates mobjects; renderers draw frames and display the live preview; the manager decides what to play, advances time, and sends frames to the writer.

  • Compile each play call once, with shared cache checks, skip handling, interpolation, updater calls, stop checks, and animation cleanup.
  • Advance animation time independently of drawing, reading pixels, or writing frames. Interactive redraws continue to display frames without advancing the animation clock.
  • Keep renderer properties such as time, num_plays, and skip_animations working by forwarding them to the manager's state, including values set before the manager was attached.
  • Remove Scene.play_internal and the separate OpenGL caching decorator. The renderer play() methods delegate to the manager rather than implementing another scheduler.

Motivation and Explanation: Why and how do your changes improve the library?

Previously, playback was split between the scene and two renderer implementations, and the backends advanced time differently. In Cairo, advancing time depended on frame output; OpenGL generally exposed the play call's start time until the animation ended. A common loop lets updaters and stop conditions use the same clock on either backend and separates animation progress from producing pixels.

Timing and compatibility changes

  • During normal playback, interpolation and updaters see the start of each frame interval. The manager advances the clock before writing the frame and checking the stop condition. Finish and cleanup see the time reached by the processed frames, including an early-stopped wait.
  • Whole-frame rounding remains explicit: at 4 fps, a non-frozen 0.3-second animation takes two steps and advances 0.5 seconds; a frozen wait of the same duration repeats one frame and advances 0.25 seconds. OpenGL now follows the same sampled clock as Cairo.
  • Cache hits advance by the frame-rounded duration of normal playback. Explicit skips retain nominal-duration fast-forwarding. Neither shortcut guarantees the same Python state as running every updater step.
  • Segment cache keys include start time, play index, and frame rate. Earlier keys change, and identical geometry at different scene times no longer shares a segment. Repeated renders can still reuse matching segments. Waits with stop conditions are evaluated instead of reused from cache.
  • Changing the frame rate after scene construction raises an error before playback. Create and render the scene inside the intended configuration context so sampling and encoding agree.

Scene.play() remains the normal entry point. Integrations that override Scene.play_internal or rely on the renderer's play() being called by the scene need updating. Direct renderer drawing no longer writes movie frames or advances animation time.

This is preparation for evaluation without rendering, not that evaluation API itself. No no-raster mode, timeline export, or targeted-frame API is added here.

Links to added or changed documentation pages

  • Updated the play-call and render-loop walkthrough in docs/source/guides/deep_dive.rst, including a frame-timing table and configuration example.
  • Updated the Manager, Scene, and renderer API descriptions.

@behackl behackl added refactor Refactor or redesign of existing code breaking changes This PR introduces breaking changes labels Sep 8, 2026
uses wall-clock time to pace its live preview.
"""

def _animation_cache_identity(self, scene: Scene) -> tuple[str, Any]: ...

def _animation_cache_identity(self, scene: Scene) -> tuple[str, Any]: ...

def _start_animation(self) -> None: ...

def _start_animation(self) -> None: ...

def _prepare_animation(self, scene: Scene) -> None: ...

def render(
self, scene: Scene, frame_offset: float, moving_mobjects: list[Mobject], /
) -> None: ...
self, scene: Scene, frame_offset: float, moving_mobjects: list[Mobject], /
) -> None: ...

def get_frame(self) -> RGBAPixelArray: ...

def get_frame(self) -> RGBAPixelArray: ...

def _present_frame(self, scene: Scene, frame_offset: float) -> None: ...

def _present_frame(self, scene: Scene, frame_offset: float) -> None: ...

def _present_frozen_frame(self, scene: Scene, duration: float) -> None: ...
@behackl
behackl force-pushed the refactor/manager-execution branch from d3f4185 to dacb6b9 Compare September 10, 2026 08:24
Excluded plays (-n, skipped sections, still output) advanced the clock by the
raw requested run time, while cached plays advanced by the frames a render would
produce. Fractional run times therefore shifted every later play by up to one
frame interval, so segments rendered under -n were never reused by a full render
and anything reading scene.time rendered differently.

Both shortcuts now advance by the sampled duration, and they advance after the
single evaluation step rather than before it, so begin() observes the play's
start time and finish() the consumed span, as in a rendered play. Skipped waits
with a stop condition are stepped per frame like rendered ones, which also fixes
wait_until consuming its whole max_time when skipped.

Cache keys embed the play's start time, so the corrected clock invalidates
existing partial movie files once.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changes This PR introduces breaking changes refactor Refactor or redesign of existing code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants