Linux: repaints get discarded when calling repaint at MouseEvent rate

Hi everyone!

I was experiencing an issue in JUCE recently, that was causing many repaint() calls to get discarded on Linux. I’m using JUCE 7.0.5, but I don’t think this is a new issue.

The problem is, if you repaint a large amount of Components on mouseDrag, almost all of those repaint requests get discarded, because it can’t keep up with the rate of MouseEvents. It seems like new repaint() calls are discarded if there is a repaint currently in progress, meaning that if you call repaint() quick enough, it will not repaint anything at all.

Here is a demo of this problem:

ezgif-4-49973d7236

And here’s what it looks like after reducing the amount MouseEvents:

ezgif-4-cee7d46c69

This was tested on Fedora/GNOME inside a VM on my Macbook Pro, but I’ve had other Linux users of my app confirm this issue. The same bug does not occur on macOS and Windows.

Not sure here, but there might be some more variables that could influence the rate of MouseEvents, like perhaps the refresh rate of your mouse? That might be something to keep in mind if anyone wants to try to reproduce this.

In case anyone’s wondering, here is the bit of code I’ve used to resolve this.

// Class that blocks events that are too close together, up to a certain rate
// We use this to reduce the rate at which MouseEvents come in, to prevent an overload of repaint() on Linux
struct RateReducer : public Timer
{
    RateReducer(int rate) : timerHz(rate) {
    }
    
    bool tooFast() {
        if(allowEvent) {
            allowEvent = false;
            startTimerHz(timerHz);
            return false;
        }
        return true;
    }
    
    void timerCallback() override {
        allowEvent = true;
    }
    
    void stop() {
        stopTimer();
        allowEvent = true;
    }
    
private:
     int timerHz;
     bool allowEvent = true;
};


// Class that allows reducing mouseDrag rate on any generic JUCE Component
// A bit hacky but it works
template<typename T, int hz = 90>
class MouseRateReducedComponent : public T
{
public:
    using T::T;
    
    void mouseDrag(const MouseEvent& e) override
    {
        if(rateReducer.tooFast()) return;
            
        T::mouseDrag(e);
    }
    
    
    void mouseUp(const MouseEvent& e) override
    {
        rateReducer.stop();
        
        T::mouseUp(e);
    }
    
private:
    RateReducer rateReducer = RateReducer(hz);
};

But I think that this is a bug, and it would be nice to find a real solution.

1 Like

If you add all the subcomponents (i.e. your little squares there) to a component, and drag the top level component, does that fix the issue?

Good question, I’ll test that!

My UI here is very dynamic, so it’s not like the little squares are part of a single conceptual component, they are all individual modules that need to move individually. Though maybe it would be efficient to add them all to a single component temporarily when dragging? You’ve given me something to think about :slight_smile:

That also solves it, thanks for the suggestion!

There’s some caveats, if I want to preserve the original order of the objects, I would have to create multiple layers of components, so for now they all just pop to front when dragging. I’m able to get a huge performance improvement by buffering the drag container component to an image, so it’s still worth it overall.

So what I understand from this, the problem is that it has to render a lot of tiny rectangles, instead of rendering one big rectangle?

Correct me if I’m wrong here, but I feel like it should still update at a lower frame rate when you’re moving too much stuff, instead of blocking completely. That’s the behaviour I get on other platforms, and because spacing the repaints apart by a small amount (even by a few milliseconds, somehow) solves this issue, it seems like LinuxComponentPeer should do that by default?

I found this bit of code in the NSViewComponentPeer, seems to be aimed at solving exactly this issue on macOS:

void setNeedsDisplayRectangles()
{
    if (deferredRepaints.isEmpty())
        return;

    auto now = Time::getMillisecondCounter();
    auto msSinceLastRepaint = (lastRepaintTime >= now) ? now - lastRepaintTime
                                                       : (std::numeric_limits<uint32>::max() - lastRepaintTime) + now;

    constexpr uint32 minimumRepaintInterval = 1000 / 30; // 30fps

    // When windows are being resized, artificially throttling high-frequency repaints helps
    // to stop the event queue getting clogged, and keeps everything working smoothly.
    // For some reason Logic also needs this throttling to record parameter events correctly.
    if (msSinceLastRepaint < minimumRepaintInterval && shouldThrottleRepaint())
        return;

  ...
}

Maybe do something similar on Linux?

EDIT: I now see that this code is only active when the window is resizing, but nonetheless it might help to cap the rate of repaints on Linux?

1 Like