Główna pętla gry (fps vs. ups) dlaczego nie ograniczamy fpsów?

0

Ostatnio zastanowiłem się nad prawidłowością głównej pętli gry z aktualizajcą stałokrokową. Podstawowy szkielet wygląda następująco (pobrany z tego PDFa):

float dt = 0.0f;                         // czas od ostatniej aktualizacji
float lastUpdateTime = GetCurrentTime(); // czas ostatniej aktualizacji
                                         // przykladowa funkcja GetCurrentTime() pobiera
                                         // nam od systemu aktualny czas w sekundach
float accumulator = 0.0f;
const float TIME_STEP = 0.03;            // krok czasowy, a zarazem czas trwania ramki
                                         // fizyki w sekundach; tutaj 30 milisekund, czyli
                                         // ok. 30 aktualizacji na sekundę
while(true) {
    dt = GetCurrentTime() - lastUpdateTime; //obliczenie czasu od ostatniej klatki
    lastUpdate += dt;                    //podmiana
    accumulator += dt;
    
    GrabInput();                         //<-- zbieranie wejscia z klawiatury, myszki, sieci, itp.
    while(accumulator > TIME_STEP) {
        UpdateGame(TIME_STEP);           //<-- aktualizacja fizyki i logiki gry
        accumulator -= TIME_STEP;
    }
    RenderGame();                        //<-- wyswietlenie aktualnego stanu na ekranie
}

Pomijam kilka błędów jak używanie typów float zamiast long, zmienna lastUpdate jest zmieniana ale nigdzie nie odczytywana.

Chodzi mi o to, że aktualizacja stanu gry wykonywana jest tak, jakby była wywoływana co stałe 30 ms nawet jeśli tak nie jest, natomiast rendering sceny za każdym obiegiem pętli głównej.
Można zatem uzyskac efekt, że dla prostej sceny update stanów będzie wykonywał się ok 33 razy na sekundę (Update Per Second), a rendering powiedzmy 1000 razy na sekundę (Frame Per Second).
Możemy mieć zatem sytuację taką:
UPS: 33, FPS: 1089
UPS: 33, FPS: 998
UPS: 33, FPS: 1102
UPS: 33, FPS: 1038
...

Mamy zatem tak, że UpdateGame() aktualizuje stan gry (przesunięcia obiektów, wykrywanie kolizji, usuwanie, zmienianie, dodanie nowych obiektów do sceny) co 30 ms, po czym następuje rendering sceny, a następnie następuje... znowu rendering tej samej sceny, bo jeszcze nie minęło 30 ms by wykonać następną aktualizację i wypchnięcie tej samej sceny do monitora. I tak jeszcze kilkadziesiąt razy zanim zmieni sie stan gry. Co więcej nawet jeśli minie 30 ms a stan się nie zmieni, to i tak wykona się kolejne kilkadziesiąc renderingów znowu tej samej sceny.

Czy to nie jest jakieś marnotrawstwo zasobów karty graficznej i procesora?

Ponadto załóżmy, że dysponujemy niskobudżetowym monitorem z odświeżaniem natywnym 60 Hz. Mamy teraz ograniczenie z góry na 60 FPS przez monitor, którego bufor graficzny jest nadpisywany kilkadziesiąt razy strumieniem danych z tymi samymi informacjami o kolorach pikseli. Włączenie czy wyłączenie v-sync nic tu nie zmieni, a tylko zapewni, że bufor tylny podmieni bufor ekranu w czasie gdy zapełni ostatni piksel (w celu prewencji poszarpanych klatek).

Moje pytanie jest takie dlaczego w głównej pętli gry nie ogranicza się wyświetlanie FPSów?
Co z tego, że po napisaniu gry, możemy pochwalić się tym, że ma ona 1000 FPSów skoro faktycznie będziemy widzieć tylko 60 klatek na sekundę (większość frameów będzie po prostu zdublowana). Nawet jeśli dysponuje się monitorem 240 Hz to i tak wiele ramek będzie zdublowana docierając od GPU do monitora.
Skad ta metodyka pisania pętli gry, żeby nie ograniczać FPSów tylko niech lecą ile fabryka dała?

Ma ktoś jakieś doświadczenie czy sensowne przemyślenia na ten temat?

PS. Tu nawet nie chodzi o to, że przesyłamy do monitora dokładnie te same klatki ale że przesyłamy ich więcej (tyle ile jest w stanie wyprodukować GPU) niż jest w stanie obsłużyć monitor. Nie jest to kontrolowane przez cokolwiek.

2

V-sync chyba zapobiega temu, że "FPSy lecą ile fabryka dała"?
https://scroll.morele.net/technologia/v-sync-informacje-o-synchronizacji-pionowej/

0

