Wątek przeniesiony 2021-10-04 15:51 z Algorytmy i struktury danych przez Shalom.

Silnik gry - obsługa myszy

0
Crow napisał(a):
katakrowa napisał(a):

Jeżeli twoim zdaniem oparcie odświeżania logiki gry na Sleep() jest dobrym pomysłem, to zupełnie nie ogarniasz...

Nie martw się do 200 klatek na sekundę w zupełności Ci wystarczy.

Precyzja sleep() w zupełności wystarczy jeśli nie szczypiesz się o jakieś ułamki obciążenia pojedynczego rdzenia CPU. Większą część do odczekania czekasz sleep() a resztę "doczekujesz" precyzyjnym while... I ot Twój wielki problem rozwiązanay. Użyj do tego np.: https://docs.microsoft.com/en[...]/nf-sysinfoapi-gettickcount64 masz dokładność 10-15milisekund.
Większa Ci potrzebna? Nie sądzę abyś obecnie używał precyzyjniejszego zegara.

0
Crow napisał(a):

Bazując na informacjach, które czytałem odnośnie Direct'a 2D (tego API używam), V-SYNC działa tam po prostu tak, że po zakończeniu renderowania danej klatki, blokuje wątek CPU obsługujący render. Następnie, we właściwym monecie, zostaje on wybudzony przez odpowiedni Notify Event wysłany przez GPU (co jest oczywiście powiązane z częstotliwością odświeżania monitora).

No to SDL robi dokładnie tak samo. Żeby mieć V-Sync i jednocześnie obsługiwać logikę z większą częstotliwością niż odświeżanie ekranu, logika musi być wykonywana w osobnym wątku, a renderowanie w głównym, czyli tym, który jest blokowany przez V-Sync.

Nie wykluczone, że w twojej implementacji to działa, ale jednak większość źródeł odradza takie podejście, nazywając je naiwnym i na dłuższą metę problematycznym.

Problematyczne to będzie, jak się ten mechanizm wykorzysta do celów, do których się nie nadaje. W przypadku gier, których logika i renderowanie nie są wymagające, sprawdza się wyśmienicie i działa bezbłędnie na każdej maszynie, wedle założeń.

Dlatego właśnie szukam "profesjonalnego" podejścia, tzn. takiego, którego używa się w "prawdziwym" gamedevie. Niestety, zaskakująco trudno dokopać się do jakichś konkreteów na ten temat.

Bo może szukasz czegoś, co po prostu nie istnieje? Nie potrzebujesz żadnych materiałów na ten temat, bo sam możesz taki mechanizm zaklepać. Wystarczy wziąć pod uwagę właściwości silnika i specyfikę wymagania jakie masz (i jakich my nie znamy, boś ich nie podał) i na tej podstawie dobrać odpowiednie rozwiązanie.

Powyższe nie rozwiązuje jednak problemu zarządzania mocą obliczeniową CPU, bo tam też wątek śmiga ile fabryka dała...

Oczywiście, że rozwiązuje, bo do tego celu został stworzony i moje gierki są tego dowodem. Fairtris działa w 60fps lub 50fps (w zależności od wybranego regionu w ustawieniach rozgrywki), zużywając raptem 3-4% mocy obliczeniowej jednego rdzenia CPU — reszta jest do dyspozycji innych procesów i wątków.

Jeśli logikę przetwarzasz w wątku „ile fabryka dała”, to się nie dziw, że zjadasz całą dostępną moc rdzenia. Chcesz oszczędzić moc obliczeniową? Wykorzystaj coś w rodzaju Sleep i oddaj systemowi moc, której nie potrzebujesz.


Crow napisał(a):

Jeżeli twoim zdaniem oparcie odświeżania logiki gry na Sleep() jest dobrym pomysłem, to zupełnie nie ogarniasz...

Mam wrażenie, że sam nie rozumiesz. Masz dosłownie trzy wyjścia:

  1. nie używasz Sleep — wątek zżera całą dostępną moc, framerate jest zmienny,
  2. używasz zmiennego Sleep — wątek zużywa minimum mocy, resztę oddaje systemowi, framerate jest stały,
  3. używasz stałego Sleep — wątek zużywa minimum mocy, resztę oddaje systemowi, framerate jest zmienny.

Innej opcji nie ma i nie będzie.

