Event loop, kolejka, odczyt klawiszy

0

Zainspirowany tym co poradził mi @alagner (bardzo dziękuję!) zacząłem główkować nad przeprojektowaniem GameLoop'a w prostym silniku, który piszę. Inputy z klawiatury rzeczywiście nie powinny być synchronizowane co klatkę obrazu (przez polling), tylko przetwarzane na zasadzie kolejkowanych eventów. Wyszło mi coś takiego (to póki co prototyp):

///////////////// Objaśnienia typów:

struct Input
{
	unsigned char Index = 0 
	unsigned char State = 0;
}

InputCleanUp, InputQueue //std::queue<Input>

struct ControlState
{
	bool Pressed = false;
	bool Released = false;
	bool Held = false;
	bool DoubleClicked = false;
};

Control //ControlState[256]

//////////////// Kod właściwy:

void MainLoop()
{
	while (!InputCleanUp.empty())
	{
		switch (InputCleanUp.front().State)
		{
		case 0: //Released
			Control[InputCleanUp.front().Index].Released = false;
			break;
		case 1: //Pressed
			Control[InputCleanUp.front().Index].Pressed = false;
			break;
		case 2: //Double Clicked
			Control[InputCleanUp.front().Index].Pressed = false;
			Control[InputCleanUp.front().Index].DoubleClicked = false;
			break;
		}
		InputCleanUp.pop();
	}

	while (!InputQueue.empty())
	{
		switch (InputQueue.front().State)
		{
		case 0: //Released
			Control[InputQueue.front().Index].Released = true;
			Control[InputQueue.front().Index].Held = false;
			break;
		case 1: //Pressed
			Control[InputQueue.front().Index].Pressed = true;
			Control[InputQueue.front().Index].Held = true;
			break;
		case 2: //Double Clicked
			Control[InputQueue.front().Index].Pressed = true;
			Control[InputQueue.front().Index].DoubleClicked = true;
			Control[InputQueue.front().Index].Held = true;
			break;
		}
		InputCleanUp.push({ InputQueue.front().Index, InputQueue.front().State });
		InputQueue.pop();
	}

	//Update Game Logic
	//Render Frame
}

W WinProc obsługuję komunikaty i przy każdym WM_KEYDOWN, WM_KEYUP i odpowiednich BUTTONDBLCLK (dwuklik myszy) wrzucam do kolejki kod klawisza (z przedziału 0-255) i status (równy 0, 1 lub 2):

void Controls::AddInput(unsigned char uchIndex, unsigned char uchState)
	{
		InputQueue.push({ uchIndex, uchState });
	}

A czemu dwie kolejki? Flagi Pressed i Released mogą być zapalone tylko na czas jednej klatki (działają jak jednorazowe eventy) i po jej zakończeniu muszą zostać zgaszone, czym zajmuje się właśnie druga kolejka InputCleanUp, resetująca odpowiednie flagi.

Problem w tym, że główny MessageLoop z WinApi (odpowiedziany za obsługę windowsowych komunikatów) i mój MainLoop to dwa różne wątki, a powyższy prototyp nie jest thread-safe i potrzebuję porady jak to rozwiązać. Wiem, że mógłbym walnąć jakiegoś mutexa, ale boję się, że wtedy wątki będą się wzajemnie spowalniać (bo jeden będzie czekał na drugi) i przez to silnik będzie gubił klatki. A może zupełnie biorę się za to od złej strony i powinienem to zaprojektować inaczej?

Byłbym bardzo wdzięczny za wszelkie sugestie.

2

Weź przed switchami pobierz zawartości
InputCleanUp.front().State oraz InputCleanUp.front().Index do zmiennych lokalnych.

0

