Skip to content

Rework output configuration options and their consumption - #4966

Merged
behackl merged 15 commits into
ManimCommunity:mainfrom
behackl:refactor/output-session-config
Sep 3, 2026
Merged

Rework output configuration options and their consumption#4966
behackl merged 15 commits into
ManimCommunity:mainfrom
behackl:refactor/output-session-config

Conversation

@behackl

@behackl behackl commented Aug 26, 2026

Copy link
Copy Markdown
Member

Summary

This PR replaces Manim's overlapping output flags with one canonical format
setting and resolves output and presentation intent once when a Scene is
constructed.

The resolved state consists of:

  • an immutable OutputSpec describing the primary artifact;
  • an immutable PresentationSpec separating post-render preview from live display;
  • a RenderSessionSpec combining both with preserved dry_run intent; and
  • renderer capabilities used to validate requests such as live preview.

The resolved output is passed explicitly to SceneFileWriter; the writer no longer
reinterprets global output configuration. Manager exposes the captured session and
performs post-render presentation from it.

User-facing output model

--format selects one primary artifact:

Value Meaning
auto Default. Produces MP4 for opaque output and MOV for transparent output. If the scene has no play or wait calls, Manim saves a PNG instead and logs a warning. With live preview, it produces no file unless a concrete format is requested.
none Evaluate the scene without producing a primary media artifact.
mp4, mov, webm, gif Produce the selected time-based artifact. An explicit video request requires at least one play or wait call.
png Fast-forward animations and save only the last frame. Equivalent to -s / --save_last_frame.
png-sequence Evaluate the full frame progression and save every frame as a numbered PNG.

Presentation is independent of artifact selection:

  • -p / --preview opens the completed artifact for either renderer.
  • -l / --live-preview requests renderer-provided live display. OpenGL supports
    it; Cairo rejects it.
  • Live preview with format=auto is display-only. Supply a concrete video format to
    display and record simultaneously.
  • --show_in_file_browser reveals the completed artifact and can be combined with
    --preview.

dry_run suppresses output for the resolved session without rewriting the configured
format or neighboring settings. The session retains dry_run=True so later execution
coordination can distinguish it from another artifact-less render.

Breaking changes and migration

Most scenes rendered with Manim's default settings do not require changes. Scripts,
configuration files, and integrations that select output through the older boolean
options must migrate to the canonical format setting.

Common replacements

Previous interface Replacement
--write_to_movie, [CLI] write_to_movie, or config.write_to_movie = True Use the default format=auto, or select mp4, mov, webm, or gif explicitly.
[CLI] write_to_movie = false or config.write_to_movie = False Use format=none.
-g, --save_pngs, [CLI] save_pngs, or config.save_pngs Use --format=png-sequence or format=png-sequence.
-i, --save_as_gif, [CLI] save_as_gif, or config.save_as_gif Use --format=gif or format=gif.
[CLI] save_last_frame Use format=png. The -s flag and programmatic config.save_last_frame alias remain available.
--force_window, [CLI] force_window, or config.force_window Use -l / --live-preview. Add a concrete video format to record the live render as well.
Deprecated -f Use --show_in_file_browser.
config.movie_file_extension or config.resolve_movie_file_extension() Select the desired artifact with config.format; extensions are derived from the resolved output.

For example:

# Before
config.save_pngs = True

# After
config.format = "png-sequence"