@Spine: Połowicznie się zgodzę ale nie jest to do końca prawdą. Przynajmniej nie w tym przykładzie. Pętla gry jest bytem programowym przetwarzanym przez CPU. CPU w ciągu 1/60 sekundy w w/w. pętli powiedzmy, że jest w stanie przetworzyć (tutaj mam namyśli wysłać żądanie odrysowania ramki obrazu do GPU) 15 żądań repaint(), które są wysyłane do GPU. To czy GPU po wysłaniu pierwszego pakietu danych do monitora zostanie zablokowane przez technologię v-sync na kolejne 14 pakietów czy nie, nie ma znczenia w moim pytaniu.
CPU musi przygotować scenę do wyrenderowania przez GPU.
Dajmy na to taki bardzo uroszczony pseudokod:

Image image = StworzPustyObiektImage() // tworzy obiekt Image o rozdzielczości ekranu np: 1920 x 1080.
DodajTileOrazSprity(image)             // w pętli nanosi na niego bezpośrednio proste obrazki albo nawet piksel po pikselu
                                       // zapełnia go odpowiednimi kolorami, usuwając te, które znajdują się poza obszarem
                                       // rysowania (poza ekranem)
image.repaint()                        // wyślij żądanie odrysowania do GPU (nie do ekranu ale do GPU!!!)

Więc jak widzisz do GPU i tak trafi w ciągu 1/60 s 15 żądań odrysowania tej samej sceny. GPU przetworzy to sobie z formatu zrozumiałego dla GPU na format zrozumiały dla ekranu. W przypadku OpenGL być może shadery wykonają jeszcze jakąś transormację tej ramki zanim wyślą ją na ekran monitora (w przypadku 3D dodadzą oświetlenie, cienie, mgłę i co tam jeszcze się chce). Ale to jest mało istotne. Podałem tylko ekstremalnie prosty pseudokod.

Pytanie jest zatem aktualne. Dlaczego pętla gry, która wykonuje się na CPU nie ogranicza ilości ich odrysowań? Chociażby do natywnej ilości odświeżania monitora. Albo jeszcze lepiej jako minimum z odświeżania natywnego i ustalonej ilości UPS. Bo jeśli mamy jak powyżej UPS 30 razy na sek. to rendering większej ilości klatek niż 30 / s nie ma chyba sensu. Dla dalszej dyskusji załóżmy, że wyłączyliśmy V-sync albo nasza biblioteka graficzna, w której programujemy grę nie obsługuje jej wyłączenia ;) Pytanie jest cały czas aktualne

1

Ale co to znaczy, że nie ogranicza się? Jak najbardziej są gry, które potrafią ograniczać FPS do podanego maksimum niezależnie od v-sync.

0
ReallyGrid napisał(a):

Możemy mieć zatem sytuację taką:
UPS: 33, FPS: 1089
UPS: 33, FPS: 998
UPS: 33, FPS: 1102
UPS: 33, FPS: 1038

Przecież to głupie jest. Tzn. z jednej strony nie jest, bo mamy dwufunkcyjny mechanizm synchronizujący działanie, co jest przydatnym ficzerem, ale z drugiej strony jednak jest głupie, bo renderuje się 1000 klatek pomimo tego, że faktycznie stan gry (a tym samym obraz na ekranie) zmienił się tylko 33 razy. Summa summarum, zmarnowało się moc obliczeniową renderując 967 klatek-duplikatów, co jest absolutnie bez sensu.

Czy to nie jest jakieś marnotrawstwo zasobów karty graficznej i procesora?

Ano jest — nie trudno to dostrzec. Ogólnie to nie widzę większego sensu w implementacji czegoś takiego, bo nie rozwiązuje to problemu sporadycznego odświeżania stanu świata gry (logiki), a tylko komplikuje sprawę związaną z renderowaniem (nie można przecież jednocześnie aktualizować logiki i renderować, to musi być zsynchronizowane) oraz marnuje moc obliczeniową renderując klatki, które niczym się nie różnią od poprzednich. A skoro gracz nie zobaczy różnicy, to nie ma żadnej różnicy czy się wyrenderuje klatkę czy nie.

Włączenie czy wyłączenie v-sync nic tu nie zmieni, a tylko zapewni, że bufor tylny podmieni bufor ekranu w czasie gdy zapełni ostatni piksel (w celu prewencji poszarpanych klatek).

V-Sync i podobne technologie mają zapewnić, że klatka zostanie wypełniona danymi do końca i dopiero wtedy zostanie wysłana do monitora, w odpowiednim momencie. To eliminuje problem z rozrywaniem ekranu, a z drugiej strony jest naturalnym odmierzaczem czasu pomiędzy klatkami, dzięki czemu można aktualizować stan gry oraz renderować klatki z taką samą częstotliwością.

Problem pojawia się wtedy, gdy aktualizacja logiki jest długotrwała i nie da się wyciągnąć więcej niż 30-40fps. W takim przypdaku trzeba użyć wątków i logikę gry aktualizować niezależnie od renderowania. Jednak renderowanie klatek w przypadku gdy stan świata się nie zmienił, jest nadal bezcelowe.

