Engine & platform

Entfernen von skriptfähigen Shadervarianten

CHRISTOPHE RICCIO / UNITY TECHNOLOGIESContributor
May 14, 2018|12 Min.
Entfernen von skriptfähigen Shadervarianten
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.

Reduzieren Sie die Player-Build-Zeit und Datengröße erheblich, indem Sie Entwicklern die Kontrolle darüber geben, welche Shader-Varianten vom Unity Shader-Compiler verarbeitet und in die Player-Daten aufgenommen werden.

Aufgrund der steigenden Anzahl von Shader-Varianten erhöhen sich die Erstellungszeit und die Datengröße des Players zusammen mit der Komplexität Ihres Projekts.

Mit dem in der Betaversion 2018.2eingeführten skriptfähigen Stripping von Shader-Varianten können Sie die Anzahl der generierten Shader-Varianten verwalten und so die Build-Zeit und Datengröße des Players drastisch reduzieren.

Mit dieser Funktion können Sie alle Shader-Varianten mit ungültigen Codepfaden entfernen, Shader-Varianten für nicht verwendete Funktionen entfernen oder Shader-Build-Konfigurationen wie „Debug“ und „Release“ erstellen, ohne die Iterationszeit oder den Wartungsaufwand zu beeinträchtigen.

In diesem Blogbeitrag definieren wir zunächst einige der von uns verwendeten Begriffe. Dann konzentrieren wir uns auf die Definition von Shader-Varianten, um zu erklären, warum wir so viele generieren können. Anschließend folgt eine Beschreibung der automatischen Entfernung von Shader-Varianten und wie die skriptbasierte Entfernung von Shader-Varianten in der Shader-Pipeline-Architektur von Unity implementiert wird. Anschließend sehen wir uns die API zum Stripping skriptfähiger Shader-Varianten an, bevor wir die Ergebnisse der Fountainbleau-Demo besprechen und zum Abschluss einige Tipps zum Schreiben von Stripping-Skripten geben.

Das Erlernen des Strippings von skriptfähigen Shader-Varianten ist kein triviales Unterfangen, kann aber zu einer enormen Steigerung der Teameffizienz führen!

Konzepte

Um die Funktion zum Entfernen skriptfähiger Shader-Varianten zu verstehen, ist es wichtig, die verschiedenen damit verbundenen Konzepte genau zu kennen.

  • Shader-Asset: Der vollständige Dateiquellcode mit Eigenschaften, Sub-Shader, Pässen und HLSL.
  • Shader-Ausschnitt: Der HLSL-Eingabecode mit Abhängigkeiten für eine einzelne Shader-Stufe.
  • Shader-Stufe: Eine bestimmte Phase in der GPU-Rendering-Pipeline, normalerweise eine Vertex-Shader-Phase und eine Fragment-Shader-Phase.
  • Shader-Schlüsselwort: Eine Präprozessorkennung für Verzweigungen zur Kompilierungszeit über Shader hinweg.
  • Shader-Schlüsselwortsatz: Ein bestimmter Satz von Shader-Schlüsselwörtern, die einen bestimmten Codepfad identifizieren.
  • Shader-Variante: Der plattformspezifische Shadercode, der vom Unity -Shader-Compiler für eine einzelne Shaderstufe für eine bestimmte Grafikebene, einen bestimmten Durchlauf, einen bestimmten Shader-Schlüsselwortsatz usw. generiert wird.
  • Über-Shader: Eine Shaderquelle, die viele Shadervarianten erzeugen kann.

In Unity werden Uber-Shader von ShaderLab- Sub-Shadern, -Pässen und -Shader-Typen sowie den Präprozessor-Direktiven#pragma multi_compile und #pragma shader_feature verwaltet.

Zählen der Anzahl der generierten Shadervarianten

Um die skriptbasierte Entfernung von Shader-Varianten zu verwenden, müssen Sie genau verstehen, was eine Shader-Variante ist und wie Shader-Varianten von der Shader-Build-Pipeline generiert werden. Die Anzahl der generierten Shader-Varianten ist direkt proportional zur Build-Zeit und der Datengröße der Player-Shader-Variante. Eine Shader-Variante ist eine Ausgabe der Shader-Build-Pipeline.