Nie do końca łapię koncepcję dwóch kolejek, imho mógłbyś w wątku klawiatury zrobić stosowny preprocessing i z głowy: masz znać stan klawiszy na czas kliku timera, a co się dzieje pomiędzy niezbyt powinno pętlę zdarzeń obchodzić imho.
Nie wiem też na ile to wykonalne, ale rozsądne zdaje się być odpytanie o stan inputu w danym ticku zegara, tzn. „zatrzaskujesz” stan klawiatury/myszki na zboczu i ew. porównujesz z poprzednim. Koniec, jak zrobisz to odpowiednio szybko - gracz nie zauważy. Wtedy temat problem wątków znika i ogólnie tematu nie ma, pytanie tylko czy możesz to robić jednowątkowo (oraz czy ma to sens, bo może syscall do czytania klawiatury trwa na tyle długo, że popsuje to fpsy).

0

@alagner:
Dlaczego dwie kolejki? Żeby w kodzie samej gry nie trzeba było wprowadzać dodatkowych flag. Bo wyobraź sobie, że chcesz zrobić coś takiego:

if (Control[Key::VK_RETURN].Pressed) OpenInventoryWindow(); //otwiera okno z przedmiotami

Gdybym odczytywał stan klawisza tylko z danego "ticku" (lub jak ja to wolę określać: z danej klatki), to ta funkcja, mimo wciśnięcia klawisza tylko raz, zostałaby wywołana od kilku do kilku tysięcy razy (zależnie od ilości klatek na sekundę i interwałów między nimi). A to dlatego, że człowiek nie jest fizycznie w stanie nacisnąć klawisza tylko raz i nawet przy frame locku na 60/30 FPS jedno naciśnięcie zostałoby rozciągnięte na wiele kolejnych klatek, a co za tym idzie, w każdej takiej klatce okno zostałoby otwarte na nowo. Żeby temu zapobiec, należałoby już w logice gry dodać sobie flagę w stylu IsInventoryWindowOpened i sprawdzać ją przed każdym wywołaniem funkcji OpenInventoryWindow. No i takich flag trzeba by tworzyć i testować mnóstwo, dziesiątki, może setki (praktycznie dla każdej akcji przypisanej do jakiegoś inputu). Żeby tego uniknąć, wzorując się na systemie zaproponowanym przez David'a Barr'a, znanego jako Javidx9, zastosowałem rozwiązanie, w którym flagi Pressed, DoubleClicked i Released mają zagwarantowaną jednorazowość (czyli 1 naciśnięcie = 1 klatka = 1 wywołanie funkcji), a potem zostają zresetowane.

Jedna kolejka przetwarza zatem input i zapala odpowiednie flagi (dając szansę by na nie zareagować w logice gry), druga zwalnia flagi na początku kolejnej klatki, by nie dopuścić do kolejnych wywołań na ich podstawie. A gdyby potrzebny był faktyczny stan klawisza w czasie rzeczywistym (wciśnięty lub wyciśnięty), to od tego właśnie jest flaga Held.

0

Możesz też poluzować zależności na wątkach.

  1. Te dwie kolejki - od klawiszy - dać na aktualizację lockami.
  2. Update Game Logic - cokolwiek to oznacza robić tak jak jest.
  3. Rendering frama dać w osobnym wątku.

Wtedy dane do renderingu wrzucasz w ostatniej chwili.
Jeśli danych zależnych od renderingu jest dużo to robisz płytką kopię obiektu i podmieniasz części (te które się zmieniły), aby nie blokować renderingu.

Tak zbudowana aplikacja powinna być banalnie prosta do testowania.

0

@Crow:

Gdybym odczytywał stan klawisza tylko z danego "ticku" (lub jak ja wolę określać: z danej klatki), to ta funkcja zostałaby wywołana od kilku do kilku tysięcy razy (zależnie od FPS).

Po to robisz event loopa, żeby to nie było zależne od fps (chyba)?

