Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
49321fc
Normalize render output requests
behackl Aug 26, 2026
f29ab5b
Document canonical output formats
behackl Aug 26, 2026
7b04d7d
Separate live and post-render preview
behackl Aug 26, 2026
421c05c
Document renderer-independent preview modes
behackl Aug 26, 2026
56eceb8
Address renderer capability review feedback
behackl Aug 26, 2026
2251e9d
Resolve render session configuration once
behackl Aug 26, 2026
5e12819
Update output and render-flow documentation
behackl Aug 26, 2026
cf18c9b
Keep deep-dive introduction focused
behackl Aug 26, 2026
af44d04
Update slow output tests for normalized formats
behackl Aug 26, 2026
c6a411a
Preserve dry-run session intent
behackl Aug 28, 2026
bf9e80b
Test canonical output path behavior
behackl Aug 28, 2026
81c3a11
Add immutable scene output planning
behackl Aug 28, 2026
e5ae801
Route writers through resolved output plans
behackl Aug 28, 2026
2258f0e
Materialize planned output paths lazily
behackl Aug 28, 2026
fd72ad5
Move file logging out of the writer
behackl Aug 28, 2026
ff26dcb
Remove the custom folders preset
behackl Aug 28, 2026
7c3ef8b
Document exact media path planning
behackl Aug 28, 2026
ea958d2
Address output naming review feedback
behackl Aug 28, 2026
cde9897
Simplify parallel encoding test setup
behackl Aug 28, 2026
62def15
Remove unused directive filename assignment
behackl Aug 28, 2026
02cc9d1
Apply batched suggestions from code review
behackl Aug 29, 2026
1f6e643
Address output documentation review feedback
behackl Aug 29, 2026
fd21061
Respect explicit video output for static scenes
behackl Aug 29, 2026
23393b7
Merge branch 'refactor/output-session-config' into refactor/media-lay…
behackl Aug 29, 2026
c5ca93f
Fix automatic output write-all test
behackl Aug 29, 2026
0dbb4a9
Merge branch 'refactor/output-session-config' into refactor/media-lay…
behackl Aug 29, 2026
5631246
Replace render-time cache flushing with cache clear
behackl Aug 30, 2026
180cc7d
Characterize cached video segment behavior
behackl Aug 30, 2026
f729180
Resolve cached segment encoder settings
behackl Aug 30, 2026
274af5d
Fingerprint encoder settings in visual cache keys
behackl Aug 30, 2026
0f7bb38
Add configurable video encoder profiles
behackl Aug 30, 2026
edddc2b
Extract the video segment encoder
behackl Aug 30, 2026
656b62b
Resolve explicit scene file writer settings
behackl Aug 30, 2026
420a9e7
Detach scene file writing from renderers
behackl Aug 30, 2026
1303289
Document resolved encoder and writer boundaries
behackl Aug 30, 2026
9f5a32c
Harden resolved writer inputs and temporary assembly
behackl Aug 30, 2026
f696066
Simplify visual cache keys and trim tests
behackl Aug 30, 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
8 changes: 4 additions & 4 deletions docs/source/contributing/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,10 @@ Note the fixtures here:
You have to generate a ``.json`` file first to be able to test your video. To
do that, use ``helpers.save_control_data_from_video``.

For instance, a test that will check if the l flag works properly will first
require rendering a video using the -l flag from a scene. Then we will test
(in this case, SquareToCircle), that lives in
``test_scene_rendering/simple_scene.py``. Change directories to ``tests/``,
For instance, testing low-quality output requires first rendering a video from a
scene with the ``-ql`` flag. The example below tests
``SquareToCircle``, which lives in ``test_scene_rendering/simple_scene.py``.
Change directories to ``tests/``,
create a file (e.g. ``create\_data.py``) that you will remove as soon as
you're done. Then run:

Expand Down
5 changes: 3 additions & 2 deletions docs/source/faq/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ ones that are documented for the base classes {class}`.VMobject` and
Yes: simply pass the CLI flag `-t` (or its long form `--transparent`).
Note that the default video file format does not support transparency,
which is why Manim will output a `.mov` instead of a `.mp4` when
rendering with a transparent background. Other movie file formats
that support transparency can be obtained by passing
rendering with a transparent background. Explicitly requesting
`--format=mp4` together with transparency is rejected. Other movie file
formats that support transparency can be obtained by passing
`--format=webm` or `--format=gif`.

---
Expand Down
75 changes: 49 additions & 26 deletions docs/source/guides/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,20 @@ An example of using the above form is:
This asks Manim to search for a Scene class called :code:`SceneOne` inside the
file ``file.py`` and render it with medium quality (specified by the ``-qm`` flag).

