Czy to jest znośnie zrobiony system do renderowania wielowątkowego w opengl?

0

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:

  1. Tylko do tego żeby nie blokował głównego wątku, na którym działają eventy SDL
  2. Wątek do "symulacji". Updateuje dane i kolejkuje komendy opengl
  3. 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();
	}

0
JarekBisnesu napisał(a):
	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;
	}

To nie jest thread safe z punktu widzenia wielu pisarzy. 2 wątki mogą dostać to samą starą pozycję. Beż użycie compare and swap (`compare_exchange_*`) raczej tego nie rozwiążesz. Jeżeli jest tylko jeden pisarz to OK.



> ```cpp
> 	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;
> 	}

To też działa tylko w przypadku jednego czytającego. W przypadku 2 czytających (których Ty nie masz) istnieje możliwość zwrócenia 2 razy tej samej wartości.

> 			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);
> 			}

Potencjalnie pętla renderowania może nigdy się nie skończyć. Po simulationDone = true; nie ma żadnej bariery pamięci, nie wiadomo kiedy i czy simulationDone zostanie zapisane do pamięci (może zostać zoptymalizowane do zmiennej rejestrowej). Warunek while (!(queue.empty() && simulationDone)) także może operować na zmiennej rejestrowej. To powinien być atomic.

> void UpdateRenderThread(WaitFreeQueue<RenderCmdQueue*, 254>& queue)
> {
> 	RenderCmdQueue* renderCmdQueue = nullptr;
> 	if (queue.pop(renderCmdQueue))
> 	{
> 		renderCmdQueue->Invoke();
> 		delete renderCmdQueue;
> 	}
> }

Nie bardzo rozumiem po co operujesz na new i delete. RenderCmdQueue to bardzo mała klasa, raczej przekazywałbym ją przez wartość.

0
nalik napisał(a):
JarekBisnesu napisał(a):
	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;
	}

To nie jest thread safe z punktu widzenia wielu pisarzy. 2 wątki mogą dostać to samą starą pozycję. Beż użycie compare and swap (`compare_exchange_*`) raczej tego nie rozwiążesz. Jeżeli jest tylko jeden pisarz to OK.



> ```cpp
> 	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;
> 	}

To też działa tylko w przypadku jednego czytającego. W przypadku 2 czytających (których Ty nie masz) istnieje możliwość zwrócenia 2 razy tej samej wartości.

> 			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);
> 			}

Potencjalnie pętla renderowania może nigdy się nie skończyć. Po simulationDone = true; nie ma żadnej bariery pamięci, nie wiadomo kiedy i czy simulationDone zostanie zapisane do pamięci (może zostać zoptymalizowane do zmiennej rejestrowej). Warunek while (!(queue.empty() && simulationDone)) także może operować na zmiennej rejestrowej. To powinien być atomic.

> void UpdateRenderThread(WaitFreeQueue<RenderCmdQueue*, 254>& queue)
> {
> 	RenderCmdQueue* renderCmdQueue = nullptr;
> 	if (queue.pop(renderCmdQueue))
> 	{
> 		renderCmdQueue->Invoke();
> 		delete renderCmdQueue;
> 	}
> }

Nie bardzo rozumiem po co operujesz na new i delete. RenderCmdQueue to bardzo mała klasa, raczej przekazywałbym ją przez wartość.

Z tego co wiem wait free queue jest tylko single producer - single consumer.

Potencjalnie pętla renderowania może nigdy się nie skończyć

Nie bardzo rozumiem co masz tutaj na myśli.

simulationDone jest atomic więc chyba rozwiązuje to problem bariery. Akurat z tymi atomikami i barierami to czasami rozumiem, a czasami średnio.

Pierwszy zamysł miał być taki - i chyba nawet przy nim zostanę - że RenderCmdQueue zamiast zadanie przyjmuje klasę RenderCmd, która jest bazową klasą dla np. DrawCmd, ViewportCmd, itd. ale dla sprawdzenia czy to w ogóle działa, zmieniłem na taki system. W kodzie testowym nie jest to duży problem.

0
JarekBisnesu napisał(a):

Z tego co wiem wait free queue jest tylko single producer - single consumer.

Pierwsze słyszę. Ta implementacja na pewno.

Potencjalnie pętla renderowania może nigdy się nie skończyć

Nie bardzo rozumiem co masz tutaj na myśli.

simulationDone jest atomic więc chyba rozwiązuje to problem bariery. Akurat z tymi atomikami i barierami to czasami rozumiem, a czasami średnio.

Moja, gafa, użyłeś operatora= zamiast store jak wcześniej, więc uznałem, że to nie atomic. Ten fragment jest OK.

0

Pierwsze słyszę. Ta implementacja na pewno.

Rzeczywiście, masz rację. Może też być wiele pisarzy i czytających.

Moja, gafa, użyłeś operatora= zamiast store jak wcześniej, więc uznałem, że to nie atomic. Ten fragment jest OK.

Tak, mój błąd. Czasami zapominam używać store i load.

1 użytkowników online, w tym zalogowanych: 0, gości: 1