Żeby tego uniknąć, wzorując się na systemie zaproponowanym przez David'a Barr'a, znanego jako Javidx9, zastosowałem rozwiązanie, w którym flagi Pressed, DoubleClicked i Released mają zagwarantowaną jednorazowość (czyli 1 naciśnięcie = 1 klatka = 1 wywołanie funkcji), a potem zostają zresetowane.

No ale czekaj, masz stan obecny inputu (zakładam, że jesteś w ticku timera) oraz stan poprzedni (z zeszłego ticku). Czytasz czy klawisz jest "zwolniony" (powiedzmy false) czy "wciśnięty" (true).
No to wiesz, że:

Pressed=!prev&&current;
Released=prev&&!current;
Held=current; //aczkolwiek jak by zrobił prev&&current, czy tam zliczasz ile cykli current==true;

i kombinacje alpejskie z dwiema kolejkami odpadają.

EDIT: przy czym ja bym jednak polecał sprawdzać czy isInventoryOpen zamiast odnosić się do globalnego stanu klawiatury.
Pomyśl o tym tak: będziesz chciał... nie wiem, powiedzmy wzorem UT, ładować dodatkową rakietę kiedy fire jest przytrzymany przez 2 sek., ale odpalić wszystkie po 10 "no matter what" - to co, lepiej zrobić "heldFor5secs" i "heldFor10Secs", jako dwa globalne eventy czy może lepiej zliczać te cykle dla broni? Łatwiej będzie tryby przełączać lokalnie.

0
Crow napisał(a):

@alagner:

if (Control[Key::VK_RETURN].Pressed) OpenInventoryWindow(); //otwiera okno z przedmiotami

Gdybym odczytywał stan klawisza tylko z danego "ticku" (lub jak ja to wolę określać: z danej klatki), to ta funkcja, mimo wciśnięcia klawisza tylko raz, zostałaby wywołana od kilku do kilku tysięcy razy.

A jeżeli już to okienko Inventory się pojawiło to jaka ma być reakcja na ten RETURN?

0

@alagner:

alagner napisał(a):

No ale czekaj, masz stan obecny inputu (zakładam, że jesteś w ticku timera) oraz stan poprzedni (z zeszłego ticku). Czytasz czy klawisz jest "zwolniony" (powiedzmy false) czy "wciśnięty" (true).
No to wiesz, że:

Pressed=!prev&&current;
Released=prev&&!current;
Held=current; //aczkolwiek jak by zrobił prev&&current, czy tam zliczasz ile cykli current==true;

i kombinacje alpejskie z dwiema kolejkami odpadają.

No ale przecież wtedy musiałbym wrócić do pollingu (sprawdzać każdy klawisz po kolei), co do którego ustaliliśmy, że nie jest najlepszym rozwiązaniem, bo wymaga odpytywania całej klawiatury na ślepo przy każdej klatce (porównując stany klawisza z obecnej i poprzedniej klatki).

EDIT: przy czym ja bym jednak polecał sprawdzać czy isInventoryOpen zamiast odnosić się do globalnego stanu klawiatury.

Globalnego? Z tego co wiem, windowsowe komunikaty są indywidualizowane dla każdego wątku, więc to stan klawiatury tylko dla mojego silnika...?

Pomyśl o tym tak: będziesz chciał... nie wiem, powiedzmy wzorem UT, ładować dodatkową rakietę kiedy fire jest przytrzymany przez 2 sek., ale odpalić wszystkie po 10 "no matter what" - to co, lepiej zrobić "heldFor5secs" i "heldFor10Secs", jako dwa globalne eventy czy może lepiej zliczać te cykle dla broni? Łatwiej będzie tryby przełączać lokalnie.