Another frequently used flag is ``-p`` ("preview"), which makes manim
open the rendered video after it's done rendering.
Another frequently used flag is ``-p`` ("preview"), which makes Manim
open the rendered artifact after rendering. This behavior is the same for all
renderers.

.. note:: The ``-p`` flag does not change any properties of the global
``config`` dict. The ``-p`` flag is only a command-line convenience.
Renderers may also support a live preview while the scene is evaluated. The
OpenGL renderer provides this with ``-l`` (or ``--live-preview``):

.. code-block:: bash

manim --renderer=opengl -l <file.py> SceneName

With ``--format=auto``, live preview does not write a media file, keeping the
interactive workflow fast. Pass a concrete format, such as ``--format=mp4``,
to record the scene while displaying the live preview.

Advanced examples
=================
Expand All @@ -53,6 +62,10 @@ instead of the whole video, you can execute

manim -sqh <file.py> SceneName

The equivalent ``--format=png`` spelling uses the same fast mode that saves only
the last frame. Use ``--format=png-sequence`` when every rendered frame should
be written as a numbered PNG instead.

The following example specifies the output file name (with the :code:`-o`
flag), renders only the first ten animations (:code:`-n` flag) with a white
background (:code:`-c` flag), and saves the animation as a ``.gif`` instead of as a
Expand Down Expand Up @@ -161,17 +174,36 @@ and serve the same purpose. Take, for example, the following config file.
[CLI]
# my config file
output_file = myscene
save_as_gif = True
format = gif
background_color = WHITE

Config files are parsed with the standard python library ``configparser``. In
particular, they will ignore any line that starts with a pound symbol ``#``.

Video encoder profiles use dedicated sections because codec options are arbitrary
key/value pairs:

.. code-block:: ini

[video_encoder]
codec = libx264
pixel_format = yuv420p

[video_encoder.options]
crf = 18
preset = slow

``codec`` and ``pixel_format`` default to ``auto``. Manim resolves them from the
output format and whether alpha is required. The equivalent CLI options are
``--video-codec``, ``--pixel-format``, and repeatable
``--encoder-option KEY=VALUE``. If any ``--encoder-option`` is supplied, the CLI
option map replaces the complete ``[video_encoder.options]`` map.

Now, executing the following command

.. code-block:: bash

manim -o myscene -i -c WHITE <file.py> SceneName
manim -o myscene --format=gif -c WHITE <file.py> SceneName

is equivalent to executing the following command, provided that ``manim.cfg``
is in the same directory as <file.py>,
Expand Down Expand Up @@ -253,7 +285,7 @@ For example, take the following user-wide config file
# user-wide
[CLI]
output_file = myscene
save_as_gif = True
format = gif
background_color = WHITE

and the following folder-wide file
Expand All @@ -262,7 +294,7 @@ and the following folder-wide file

# folder-wide
[CLI]
save_as_gif = False
format = auto

Then, executing :code:`manim <file.py> SceneName` will be equivalent to not
using any config files and executing
Expand Down Expand Up @@ -344,26 +376,17 @@ highest precedence is:
5. any programmatic changes made after the config system is set.


A list of all config options
****************************
Inspecting available config options
***********************************

.. code::
Run ``manim cfg show`` to inspect the currently resolved configuration and
``manim render --help`` for the authoritative list of render CLI options. The
attributes available for programmatic configuration are documented on
:class:`.ManimConfig`.

['aspect_ratio', 'assets_dir', 'background_color', 'background_opacity',
'bottom', 'custom_folders', 'disable_caching', 'dry_run',
'encoder_queue_size', 'ffmpeg_loglevel', 'flush_cache', 'frame_height', 'frame_rate',
'frame_size', 'frame_width', 'frame_x_radius', 'frame_y_radius',
'from_animation_number', `fullscreen`, 'images_dir', 'input_file', 'left_side',
'log_dir', 'log_to_file', 'max_files_cached', 'max_inflight_encoders',
'media_dir', 'media_width', 'movie_file_extension', 'notify_outdated_version',
'output_file', 'partial_movie_dir',
'pixel_height', 'pixel_width', 'plugins', 'preview',
'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
'upto_animation_number', 'use_opengl_renderer', 'verbosity', 'video_dir',
'window_position', 'window_monitor', 'window_size', 'write_all', 'write_to_movie',
'enable_wireframe', 'force_window']
Some CLI conveniences intentionally map to a canonical configuration value. In
particular, ``-s`` / ``--save_last_frame`` sets ``format = png``; configuration
files should use the canonical ``format`` option directly.


