Grafiksystem virtualisieren/wrappen



  • Hallo Leute!
    Ich habe mir eben Gedanken darüber gemacht, wie ich mein Grafiksystem dermaßen gestalten könnte, dass es eine Quellcodestabile Schnittstelle bietet und entweder zur Laufzeit mehrere verschiedene Grafikschnittstellen verwendet (Fall 1) oder genau eine (Fall 2). Im speziellen habe ich mir Gedanken darüber gemacht, wie ich Ressourcen darstellen könnte. Der benutzende Code soll sie wie echte Objekte behandeln können, also keine Zeiger, daher brauche ich sozusagen eine Handle-Klasse pro Ressourcentyp. Die Teile sollen der Einfachheit halber einfach Move-Semantik besitzen, mir fällt auch im Moment kein Beispiel ein, wo mir das nicht reichen würde. Ich nehme jetzt einfach mal einen SolidBrush als Beispiel her.

    Hier einmal das momentane Ergebnis, zu dem ich gekommen bin:

    class ISolidBrush {
        // ...
    };
    
    class BrushMulti {
        // ...
    };
    
    #ifdef SUPPORT_MULTI
    class SolidBrushMulti :
        public BrushMulti,
        public ISolidBrush  // Die Ableitung ist unnötig, soll aber dafür sorgen
                            // dass das Interface quellcodestabil implementiert wird,
                            // nur als Sicherheitsmaßnahme
    {
    public:
        SolidBrushMulti( DeviceType device_type, unique_ptr<ISolidBrush> specific_impl );
    
    public:
        //  ... ISolidBrush forwarding ...
    
    private:
       DeviceType device_type_;
       unique_ptr< ISolidBrush > specific_impl_; // Der würde wahrscheinlich eher
                                                 // im BrushMulti
                                                 // gespeichert werden und hier 
                                                 // hochgecastet werden                       
    };
    
    typedef SolidBrushMulti SolidBrush;
    #endif
    
    template< typename Type >
    struct derive_if_multi_support {
    #ifdef MULTI_SUPPORT
        typedef Type type;
    #else
        typedef struct {} type;
    #endif
    };
    
    #ifdef SUPPORT_DIRECT2D
    class SolidBrushDirect2d_data { /* ... */ };
    
    class SolidBrushDirect2d :
        public derive_if_multi_support<ISolidBrush>::type
        // nur ableiten, wenn das Interface für SolidBrushMulti unterstützt werden
        // muss
    {
    public:
        SolidBrushDirect2d( shared_ptr< SolidBrushDirect2d_data > data_ptr );
    
    public:
        // ISolidBrush-Implementierung
    
    private:
        shared_ptr< SolidBrushDirect2d_data > data_ptr_; // Geteilte Ressource... nicht wichtig
                                                         // für das aktuelle Problem.
    };
    
    #ifndef SUPPORT_MULTI
        typedef SolidBrushDirect2d SolidBrush;
    #endif // ndef SUPPORT_MULTI
    #endif // def SUPPORT_DIRECT2D
    

    Soweit so gut, jetzt kann Client-Code SolidBrushes als konkrete Objekte benutzen, egal ob multiple devices unterstützt werden, oder nur ein einziges.
    Jetzt ist aber noch das Problem zu handlen, wie diese SolidBrushes von dem entsprechenden Device erzeugt werden.
    Im Moment läuft es auf folgendes hinaus:

    class RenderTargetDirect2d :
        public RenderTarget
    {
        // ...
    
        SolidBrush MakeSolidBrush( vis::Color color )
        {
    #ifdef SUPPORT_MULTI
            return SolidBrush( make_unique<SolidBrushDirect2d>( params ) );
    #else
            return SolidBrush( params ); // oder SolidBrushDirect2d, um klarer zu sein
    #endif // def SUPPORT_MULTI
        }
        // ...
    
        void FillRectangle( vis::RectI rect, Brush& brush )
        {
        }
    
    };
    

    Das ist natürlich äußerst ätzend, weil dann überall Präprozessor-Unterscheidungen gemacht werden müssen (immerhin aber nicht im Client-Code)

    Ich weiß mir da nur noch mit einem weiteren template zu helfen:

    #ifdef SUPPORT_MULTI
    template< typename MultiType, typename ConcreteType, typename... Params >
    MultiType MakeHandle( Params&& params... )
    {
       return MultiType( make_unique<ConcreteType>( std::forward<Params>(params)... ) ); 
    }
    #else
    template< typename MultiType, typename ConcreteType, typename... Params >
    ConcreteType MakeHandle( Params params )
    {
        return ConcreteType( std::forward<Params>(params)... );
    }
    #endif
    

    Man möge mir verzeihen, dass die variadics da fehlerhaft sind, ich tippe hier nur pseudocode und habe keine Variadic-Unterstützung in VC++2012...
    Das heißt für mich also, alleine für dieses Problem muss ich überhaupt Variadics auspacken und in meinem Fall sogar noch ätzend aufwendig simulieren.

    Also zusammengefasst: Mit meinem derzeitigen Ansatz für Ressourcen unter Anbetracht der formulierten Voraussetzungen, habe ich einen unübersichtlichen Wust an Klassen und muss sogar noch Variadics auspacken.
    Nun bin ich wohl nicht der einzige, der jemals etwas "gleichförmig wrappen" wollte, vielleicht fällt euch auf Anhieb eine bessere Aufteilung/Umsetzung auf?

    Viele Grüße,
    Deci



  • Nicht wirklich auf dein Problem bezogen, aber hoffentlich dennoch hilfreich: Vergiss das mit dem "genau eine" einfach und mach alles virtual. Die Performancelöcher einer (Grafik)-Engine hängen an anderen Orten. Du kommunizierst hier an irgendeiner Stelle mit einem anderen device (der Grafikkarte), dagegen ist jeder virtual call ein Klacks und du wirst die Interfaces eh so designen müssen, dass du mehrere Dinge mit einem Call erledigen kannst. Das was du hier versuchst, ist eine Optimierung die dir letztlich nichts bringen wird. Und das sagt dir ein C89- und Mikrooptimierungs-Liebhaber der sich dauernd den ASM Output von C++ Compilern ansieht. 😉



  • Hrmmmm, jetzt wo Du es so sagst, könnte mir das natürlich viel Ärger ersparen, ohne mich signifikant etwas zu kosten, außer einer zusätzlichen Indirektionsschicht vor etwas wesentlich teurerem. Und ja, die Graphik-Calls sind natürlich immer viel teurer als ein virtual call, weshalb man dann halt immer so Batch-Funktionen hat ala DrawLines, glDrawArrays, DrawShape usw... Hrmmm hrmmm. Okay, ist gebongt 😃 Danke Dir, hast mich vollauf überzeugt (Aber lass Dir das nicht zu Kopfe steigen, wegen meiner akuten Faulheit hat hier nicht mehr viel gefehlt :D).
    Dennoch: Falls jemand für das von mir vorgestellte noch eine bessere Lösung parat hat, würde sie mich einfach interessieren, falls ich soetwas mal in realistischen Fällen umsetzen muss! 😃



  • glDrawArrays ist doch schon wieder uncool, guck dir erstmal glDrawArraysInstanced etc. an. :p



  • Na was OpenGL oder ähnliches angeht, bin ich leider überhaupt nicht mehr auf dem laufenden, einfach keine Zeit mehr :...( Drück nur tief in die Wunde, ja!
    Edit: Aber wenn ich raten müsste: Man legt einen Satz von Matrizen/oder ähnlichen Per-Instanz-Daten von einem Array irgendwo an und kann dann statt mehrmals glDrawArrays aufzurufen einmal glDrawArraysInstanced aufrufen? 😃



  • Was auch immer SolidBrush bei dir ist. Warum 2 verschiedene Brushs implementieren, wenn du doch einfach nur die Api (OpenGl oder DirectX) durch deinen Renderer angepasst auf deine Zwecke abstrahierst. SolidBrush sollte wenn moeglich gar nicht wissen, was unterhalb des Renderers benutzt wird.



  • Hrmmm, ich kann nicht genau erfassen, was Du meinst.
    Also SolidBrush ist in dem Fall ein einfarbiger Brush (irgendwas zum Füllen von Geometrien), bei dem man noch die Farbe anpassen kann. Dem RenderTarget-Interface gibt man nur eine Referenz auf ein Brush, der konkrete Renderer hinter der Schnittstelle kann dann schauen, ob es seine eigene Ressource ist und sie dann in seinen speziellen Typen casten, um direkt oder indirekt (je nachdem ob es eine shared-Ressource ist oder nicht) Zugriff auf einen COM-Pointer (oder was auch immer) zur Ressource zum Zeichnen auf der eigentlichen API zu erhalten.
    Mache ich da irgendwas falsch? Klingt ziemlich einleuchtend für mich.

    Edit: Mir fiele noch ein, dass der Renderer gar keinen Zeiger mit konkreter Implementierung im Brush-Objekt versteckt, sondern nur eine ID, mit der er in einem internen Hash-Table nachschauen könnte. Aber im Prinzip ist das ja das gleiche nur etwas anders umgesetzt.


Anmelden zum Antworten