Output behavior changes

  • --format=png now fast-forwards animations and saves only the last frame, like
    -s. Use --format=png-sequence to save every rendered frame.
  • PNG sequences are stored in a scene-specific directory such as
    media/images/<module>/<Scene>/0000.png.
  • With format=auto, a scene without play or wait calls produces a PNG and logs
    a warning. An explicitly requested video format fails instead; use format=png
    if a still image is intended.
  • OpenGL -p now opens the completed artifact after rendering, matching Cairo. Use
    -l --renderer=opengl for a live window.
  • Selecting the OpenGL renderer no longer disables automatic file output. Headless
    OpenGL with format=auto writes MP4 or MOV; live preview with format=auto remains
    display-only.
  • --preview and --show_in_file_browser can be combined. Both require a produced
    artifact.
  • Transparent MP4 output is rejected. Use format=auto, mov, or webm.
  • Section output requires a video format.
  • None and an empty format value normalize to auto.
  • dry_run no longer mutates format, write_all, or still-output settings.
  • Output and presentation settings are captured when the Scene is constructed.
    Construct the scene inside the intended tempconfig context. Unrelated temporary
    scene configuration, such as changing disable_caching around an individual
    play, remains available.
  • Omitted CLI options no longer overwrite corresponding configuration-file values.
    In particular, format is now loaded from configuration files.
  • Rendering no longer replaces config.output_file with the completed artifact
    path. Use scene.renderer.file_writer.final_file_path to find the produced file
    or PNG-sequence directory.
  • config.preview now exclusively means opening completed output.
    config.live_preview requests live display; the existing enable_gui option is
    also interpreted as a live-preview request.

Python integrations

The format helpers is_mp4_format, is_gif_format, is_png_format,
is_webm_format, is_mov_format, and write_to_movie have been removed from
manim.utils.file_ops. Code that decides output behavior should inspect the resolved
OutputSpec.

Custom renderers and direct SceneFileWriter users must also:

  • declare capabilities: RendererCapabilities;
  • change renderer.init_scene(scene) to
    renderer.init_scene(scene, session_spec);
  • pass the resolved session to OpenGLRenderer.should_create_window();
  • pass an explicit output_spec when constructing SceneFileWriter and accept it in
    injected custom writer classes; and
  • pass explicit preview= and show_in_file_browser= arguments to
    open_media_file().

Resolved intent is available through Scene.session_spec, Manager.session_spec,
Manager.output_spec, and SceneFileWriter.output_spec.

Out of scope / follow-up work

This PR does not yet:

  • implement Manager-owned exact no-raster evaluation for dry runs;
  • move play, section, audio, segment, or artifact ownership fully into Manager;
  • decompose SceneFileWriter into encoding, timeline, and assembly services;
  • add configurable encoder profiles or include them in cache fingerprints;
  • redesign the broader media directory layout or remove custom_folders; or
  • change the current cached-video-segment strategy.

Those remain stacked follow-ups after the session intent and ownership boundary.