Shader-Schlüsselwörter sind eines der Elemente, die die Generierung von Shader-Varianten bewirken. Eine unüberlegte Verwendung von Shader-Schlüsselwörtern kann schnell zu einer Explosion der Anzahl an Shader-Varianten und damit zu einer extrem langen Build-Zeit führen.

Um zu sehen, wie Shader-Varianten generiert werden, zählt der folgende einfache Shader, wie viele Shader-Varianten er produziert:

Shader "ShaderVariantsStripping"
{
	SubShader
	{
		Pass
		{
			Name "ShaderVariantsStripping/Pass"

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY
			#pragma multi_compile OP_ADD OP_MUL OP_SUB

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}

			fixed4 get_color()
			{
				#if defined(COLOR_ORANGE)
					return fixed4(1.0, 0.5, 0.0, 1.0);
				#elif defined(COLOR_VIOLET)
					return fixed4(0.8, 0.2, 0.8, 1.0);
				#elif defined(COLOR_GREEN)
					return fixed4(0.5, 0.9, 0.3, 1.0);
				#elif defined(COLOR_GRAY)
					return fixed4(0.5, 0.9, 0.3, 1.0);
				#else
					#error "Unknown 'color' keyword"
				#endif
			}

			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 diffuse = tex2D(_MainTex, i.uv);

				fixed4 color = get_color();

				#if defined(OP_ADD)
					return diffuse + color;
				#elif defined(OP_MUL)
					return diffuse * color;
				#elif defined(OP_SUB)
					return diffuse - color;
				#else
					#error "Unknown 'op' keyword"
				#endif
			}
			ENDCG
		}
	}
}

Die Gesamtzahl der Shadervarianten in einem Projekt ist deterministisch und wird durch die folgende Gleichung angegeben:

Gleichung

Das folgende triviale ShaderVariantStripping-Beispiel bringt Klarheit in diese Gleichung. Es handelt sich um einen einzelnen Shader, der die Gleichung wie folgt vereinfacht:

Gleichung

Ebenso verfügt dieser Shader über einen einzelnen Sub-Shader und einen einzelnen Durchgang, was die Gleichung weiter vereinfacht:

Gleichung

Schlüsselwörter in der Gleichung beziehen sich sowohl auf Plattform- als auch auf Shader-Schlüsselwörter. Eine Grafikebene ist eine plattformspezifische Kombination aus Schlüsselwortsätzen.

ShaderVariantStripping/Pass hat zwei Multi-Compile-Direktiven. Die erste Direktive definiert 4 Schlüsselwörter (COLOR_ORANGE, COLOR_VIOLET, COLOR_GREEN, COLOR_GRAY) und die zweite Direktive definiert 3 Schlüsselwörter (OP_ADD, OP_MUL, OP_SUB). Schließlich definiert der Pass zwei Shader-Stufen: eine Vertex-Shader-Stufe und eine Fragment-Shader-Stufe.

Diese Gesamtzahl der Shadervarianten wird für eine einzige unterstützte Grafik- API angegeben. Allerdings benötigen wir für jede unterstützte Grafik- API im Projekt einen dedizierten Satz von Shader-Varianten. Wenn wir beispielsweise einen Android-Player erstellen, der sowohl OpenGL ES 3 als auch Vulkan unterstützt, benötigen wir zwei Sätze von Shader-Varianten. Daher sind die Player-Build-Zeit und die Shader-Datengröße direkt proportional zur Anzahl der unterstützten Grafik-APIs.

Shader-Build-Pipeline

Die Shader-Kompilierungspipeline in Unity ist eine Blackbox, in der jeder Shader im Projekt analysiert wird, um Shader-Snippets zu extrahieren, bevor die Varianten-Vorverarbeitungsanweisungen wie „multi_compile“ und „shader_feature“ gesammelt werden. Dadurch wird eine Liste mit Kompilierungsparametern erstellt, einer pro Shader-Variante.

Zu diesen Kompilierungsparametern gehören der Shader-Ausschnitt, die Grafikebene, der Shader-Typ, der Shader-Schlüsselwortsatz, der Pass-Typ und der Name. Jeder der festgelegten Kompilierungsparameter wird verwendet, um eine einzelne Shader-Variante zu erstellen.