Ale ja nie chcę 200 klatek, tylko możliwości własnoręcznego ustawienia limitu odświeżania, z opcją precyzyjnego odmierzania interwałów między iteracjami […]

Ustalenie własnego limitu klatek na sekundę to nic innego jak odpowiednie obliczanie interwału dla Sleep. Im dłuższy Sleep, tym mniej klatek będzie renderowanych w czasie jednej sekundy. Dokładnie to dzieje się w Deep Platformerze — w trybie debug mogę zmieniać limit od 1fps to 60fps, im mniej klatek tym więcej mocy jest oszczędzanej. Tyle że platformer działa zgodnie z NES-em, czyli nie używa delty.

[…] i bez niepotrzebnego busy waitingu, zjadającego moc obliczeniową CPU.

Nie da się zrobić precyzyjnego odmierzania czasu bez busy waitingu pod Windows, bo żaden systemowy mechanizm wstrzymywania pracy wątku nie zapewnia mikrosekundowej precyzji, która była by wystarczająca — koniec kropka. Bazowanie na samym Sleep pozwoli osiągnąć w miarę stabilny klatkaż, ale tylko w miarę stabilny — rozrzut będzie pomiędzy 58fps a 62fps. Wiem, bo probowałem wielokrotnie na przestrzeni ostatnich lat i po prostu się nie da, bo Sleep jest nieprecyzyjny. A innego, bardziej precyzyjnego jego odpowiednika nie ma w Windows.

Chcesz się przekonać? Pobierz Fairtrisa, zaremuj busy waiting, skompiluj i odpal. Dostaniesz to:

fairtris framerate.gif

Ma być sztywne 60fps, ale niedokładność Sleep (tu: SDL_Delay) powoduje wahania. Przy czym Fairtris ma wybitnie prosta logikę, więc w każda klatka generowana jest w niemal identycznym czasie. W przypadku gdy logika będzie wykonywana od kilku do kilkunastu milisekund, w zależności od danej sceny, wahania mogą być i będą większe. Nieważne czy podzielisz proces na kilka wątków, czy będziesz używał jednego.


A jeśli chodzi o deltę, to nie jest ona w żaden sposób problematyczna. Jeśli gra działa stabilnie i ma zapas mocy obliczeniowej, wykonujesz jeden update logiki per klatka. Jeśli zdarzy się turbo-lag, logikę aktualizujesz w pętli, małymi skokami, tak aby wyeliminować błędy fizyki (np. przechodzenie przez ściany).

Upraszczając, chcesz mieć 60fps, czyli na jedną klatkę przypada delta w rozmiarze 1/60, czyli ~0.0166. Tę deltę używasz jako mnożnik do obliczania ruchu obiektów. Zdarza się lag, dostajesz deltę o wartości 0.200, czyli lag trwał 1/5 sekundy, czyli 200ms. Jeśli takiej delty użyjesz jako mnożnika, obiekt może przeniknąć przez ścianę. Temu trzeba zaradzić.

Delta jest zbyt duża, więc w pętli odejmujesz ~0.0166 od obecnej delty i wykonujesz logikę dotąd, aż obecna delta będzie mniejsza niż ~0.0166. Kończysz pętlę, a pozostąłą deltę używasz jeszcze raz do jeszcze jednej aktualizacji logiki. W ten sposób lag nie zepsuje Ci silnika. Coś a la poniższe:

//metoda aktualizacji logiki
procedure UpdateLogic(ADelta: Single);
begin
  // tu używasz delty jako mnożnika
  // delta nigdy nie jest zbyt duża
end;

// metoda generowania klatki
procedure Update(ADelta: Single);
begin
  // nie ma laga
  if ADelta < 1 / 60 {+ malutki margines} then
    UpdateLogic(ADelta)
  else
  // jest lag
  begin
    repeat
      UpdateLogic(1 / 60);
      ADelta -= 1 / 60;
    until ADelta < 1 / 60;

    UpdateLogic(ADelta);
  end;
end;

To jest bardzo mocno uproszczona wersja, tylko do zobrazowania sytuacji. Wyżej użyłem wartości 60, co da 60fps. Jeśli zamiast literału użyjesz zmiennej przechowującej limit klatkażu, będziesz mógł tym limitem manipulować, a kod się do niego zastosuje.