No ale czy właśnie moja metoda nie sprzyja takiemu rozwiązaniu bardziej?

  1. Odbiera flagę Pressed i uruchamia timer zliczający czas przytrzymania klawisza.
  2. Sprawdza stan timera przy każdym ticku (klatce).
  3. Sprawdza flagę Released, jeżeli jest zapalona, zatrzymuje timer i podejmuje odpowiednie kroki zależnie od jego stanu (np. wystrzeliwuje 3 rakiety) i resetuje timer.
  4. Jeżeli nie wykryto Released, dokłada rakietę gdy stan timera wynosi 2s, 4s, 6s, 8s albo 10s.
  5. Gdy stan timera = 10s, a po drodze nie wykryto Released wypuszcza wszystkie rakiety, deaktywuje timer i resetuje go.

A bez mojego rozwiązania wyglądałoby to tak:

  1. Obiera flagę Pressed i sprawdza czy timer już działa (np. odczytując dodatkową flagę IsRocketTimerAlreadyActive). Jeżeli pomocnicza flaga nie jest aktywna, uruchamia timer i ją zapala, jeżeli jest, nie robi nic.
  2. Sprawdza stan timera przy każdym ticku (klatce).
  3. Sprawdza flagę Released, jeżeli jest zapalona, sprawdza, czy była też zapalona w ostatniej klatce (np. odczytując dodatkową flagę WasRocketAlreadyReleased). Jeżeli pomocnicza flaga była zapalona w ostatniej klatce, nie robi nic, jeżeli nie była, zatrzymuje timer i podejmuje odpowiednie kroki zależnie od jego stanu (np. wystrzeliwuje 3 rakiety), następnie zapala pomocniczą flagę i resetuje timer.
  4. Jeżeli flaga Released nie była zapalona w ostatniej klatce i nie jest zapalona teraz, dokłada rakietę gdy stan timera wynosi 2s, 4s, 6s, 8s albo 10s.
  5. Gdy stan timera = 10s, a po drodze nie wykryto Released wypuszcza wszystkie rakiety, deaktywuje timer i resetuje go.

Czy ta druga metoda nie jest, zupełnie niepotrzebnie, bardziej skomplikowana (wymaga dodatkowego sprawdzania flag i stosowania flag pomocniczych)?

0

@faffik:

Myślałem o tym, ale jak szkielet takiego rozwiązania miałby wyglądać? Coś tam ogarniam w zakresie wielowątkowości, ale nie aż tak dużo i nie jestem pewien jak powinien działać taki system. Do głowy przychodzi mi coś w tym stylu:.

std::mutex M;
std::condition_variable CV;
std::atomic<bool> ThreadAlive;

void InputLoop()
	{
		std::unique_lock<std::mutex> UL(M);

		while (ThreadAlive)
		{
			while (!InputQueue.empty())
			{
				//Obróbka klawisza
				InputQueue.pop();
			}

			CV.wait(UL);
		}	
	}
};

void MainLoop()
{
	CV.notify_one(); //Wybudza wątek obrabiający klawisze, wątek opróżnia kolejkę i znowu zasypia, aż do następnej klatki.
	//Update Game Logic
	//Render Frame - ewentualnie to też do osobnego wątku, ale po kolei, najpierw muszę uporać się z inputami :).
}

To właściwy trop? Byłbym bardzo wdzięczny za jakieś wskazówki, choćby w formie pseudo kodu.

A może wybudzanie powinno następować na po

0

@Crow: googlasz i masz:
https://stackoverflow.com/a/16075550/4885321
Analogiczna implementacja jest w C++ Concurrency in action Williamsa.

0

@Crow
Bardziej chodziło mi tutaj o niezależne działanie.
Model synchronizacji może być jeden (jedna baza kodowa - zalecane rozwiązanie - mniej testowania i z reguły łatwiejsze w obsłudze i implementacji).

Nie wiem jak wygląda Twój pozostały kod.
Jeśli dobrze się domyślam to powinieneś mieć coś takiego jak (dzieląc to na niezależne moduły)
Struktura:

  1. Odczyt klawiszy (te dwie kolejki)
  2. Reakcja logiki
  3. Rendering sceny

