Tipp: Wie man ID3D10Device::Create…Shader() richtig wrappt

Hier können Artikel, Tutorials, Bücherrezensionen, Dokumente aller Art, Texturen, Sprites, Sounds, Musik und Modelle zur Verfügung gestellt bzw. verlinkt werden.
Forumsregeln
Möglichst sinnvolle Präfixe oder die Themensymbole nutzen.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Tipp: Wie man ID3D10Device::Create…Shader() richtig wrappt

Beitrag von Krishty »

Tipp: Wie man ID3D10Device::Create…Shader() richtig wrappt

Wer auf das FX-Framework verzichtet und seine Shader noch von Hand kompiliert, wird CreateVertexShader(), CreateGeometryShader() und CreatePixelShader() kennen. Doch die meisten Leute – auch die Direct3D-Samples selbst – nutzen die vereinigte Shader-Architektur nicht aus und machen sich das Leben schwer.
An dieser Stelle ein Hinweis: Mit D3D11 kommen Compute-, Hull- und Domain-Shader … es wird also nicht einfacher ;) Darum erarbeiten wir uns hier eine kurze, sichere Methode zum Laden von Shadern, die sich zunutze macht, dass sich die vereinigten Shader nur im Profil und dem entsprechenden Aufruf von Create…Shader() unterscheiden.

Schauen wir uns zuerst einmal die gängigen Praktiken an:


1. Welches Problem? Was lösen?

Leider am weitesten verbreitet ist, jeden Gedanken an dieses Problem mit Strg+C und Strg+V zu unterdrücken. Oder: Es wird eine Funktion zum Laden eines Vertex-Shaders geschrieben, dann kopiert und für Geo- und Pixelshader angepasst. Beispiel (ohne Fehlerverarbeitung zwecks Übersicht):

Code: Alles auswählen

void LoadVertexShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ::ID3D10VertexShader ** p_ppShader) {
	// Shader aus Datei laden und kompilieren
	::ID3DBlob * l_Code;
	::ID3DBlob * l_Fehlermeldungen;
	D3DXCompileShaderFromFile(p_Dateiname, …, p_Funktionsname, "vs_4_0", … l_Code, l_Fehlermeldungen);

	// Hier Shader erzeugen
	p_pDevice->CreateVertexShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), p_ppShader);
}

void LoadGeoShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ::ID3D10GeometryShader ** p_ppShader) {
	// Shader aus Datei laden und kompilieren
	::ID3DBlob * l_Code;
	::ID3DBlob * l_Fehlermeldungen;
	D3D10CompileShader(p_Dateiname, …, p_Funktionsname, "gs_4_0", … l_Code, l_Fehlermeldungen);

	// Hier Shader erzeugen
	p_pDevice->CreateGeometryShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), p_ppShader);
}

void LoadPixelShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ::ID3D10PixelShader ** p_ppShader) {
	// Shader aus Datei laden und kompilieren
	::ID3DBlob * l_Code;
	::ID3DBlob * l_Fehlermeldungen;
	D3D10CompileShader(p_Dateiname, …, p_Funktionsname, "ps_4_0", … l_Code, l_Fehlermeldungen);

	// Hier Shader erzeugen
	p_pDevice->CreatePixelShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), p_ppShader);
}
Wie wir sehen, schreiben wir dreimal fast identischen Code, der sich nur im Paramter pProfile von D3DXCompileShaderFromFile() und in Create…Shader() unterscheidet. Denkt euch jetzt noch zehn, zwanzig Zeilen Fehlerbehandlung dazu, und das ganze unter D3D11 mit drei weiteren Shader-Typen… das geht doch einfacher!


2. Problem verstanden, aber falsch gelöst

Findige Zeitgenossen haben schon erkannt, dass sich nur die beiden oben genannten Stellen vom restlichen Code unterscheiden, und kamen deshalb auf die Idee: Wenn individuelle Unterschiede behandelt werden müssen, dann natürlich mit if-else-Blöcken! (Zugegeben: dazu gehörte ich auch mal ;) ) Beispiel:

