Skip to content

Draw each 2D mesh using a single CairoMeshPattern#5446

Merged
ffreyer merged 20 commits intoMakieOrg:masterfrom
jmert:jw/smaller_svg_meshes
Feb 2, 2026
Merged

Draw each 2D mesh using a single CairoMeshPattern#5446
ffreyer merged 20 commits intoMakieOrg:masterfrom
jmert:jw/smaller_svg_meshes

Conversation

@jmert
Copy link
Contributor

@jmert jmert commented Nov 30, 2025

Disclaimer: Running the test suite locally showed no change across this PR (one local failure), but I don't fully understand the Cairo drawing system. I noticed the opportunity for this change when trying to debug my own SVG problems, which ends up probably being #4155.


It seems each pattern mesh in Cairo can contain multiple patches [1], so move the pattern create and finalize out of the loop over patch elements.

This reduces the PDF and SVG file sizes created by the sample code below. Presumably the savings will be larger for figures with greater number and/or more complex meshes.

File Before After Ratio
svgbug.svg 8168 B 4197 B 0.514
svgbug.pdf 2854 B 2129 B 0.746
function svgbug()
    fig = Figure(size = (50, 50))
    ax = Axis(fig[1, 1])
    hidedecorations!(ax)

    arrows2d!(ax, Point(0.0, 0.0), Point(1.0, 2.0), argmode = :endpoint, color = :firebrick3)

    save("svgbug.svg", fig, backend = CairoMakie)
    save("svgbug.pdf", fig, backend = CairoMakie)
    return fig
end

svgbug();

[1] https://www.cairographics.org/manual/cairo-cairo-pattern-t.html#cairo-pattern-create-mesh
Says: "Additional patches may be added with additional calls to
cairo_mesh_pattern_begin_patch()/cairo_mesh_pattern_end_patch()."


  • Bug fix (non-breaking change which fixes an issue) — no known issue, but it should be a non-breaking change nonetheless

Checklist

  • Added an entry in CHANGELOG.md (for new features and breaking changes)

It seems each pattern mesh in Cairo can contain multiple patches [1],
so move the pattern create and finalize out of the loop over patch
elements.

This reduces the PDF and SVG file sizes created by the sample code
below. Presumably the savings will be larger for figures with greater
number and/or more complex meshes.

|    File      | Before |  After | Ratio |
|:------------:| ------:| ------:|:----- |
| `svgbug.svg` | 8168 B | 4197 B | 0.514 |
| `svgbug.pdf` | 2854 B | 2129 B | 0.746 |

```julia
function svgbug()
    fig = Figure(size = (50, 50))
    ax = Axis(fig[1, 1])
    hidedecorations!(ax)

    arrows2d!(ax, Point(0.0, 0.0), Point(1.0, 2.0), argmode = :endpoint, color = :firebrick3)

    save("svgbug.svg", fig, backend = CairoMakie)
    save("svgbug.pdf", fig, backend = CairoMakie)
    return fig
end

svgbug();
```

[1] https://www.cairographics.org/manual/cairo-cairo-pattern-t.html#cairo-pattern-create-mesh
    Says: "Additional patches may be added with additional calls to
    cairo_mesh_pattern_begin_patch()/cairo_mesh_pattern_end_patch()."

[2] The name for `svgbug()` comes from figuring out a SVG rendering
    problem, eventually tracked down to probably being bug
    MakieOrg#4155
@github-project-automation github-project-automation bot moved this to Work in progress in PR review Nov 30, 2025
@ffreyer
Copy link
Collaborator

ffreyer commented Dec 1, 2025

Did a bit of digging, the pattern = Cairo.CairoPatternMesh() used to be outside the loop and got moved inside in #2459. Maybe we could add a counter to the loop and commit the mesh every so often?

This should also apply to mesh3d btw

@jmert
Copy link
Contributor Author

jmert commented Dec 1, 2025

Good find. I was hesitant to go beyond my already-shaky understanding of Cairo 2D into the 3D case, so the feedback is greatly appreciated. (Soon-ish) I'll take a stab at implementing the simple loop counter idea for both 2D and 3D meshes.

@jmert
Copy link
Contributor Author

jmert commented Dec 5, 2025

The latest commit uses a simple counter to flush the pattern every Nth polygon (semi-arbitrarily chosen as N = 16384).

Using the MWE from #2454, I ran save() for PNG, SVG, and PDF outputs for the "status quo" (git merge-base for this branch) and this branch to compare run times and output file sizes. The results are:

File Type Time File Size
PNG 55s → 44s 412K → 412K
SVG 331s → 144s 4.0MiB → 1.3MiB
PDF 83s → 19s 4.2GiB → 1.2GiB

Master (merge-base, 1163053) → Branch (commit 2, 0786ab0)

I can't actually reproduce the empty PNG case from #2454 on my machine (it renders even if I set N = typemax(Int)), so I haven't thought yet of a way to tune the flushing period — any suggestions are welcome.

@ffreyer
Copy link
Collaborator

ffreyer commented Dec 29, 2025

I've looked into the failing tests and eventually learned that patches within a pattern don't draw over each other. So any transparent 3D mesh will only render one side if it doesn't break out of the pattern. That's why a couple of tests became lighter in color. This often looks more like GLMakie, but is technically just incorrect. This also gets rid of lines where patches meet, which is nice.

I restored the master behavior for now, by flushing the pattern when the face/patch flips between front and back facing. I'm not sure if that's preferable though.

For comparison:

master Screenshot from 2025-12-29 18-35-00
pr before my commits Screenshot from 2025-12-29 18-35-09
pr after my commits Screenshot from 2025-12-29 18-35-30
with sort!(..., by = frontfacing) Screenshot from 2025-12-29 18-35-59
cluster sort Screenshot from 2025-12-29 18-56-47

