GLMakie & Observables: Cannot update screen when observable holds reference to struct field

I’m working on a simulation and was using QML before, but since it is not well maintained, I was looking into using GLMakie.

I have this code snippet where I update data in a matrix from a thread, and visualize it using heatmap from GLMakie. The updates are too fast to call a screen update every time the data (in the snippet just a while loop generating a random matrix), so I’m updating the screen on a timer with 60fps. The following code works fine:

using GLMakie, Observables

function update(obs)
    obs[] = obs[]
    return
end

const m = rand(500,500)
const obm = Observable(m)

f = Figure()
ax = Axis(f[1,1])
scene = heatmap!(ax, obm, colormap = :thermal)

display(f)

tm = Timer((timer) -> update(obm), 0., interval = 1/60)

Threads.@spawn while true
    m .= rand(500,500)
    GC.safepoint()
end

However, when I have data in a struct, suddenly the screen doesn’t update anymore when assigning the observable. The following code displays a static image, even though the data in the Observable is being updated:

using GLMakie, Observables

struct Container
    m::Matrix{Float32}
end

function update(obs)
    obs[] = obs[]
    return
end

const c = Container(rand(500,500))
const obc = Observable(c.m)

f = Figure()
ax = Axis(f[1,1])
scene = heatmap!(ax, obc, colormap = :thermal)

display(f)

tm = Timer((timer) -> update(obc), 0., interval = 1/60)

Threads.@spawn while true
    c.m .= rand(500,500)
    GC.safepoint()
end

I don’t see why this shouldn’t work, can anyone explain to me?

You’re making an observable that stores the same array as your container in it. So mutations using the container field will affect the observable as well, because the content object is one and the same. However, the mutation of the inner array is nothing that the observable knows about. So once you’ve mutated you have to call notify(observable) to signal the change to all listeners.

I think he does that with the Timer!
Could just be notify(obc) though :wink:
Either I’m missing something here, or it’s a genuine bug!
But I can’t actually reproduce it with neither GLMakie@0.8.6 nor master (pretty close to 0.8.7)…
Maybe there’s something going on with the array in the container?
At least this works perfectly fine:

using GLMakie
cm = rand(500, 500)
const obc = Observable(cm)

f = Figure()
ax = Axis(f[1,1])
scene = heatmap!(ax, obc, colormap = :thermal)

display(f)

tm = Timer((timer) -> notify(obc), 0.0, interval=1 / 10)

Threads.@spawn while true
    cm .= rand(500, 500)
    GC.safepoint()
end

Btw, image! should work perfectly fine as a drop in replacement for this example and should run a lot faster! :wink:

Ah another thing: if you start julia with only one thread, this will hang and never yield to the rendering thread.
So, this could also be a problem you may run into!

Thanks for the replies. I’m aware I the observable doesn’t know when the data is mutated, which is why I defined the update function, I didn’t know about the notify function.

I tried this as well now and indeed it seems to be a problem with 0.8.7. I don’t understand how this can happen thought, because I thought the Observable shouldn’t really be aware where the vector comes from, so how can this have an effect on how the updates are pushed?

I can’t reproduce it with 0.8.7!
Are you sure you’re running exactly the same thing on the different versions?]st

Yep, I’m running exactly the same thing. Just removing GLMakie and adding 0.8.6 makes the screen interactive again. Also, I’m running Julia with 8 threads. Going back and forth between 0.8.6 and 0.8.7 the behavior is consistent; it always works with 0.8.6. I’m having another problem that prevents me from using 0.8.6, though which seems to be fixed with 0.8.7 (I’m using a reshape of a view of an Vector, which gives an error when running the Texture function on 0.8.6).

what do you mean by the window being interactive? does it freeze on 0.8.7, or just not update the image but you can zoom etc?

Sorry I meant it doesn’t update. I can interact with it in that way, e.g. zooming moving the image etc. I just tried the master branch and there I’m observing the same problem.

Here is a video showing what I mean.

