Speeding up WebGL program

Hi there,
I wanted to implement the Reaction-Diffusion Algorithm and notices that I can’t get past 4 FPS. (JS implementations have typically a stable 60 FPS)

How it should look like after a couple of seconds:

My Implemenation

So why is Elm/WebGL that slow? How can i improve the calculations?


For some context: I’m working on an Elm package for writing generative Art. The reaction-diffusion algorithm is an essential part of this package. If I find out that it is defacto impossible to pull off a fast Reaction-Diffusion Algorithm in Elm, I can throw my package in the bin.

The example you linked performs very well because it runs the Gray-Scott algorithm on the GPU, rather than the CPU. It uses a feature of WebGL where it renders to a texture. Elm doesn’t expose an API to do this so I don’t think it can be written with pure Elm at this point.

2 Likes

Quick update: I now believe that it has nothing to do with WebGL.

I’ve created a new Ellie.

I’ve replaced the Dict with an Array → im now getting 14 Frames locally, 7 in Ellie.

Also i added a way to skip frames (renderEvery on line 55). If this would be a WebGL issue, then the frames should not change when increasing renderEvery. But in fact when doubling it, the fps drop by half.

Another observation: removing the laplace function gets me up to 60 fps in Ellie. Heres the laplace function:

convolution : List ( ( Int, Int ), Float )
convolution =
    [ [ 0.05, 0.2, 0.05 ]
    , [ 0.2, -1, 0.2 ]
    , [ 0.05, 0.2, 0.05 ]
    ]
        |> List.indexedMap
            (\j list ->
                list
                    |> List.indexedMap (\i factor -> ( ( i, j ), factor ))
            )
        |> List.concat


get : ( Int, Int ) -> Array (Array a) -> Maybe a
get ( i, j ) grid =
    grid
        |> Array.get j
        |> Maybe.andThen (Array.get i)


laplace : (( Float, Float ) -> Float) -> ( Int, Int ) -> Array (Array ( Float, Float )) -> Float
laplace fun ( x, y ) dict =
    convolution
        |> List.map
            (\( ( i, j ), factor ) ->
                (dict
                    |> get ( x - 1 + i, y - 1 + j )
                    |> Maybe.map fun
                    |> Maybe.withDefault (fun ( 1, 0 ))
                )
                    * factor
            )
        |> List.sum

For a given point it sums up the neighbors (using the weights defined in convolution). Every cell has two values, thats why i need to pass fun along, the function is currently called twice per cell: once with fun = Tuple.first and once with fun = Tuple.second. My next experiment will be to get rid of fun. This should theoretically double the fps.

@Lucas_Payr on WebGL performance, read the “Making the most of the GPU” section of the package docs webgl 1.1.3 that is also linked from the Mesh docs.

In the nutshell, WebGL API in Elm is not meant for creating meshes in the view on each frame, because each time such mesh would have to be uploaded to GPU, that is a rather slow operation. The mesh needs to be created and cached in the model or globally, then reused.

Given your use case, I don’t think the current WebGL API works well, because it is not suitable for manipulating individual pixels on the screen.

5 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.