(And just to be explicit about this: I've been working a bunch with GPT-5.6/Sol to put this together; everything in here has been reviewed at least coarsely by me though; critical parts and the general design shape were hand-crafted.)

@behackl behackl added refactor Refactor or redesign of existing code breaking changes This PR introduces breaking changes labels Aug 26, 2026
@behackl
behackl marked this pull request as ready for review August 27, 2026 16:21
@nikolajmunk

Copy link
Copy Markdown
Contributor

I'm hoping to take a closer look at this over the weekend, but here are some immediate thoughts. I'm not very familiar with the details of the current config-render-scene pipeline, so perhaps these are all obvious or irrelevant!

  • I really like the very explicit config model. IMO, Manim should do as little magic behind the scenes as possible, so this works well.
  • If I'm understanding you correctly, a render session is initially built from the provided config options. This session object is passed to the scene that is to be rendered, which then builds a session spec and uses that to provide output somehow. How does this work for rendering multiple scenes?
  • Something feels a bit off wrt the relationship between a scene and its config. Some things are obviously necessary for the scene to know what do to (camera size, frame rate, LaTeX templates, etc.). But does a scene really need to know things like output format or whether the file browser will be opened at the end? Those feel like things that should be owned by the renderer or a manager.

Exciting stuff so far 👍

@behackl

behackl commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Great questions!

  • As far as the design reaches right now, when rendering multiple scenes each scene would get a separate manager (and as an extension also a separate render session spec). When using the CLI (manim render ...), each call constructs a new manager with an independent spec etc.
  • You are absolutely right: the scene should not own output settings etc. at all! This is just a transitional design and makes sense because we more or less had it like that before -- but in a few PRs, handling these settings will indeed be taken away from the scene and moved to the manager.

@nikolajmunk

nikolajmunk commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Great questions!

  • As far as the design reaches right now, when rendering multiple scenes each scene would get a separate manager (and as an extension also a separate render > session spec). When using the CLI (manim render ...), each call constructs a new manager with an independent spec etc.
  • You are absolutely right: the scene should not own output settings etc. at all! This is just a transitional design and makes sense because we more or less had > it like that before -- but in a few PRs, handling these settings will indeed be taken away from the scene and moved to the manager.

Cool, I thought that might be the case! In my mind, a "session" would be the execution and rendering of all scenes provided by the user, but that's just nomenclature stuff. I'm also thinking about this from a non-CLI perspective (let's say I'm building an editor for Manim which builds its own manager or whatever), but none of this seems to directly preclude doing that, so I'm happy there.

Another off-the-cuff thought before I start looking at the code: Maybe I'm an extreme outlier and my workflow shouldn't weigh too heavily in these considerations, but I actually find it very useful to be able to invoke with tempconfig inside a scene. Here's an example of a dumb thing I'm currently doing to reuse scenes inside another scene:

class CombinedScene(Scene):
    def construct(self):
        square = Square()
        self.play(FadeIn(square))
        self.play(FadeOut(square))
        # this scene is very dense, so turn off caching
        with tempconfig(dict(disable_caching=True)):
            ReusableScene.construct(self)
        self.play(FadeOut(*self.mobjects))

Obviously I'll survive if I have to do something else! There's also this workaround which again isn't strictly necessary, but definitely a nice thing to have at your disposal.
Maybe there's a way to construct a temporary config on top of the current frozen one, so we don't mutate state but rather construct a data structure of stacked configs - sooort of similar to how setdefault works for mobjects and animations.

@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.

I've only taken a cursory glance at the implementation itself so far, but here's a quick pass of documentation. Many of these were pretty LLM-y; in particular, I've noticed that LLMs love to write documentation that explains what was changed rather than what is now true about the code. For example it might write "this function accepts both string and integer input; integers are correctly cast to string and do not raise an error" rather than just "this function accepts string and int input". I've tried to make the wording clearer, more human-sounding and more useful to future users of Manim.

Hope these are of any use!

Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/tutorials/output_and_config.rst Outdated
Comment thread docs/source/tutorials/output_and_config.rst Outdated
Comment thread docs/source/tutorials/output_and_config.rst Outdated
Comment thread manim/_config/utils.py Outdated
Comment thread docs/source/contributing/testing.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Co-authored-by: nikolajmunk <28557236+nikolajmunk@users.noreply.github.com>
@nikolajmunk

Copy link
Copy Markdown
Contributor

OK, as far as I can tell everything looks good on the code side. I have a few comments about config options:

  • Both the implementation and docs emphasize that if a video format is requested but the scene contains no play calls, then the renderer should produce a "useful still image", i.e., render a last-state PNG. Is this really the behavior we'd want?

    Part of the philosophy behind this PR is that a) configs are immutable once the scene begins and b) Manim should very rarely try and "fix" the user's config options, and this behavior is effectively the same as changing the config's output format to PNG. I think that's 100% fine if format = auto, but if the user has explicitly specified e.g. format = mp4, then that feels a little iffy. Here are two alternate options for when format = [video] but there are no play calls:

    1. Assume the user has done this on purpose and output the last (and only) frame of the scene to an mp4 (or whatever format the user requested). I don't know if this is technically possible or why someone would ever wanna do this, but it feels "honest" in the same way the new config design does.
    2. Keep the current behavior and output a PNG instead of a video, but also log a message or warning à la No animation frames were produced in {scene_name}. The output has been saved as an image instead.. Then the user is explicitly informed that we're going against the wishes stated in the config.
  • Currently, preview and show_in_file_browser are mutually exclusive. Is there any reason for this? I could easily see a user wanting both Finder and VLC to open when the render finishes. Order of opening could be determined either by the order the flags are provided, or we could pick one winner (always having the video player/image viewer open above the file system feels right to me).

  • Similarly, I'm not completely convinced that save_last_frame should set format = png. It seems perfectly reasonable for a user to want to render a video and a final-state PNG for, say, thumbnails. I could see it being an execution intent similar to dry-run: the scene behaves as it normally would based on the output format, but the final output is augmented by the execution intent. They also both have a similar canceling-out case: setting dry-run=True, output=none does the "same thing" twice, and save_last_frame=True, output=png does the same thing twice. If this isn't technically feasible, I think the current behavior is fine as well :)

