Repaint glitch?

Hi , in the following code a fully transparent component is continuously repainted, while another semitransparent component is also continously repainted over the transparent component.

The code should just blink alternatively between dark gray and light gray, but randomly, the shape of the transparent component becomes visible, as can be seen in the attached animated (caution : this is blinking very hard)

class MyGlitchComponent : public Component {
  class InvisibleComponent : public Component, public Timer {
  public:
    InvisibleComponent() {
      startTimerHz(60);
    }
    void timerCallback() override { repaint(); }
  };

  /** A semi-invisible foreground component */
  class AnimatedForeground : public Component, public Timer {
  private:
    int cnt = 0;
  public:
    AnimatedForeground() {
      startTimerHz(30);
    }
    void paint(Graphics &g) override {
      g.fillAll(Colours::black.withAlpha((cnt & 2) == 0 ? 0.6f : 0.4f));
    }

    void timerCallback() {
      ++cnt;
      repaint();
    }
  };

  InvisibleComponent invisible;
  AnimatedForeground foreground;
public:
  MyGlitchComponent () {

    addAndMakeVisible(invisible);
    addAndMakeVisible(foreground);
    setSize(600,600);
  }
  void resized() override {
    invisible.setBounds(getLocalBounds().reduced(getWidth()/4,getHeight()/4));
    foreground.setBounds(getLocalBounds());
  }
  void paint(Graphics &g) {
    g.fillAll(Colours::white);
  }
};

This is on macOS, with the latest JUCE develop, but it happens also with JUCE 7 so maybe I’m rediscovering something that is already well known ? I see the glitch more often when the window is displayed on my external display than the builtin display of the macbook.

There is a lot of paint glitches everywhere. the key is to prevent it from happening .

For instance drawing lines you will see “dust”

From previous paint calls

So you clear your drawing area with the background color first before drawing your paths

Sudaras blog post on the topic .

The timing difference means you’re telling the smaller area to be repainted more frequently than the larger area so it picks up the changes earlier.

Remember that repaint() tells an area of the screen to be repainted, not just the component you call it on. So both the invisible and semi-transparent components are having their paint() methods called at 60FPS.

If you moved ++cnt to the start of the animated component’s paint() method, I think the issue would go away (albeit replaced with a different issue where the count will increment 60 times a second instead of 30).

I think what is happening here is something like this…

  1. The invisible components timer callback fires which registers that area as needing to be repainted (as already pointed out this is going to trigger the foreground to repaint just this area).

  2. The Foreground component timer callback also fires which registers that area as needing to be repainted.

  3. Very soon after the OS comes back telling us to repaint the invisible area, but we haven’t yet been told about the foreground area.

This will result in a rectangle matching the invisible component being repainted but with the new shade of grey because the timer for the foreground has already fired.

If you really really want to avoid this I think setting JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS would prevent this “issue” but I wouldn’t recommend it.

Could you maybe share a more realistic use case to help me understand what the problem is in practice.

At a guess a VBlankAttachment might be better suited, you could then use the timestamp provided as a way to measure between calls so that something occurs at some specific frequency?

I will also add that just because one timer is set to 30Hz and the other 60Hz and they both call repaint(), this does not mean one will paint half as often as the other. The timer is not that accurate and paint calls may be coalesced.

1 Like

In the more realistic use case, the “invisible component” is a knob that is continuously animated by an LFO, so it is always repainting itself, and a large part of its component area is transparent.

On top of it, sometimes a large semi-transparent layer displayed, it starts fully transparent and progressively becomes semi-transparent. During this animation we randomly see the rectangles of the bounds of the knobs that are below this semi-transparent layer.

This not macOS specific by the way, it happens also on windows.

We will try to switch everything to VBlank , so that all calls to repaint are done at the same moment.

Having given this a bit more time and thought I think this is what happens

  1. The timer fires on the InvisibleComponent, JUCE records the region that needs repainting

  2. The VBLANK callback occurs and JUCE notifies the OS of the region that needs redrawing

  3. The timer fires on the AnimatedForeground, the cnt variable is incremented, and JUCE records the region that needs repainting

  4. The OS asks to draw the region that JUCE previously notified it about, this results in the InvisibleComponent paint method being called which in turn triggers the AnimatedForeground paint method and results in the glitch.