Działanie:

  1. Odczyt klawiszy
  2. Odczytane klawisze trafiają do logiki - tam następuje przetwarzanie
  3. Logika generuje dane / obiekty które mają być wyrenderowane
  4. Klasa renderująca pobiera te dane i renderuje klatkę

Miałem na myśli, aby odczyt klawiszy żył własnym życiem i wysyłał dane do logiki.
Logika żyje własnym życiem i generuje dane do renderingu
Rendering żyje własnym życiem - pobiera dane (do renderingu) takie jakie ma lub czeka niewielki czas, jeśli akurat trafiła na moment aktualizacji danych. Jeśli dane nie są aktualizowane (nie są gotowe lub się nie zmieniły) to rendering renderuje normalnie - olewając co się dzieje w pozostałych częściach.

Wtedy między odczytem klawiszy a logiką masz jednego locka do update danych (jeden mutex - tylko na czas aktualizacji),
Pomiędzy logiką a danymi do renderingu masz jednego locka (ten sam schemat)
Rendering odwołuje się do tego samego locka co aktualizacja danych przez logikę - lub innego to zależy co tam masz.

Jeśli aktualizujesz niewielkie ilości danych lub robisz płytką kopię / podmieniasz / swapujesz, problem z blokowaniem się wątków (wydajnością) nie powinien wystąpić (po prostu nie ma co ich zablokować, operacje są bardzo szybkie)

W najprostszym wypadku potrzebujesz tylko jednego mutexa na każde połączenie.
Logika może mieć waita, natomiast rendering raczej nie powinien mieć waita.

0

@faffik

No właśnie zastanawiam się, co będzie najkorzystniejsze (póki co skupiam się na rozwiązaniu kwestii klawiszy). Bo mógłbym to (chyba?) zrobić tak, że odświeżanie klawiszy (przepisywanie ich z kolejki do tablicy) mogłoby być obsługiwane przez osobny wątek i wymieniać się dostępem z wątkiem głównym, tj., tym odpowiedzialnym za windowsowy MessageLoop:

//Pseudo kod:

//Funkcja używana przez główny wątek:
void AddInputToQueue()
{
	InputMutex.lock();
	//dodaje klawisz do kolejki
	InputMutex.unlock();
}

//Funkcja używana przez wątek do obróbki klawiszy:
void HandleTheQueue()
{
	InputMutex.lock();
	//przepisuje klawisze do tablicy, opróżnia kolejkę
	InputMutex.unlock();
}

Wtedy MainLoop (nie biorący bezpośredniego udziału w obsłudze klawiszy) mógłby zachowywać się na dwa sposoby:

void MainLoop()
{
	InputMutex.lock();
	//Update Game Logic
	InputMutex.unlock();
	//Render Frame
}

Czyli wtedy odświeżanie klawiszy byłoby wstrzymywane na czas odświeżania logiki gry. Minus byłby taki, że wtedy czasem to logika gry musiałaby czekać na uwolnienie mutexa, żeby móc go przejąć (co mogłoby powodować jakieś tam opóźnienia czy nawet obniżać FPS. Tylko czy to w ogóle mogłoby być znaczące? W końcu ile może trwać przepisane 5-10 elementów, po 2 bajty każdy, z kolejki do tablicy?).

Alternatywnie MainLoop mógłby to robić tak:

void MainLoop()
{
	//CanUpdate = std::atomic<bool>

	CanUpdate = false;
	//Update Game Logic
	CanUpdate = true;
	//Render Frame
}

Wtedy po prostu MainLoop zapalałby flagę, która blokowałaby przepisywanie kolejki do tablicy (ale nowe inputy nadal mogłyby być dodawane do kolejki). Tutaj minus byłby z kolei taki, że czasem nie wszystkie inputy dałyby radę złapać się na jedną klatkę. Np. zakolejkowane było 5, ale do tablicy udało się wpisać tylko 3, więc pozostałe 2 będą przepisane dopiero później (przy kolejnej klatce).