The sort!(..., by = frontfacing) sorts the zorder of faces before passing them to draw_pattern, so that we just have one chunk of back-facing faces and one chunk of front facing faces. If the mesh doesn't overlap itself (e.g. Rect, Sphere), it renders without lines and with correct transparency. But as soon at multiple layers of front and back facing faces, we get issues with depth ordering.

The cluster sort was an attempt to do a partial sort, where a front (back) facing triangle is only allowed to move through N back (front) facing triangles. I was hoping this would naturally generate clusters of faces that belong to certain parts of the torus, but it doesn't really work.

Anyway, I think those things are over-complicating the pr. I think we should just restore how master renders and leave it at that

@asinghvi17
Copy link
Member

asinghvi17 commented Dec 30, 2025

Flushing the pattern on side switch seems reasonable to me. My main use case is for 2D meshes in a 2D camera anyway, where (if I understand correctly) this should render all mesh elements in a single patch.

@jmert
Copy link
Contributor Author

jmert commented Dec 30, 2025

Thanks for making the improvements!

FYI, I've re-run the benchmark I ran in #5446 (comment) and saw a significant slowdown, catastrophically so for SVG. To isolate which commit impacts the runtime, I ran every commit between 86ecf6b and 581f592:

Commit PNG SVG PDF
86ecf6b 41.8s, 412K 141.8s, 1.3M 16.7s, 1.2G
d951234 40.5s, 412K 141.0s, 1.3M 16.5s, 1.2G
de0d551 41.6s, 412K 142.6s, 1.3M 17.9s, 1.2G
b51e031 43.0s, 412K 140.7s, 1.3M 17.5s, 1.2G
dd95652 75.7s, 412K 3015.8s, 1.3M 59.7s, 2.7G
581f592 68.3s, 412K 2116.4s, 1.3M 52.6s, 2.7G

Now, the benchmark is maybe not that important since it tries to generate a very high resolution vector mesh and (a) fails for SVG and (b) creates huge files for PDF — choosing to set rasterize = 2 on the surface element results instead in:

Commit PNG SVG PDF
581f592 with rasterize = 2 68.7s, 412K 67.6.8s, 378K 68.2s, 410K

In the end, my use case is fixed by #5459 instead of this PR, so I don't have any opinion about whether there's a problem to fixed here or not.

@ffreyer
Copy link
Collaborator

ffreyer commented Dec 30, 2025

Yea I've noticed the slowdown too. I ran the example with 1001 instead of 8001 and got 50s -> 88s. In @profview it's basically all Cairo.paint for pngs and Cairo.finish for svgs. With Cthulhu.@descend I noticed there are a lot of unknown types so I tried optimizing that. Got down to 80s with that, which is still disappointing.

Maybe mixing patterns with few and many patches is just slow? Much slower than either extreme?

The 1001 surface ends up flushing 510 564 times for 1 022 000 faces, with a group of about 50 faces/patches at the start/end and lots of single face/patch groups inbetween. Both never flushing on back/front face switches (like before my commits, ~20s) and always flushing (~50s, matches master) are significantly faster (than the 80s). Filesizes are still better than master though.

I guess we should just revert to master for 3D, or add a check to only use this when the number of flushes remains low

@jmert
Copy link
Contributor Author

jmert commented Dec 30, 2025

Was typing this as I saw the previous comment come in...

I started running the SVG case in a Profile.@profile, but triggering a partial report by sending SIGUSR1 didn't do anything quickly. I took that to imply that most of the time was being spent in libcairo, and attached gdb to it. The stacktrace I saw had over 15,000 recursive calls to

#15778 0x00007fd73f4dadd5 in bbtree_add (bbt=0x1a65665b0, header=0xb51e5380, box=0x7fff872f8170)                                     
    at ../src/cairo-recording-surface.c:238

so it seems the usage pattern is just a catastrophic case for how libcairo implements its SVG rasterization.

@jkrumbiegel
Copy link
Member

jkrumbiegel commented Dec 30, 2025

Maybe it would be much easier to add a basic CPU mesh renderer with a z buffer than using the patch functionality in Cairo which is not actually optimized for triangles and pretty slow and degrades svg files somehow when used.

Claude might be pretty good at transferring functionality from glmakie shaders.

@jmert
Copy link
Contributor Author

jmert commented Dec 30, 2025

An even easier (partial) solution might be to just always set the rasterize property on meshes when exporting to SVG — it doesn't solve the poor performance of the PNG case (which a custom renderer would do), but (a) it avoids the catastrophic case for SVG, and also (b) avoids the problematic SVG forms that create files that Firefox wont render (as I mentioned in #4155 (comment)).

@SimonDanisch
Copy link
Member

Maybe it would be much easier to add a basic CPU mesh renderer with a z buffer

The new Raytracing julia backend could be extended for that use case - i think we could even quite easily teach it rasterization ...
I think we'll be able to do some pretty cool things with it, since we have full control over the whole implementation in Julia.

@asinghvi17
Copy link
Member

I do want CPU based rasterization for Tyler in CairoMakie and GeoMakie meshimage in general, so would be happy to help push that through

@ffreyer ffreyer moved this from Work in progress to Ready to merge in PR review Jan 30, 2026
@ffreyer ffreyer merged commit 750c4fa into MakieOrg:master Feb 2, 2026
22 checks passed
@github-project-automation github-project-automation bot moved this from Ready to merge to Merged in PR review Feb 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Merged

Development

Successfully merging this pull request may close these issues.

5 participants