Drawing a cursor synchronized with a Metronome

@Wah0airo consider these points…

  • The cursor in this case is just another Component.

  • The OS is not aware of the layers of Components your application is made of.

  • The OS redraws in rectangles. An region gets marked invalidated and if your application is in the invalidated region the whole rectangle needs redrawing.

  • When the Component (your cursor) moves, the OS will need to update the region in which the cursor was previously, otherwise how does it know what was behind it? Therefore this part needs redrawing.

  • When the cursor moves over some new area your application will need to paint everything behind the cursor too *

  • When some component is inside the region marked invalidated, the whole thing will need to be redrawn, JUCE will then only use the part of it that was invalidated to update the screen. (Imagine having to deal with callbacks on your paint methods that meant you only painted the part of your component that was invalidated, it would normally be a total nightmare!)

* In the case of a static cursor it may be true that there is a possible optimisation, but in the case of an animated cursor it isn’t always possible, i.e. if some pixels in the cursor change from non-transparent to having any transparency then everything that is behind the cursor will need repainting too.

There is also an optimisation that can be made if the cursor happens to have no transparency at all, since then nothing behind it would need repainting.

Now I suspect that the OS can optimise for the installed cursors as it is a level of layering it is aware of (I’m making assumptions here), so maybe (I haven’t tried) you can install a cursor to the OS and set it to this cursor as and when you need?

If for some reason you actually do need a Component, then the best way to optimise IME is as follows…

  • Split your GUI into as many logical layers and components as makes sense

  • Call setOpaque (true) on every component that fills it’s entire bounds with solid colours.

  • Make every paint method only paint within its local bounds and call setPaintingIsUnclipped (true) on all such components (this mostly means avoiding fillAll()).

  • For any component whose paint method takes longer than painting an image of the same size and isn’t changing often (like a background for example), call setBufferedToImage (true)

  • For any component that has child components avoid any painting in the component itself, instead have another child component do the work. This is the least intuitive of all the steps I agree, but there is method in the madness - it’s particularly useful when you have some background that’s expensive to draw that you’ve called setBufferedToImage (true) on but you don’t want the background to be invalidated by some component on top, as long as the component is a sibling then only the buffered image will be redrawn, whereas if the background is drawn in the parent the parent will be invalidated in such a way that the image itself it’s invalidated and therefore redrawn!

  • Profile, profile, profile!!!

  • If there is any heavy work that can be kept off the message thread - then keep it off the message thread. You can start your own thread or use the ThreadPool class.

  • Normally it’s worth seeing if drawing into an OpenGL context improves matters - but I recommended having the option be user switchable.

Using the above steps it’s totally possible to have your drawing pretty decently optimised in most use cases.

One of the only other big issues I’ve seen is if your GUI has something changing regularly at either end of the window (left/right, top/bottom), then sometimes the invalidated regions are combined together into one which can mean a whole bunch of stuff in the middle can be redrawn that may not need to be. A good example of this is if you have meters at either end of the GUI, such as input/output meters, but otherwise mostly static components throughout the rest of the GUI. If I remember correctly there is a preprocessor flag to change the behaviour of this, profiling will reveal which is best for your situation. I think this was only an issue on macOS.

Hope that helps.

3 Likes