Folglich führt Unity einen automatischen Durchgang zum Entfernen der Shader-Varianten auf der Grundlage von zwei Heuristiken aus. Erstens basiert das Entfernen auf den Projekteinstellungen. Wenn beispielsweise die Unterstützung für virtuelle Realität deaktiviert ist, werden VR Shader-Varianten systematisch entfernt. Zweitens basiert das automatische Stripping auf der Konfiguration des Abschnitts „Shader-Stripping“ der Grafikeinstellungen.

Optionen zum automatischen Entfernen von Shader-Varianten in den Grafikeinstellungen.
Optionen zum automatischen Entfernen von Shader-Varianten in den Grafikeinstellungen.

Das automatische Entfernen von Shadervarianten basiert auf Einschränkungen bei der Build-Zeit. Unity kann zur Build-Zeit nicht automatisch nur die erforderlichen Shader-Varianten auswählen, da diese Shader-Varianten von der C#-Ausführung zur Laufzeit abhängen. Wenn beispielsweise ein C#-Skript ein Punktlicht hinzufügt, zum Build-Zeitpunkt aber keine Punktlichter vorhanden waren, kann die Shader-Build-Pipeline nicht erkennen, dass der Player eine Shader-Variante benötigt, die Punktlicht-Shading ermöglicht.

Hier ist eine Liste von Shader-Varianten mit aktivierten Schlüsselwörtern, die automatisch entfernt werden:

Lightmap-Modi: LIGHTMAP_ON, DIRLIGHTMAP_COMBINED, DYNAMICLIGHTMAP_ON, LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK

Nebelmodi: FOG_LINEAR, FOG_EXP, FOG_EXP2

Instanziierungsvarianten: INSTANCING_ON

Darüber hinaus werden bei deaktivierter Virtual Reality-Unterstützung die Shader-Varianten mit den folgenden integrierten aktivierten Schlüsselwörtern entfernt:

STEREO_INSTANCING_ON, STEREO_MULTIVIEW_ON, STEREO_CUBEMAP_RENDER_ON, UNITY_SINGLE_PASS_STEREO

Wenn das automatische Entfernen abgeschlossen ist, verwendet die Shader-Build-Pipeline die verbleibenden Kompilierungsparametersätze, um die Kompilierung der Shader-Varianten parallel zu planen und so viele gleichzeitige Kompilierungen zu starten, wie die Plattform über CPU-Kernthreads verfügt.

Hier ist eine visuelle Darstellung dieses Prozesses:

Shader-Pipeline-Architektur mit skriptfähiger Shader-Varianten-Stripping-Integration in Orange.
Shader-Pipeline-Architektur mit skriptfähiger Shader-Varianten-Stripping-Integration in Orange.

In der Betaversion von Unity 2018.2führt die Shader-Pipeline-Architektur direkt vor der Planung der Shader-Variantenkompilierung eine neue Phase ein, die es Benutzern ermöglicht, die Kompilierung der Shader-Varianten zu steuern. Diese neue Phase wird über C#-Rückrufe für den Benutzercode verfügbar gemacht und jeder Rückruf wird pro Shader-Snippet ausgeführt.

Skriptfähige API zum Entfernen von Shader-Varianten

Das folgende Skript ermöglicht beispielsweise das Entfernen aller Shader-Varianten, die mit einer „DEBUG“-Konfiguration verknüpft wären, die durch ein im Player-Entwicklungsbuild verwendetes Schlüsselwort „DEBUG“ identifiziert wird.

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

// Simple example of stripping of a debug build configuration
class ShaderDebugBuildProcessor : IPreprocessShaders
{
    ShaderKeyword m_KeywordDebug;

    public ShaderDebugBuildProcessor()
    {
        m_KeywordDebug = new ShaderKeyword("DEBUG");
    }

    // Multiple callback may be implemented.
    // The first one executed is the one where callbackOrder is returning the smallest number.
    public int callbackOrder { get { return 0; } }

    public void OnProcessShader(
        Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
    {
        // In development, don't strip debug variants
        if (EditorUserBuildSettings.development)
            return;

        for (int i = 0; i < shaderCompilerData.Count; ++i)
        {
            if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug))
            {
                shaderCompilerData.RemoveAt(i);
                --i;
            }
        }
    }
}


OnProcessShader wird unmittelbar vor der Planung der Kompilierung der Shader-Variante aufgerufen.

