Vblank details

Hi everyone!

In my search for super smooth animations, I’d appreciate some extra info around how the VBlank callback mechanism works. At a high level, what are the order of events? My understanding is:

  1. The os knows it’s about to draw a new frame to the screen.

  2. Vblank listeners are called.

  3. The frame is drawn to the screen.

If this is correct, what is the expected time difference between steps 2 and 3? I’m trying to get a pulse on how much time one reasonably has to spend inside vBlank callbacks without impacting frame times.

Thanks!

The order of events is correct.

The calls to 2 and 3 follow each other on the same call stack, under the same vblank event, so there is no programmed time difference between them.

As a general rule, vblank listeners should only be used for fast state updates and calling the repaint() function. The lengthy painting operations should happen inside Component::paint().

Regarding the feature request to Add timing info to VBlankListener callbacks: the request does make sense, so it’s being considered. But until then it would make sense for each Component to make only two calls

this->now = Time::getMillisecondCounterHiRes();
this->repaint();

ensuring that Component’s stay in sync.

Thanks for your response.

For an example, say I have a vector of new audio data that I need to shift to align with the time elapsed since last draw, and then I need to build some paths from that data, are you suggesting that work should take place within the paint routine itself?

The general rule of thumb out in the wild has always been don’t mutate any state or data in paint, and have everything ready to go by the time you get there. And it sounds like you are saying the same for the vblank callback too.

Where should heavy lifting be done, in such a way that steps can be synchronised to frames? Perhaps this type of work should be done in a predicted way on worker threads?

Worker thread is frame rate aware, creates a queue of results of easily drawable objects.

Vblank calls which provides a timestamp.

Paint can pull as many pre-generated frames worth of data as it needs to to jump to the ‘now’ point.

Would love to hear about how you envision things working optimally.

1 Like

Ideally, you would update your model in the vblank callback, call repaint(), and only do drawing in paint(). That is assuming that the update isn’t too expensive that it interferes with drawing at the full refresh rate.

However, I recognize, that to keep all Components visually in sync, you’d have to have a single timestamp per vblank callback shared by all callees. We will investigate adding this timestamp to the callback. In the meantime a possible workaround would be writing a VblankTimeSource singleton, where you register all your callbacks, and the 1st callback out of N will write the time using Time::getMillisecondCounterHiRes(); and the Nth callback erase it. You can do this because all vblank callbacks are called sequentially on the MessageThread, but the order is not defined, hence the need for this trickery.

I mentioned the assumption that the update isn’t too expensive. Since this is the case, it is also practically possible to move the update into the paint() call. There won’t be observable drawbacks, since this method is still being called on the same stack as the vblank callback was, but this way you can omit having to write VblankTimeSource. Personally, now that I’m reaching this point in the explanation, I would write VblankTimeSource, because that will be easy to replace, when the timestamp is added to the vblank callback.

The rule, not to modify any state or data in paint(), sounds too general to me. Not calling any mutable functions of the juce::Component base sounds like a better rule, because that could cause recursion and could be generally difficult to reason about.

Finally, if your update function is too expensive, you’ll need to use a worker thread. Partly because it allows you to utilise another CPU core, partly because it gives you the freedom to write synchronisation code, that can handle the situation when you can no longer do the updates at screen refresh rate.

2 Likes

Fantastic information, thank you!

For the time being, I have written a class which my editor inherits from, which holds a vBlankAttachment. This is the only vBlankAttachment in my entire gui.

Then I’ve written an extended version of vBlankAttachment, which my components lower down hold, and they add themselves as a listener to the class the top level editor inherited from. This is hooked into via it’s constructor:

	{
		component->addComponentListener(this);
		auto master = component->findParentComponentOfClass<VBlankMasterSync>();

		if (master)
			master->addListener(this);

	}

And I have added the callbacks:

	std::function<void(double deltaTimeMs)> onVBlank;
	std::function<void(int fps)> onFPSChange;

I’m using OS specific hooks on a timer to query the fps of the display that the peer is on, which also allows me to set my movement speeds in components to a value that aligns with refresh rate.

So this gets me vBlank callbacks with a single timestamp across all listeners, and another callback when a change in hardware refresh rate is detected.

Good to know I’m (kind of) on the right track. I look forward to seeing what the official implementation looks like!

2 Likes