0
furious programming napisał(a):

Problematyczne to będzie, jak się ten mechanizm wykorzysta do celów, do których się nie nadaje. W przypadku gier, których logika i renderowanie nie są wymagające, sprawdza się wyśmienicie i działa bezbłędnie na każdej maszynie, wedle założeń.

Bo może szukasz czegoś, co po prostu nie istnieje? Nie potrzebujesz żadnych materiałów na ten temat, bo sam możesz taki mechanizm zaklepać. Wystarczy wziąć pod uwagę właściwości silnika i specyfikę wymagania jakie masz (i jakich my nie znamy, boś ich nie podał) i na tej podstawie dobrać odpowiednie rozwiązanie.

Ja szukam standardowego rozwiązania stosowanego w profesjonalnych implementacjach, które nada się nawet do najbardziej wymagających zastosowań, nie tylko prostych gierek. Nie wiem, czy zdołam je zrozumieć i zastosować u siebie, ale chcę je przynajmniej poznać, z czystej ciekawości, po prostu.

Oczywiście, że rozwiązuje (...)

Jak już pisałem, chodziło mi o artykuł z linka, nie twoją implementację.

Mam wrażenie, że sam nie rozumiesz. Masz dosłownie trzy wyjścia:

  1. nie używasz Sleep — wątek zżera całą dostępną moc, framerate jest zmienny,
  2. używasz zmiennego Sleep — wątek zużywa minimum mocy, resztę oddaje systemowi, framerate jest stały,
  3. używasz stałego Sleep — wątek zużywa minimum mocy, resztę oddaje systemowi, framerate jest zmienny.

Innej opcji nie ma i nie będzie.

Ja rozumiem, że muszę użyć czegoś, co daje efekt JAK Sleep(), ale to jednocześnie nie może być Sleep(). Właśnie o to chodzi, że próbuję rozgryźć czego używa się profesjonalnie, natomiast wiem na 100%, żeby trzymać się z daleka od Sleep(), bo jest to rozwiązanie odradzane dosłownie wszędzie i nie spotkałem się z ani jedną sugestią jego zastosowania na potrzeby gry, choćby nawet w formie hybrydowej.

Ustalenie własnego limitu klatek na sekundę to nic innego jak odpowiednie obliczanie interwału dla Sleep. Im dłuższy Sleep, tym mniej klatek będzie renderowanych w czasie jednej sekundy. Dokładnie to dzieje się w Deep Platformerze — w trybie debug mogę zmieniać limit od 1fps to 60fps, im mniej klatek tym więcej mocy jest oszczędzanej. Tyle że platformer działa zgodnie z NES-em, czyli nie używa delty.

Jak wyżej, zdaję sobie sprawę z konieczności odmierzania interwałów i wygaszania wątku na zadany czas, po prostu muszę znaleźć zamiennik dla Sleep(). @alagner podsunął mi pomysł z CreateWaitableTimer(), więc poczytam coś więcej na ten temat i zobaczę jak tam wygląda kwestia precyzji. Czytałem też o innej konstrukcji, tj. takiej, w której wątek z logiką gry blokuje się po każdej iteracji i jest budzony przez wątek renderujący (który działa z opóźnieniem o jedną iterację, przechowywaną w buforze). Co prawda sprawia to, że odświeżanie logiki jest zsynchronizowane z renderowaniem klatek, ale mimo wszystko pozwala na jakąś formę równoległości wątków (rysowanie klatki i odświeżanie logiki odbywa się w tym samym czasie) i jednocześnie korzysta z dobrodziejstwa V-SYNC'a.

Nie da się zrobić precyzyjnego odmierzania czasu bez busy waitingu pod Windows, bo żaden systemowy mechanizm wstrzymywania pracy wątku nie zapewnia mikrosekundowej precyzji — koniec kropka. Bazowanie na samym Sleep pozwoli osiągnąć w miarę stabilny klatkaż, ale tylko w miarę stabilny — rozrzut będzie pomiędzy 58fps a 62fps. Wiem, bo probowałem wielokrotnie na przestrzeni ostatnich lat i po prostu się nie da, bo Sleep jest nieprecyzyjny. A innego, bardziej precyzyjnego jego odpowiednika nie ma w Windows.

Jeszcze zobaczymy :).