No i pytanie, która opcja jest lepsza? A może w ogóle źle kombinuję?

A jeżeli opcja 1 jest ok (ta z twardym blokowaniem mutexem), to może w ogóle nie warto tworzyć osobnego wątku dla odświeżania klawiszy (przepisywania ich z kolejki do tablicy), tylko zostawić to w MainLoopie (tak jak zaproponowałem w pierwszym poście) ale z dołożonym mutexem (co spowalniałoby kolejkowanie nowych klawiszy na czas blokady mutexem, ale to chyba akurat niewielki problem)? Bo to chyba wyjdzie na to samo?

1

@Crow pozwolę sobie jeszcze na chwilę odejść od tematu wątków, wrócić do released/pressed i Twojego wcześniejszego posta: jak chcesz o tym konkretnie pogadać to załóż nowy temat, bo tu się może bajzel zrobić:

ja generalnie skłaniałbym się do opcji nr 2, bo enkapsualacja lepsza a większa dostępność do software'owych timerów to jest wartość, nie kompilkacja. IsRocketTimerAlreadyActive jest polem wewnętrznym wyrzutni/gracza. Ad.3 - nie czytasz dodatkowych flag tylko wykorzystujesz maszynę stanów (przykładowa implementacja tu: https://gameprogrammingpatterns.com/state.html), możesz nawet zrobić hierarchiczną.
Rozrysuj sobie graf przejść-wyjść poszczególnych elementów gry, to Ci się to uprości. Fakt, można przegiąć i czasem flaga będzie lepsza, ale dla bardziej skomplikowanych sekwencji ten wzorzec Ci się opłaci.
Odnoszę wrażenie, że bardzo starasz się pracować na zmiennych globalnych/"paraglobalnych". A jedyne co jest globalne (w idealnym przypadku) to stan inputu i może ekranu.

0

@alagner:
No ja bym to widział tak jak pisałem w pierwszym poście i poście powyżej:

//Pseudo kod:

void MainLoop()
{
    InputMutex.lock();
    //Kolejka czyszcząca flagi klawiszy
    //Kolejka przepisująca klawisze do tablicy Control[256] (zawierającej structa z flagami Pressed, Released, Double Clicked i Held)...
    //...i dodająca elementy do kolejki czyszczącej
    //Odświeżenie logiki gry (na bazie tablicy).
    InputMutex.unlock();
    //Renderowanie klatki obrazu
}

Jak rozumiem, model preferowany przez ciebie wyglądałby identycznie, tylko zamiast dwóch kolejek byłaby jedna, a zamiast 4 flag byłyby 2 - Up i Down, które byłyby odświeżane tylko w momencie kliknięcia bądź odpuszczenia klawisza. Zgadza się czy ja coś źle rozumiem?

1

Zastanów się nad:

#include <algorithm>
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;

class KeySlower;

enum KeyState { Pressed,Realized,Held,BitSize };

class CallList
{
	private:
	vector<KeySlower*> list;
	CallList() {}
	public:
	static CallList *inst()
	{
		static CallList cs;
		return &cs;
	}
	void add(KeySlower *ks) { list.push_back(ks); }
	void del(KeySlower *ks) 
	{
		vector<KeySlower*>::iterator end=remove(list.begin(),list.end(),ks);
		list.resize(end-list.begin());
	}
	void keyinvoke(int key,KeyState st)const;
	void nextframe()const;
};

class KeySlower
{
	private:
	int key;
	bitset<KeyState::BitSize> state,nextstate;
	public:
	KeySlower(int key):key(key) { CallList::inst()->add(this); }
	~KeySlower() { CallList::inst()->del(this); }
	void keyinvoke(int key,KeyState st)
	{
		if(key!=this.key) return;
		nextstate.set(st);
		// pozostała logika
	}
	void nextframe() { state=nextstate; }
	bool presed()const { return state.test(KeyState::Pressed); }
	bool realized()const { return state.test(KeyState::Realized); }
	bool held()const { return state.test(KeyState::Held); }
};