V-Sync nadal zapewni brak rozrywania ekranu, jednak aby oszczędzić maksymalnie moc obliczeniową, wątek renderujący powinien najpierw sprawdzić czy stan gry się zmienił i jeśli nie, to odpuścić renderowanie i poczekać aż stan się zmieni. Wtedy nowa klatka będzie zawierać nowe dane (z nowym stanem gry), co będzie widoczne na ekranie.

Moje pytanie jest takie dlaczego w głównej pętli gry nie ogranicza się wyświetlanie FPSów?

To zależy — większość gier jednak ma jakąś kontrolę nad klatkażem, więc to trochę niesprawiedliwie wrzucać wszystko do jednego worka. Można mieć zmienny framerate lub stały, ten stały może być ograniczony przez dewelopera lub przez V-Sync, można mieć częstotliwość aktualizacji logiki oraz renderowania zgodne lub nie. Nieważne co się wybierze, ważne aby gra wykonywała wszystko co potrzebuje, a resztę mocy oddała innym procesom.

To wszystko zależy od wymagań samej gry oraz również od woli deweloperów. Dając możliwość konfiguracji silnika przez graczy, będą mogli dostosować działanie gry do własnego sprzętu. A że współcześnie używamy komputerów o różnej sprawności, od potężnych gamingowych pecetów do budżetowych laptopów, dla mnie (jako gracza) możliwość konfiguracji działania gry to mus. Lepiej jest wyłączyć trochę ficzerów i zmniejszyć rozdzielczość, niż grać w 15fps.

Co z tego, że po napisaniu gry, możemy pochwalić się tym, że ma ona 1000 FPSów skoro faktycznie będziemy widzieć tylko 60 klatek na sekundę (większość frameów będzie po prostu zdublowana). Nawet jeśli dysponuje się monitorem 240 Hz to i tak wiele ramek będzie zdublowana docierając od GPU do monitora.

Dokładnie. Choć zawsze możesz się pochwalić, że umiesz w wątki. :D

Skad ta metodyka pisania pętli gry, żeby nie ograniczać FPSów tylko niech lecą ile fabryka dała?

Uniezależnienie pracy modułu aktualizującego logikę od modułu renderującego jest konieczne w przypadku bardzo wymagających gier. Jednak wielowątkowość oraz synchronizacja tych mechanizmów jest rozwiązaniem problemu dużej złożoności obliczeniowej, wymaganej przez grę i taka technika powinna być stosowana tylko w przypadku, gdy niemożliwe jest utrzymanie zgodnej częstotliwości aktualizacji oraz renderowania, albo jeśli chcemy zapewnić działanie gry na starszych sprzętach o niskiej wydajności. Nie robi się tego po to, aby było ”fajnie” czy ”pro”.

W przypadkach gier, które nie są zasobożerne, implementacja czegoś takiego jest absolutnie bezsensowna i będzie działać na szkodę gracza. Zmarnuje zasoby jego komputera i zwiększy pobór mocy, co przełoży się na wyższy rachunek za prąd (który jest coraz droższy) oraz skróci czas pracy baterii (w przypadku laptopów i urządzeń mobilnych).

Brzydko podsumowując, myślę, że wielu wyssało ten mechanizm z tej samej d**y, z której wyssano wciąż popularne przeświadczenie, że po to jest procesor, aby zużywać tyle jego mocy ile tylko oferuje. I przez takie deweloperskie ofermy dana gra bez potrzeby żre tyle mocy, że wentylator nie wyrabia, bateria się szybko rozładowuje, a procesy działające w tle (takie jak przeglądarka, OBS itp.) nie działają wydajnie.

Ma ktoś jakieś doświadczenie czy sensowne przemyślenia na ten temat?

Moja rada dla wszystkich jest taka — nie potrzebujesz zużywać mocy CPU/GPU, to jej do cholery nie zużywaj. Czasy DOS-a/NES-a już dawno minęły, dziś każdy pecet obsługuje setki procesów i przetwarza nawet kilka tysięcy wątków w tle, więc zostaw tyle mocy obliczeniowej dla nich ile tylko możesz.

0

W sumie to nie wspomniałem o istotnej kwestii, czyli o artykule źródłowym, z którego pochodzi pętla z pierwszego posta. Ogólnie traktuje on o tym, żeby aktualizacja stanu gry nie polegała na dowolnie dużej delcie, która to — bez żadnej kontroli — przekazywana jest do metody aktualizującej. Kluczem więc jest obliczenie delty i pojedyncza aktualizacja logiki, a jeśli delta jest za duża, to wykonanie od razu kilku kolejnych kroków.

I to jest jak najbardziej sensowne i logiczne podejście. W samą jakość i poprawność podanego kodu nie wnikam, bo to nieistotne. Obliczenia fizyki mogą bazować na z góry określonej maksymalnej delcie, więc zwiększa się wydajność obliczeń, a żaden lag ich nie da rady popsuć i wykrzaczyć gry.

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