Skip to content

Added opengl support for --format png output #1982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
039bd0d
Add png output support for opengl
Aug 31, 2021
fb2a2f0
Handle when interactive embed should launch
Aug 31, 2021
d6706b1
Handle when window should be created
Aug 31, 2021
7ac0c1f
Remove unused import
Aug 31, 2021
84746fe
Remove confusing field
Aug 31, 2021
50a2e12
Merge branch 'main' into add_png_opengl_support
k4pran Sep 1, 2021
e44ed7e
Resolve conflicts
Sep 4, 2021
10c0309
Merge branch 'add_png_opengl_support' of https://github.com/ryanmccau…
Sep 4, 2021
af75283
Avoid creating window when any format is set
Sep 4, 2021
787d891
Revert removing manim import
Sep 4, 2021
0b95889
Remove second log for interactive_embed
Sep 4, 2021
4f9d87f
Add force_window flag
Sep 5, 2021
604ff11
temp skip tests due to opengl context issue
Sep 6, 2021
1a4636e
Fix context issue
Sep 6, 2021
7f8a183
Merge branch 'main' of https://github.com/ManimCommunity/manim into a…
Sep 8, 2021
dad9f8f
Try manually closing window in opengl tests
Sep 8, 2021
a76c323
Test disabling parallel runs
Sep 11, 2021
4db3f04
Test disabling parallel runs
Sep 11, 2021
60d2878
Try clearing frame buffer on finish
Sep 11, 2021
7d1cfc6
revert clear
Sep 11, 2021
d1c3544
Skip tests that pass locally but fail windows CI
Sep 12, 2021
6046258
Reenable opengl tests
Sep 13, 2021
b87f966
Remove stdin closed check
Sep 18, 2021
d042086
Merge branch 'main' of https://github.com/ManimCommunity/manim into a…
Sep 19, 2021
657f2e4
Skip flakey test
Sep 21, 2021
6f12024
Merge branch 'main' into add_png_opengl_support
hydrobeam Sep 22, 2021
49c5948
Merge branch 'main' of https://github.com/ManimCommunity/manim into a…
Sep 22, 2021
20ff11a
Stop window creation when save_last_frame is set
Sep 22, 2021
cf2eb9b
Merge branch 'main' into add_png_opengl_support
hydrobeam Sep 26, 2021
4134ae4
Merge branch 'main' into add_png_opengl_support
k4pran Sep 28, 2021
5b5284e
Update config path
Sep 28, 2021
eff59ff
Merge branch 'main' into add_png_opengl_support
k4pran Sep 28, 2021
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
3 changes: 2 additions & 1 deletion docs/source/tutorials/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ A list of all config options
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
'upto_animation_number', 'use_opengl_renderer', 'use_webgl_renderer',
'verbosity', 'video_dir', 'webgl_renderer_path', 'window_position',
'window_monitor', 'window_size', 'write_all', 'write_to_movie', 'enable_wireframe']
'window_monitor', 'window_size', 'write_all', 'write_to_movie', 'enable_wireframe',
'force_window']


A list of all CLI flags
Expand Down
3 changes: 3 additions & 0 deletions manim/_config/default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ window_size = default
# --window_monitor
window_monitor = 0

# --force_window
force_window = False

# --use_projection_fill_shaders
use_projection_fill_shaders = False

Expand Down
15 changes: 11 additions & 4 deletions manim/_config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ class MyScene(Scene):
"write_all",
"write_to_movie",
"zero_pad",
"force_window",
}

def __init__(self) -> None:
Expand Down Expand Up @@ -541,6 +542,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig":
"use_projection_fill_shaders",
"use_projection_stroke_shaders",
"enable_wireframe",
"force_window",
]:
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))

Expand Down Expand Up @@ -690,6 +692,7 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig":
"use_projection_stroke_shaders",
"zero_pad",
"enable_wireframe",
"force_window",
]:
if hasattr(args, key):
attr = getattr(args, key)
Expand Down Expand Up @@ -891,6 +894,12 @@ def log_to_file(self, val: str) -> None:
doc="Enable wireframe debugging mode in opengl.",
)

force_window = property(
lambda self: self._d["force_window"],
lambda self, val: self._set_boolean("force_window", val),
doc="Set to force window when using the opengl renderer",
)

@property
def verbosity(self):
"""Logger verbosity; "DEBUG", "INFO", "WARNING", "ERROR", or "CRITICAL" (-v)."""
Expand Down Expand Up @@ -1121,8 +1130,7 @@ def dry_run(self):
self.write_to_movie is False
and self.write_all is False
and self.save_last_frame is False
and self.save_pngs is False
and self.save_as_gif is False
and not self.format
)

