FR: Thread-safe `repaint`

It would be great to help users with “best practices” around repainting by providing a couple pragmatic things out of the box:

  1. A VBlankAttachment as a default member on each juce::Component.
  2. A mechanism to avoid thread safety issues when calling repaint().

The use case: I see both beginners and experienced devs have a hard time remembering in practice that a component can be a ParameterListener or a ChangeListener but cannot actually do anything component-y in the listeners. I just caught myself (via pluginval) yesterday adding repaint() to a function that was called off a ChangeListener (fired by the UndoManager, which was called by the apvts on the audio thread).

Providing a good default in the framework would both highlight and solve this issue (vs. leaving this for users to discover the hard way and research a solution).

My way of solving is by adding a dirty mechanism. I have class like this to my components:

    class RepaintWhenDirty
    {
    public:
        explicit RepaintWhenDirty (juce::Component* component) : component (component)
        {
            vBlankCallback = { component, [this] {
                                  if (this->isDirty)
                                  {
                                      this->component->repaint();
                                  }
                              } };
        }

        void dirty() { isDirty = true; }
        void clean() { isDirty = false; }

    private:
        juce::Component::SafePointer<juce::Component> component;
        juce::VBlankAttachment vBlankCallback {};
        bool isDirty = false;
    };

Then, you can dirty the flag like so:

    void changeListenerCallback (juce::ChangeBroadcaster* source) override
    {
        // this can't call repaint directly
        // as it could be called from the audio thread
        repaintWhenDirty.dirty();
    }

And mark it as clean in repaint:

    void paint (juce::Graphics& g) override
    {
        repaintWhenDirty.clean();
        // do your repainting
    }

(The flag could be atomic, in practice I’m not sure it matters.)

I realize adding anything to the behemoth that is juce::Component is a tough ask or even a no-no. However, it does feel like components would receive more benefit from being VBlank-aware and being able to be marked “dirty” from callbacks out of the box. Having a VBlankAttachment come standard with components would also reduce friction when setting up scaffolding for the new animation in JUCE 8.

Maybe another way of thinking about this — If we were rewriting juce::Component in 2025, would it make sense for repaint to be a flag or a message-thread-only function? Would moving to a flag significantly improve developer experience? Heck, could repaint() be deprecated entirely or made private in favor of a flag that JUCE automatically consumes and processes at the start of every paint call?

I like the idea. So far I’ve been using async updaters or timers to fix these issues.

I’m not sure if I’m seeing the whole picture, but wouldn’t it be possible to hide the dirty flag mechanism from the users and integrate it directly into the repaint() method of the component? Using the VBlank to check the flag and update the view?

1 Like

Yeah, that’s exactly what I was thinking at the end there. That perhaps the cleanest, most backwards compatible solution is to keep repaint() but have it just touch a flag and the existing repaint logic is called at the start of the next frame.

That’s sort of the new-ish “frame-perfect” VBlank way to do repainting (like with animation), it’s just not built into components themselves.

1 Like

I think that would be great. It also makes sense to synchronize this with the VBlank. Right now it also looks like multiple repaint() calls to the same component wastes CPU time. This would also be solved with a simple flag.

1 Like

Repaint already only sets a flag that then triggers the paint function in the next vblank callback, doesn’t it?

1 Like

Conceptually, it marks the component as dirty (vs. actually painting).

But practically it does quite a bit of stuff and must be called on the message thread. It calls internalRepaintUnchecked (which checks JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED) which handles cached image invalidations, calculates dirty areas, transverses the component hierarchy, eventually adding the dirty areas to the peer.

1 Like

The problem to me is with ParameterListener and the ergonomics of similar library objects in JUCE.
Both should operate asynchronously internally (with a Timer) and only send messages on the message thread.

So for example my 'ParamListener` works like this, with the user not worrying about thread safety.

struct MyComponent: Component
{
    MyComponent(AudioParameterFloat& param)
        : listener(param, [&] { repaint(); }) 
{}
    
    ParamListener listener;
};