As a side note, here's a possibly overengineered thing I'm wondering: would it perhaps make sense to encode the constraints of the config (e.g. not (transparent is True and format == 'mp4') and not (preview is True and format == 'none')) and store it in a SessionConstraints class or similar? This would provide a single source of truth for validation, and it might make for easy testing since we could exhaustively test that no invalid combinations of options are allowed. This is obviously easy to add later, so no worries if it's out of scope for this PR.

@behackl behackl left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed with all direct suggestions, and attempted to improve wording for all other regions where you left comments as well. Thanks for the detailed look at this!

Comment thread docs/source/contributing/testing.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/tutorials/output_and_config.rst Outdated
Comment thread manim/_config/utils.py Outdated
@behackl

behackl commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

OK, as far as I can tell everything looks good on the code side. I have a few comments about config options:

Thanks for the careful review, much appreciated!

  • Both the implementation and docs emphasize that if a video format is requested but the scene contains no play calls, then the renderer should produce a "useful still image", i.e., render a last-state PNG. Is this really the behavior we'd want?
    Part of the philosophy behind this PR is that a) configs are immutable once the scene begins and b) Manim should very rarely try and "fix" the user's config options, and this behavior is effectively the same as changing the config's output format to PNG. I think that's 100% fine if format = auto, but if the user has explicitly specified e.g. format = mp4, then that feels a little iffy. Here are two alternate options for when format = [video] but there are no play calls:

    1. Assume the user has done this on purpose and output the last (and only) frame of the scene to an mp4 (or whatever format the user requested). I don't know if this is technically possible or why someone would ever wanna do this, but it feels "honest" in the same way the new config design does.
    2. Keep the current behavior and output a PNG instead of a video, but also log a message or warning à la No animation frames were produced in {scene_name}. The output has been saved as an image instead.. Then the user is explicitly informed that we're going against the wishes stated in the config.

Ah, this is a great question! I tend to agree that any "smartness" in the handling of the output format should be restricted to when the output format is set to "auto". The current implementation more or less simply mimics the behavior on the main branch, but I wouldn't mind introducing another breaking change here.

There actually is an argument that could be made for the library always appending a 1-frame long animation at the end (which would resolve the issue reported many times that for a scene just containing a self.play(some_animation, run_time=1) call and nothing afterwards the last frame is not showing correctly, but it would mean that the video length would no longer be the sum of the run times in the scene, but the run time sum plus one frame instead -- but I feel like this is something that we should discuss a bit more broadly, and I don't want to hide something like this in the otherwise already massive PR.

I think what I'd like to do is implement a combination of your two suggestions: only let manim try to be smart about the output format while the value is "auto" (the default), raise an error when a video is requested in a scene without animations, and log a warning when "auto" resolves to an image instead of a video. Thoughts?

  • Currently, preview and show_in_file_browser are mutually exclusive. Is there any reason for this? I could easily see a user wanting both Finder and VLC to open when the render finishes. Order of opening could be determined either by the order the flags are provided, or we could pick one winner (always having the video player/image viewer open above the file system feels right to me).

Are you sure? This might just be explained incorrectly in the docs, the code just has two sequential if branches, not an if/elif.

  • Similarly, I'm not completely convinced that save_last_frame should set format = png. It seems perfectly reasonable for a user to want to render a video and a final-state PNG for, say, thumbnails. I could see it being an execution intent similar to dry-run: the scene behaves as it normally would based on the output format, but the final output is augmented by the execution intent. They also both have a similar canceling-out case: setting dry-run=True, output=none does the "same thing" twice, and save_last_frame=True, output=png does the same thing twice. If this isn't technically feasible, I think the current behavior is fine as well :)

