2015-12-23 10 views
8

Sto scrivendo un motore di particelle e ho notato che è molto più lento di quanto dovrebbe essere (ho scritto motori 3D C++ altamente non ottimizzati che possono rendere 50k particelle a 60 fps, questo scende a 32 fps intorno a 1,2 k ..), ho fatto alcune analisi sul codice ipotizzando il rendering delle particelle o le rotazioni erano l'operazione più intensiva della CPU, tuttavia ho scoperto che in realtà queste due piccole proprietà dell'oggetto grafico stanno effettivamente consumando più del 70% di la mia prestazione ....Graphics.Transform è estremamente inefficiente, cosa posso fare a riguardo?

public void RotateParticle(Graphics g, RectangleF r, 
           RectangleF rShadow, float angle, 
           Pen particleColor, Pen particleShadow) 
    { 
     //Create a matrix 
     Matrix m = new Matrix(); 
     PointF shadowPoint = new PointF(rShadow.Left + (rShadow.Width/1), 
             rShadow.Top + (rShadow.Height/1)); 
     PointF particlePoint = new PointF(r.Left + (r.Width/1), 
              r.Top + (r.Height/2)); 
     //Angle of the shadow gets set to the angle of the particle, 
     //that way we can rotate them at the same rate 
     float shadowAngle = angle;     
     m.RotateAt(shadowAngle, shadowPoint); 

     g.Transform = m; 

     //rotate and draw the shadow of the Particle 
     g.DrawRectangle(particleShadow, rShadow.X, rShadow.Y, rShadow.Width, rShadow.Height); 

     //Reset the matrix for the next draw and dispose of the first matrix 
     //NOTE: Using one matrix for both the shadow and the partice causes one 
     //to rotate at half the speed of the other. 
     g.ResetTransform(); 
     m.Dispose(); 

     //Same stuff as before but for the actual particle 
     Matrix m2 = new Matrix(); 
     m2.RotateAt(angle, particlePoint); 

     //Set the current draw location to the rotated matrix point 
     //and draw the Particle 
     g.Transform = m2; 

     g.DrawRectangle(particleColor, r.X, r.Y, r.Width, r.Height); 
     m2.Dispose(); 
    } 

Cosa sta uccidendo il mio rendimento è specificamente queste righe:

g.Transform = m; 
g.Transform = m2; 

Un po 'di background, l'oggetto grafico viene catturato da painteventargs, quindi esegue il rendering di particelle sullo schermo in un metodo di rendering delle particelle, che chiama questo metodo per eseguire qualsiasi rotazione, il multi-threading non è una soluzione come l'oggetto grafico non può essere condiviso tra più thread. Ecco un link per l'analisi del codice mi sono imbattuto solo così si può vedere cosa sta succedendo così:

https://gyazo.com/229cfad93b5b0e95891eccfbfd056020

sto po 'pensando che questo è qualcosa che non può davvero essere aiutato perché sembra che la proprietà stesso sta distruggendo le prestazioni e non tutto quello che ho effettivamente fatto (anche se sono sicuro che ci sono margini di miglioramento), specialmente dal momento che la DLL che la classe chiama utilizza la maggior parte della potenza della CPU. Ad ogni modo, qualsiasi aiuto sarebbe molto apprezzato nel tentativo di ottimizzare questo ... forse mi limiterò a abilitare/disabilitare la rotazione per aumentare le prestazioni, vedremo ...

+0

Anche io mi scuso per l'immagine, so che è veramente piccola, se si strizza l'occhio si possono vedere i numeri però! : P –

risposta

3

Bene, dovresti grattarti la testa per un po 'sui risultati del profilo che vedi. C'è qualcosa altro in corso quando si assegna la proprietà Transform. Qualcosa che puoi ragionare notando che ResetTransform() non costa nulla. Non ha senso, ovviamente, quel metodo cambia anche la proprietà Transform.

E si noti che dovrebbe essere DrawRectangle() che dovrebbe essere il metodo costoso poiché quello è quello che effettivamente mette il pedale al metallo e genera comandi di disegno reali. Non possiamo vedere quanto costa dal tuo screenshot, non può essere superiore al 30%. Non è abbastanza.

Penso che quello che vedete qui sia una caratteristica oscura di GDI/plus, it batch di comandi di disegno. In altre parole, internamente genera un elenco di comandi di disegno e non li passa al driver video fino a quando non è necessario. Il winapi nativo ha una funzione che forza esplicitamente quella lista a essere svuotata, è GdiFlush(). Ciò non è tuttavia esposto dalla classe .NET Graphics, è fatto automaticamente.

