multiprocessing/threading mit pygame

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Folgendes, mittlerweile lässt sich meine GUI Version 0.0.5 ganz gut anschauen. Hat aber das Problem das gerademal 8-9 fps bei voller Auslastung eines 1 GHz Kerns erreicht werden können und das bei einem relativ kleinen Testprogramm. Der Haupteil der Leistung wird für das "blitten" benötig, also habe ich ein Redraw-System eingeführt. Die GUI muss also das Neuzeichnen eines Widgets erst freigeben sonst wird einfach nur das Widget selbst "geblittet".
Dieses Sytem funktioniert schon ganz gut und ich komme jetzt in der Version 0.0.6, bei einer kleinen GUI auf 5-10% Auslastung des Kernes bei einer Beschränkung auf 30 fps - theoretisch ist der Wert bei der momentanen Test-Datei bis zu 300 fps möglich. Ich gehe aber davon aus das man bei Spielen etwas Größer GUIs braucht, wenn es sich nicht nur um ein Startmenü handelt. Daher schätze ich später Auslastungen zwischen 20-30% bei 30fps. (Achja, das die Zahlen relativ sind ist mir bewusst :D, sie sollen das nur ein wenig veranschaulichen.)

Mein Problem sehe ich nun bei der Nutzung der GUI in einem Spiel. Da Spiele in Python doch den Ruf habe etwas mehr Leistung zu benötigen als andere, Spiele der selben Art und ich auch schon etwas Erfahrung gemacht habe wo ein paar Engpässe liegen, ist so eine zusätzliche GUI schon eine Belastung von so einem Spiel. Dazu kommt auch noch der GIL. Das sind dann doch schon grobe Einschränkungen.

Meine Frage nun ist es z.B. möglich die GUI und das Spiel zwar im selben Fenster laufen zu lassen aber in getrennten Prozessen. Sprich das die GUI immer einen extra Prozess zugeordnet bekommt ohne das sich der Entwickler des Spiels damit auseinander setzen muss.
Mein Wunsch wäre halt auch noch die anderen Kerne mit Nutzen zu können um die Performance des Spieles an sich zu heben. Ich bin natürlich auch für jegliche andere Möglichkeiten offen das zu verbessern. Mein Ziel ist es am Ende ein flüssiges Ergebnis bei ca. 30-35 fps zu erhalten.
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
deets

Theoretisch ja, wie google chrome es zB vormacht mit plugins, die in eigenen Prozessen laufen. Praktisch halte ich es fuer unrealistisch.

Was *macht* deine GUI denn, dass sie soviel Zeit verbraet? Reden wir hier ueber Animationen? Mit fetten compositing-Effekten? Oder ist das Problem schon im 'stillstand' zu beobachten?
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Sieh es dir doch einfach an :D

Es ist im Stillstand in der 0.0.5 Version so gewesen schwerwiegend gewesen, in der neuen frisst es im Stillstand gut 10% bei einem 1 GHz Kern. Bisher gibt es keine derartigen Animationen, verursacht wird das ganze nur durch das "blitten". Da das komplette SDL-Fenster ja immer neu gezeichnet werden muss. Halt x Bilder pro Sekunde. Ich muss zumindest immer die eigenständigen Widgets blitten sonst kann man Eingaben vom Benutzer nicht Visualisieren.
Mit anderen Worten so wie es jetzt ist bin ich schon ganz zufrieden, ich nehme halt nur an das bei höherer Anzahl von Widgets und einem Spiel welches ja auch eigenständig agieren soll, die CPU Last stark zunimmt.
In meinem bisherigen Programm gibt es mit Sicherheit noch einige Einsparmöglichkeiten, das zumindest weniger "geblittet" werden muss bzw. gibt es durch einige SDL-Flags auch nochmal Geschwindigkeitsboni.
Ich würde mir halt nur gerne Vorstellen das die GUI, welche bis auf wenige Interaktion mit dem Spiel eigentlich nichts zu tun hat auch getrennt laufen kann. Man möchte schließlich die Ressourcen seines Rechnes auch voll nutzen können, gerade bei Spielen. So finde das ich bei einem Zwei/Vier/Mehr-Kern Prozessor mit *geringer* Taktzahl dann doch irgendwie als störend.