Jede Kombination aus einem Shader, einer ShaderSnippetData- und ShaderCompilerData-Instanz ist ein Bezeichner für eine einzelne Shader-Variante, die der Shader-Compiler erzeugt. Um diese Shader-Variante zu entfernen, müssen wir sie nur aus der ShaderCompilerData-Liste entfernen.

Jede einzelne Shader-Variante, die der Shader-Compiler generieren soll, wird in diesem Rückruf erscheinen. Wenn Sie an der Skripterstellung zum Entfernen der Shader-Varianten arbeiten, müssen Sie zunächst herausfinden, welche Varianten entfernt werden müssen, da sie für das Projekt nicht nützlich sind.

Ergebnisse
Entfernen von Shadervarianten für eine Renderpipeline

Ein Anwendungsfall für das skriptbasierte Entfernen von Shader-Varianten ist das systematische Entfernen ungültiger Shader-Varianten einer Render-Pipeline aufgrund der verschiedenen Kombinationen von Shader-Schlüsselwörtern.

Ein in der HD-Render-Pipeline enthaltenes Skript zum Entfernen von Shader-Varianten ermöglicht Ihnen, die Erstellungszeit und -größe eines Projekts mithilfe der HD-Render-Pipeline systematisch zu reduzieren. Dieses Skript gilt für die folgenden Shader:

HDRenderPipeline/Lit
HDRenderPipeline/LitTessellation
HDRenderPipeline/LayeredLit
HDRenderPipeline/LayeredLitTessellation

Das Skript erzeugt die folgenden Ergebnisse:

                                 Unstripped    Stripped 
Player Data Shader Variant Count 24350 (100%)  12122 (49.8%) 
Player Data Size on disk         511 MB        151 MB 
Player Build Time                4864 seconds  1356 seconds
Screenshot der Fontainebleau-Photogrammetrie-Demo mit der HD-Render-Pipeline der Standardauflösung 1920 x 1080 der PlayStation 4.
Screenshot der Fontainebleau-Photogrammetrie-Demo mit der HD-Render-Pipeline der Standardauflösung 1920 x 1080 der PlayStation 4.

Darüber hinaus verfügt die Lightweight-Render-Pipeline für Unity 2018.2 über eine Benutzeroberfläche zur automatischen Speisung eines Stripping-Skripts , das bis zu 98 % der Shader-Varianten automatisch entfernen kann, was unserer Erwartung nach insbesondere für mobile Projekte wertvoll ist.

Shader-Varianten-Stripping für ein Projekt

Ein weiterer Anwendungsfall ist ein Skript zum Entfernen aller Rendering-Funktionen einer Render-Pipeline, die für ein bestimmtes Projekt nicht verwendet werden. Mithilfe einer internen Testdemo für die Lightweight-Rendering-Pipelinehaben wir die folgenden Ergebnisse für das gesamte Projekt erhalten:

                                 Unstripped  Stripped 
Player Data Shader Variant Count 31080       7056 
Player Data Size on disk         121         116 
Player Build Time                839 seconds 286 seconds

Wie wir sehen, kann die Verwendung des skriptbasierten Strippings von Shader-Varianten zu signifikanten Ergebnissen führen, und mit mehr Arbeit am Stripping-Skript könnten wir sogar noch weiter gehen.

Screenshot einer Lightweight-Pipeline-Demo.
Screenshot einer Lightweight-Pipeline-Demo.
Tipps zum Schreiben von Code zum Entfernen von Shadervarianten
Verbesserung des Shader-Code-Designs

Bei einem Projekt kann es schnell zu einer Explosion der Anzahl an Shader-Varianten kommen, was zu untragbaren Kompilierungszeiten und einer Vergrößerung der Player-Datenmenge führt. Das skriptbasierte Stripping von Shadern hilft bei der Lösung dieses Problems. Sie sollten jedoch Ihre Verwendung von Shader-Schlüsselwörtern überdenken, um relevantere Shader-Varianten zu generieren. Wir können uns auf das #pragma skip_variants verlassen, um nicht verwendete Schlüsselwörter im Editor zu testen.

Beispielsweise werden in ShaderStripping/Color Shader die Vorverarbeitungsdirektiven mit dem folgenden Code deklariert:

#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY // color keywords
#pragma multi_compile OP_ADD OP_MUL OP_SUB // operator keywords


Dieser Ansatz impliziert, dass alle Kombinationen aus Farbschlüsselwörtern und Operatorschlüsselwörtern generiert werden.

