MakieOrg / Makie.jl

Interactive data visualizations and plotting in Julia
https://docs.makie.org/stable
MIT License
2.42k stars 313 forks source link

Slow code path for `mesh` when `ShaderAbstractions.Buffer` used #2864

Open koehlerson opened 1 year ago

koehlerson commented 1 year ago

I see some overhead in terms of memory and time to plot for a mesh that contains ShaderAbstractions.Buffer

julia> mesh = GeometryBasics.Mesh(collect(plotter.physical_coords),collect(plotter.vis_triangles));

julia> @btime GLMakie.mesh(mesh)
  456.759 ms (33435 allocations: 2.32 MiB)

julia> @btime GLMakie.mesh(plotter.mesh)
  829.511 ms (33447 allocations: 755.98 MiB)

julia> typeof(mesh)
GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, GeometryBasics.Point{3, Float32}}, GeometryBasics.SimpleFaceView{3, Float32, 3, GeometryBasics.OffsetInteger{-1, UInt32}, GeometryBasics.Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}

julia> typeof(plotter.mesh)
GeometryBasics.Mesh{3, Float32, GeometryBasics.Ngon{3, Float32, 3, GeometryBasics.Point{3, Float32}}, GeometryBasics.FaceView{GeometryBasics.Ngon{3, Float32, 3, GeometryBasics.Point{3, Float32}}, GeometryBasics.Point{3, Float32}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}, ShaderAbstractions.Buffer{GeometryBasics.Point{3, Float32}, Vector{GeometryBasics.Point{3, Float32}}}, ShaderAbstractions.Buffer{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}, Vector{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}}}

The collect call is just to convert it from Bufferto the normal triangle type (I'm not sure about the difference between GeometryBasics.SimpleFaceView and GeometryBasics.FaceView

koehlerson commented 1 year ago

MWE

import GeometryBasics
using ShaderAbstractions
using GLMakie
using FileIO
using BenchmarkTools

brain = load(assetpath("brain.stl"))

mesh_brain = @btime mesh(
    brain,
    color = [tri[1][2] for tri in brain for i in 1:3],
    colormap = Reverse(:Spectral),
    figure = (resolution = (1000, 1000),)
)

triangles = Buffer(GeometryBasics.faces(brain))
positions = Buffer(Observable(GeometryBasics.metafree(GeometryBasics.coordinates(brain))))
brain_buf = GeometryBasics.Mesh(positions,triangles)

mesh_brain_buf = @btime mesh(
    brain_buf,
    color = [tri[1][2] for tri in brain for i in 1:3],
    colormap = Reverse(:Spectral),
    figure = (resolution = (1000, 1000),)
)
  11.978 ms (37014 allocations: 3.20 MiB)
  12.345 ms (37020 allocations: 7.23 MiB)
koehlerson commented 1 year ago

This happens only for Buffer(Observabe(Point32[])) for a Vector{Point32} its fine

koehlerson commented 1 year ago

hm I don't understand this. Why should the change from typeof(positions)==Buffer{Point{3,Float32},Vector{Point{3,Float32}} to typeof(positions)==Vector{Point{3,Float32}} be beneficial at all?

import GeometryBasics
using ShaderAbstractions
using GLMakie
using FileIO
using BenchmarkTools

brain = load(assetpath("brain.stl"))

println("benchmark with typeof(positions): $(typeof(brain.position))")
println("benchmark with typeof(simplices): $(typeof(getfield(brain,:simplices)))")
mesh_brain = @btime mesh(
    brain,
    color = [tri[1][2] for tri in brain for i in 1:3],
    colormap = Reverse(:Spectral),
    figure = (resolution = (1000, 1000),)
)
println("")

triangles = Buffer(GeometryBasics.faces(brain))
positions = Buffer(Observable(GeometryBasics.metafree(GeometryBasics.coordinates(brain))))
brain_buf = GeometryBasics.Mesh(positions,triangles)

println("benchmark with typeof(positions): $(typeof(positions))")
println("benchmark with typeof(simplices): $(typeof(triangles))")
mesh_brain_buf = @btime mesh(
    brain_buf,
    color = [tri[1][2] for tri in brain for i in 1:3],
    colormap = Reverse(:Spectral),
    figure = (resolution = (1000, 1000),)
)
println("")

positions_as_vec_without_buf_or_obs = GeometryBasics.metafree(GeometryBasics.coordinates(brain))
brain_buf_without = GeometryBasics.Mesh(positions_as_vec_without_buf_or_obs,triangles)

println("benchmark with typeof(positions): $(typeof(positions_as_vec_without_buf_or_obs))")
println("benchmark with typeof(simplices): $(typeof(triangles))")
mesh_brain_buf = @btime mesh(
    brain_buf_without,
    color = [tri[1][2] for tri in brain for i in 1:3],
    colormap = Reverse(:Spectral),
    figure = (resolution = (1000, 1000),)
)
benchmark with typeof(positions): Vector{Point{3, Float32}}
benchmark with typeof(simplices): GeometryBasics.FaceView{GeometryBasics.TriangleP{3, Float32, GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{GeometryBasics.Vec{3, Float32}}}}, GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{GeometryBasics.Vec{3, Float32}}}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}, StructArrays.StructVector{GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{GeometryBasics.Vec{3, Float32}}}, NamedTuple{(:position, :normals), Tuple{Vector{Point{3, Float32}}, Vector{GeometryBasics.Vec{3, Float32}}}}, Int64}, Vector{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}
  12.015 ms (37014 allocations: 3.20 MiB)

