windows - Håndtering af spilsløjfen med OS timere

Indlæg af Hanne Mølgaard Plasc

Problem



En simpel spilsløjfe løber lidt sådan:


MSG msg;
while (running){
    if (PeekMessage(&msg, hWnd, 0, 0, PM\_REMOVE)){
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
        try{
            onIdle();
        }
        catch(std::exception& e){
            onError(e.what());
            close();
        }
}


(taget fra dette spørgsmål)


Jeg bruger Windows som et eksempel her for eksemplets skyld, det kan være nogen platform. Korriger mig, hvis jeg tager fejl, men en sådan loop ville bruge 100\% af en cpu/core (bruger 50\% af en kerne på min computer), da det altid kigger efter tilstanden af ​​variablen running.


Mit spørgsmål er, ville det være bedre (performance-wise) at gennemføre spilsløjfen ved hjælp af OS'et (i Windows-timer) -funktionerne, indstiller det ønskede interval i henhold til det ønskede antal ticks of game logic wanted per second? spørg dette fordi jeg antager, at timerfunktionerne bruger CPU'ens RTC-afbrydelser.

Bedste reference


Normalt vil et spil fortsætte med at tegne nye rammer hele tiden, selvom brugeren ikke gør noget. Dette ville normalt ske, hvor du har din onIdle () opkald. Hvis dit spil kun opdaterer vinduet/skærmen, når brugeren trykker på en knap eller sådan, eller sporadisk imellem, er MSGWaitForMultipleObjects en god mulighed.


I et kontinuerligt animationsspil ville du normalt ikke gerne blokere eller sove i render-tråden overhovedet , hvis du kan hjælpe det, i stedet vil du gøre med maksimal hastighed og stole på vertikal synkronisering som en gasprop. Årsagen til det er, at timing, blokering og sovende i bedste fald er upræcise og upåliteligt i værste fald, og det vil muligvis tilføje forstyrrende genstande til dine animationer.

Hvad du normalt vil gøre, er at skubbe alt, der hører til en ramme til grafik-API'en (sandsynligvis OpenGL siden du sagde 'nogen platform') så hurtigt som muligt, signalere en arbejdstråd for at begynde at lave spillogikken og fysikken osv. Bloker derefter på SwapBuffers.


Alle timere, timeouts og søvn er begrænset af planlæggerens opløsning, som er 15ms under Windows (kan indstilles til 1ms ved hjælp af timeBeginPeriod. Ved 60fps er en ramme 16.666ms, så blokering for 15ms er katastrofal , men selv 1ms er stadig en betydelig tid. Der er ikke noget du kan gøre for at få en bedre opløsning (dette er betydeligt bedre under Linux).


Sleep eller en blokeringsfunktion, der har en timeout, garanterer at din proces sover for i det mindste så længe du bad om det (faktisk på Posix-systemer kan det sove mindre hvis der opstod en afbrydelse). Det giver dig ingen garanti for at din tråd løber så snart tiden er op, eller andre garantier.

Sleep (0) under Windows er endnu værre. Dokumentationen siger 'tråden vil afstå resten af ​​sin tidsskive, men forbliver klar. Bemærk, at en klar tråd ikke garanteres at køre med det samme' . Virkeligheden har det, at det virker okay det meste, hvor man blokkerer hvor som helst fra 'slet ikke' til 5-10 ms, men i nogle tilfælde har jeg set Sleep (0) blokken for 100 ms, hvilket er en desaster, når det sker .


Brug af QueryPerformanceCounter kræver problemer - bare gør det. På nogle systemer (Windows 7 med nyere CPU) fungerer det fint, fordi det bruger en pålidelig HPET, men på mange andre systemer vil du se alle slags mærkelige 'tidstids' -effekter, som ingen kan forklare eller fejle. Det skyldes, at i disse systemer læses resultatet af QPC TSC-tælleren, som afhænger af CPU'ens frekvens. CPU-frekvensen er ikke konstant, og TSC-værdier på multicore/multiprocessorsystemer behøver ikke at være konsekvente. [8]


Så er der synkroniseringsproblemet. To separate timere, selv når du tikker med samme frekvens, bliver nødvendigvis usynkroniseret (fordi der ikke er noget som 'samme'). Der er en timer i dit grafikkort, der gør den vertikale synkronisering allerede. Ved at bruge denne til synkronisering betyder alt, hvad der vil fungere, ved hjælp af en anden vil det betyde, at ting i sidste ende vil være ude af synkronisering. At være ude af synkronisering kan slet ikke have nogen virkning. Kan trække en halv færdig ramme eller blokere for en fuld ramme eller hvad som helst.

Mens der mangler en ramme, er det normalt ikke så meget af en big deal (hvis det kun er en og sker sjældent), er det i dette tilfælde noget, du helt undgår i første omgang.

Andre referencer 1


Afhængig af, hvilken ydeevne du ønsker at optimere til. Hvis du vil have den hurtigste billedfrekvens for dit spil, så brug den løkke du skrev. Hvis dit spil er fuldskærm, er det nok det rigtige.


Hvis du vil begrænse billedfrekvensen og lade skrivebordet og andre apps få nogle cylindre, så ville en timer tilgang være tilstrækkelig. I Windows ville du blot bruge et skjult vindue, et SetTimer-opkald og ændre PeekMessage til GetMessage. Husk bare, at WM\_TIMER intervaller ikke er præcise. Når du skal finde ud af, hvor meget realtid der er gået siden sidste frame, skal du ringe GetTickCount i stedet for at antage, at du vågnede præcist på tidsintervallet.


En hybrid tilgang, der fungerer godt med hensyn til spil og desktop apps, er at bruge den sløjfe, du har bogført ovenfor, men indsæt en Sleep (0), efter at din OnIdle-funktion returneres.

Andre referencer 2



  1. MSGWaitForMultipleObjects skal bruges i en Windows-meddelelsessløjfe, da du kan få den til automatisk at stoppe med at vente på meddelelser i henhold til en definerbar timeout. Dette kan medføre, at vinduet reagerer hurtigt på meddelelser. [9]

  2. Hvis dit vindue ikke er fuldskærm, er en SetTimer, der kalder en timerforbindelse eller sender WM\_TIMER meddelelser til dit spil s WindowProc, påkrævet . Medmindre du ikke har noget imod animationen, der stående, når brugeren, for eksempel klikker på dit vindues titellinje. Eller du pop op en modal dialog af en slags. [10]

  3. I Windows er der ingen mulighed for at få adgang til faktiske timerinterrupter. Disse ting er begravet væk under mange lag af hardware abstraktion. Timerafbrydelser håndteres af drivere for at signalere kerneobjekter, som brugertilstandskoden kan bruge i opkald til WaitForMultipleObjects. Dette betyder, at kernen vil vække din tråd til at håndtere en kernetimer, der er forbundet med din tråd, når du kaldte SetTimer (), på hvilket tidspunkt GetMessage/PeekMessage vil, hvis meddelelseskøen er tom, syntetisere en WM\_TIMER-besked.


Andre referencer 3


Brug aldrig GetTickCount eller WM\_TIMER til timing i et spil, de har forfærdelig opløsning. Brug i stedet QueryPerformanceCounter.