Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Graphics.Transform is massively inefficient, what can I do about this?

I am writing a particle engine and have noticed it is massively slower than it should be (I've written highly un-optimized 3D C++ particle engines that can render 50k particles at 60 fps, this one drops to 32 fps at around 1.2k..), I did some analysis on the code assuming the rendering of the particles or the rotations were the most CPU intensive operation, however I discovered that in fact these two little properties of the graphics object are actually eating up over 70% of my performance....

    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();
    }

What is killing my performance is specifically these lines:

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

A little background, the graphics object is getting grabbed from painteventargs, it is then rendering particles to the screen in a render particles method, which calls this method to do any rotations, multi-threading isn't a solution as the graphics object cannot be shared between multiple threads. Here is a link to the code analysis I ran just so you can see what is happening as well:

https://gyazo.com/229cfad93b5b0e95891eccfbfd056020

I am kinda thinking this is something that can't really be helped because it looks like the property itself is destroying the performance and not anything I've actually done (though I'm sure there's room for improvement), especially since the dll the class calls into is using the most cpu power. Anyways, any help would be greatly appreciated in trying to optimize this...maybe I'll just enable/disable rotation to increase performance, we'll see...

like image 601
Trevor Hart Avatar asked Dec 23 '15 07:12

Trevor Hart


3 Answers

Well, you should scratch your head a while over the profile results you see. There is something else going on when you assign the Transform property. Something you can reason out by noting that ResetTransform() does not cost anything. Doesn't make sense of course, that method also changes the Transform property.

And do note that it should be DrawRectangle() that should be the expensive method since that is the one that actually puts the pedal to the metal and generates real drawing commands. We can't see what it cost from your screenshot, can't be more than 30%. That is not nearly enough.

I think what you see here is an obscure feature of GDI/plus, it batches drawing commands. In other words, internally it generates a list of drawing commands and does not pass them to the video driver until it has to. The native winapi has an function that explicitly forces that list to be flushed, it is GdiFlush(). That is however not exposed by the .NET Graphics class, it is done automagically.

So a pretty attractive theory is that GDI+ internally calls GdiFlush() when you assign the Transform property. So the cost you are seeing is actually the cost of a previous DrawRectangle() call.

You need to get ahead by giving it more opportunity to batch. Very strongly favor the Graphics class method that let you draw a large number of items. In other words, don't draw each individual particle but draw many. You'll like DrawRectangles(), DrawLines(), DrawPath(). Unfortunately no DrawPolygons(), the one you really like, technically you could pinvoke PolyPolygon() but that's hard to get going.

If my theory is incorrect then do note that you don't need Graphics.Transform. You can also use Matrix.TransformPoints() and Graphics.DrawPolygon(). Whether you can truly get ahead is a bit doubtful, the Graphics class doesn't use GPU acceleration directly so it never competes that well with DirectX.

like image 106
Hans Passant Avatar answered Nov 02 '22 02:11

Hans Passant


I'm not sure if the following would help, but it's worth trying. Instead of allocating/assigning/disposing new Matrix, use the preallocated Graphics.Transform via Graphics methods - RotateTransform, ScaleTransform, TranslateTransform (and make sure to always ResetTransform when done).

The Graphics does not contain a direct equivalent of Matrix.RotateAt method, but it's not hard to make one

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);
    }
}

Then you can update your code like this and see if that helps

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();
}
like image 33
Ivan Stoev Avatar answered Nov 02 '22 02:11

Ivan Stoev


Can you create an off-screen buffer to draw your particle, and have OnPaint simply render your off screen buffer? If you need to periodically update your screen, you can invalidate your OnScreen control/canvas, say using a 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, ...);
}


On another note, any reason to create a matrix object every time you call RotateParticle? I haven't tried it, but the MSDN docs seem to suggest that get and set on Graphics.Transform will always create a copy. So you can keep a Matrix object at say class level and use it for transform. Just make sure to call Matrix.Reset() before using it. This might get you some performance improvement.

like image 26
Vikhram Avatar answered Nov 02 '22 02:11

Vikhram