benchmark with typeof(positions): Buffer{Point{3, Float32}, Vector{Point{3, Float32}}}
benchmark with typeof(simplices): Buffer{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}, Vector{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}
  12.358 ms (37020 allocations: 7.23 MiB)

benchmark with typeof(positions): Vector{Point{3, Float32}}
benchmark with typeof(simplices): Buffer{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}, Vector{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}
  11.982 ms (37012 allocations: 3.20 MiB)
koehlerson commented 1 year ago

Note that the benchmarks remain the same for

< positions = Buffer(Observable(GeometryBasics.metafree(GeometryBasics.coordinates(brain))))
---
> positions = Buffer(GeometryBasics.metafree(GeometryBasics.coordinates(brain)))

So the performance hit solely stems from the fact that it's a Buffer of Point and not a Vector of Point

koehlerson commented 1 year ago

Okay, so to summarize the problem and report a possible option for fixing it I start with the stacktrace I looked at by including

        Base.getindex(A::$Typ, idx...) = error("hi")

in ShaderAbstractions/src/types.jl line 63.

From this I got a stacktrace which looked (from highest level to lowest level)

    update_boundingbox!(bb_ref, data_limits(plot)) 
    limits_from_transformed_points(iterate_transformed(plot))
    points = point_iterator(plot)
    point_iterator(plot::Mesh) = point_iterator(plot.mesh[])

Therefore, the computation of data_limits is the bottleneck by iterating somehow expensively over ShaderAbstractions.Buffer.

There are in my opinion two fixes (as far as I understand).

  1. The iterator over ShaderAbstractions.Buffer need to be improved
  2. provide a data_limits dispatch for ::Mesh

I played around with the second and I came up with a small function that produce a mesh plot call that does not allocate

function data_limits(plot::Mesh)
    xyz = plot.mesh[].position
    mini,maxi = extrema(xyz)
    return Rect3f(mini, maxi)
end

with data_limits

julia> begin
           fig = GLMakie.Figure()
           ax = GLMakie.LScene(fig[1, 1]; scenekw=(; limits=GeometryBasics.Rect3f(GeometryBasics.Vec3f(-1), GeometryBasics.Vec3f(2))))
           @time meshplot =GLMakie.mesh!(ax, plotter.mesh)
           fig
       end
  0.093828 seconds (15.10 k allocations: 1.110 MiB)

without it

julia> begin
           fig = GLMakie.Figure()
           ax = GLMakie.LScene(fig[1, 1]; scenekw=(; limits=GeometryBasics.Rect3f(GeometryBasics.Vec3f(-1), GeometryBasics.Vec3f(2))))
           @time meshplot =GLMakie.mesh!(ax, plotter.mesh)
           fig
       end
  0.173314 seconds (15.10 k allocations: 283.734 MiB)

If I drop scenekw with limits then it behaves faulty in terms of performance

julia> begin
           fig = GLMakie.Figure()
           ax = GLMakie.LScene(fig[1, 1])
           @time meshplot =GLMakie.mesh!(ax, plotter.mesh)
           fig
       end
  0.247098 seconds (17.31 k allocations: 1.328 MiB)

and in terms of camera perspective:

faulty

So, my question is: How is data_limits different to the provided scenekw. Does it make at all sense to pursue approach 2 or do I miss something fundamentally due to the lack of my Makie knowledge?

koehlerson commented 1 year ago

Ah nvm, found my problem. Rect3f needs origin and width, but I assumed that it takes two corners. So, I'd have a solution to it and could make a PR for option two, that basically adds

function data_limits(plot::Mesh)
    xyz = plot.mesh[].position
    mini,maxi = extrema(xyz)
    return Rect3f(mini, maxi-mini)
end

which performs without specifying the limits in the following way:

julia> begin
           fig = GLMakie.Figure()
           ax = GLMakie.LScene(fig[1, 1])
           @time meshplot =GLMakie.mesh!(ax, plotter.mesh)
           fig
       end
  0.189127 seconds (15.13 k allocations: 1.111 MiB)
termi-official commented 11 months ago

Btw can this be closed or still a thing?

SimonDanisch commented 11 months ago

I think this is still a thing

ffreyer commented 2 months ago

I don't know what kind of times I should optimally be expecting here. The initial example runs at 3.7ms / 5.6ms for me now, with data_limits/boundingbox using~0.4ms each. A 10 points 3D scatter takes 2.1ms for comparison.