Conversation
|
for my use cases i tend to want a parallel for (often 2d/3d, too), fwiw |
|
There will be a (async) function to run Lua code on a worker thread, and likely a parallel-for helper on top of this. I haven't figured out the specific API for the parallel-for function yet though. |
742c177 to
c43cd09
Compare
- Pool RunContexts - Cache bytecode + functions
Polled tasks wait for a condition instead of doing compute work
|
Decided to change the API back to plain coroutines instead of Task objects. I think it's a little easier to understand (it's more clear that tasks are just coroutines), interops better with other libraries, and has slightly less overhead. Semantics are pretty much exactly the same. Also, added a way to wait on a polled condition instead of a job. This is good for IO bound stuff like sleeps, readbacks, async IO, condition variables, etc. As a test, I made |
This is an experiment to add a coroutine-based task system to LÖVR, with the main goal of making it easier for projects to scale to multiple CPU cores and run expensive work in the background.
Design
The basic idea is to create lots of tasks (coroutines) that are each running independent work. Lots of tasks can all run more or less at the same time, cooperatively.
Normally, Lua coroutines are still single threaded, and don't allow for parallelism. However, LÖVR can achieve parallelism with them by running asynchronous functions.
When a coroutine calls an asynchronous function, it will start work on a thread pool, yield itself, and wake up automatically once the work is complete. While the coroutine is yielded, other coroutines get a chance to run.
In this way, many coroutines can all run work "at the same time", with all of them doing work on a thread pool, making use of all of the CPU cores.
API
Everything is in a new module,
lovr.task:Notes:
coroutine.create. Note that task functions share the global scope with the caller and other tasks. You don't have to useChannelto communicate with the task.success, ...results = lovr.task.wait(...tasks). Blocks until all the tasks are complete, returning their results or the first error that was encountered.lovr.task.wait(a, b, c)returns1, 1, 1, 2, 3.lovr.taskreadyanymore, because it's already finished. You can choose to "fire and forget" a task and letlovr.taskreadytake care of running it on a future frame, or explicitly wait on the task if you need its results sooner.success, ...results = lovr.task.resume(task, ...args). Resumes a task, if possible, otherwise returns false and an error.argsare ignored and the results from the async operation are given to the task instead.trueand you'll get any yield/return values.lovr.taskready, since you already ran it.Asynchronous Functions
Asynchronous functions are where the magic happens. Asynchronous functions behave differently, depending on whether they are called inside of a Task or not:
lovr.taskreadyon the next frame (unless it gets run sooner than that).Any LÖVR function can become asynchronous. Let's take
lovr.data.newImage, which I've been using as a test async function on this branch:Even though this is an async function, it behaves exactly the same as it does today, and you don't need to change any code. But if you run it on a task, it yields the task:
By itself this isn't interesting. But there are interesting things you can do with asynchronicity and parallelism.
Here's an example that loads an image in the background:
Here's an example that loads 100 images on multiple threads:
Discussion
taskreadyjust a regular event?lovr.data.newSound/lovr.audio.newSourcelovr.data.newImage/lovr.graphics.newTexturelovr.data.newModelData/lovr.graphics.newModel/lovr.headset.newModellovr.timer.sleepBuffer:getDataandTexture:getPixels(theoretically obviates Readback)Channel:pushandChannel:poplovr.task.waitlovr.thread.run-- note that this can be used to make custom async functions, or even make an async version of an existing LÖVR functionTODO
lovr.task.sleep(async, wakes up task later, could cause problems if you want the wait time to be based ondt?) Maybe justlovr.timer.sleepis asyncdebuglibrary with the task to inspect its state.