Skip to content

Rewrite VMobject subpath getters by calculating split indices - #3759

Closed
chopan050 wants to merge 3 commits into
ManimCommunity:mainfrom
chopan050:optimize-subpaths
Closed

chopan050 wants to merge 3 commits into
ManimCommunity:mainfrom
chopan050:optimize-subpaths

Conversation

@chopan050

Copy link
Copy Markdown
Member

Overview: What does this pull request change?

Related PR: #3292

Taking inspiration from ManimGL, I added new methods VMobject.get_subpath_split_indices() and VMobject.get_subpath_split_indices_from_points(), which instead of explicitly obtaining a Python list of subpaths, it obtains an (n_subpaths, 2) ndarray of indices indicating where every subpath starts and ends.

These methods also accept parameters n_dims (which allows us to choose between 2D or 3D point comparison) and strip_null_end_curves (useful in a future PR for VMobject.align_points(), where the null end curves at every subpath must be removed as a fix to certain bug).

Then I rewrote VMobject.get_subpaths_from_points() and VMobject.gen_subpaths_from_points_2d() to use this new method VMobject.get_subpath_split_indices_from_points().

TODO: vectorize VMobject.consider_points_equals() in a subsequent PR for extra optimization in VMobject.get_subpath_split_indices_from_points().

Motivation and Explanation: Why and how do your changes improve the library?

Methods like VMobject.change_anchor_mode() and VMobject.align_points() could benefit from calculating split indices, since they allow for things such as calculating the lengths of the subpaths in a vectorized fashion, which then allows for creating a single NumPy empty array with the right length from the beginning, rather than appending into a NumPy array.

Links to added or changed documentation pages

Further Information and Comments

Reviewer Checklist

  • The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • If applicable: newly added functions and classes are tested

HamdiBarkous added a commit to HamdiBarkous/manim that referenced this pull request Jul 7, 2026
Add VMobject.get_subpath_split_indices_from_points(), a vectorized
computation of the point-index ranges delimiting each subpath, and have
Camera.set_cairo_context_path() call it instead of computing the split
indices inline. Keeps the vectorized comparison (the exact vectorized
form of consider_points_equals_2d), so rendered output is unchanged.
Mirrors the API of the unmerged ManimCommunity#3759. Addresses review feedback on ManimCommunity#4695.
behackl added a commit that referenced this pull request Aug 9, 2026
* Optimize set_cairo_context_path: vectorize subpath splitting and use flat array indexing

Replace Python generators and tuple unpacking with numpy-based subpath
splitting and direct flat-array indexing for bezier point lookups.
Same Cairo calls, same output, ~2-7x faster path building.

- Replace gen_subpaths_from_points_2d generator with vectorized numpy
  boundary detection using np.arange + boolean masking
- Replace gen_cubic_bezier_tuples_from_points generator with direct
  integer-range iteration over pre-flattened xy array
- Eliminate per-curve numpy slice creation (*p[:2] splat)
- Cache method references (ctx.curve_to → local) to avoid attribute
  lookup per call

Benchmarks (1920x1080 @ 60fps):
- set_path: 2-7x faster across scene types
- Overall: up to 1.5x faster on shape/text-heavy scenes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Eliminate redundant numpy copies in camera reset and frame retrieval

- camera.reset(): Replace set_pixel_array() → convert_pixel_array() →
  np.array() (copy) → slice assignment (second copy) with a single
  np.copyto() call. Removes one full-frame copy per frame.
- set_frame_to_background(): Same optimization for static frame restore.
- renderer.get_frame(): Replace np.array() with .copy() — avoids
  dtype inference overhead on an already-typed array.

Benchmarks (1920x1080 @ 60fps):
- camera_reset: 3-10x faster (e.g. 390ms → 120ms on AnimatedTransforms)
- Overall: ~2x faster across scene types when combined with set_path opt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add Lissajous Table benchmark for measuring render performance

Adds benchmarks/bench_lissajous.py — a heavy real-world animation
workload with grid-of-circles updaters tracing Lissajous curves.
Stresses the per-frame render path far more than static gallery
scenes, making rendering optimizations visible end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix mypy union-attr error in Camera.reset