void CallList::keyinvoke(int key,KeyState st)const { for(KeySlower *ks:list) ks->keyinvoke(key,st); }
void CallList::nextframe()const { for(KeySlower *ks:list) ks->nextframe(); }


int main()
{
	KeySlower ret(13); // lapiemy enter
	KeySlower esc(27); // lapiemy escape
	
	CallList::inst()->keyinvoke(60,KeyState::Pressed); // Wywolania pętli WinMain
	CallList::inst()->keyinvoke(13,KeyState::Pressed); // Wywolania pętli WinMain
	CallList::inst()->keyinvoke(27,KeyState::Pressed); // Wywolania pętli WinMain
	cout<<"ret.presed() == "<<ret.presed()<<endl;
	cout<<"esc.presed() == "<<esc.presed()<<endl;
	CallList::inst()->nextframe(); // Wywolanie przy starcie klatki
	cout<<"ret.presed() == "<<ret.presed()<<endl;
	cout<<"esc.presed() == "<<esc.presed()<<endl;
	
	return 0;
}

Wtedy obrabiasz wyłącznie "zarejestrowane klawisze", ba żadnych locków i/lub dodatkowych wątków.

0

Żeby nie zostawiać wątku bez rozwiązania (może ktoś kiedyś na niego trafił), ostatecznie zdecydowałem się na coś takiego:

void MainLoop()
{
	Controls.Update(); //<-- Synchronizuje kolejkę klawiszy z tablicą
	//Update Game Logic
	Controls.CleanUp(); //<-- Czyści flagi
	//Draw Frame
}

Sama kolejka wygląda tak:

struct Input
{
	unsigned char Index;
	unsigned char State;
	void Set(const unsigned char& uiIndex, const unsigned char& uchState)
	{
		Index = uiIndex;
		State = uchState;
	}
};

class InputQueue
{
private:
	Input DataA[256], DataB[256];
	Input* Gather = DataA;
	Input* Process = DataB;
	std::mutex Mutex;
public:
	unsigned char& GatherSize()
	{
		return Gather[0].Index; //Wielkość kolejki (ilość elementów) zapisywana jest w pierwszym indexie, w zmiennej... Index.
	}
	unsigned char& ProcessSize()
	{
		return Process[0].Index; //Wielkość kolejki (ilość elementów) zapisywana jest w pierwszym indexie, w zmiennej... Index.
	}
	void Swap()
	{
		std::lock_guard<std::mutex> Guard(Mutex);
		std::swap(Gather, Process);
		Gather[0].Index = 0;
	}
	void AddInput(const unsigned int& uiIndex, unsigned char uchState)
	{
		std::lock_guard<std::mutex> Guard(Mutex);
		Gather[++Gather[0].Index].Set(uiIndex, uchState);
	}
	void AddSpecialInput(const unsigned int uiIndex, const unsigned char uchState);
	{
		Process[++Process[0].Index].Set(uiIndex, uchState);
	}
	Input* begin()
	{
		return &Process[1];
	}
	Input* end();
	{
		return Process + (Process[0].Index + 1);
	}
};