Note if the AnimatedForeground timer could have fired before the VBLANK or after the OS asks to draw the recently invalidated region, there would be no glitch.

So what’s needed is a way to ensure repaint() is never called between the VBLANK event and the actual drawing. This is where using a VBlankAttachment helps because it ensures that the repaint call happens just before JUCE notifies the OS of the regions that need updating.

In the case you’ve shared the most obvious solution is probably to draw your animations at the display refresh rate using a VBlankAttachment. If you just call repaint() in the callback that region should always be painted.

However, if you wanted to avoid that, the key would still be to get repaint and state changes into the VBlankAttachment callback.

For example I think this would work…

    class AnimatedForeground : public juce::Component
    {
    public:
        AnimatedForeground() = default;

        void paint (juce::Graphics& g) override { g.fillAll (colour); }

    private:
        juce::VBlankAttachment vblank{ this, [&] (double t)
        {
            const auto count = juce::roundToInt (t * 30.0);
            const auto newColour = juce::Colours::black.withAlpha ((count & 2) == 0 ? 0.6f : 0.4f);

            if (auto oldColour = std::exchange (colour, newColour); oldColour != newColour)
                repaint();
        }};

        juce::Colour colour;
    };

Thanks for coming back to us. Using the VBlankAttachment fixed the glitch on the minimal example on macOS! We still have it on Windows though. We’ll check how it behaves in the whole GUI.

Note that this also happens when the timer callback interfers with mouse events.

Here is another realistic use case: an opaque component which state depends on mouse events, above an animated knob component with a large transparent area. I reduced it to this minimal example. I tried the VBlankAttachment approach too, works fine on macOS, not Windows.

class MyGlitchComponent : public Component {
  class InvisibleComponent : public Component, public Timer {
  public:
    InvisibleComponent() {
      startTimerHz(60);
    }
    void timerCallback() override { repaint(); }
  };

  class OpaqueForeground : public Component {
  private:
    class QuarterComponent : public Component {
    public:
      void paint(Graphics &g) override {
        g.fillAll(isMouseOver() ? Colours::aqua : Colours::green);
      }
      void mouseEnter(const MouseEvent &) override { repaint(); }
      void mouseExit(const MouseEvent &) override { repaint(); }
    };
    class QuarterComponentVBlank : public Component {
    public:
      void paint(Graphics &g) override { g.fillAll(colour); }
    private:
      juce::VBlankAttachment vblank{ this, [&] (double t) {
          const auto newColour = isMouseOver() ? Colours::aquamarine : Colours::forestgreen;
          if (auto oldColour = std::exchange (colour, newColour); oldColour != newColour)
            repaint();
      }};
      Colour colour;
    };
    QuarterComponent comp1, comp2;
    QuarterComponentVBlank comp3, comp4;
  public:
    OpaqueForeground() {
      addAndMakeVisible(comp1);
      addAndMakeVisible(comp2);
      addAndMakeVisible(comp3);
      addAndMakeVisible(comp4);
      setOpaque(true);
    }
    void resized() override {
      auto r2  = getLocalBounds();
      auto r1 = r2.removeFromTop(roundToInt(0.5f*getHeight()));
      comp1.setBounds(r1.removeFromLeft(roundToInt(0.5f*getWidth())));
      comp2.setBounds(r1);
      comp3.setBounds(r2.removeFromLeft(roundToInt(0.5f*getWidth())));
      comp4.setBounds(r2);
    }
    void paint(Graphics &g) override {
      g.fillAll(Colours::magenta);
    }
  };

  InvisibleComponent invisible;
  OpaqueForeground foreground;

public:

  MyGlitchComponent () {
    addAndMakeVisible(invisible);
    addAndMakeVisible(foreground);
    setSize(300,100);
  }

  void resized() override {
    invisible.setBounds(getLocalBounds().reduced(getWidth()/4,getHeight()/4));
    foreground.setBounds(getLocalBounds());
  }

  void paint(Graphics &g) override {
    g.fillAll(Colours::white);
  }

};