sokol
sokol copied to clipboard
Feature request - multithreaded rendering
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.
The same ideas i looked in rizz. https://github.com/septag/rizz
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
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.
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.
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!
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
- build command list
- render_semaphore.wait()
- commit command list (swaps command list indexes)
- update_semaphore.signal()
Sokol/main thread
- update_semaphore.wait()
- execute command list (calls actual sg_xxx commands)
- 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...
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!!
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!