void Controls::Update()
{
	if (Queue.GatherSize())
	{
		Queue.Swap();
		for (Input& I : Queue)
		{
			switch (I.State)
			{
			case 0: //Released
				Control[I.Index].R = true;
				Control[I.Index].H = false;
				break;
			case 1: //Pressed
				Control[I.Index].P = true;
				Control[I.Index].H = true;
				break;
			case 2: //Double Clicked
				Control[I.Index].P = true;
				Control[I.Index].S = true;
				Control[I.Index].H = true;
				break;
			}
				if (I.Index == 0x2C) Control[I.Index].S = false;
		}
	}

	//Print Screen

	if ((GetKeyState(0x2C) & 0x80) && !Control[0x2C].S)
	{
		Control[0x2C].P = true;
		Control[0x2C].H = true;
		Control[0x2C].S = true;
		Queue.AddSpecialInput(0x2C, 1);
	}

	//Toggle Keys

	if (GetKeyState(0x14) & 0x1) //CapsLock Toggle
	{
		if (!Control[0x14].S) Control[0x14].S = true;
	}
	else
	{
		if (Control[0x14].S) Control[0x14].S = false;
	}

	if (GetKeyState(0x90) & 0x1) //NumLock Toggle
	{
		if (!Control[0x90].S) Control[0x90].S = true;
	}
	else
	{
		if (Control[0x90].S) Control[0x90].S = false;
	}

	if (GetKeyState(0x91) & 0x1) //ScrollLock Toggle
	{
		if (!Control[0x91].S) Control[0x91].S = true;
	}
	else
	{
		if (Control[0x91].S) Control[0x91].S = false;
	}
}

void Controls::CleanUp()
{
	if (Queue.ProcessSize())
	{
		for (Input& I : Queue)
		{
			switch (I.State)
			{
			case 0: //Released
				Control[I.Index].R = false;
				break;
			case 1: //Pressed
				Control[I.Index].P = false;
				break;
			case 2: //Double Clicked
				Control[I.Index].P = false;
				Control[I.Index].S = false;
				break;
			}
		}
	}
}

Kolejka składa się z dwóch statycznych tablic, do których dostęp można uzyskać poprzez wskaźniki. Jedna tablica służy tylko do zbierania nowych inputów (dodawanych do listy przez windowsowe komunikaty jak WM_KEYDOWN czy WM_KEYUP), a druga do ich faktycznej obróbki (konwertowania inputów na właściwe flagi). Sztuczka polega na tym, że na początku każdej klatki wskaźniki są zamieniane (funkcja swap()) i jest to jedyny, krótki moment, który wymaga zamknięcia mutexem, w pozostałej części tablice działają thread-safe. Nie ma też już przepisywania danych z jednej tablicy do drugiej (jak wcześniej), bo teraz jedna wystarczy do ustawienia stosownych flag na początku klatki, a potem ich zresetowania na końcu tej samej klatki. Dodatkowo kolejki nie są czyszczone (zerowane), a jedynie nadpisuje się ich wcześniejsza zawartość (aktualizowana jest zmienna przechowująca ilość elementów, ukryta w pierwszym indexie), więc powinny być raczej szybkie.

Print Screen jest "dziwnym" klawiszem, bo nie wysyła komunikatu WM_KEYDOWN i trzeba go testować ręcznie, w każdej klatce z osobna, a potem porównywać stan z ostatniej i obecnej klatki. Wykorzystuję do tego flagę S (specjalną), która normalnie służy do przechowywania dodatkowych informacji o specjalnych klawiszach (podwójne kliknięcie w przypadku przycisków myszy, albo też stan kontrolki w przypadku klawiszy typu LOCK - Scroll, Num i Caps). Zwykłe klawisze jej nie używają (nie jest w ogóle widoczna), więc mogłem się nią posłużyć.

Gdyby ktoś miał jakieś uwagi, chętnie wysłucham. Dziękuję też wszystkim, którzy udzielili mi konstruktywnej pomocy i wskazówek.

0
    if (GetKeyState(0x90) & 0x1) //NumLock Toggle
    {
        if (!Control[0x90].S) Control[0x90].S = true;
    }
    else
    {
        if (Control[0x90].S) Control[0x90].S = false;
    }

Doprawdy?
Czy zdajesz sobie sprawy że to jest równoważne:
Control[0x90].S = (GetKeyState(0x90) & 0x1);

Czyli 8 długo działających zagmatwanych wierszy zamiast jednego szybkiego i czytelnego?
To samo z resztą tego kodu wyżej.

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