ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4411131
Jetzt (Chat):
15 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-

ZFX
Sonstige Foren
Tutorials, FaQ und Links
Re: Tipp: Wie man ID3D10Device::Create…Shader() richtig wrappt
Normal
AutorThema
Krishty Offline
ZFX'ler


Registriert seit:
01.02.2004

Nordrhein-Westfalen
342173470
Tipp: Wie man ID3D10Device::Create…Shader() richtig wrapptNach oben.
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:
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:
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:
// Ein generisches template
template  void MachWas(t_Datentyp & p_Parameter) {
    std::cout<<\"Macht was mit irgendeinem Typ...\"<// Eine Spezialisierung für Doubles
template <> void MachWas(double & p_Parameter) {
    std::cout<<\"Macht was mit double!\"<// 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:
// Deklarieren
template  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:
// Deklarieren
template  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:
template  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(), … 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:
::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:
class CGPU {
private:

    // Diese Funktionen werden ausschließlich spezialisiert, darum stehen in der Klassendeklaration nur ihre Deklarationen.
    template  const char * const ShaderProfile(const ::D3D10_FEATURE_LEVEL1 p_FeatureLevel);
    template  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  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(),
            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

4 Mal gendert, zuletzt am 22.11.2008, 14:26:26 Uhr von Krishty.
22.11.2008, 12:23:21 Uhr
PuMi Online ist nicht jeder!
ZFX'ler


Registriert seit:
02.04.2006

Berlin
266670062
Re: Tipp: Wie man ID3D10Device::Create…Shader() richtig wrapptNach oben.
Lieber Krishty, [jetzt richtig geschrieben ]

das ist ein richtig gutes Sample wie man Shader mithilfe der Create...Shader() richtig wrappt.

Der Code wartet schon von mir mal in D3D11 getestet zu werden, denn dort existiert ja (derzeit) kein ID3D11Effect-Interface.
23.11.2008, 23:23:14 Uhr
Art Train Developer
Krishty Offline
ZFX'ler


Registriert seit:
01.02.2004

Nordrhein-Westfalen
342173470
Re: Tipp: Wie man ID3D10Device::Create…Shader() richtig wrapptNach oben.
Freut mich, dass du es gebrauchen kannst

Pass unter D3D11 auf: Bei mir ist D3DCompile() unfähig, aus Dateien zu lesen, ich muss sie selbst lesen und den Zeiger zu den Daten übergeben. Afaik ist das in den Samples auch so, wird also noch nicht richtig implementiert sein… aber ist ja noch ein Grund mehr, den Ladecode in einer einzigen Funktion zu vereinen

Weil man den Bytecode des Vertex-Shaders für Input-Layouts noch braucht, ist es vielleicht sinnvoll, eine spezielle Funktion zu überladen die Vertex-Shader und Input-Layout in einem Rutsch erzeugt. Sobald mir das elegant gelungen ist, trage ich es hier nach.

Gruß, Ky
24.11.2008, 00:07:52 Uhr
Normal


ZFX Community Software, Version 0.9.1
Copyright 2002-2003 by Steffen Engel