Skip to content

Refactor: add scene evaluation without drawing or media output - #5005

Open
behackl wants to merge 9 commits into
refactor/manager-executionfrom
refactor/manager-no-raster
Open

behackl wants to merge 9 commits into
refactor/manager-executionfrom
refactor/manager-no-raster

Conversation

@behackl

@behackl behackl commented Sep 8, 2026

Copy link
Copy Markdown
Member

Overview: What does this pull request change?

Stacked on #5004.

Adds Manager.evaluate() to run a scene's animation logic without drawing its frames or producing media. This is useful for inspecting animation time, mobject state, and values computed by updaters without first rendering a movie.

with tempconfig({"frame_rate": 30}):
    scene = MyScene()
    manager = scene.manager or Manager(scene)
    manager.evaluate()
    print(scene.time)
    # Inspect scene.mobjects or values saved by MyScene.

Evaluation calls setup(), construct(), and tear_down() through the same animation loop used by normal rendering. It runs interpolation, updaters, stop checks, and animation finish/cleanup, with the same frame-rate sampling and frozen-wait rounding. It does not estimate elapsed time by summing requested durations.

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

Skipping animations or reusing cached movies takes shortcuts through Python state updates. Neither is a substitute for running all animation steps without rendering. This PR separates that use case from output: evaluation ignores movie caches, animation-range selection, and skip flags, while running the scene's animation logic normally.

The manager does not create a file writer, encoder, Cairo drawing buffer, OpenGL context, preview window, or file log for evaluation. It also skips output finalization and automatic last-frame capture.

Usage and limits

  • Start with a fresh scene, before playing animations or opening its renderer's drawing resources or file writer. Each scene can be evaluated once. Use the same frame-rate configuration for construction and evaluation.
  • next_section(), add_subcaption(), and add_sound() calls are allowed but produce no output or report. Sound files are neither checked nor decoded. Timeline export is a separate follow-up.
  • Requests for the evaluated scene's images, renderer pixels/GPU context, output writer, nested rendering, or interactive preview raise an error. Raw GPU-backed meshes are unsupported.
  • After evaluation, inspect the scene's time and Python state. An explicit scene.get_image() call can draw the resulting state afterward; that is a separate rendering operation.
  • Failures trigger cleanup and preserve the original exception. Changes already made to the scene are not rolled back.

This is not a sandbox or a guarantee of zero I/O. Geometry construction, typesetting, image loading, and arbitrary user code still run. Code that creates independent renderers, scenes, files, or processes is not isolated by this API. Ordinary Python-side Cairo and OpenGL mobject updates are supported; shader-only visual effects are not evaluated.

Cairo 3D projection

Camera projection queries now use the current angle trackers without depending on a preceding draw. The rotation matrix is computed when needed and excluded from the camera's cache key, so querying it does not invalidate cached movies. rotation_matrix is read-only: change the angle trackers or customize generate_rotation_matrix() instead of assigning the cached matrix.

Links to added or changed documentation pages

  • New guide: docs/source/guides/evaluation.rst.
  • Added a link from the Deep Dive and updated Manager.evaluate() and camera API descriptions.

@behackl behackl added refactor Refactor or redesign of existing code breaking changes This PR introduces breaking changes labels Sep 8, 2026
@behackl
behackl force-pushed the refactor/manager-no-raster branch from c7aa27d to 64021f9 Compare September 10, 2026 08:24

@nikolajmunk nikolajmunk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some quick documentation suggestions. I haven't done much here, since I'm guessing much of this will be rewritten soonish. Otherwise this looks very nice :)

Comment on lines +98 to +110
Cairo 3D camera queries
-----------------------

Calls such as ``camera.project_point()`` use the camera's current angle trackers,
even when no frame has been drawn. Reading the rotation matrix does not change
the camera's movie-cache key: the matrix is computed from the trackers, not a
separate camera setting.

``rotation_matrix`` is read-only. To rotate the camera, change its angle trackers
or use setters such as ``set_theta()``. Subclasses can customize
``generate_rotation_matrix()`` rather than assigning a matrix to the property.
If that customization depends on state beyond the angle trackers, call
``reset_rotation_matrix()`` when those extra inputs change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like this information fits better in the camera guide.

Comment on lines +34 to +43
Evaluation calls ``setup()``, ``construct()`` and ``tear_down()``. Play and wait
calls use the same animation loop as rendering with caching and skipping turned
off: interpolation, updaters, stop conditions, and animation finish/cleanup run
as usual. The clock follows the same frame-rate sampling and frozen-wait rounding
rules described in :doc:`deep_dive`. Time comes from running those steps, not
from adding up the requested animation durations.