Code: Alles auswählen

enum ShaderTyp {
	VertexShader,
	PixelShader,
	GeoShader
};

void LoadShader(::ID3D10Device * p_pDevice, const char p_Dateiname[], const char p_Funktionsname[], ShaderTyp p_Typ, void ** p_ppShader) {
	const char * ProfilNachTyp[3] = { "vs_4_0", "gs_4_0", "ps_4_0" };
	// Shader aus Datei laden und kompilieren
	::ID3DBlob * l_Code;
	::ID3DBlob * l_Fehlermeldungen;
	D3D10CompileShader(p_Dateiname, …, p_Funktionsname, ProfilNachTyp[p_Typ], … l_Code, l_Fehlermeldungen);

	// Hier Shader erzeugen
	switch(p_Typ) {
	case VertexShader:
		p_pDevice->CreateVertexShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), (::ID3D10VertexShader**)p_ppShader);
	case GeoShader:
		p_pDevice->CreateGeometryShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), (::ID3D10GeometryShader**)p_ppShader);
	case PixelShader:
		p_pDevice->CreatePixelShader(l_Code->GetBufferPointer(), l_Code->GetBufferSize(), (::ID3D10PixelShader**)p_ppShader);
	}
}
Diese Version ist schon signifikant kürzer – insbesondere wenn sie Fehlerverarbeitung enthält – dafür beinhaltet sie aber auch fiese Zeiger-Casts und einen void-Zeiger als Parameter. Auch das enum ist nicht elegant, weil nirgendwo sicher gestellt wird dass es zum übergebenen Zeiger passt.

Denken wir also nach. Wie können wir automatisch Code aufrufen, der immer zum Typ passt? Durch Überladungen? Ja, aber dann müssten wir wieder drei Funktionen wie oben schreiben… natürlich meine ich Templates!


3. Problem erkannt und gebannt

Zur Erinnerung: Deklarieren wir ein template, setzt der Compiler beim Aufruf die passenden template-Parameter ein und kompiliert damit. Es sei denn, es existiert eine Spezialisierung, die auf die Parameter passt. Beispiel:

Code: Alles auswählen

// Ein generisches template
template <typename t_Datentyp> void MachWas(t_Datentyp & p_Parameter) {
    std::cout<<"Macht was mit irgendeinem Typ..."<<endl;
}
// Eine Spezialisierung für Doubles
template <> void MachWas(double & p_Parameter) {
    std::cout<<"Macht was mit double!"<<endl;
}

// In der main():
MachWas(1);	  // Ausgabe: Macht was mit irgendeinem Typ...
MachWas('x');	// Ausgabe: Macht was mit irgendeinem Typ...
MachWas(5.0);	// Ausgabe: Macht was mit double!
Indem wir den Funktionsrumpf des generischen MachWas() weglassen, können wir sogar verhindern dass MachWas() mit irgendeinem Typ außer double aufgerufen wird. Das ist für uns perfekt: So können wir je nachdem, was der User für einen Shader kompilieren möchte etwas anderes machen und dabei noch sicher stellen, dass er nichts anderes als einen Shader übergibt!

Zum Aufwärmen abstrahieren wir erst einmal CreateVertex-, Geometry- und -PixelShader(). Dazu eine Anmerkung: Von nun an übergeben wir den Shader der Funktion nicht mehr als Zeiger auf einen Zeiger, sondern als Referenz auf einen Zeiger – so können wir den Zeiger immernoch verändern, sparen uns aber eine Dereferenzierung und verhindern, dass man NULL übergeben kann.

Code: Alles auswählen

// Deklarieren
template <typename t_ShaderTyp> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, t_ShaderTyp *& p_pShader);