1
Crow napisał(a):

Ja szukam standardowego rozwiązania stosowanego w profesjonalnych implementacjach, które nada się nawet do najbardziej wymagających zastosowań, nie tylko prostych gierek. Nie wiem, czy zdołam je zastosować u siebie, ale chce je przynajmniej poznać, z czystej ciekawości.

Znasz taki, widzę, że wiesz jak to ma działać. Ale jeśli chcesz wiedzieć jak to działa w innych, dużych produkcjach, to nie pozostaje nic innego jak poszukanie źródeł dużej gry i sprawdzenie w jej kodzie jak jest to realizowane. W razie czego możesz napisać do autorów, aby wskazali gdzie szukać odpowiedniego fragmentu kodu, coby oszczędzić trochę czasu.

Ja rozumiem, że muszę użyć czegoś, co daje efekt JAK Sleep(), ale to jednocześnie nie może być Sleep().

Tyle że nic innego pod Windows nie ma. Nawet jeśli użyjesz czegoś z biblioteki do akceleracji sprzętowej (np. SDL), to taka funkcja w dalszym ciągu wewnętrznie albo używa Sleep, albo busy waitingu, albo kombinacji. W sumie to SDL jest otwarty, można sprawdzić jak działa (tak samo SFML, raylib itd.).

Właśnie o to chodzi, że próbuję rozgryźć czego używa się profesjonalnie, natomiast wiem na 100%, żeby trzymać się z daleka od Sleep(), bo jest to rozwiązanie odradzane dosłownie wszędzie i nie spotkałem się z ani jedną sugestią jego zastosowania na potrzeby gry.

Sam Sleep to tylko jeden ze sposobów wstrzymywania pracy wątku. Są jeszcze inne, jednak nieistotne czego się użyje, bo sam systemowy sheduler pracuje ze zbyt niską rozdzielczością, więc dosłownie nie da się uzyskać wyższej precyzji niż co do milisekundy, bez spinningu. Tyle tylko, że taka dokładność jest zbyt niska, aby utrzymać sztywny klatkaż. I tutaj z pomocą przychodzi busy waiting i assemblerowa instrukcja pause, która została zaprojektowana specjalnie dla busy waitingu.

Jak wyżej, zdaję sobie sprawę z konieczności odmierzania interwałów i wygaszania wątku na zadany czas, po prostu muszę znaleźć zamiennik dla Sleep(). @alagner podsunął mi pomysł z CreateWaitableTimer(), więc poczytam coś więcej na ten temat i zobaczę jak tam wygląda kwestia precyzji.

Wyjdzie na to samo co w przypadku Sleep — patrz akapit wyżej. Wszystkie timery i inne mechanizmy używają dokładnie tego samego, mało precyzyjnego mechanizmu, więc sugeruję sobie ten temat odpuścić.

Czytałem też o innej konstrukcji, tj. takiej, w której wątek z logiką gry blokuje się po każdej iteracji i jest budzony przez wątek renderujący (który działa z opóźnieniem o jedną iterację, przechowywaną w buforze). Co prawda sprawia to, że odświeżanie logiki jest zsynchronizowane z renderowaniem klatek, ale mimo wszystko pozwala na jakąś formę równoległości wątków i jednocześnie korzysta z dobrodziejstwa V-SYNC'a.

Tyle że w ten sposób klatkaż będzie zmienny i będzie zależał od czasu aktualizacji logiki oraz od czasu renderowania. Im więcej do przetworzenia podczas aktualizacji logiki, tym większy spadek klatkażu. Im bardziej skomplikowane renderowanie na potrzeby klatki, tym bardziej framerate ucierpi. Poza tym jeśli zdaży się lag, klatkaż tak czy siak spadnie za bardzo i trzeba bedzie logikę aktualizować w kilku krokach na potrzeby kolejnej klatki.

Jeszcze zobaczymy :).

No cóż — fajnie by było, gdybyś coś takiego znalazł, ale czegoś takiego po prostu nie ma. ;)

0

@Crow: paczaj https://fabiensanglard.net
Tylko może Ci życia nie starczyć ;)

0
furious programming napisał(a):