It’s also possible to dispatch the change right away if we’re already in the message thread, but JUCE’s current message thread check needs to be updated for it to work, see here:

Also I think it’s really suspicious that UndoManager does something in another thread - this sounds like a problem to solve on the plugin top level - APVTS functions should not be called on a non-message thread, so if your setStateInformation is called from the thread, you should defer it to the message thread IMO - which JUCE can also solve internally but the user can solve as well.

2 Likes

Both should operate asynchronously internally (with a Timer) and only send messages on the message thread.

Well said. Any system where GUI components are intimately involved with multi-threading is mixing too many unrelated concerns together. This results in brittle, over-complex, bug-prone code.
Ideally JUCE would provide a canned solution for synchronizing parameters and state between the real-time and message-threads. We should have the ability to receive callbacks on the message-thread when parameters or other state is changed by the real-time thread. And the other way round.
VST3 and AU already support this separation of concerns fairly cleanly. So it should be possible for JUCE to build on those examples.

2 Likes

Seems this is the actual problem that needs to be solved, rather than trying to come up with a thread-safe, asynchronous way to request a component repaint? Firing off change messages from the audio thread seems a really bad idea.

1 Like

Thanks everyone for chiming in here.

The way I see it, it’s true that the root problem is this: even basic parameter synchronization between message and audio threads feels murky, a footgun that we all regularly run into, and something that could be clarified and improved in the framework.

There are a few levers I see:

  • Provide API with clear guarantees as described by Eyal
  • Reduce the surface area of the problem (this FR)
  • Clearly document best practices (something I hope to be doing shortly)

Obviously the ideal route is if we don’t have to worry about threads when calling the aptvs in setStateInformation or in general with parameter callbacks. But I’m not personally sure what kind of effort that takes, or if its even pragmatically possible re: breaking changes.

So while this FR was triggered by a specific threading issue, I opened it because despite the current mechanisms being messy, this specific issue does not need to exist at all. It could be a great candidate to improve a regular footgun without breaking changes and as a bonus align with/embrace what’s become the implicit best practice for repainting via vblank.

But yes, I’d also love for the aptvs to just do the right thing (or to even know how to properly work around it in setStateInformation).

Yes, repaint() asserting is really the symptom of the problem/programming model.

If you imagine the flow of a traditional Component based on a parameter, it might do something like this:

struct MyComp
    : Component
    , AudioProcessorParameter::Listener
{
    void parameterValueChanged(int, float) override
    {
        path = updatePathFromParameter();
        repaint();
    }  

    void paint(Graphics& g) override
    {
        g.fillPath(path);
    }

    Path path;
};

Making repaint() thread safe here just removes the assert - calling updatePathFromParameter() from the other thread is a major data race with the read of that same path from paint().

Pretty much every possible interaction with any variable or member function of Component would also cause similar issues (resizing children for example).

So personally, I would like repaint() and other Component member functions to keep asserting, while perhaps hoping the JUCE team/users of JUCE would offer better library objects to solve these problems and make sure that reactions to audio thread changes are called on the message thread.

5 Likes

Thanks @sudara for the clarification!

Is setStateInformation being called from the audio thread the only case where parameter values get changed from the audio thread, causing messages to be sent around, which may then trigger repaints when components are listeners for these messages? Reason I’m asking is that in the few hosts I’ve looked at, setStateInformation seems to be typically called from the message thread.

You are totally right. If we just made repaint() thread-safe, it kicks the can down the road and would give a false sense of security to devs.

That’s not why I opened this FR, which is 1000% my fault, because the FR is titled exactly that! I also got a bit distracted by the idea of turning repaint into a dumb flag, like my dirty flag. It’s probably best to keep the concept of “dirty because of parameter change” and “needs repainting” separate.

What I was trying to illustrate is that since VBlank was added, there’s the perfect mechanism for frame-perfect thread-safe parameter listening in components — and it would be great if something along these lines could be baked into JUCE and described as the new best practice for components.

