sokol icon indicating copy to clipboard operation
sokol copied to clipboard

Feature request - multithreaded rendering

Open oviano opened this issue 4 years ago • 8 comments

As part of migrating my project (a video player) from bgfx to sokol, it became apparent pretty quickly that I would need to figure out how to deal with sokol being designed for single threaded use with my application having a separate update and render thread.

So I wrote a proof-of-concept multithreaded rendering class, and adapted sokol_imgui.h to use this too.

I know @floooh mentioned in https://github.com/floooh/sokol/issues/403 about the possibility of a command queue so apart from making a FR for this, I thought I'd share my proof-of-concept code which is working well in my project. The code is not a complete working example, but it shows how it works, in case anyone is interested.

https://ovcollyer-colebrooke.synology.me:5001/d/f/584662710053025875

It's in C++ but maybe if you ever go down this route and produce some kind of sokol_cmdqueue.h it might be of some use.

Basic principle is that the application would create the window/device etc as usual in the main thread, but then create an update/game-logic thread which would create this renderer object. Thereafter, you would add commands to the renderer, and at the end, commit them. This would move them into the commit list, and the main/render thread would then call into the renderer object to execute the commands, with two semaphores used to coordinate things. This part is similar to bgfx.

To deal with things like creating/updating images, buffers, etc, a copy of the data has to be included in the queue system, but since these allocations would often be repeated every frame it caches these blocks and re-uses them, periodically flushing those that haven't been re-used for a while (1000ms).

It can almost certainly be optimised, and probably if @floooh wrote something like this in C it would be way more efficient, but thought I'd share it anyway.

In a proper implementation, maybe things like sokol_imgui, sokol_fontstash etc could have compile-time options to render to a queue, rather than directly otherwise all of that magic wouldn't be taken advantage of. Or some way of making the sg_xxxx() calls resolve to queue functions instead, somehow.

Anyway, I don't know if this is on the roadmap, but thought I'd share this anyway.

oviano avatar Oct 31 '20 17:10 oviano

The same ideas i looked in rizz. https://github.com/septag/rizz

alex-rass-88 avatar Nov 05 '20 08:11 alex-rass-88

So I refined/simplified the concept a little, and made a repo for it.

It no longer deals with copying the memory used for buffers, images etc, instead this is left to the caller to manage with a completion callback called when the memory is finished with. This is more flexible and efficient.

https://github.com/oviano/sokol-multithread

oviano avatar Nov 06 '20 13:11 oviano

https://github.com/oviano/sokol-multithread

It is very interesting! I'm having trouble using multithread with Sokol gfx in my project https://github.com/supernovaengine/supernova, but I think I'll use your solution.

eduardodoria avatar Feb 24 '23 16:02 eduardodoria

https://github.com/oviano/sokol-multithread

It is very interesting! I'm having trouble using multithread with Sokol gfx in my project https://github.com/supernovaengine/supernova, but I think I'll use your solution.

How did you get on with this? The code might need tweaking to work with latest sokol_gfx.h.

Even if you don't use this code specifically, the concept has worked really well for me.

oviano avatar Feb 27 '23 18:02 oviano

How did you get on with this? The code might need tweaking to work with latest sokol_gfx.h.

It works with latest sokol_gfx.h. You can see my commit https://github.com/supernovaengine/supernova/commit/354d3a21515ba25a888d57f10287a48788562e4f.

I don't undestand is why is necessary two semaphores instead of one. But it works perfectly!

eduardodoria avatar Feb 27 '23 19:02 eduardodoria

Nice one!

Actually I borrowed the dual semaphore approach from bgfx, and my understanding from studying that code was that you could end up with the following:

Update thread

  1. build command list
  2. render_semaphore.wait()
  3. commit command list (swaps command list indexes)
  4. update_semaphore.signal()

Sokol/main thread

  1. update_semaphore.wait()
  2. execute command list (calls actual sg_xxx commands)
  3. render_semaphore.signal()

Doing it like this allows the update thread to begin populating a new command list (step 1) while the sokol/main thread is executing the last command list (step 6).

With one semaphore I guess you wouldn't be able to have that overlap, so for example if the sokol/main thread had to wait for the display (v-sync) at step 6, then you'd be losing time when the update thread could be building a new command list...

oviano avatar Feb 27 '23 20:02 oviano

I did some tests and I think I understand now the reason of two semaphores. It is necessary to avoid two or more sequences of (step 3) before (step 6). With two semaphores you garantee the sequence (step 3), (step 6), (step 3), (step 6), etc.

Thanks, your contribuition help me a lot!!

eduardodoria avatar Feb 28 '23 12:02 eduardodoria

I did some tests and I think I understand now the reason of two semaphores. It is necessary to avoid two or more sequences of (step 3) before (step 6). With two semaphores you garantee the sequence (step 3), (step 6), (step 3), (step 6), etc.

Thanks, your contribuition help me a lot!!

Nice one - your library looks pretty well featured!

oviano avatar Feb 28 '23 12:02 oviano