Rendering-Optimierung für Mobile

Hallo liebe Leser, Liebhaber und Profis der Programmiergrafik! Wir machen Sie auf eine Reihe von Artikeln aufmerksam, die sich mit der Optimierung des Renderings für mobile Geräte befassen: Telefone und Tablets auf Basis von iOS und Android. Der Zyklus besteht aus drei Teilen. Im ersten Teil werden wir die Funktionen der beliebten GPU- Kachelarchitektur auf Mobile untersuchen . Im zweiten Teil werden wir die Hauptfamilien von GPUs betrachten, die in modernen Geräten vorgestellt werden, und ihre Stärken und Schwächen betrachten. Im dritten Teil werden wir uns mit den Funktionen der Shader-Optimierung vertraut machen.

Kommen wir also zum ersten Teil.

Die Entwicklung von Grafikkarten auf Desktops und Konsolen erfolgte ohne wesentliche Einschränkungen des Stromverbrauchs. Mit dem Aufkommen von Grafikkarten für mobile Geräte standen die Ingenieure vor der Aufgabe, eine akzeptable Leistung bei vergleichbaren Desktop-Auflösungen sicherzustellen, während der Stromverbrauch solcher Grafikkarten um 2 Größenordnungen niedriger sein sollte. 



Die Lösung wurde in einer speziellen Architektur namens Tile Based Rendering (TBR) gefunden . Für einen Grafikprogrammierer mit Erfahrung in der PC-Entwicklung scheint alles bekannt zu sein, wenn er sich mit der mobilen Entwicklung vertraut macht: Es wird eine ähnliche OpenGL ES-API verwendet, dieselbe Struktur wie die Grafikpipeline. Die Kachelarchitektur mobiler GPUs unterscheidet sich jedoch erheblich von der auf PC / Immediate Mode- Konsolen verwendeten . Wenn Sie die Stärken und Schwächen von TBR kennen, können Sie die richtigen Entscheidungen treffen und mit Mobile eine hervorragende Leistung erzielen.

Unten sehen Sie ein vereinfachtes Diagramm einer klassischen Grafikpipeline, die im dritten Jahrzehnt auf PCs und Konsolen verwendet wurde.


In der Geometrieverarbeitungsphase werden die Scheitelpunktattribute aus dem GPU-Videospeicher gelesen. Nach verschiedenen Transformationen (Vertex Shader) werden renderfertige Grundelemente in der ursprünglichen Reihenfolge (FIFO) an den Rasterisierer übergeben, der die Grundelemente in Pixel unterteilt. Danach wird die Fragmentverarbeitungsstufe jedes Pixels (Fragment Shader) ausgeführt , und die erhaltenen Farbwerte werden in den Bildschirmpuffer geschrieben, der sich ebenfalls im Videospeicher befindet. Ein Merkmal der traditionellen Architektur des „Sofortmodus“ ist die Aufzeichnung des Ergebnisses des Fragment-Shaders in beliebigen Abschnitten des Bildschirmpuffers bei der Verarbeitung eines einzelnen Zeichnungsaufrufs. Daher kann für jeden Ziehungsaufruf der Zugriff auf den gesamten Bildschirmpuffer erforderlich sein. Das Arbeiten mit einem großen Speicherarray erfordert eine geeignete Busbandbreite ( Bandbreite ) und ist mit einem hohen Stromverbrauch verbunden. Daher verfolgten mobile GPUs einen anderen Ansatz. Bei der für mobile Grafikkarten typischen Kachelarchitektur erfolgt das Rendern in einem kleinen Speicher, der dem Teil des Bildschirms entspricht - der Kachel. Die geringe Größe der Kachel (z. B. 16 x 16 Pixel für Mali-Grafikkarten, 32 x 32 Pixel für PowerVR) ermöglicht es Ihnen, sie direkt auf dem Grafikkartenchip zu platzieren, wodurch die Zugriffsgeschwindigkeit mit der Zugriffsgeschwindigkeit auf die Shader-Kernregister vergleichbar wird, d. H. sehr schnell.


Da Grundelemente jedoch in beliebige Abschnitte des Bildschirmpuffers fallen können und eine Kachel nur einen kleinen Teil davon abdeckt, war ein zusätzlicher Schritt in der Grafikpipeline erforderlich. Das folgende Diagramm zeigt vereinfacht, wie die Pipeline mit der Kachelarchitektur funktioniert.