@dry_run.setter
Expand All @@ -1131,8 +1139,7 @@ def dry_run(self, val: bool) -> None:
self.write_to_movie = False
self.write_all = False
self.save_last_frame = False
self.save_pngs = False
self.save_as_gif = False
self.format = None
else:
raise ValueError(
"It is unclear what it means to set dry_run to "
Expand Down
6 changes: 6 additions & 0 deletions manim/cli/render/global_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,10 @@ def validate_gui_location(ctx, param, value):
help="Enable wireframe debugging mode in opengl.",
default=None,
),
option(
"--force_window",
is_flag=True,
help="Force window to open when using the opengl renderer, intended for debugging as it may impact performance",
default=False,
),
)
30 changes: 24 additions & 6 deletions manim/renderer/opengl_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import numpy as np
from PIL import Image

from manim import config
from manim import config, logger
from manim.renderer.cairo_renderer import handle_play_like_call
from manim.utils.caching import handle_caching_play
from manim.utils.color import color_to_rgba
Expand Down Expand Up @@ -242,7 +242,7 @@ def init_scene(self, scene):
)
self.scene = scene
if not hasattr(self, "window"):
if config["preview"]:
if self.should_create_window():
from .opengl_renderer_window import Window

self.window = Window(self)
Expand All @@ -268,6 +268,20 @@ def init_scene(self, scene):
moderngl.ONE,
)

def should_create_window(self):
if config["force_window"]:
logger.warning(
"'--force_window' is enabled, this is intended for debugging purposes "
"and may impact performance if used when outputting files",
)
return True
return (
config["preview"]
and not config["save_last_frame"]
and not config["format"]
and not config["write_to_movie"]
)

def get_pixel_shape(self):
if hasattr(self, "frame_buffer_object"):
return self.frame_buffer_object.viewport[2:4]
Expand Down Expand Up @@ -401,8 +415,7 @@ def render(self, scene, frame_offset, moving_mobjects):
if self.skip_animations:
return

if config["write_to_movie"]:
self.file_writer.write_frame(self)
self.file_writer.write_frame(self)

if self.window is not None:
self.window.swap_buffers()
Expand All @@ -426,7 +439,11 @@ def update_frame(self, scene):
self.animation_elapsed_time = time.time() - self.animation_start_time

def scene_finished(self, scene):
if config["save_last_frame"]:
# When num_plays is 0, no images have been output, so output a single
# image in this case
if config["save_last_frame"] or (
config["format"] == "png" and self.num_plays == 0
):
self.update_frame(scene)
self.file_writer.save_final_image(self.get_image())
self.file_writer.finish()
Expand All @@ -444,10 +461,11 @@ def get_image(self) -> Image.Image:
PIL.Image
The PIL image of the array.
"""
raw_buffer_data = self.get_raw_frame_buffer_object_data()
image = Image.frombytes(
"RGBA",
self.get_pixel_shape(),
self.context.fbo.read(self.get_pixel_shape(), components=4),
raw_buffer_data,
"raw",
"RGBA",
0,
Expand Down
5 changes: 4 additions & 1 deletion manim/renderer/shader.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,10 @@ def __init__(
self.name = name

# See if the program is cached.
if self.name in shader_program_cache:
if (
self.name in shader_program_cache
and shader_program_cache[self.name].ctx == self.context
):
self.shader_program = shader_program_cache[self.name]
elif source is not None:
# Generate the shader from inline code if it was passed.
Expand Down
26 changes: 23 additions & 3 deletions manim/scene/scene.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Basic canvas for animations."""


__all__ = ["Scene"]


import copy
import inspect
import platform
Expand Down Expand Up @@ -1006,11 +1004,33 @@ def play_internal(self, skip_rendering=False):
# Closing the progress bar at the end of the play.
self.time_progression.close()

def check_interactive_embed_is_valid(self):
if config["force_window"]:
return True
if self.skip_animation_preview:
logger.warning(
"Disabling interactive embed as 'skip_animation_preview' is enabled",
)
return False
elif config["write_to_movie"]:
logger.warning("Disabling interactive embed as 'write_to_movie' is enabled")
return False
elif config["format"]:
logger.warning(
"Disabling interactive embed as '--format' is set as "
+ config["format"],
)
return False
elif not self.renderer.window:
logger.warning("Disabling interactive embed as no window was created")
return False
return True