Nie da się zrobić precyzyjnego odmierzania czasu bez busy waitingu pod Windows, bo żaden systemowy mechanizm wstrzymywania pracy wątku nie zapewnia mikrosekundowej precyzji — koniec kropka. Bazowanie na samym Sleep pozwoli osiągnąć w miarę stabilny klatkaż, ale tylko w miarę stabilny — rozrzut będzie pomiędzy 58fps a 62fps. Wiem, bo probowałem wielokrotnie na przestrzeni ostatnich lat i po prostu się nie da, bo Sleep jest nieprecyzyjny. A innego, bardziej precyzyjnego jego odpowiednika nie ma w Windows.

Napisałem takie cuś przy założeniach, że:

  1. sleep() daje m minimalną precyzję +/- N milisekund
  2. w wątku pozostaje nam jakiś tam czas T do odczekania.

Tą "grubszą" część czasu odczekujemy sleep(), a do precyzyjnego czasu "dociągamy" już zwykłym while + QueryPerformanceCounter().
W poniższym przykładzie ustawiłem sobie czas 1s i wygląda, że działa bardzo dobrze i i odmierza czas z dokładnością wywoływania QueryPerformanceCounter().

I teraz robimy tak ( kod poglądowy ):

procedure TMyWorkerThread.Execute;
var i : integer ;
    currCounter, nextFrameCounter, cpuFrq, calcTime: int64;
const
  frameTime = 1000 ;     // miliseconds
  sleepPrecision = 20 ; // miliseconds
begin
  i := 0 ;
  nextFrameCounter := 0 ;
  while x = 0 do
  begin
    QueryPerformanceFrequency( cpuFrq );
    QueryPerformanceCounter ( currCounter );
    nextFrameCounter := currCounter + round( frameTime * cpuFrq / 1000 ) ;

    // here real calc code equiv.
       sleep(random(800));

    QueryPerformanceCounter ( calcTime ) ;
    calcTime := ( calcTime - currCounter ) mod 1000 ;
    sleep ( frameTime - sleepPrecision - calcTime );
    while currCounter <= nextFrameCounter do
    begin
      i := i + 1 ;
      QueryPerformanceCounter ( currCounter );
    end;

    write ( #13#10+'Counter: ', currCounter );
  end;
end;

screenshot-20211005211513.png

Bez sleep() CPU 27%:

screenshot-20211005211728.png

1
katakrowa napisał(a):

Napisałem takie cuś przy założeniach, ze

  1. sleep() daje m minimalną precyzję +/- N milisekund
  2. w wątku pozostaje nam jakiś tam czas T do odczekania.

Ten grubszy czas odczekujemy sleep(), a do precyzyjnego czasu "dociągamy" już zwykłym while + QueryPerformanceCounter .

No i zrobiłeś dokładnie to samo, czego używam i ja. Różnica polega na tym, że CPU nie lubi takiego mielenia za pomocą QueryPerformanceCounter, dlatego sam użyłem asmowego pause, który został do tego celu zaprojektowany. Precyzja jest bardzo wysoka, a CPU szczęśliwy. ;)

I tego mechanizmu można użyć do dowolnego celu, do zastosowań jednowątkowych oraz wielowątkowych, w których istnieje limit wywołań na sekundę i potrzeba oszczędzania mocy obliczeniowej. I jest to rozwiązanie elastyczne, bo można dowolnie manipulować frameratem, a także łatwo dodać opcję zmniejszenia precyzji (wykluczając busy waiting) oraz wykluczając limit (wykluczenie Sleepa).

0
furious programming napisał(a):

No i zrobiłeś dokładnie to samo, czego używam i ja. Różnica polega na tym, że CPU nie lubi takiego mielenia za pomocą QueryPerformanceCounter, dlatego sam użyłem asmowego pause, który został do tego celu zaprojektowany. Precyzja jest bardzo wysoka, a CPU szczęśliwy. ;)

Kiedyś dawno temu jeszcze za czasów 386 odczytywałem wartość cykli procesora wstawką Assemblerową (chyba rdtsc) i faktycznie znając frq procesora dało się robić precyzyjne pomiary. Nie wiem czy QueryPerformanceCounter czyta właśnie ten licznik czy coś innego?

No faktycznie podobnie :-)

procedure TClock.UpdateFrameAlign();
var
  SleepTime: Single;