Accessing CLI command options
Expand Down
149 changes: 107 additions & 42 deletions docs/source/guides/deep_dive.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,19 @@ Preliminaries
Importing the library
^^^^^^^^^^^^^^^^^^^^^

Independent of how exactly you are telling your system
to render the scene, i.e., whether you run ``manim -qm -p file_name.py ToyExample``, or
whether you are rendering the scene directly from the Python script via a snippet
like
You can ask Manim to render in several ways. For example, you can run
``manim -qm -p file_name.py ToyExample`` or render directly from a Python script
with a snippet like

::

with tempconfig({"quality": "medium_quality", "preview": True}):
scene = ToyExample()
scene.render()

or whether you are rendering the code in a Jupyter notebook, you are still telling your
python interpreter to import the library. The usual pattern used to do this is
Whether you are rendering this way or from a Jupyter notebook, you are still
telling your Python interpreter to import the library. The usual pattern used to
do this is

::

Expand Down Expand Up @@ -243,7 +243,7 @@ then calls the scene's render method in the
**Jupyter notebooks.** In Jupyter notebooks, the communication with the library
is handled by the ``%%manim`` magic command, which is implemented in the
``manim.utils.ipython_magic`` module. There is
:meth:`some documentation <.ManimMagic.manim>` available for the magic command,
:meth:`some documentation <.ManimMagic.manim>` available for the magic command.
The implementation instantiates the requested scene and calls its render method;
the scene attaches a manager lazily through that entry point.

Expand All @@ -263,18 +263,72 @@ depend on any configuration options set in ``config``. Then the scene inspects t
``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an
``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute.

The scene then asks its renderer to initialize the scene by calling
After selecting the renderer, the scene resolves the mutable configuration into
one immutable render-session specification. In abbreviated form, initialization
continues as follows:

::

self.renderer.init_scene(self)

Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene``
method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which
basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo
renderer (see the implementation `here <https://github.com/ManimCommunity/manim/blob/main/manim/renderer/cairo_renderer.py>`__) does not require any further initialization. The OpenGL renderer
does some additional setup to enable the realtime rendering preview window, which we do not go
into detail further here.
self.session_spec = resolve_render_session(
config,
self.renderer.capabilities,
renderer_name=type(self.renderer).__name__,
)
self.output_plan = resolve_output_plan(
resolve_media_layout(...),
self.session_spec.output,
scene_name=type(self).__name__,
requested_output_name=...,
)
self.file_writer_settings = _SceneFileWriterSettings(
plan=self.output_plan,
video_encoder=self.session_spec.video_encoder,
max_inflight_encoders=config.max_inflight_encoders,
encoder_queue_size=config.encoder_queue_size,
max_files_cached=config.max_files_cached,
assets_dir=...,
)
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
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
an effective output format of ``none``, so the session's ``dry_run`` field
preserves the intended execution behavior.

The current renderer loops do not yet enforce no-raster dry runs; the session
field preserves that request for Manager-owned execution. Resolution also
validates requests against the selected renderer's capabilities. For example,
Cairo rejects live preview, while OpenGL advertises support for it. A concrete
format records the live preview as well.

The scene resolves configured 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. ``output_file``
supplies the artifact name, while the resolved format supplies its suffix.

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 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
Expand All @@ -283,10 +337,10 @@ attribute is initially ``None`` unless the caller attaches a manager explicitly.

.. warning::

:class:`.Manager` is an incremental coordination boundary. At this stage the
renderer still owns its camera, clock, play count, skip state, and file writer;
the manager exposes forwarding views of them. The scene and renderer therefore
still have substantial interplay that later refactors aim to remove.
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 currently exposes these through forwarding properties.

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

Expand All @@ -312,13 +366,19 @@ The first three call the corresponding customizable scene hooks:
hook is more relevant for situations where Manim is used within other
Python scripts).

After these hooks have run, :meth:`.Manager.post_construct` asks the renderer to
finish the scene. For Cairo this calls :meth:`.CairoRenderer.scene_finished`,
which checks whether animations have been played and tells the
:class:`.SceneFileWriter` to finish the output. For video output, the file writer
waits for partial movie files that are still being encoded and combines them into
the final movie. If no animations have been played, Manim assumes that a static
image should be output.
After these hooks have run, :meth:`.Manager.post_construct` checks whether the
resolved output can be finalized. An explicit video format for a scene without
play calls raises an error instead of silently changing the requested artifact.
Otherwise, the manager asks the renderer to finish the scene. For Cairo this
calls :meth:`.CairoRenderer.scene_finished`, which tells the
:class:`.SceneFileWriter` to finish any time-based output. For video output, the
file writer waits for partial movie files that are still being encoded and
combines them into the final movie. Last-frame PNG output fast-forwards
animations and renders a single image. When automatic output selected a video
for a scene without play calls, the writer saves that image as a fallback PNG and
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.