Nach der Verarbeitung der Eckpunkte und der Konstruktion der Grundelemente fallen letztere, anstatt an die Fragment-Pipeline gesendet zu werden, in den sogenannten Tiler . Hier sind die Grundelemente durch Kacheln verteilt, in deren Pixel sie fallen. Nach einer solchen Verteilung, die in der Regel alle Zeichnungsaufrufe abdeckt, die an ein Frame Buffer Object (auch als Render Target bezeichnet ) gerichtet sind, werden die Kacheln nacheinander gerendert. Für jede Kachel wird die folgende Abfolge von Aktionen ausgeführt:

  1. Laden alter FBO- Inhalte aus dem Systemspeicher ( Laden
  2. Darstellung von Primitiven, die in diese Kachel fallen
  3. Hochladen neuer FBO- Inhalte in den Systemspeicher ( Store )


Es ist zu beachten, dass die Ladeoperation als zusätzliche Überlagerung der "Vollbildtextur" ohne Komprimierung betrachtet werden kann. Vermeiden Sie nach Möglichkeit diesen Vorgang, d. H. Lassen Sie FBO nicht hin und her wechseln . Wenn alle Inhalte vor dem Rendern in FBO gelöscht wurden, wird der Ladevorgang nicht ausgeführt. Um jedoch das richtige Signal an den Fahrer zu senden, müssen die Parameter einer solchen Reinigung bestimmte Kriterien erfüllen:

  1. Muss deaktiviert sein Scissor Rect
  2. Die Aufnahme in allen Farbkanälen und Alpha sollte erlaubt sein.

Um den Ladevorgang für den Tiefenpuffer und die Schablone zu verhindern, müssen diese vor Beginn des Renderns gereinigt werden.

Es ist auch möglich, den Speichervorgang für den Tiefen- / Schablonenpuffer zu vermeiden . Schließlich wird der Inhalt dieser Puffer in keiner Weise auf dem Bildschirm angezeigt. Vor dem glSwapBuffers Betrieb, Sie können anrufen glDiscardFramebufferEXT oder glInvalidateFramebuffer

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glDiscardFramebufferEXT (GL_FRAMEBUFFER, 2, attachments);

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, attachments);

Es gibt Rendering-Szenarien, in denen die Platzierung von Tiefen- / Schablonenpuffern sowie MSAA- Puffern im Systemspeicher nicht erforderlich ist. Wenn beispielsweise das Rendern im FBO mit dem Tiefenpuffer kontinuierlich ist und die Tiefeninformationen aus dem vorherigen Frame nicht verwendet werden, muss der Tiefenpuffer vor dem Beginn des Renderns nicht in den Kachelspeicher geladen oder nach Abschluss des Renderns entladen werden. Daher kann der Systemspeicher nicht unter dem Tiefenpuffer zugeordnet werden. Mit modernen Grafik-APIs wie Vulkan und Metal können Sie den Speichermodus für Ihre FBO- Gegenstücke explizit festlegen  ( MTLStorageModeMemoryless in Metal, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT + VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT in Vulkan ).

Besonders hervorzuheben ist die Implementierung von MSAA auf Kachelarchitekturen. Der hochauflösende Puffer für die MSAA verlässt den Kachelspeicher nicht, indem der FBO in mehrere Kacheln aufgeteilt wird. Beispielsweise werden für MSAA 2x2 16x16- Kacheln während des Speichervorgangs als 8x8 aufgelöst , d. H. Insgesamt müssen viermal mehr Kacheln verarbeitet werden. Zusätzlicher Speicher für MSAA ist jedoch nicht erforderlich, und aufgrund des Renderns im schnellen Kachelspeicher gibt es keine signifikanten Bandbreitenbeschränkungen. Verwenden Sie jedochMSAA für die Kachelarchitektur erhöht die Belastung von Tiler, was sich negativ auf die Renderleistung von Szenen mit viel Geometrie auswirken kann.

Zusammenfassend stellen wir das gewünschte Schema für die Arbeit mit FBO an der Kachelarchitektur vor:

// 1.   ,    auxFBO
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
glDisable(GL_SCISSOR);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
// glClear,     
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | 
           GL_STENCIL_BUFFER_BIT);

renderAuxFBO();         

//   /      
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);
// 2.   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
glDisable(GL_SCISSOR);

glClear(...);
//   mainFBO    auxFBO
renderMainFBO(auxFBO);

glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);