// Für jeden Shadertyp spezialisieren
template <> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, ::ID3D10VertexShader *& p_pShader) {
	return p_Device.CreateVertexShader(p_Bytecode.GetBufferPointer(), p_Bytecode.GetBufferLength(), &p_pShader);
}
template <> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, ::ID3D10GeometryShader *& p_pShader) {
	return p_Device.CreateGeometryShader(p_Bytecode.GetBufferPointer(), p_Bytecode.GetBufferLength(), &p_pShader);
}
template <> HRESULT CreateShaderFromBytecode(::ID3D10Device & p_Device, ::ID3D10Blob & p_Bytecode, ::ID3D10PixelShader *& p_pShader) {
	return p_Device.CreatePixelShader(p_Bytecode.GetBufferPointer(), p_Bytecode.GetBufferLength(), &p_pShader);
}
Kurz zurück erinnern: Es gab zwei Stellen, an denen sich der Code von Shadertyp zu Shadertyp unterschied. Ja, die andere war der Profilname beim Kompilieren (vs_4_0, gs_4_0, …). Also machen wir uns auch dafür ein Template:

Code: Alles auswählen

// Deklarieren
template <typename t_ShaderTyp> const char * ProfilDesShaderTyps(void);
// Eine Spezialisierung für jeden Shadertyp:
template <> const char * ProfilDesShaderTyps<::ID3D10VertexShader>(void) 	{ return "vs_4_0"; }
template <> const char * ProfilDesShaderTyps<::ID3D10GeometryShader>(void) 	{ return "gs_4_0"; }
template <> const char * ProfilDesShaderTyps<::ID3D10PixelShader>(void) 	{ return "ps_4_0"; }
Im Gegensatz zur vorherigen Funktion hat diese Funktion hier keinen Parameter, wir müssen den Typen also selbst mit angeben. Das ist aber kein Problem, wie wir sehen, wenn wir alles zusammensetzen:

Code: Alles auswählen

template <typename t_ShaderTyp> void LoadShader(::ID3D10Device & p_Device, const char p_Dateiname[], const char p_Funktionsname[], t_ShaderTyp *& p_pShader) {
	// Shader aus Datei laden und kompilieren
	::ID3DBlob * l_Code;
	::ID3DBlob * l_Fehlermeldungen;
	D3D10CompileShader(p_Dateiname, …, p_Funktionsname, ProfilDesShaderTyps<t_ShaderTyp>(), … l_Code, l_Fehlermeldungen);

	CreateShaderFromBytecode(p_Device, l_Code, p_pShader);
}
Wie wir sehen, ist die Funktion LoadShader() nun selbst ein Template, weil die Shader in erster Linie ihr übergeben werden und entsprechend auch dort die Entscheidung für den richtigen Typen vorgenommen werden muss. Da innerhalb der Funktion der korrekte Shadertyp im template-Parameter t_ShaderTyp gespeichert ist, können wir ihn ProfilDesShaderTyps() direkt übergeben.

Angewendet sieht das ganze nun so aus:

Code: Alles auswählen

::ID3D10VertexShader * MeinVertexShader = NULL;
::ID3D10PixelShader * MeinPixelShader = NULL;
LoadShader(MeinDevice, "Blubb.hlsl", "VSMain", MeinVertexShader);
LoadShader(MeinDevice, "Blubb.hlsl", "PSMain", MeinPixelShader);

4. Der komplette Code

Jetzt wo ihr euch all das durchgelesen habt, solltet ihr eure eigenen Shader-Funktionen vielleicht überarbeiten. Seid kreativ, z.B. kann man auf dieselbe Weise auch VSSetShader(), GSSetShader() und PSSetShader() abstrahieren oder gleich alle Shader samt Input-Layout in Klassen kapseln.

Weil auch immer wieder vergessen wird, die temporären Puffer zu löschen und das mit den Fehlermeldungen so eine Sache ist, spendiere ich euch hier meinen eigenen Quellcode zum Laden von Shadern. Ich habe ihn um den Parameter Feature-Level erweitert – momentan kann man seine Shader damit, je nachdem was die GPU unterstützt, für Shader Model 4.0 oder 4.1 kompilieren, in D3D11 wird dieser Parameter noch an Einfluss gewinnen. Denkt daran, dass mein Code in einer Klasse CGPU steckt (weil globale Funktionen, außer zu Anschauungszwecken, böse sind ;) )

