Frameratenbegrenzung

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Problemstellung

Bei vielen 3D-Anwendungen erfolgt die Bildausgabe so schnell hintereinander, wie die Hardware dies ermöglicht. Da die CPU die Befehle erzeugen muss, um ein Frame zu rendern, benötigt diese Zeit auf der CPU und so entsteht bei hohen Frameraten entsprechend hohe Auslastung der CPU. Das wiederum benötigt jede Menge Strom. Unser Auge wiederum nimmt eine Bewegung ab ca. 24 Bildern pro Sekunde theoretisch als flüssig wahr - es bringt also nicht sehr viel, wenn unsere Anwendung mit sehr hohen FPS läuft - wir bekommen davon sowieso nichts mit, außer niedriger Akkulaufzeit und hoher Stromrechnung. Abhängig von den Lichtquellen, die in der Umgebung strahlen sollte sollte man bis zu 120Hz als maximale FPS wählen, darunter kann es sonnst zu flimmern, oder Jitter Effekten kommen, wenn man das Auge schnell bewegt. Dieses Phänomen kann man üblicherweise bei Leuchtstoffröhren in Büros beobachten. Ein hoher Hertz(kurz Hz) Wert entspannt auch das Auge, so das es längere Zeiten vor dem Monitor ermöglicht, bevor der Augenapparat versagt und Zuckungen, Fokusprobleme und verzögertes Fokusieren als resultat zu folge hat. Die Lösung für dieses Problem ist eine Frameratenbegrenzung, die die Bildwiederholrate auf einen gewissen Wert limitiert und unsere Anwendung in der Zeit, in der nichts getan werden muss, andere dinge machen lässt. Diese Bildwiederholrate wird üblicherweise auf die aktuelle Monitor Frequenz gesetzt bzw. diese auch entsprechend angepasst. Der Monitor ist das schwächste Glied in der Kette und die unterstützen Frequenzen gehen bis zu 120Hz, bzw. höher, wenn diese 3D fähig sind.

Ansatz

Die naive Lösung

Die einfachste Realisierung der Frameratenbegrenzung ist ein einfaches Hinzufügen der "Sleep" Anweisung in die Hauptschleife. Gehen wir von Sleep(5) aus, so haben wir eine Maximale Framerate von 200FPS. Diese Lösung ist jedoch etwas suboptimal. Gehen wir davon aus, dass das Zeichnen der Inhalte an sich schon 5 Millisekunden dauert, so haben wir nur noch eine Framerate von 10FPS. Auf einem älterem Rechner könnte diese Berechnung auch länger dauern und die erreichte Framerate sich daher noch mehr von unserem anvisierten Wert entfernen.

Die intelligente Lösung

Schlauer ist es, von der Zeit, die unser Programm pausiert, die Dauer des Rendervorgangs abzuziehen. Dazu ist es nötig, die Zeit, die zwischen zwei Frames vergeht zu messen. Um die Schlafenszeit nicht in dieser Messung zu haben, müssen wir uns diese natürlich merken und vorher abziehen. Mit diesem Ansatz passt sich die CPU-Auslastung an die Leistung des Systems an: Auf einem High-End PC wird sie automatisch niedriger als auf einem Rechner aus dem letztem Jahrhundert - so gehört es sich schließlich auch.

Implementierung

var
  FMaximumFrameRate: double;
  FLastSleep: double = 0;

procedure LimitFrameRate(atd: double); //"atd" ist die Zeitdifferenz zwischen zwei Frames - inklusive der "Sleeptime"
var
  sleeptime: Double;
begin
  sleeptime := 1000 / FMaximumFrameRate - (atd - FLastSleep);
  if sleeptime > 0 then
  begin
    //statt sleep kann unter SDL auch SDL_Delay verwendet werden 
    Sleep(trunc(sleeptime));
    FLastSleep := sleeptime;
  end else
    FLastSleep := 0;
end;

Genauigkeit und zuverlässigkeit von Framebeschränkungen