The manager ignores cached movie segments, animation-range selection, and skip
flags, including those set by sections. All play calls reached by the scene's
Python code are evaluated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Evaluation calls ``setup()``, ``construct()`` and ``tear_down()``. Play and wait
calls use the same animation loop as rendering with caching and skipping turned
off: interpolation, updaters, stop conditions, and animation finish/cleanup run
as usual. The clock follows the same frame-rate sampling and frozen-wait rounding
rules described in :doc:`deep_dive`. Time comes from running those steps, not
from adding up the requested animation durations.
The manager ignores cached movie segments, animation-range selection, and skip
flags, including those set by sections. All play calls reached by the scene's
Python code are evaluated.
Evaluation calls ``setup()``, ``construct()`` and ``tear_down()``. Play and wait
calls use the same animation loop as rendering with caching and skipping turned
off: interpolation, updaters, stop conditions, and animation finish/cleanup run
as usual. The clock follows the same frame-rate sampling and frozen-wait rounding
rules described in :doc:`deep_dive`.

Removing redundant info.

Comment on lines +89 to +90
* Constructing geometry, typesetting text, loading images, and running updaters
still take time. Evaluation does not make expensive Python code instantaneous.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would remove this bullet point - let's assume the reader is already aware that Python code is still subject to the laws of space and time.

Comment on lines +95 to +96
* Choose the renderer and frame rate before constructing the scene. Normal
configuration validation and renderer-specific mobject classes still apply.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this not the case always? If so, it probably doesn't need a separate callout.

Comment thread manim/manager.py
Comment on lines +363 to +364
by evaluation. User code still runs and can perform its own I/O: this is
not a sandbox.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO this "this is not a sandbox" warning is fine to include in the thematic guide, but I think it can be left out of the docstring. At this point in the docstring, the user is already aware that the scene's methods (and therefore the code inside it) will be executed.

Comment thread manim/manager.py
or getattr(self.renderer, "_target", None) is not None
or getattr(self.renderer, "_context", None) is not None
):
raise RuntimeError("No-raster evaluation requires a cold, unused Scene.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
raise RuntimeError("No-raster evaluation requires a cold, unused Scene.")
raise RuntimeError("No-raster evaluation requires an unused Scene.")

the matrix corresponding to the current position of the camera
"""
self.rotation_matrix = self.generate_rotation_matrix()
"""Recompute the cached rotation matrix from the camera's current angles."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"""Recompute the cached rotation matrix from the camera's current angles."""
"""Recompute and cache the rotation matrix from the camera's current angles."""

Excluded and cached plays produce no frames, but they still called
renderer.render() for every sample, drew and read back frozen frames, prepared
Cairo's static frame, and presented frozen frames. Nothing consumed the result:
still output redraws explicitly in scene_finished, and OpenGL never presented
skipped frames. A skipped wait_until drew 900 frames at 15 fps, and an OpenGL
window busy-waited for the wall-clock duration of a skipped frozen wait.

Shortcut plays now take the same path evaluation does, which Manager.evaluate
already established does not change scene state.

Eliding those draws exposed a latent cache-identity bug. Vertex buffers, the
triangulation, shader-wrapper buffers and the OpenGL camera's view matrices are
all filled in on first draw, and hashing walks __dict__, so a play hashed
differently depending on whether an earlier play in the same run had been drawn.
Classes now declare such derived state in _hash_excluded_attributes, and
cached_property values are excluded automatically; ThreeDCamera's existing
inline exclusion moves to the same mechanism. The collected names are memoized
per MRO rather than per class, because ConvertToOpenGL rebases already created
classes when config.renderer changes.
Manager.add_sound dropped every request made while skip_animations was set, which
conflates two unrelated situations. A cache hit fast-forwards a play but its span
is still present in the artifact, so dropping the sound silently changed a
scene's audio depending on whether its partial movie files happened to exist: a
plain re-render of a three-sound scene kept only the first.

Exclusion by -n, a skipped section, or still output is different. Those plays are
absent from the artifact, whose timeline then covers only part of the scene,
while sound is placed at scene time. Such requests are still dropped, because
positioning them needs a map from scene time onto the selected output span. That
mapping, and the mixer it belongs to, are separate work; add_sound and the deep
dive now state the limitation instead of leaving it implicit.

Exclusion is therefore tracked separately from skipping rather than inferred from
the overloaded flag.
@behackl
behackl force-pushed the refactor/manager-no-raster branch from 64021f9 to dbd8b90 Compare September 17, 2026 15:27
pydub shells out to the ffprobe binary, which is not installed on CI runners,
so test_reused_segments_produce_an_identical_audio_track failed on every
platform with FileNotFoundError. PyAV is already a manim dependency, and the
suite already decodes video with it.

The assertion now compares decoded samples rather than pydub's duration and
loudness, which still catches the bug this covers: dropping sounds on a cache
hit removes roughly two thirds of the samples.
Memoizing on the MRO kept every class the hasher ever saw alive, which matters for
scenes that build classes per run. The names are now memoized weakly per class and
discarded by the renderer setter, the one place that rebases classes, so a class
that merely inherits from a rebased one is recollected too.
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