Code: Alles auswählen


class CGPU {
private:

	// Diese Funktionen werden ausschließlich spezialisiert, darum stehen in der Klassendeklaration nur ihre Deklarationen.
	template <typename t_ShaderType> const char * const ShaderProfile(const ::D3D10_FEATURE_LEVEL1 p_FeatureLevel);
	template <typename t_ShaderType> void CreateShader(t_ShaderType *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength);

public:

	// Diese Funktion wird nicht spezialisiert sondern ist ein generisches Template. Deshalb muss sie komplett in der Klassendeklaration definiert
	//	werden (extern templates werden nicht von jedem Compiler unterstützt!).
	template <typename t_ShaderType> void LoadShader(
		t_ShaderType *&	p_pShader,
		const char		p_sFilename[],
		const char		p_sEntryPointName[]
	) {
		// Speichert den Bytecode (falls die Kompilierung erfolgreich war) sowie die Fehlermeldungen (falls die Kompilierung fehl schlug).
		::ID3DBlob * l_pBytecode = NULL;
		::ID3DBlob * l_pErrors	 = NULL;

		// Kompilieren des Shaders mit dem D3D-Shader-Compiler.
		::D3D10CompileShader(
			NULL, 0,
			p_sFilename,
			NULL, NULL,
			p_sEntryPointName,
			ShaderProfile<t_ShaderType>(),
			0,
			l_pBytecode, l_pErrors);

		// Sind Fehler aufgetreten?
		if(NULL != l_pErrors) {
			// Fehlermeldung ausgeben
			::MessageBoxA(NULL, l_pErrors->GetBufferPointer(), "Fehler beim Kompilieren eines Shaders", MB_OK|MB_ICONERROR|MB_SETFOREGROUND);
			// Fehlermeldungen wieder freigeben! Wird oft vergessen…
			l_pErrors->Release();
		}
		else {
			// Shader aus dem Bytecode erzeugen.
			this->CreateShader(p_pShader, l_pBytecode->GetBufferPointer(), l_pBytecode->GetBufferSize());
			// Bytecode wieder freigeben. Wird auch oft vergessen…
			l_pBytecode->Release();
		}
	}

} // class CGPU


template <> const char * const CGPU::ShaderProfile<::ID3D10VertexShader>(void) {
	switch(this->FeatureLevel()) {
		case ::D3D10_FEATURE_LEVEL_10_0:
			return "vs_4_0";
		case ::D3D10_FEATURE_LEVEL_10_1:
			return "vs_4_1";
	}
}
template <> const char * const CGPU::ShaderProfile<::ID3D10GeometryShader>(void) {
	switch(this->FeatureLevel()) {
		case ::D3D10_FEATURE_LEVEL_10_0:
			return "gs_4_0";
		case ::D3D10_FEATURE_LEVEL_10_1:
			return "gs_4_1";
	}
}
template <> const char * const CGPU::ShaderProfile<::ID3D10PixelShader>(void) {
	switch(this->FeatureLevel()) {
		case ::D3D10_FEATURE_LEVEL_10_0:
			return "ps_4_0";
		case ::D3D10_FEATURE_LEVEL_10_1:
			return "ps_4_1";
	}
}


template <> void CGPU::CreateShader(::ID3D10VertexShader *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength) {
	this->D3DDevice().CreateVertexShader(p_pBytecode, p_iBytecodeLength, &p_pShader);
}
template <> void CGPU::CreateShader(::ID3D10GeometryShader *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength) {
	this->D3DDevice().CreateGeometryShader(p_pBytecode, p_iBytecodeLength, &p_pShader);
}
template <> void CGPU::CreateShader(::ID3D10PixelShader *& p_pShader, const void * const p_pBytecode, const size_t p_iBytecodeLength) {
	this->D3DDevice().CreatePixelShader(p_pBytecode, p_iBytecodeLength, &p_pShader);
}

Fragen, Kritik, Lob, Verbesserungsvorschläge usw. könnt ihr wie immer direkt hier los werden :)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Antworten