Implement A Co-Operative Multitasking Model For Main Loop

Issue #1268 new
TannerRogalsky
created an issue

Currently, Love's main game loop is in Lua and uses a while true do loop. This is desirable for a number of reasons but it is at odds with platforms that have a co-operative multitasking model (read: web browsers).

Emscripten requires ceding control back to the system once per frame. Consider the folllowing code as a basic example of the difference between a more traditional and the emscripten model of game loop:

#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop_arg([](void *arg) {
    Love *self = static_cast<Love *>(arg);
    self->Update();
  }, (void *)this, 0, 1);
#else
  while (!quit) {
    Update();
  }
#endif

To facilitate platforms with this multitasking model, I propose a change to the way that Love handles its game loop. I can think of two possible solutions.

  1. A coroutine-based approach. It makes sense to me to apply Lua's own co-operative multitasking to this problem. Potential caveats are that Lua5.1's C coroutine API is more limited than 5.2's (it is missing lua_yieldk, lua_callk, and lua_pcallk).
  2. Move the main loop to C++. A simple solution but undesirable for a number of reasons. Being able to override love.run and have complete control over the game loop is great for developers.

Please post any additional problems or solutions that you may think of.

Comments (20)

  1. Bart van Strien

    Well, the downside is that I don't know how to deal with errors when using that (yet), I think the errors end up at resume, so maybe a wrapper that xpcalls resume and yields?

  2. TannerRogalsky reporter

    @Bart van Strien Been doing some emscripten work today and figured I'd integrate your (at least temporary) solution to see how it works. Unfortunately, even if just compiling native Love with Lua5.1, this doesn't work. We run out this error: "attempt to yield across metamethod/C-call boundary". LuaJIT doesn't have this limitation but that's not gonna happen through emscripten, unfortunately. The least obtrusive solution to that problem is probably to patch Lua5.1 with Coco but I wonder if there might a simpler solution still.

  3. Bart van Strien

    Yeah, when I was working with it I noticed this too (see the commit message), I think the boundary it's talking about is xpcall. You could try removing the xpcalls first, to see if that solves it, and if so, we'd need to find another way to catch these errors.

  4. TannerRogalsky reporter

    An update:

    While this approach (sans xpcalls) works well, not having error handling isn't really an option.

    I tried patching Lua with Coco and it worked with a native build (there were still some problems: calling print from inside a coroutined and protected function caused a segfault?!?) but it's answer to this problem is some custom assembly to facilitate the context switches. Not really an option for web.

    I also tried compiling LuaJIT with all the optimizations I could find turned off (nothing wagered, nothing gained) but that has the same problem.

    I think that this approach may be a dead end, unfortunately. Edit: To clarify, I mean the approach that bartbes implemented in the above commit.

  5. TannerRogalsky reporter

    I have a potential solution here: https://bitbucket.org/TannerRogalsky/love_emscripten/commits/4e52abe7b6945

    There are two parts to it:

    1. The fake_xpcall call function which emulates xpcall but the error handler function is called outside of the pcall and so is free to yield. This works well for the boot and init functions but run would still have a yield inside of the pcall so additional work was needed:
    2. Wrapping the two parts of love.run individually so as to exclude the actual yield from the protected code. It complicates the code a little but it's not too bad, I hope.

    These things together have allowed me to adopt the coroutine-style in lua5.1 while maintaining error catching. Here's an image of love.js with an actually helpful error message! https://i.imgur.com/Z5EvLBq.png

  6. Bart van Strien

    The reason to prefer xpcall over pcall is that the stack is still intact for your stacktrace, though. Are stack traces still reasonable with your solution?

    Why is fake_xpcall needed anyway, you can't yield from a pcall either, can you?

  7. TannerRogalsky reporter

    Stack traces aren't very good, unfortunately. You can see one in the screenshot in my last post. I was hoping there was a way to fix that but I haven't looked into it yet.

    The problem that fake_xpcall fixes is yielding from an xpcall's error handler, rather than the protected function itself. You still can't yield from pcall but at least this way you can pretend to yield from the error handler.

  8. Bart van Strien

    Right, but you might not need to yield from xpcall if the error handler is patched. That's why my (failed) attempt stored the current coroutine in a variable somewhere, so I could swap it out with the error handler's coroutine instead of having the error handler not terminate like it does now.

  9. Pablo Mayobre

    I reported this A WHILE ago in #1052

    My idea was to split love.run in two functions, a initializer that calls love.load and seeds love.math.setRandomeSeed

    And a per-frame function that calls the event handlers, update and draw.

    SIDE QUESTION: Tanner is it fine to sleep in Emscripten?

    Also the love.run in Tanner's code is kinda complex... You should try to keep modifications to a minimum (adding coroutine.yield is simple but the fake_xpcall and internal_update, not so much)

  10. Log in to comment