We’re like… 98% on the same page. “We want better thread aware listeners that do the right thing with timers” is totally valid, but for components in particular, VBlank is the ideal mechanism, as it runs in lock-step with component painting.

In my RepaintWhenDirty example, it’s just repainting. When I need to calculate things in my own projects, I have the dirty flag in the component itself. I then recalculate whatever I need in paint based on the dirty flag’s state. I guess this could also be a callback provided by Component…

3 Likes

If repaint must be called on the message thread, shouldn’t the assert be JUCE_ASSERT_MESSAGE_THREAD instead of ‘IS_LOCKED’? Would that be a simple first step to take?

i kept running into similar issues and i ended up implementing the following horrible solution:

class MessageThreadUtils {
public:
  static void call(std::function<void()> fn) {
    if (juce::MessageManager::getInstance()->isThisTheMessageThread())
      fn();
    else
      juce::MessageManager::callAsync(fn);
  }
};

// in FooComponent.h
JUCE_DECLARE_WEAK_REFERENCEABLE(FooComponent)

// in FooComponent.cpp
void FooComponent::parameterChanged(const juce::String &parameterID,
                                                float newValue) {
  if (parameterID == someParamId) {
    // do something with newValue, then repaint so the component picks up the change
   // but only if the component is still with us. 
    MessageThreadUtils::call([this, masterReference = &masterReference]() {
      if (masterReference) {
        resized();
        repaint();
      }
    });
  }
}

tracing down every possible place where a callback was potentially interacting with a Component was a huge PITA for me but it stopped a ton of crash reports that i was getting (most of which i could not reproduce myself).

that being said - it would be great if these problems were solved in the framework itself because i don’t remember seeing anything like this in the tutorials or docs.

2 Likes

It looks like your helper reimplements exactly what ParameterAttachment does under the hood.

I guess parameter listeners could be more clearly documented as “Low level callback” vs. ParameterAttachments as “Higher level, thread-safe. The callback will fire on the message thread, regardless of the caller’s origin thread”

(although as Eyal describes above and in his FR, isThisTheMessageThread() is apparently a locking call maybe better replaced by a mechanism like static thread ids checks).

1 Like

It would make sense to have two different parameter listeners: a low-level listener that can be called from any thread and one for the UI that makes the callback from the UI thread.

I need the low-level listener to update my parameter class used by the DSP code. Many parameters need to be scaled or calculated, and I can’t do this for hundreds or even thousands of parameters at each process block call. This is not an optimal solution, but I see it as a stopgap until we have sample-accurate automation. In that case, the JUCE team will have to find another solution for handling audio thread parameters anyway.

My current solution is to update atomic bool(s) in parameterChanged and check bool flags per block & update the underlying dsp model.

IIRC ParameterAttachment would place a (small?) lock on the audio thread during initialization. I have never encountered problems though.

It’s not just during initialization. In fact, ParameterAttachment locks twice for each parameter change - once for checking the message thread, once for posting the message to the message thread. It also very likely allocates memory when doing so (that part slightly changes in each platform).

AudioProcessorParameter::Listener however is slightly better in that regard - it doesn’t check for the message thread and isn’t posting anything to the system queue.

BUT - it still locks in sendValueChangedMessageToListeners right before it triggers your listeners. If you must be using one of them, that one is less bad than the other one.

For ‘user’ code I would suggest to not use either of them. You’ll see for example that JUCE’s ParameterListener (a private class meant to only be used in GenericAudioProcessorEditor) is trying to work around that by using Timer and an atomic to check parameter changes.

1 Like

hah, it didn’t occur to me to use ParameterAttachment for this since it’s not a two-way binding but that’s good to know. this implementation is more concise than mine and doesn’t involve std::function so it seems preferable 9although it’s a bit inconvenient to have to use a separate api to do the same thing in a Component as in a processor). if i have to do this again i think i’ll just end up implementing APVTSListenerComponent / ValueTreeListenerComponent classes to inherit from so i can maintain the same API and centralize the thread checking logic.