The following problem originates from https://github.com/cycfi/elements/issues/144 which is me struggling to find a way in elements GUI library to invoke a callback once per frame.
So far in every library I have seen, there is some callback/explicit loop that continuously processes user input, measures time since last frame and performs the render.
In the elements library, such loop is a platform-specific implementation detail and instead, the library-using code is given access to boost::asio::io_context
object to which any callable can be post
ed. poll
is invoked inside platform-specific event loop.
I had no problems changing code from typical waterfall update(time_since_last_frame)
to post
ing functors that do it, however this is where the real problem begins:
- Posted functors are only invoked once. The answer from the library author is "just post again".
- If I post again immediately from the functor, I create an endless busy loop because as soon as one functor from the
poll
is completed, boost asio runs the newly posted one. This completely freezes the thread that runs the GUI because of an infinite self-reposting callback loop. The answer from the library author is "post with a timer". - If I post with a timer, I don't fix anything:
- If the time is too small, it runs out before the callback finishes so the newly posted callback copy is invoked again ... which brings infinite loop again.
- If the time is too large to cause an infinite loop, but small enough to fit in multiple times within one frame, it is run multiple times per frame ... which is a waste because there is no point in calculating UI/animation/input state multiple times per frame.
- If the time is too large, the callback is not invoked on each frame. The application renders multiple times without processing user-generated events ... which is a waste because identical state is rendered multiple times for each logic update.
- There is no way to calculate FPS because library-using code does not even know how many frames have been rendered between posted callbacks (if any).
In other words:
- In a typical update+input+render loop the loop runs as fast as possible, yielding as many frames as it can (or to a specified cap thanks to sleeps). If the code is slow, it's just FPS loss.
- In elements library, if the callback is too fast it is repeated multiple times per frame because registered timer may finish multiple times within one frame. If the code is too slow, it's a "deadlock" callback loop that never gets out of asio's
poll
.
I do not want my code to be invoked every X time (or more-than-X because of OS-scheduler). I want my code to be invoked once per frame (preferably with the time delta argument, but I can also measure it myself from previous invokation).
Is such usage of asio in the elements library a bad design? I find the "post with a timer" solution to be an antipattern. It feels to me like fixing a deadlock between 2 threads by adding a sleep in one of them and hoping they will never collide after such change - in case of elements I'm posting a timed callback and hoping it's not too fast to waste CPU but also not to slow to cause infinite timed-callback loop. The ideal time is too hard to calculate because of so many factors that can affect it, including user actions - basically a lose-lose situation.
Extra note 1: I have tried defer
instead of poll
, no difference.
Extra note 2: I have already created 100+ issues/PRs for the library so it's very likely that a motivating answer will end in another PR. In other words, solutions that attempt to modify library are fine too.
Extra note 3: MCVE (here without a timer, which causes almost-infinite loop until the counter finishes, during coutning the GUI thread is frozen):
#include <elements.hpp>
using namespace cycfi::elements;
bool func()
{
static int x = 0;
if (++x == 10'000'000)
return true;
return false;
}
void post_func(view& v)
{
if (!func())
v.post([&v](){ post_func(v); });
}
int main(int argc, char* argv[])
{
app _app(argc, argv);
window _win(_app.name());
_win.on_close = [&_app]() { _app.stop(); };
view view_(_win);
view_.content(box(rgba(35, 35, 37, 255)));
view_.post([&view_](){ post_func(view_); });
_app.run();
return 0;
}