self.background is typed as PixelArray | None (from __init__ param)
but is guaranteed non-None after init_background() runs during
construction. Add an assert to satisfy mypy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix single-curve VMobject skipped in set_cairo_context_path

When a VMobject had exactly nppcc points (one cubic curve, e.g. Line),
np.arange(nppcc, n_pts, nppcc) returned an empty array and the function
exited before drawing. The original code handled this via split_indices
of [0, n_pts], yielding one subpath of all 4 points.

Handle the empty-boundary case explicitly as a single subpath.

Verified pixel-identical vs main across 12 scenes (Line, Dot, Square,
Circle, Arrow, Text, MathTex, Polyline, DashedLine, OpenPath, mixed,
animated).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Accept list-typed VMobject.points in set_cairo_context_path

The vectorized path builder uses numpy fancy indexing (e.g.
points[boundary_indices - 1, :2]), which fails when vmobject.points
is a plain Python list. The documented VMobjectDemo example sets
points this way, which broke the docs build.

np.asarray the points array once on entry; it's a no-op when the
input is already an ndarray.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix redundant copies in set_pixel_array instead of bypassing it

Rework set_pixel_array/convert_pixel_array so the per-frame reset path
does a single in-place copy, then route Camera.reset() and
set_frame_to_background() back through set_pixel_array() rather than
inlining np.copyto. convert_pixel_array() no longer takes
convert_from_floats and only performs the float->RGB conversion; callers
convert only when needed. Addresses review feedback on #4695.

* Move subpath split-index calculation into VMobject

Add VMobject.get_subpath_split_indices_from_points(), a vectorized
computation of the point-index ranges delimiting each subpath, and have
Camera.set_cairo_context_path() call it instead of computing the split
indices inline. Keeps the vectorized comparison (the exact vectorized
form of consider_points_equals_2d), so rendered output is unchanged.
Mirrors the API of the unmerged #3759. Addresses review feedback on #4695.

* Fix VMobjectDemo docs example to keep points as an ndarray

The deep-dive VMobjectDemo example assigned a Python list to
VMobject.points, violating the invariant that points is always a NumPy
array (and breaking the docs build with the vectorized path builder).
Use VMobject.set_points() instead, then drop the defensive np.asarray()
workaround in set_cairo_context_path since points is now always an
ndarray. Addresses review feedback on #4695.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Satisfy ruff lint and format

Add strict=True to zip() in the new subpath test (B905) and collapse
two signatures/calls that fit on one line per ruff format.

* Add type hints to Lissajous benchmark

Type-annotate all functions/methods in bench_lissajous.py per review, and
replace the attribute-tagged plain VMobject paths with a dedicated
LissajousCurve(VMobject) class so the dot/row_circle/column_circle
attributes are properly typed. Also note upstream author's permission in
the module docstring. Addresses review feedback on #4695.

* Match scalar non-finite handling in subpath split indices

get_subpath_split_indices_from_points used the raw tolerance formula,
which diverges from np.isclose/np.allclose on NaN/inf coordinates. This
could yield non-pixel-identical output vs main when ThreeDCamera
projection produces inf/NaN (a point whose rotated z equals the focal
distance). Transcribe the scalar comparisons exactly: the asymmetric
consider_points_equals_2d form for n_dims=2, and np.isclose (allclose
semantics) for n_dims=3. Finite results are unchanged. Thanks @pjfo for
the analysis on #4695.

* Ensure consistent point comparison semantics

* Allow unsafe casting in set_pixel_array's in-place copy

np.copyto defaults to same_kind casting, which rejects the int64 array
np.asarray produces for a plain Python list being written into the
camera's uint8 buffer. The slice assignment this replaced casted
unsafely, so restore that behaviour explicitly.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
@chopan050

Copy link
Copy Markdown
Member Author

Superseded by #4695.

@chopan050 chopan050 closed this Sep 12, 2026
@chopan050
chopan050 deleted the optimize-subpaths branch September 12, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 🆕 New

Development

Successfully merging this pull request may close these issues.

1 participant