def interactive_embed(self):
"""
Like embed(), but allows for screen interaction.
"""
if self.skip_animation_preview or config["write_to_movie"]:
if not self.check_interactive_embed_is_valid():
return

def ipython(shell, namespace):
Expand Down
46 changes: 32 additions & 14 deletions manim/scene/scene_file_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,25 +286,43 @@ def write_frame(self, frame_or_renderer):
Pixel array of the frame.
"""
if config.renderer == "opengl":
renderer = frame_or_renderer
self.writing_process.stdin.write(
renderer.get_raw_frame_buffer_object_data(),
)
self.write_opengl_frame(frame_or_renderer)
else:
frame = frame_or_renderer
if write_to_movie():
self.writing_process.stdin.write(frame.tobytes())
if is_png_format() and not config["dry_run"]:
target_dir, extension = os.path.splitext(self.image_file_path)
if config["zero_pad"]:
Image.fromarray(frame).save(
f"{target_dir}{str(self.frame_count).zfill(config['zero_pad'])}{extension}",
)
else:
Image.fromarray(frame).save(
f"{target_dir}{self.frame_count}{extension}",
)
self.frame_count += 1
self.output_image_from_array(frame)

def write_opengl_frame(self, renderer):
if write_to_movie():
self.writing_process.stdin.write(
renderer.get_raw_frame_buffer_object_data(),
)
elif is_png_format() and not config["dry_run"]:
target_dir, extension = os.path.splitext(self.image_file_path)
self.output_image(
renderer.get_image(),
target_dir,
extension,
config["zero_pad"],
)

def output_image_from_array(self, frame_data):
target_dir, extension = os.path.splitext(self.image_file_path)
self.output_image(
Image.fromarray(frame_data),
target_dir,
extension,
config["zero_pad"],
)

def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
if zero_pad:
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
else:
image.save(f"{target_dir}{self.frame_count}{ext}")
self.frame_count += 1

def save_final_image(self, image):
"""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ markers = "platform_python_implementation == 'CPython'"

[tool.pytest.ini_options]
markers = "slow: Mark the test as slow. Can be skipped with --skip_slow"
addopts = "--no-cov-on-fail --cov=manim --cov-report xml --cov-report term -n auto"
addopts = "--no-cov-on-fail --cov=manim --cov-report xml --cov-report term -n auto --dist=loadfile"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@naveen521kk I added this argument as it seems like it would allow parallelization but, for tests in the same file they would share a worker to avoid issues with multiple opengl windows running at the same time. Does this make sense to you? got the information here - https://stackoverflow.com/questions/4637036/is-there-a-way-to-control-how-pytest-xdist-runs-tests-in-parallel

Copy link
Member

Choose a reason for hiding this comment

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

Looks good


[tool.isort]
# from https://black.readthedocs.io/en/stable/compatible_configs.html
Expand Down
8 changes: 2 additions & 6 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,19 +141,15 @@ def test_temporary_dry_run():

def test_dry_run_with_png_format():
"""Test that there are no exceptions when running a png without output"""
with tempconfig(
{"write_to_movie": False, "disable_caching": True, "format": "png"},
):
with tempconfig({"write_to_movie": False, "disable_caching": True}):
assert config["dry_run"] is True
scene = MyScene()
scene.render()


def test_dry_run_with_png_format_skipped_animations():
"""Test that there are no exceptions when running a png without output and skipped animations"""
with tempconfig(
{"write_to_movie": False, "disable_caching": True, "format": "png"},
):
with tempconfig({"write_to_movie": False, "disable_caching": True}):
assert config["dry_run"] is True
scene = MyScene(skip_animations=True)
scene.render()
23 changes: 23 additions & 0 deletions tests/test_scene_rendering/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ def using_temp_config(tmpdir):
yield


@pytest.fixture
def using_temp_opengl_config(tmpdir):
"""Standard fixture that makes tests use a standard_config.cfg with a temp dir."""
with tempconfig(
config.digest_file(Path(__file__).parent.parent / "standard_config.cfg"),
):
config.media_dir = tmpdir
config.renderer = "opengl"
yield


@pytest.fixture
def disabling_caching():
with tempconfig({"disable_caching": True}):
Expand All @@ -34,3 +45,15 @@ def disabling_caching():
@pytest.fixture
def infallible_scenes_path():
return str(Path(__file__).parent / "infallible_scenes.py")


@pytest.fixture
def force_window_config_write_to_movie():
with tempconfig({"force_window": True, "write_to_movie": True}):
yield


@pytest.fixture
def force_window_config_pngs():
with tempconfig({"force_window": True, "format": "png"}):
yield
Loading