Nehmen wir an, wir möchten die folgende Szene rendern:

COLOR_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.
COLOR_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.

Zunächst sollten wir sicherstellen, dass jedes Schlüsselwort tatsächlich nützlich ist. In dieser Szene werden COLOR_GRAY und OP_SUB nie verwendet. Wenn wir garantieren können, dass diese Schlüsselwörter niemals verwendet werden, sollten wir sie entfernen.

Zweitens sollten wir Schlüsselwörter kombinieren, die effektiv einen einzelnen Codepfad erzeugen. In diesem Beispiel wird die Operation „Addieren“ immer ausschließlich mit der Farbe „Orange“ verwendet. Wir können sie also in einem einzigen Schlüsselwort kombinieren und den Code wie unten gezeigt umgestalten.

#pragma multi_compile ADD_COLOR_ORANGE MUL_COLOR_VIOLET MUL_COLOR_GREEN

#if defined(ADD_COLOR_ORANGE)
	#define COLOR_ORANGE
	#define OP_ADD
#elif defined(MUL_COLOR_VIOLET)
	#define COLOR_VIOLET
	#define OP_MUL
#elif defined(MUL_COLOR_GREEN)
	#define COLOR_GREEN
	#define OP_MUL
#endif


Natürlich ist es nicht immer möglich, Schlüsselwörter umzugestalten. In diesen Fällen ist das skriptbasierte Entfernen von Shader-Varianten ein wertvolles Werkzeug!

Verwenden von callbackOrder zum Entfernen von Shadervarianten in mehreren Schritten

Für jeden Snippet werden alle Skripte zum Entfernen der Shader-Varianten ausgeführt. Wir können die Ausführung der Skripte sortieren, indem wir den von der Memberfunktion „callbackOrder“ zurückgegebenen Wert sortieren. Die Shader-Build-Pipeline führt die Rückrufe in aufsteigender Reihenfolge aus, also den niedrigsten zuerst und den höchsten zuletzt.

Ein Anwendungsfall für die Verwendung mehrerer Shader-Stripping-Skripte besteht darin, die Skripterstellung nach Zweck zu trennen. Zum Beispiel:

  • Skript 1: Entfernt systematisch alle Shader-Varianten mit ungültigen Codepfaden.
  • Skript 2: Entfernt alle Debug-Shader-Varianten.
Varianten
  • Skript 3: Entfernt alle Shader-Varianten in der Codebasis, die für das aktuelle Projekt nicht erforderlich sind.
  • Skript 4: Protokolliert die verbleibenden Shader-Varianten und entfernt sie alle, um eine schnelle Iterationszeit der Stripping-Skripte zu gewährleisten.
Vorgehensweise zum Schreiben eines Skripts zum Entfernen von Shader-Varianten

Das Entfernen von Shader-Varianten ist äußerst leistungsfähig, erfordert aber viel Arbeit, um gute Ergebnisse zu erzielen.

Filtern Sie in der Projektansicht nach allen Shadern.

Wählen Sie einen Shader aus und klicken Sie im Inspektor auf „Anzeigen“, um die Liste der Schlüsselwörter/Varianten dieses Shaders zu öffnen. Es wird eine Liste mit Schlüsselwörtern geben, die immer im Build enthalten sind.

Stellen Sie sicher, dass Sie wissen, welche spezifischen Grafikfunktionen das Projekt verwendet.

Überprüfen Sie, ob die Schlüsselwörter in allen Shader-Stufen verwendet werden. Für Phasen, die diese Schlüsselwörter nicht verwenden, ist nur eine Variante erforderlich.

Entfernen Sie Shader-Varianten aus dem Skript.

Überprüfen Sie die visuellen Elemente im Build.

Wiederholen Sie die Schritte 2 – 6 für jeden Shader.

Herunterladen des Beispielprojekts

Das zur Illustration dieses Blogbeitrags verwendete Beispielprojekt kann hierheruntergeladen werden. Es erfordert Unity 2018.2.0b1.

Erfahren Sie mehr über die Optimierung der binären Bereitstellungsgröße bei Unite Berlin

Kommen Sie zum Vortrag von Jonas Echterhoff am 21. Juni und erfahren Sie mehr über alle neuen Tools, die Ihnen mehr Kontrolle darüber geben, was in Ihrem Build landet!