begin
  SleepTime := 1000 / FFrameRateLimit * (1 - (FFrameTicksEnd - FFrameTicksBegin) / FTicksPerFrame) - 1;
  SleepTime -= Ord(Round(SleepTime) > SleepTime);
  SleepTime := Max(SleepTime, 0);

  SDL_Delay(Round(SleepTime));

  while GetCounterValue() < FFrameTicksNext do
  asm
    pause
  end;
end;
0
furious programming napisał(a):

Tyle że w ten sposób klatkaż będzie zmienny i będzie zależał od czasu aktualizacji logiki oraz od czasu renderowania. Im więcej do przetworzenia podczas aktualizacji logiki, tym większy spadek klatkażu. Im bardziej skomplikowane renderowanie na potrzeby klatki, tym bardziej framerate ucierpi. Poza tym jeśli zdaży się lag, klatkaż tak czy siak spadnie za bardzo i trzeba bedzie logikę aktualizować w kilku krokach na potrzeby kolejnej klatki.

Ale przecież logika i rysowanie klatek powinny być w tym modelu o wiele bardziej niezależne. W obecnej implementacji wygląda to u mnie tak:

GameThread()
{
   GameLoop()
   {
      while(!Dead)
      {
         SynchronizeInput();
         UpdateLogic();
         RenderFrame();
         //Czekanie V-SYNC
      }
   }
}

Im więcej zajmuje krok wcześniejszy, tym mniej czasu będzie mieć krok kolejny (żaby całość zmieściła się w MaxFrameTime). W modelu równoległym wyglądałoby to tak:

GameThread()
{
   GameLoop()
   {
      LogicThread();
      RenderThread();
   }
}

//-------------------------------

LogicThread()
{
   SynchronizeInput();
   UpdateLogic(); //Zapisuje wynik do bufora
   GoSleep(); //Zasypia
}

RenderThread()
{
   WakeUpLogicThread(); //Wybudza wątek logiki
   DrawFrame(); //Rysuje klatkę na podstawie danych z bufora, w tym samym czasie gdy wątek logiki odświeża logikę. Buforowanie zapobiega data race.
   //Czekanie V-SYNC
}

Czemu tu miałyby być jakieś opóźnienia?

1
katakrowa napisał(a):

Kiedyś dawno temu jeszcze za czasów 386 odczytywałem wartość cykli procesora wstawką Assemblerową (chyba rdtsc) i faktycznie znając frq procesora dało się robić precyzyjne pomiary. Nie wiem czy QueryPerformanceCounter czyta właśnie ten licznik czy coś innego?

Raczej coś innego. U mnie QueryPerformanceFrequency zwraca 10 000 000 (dziesięć milionów), choć bazowe taktowanie procesora wynosi 2.13GHz. Tyle że CPU działa ze zmienną szybkością, w zależności od zapotrzebowania na moc obliczeniową (od mniej więcej 1.3GHz do 3.4GHz), dlatego to nie jest fizyczna liczba cykli, a jakaś wirtualna wartość.

Ale mnie akurat nie obchodzi czym ta wartość jest, skoro jest ona prawidłowa z punktu widzenia mojego kodu i jej wykorzystywanie nie powoduje żadnych skutków ubocznych. Korzystam z tego co mi system dostarcza, a co sam system musi robić, aby działać zgodnie z bieżącym taktowaniem, to już jego problem. :D


Crow napisał(a):

Ale przecież logika i rysowanie klatek powinny być w tym modelu o wiele bardziej niezależne.

Z opisu, który podałeś wcześniej wynika, że wątki działają naprzemian — raz ten aktualizujący logikę, raz ten renderujący. Tak więc trudno tu o jakąkolwiek niezależność, a samo działanie procesu w dwóch takich wątkach nie ma żadnego sensu, bo dokładnie to samo można osiągnąć jednym wątkiem (mój kod tak robi).

Niepotrzebnie zagłębiasz się w wątki, bo wychodzi z tego coś znacznie bardziej skomplikowanego niż powinno. Jeśli używasz V-Sync, to po prostu aktualizuj logikę zgodnie z nim, używając delty i sprawdzając czy był lag czy nie (w razie czego logikę aktualizuj krokami, jak pisałem wcześniej). Tutaj nie ma żadnej magii — większość normalnych gier tak działa, a wyjątkiem są jedynie te największe, bardzo zasobożerne produkcje.

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