Zuverlässigkeit

Wenn man mit hilfe der Funktion Sleep den Prozess pausiert, erwartet man das dieser auch so lange wie angegeben pausiert. Dies ist leider nicht der Fall. Sleep ruft die System Funktion für Sleep auf, welche bei Windows Milisekunden und Unix basierte Betriebssysteme Microsekunden an nimmt. Allerdings bedeutet dies noch nicht, dass das Betriebssystem auch wirklich so genau ist, denn dies hängt von mehreren Faktoren ab. Die Prozess Sheduler, die für das schlafen legen und wieder aufwäcken zuständig sind haben Gemeinsamkeiten. Jeder Prozess hat eine Priorität, die den Sheduler erlaubt mehr Zeit ein zu räumen. Die meisten Process Sheduler arbeiten mit Time-Slices/Time-Windows, welche den Prozessen zugewiesen werden. Unter Windows hat ein Prozess, welcher mit normaler Priorität erzeugt wird ein 16ms Time-Slice zur verfügung, danach legt der Sheduler den Prozess schlafen und wechselt den zu dem folgendem Prozesscontext. Dies bedeutet auch, dass ein Sleep von 16ms Fatal wäre, da im Idealfall der Prozess im nächsten Slice wieder dran wäre und weil er noch schläft, kann er nicht mehr die ganzen 16ms vom Time-Slice nutzen, bzw. auch noch übergangen werden und den Time-Slice nicht bekommt. Man kann die Process Priorität mit der Betriebssystem API erhöhen und so nimmt die Laufzeit und auch Sleep Genauigkeit zu. Es ist also zu empfehlen, die Prozess Priorität zu erhöhen, weil die Genauigkeit von Sleep zu nimmt.

Genauigkeit

Wie schon erwähnt, ist die Genauigkeit der Sleep Funktionen nicht genau und es gibt keine Garantie für die wiederaufnahme des Prozess. Wenn man also weniger als 16ms Sleep erreichen will, dann ist man unter Unix mit usleep auf der sicheren Seite aber Windows liefert dann alternative Lösungen. Man kann z.B. ein MediaTimer erzeugen und diesen für die Render-Loop einsetzen, die Media Timer API ist für höhere Präzision ausgelegt. Möchte man eine Präzision von 1ms oder weniger kann man auf Sleep(0) setzen. Sleep und usleep verhalten sich bei 0 als Wartezeit gleich, der Prozess gibt sofort die Kontrolle an den Sheduler, dieser Prüft, ob ein anderer Prozess auf den Logischen Kern, wo der Prozess/Prozess-Thread läuft, laufen will und wechselt erst dann auf diesen für die restliche Zeit, für den aktuelle Time Slice. Wenn der andere Prozess durch ist, dann wird er entsprechend niederer in der Ausführungsliste gestuft und im kommenden Slice ist die Wahrscheinlichkeit, dass der eigene Prozess wieder läuft viel höher. Sollte kein anderer Prozess auf den Kern laufen wollen, dann springt der Sleep Aufruf wieder zurück und arbeitet für die restliche Zeit. Dies kann dann auf Systemen, wo leerlauf ist zu hoher CPU Last führen und das ist kontraproduktiv für Mobile Geräte. Daher ist es besser sich für Rendering auf vsync zu verlassen. Die beste Lösung ist ein Thread für das render ab zu stellen und vsync für den Renderkontext zu aktivieren. SwapBuffers wird nun bei jedem Aufruf warten, bis der Monitor den nächsten Time-Slice erreicht hat, wechselt Back- und Fron-Buffer, der neue Back-Buffer wird zur Darstellung gebracht und die Funktion kehrt zurück. Der Render Thread wird nun M mal die Sekunde durch laufen, wobei M für die aktuelle Monitor Frequenz(aF) steht bzw. darunter, wenn das Rendern länger dauert als 1000ms / aF dauert. Mehr zu Render Threads findet ihr hier.

Siehe auch

Framecounter
Framerate