diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d81971b3a7..c47847b418 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,8 @@ jobs: runs-on: ${{ matrix.os }} env: POETRY_VIRTUALENVS_CREATE: false + DISPLAY: :0 + PYTEST_ADDOPTS: "--color=yes" # colors in pytest strategy: fail-fast: false matrix: @@ -37,8 +39,10 @@ jobs: run: | sudo apt update sudo apt install -y ffmpeg - sudo apt-get -y install texlive texlive-latex-extra texlive-fonts-extra texlive-latex-recommended texlive-science texlive-fonts-extra tipa python-opengl libpango1.0-dev + sudo apt-get -y install texlive texlive-latex-extra texlive-fonts-extra texlive-latex-recommended texlive-science texlive-fonts-extra tipa python-opengl libpango1.0-dev xvfb echo "$HOME/.poetry/bin" >> $GITHUB_PATH + # start xvfb in the background + sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 & - name: Install system dependencies (MacOS) if: runner.os == 'macOS' @@ -77,6 +81,16 @@ jobs: path: ${{ github.workspace }}\ManimCache key: ${{ runner.os }}-dependencies-ffmpeg-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.pip-cache-and-time.outputs.date }} + - name: Setup MSYS2 + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + release: false + msystem: MINGW64 + path-type: inherit + install: >- + mingw-w64-x86_64-mesa + - name: Download system dependencies (Windows) if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true' run: | @@ -105,10 +119,13 @@ jobs: - name: Add Windows dependecies to path if: runner.os == 'Windows' run: | + Remove-Item C:\msys64\mingw64\bin\python.exe -Force + Remove-Item C:\msys64\mingw64\bin\python3.exe -Force $env:Path += ";" + "$($PWD)\ManimCache\FFmpeg\bin" $env:Path += ";" + "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\win32" $env:Path += ";" + "$($PWD)\ManimCache\Pango\pango" $env:Path = "$env:USERPROFILE\.poetry\bin;$($env:PATH)" + $env:PATH = "C:\msys64\mingw64\bin;$($env:PATH)" echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Install manim diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index bbdcf2fb09..100fb59465 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -440,6 +440,13 @@ def get_raw_frame_buffer_object_data(self, dtype="f1"): ) return ret + def get_frame(self): + # get current pixel values as numpy data in order to test output + raw = self.get_raw_frame_buffer_object_data(dtype="f1") + result_dimensions = (config["pixel_height"], config["pixel_width"], 4) + np_buf = np.frombuffer(raw, dtype="uint8").reshape(result_dimensions) + return np_buf + # Returns offset from the bottom left corner in pixels. def pixel_coords_to_space_coords(self, px, py, relative=False): pw, ph = config["pixel_width"], config["pixel_height"] diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb2..27e33d1ad0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +import os + +if os.getenv("CI") and os.name == "nt": + location = r"C:\msys64\mingw64\bin" + os.environ["PATH"] = location + os.pathsep + os.getenv("PATH") + import ctypes + + ctypes.CDLL(r"C:\msys64\mingw64\bin\OPENGL32.dll") + if hasattr(os, "add_dll_directory"): + os.add_dll_directory(location) diff --git a/tests/control_data/graphical_units_data/opengl/CircleTest.npz b/tests/control_data/graphical_units_data/opengl/CircleTest.npz new file mode 100644 index 0000000000..8754ab2afd Binary files /dev/null and b/tests/control_data/graphical_units_data/opengl/CircleTest.npz differ diff --git a/tests/test_graphical_units/test_opengl.py b/tests/test_graphical_units/test_opengl.py new file mode 100644 index 0000000000..362e3e2d05 --- /dev/null +++ b/tests/test_graphical_units/test_opengl.py @@ -0,0 +1,25 @@ +import pytest + +from manim import * +from manim.opengl import * +from ..utils.testing_utils import get_scenes_to_test +from ..utils.GraphicalUnitTester import GraphicalUnitTester + + +class CircleTest(Scene): + def construct(self): + circle = OpenGLCircle().set_color(RED) + self.add(circle) + self.wait() + + +MODULE_NAME = "opengl" + + +@pytest.mark.parametrize("scene_to_test", get_scenes_to_test(__name__), indirect=False) +def test_scene(scene_to_test, tmpdir, show_diff): + with tempconfig({"use_opengl_renderer": True}): + # allow 1/255 RGB value differences with opengl tests because of differences across platforms + GraphicalUnitTester(scene_to_test[1], MODULE_NAME, tmpdir, rgb_atol=1.01).test( + show_diff=show_diff + ) diff --git a/tests/utils/GraphicalUnitTester.py b/tests/utils/GraphicalUnitTester.py index 5dd9e6ff43..b4cdad99a4 100644 --- a/tests/utils/GraphicalUnitTester.py +++ b/tests/utils/GraphicalUnitTester.py @@ -3,6 +3,7 @@ import numpy as np from manim import config, tempconfig +from manim.renderer.opengl_renderer import OpenGLRenderer class GraphicalUnitTester: @@ -10,7 +11,7 @@ class GraphicalUnitTester: Parameters ---------- - scene_object : :class:`~.Scene` + scene_class : :class:`~.Scene` The scene to be tested config_scene : :class:`dict` The configuration of the scene @@ -27,12 +28,7 @@ class GraphicalUnitTester: The scene tested """ - def __init__( - self, - scene_object, - module_tested, - tmpdir, - ): + def __init__(self, scene_class, module_tested, tmpdir, rgb_atol=0): # Disable the the logs, (--quiet is broken) TODO logging.disable(logging.CRITICAL) tests_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -41,11 +37,12 @@ def __init__( "test_graphical_units", "tests_cache", module_tested, - scene_object.__name__, + scene_class.__name__, ) self.path_control_data = os.path.join( tests_directory, "control_data", "graphical_units_data", module_tested ) + self.rgb_atol = rgb_atol # IMPORTANT NOTE : The graphical units tests don't use for now any # custom manim.cfg, since it is impossible to manually select a @@ -64,7 +61,10 @@ def __init__( os.makedirs(dir_temp) with tempconfig({"dry_run": True}): - self.scene = scene_object(skip_animations=True) + if config["use_opengl_renderer"]: + self.scene = scene_class(renderer=OpenGLRenderer()) + else: + self.scene = scene_class(skip_animations=True) self.scene.render() def _load_data(self): @@ -126,15 +126,17 @@ def test(self, show_diff=False): + f"\nframe_data.shape = {frame_data.shape}" ) - test_result = np.array_equal(frame_data, expected_frame_data) - if not test_result: - incorrect_indices = np.argwhere(frame_data != expected_frame_data) + mismatches = np.logical_not( + np.isclose(frame_data, expected_frame_data, atol=self.rgb_atol, rtol=0) + ) + if mismatches.any(): + incorrect_indices = np.argwhere(mismatches) first_incorrect_index = incorrect_indices[0][:2] first_incorrect_point = frame_data[tuple(first_incorrect_index)] expected_point = expected_frame_data[tuple(first_incorrect_index)] if show_diff: self._show_diff_helper(frame_data, expected_frame_data) - assert test_result, ( + assert not mismatches.any(), ( f"The frames don't match. {str(self.scene).replace('Test', '')} has been modified." + "\nPlease ignore if it was intended." + f"\nFirst unmatched index is at {first_incorrect_index}: {first_incorrect_point} != {expected_point}"