Die Schnittstellen fallen denke ich sehr knapp aus. Man braucht bei beiden die pygame Events und am Ende beide Oberflächen die dann im letzten Schritt übernander "geblittet" werden. Ansonsten bleiben dann nur noch die Spieldaten. Aber eventuell ist die Kommunikation doch schon zu Aufwendig beim übertragen der Oberfläche, wenn ich das richtig verstanden habe, dann gibt es bei multiprocessing keine gemeinsame Speicherverwaltung und das Bild* muss ja bei meinem Ziel 30mal pro Sekunde übertragen werden. Ich habe auf diesem Gebiet leider noch nicht viel Erfahrung gesammelt. - Deswegen frage ich ja nach :mrgreen:

*sagen wir maximal 1920x1080 bei 4 Farbkanälen als 32bit
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
deets

Ich denke, das mindeste was du optimieren kannst ist, nicht immer alles komplett neu zu blitten, sondern deine GUI in einen hintergrund-buffer zu rendern, der dann final (so wie beim multi-prozess-ansatz) in das Ergebnis "gemischt" wird.

Das cachest du, und blittest es jeden Frame neu. Und nur, wenn die GUI wirklich etwas veraendert, rechnest du die durch.

Wenn das nicht schnell genug ist, dann hilft dir dein anderer Ansatz ja auch nicht.

Was das angucken angeht: das sind ein paar hundert oder tausend Zeilen, das liest man sich nicht mal so eben durch...
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

deets hat geschrieben:Was das angucken angeht: das sind ein paar hundert oder tausend Zeilen, das liest man sich nicht mal so eben durch...
Ich meinte eigentlich du solltest es mal ausführen, war jetzt aber auch nicht zwingend notwendig, aber ich muss mal nachzählen :mrgreen: .

Momentan, also in dem neuen System cache ich jedes Widget, das kann ich noch ein wenig optimieren und auf jedes Surface ausbauen. Die Schwachstelle bleibt aber nach wievor das "blitten". Was nützt es mir schließlich beim Stillstand eine optimale Leistung zu erzielen und wenn ein Fenster bewegt wird gehen die fps in den Keller.
Deine Idee nur das "blitten" auszulagern kam mir jetzt auch nach ein paar Stunden schlaf. Aber wie bewerkstelligt man das am besten. Bis auf ein paar Ausnahmen wird für das "blitten" immer eine einzige Funktion* ausgeführt. Ich werde mich erstmal näher mit der "blit" Implimentation von pygame auseinander setzen. Ich bin natürlich für allen Ratschläge offen.

*blit() in BaseWidget
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Ich bin kein pygame-Experte und habe auch nur ganz kurz auf den Code auf github geschaut, aber hast du mal probiert, statt in jedem Durchlauf ein neues Surface-Objekt (in Gui.draw) zu erzeugen und explizit auf das Display zu blitten, stattdessen den eingebauten Double-Buffer-Mechanismus von pygame mittels pygame.display.flip() zu nutzen?

Ich könnte mir vorstellen, dass Erschaffen eines Surface - immerhin ein Speicherbereich von etlichen MB (mein zukünftiger iMac würde hier pro Screen 14 MB brauchen) - eine doch recht teure Operation ist. Wenigstens deinen Hintergrund-Surface könntest du doch aufbewahren.

Andernfalls müsstest du auf den klassischen Weg, ein UI zu zeichnen ausweichen und auf nur einem Surface damage rectangles verwalten und dann nur diese Bereiche neu zeichnen. Das sollte gerade wenn das UI z.B. nicht mehr macht als einen Cursor blinken zu lassen, deutlich (sehr, sehr deutlich) weniger Rechenleistung erfordern. Dafür ist das UI komplizierter. Doch das funktionierte schon auf Rechnern, die wie z.B. der Amiga 8 MHz Prozessoren hatten. wenn auch bei deutlich geringerer Bildschirmauflösung.