Wenn Sie mitten in der MainFBO- Formation zum AuxFBO- Rendering wechseln , können unnötige Lade- und Speicheroperationen auftreten , die die Frame-Formationszeit erheblich verlängern können. In unserer Praxis haben wir eine Verlangsamung beim Rendern festgestellt, selbst im Fall von FBO-Einstellungen im Leerlauf, d. H. ohne den tatsächlichen Render in ihnen. Aufgrund der Architektur des Motors sah unsere alte Schaltung folgendermaßen aus:

//   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
//  auxFBO
renderAuxFBO();

glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   mainFBO
renderMainFBO(auxFBO);

Trotz des Fehlens von gl-Aufrufen nach der ersten Installation von mainFBO haben wir auf einigen Geräten zusätzliche Load & Store- Vorgänge und eine schlechtere Leistung erhalten.

Um unser Verständnis des Overheads durch die Verwendung von Zwischen- FBOs zu verbessern , haben wir den Zeitverlust für das Umschalten von Vollbild- FBOs mithilfe eines synthetischen Tests gemessen. Die Tabelle zeigt die Zeit, die für die Speicheroperation aufgewendet wurde, wenn FBO mehrmals in einem Frame gewechselt wurde (die Zeit für eine solche Operation ist angegeben). Lastbetrieb fehlt aufgrund glCleard.h. ein günstigeres Szenario wurde gemessen. Die auf dem Gerät verwendete Berechtigung trug dazu bei. Es könnte mehr oder weniger der Leistung der installierten GPU entsprechen. Daher geben diese Zahlen nur einen allgemeinen Überblick darüber, wie teuer das Umschalten von Zielen auf mobilen Grafikkarten verschiedener Generationen ist.
GPUMillisekundenGPUMillisekunden
Adreno 3205.2
Adreno 5120,74
PowerVR G62003.3Adreno 6150,7
Mali-4003.2Adreno 5300,4
Mali-t7201.9Mali-g510,32
PowerVR SXG 5441.4Mali-t830
0,15

Basierend auf den erhaltenen Daten können wir zu der Empfehlung kommen, nicht mehr als einen oder zwei FBO-Schalter pro Frame zu verwenden, zumindest für ältere Grafikkarten. Wenn das Spiel einen separaten Code-Pass für Low-End-Geräte hat, ist es ratsam, die FBO-Änderung dort nicht zu verwenden. Im Low-End-Bereich wird jedoch häufig das Problem der Verringerung der Auflösung relevant. Unter Android können Sie die Renderauflösung verringern, ohne auf einen Zwischen-FBO zurückgreifen zu müssen, indem Sie SurfaceHolder.setFixedSize () aufrufen :

surfaceView.getHolder().setFixedSize(...)

Diese Methode funktioniert nicht, wenn das Spiel über die Hauptanwendung Surface gerendert wird (ein typisches Schema für die Arbeit mit NativeActivity ). Wenn Sie die Hauptoberfläche verwenden , können Sie eine niedrigere Auflösung festlegen, indem Sie die native Funktion ANativeWindow_setBuffersGeometry aufrufen.

JNIEXPORT void JNICALL Java_com_organization_app_AppNativeActivity_setBufferGeometry(JNIEnv *env, jobject thiz, jobject surface, jint width, jint height)
{
ANativeWindow* window = ANativeWindow_fromSurface(env, surface); 
ANativeWindow_setBuffersGeometry(window, width, height, AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM); 
}

In Java:

private static native void setBufferGeometry(Surface surface, int width , int height )
...
//   SurfaceHolder.Callback
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
     setBufferGeometry(holder.getSurface(), 768, 1366); /* ... */
...

Schließlich erwähnen wir den praktischen ADB-Befehl zum Steuern ausgewählter Oberflächenpuffer unter Android:

adb shell dumpsys surfaceflinger

Sie können eine ähnliche Schlussfolgerung ziehen, mit der Sie den Speicherverbrauch für Oberflächenpuffer abschätzen können:


Die Abbildung zeigt das System hervorgehoben 3 Puffer für triple Pufferung des GLSurfaceView Spiel (gelb markiert), sowie 2 - Puffer für die Hauptfläche (rot markiert). Beim Rendern über die Hauptoberfläche, die bei Verwendung von NativeActivity das Standardschema ist , kann die Zuweisung zusätzlicher Puffer vermieden werden. 

Das ist alles für jetzt. In den folgenden Artikeln werden wir mobile GPUs klassifizieren und Methoden zur Optimierung von Shadern für sie analysieren.

All Articles