Quindi una teoria piuttosto interessante è che GDI + chiama internamente GdiFlush() quando si assegna la proprietà Transform. Quindi il costo visualizzato è in realtà il costo di una chiamata DrawRectangle() precedente.

È necessario andare avanti dandogli più opportunità di batch. Apprezzo molto il metodo della classe Graphics che ti consente di disegnare un numero elevato di elementi. In altre parole, non disegnare ogni singola particella ma disegnarne molte. Ti piacerà DrawRectangles(), DrawLines(), DrawPath(). Sfortunatamente nessun DrawPolygons(), quello che ti piace davvero, tecnicamente potresti farti polipropilare() ma è difficile andare avanti.

Se la mia teoria non è corretta, si noti che non è necessario Graphics.Transform. Puoi anche usare Matrix.TransformPoints() e Graphics.DrawPolygon(). Se si può veramente andare avanti è un po 'incerto, la classe Graphics non usa direttamente l'accelerazione GPU, quindi non è mai in grado di competere così bene con DirectX.

+0

Risposta solida, ha senso, e spiegherebbe molto sul motivo per cui ogni chiamata individuale è così intensa. Approfitterò di questo quando torno a casa dal lavoro e ti farò sapere come va, grazie mille! –

3

Non sono sicuro se quanto segue potrebbe aiutare , ma vale la pena provare. Invece di allocazione/assegnazione/smaltimento nuova Matrix, utilizzare i preassegnate Graphics.Transform via Graphics metodi - RotateTransform, ScaleTransform, TranslateTransform (e assicurarsi di sempre ResetTransform quando fatto).

Il Graphics non contiene un equivalente diretto di Matrix.RotateAt metodo, ma non è difficile fare una

public static class GraphicsExtensions 
{ 
    public static void RotateTransformAt(this Graphics g, float angle, PointF point) 
    { 
     g.TranslateTransform(point.X, point.Y); 
     g.RotateTransform(angle); 
     g.TranslateTransform(-point.X, -point.Y); 
    } 
} 

Quindi è possibile aggiornare il codice come questo e vedere se questo aiuta

public void RotateParticle(Graphics g, RectangleF r, 
           RectangleF rShadow, float angle, 
           Pen particleColor, Pen particleShadow) 
{ 
    PointF shadowPoint = new PointF(rShadow.Left + (rShadow.Width/1), 
            rShadow.Top + (rShadow.Height/1)); 
    PointF particlePoint = new PointF(r.Left + (r.Width/1), 
             r.Top + (r.Height/2)); 
    //Angle of the shadow gets set to the angle of the particle, 
    //that way we can rotate them at the same rate 
    float shadowAngle = angle; 

    //rotate and draw the shadow of the Particle 
    g.RotateTransformAt(shadowAngle, shadowPoint); 
    g.DrawRectangle(particleShadow, rShadow.X, rShadow.Y, rShadow.Width, rShadow.Height); 
    g.ResetTransform(); 

    //Same stuff as before but for the actual particle 
    g.RotateTransformAt(angle, particlePoint); 
    g.DrawRectangle(particleColor, r.X, r.Y, r.Width, r.Height); 
    g.ResetTransform(); 
} 
+0

Questo in realtà ha aiutato un po 'l'FPS, non aiuta molto quando le particelle sono grandi, ma quando sono più piccole c'è un po' di guadagno in termini di prestazioni, sfortunatamente continua a perdere 64 fps a circa 1700 particelle. Anche la rotazione è molto diversa con questo metodo, ma in senso positivo, sembra interessante :) –

1

È possibile creare un buffer fuori campo per disegnare le particelle e avere OnPaint semplicemente il rendering del buffer fuori campo? Se hai bisogno di aggiornare periodicamente il vostro schermo, si può invalidare la OnScreen di controllo/tela, dire utilizzando un Timer

Bitmap bmp; 
Graphics gOff; 

void Initialize() { 
    bmp = new Bitmap(width, height); 
    gOff = bmp.FromImage(); 
} 

private void OnPaint(object sender, System.Windows.Forms.PaintEventArgs e) { 
    e.Graphics.DrawImage(bmp, 0, 0); 
} 

void RenderParticles() { 
    foreach (var particle in Particles) 
     RotateParticle(gOff, ...); 
} 


In un'altra nota, alcun motivo per creare un oggetto matrice ogni volta che si chiama RotateParticle ? Non l'ho provato, ma i documenti MSDN sembrano suggerire che ottenere e impostare su Graphics.Transform creerà sempre una copia. In questo modo è possibile mantenere un oggetto Matrix a livello di classe e utilizzarlo per la trasformazione. Assicurati di chiamare lo Matrix.Reset() prima di usarlo. Questo potrebbe farti migliorare le prestazioni.