Implement A Co-Operative Multitasking Model For Main Loop

Issue #1268 resolved
Tanner Rogalsky 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);
  }, (void *)this, 0, 1);
  while (!quit) {

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 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 (22)

  1. Bart van Strien

    So you're basically suggesting replacing love.timer.sleep with coroutine.yield at the bottom of

  2. Tanner Rogalsky reporter

    I think that, because of the way that the emscripten callback works, the yield has to come from C++ into Lua, not the other way around.

  3. Bart van Strien

    Why? Can't you just do something like:

    emscripten_set_main_loop_arg([](void *) {
        lua_pushvalue(L, -1);
        lua_resume(L, 0);
    }, nullptr, 0, 1);
  4. Tanner Rogalsky reporter

    It's possible that it's less complicated than I think. If that works then, absolutely, that would be ideal.

  5. 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?

  6. Tanner Rogalsky reporter

    @bartbes 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.

  7. 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.

  8. Tanner Rogalsky reporter

    Ah, whoops, I had forgotten about that. Removing the xpcalls does make it work with PUC Lua and it works really well with the emscripten loop callback setup.

  9. Tanner Rogalsky 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.

  10. Tanner Rogalsky reporter

    I have a potential solution here:

    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 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!

  11. 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?

  12. Tanner Rogalsky 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.

  13. 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.

  14. Tanner Rogalsky reporter

    Ah, I think I understand. And then having the C code call the error handler might help with the call stack, you think? I'll give that a whack.

  15. Pablo Mayobre

    I reported this A WHILE ago in #1052

    My idea was to split 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 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)

  16. Bart van Strien

    Make and love.errhand return a function for their main loop instead (resolves #1268)

    This way lua code can stop every iteration (usually frame), and return to the c++ code, so love can work in environments with co-operative multithreading like emscripten.

    It may be interesting to see if we can make this optional, so this only happens on platforms that require it, but I doubt it's significant in the grand scheme of things.

    → <<cset c3949ee79eb3>>

  17. Log in to comment