Interesuje mnie renderowanie przy użyciu co najmniej 2 wątków. Do tworzenia okna i contekstu korzystam z SDL2
.
Co można tutaj poprawić?
Czy to jest poprawna implementacja renderowania wielowątkowego?
Wszystko działa, problemów żadnych nie ma, ale po prostu zastanawiam się czy można to tak zostawić.
Stworzyłem 3 nowe wątki:
- Tylko do tego żeby nie blokował głównego wątku, na którym działają eventy SDL
- Wątek do "symulacji". Updateuje dane i kolejkuje komendy opengl
- Wykonuje komenty z kolejki, po kolei.
Kolejka jest typu thread-safe wait free
template<typename T, uint bufferSize = 1>
class WaitFreeQueue
{
public:
typedef T* iterator;
typedef const T* const_iterator;
WaitFreeQueue() = default;
~WaitFreeQueue() = default;
inline bool push(const T& newElement);
inline bool pop(T& returnedElement);
inline bool empty() const noexcept { return readPosition.load() == writePosition.load(); }
inline void clear()
{
readPosition.store(0);
writePosition.store(0);
std::memset(ringBuffer.data(), 0, sizeof(ringBuffer[0]) * ringBufferSize);
}
private:
static constexpr uint getPositionAfter(uint position) noexcept
{
return ++position == ringBufferSize ? 0 : position;
}
static constexpr uint ringBufferSize = bufferSize + 1;
std::array<T, ringBufferSize> ringBuffer;
std::atomic<uint> readPosition = 0;
std::atomic<uint> writePosition = 0;
};
template<typename T, uint size>
inline bool WaitFreeQueue<T, size>::push(const T& newElement)
{
uint oldWritePosition = writePosition.load();
uint newWritePosition = getPositionAfter(oldWritePosition);
if (newWritePosition == readPosition.load()) {
return false;
}
ringBuffer[oldWritePosition] = newElement;
writePosition.store(newWritePosition);
return true;
}
template<typename T, uint size>
inline bool WaitFreeQueue<T, size>::pop(T& returnedElement)
{
uint oldWritePosition = writePosition.load();
uint oldReadPosition = readPosition.load();
if (oldWritePosition == oldReadPosition) {
return false;
}
returnedElement = ringBuffer[oldReadPosition];
readPosition.store(getPositionAfter(oldReadPosition));
return true;
}
Do komend mam klasę, która przyjmuje zadania/komendy, a w niej metodę, która wykonuje to zadanie.
struct RenderCmdQueue
{
using Cmd = std::function<void()>;
RenderCmdQueue(Cmd command)
{
cmd = command;
}
void Invoke()
{
cmd();
}
private:
Cmd cmd;
};
Wszystko dzieje się w 2 głównych metodach:
void UpdateSimulationThread(WaitFreeQueue<RenderCmdQueue*, 254>& queue)
{
// Update gameplay here.
// Determine what to draw based on the new game state below.
// The graphics commands will be queued up on the render thread
// which will execute the graphics API calls.
queue.push(new RenderCmdQueue([]() {
glViewport(0, 0, 1600, 900);
}));
queue.push(new RenderCmdQueue([]() {
glClearColor(1, 0, 1, 1);
}));
queue.push(new RenderCmdQueue([]() {
glClear(GL_COLOR_BUFFER_BIT);
}));
}
void UpdateRenderThread(WaitFreeQueue<RenderCmdQueue*, 254>& queue)
{
RenderCmdQueue* renderCmdQueue = nullptr;
if (queue.pop(renderCmdQueue))
{
renderCmdQueue->Invoke();
delete renderCmdQueue;
}
}
i pętla
void ThreadLoop()
{
// Thread to not block SDL main thread that handles events
std::thread loopThread([&]() {
WaitFreeQueue<RenderCmdQueue*, 254> queue;
std::atomic<bool> simulationDone = false;
SDL_GL_MakeCurrent(loopThreadContext);
while (!quit) {
simulationDone = false;
queue.clear();
std::thread simulationThread = std::thread([&] {
UpdateSimulationThread(queue);
simulationDone = true;
});
std::thread renderThread = std::thread([&] {
SDL_GL_MakeCurrent(renderThreadContext);
// Continue to read data from the ring buffer until it is both empty
// and the simulation thread is done submitting new items into the ring buffer.
while (!(queue.empty() && simulationDone)) {
UpdateRenderThread(queue);
}
});
// Ensure that both the simulation and render threads have completed their work.
simulationThread.join();
renderThread.join();
SDL_GL_SwapWindow(window);
}
});
loopThread.detach();
}