I can’t see the video…
But seems like this is an m1 issue somehow…
Could you try to reduce the issue to something smaller?
E.g. switch out timer with @async while true or things like that :wink:
Could also check if obs updates on(x-> println("updating"), obs), or if it somethings deeper in the stack... Would also be nice to check if the intensityobservable here actually updates: https://github.com/MakieOrg/Makie.jl/blob/master/GLMakie/src/drawing_primitives.jl#L448 Also, did you try usingimage!` instead? That could help pin down the problem as well!

Actually, I’m currently on Windows. Also the whole while loop on a thread and timer where similar to how I would setup my program, but not necessary to show the problem. This is sufficient:

using GLMakie

struct Container
    m::Matrix{Float32}
end

const c = Container(rand(500,500))
const obc = Observable(c.m)

on(obc) do m
    println("Observable changed")
end


f = Figure()
ax = Axis(f[1,1])
scene = image!(ax, obc, colormap = :thermal)
GLMakie.activate!(inline=false)

display(f)

c.m .= rand(500,500)
notify(obc)

When calling notify, the line is printed but the screen is not updated. The same problem is also observed with image.

How do I check wether the intensity observable is being updated?

Still my question remains, how can this problem even exist? I’m really confused, because as far as the observable and everything in Makie are concerned, they’re just being passed a regular old vector, right?

If I could reproduce it, I’d say it’s related to the observable feature of ignore_equal_values which will skip updates if you mutate in place… but it should then just always fail, unless we somehow managed to run slightly different code in the end. I added it in a few places which should only be internal, so user facing mutation should still work, but maybe I didn’t do it carefully enough.
Otherwise I’m a bit clueless and would suspect something wonky with the tasks somewhere…

1 Like

I just tried reproducing it on my M1 machine on Julia 1.9.2 and see the same results with GLMakie 0.8.7.

I ran the exact code that I posted in my last post.

I also ran into this problem. It seems to be related to matrix m being Float32. If you define it as Float64, it will update. Here’s a modified example that shows this behavior. It will try to flash random arrays, but only the 64-bit one will update.

using GLMakie
GLMakie.activate!(inline=false)
GLMakie.set_window_config!(float=true)

struct Container
    m32::Matrix{Float32}
    m64::Matrix{Float64}
end

const c = Container(rand(20,20),rand(20,20))
const obc32 = Observable(c.m32)
const obc64 = Observable(c.m64)

on(obc32) do m
    println("Observable changed")
end


f = Figure()
ax = Axis(f[1,1],title="Float32")
scene = heatmap!(ax, obc32, colormap = :thermal)
ax1=Axis(f[1,2], title="Float64")
scene = heatmap!(ax1, obc64) 
display(f)


for n in 1:10
    c.m32 .= rand(20,20)
    c.m64 .= rand(20,20)

    notify(obc32)
    notify(obc64)
    sleep(0.1)
end

1 Like

This seems to have been fixed in the new update, 0.8.8, thanks a lot!!

2 Likes

Can you elaborate for a second, how you found QML to be not well maintained?

Well, the latest release gives segfaults, which is fixed already for 5 months in main, but despite request to make a new release, this hasn’t been done already for quite a while.

1 Like

Thanks a lot for the heads-up. I did comment on the GitHub issue about the topic, and the maintainer explained that he tried to fix another issue, that turned out to be much more complex than initially assumed.

That stopped him from releasing a fixed version.

I do think, that a segfauling release should not happen to begin with, and a hotfix is suitable in such a case.

To all our benefit, will the QML.jl project now join the bigger JuliaGraphics organization.
This allows a dedicated group of people to commit and release, see the surrounding discussion here.

I see, thanks for the explanation. It was actually a big headache for me because I was including it in a package I’m building, not just using it for a personal project where I can just install the main branch. Anyway, Makie is a bit more flexible for my use, so I’m happy with the switch, it just came at a bit of an unfortunate time.

1 Like