Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
62e8803
Preserve encoder interrupt identity through cleanup
behackl Sep 5, 2026
7b5fcd3
Characterize pending Manager lifecycle cleanup guarantees
behackl Sep 5, 2026
0907861
Abort encoding jobs on render lifecycle failures
behackl Sep 5, 2026
36930fa
Allocate Cairo raster targets on first pixel demand
behackl Sep 5, 2026
0f9ac5c
Roll back failed OpenGL context and framebuffer initialization
behackl Sep 5, 2026
5c84a57
Close native windows when initialization fails
behackl Sep 5, 2026
e385e6e
Reuse constructor-attached Managers in CLI execution
behackl Sep 5, 2026
8378fa4
Move lazy file writer ownership into Manager
behackl Sep 5, 2026
2c81a85
Resolve default OpenGL window size at construction time
behackl Sep 5, 2026
98e0073
Separate OpenGL window inputs from native acquisition
behackl Sep 5, 2026
1434b3f
Defer OpenGL resource opening and add explicit retirement
behackl Sep 5, 2026
4400f00
Make Manager own backend retirement and execution-scoped logging
behackl Sep 5, 2026
cbcc503
Retire successful renders unless a Manager scope retains inspection
behackl Sep 6, 2026
10750b1
Consolidate lifecycle failure probes without losing cleanup boundaries
behackl Sep 7, 2026
e78a97d
Bind renderers and writers to one Scene and simplify CLI reruns
behackl Sep 7, 2026
22d19c3
Clarify rendering resource lifetimes and inspection guidance
behackl Sep 8, 2026
1f49033
Apply batched suggestions from code review
behackl Sep 8, 2026
42b2dae
Repair lifecycle review prose and preserve Unicode CLI diagnostics
behackl Sep 9, 2026
7d4d2bb
Temporarily diagnose Windows WGL context transitions in CI
behackl Sep 9, 2026
d667381
Restore native WGL bindings when activating preview contexts
behackl Sep 9, 2026
46fffe8
Capture child tracebacks before preview regression timeouts
behackl Sep 9, 2026
1e728de
Print full child diagnostics when preview subprocesses time out
behackl Sep 9, 2026
cd0caee
Remove temporary native-context CI diagnostics
behackl Sep 9, 2026
530a1ea
Allow cold-start headroom in subprocess integration tests
behackl Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 85 additions & 39 deletions docs/source/guides/deep_dive.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,10 @@ method. Inspecting the corresponding code (see
reveals that ``Scene.__init__`` first sets several attributes of the scene objects that do not
depend on any configuration options set in ``config``. Then the scene inspects the value of
``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an
``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute.
``OpenGLRenderer`` object, exposed by its read-only ``renderer`` property.

After selecting the renderer, the scene resolves the mutable configuration into
one immutable render-session specification. In abbreviated form, initialization
an immutable *render-session specification*. In abbreviated form, initialization
continues as follows:

::
Expand All @@ -291,15 +291,16 @@ continues as follows:
self.renderer.init_scene(
self,
self.session_spec,
self.file_writer_settings,
)

The session specification separates primary artifact intent (an ``OutputSpec``)
from presentation requests such as opening the completed artifact or displaying
a live preview. For video output it also contains the resolved segment profile:
container, codec, pixel format, dimensions, exact frame rate, and codec options.
It records dry-run execution separately from artifact selection. A dry run
requests semantic scene evaluation without rasterizing
The session specification separates *primary artifact* intent (an ``OutputSpec``
which defines the type of file we wish Manim to create based on our scene) from
*presentation requests* such as displaying a live preview or opening the
artifact after rendering finishes.
When the primary artifact is a video, the session specification also contains
information about the video codec, pixel format, dimension, frame rate, etc.
The specification also separately records requests for *dry-run execution*.
A dry run requests semantic scene evaluation without rasterizing
frames or using media and cache resources. In contrast, ``format = none`` only
suppresses the primary artifact; an OpenGL live preview with automatic output
still rasterizes and displays frames without writing a file. Both requests have
Expand All @@ -313,36 +314,55 @@ Cairo rejects live preview, while OpenGL advertises support for it. A concrete
format records the live preview as well.

The scene then resolves existing directory templates once into an immutable
output plan containing exact scene-specific artifact, section, image-sequence,
and cache paths. Planning performs no file I/O and creates no directories. The
resolved format determines the artifact suffix; ``output_file`` supplies only a
name and cannot change the format.

The scene combines the output plan and segment profile with the encoder-pool,
cache-maintenance, and sound-asset inputs in immutable
``_SceneFileWriterSettings``. Both renderers instantiate a
:class:`.SceneFileWriter` from these settings. The writer does not retain a
renderer reference or read mutable global configuration. Directories are created
lazily when their owning operation first writes. The writer remains Manim's
interface to ``libav`` for media assembly. The Cairo renderer (see the
implementation `here
<https://github.com/ManimCommunity/manim/blob/main/manim/renderer/cairo/renderer.py>`__)
does not require further renderer-specific initialization. OpenGL creates a
window only when the resolved presentation specification requests a live preview.
The ``-p`` / ``--preview`` option does not create this window; it opens the
completed artifact after rendering.

After the renderer has been instantiated and initialized its file writer, the scene
populates further initial attributes (notable mention: the ``mobjects`` attribute
which keeps track of the mobjects that have been added to the scene). Its ``manager``
attribute is initially ``None`` unless the caller attaches a manager explicitly.

.. warning::

The scene captures the immutable session specification and output plan before
renderer initialization. The manager coordinates the scene lifecycle, while
the renderer still owns its camera, clock, play count, skip state, and file
writer. The manager exposes these through forwarding properties.
*output plan* containing scene-specific output paths for the artifact, sections,
image-sequences, and cached files. If any of these paths contain directories
which do not exist, those directories will be created when a file writer needs
to use them and not at this point in time.
The resolved output format determines the file extension; ``output_file`` supplies
a name rather than overriding that format.

The scene stores the output plan, video encoding settings, cache limits, and
sound-asset directory in ``_SceneFileWriterSettings``. The :class:`.Manager` uses
these settings to create a :class:`.SceneFileWriter` when rendering starts, before
``setup()``. Accessing ``manager.file_writer`` or ``renderer.file_writer`` also
creates this writer if needed. Both properties return the same writer for the
rest of the scene. To customize it, pass ``file_writer_class`` when constructing
the renderer.

The writer uses the saved settings to encode and assemble media through ``libav``.
It creates directories as it writes files. A snapshot from ``scene.get_image()``
uses just the renderer, leaving the writer uninitialized if it has not been used.

**Creating graphics resources.** Renderer initialization stores the settings for
later use. The two backends create their drawing resources at different points:

- Cairo allocates an image buffer and drawing context on the first draw or request
for pixels.
- OpenGL creates its context and framebuffer when ``Manager.render()`` starts,
before ``setup()``. An explicit request for GPU resources, such as accessing
``renderer.context``, creates them sooner. A live-preview request also opens a
window. A snapshot requested before this initialization uses a temporary context
without a window.

The ``-p`` / ``--preview`` option opens the completed output file after rendering;
it is separate from OpenGL's live-preview window.

Scene initialization also prepares ``mobjects``, the list of mobjects added to the
scene, and sets ``manager`` to ``None``. The manager is lazily created: the
first time a call is made which requires a manager, such as ``render()`` or
``get_image()``, a new manager is created and attached to the scene as
``scene.manager``. This manager is then reused for any subsequent calls.

**One renderer per scene.** A scene keeps the renderer selected at construction;
``scene.renderer`` is read-only. Successive plays reuse that renderer and its
writer. For another scene, create a new renderer or let the scene create its
default. The renderer stores the camera, animation clock, play count, and skip
state; the manager has corresponding properties which are used to read and
update those values.

The CLI creates a fresh scene for each selected class and each interactive rerun,
on both Cairo and OpenGL. It closes the previous run's resources before creating
the next scene, which starts with a new camera and animation clock.

The rest of this article is concerned with the last line in our toy example script::

Expand Down Expand Up @@ -382,6 +402,32 @@ the manager logs a warning. The writer records the completed artifact as
``final_file_path``. After finalization, the manager uses the presentation
specification to open the artifact or reveal it in the file browser.

**Closing resources and inspecting results.** By default, rendering closes the
renderer once the output is complete. To read the last-rendered frame directly
from the renderer, keep it open with a ``with manager:`` block::

scene = ToyExample()
manager = Manager(scene)
with manager:
manager.render()
last_frame = manager.renderer.get_frame()

The renderer stays open after successful rendering until the end of the ``with``
block. The scene and its mobjects remain available afterward. To draw a new image,
call ``scene.get_image()``; it creates temporary rendering resources as needed.
OpenGL meshes stored directly on the GPU require their original context, so these
must be inspected inside the block.

When file logging is enabled, the manager opens the scene's log at the start of
rendering or the first direct ``play()`` call. It removes and closes that handler
when rendering ends or the manager closes, leaving other log handlers in place.
If rendering fails, the manager stops unfinished encoding work and closes the
renderer before raising the original error. Errors from cleanup are logged
separately. A failed ``play()`` call performs the same cleanup even when called
outside ``render()``. Put animation code in ``setup()`` or ``construct()`` so it
runs within the normal render lifecycle; custom file or process operations need
their own cleanup.

**Back in our toy example,** the call to :meth:`.Scene.render` creates a manager,
then :meth:`.Manager.render` triggers :meth:`.Scene.setup` (which only consists of
``pass``), followed by :meth:`.Scene.construct`. At this point, our *animation
Expand Down
28 changes: 22 additions & 6 deletions manim/_config/logger_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,36 @@ def parse_theme(parser: configparser.SectionProxy) -> Theme | None:
return custom_theme


def set_file_logger(log_file_path: Path) -> None:
"""Add a file handler for one exact, already resolved log path.
def set_file_logger(log_file_path: Path) -> logging.FileHandler:
"""Add a file handler to Manim's logger at the given path.

Parameters
----------
log_file_path
Exact path of the log file for this scene.

Returns
-------
logging.FileHandler
The attached handler. Remove it from the logger and close it when
logging for this scene ends.
"""
file_handler = logging.FileHandler(log_file_path, mode="w")
file_handler.setFormatter(JSONFormatter())

logger = logging.getLogger("manim")
logger.addHandler(file_handler)
logger.info("Log file will be saved in %(logpath)s", {"logpath": log_file_path})
try:
file_handler.setFormatter(JSONFormatter())
logger.addHandler(file_handler)
logger.info("Log file will be saved in %(logpath)s", {"logpath": log_file_path})
except BaseException as error:
logger.removeHandler(file_handler)
try:
file_handler.close()
except BaseException as cleanup_error:
error.add_note(
f"Closing the new log handler also failed: {cleanup_error!r}"
)
raise
return file_handler


class JSONFormatter(logging.Formatter):
Expand Down
35 changes: 9 additions & 26 deletions manim/cli/render/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
from manim.cli.render.global_options import global_options
from manim.cli.render.output_options import output_options
from manim.cli.render.render_options import render_options
from manim.constants import EPILOG, RendererType
from manim.manager import Manager
from manim.constants import EPILOG
from manim.utils.module_ops import scene_classes_from_file

__all__ = ["render"]
Expand Down Expand Up @@ -94,32 +93,16 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
scene_classes = scene_classes_from_file(file)
_validate_scene_batch_output_name(scene_classes)

if config.renderer == RendererType.OPENGL:
from manim.renderer.opengl import OpenGLRenderer

renderer = OpenGLRenderer()
keep_running = True
while keep_running:
for SceneClass in scene_classes:
with tempconfig({}):
scene = SceneClass(renderer)
# Attach explicitly, but preserve custom Scene.render overrides.
Manager(scene)
rerun = scene.render()
if rerun or config["write_all"]:
renderer.num_plays = 0
continue
keep_running = False
break
if config["write_all"]:
keep_running = False
else:
for SceneClass in scene_classes:
for SceneClass in scene_classes:
while True:
with tempconfig({}):
scene = SceneClass()
# Attach explicitly, but preserve custom Scene.render overrides.
Manager(scene)
scene.render()
# Reuse a manager created by the scene's constructor. The
# with block also cleans up after custom render() overrides.
with scene._get_manager():
rerun = scene.render()
if not rerun:
break
except Exception:
error_console.print_exception()
sys.exit(1)
Expand Down
Loading