**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
Expand Down Expand Up @@ -767,13 +827,15 @@ to learn more, the :func:`.get_hash_from_play_call` function in the
:mod:`.utils.hashing` module is essentially the entry point to the caching
mechanism.

In the event that the animation has to be rendered, the renderer asks
its :class:`.SceneFileWriter` to open a partial movie stream. The file writer
uses ``libav`` to create a container and video stream, then wraps them in a
``_PartialMovieEncodeJob``. Each encoding job owns its container, stream, frame
queue, and worker thread. During the render loop, rendered raw frames are added
to this queue and encoded by the worker. With the writing process in place, the
renderer then asks the scene to "begin" the animations.
In the event that the animation has to be rendered, the renderer gives its
:class:`.SceneFileWriter` the current animation index and asks it to start a
segment job. The writer creates a ``VideoSegmentEncoder`` from the resolved
profile and wraps it in a ``_PartialMovieEncodeJob``. The synchronous segment
encoder owns its container, video stream, sequential presentation timestamps,
and target cleanup. The job owns only the frame queue and worker thread. During
the render loop, concrete top-left-origin ``uint8`` RGBA arrays are added to the
queue and encoded by the worker. With the writing process in place, the renderer
then asks the scene to "begin" the animations.

By default, Manim finishes encoding each partial movie file before rendering the
next animation. If ``max_inflight_encoders`` is set to a value greater than 1,
Expand Down Expand Up @@ -826,9 +888,8 @@ time is extracted (3 seconds long) and stored in
``Scene.duration``. The renderer then checks whether it should
skip (it should not), then whether the animation is already
cached (it is not). The corresponding animation hash value is
determined and passed to the file writer, which then also calls
``libav`` to start the writing process which waits for rendered
frames from the library.
determined and passed to the file writer. The writer resolves the segment target
from that key and starts its queued encoder, which waits for rendered frames.

The scene then ``begin``\ s the animation: for the
:class:`.ReplacementTransform` this means that the animation populates
Expand Down Expand Up @@ -968,8 +1029,10 @@ camera is asked to capture the scene:

After all batches have been processed, the camera has an image representation
of the Scene at the current time stamp in form of a NumPy array stored in its
``pixel_array`` attribute. The renderer then takes this array and passes it to
its :class:`.SceneFileWriter`. This concludes one iteration of the render loop,
``pixel_array`` attribute. The renderer passes a top-left-origin,
C-contiguous ``uint8`` RGBA array to its :class:`.SceneFileWriter`. OpenGL uses
the same array contract and performs GPU readback at this renderer boundary only
when file output needs a frame. This concludes one iteration of the render loop,
and once the time progression has been processed completely, a final bit
of cleanup is performed before the :meth:`.Scene.play_internal` call is completed.

Expand Down Expand Up @@ -1038,9 +1101,11 @@ calls the scene's cleanup method :meth:`.Scene.tear_down`, followed by
scene, and the renderer in turn asks its scene file writer to wrap things up by
calling :meth:`.SceneFileWriter.finish`. The file writer first waits for all
remaining encoding jobs, then combines the completed partial movie files into the
final product. If rendering aborts during a play instead, the manager asks the file
writer to abort its encoding jobs; the incomplete current partial movie file is
removed so that it cannot be mistaken for a valid cached result on a later render.
final product and records its path. If rendering aborts during a play instead, the
manager asks the file writer to abort its encoding jobs; the incomplete current
partial movie file is removed so that it cannot be mistaken for a valid cached
result on a later render. Once output is finalized, the manager carries out any
post-render presentation request from the immutable session specification.

And there you go! This is a more or less detailed description of how Manim works
under the hood. While we did not discuss every single line of code in detail
Expand Down
6 changes: 3 additions & 3 deletions docs/source/installation/docker.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ For our image ``manimcommunity/manim``, there are the following tags:

.. note::

When using Manim's CLI within a Docker container, some flags like
``-p`` (preview file) and ``-f`` (show output file in the file browser)
are not supported.
When using Manim's CLI within a Docker container, options that launch host
applications, such as ``-p`` and ``--show_in_file_browser``, are not
supported. Live preview also requires explicit display forwarding.

.. note::

Expand Down
Loading