Stefan
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Ok, ich möchte dir nicht vorwerfen den Quellcode nicht ganz gelesen zuhaben, schließlich ist es schon ziemlich viel. :wink:
Das Prinzip wie die GUI momentan entworfen ist sieht so aus:
- Es gibt einen Toplevel-Container der am Anfang angelegt und einem GUI-Objekt zugeordnet wird. Dieses Objekt ist also für jedes Widget auf sich verantwortlich vom Focus/Event-handling bis zum zeichnen.

- Es gibt eine Hauptschleife in der zwei Punkte regelmäßig abgefragt werden zum eine "gui.update()" und "gui.draw()". In beiden Methode befindet sich eine Top-Bottom Strucktur, sprich das Toplevel-Widget ruft die Update/Draw-Methoden der Kinder auf und die deren Kinder.

- Die Draw-Methode "blittet" bei jedem Aufruf nur das zwischengespeicherte Widget, es wird also kein neues Surface erstellt.
Was im übrigen nicht anährend so teuer ist wie du vermutest, da sie erstens immer nur in der Größe vom Widget erzeugt werden, man bräuchte schon ein einziges Widget in der Größe des Screens bei Vollbild um das zuereichen und zweitens ist abgesehen vom Speicherverbrauch des Python Objektes nicht wesentlich höher der GC ist zumindest schnell genug. Zudem wird der Speicherverbrauch nochmal durch die genutzten Subsurfaces minimalisiert, weil ich immer nur die Referenzen auf den Speicher halte und ihn nicht dupliziere.

- Ein Neuzeichnen(redraw) muss entweder angefordert oder erzwungen werden, erst dann wird ein neues Surface erstellt, theoretisch könnte man das erzeugen noch etwas umgestalten, sollte aber nicht ins Gewicht fallen.
In einem Test ist mir auch aufgefallen das das erzeugen eines neuen Surfaces fast genauso viel Leistung braucht wie es mit "fill" wieder schwarz zu färben. Als Optimierung könnte ich mir aber noch vorstellen das man immer nur den geänderten Ausschitt ermittelt, aber das ist sicher nicht sehr ein einfach zu ermitteln - und lohnt sich warscheinlich nicht.

- Ein "redraw" wird von einem Widget ausgelöst auf welchem ein Event stattfindet, also nicht bei sowas wie "Maus bewegt sich über dem Widget", aber bei Maus Ein/Aus-tritt auf dem Widget, halt immer wenn sich das aussehen des Widgets verändert.
Hier hatte ich mir überlegt sowas in der Art wie den hier vorgestellten Caching zu verwenden. Da Beispielsweise bei einem Button nur vier Status existieren, also wenn einer davon schon erzeugt wurde könnte man sich damit ersparen nochmal einen zu erzeugen. Brauch aber wieder etwas mehr Speicher.

- Neuzeichnen anfordern bedeutet das ein Kind-Objekt an seine Eltern weiter gibt das es verändert wurde und das entsprechende Elternteil ebenfalls neu gezeichnet werden muss. Beispiel: Toplevel - Window1 - HBox - Button1, Button2, Button3. Toplevel - Window2 -...
Wird Button2 geklickt, wird ein "redraw" angefordert und an HBox weitergeleitet, HBox leitet diesen Request an Window1 und dann an Toplevel weiter. Zuerst wird also Toplevel.draw() aufgerufen dieses hat ein angefordertes "redraw", also läst es alle Kinder-Widgets neuzeichnen. Window2 ist nicht verändert wurden, hat keinen "redraw" (nötig), also wird der alte Stand "geblittet". Window1 wurde geändert also wird der Rahmen neugezeichnet und dann alle Kinder-Widgets wieder gezeichnet. Es gibt nur eines also wird HBox auf ein "redraw" geprüft, dieses liegt vor also wird die Box neu gezeichnet. Die Box geht wiederum alle Kinder-Widgets durch und zeichnet nur Button2 neu, die anderen werden im alten Stand geblittet.