It's an interesting suggestion, and sort of prompts the question whether it should be allowed to specify multiple output formats at once. I tend to agree, but would rather want to implement this as a fancy new feature later, separately from this refactor.

As a side note, here's a possibly overengineered thing I'm wondering: would it perhaps make sense to encode the constraints of the config (e.g. not (transparent is True and format == 'mp4') and not (preview is True and format == 'none')) and store it in a SessionConstraints class or similar? This would provide a single source of truth for validation, and it might make for easy testing since we could exhaustively test that no invalid combinations of options are allowed. This is obviously easy to add later, so no worries if it's out of scope for this PR.

Interesting. I am sort of satisfied with the restrictions being all spelled out in OutputSpec.__post_init__ -- but perhaps something for a later extension of the system when we, say, let users also pass custom encoder config options?

I'll push a commit to change the behavior overriding user intent with the output format when there are no animations, plus fix the wording regarding preview + open in file browser. Thanks again!

@nikolajmunk

Copy link
Copy Markdown
Contributor

I think what I'd like to do is implement a combination of your two suggestions: only let manim try to be smart about the output format while the value is "auto" (the default), raise an error when a video is requested in a scene without animations, and log a warning when "auto" resolves to an image instead of a video. Thoughts?

Yep, that's a very nice intermediary step!

Are you sure? This might just be explained incorrectly in the docs, the code just has two sequential if branches, not an if/elif.

Oop you're right! Both work at the same time on my machine. I had interpreted open_file as setting a variable for later, so I thought the latter in_browser argument would overwrite the former. Then it's just docs that need to be fixed, thanks!

sort of prompts the question whether it should be allowed to specify multiple output formats at once. I tend to agree, but would rather want to implement this as a fancy new feature later, separately from this refactor.

Agreed! I do like the idea of save_last_frame as execution intent along with live-preview and dry-run though. I think that class of behavior is interesting! Might be possible to expose hooks for user-written execution intents down the line.

Interesting. I am sort of satisfied with the restrictions being all spelled out in OutputSpec.post_init -- but perhaps something for a later extension of the system when we, say, let users also pass custom encoder config options?

I was envisioning __post_init__ using the constraint class to perform its validation, but again this is easily doable later.

@behackl

behackl commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Pushed changes as discussed and fixed one test that failed as a consequence. I have also added a couple more cheap tests to make sure the behaviour is exactly what we want for now. (Need a lot of tests for the upcoming file writer decoupling and renderer migrations.)

@behackl

behackl commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

I think the only point I haven't really replied to in your review was about the mid-scene change of config.disable_caching -- this still works as of now, and I think it would be okay to have the manager keep checking the config value before each play call to act accordingly. Other settings -- output format, render options, ... I would rather freeze and discuss rereading them during the render process if there is a compelling argument about it. I feel like this is mostly in line with the current behavior.

I'll update the branch, then get this merged to proceed with the refactor. Thanks again for the feedback! (I've made sure to incorporate the same points to the follow-up PRs as well, especially regarding documentation.)

@behackl behackl changed the title RFC: rework output configuration options and their consumption Rework output configuration options and their consumption Sep 3, 2026
@behackl
behackl merged commit 060e801 into ManimCommunity:main Sep 3, 2026
29 of 30 checks passed
@behackl
behackl deleted the refactor/output-session-config branch September 3, 2026 19:31
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