- Neuzeichnen erzwingen bedeutet aller Kinder-Widgets und deren Kinder-Widgets werden neugezeichen. Also Top-Bottom statt wie bei dem Anfordern wo es eine Bottom-Top Strucktur ist. Wird vorallem bei dem ScrollArea benötigt, da beim ändern des Inhaltes sich eventuell die ScrollBars verschieben.

Warum das blitting allerdings dermaßen teuer ist verstehe ich auch noch nicht so richtig. Ich verstehe ja warum es mehr braucht als der Amiga :) - schließlich haben ich 32-bit Bilder, die Bildschirmauflösung war wohl damals auch noch etwas geringer und das ganze spielte sich nicht in einem SDL-Fenster ab. Unter C ist das "blitting" auch nochmal um einiges schneller, Beispielprogramme die ich gesehen hatte kamen auch mit dauerhaftem "blitten", also ohne ein "redraw"-System nicht über 25% Leistung des oben genannten CPUs und das bei 60 fps.

Ein Punkt der das ganze noch etwas verlangsamt, sind die automatisch generierten Bilder die ich erzeuge. Aber zur Information, ich lese die Bilder, also das Themeset, einmal zu Beginn aus. Dazu verwende ich XML-Schablonen welche definieren was genau von den einzelnen Bildern benötigt wird. Im Grundprinzip wird jedes Bild in neun Teile zerlegt, d.h. in Ecken, Seiten und die Mitte. Dann gibt es zum zeichnen dieser Bilder auf einen bspw. Button drei Methoden welche zum strecken der Seiten und der Mitte ausgewählt werden können. So wäre 2 Methoden eine automatische Skalierung auf die Größe des Buttons und die dritte ist sozusagen ein "fill" was die Teile aneinander reiht, also kopiert. Hier werden immer nur die Referenzen auf die Grafik benutzt. Dadurch kann ich die Widgets in beliebig viele Größen rendern.
Dennoch laut cProfile nur ein heißer Tropfen auf dem Stein im Vergleich zu dem was das "blitten" verbraucht.

Last but not least:
pygame.display.flip() lohnt sich nur bei Hardware-Rendering, wie ich der Dokumentation entnehme ist pygame.display.update() für Software-Rendering besser geeignet und danach richtet sich auch mein Test-Programm:

Code: Alles auswählen

     if SCREEN_FLAGS & pygame.HWSURFACE:
            pygame.display.flip()
        else:
            pygame.display.update()
Der Dokumentation von SDL entnehme ich noch SDL_DOUBLEBUF
Enable hardware double buffering; only valid with SDL_HWSURFACE.
, sprich es kann mit in den "Screenflags" mit angegeben werden. Wie du meiner test.py entnehmen kannst ist das auch schon geplant. Auch andere Flags muss ich erst noch durchgehen ein paar habe ich noch gefunden die "ein schnelleres Zeichnen ermöglichen" aber jede dieser Flags hat auch immer einen Haken, welches das zeichnen in anderen Fällen drastisch verschlimmern kann.
Falls davon mal jemand eine Übersicht hat immer her damit.

Ich hoffe ich konnte dir mein Zeichen-System etwas näher bringen und danke für dein immer noch bestehendes Interesse, obwohl du mit SDL nicht viel am Hut hast. :D
MfG Xynon
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Newcomer
User
Beiträge: 131
Registriert: Sonntag 15. Mai 2011, 20:41

Hast du den COPYING Text selbst geschrieben????!!!!
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

:shock: wie kommst du darauf ?
Die erst Zeile sagt doch schon alles und in jedem Modul findest du auch die kurze Fassung und den Verweis auf die Quelle. Ich habe das nur so gebaut wie es vom Herausgeber der Lizenz, also GNU und da steht nun mal
http://www.gnu.org/licenses/gpl-howto.html hat geschrieben:You should also include a copy of the license itself somewhere in the distribution of your program. All programs, whether they are released under the GPL or LGPL, should include the text version of the GPL. In GNU programs the license is usually in a file called COPYING.
Also habe ich die GNU GPL auch mit Dokumentnamen COPYING beigelegt.
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Antworten