views:

1499

answers:

7

I'm developing a retained mode drawing application in GDI+. The application can draw simple shapes to a canvas and perform basic editing. The math that does this is optimized to the last byte and is not an issue. I'm drawing on a panel that is using the built-in Controlstyles.DoubleBuffer.

Now, my problem arises if I run my app maximized on a big monitor (HD in my case). If I try to draw a line from one corner of the (big) canvas to the diagonally oposite other, it will start to lag and the CPU goes high up.

Each graphical object in my app has a boundingbox. Thus, when I invalidate the boundingbox of a line that goes from one corner of the maximized app to the oposite diagonal one, that boundingbox is virtually as big as the canvas. When a user is drawing a line, this invalidation of the boundingbox thus happens on the mousemove event, and there is a clear lag visible. This lag also exists if the line is the only object on the canvas.

I've tried to optimize this in many ways. If I draw a shorter line, the CPU and the lag goes down. If I remove the Invalidate() and keep all other code, the app is quick. If I use a Region (that only spans the figure) to invalidate instead of the boundingbox, it is just as slow. If I split the boundingbox into a range of smaller boxes that lie back to back, thus reducing the invalidation area, no visible performance gain can be seen.

Thus I'm at a loss here. How can I speed up the invalidation?

On a side note, both Paint.Net and Mspaint suffers from the same shortcommings. Word and PowerPoint however, seem to be able to paint a line as described above with no lag and no CPU load at all. Thus it's possible to achieve the desired results, the question is how?

+1  A: 

To clarify: Is the user drawing a straight line, or is your line actually a bunch of line segments connecting mouse points? If the line is a straight line from the origin to the current mouse point, don't Invalidate() and instead use an XOR brush to draw an undoable line, and then undraw the previous line, only Invalidating when the user is done drawing.

If you're drawing a bunch of little line segments, just invalidate the bounding box of the most recent segment.

Aric TenEyck
Well, this is a CAD application and I used the "line" metaphor for simplicity. In reality I'm drawing a narrow rectangle at an angle, thus it consists of four GDI+ drawline operations plus an invalidation of the canvas. The lines can have any colour and intersect lots of other graphical objects on the stage, so I'm not sure XOR is the right way to go...
Pedery
+1  A: 

How about having a different thread that "post updates" to the real canvas.

Image paintImage;
private readonly object paintObject = new object();
public _ctor() { paintImage = new Image(); }

override OnPaint(PaintEventArgs pea) {
    if (needUpdate) {
        new Thread(updateImage).Start();
    }
    lock(paintObject) {
        pea.DrawImage(image, 0, 0, Width, Height);
    }
}

public void updateImage() {
    // don't draw to paintImage directly (it might cause threading issues)
    Image img = new Image();
    using (Graphics g = img.GetGraphics()) {
        foreach (PaintableObject po in renderObjects) {
            g.DrawObject(po);
        }
    }
    lock(paintObject){
        using (Graphics g = paintImage.GetGraphics()) {
            g.DrawImage(img, 0, 0, g.Width, g.Height);
        }
    }
    needUpdate = false;
}

Just an idea, so I haven't tested the code ;-)

Patrick
This could work, but I doubt it. First of all I would have to move away from the built-in GDI+ double buffering (which aparently has quite good performance), and secondly the bottleneck seems to be the invalidation itself. All other overhead is neglible. Since GDI+ is pure software graphics manipulation (no HW accelleration), it would simply mean only moving the problem from one thread to the next. I'm interested to know if anyone have any experience with optimizing rendering by using separate threads though. Another option could be to use a HW accellerated approach.
Pedery
Hm, if it's the "Invalidate()" that is slow, how about just calling OnPaint(new PaintEventArgs(CreateGraphics(), null || constRectangle)?
Patrick
+3  A: 

For basic display items like lines, you should consider breaking them up into a few parts if you absolutely must invalidate their entire bounds per drawing cycle.

The reason for this is that GDI+ (as well as GDI itself) invalidates areas in rectangular shapes, just as you specify with your bounding box. You can verify this for yourself by testing some horizontal and vertical lines versus lines where the slope is similar to the aspect of your display area.

So, let's say your canvas is 640x480. If you draw a line from 0,0 to 639,479; Invalidate() will invalidate the entire region from 0,0 to 639,0 at the top down to 0,479 to 639,479 at the bottom. A horizontal line from, say, 0,100 to 639,100 results in a rectangle only 1 pixel high.

Regions will have the very same problem because regions are treated as sets of horizontal extents grouped together. So for a large diagonal line going from one corner to the other, in order to match the bounding box you have setup- a region would have to specify either every set of pixels on each vertical line or the entire bounding box.

So as a solution, if you have a very large line, break it into quarters or eighths and performance should increase considerably. Revisting the example above, if you just divide in half for two parts- you will reduce the total invalidated area to 0,0 x 319,239 plus 320,240 x 639,479.

Here is a visual example of a quarter splits. The pink area is what is invalidated. Unfortunately SO won't let me post images or more than 1 link, but this should be enough to explain everything.

(Line Split in Quarters, Total Invalidated Area is 1/4 of the surface)

a 640x480 extent with 4 equal sized boxes carved behind a line drawn across the diagonal

Or, instead of specifying a bounding box, you may want to consider rewriting your updates so that you only draw the portions of items that match the region that must be updated. It really depends on how many objects need to participate in a drawn update. If you have thousands of objects in a given frame, you might consider just ignoring all the invalidated areas and just redraw the entire scene.

meklarian
Hi, and thanks for you answer. Unfortunately, if you had read my response to answer #2, I've already tried what you describe and it doesn't work. That IS surprising though. The scene will probably not contain more than a few dozen items, but that is really up to the user. There was virtually no gain in doing it this way. Updating only a quarter of the line (as a test) did improve (as expected), and there is almost a linear correlation between the CPU and the updated area in this case.
Pedery
I must underscore that this is only a problem on my large screen, 1980x1050, when I run the program maximized. The CPU load goes up to about 50%, redraws slows down considerably, and even mouse events are skipped from the message loop.
Pedery
Hmmm, I have a question, then.Are you handling the WM_ERASEBKGND message? If you aren't, try adding a handler for it in your app and suppress any drawing upon that windows message for the window hosting your GDI+ canvas. Since you are drawing the entire scene in the GDI+ canvas, this should save your app some effort.Also try using Spy++ to examine the ordering and frequency of WM_PAINT messages sent to that window as well, to see if there are duplicate or repeated redraw requests occurring.
meklarian
meklarian
I already set the proper controlstyles of course. I have not tried handling the WM_ERASEBKGND message, but I have tested overriding OnPaintBackground with no visible gain in performance. At this point I'm wondering if blitting will improve my performance over the built-in double buffering and invalidation. I'm also gonna try slowing invalidations down to a maximum of 25fps, instead of today where they happen upon every mouse movement. A quick test revealed that you can get as many as 80 mousemove events generated per second if you move the mouse around a lot.
Pedery
Ah I see. That's quite strange... are you getting a matching number (or greater) of OnPaint calls that match up to your mouse events? Actual drawing operations are more expensive than the calculations for Invalidate(), assuming you are letting GDI/GDI+ handle the work.
meklarian
... handle the work of tracking Invalidated areas, of course.
meklarian
Hi Again,I looked at your sample and I think your technique would probably benefit the most from decoupling your drawing code from your activity handlers. I've uploaded a sample that you can examine to the following location.http://www.amorph.com/stack-overflow/DrawTest1.zipHere are a few differences you may want to note:1. Derived from UserControl instead of Panel.2. No use of OptimizedDoubleBuffer.3. Initialize personal back buffer using Graphics object obtained from the control (to retain HDC color depth and settings).Btw I posted here because I don't have enough rep yet. :)
meklarian
Thanks! Your techinques definitely speed the process up, albeit just a little. Nonetheless I'm not sure it's possible to improve the redraws too much witin the limitations of Windows Forms. Maybe your example is the best one can do without resorting to other technologies such as DX.On a different note, I have a theory why invalidation of smaller areas and adding them together won't work. Since all calls to Invalidate just put that area in a queue, maybe the form will calculate the bounding box of the total area and blit that area alltogether. That might be faster than amny small blits.
Pedery
Invalidated areas are always in the shape of a rectangle. When you invalidate a number of areas, they are often merged together into the largest rectangle that encapsulates all the areas awaiting updates. But, to make matters worse, sometimes they are received in order. So if you are redrawing a significant area frequently- as in the samples; it is typically better to just combine lazy redraws with a fixed rate of updates as in the sample.
meklarian
Oh, and in regards to your other ideas, DirectDraw/DirectX works quite well in a windowed environment. Internet Explorer has used DirectDraw internally for quite a long time. But, it will be a lot of work to maintain consistency with DirectDraw. You will have to worry about people changing color depth on you, overlapping windows, multiple-monitor spanning, etc. You will also find that Hardware accelerators often vary greatly in handling these situations. If animation is a significant part of the user experience in your app, it could be worth the switch.
meklarian
Coo, great answer! I managed to lower the CPU load in your example by changing the timer interval to 40 instead of 10 as well. Since 40ms equals 25fps, there is no need to update more than this, methinks. Animation is not really a part of the application, but the user experience relies on the user actually seeing what is being drawn :)I'd like to set your answer as the accepted answer, but it would also be interesting to see if other people have more experiences around this. This is after all a revolving topic.
Pedery
No worries. Also try to get your hands on the book "Windows Graphics Programming: Win32 GDI and DirectDraw", by Feng Yuan. Although it was published years ago, it is the definitive guide on getting the best results out of graphics programming in windows through GDI. Here is the link to the book on Amazon. http://www.amazon.com/Windows-Graphics-Programming-Hewlett-Packard-Professional/dp/0130869856
meklarian
Awesome, I'll check it out!
Pedery
A: 

If you have used regions properly, as you described then it should work fast enough. It means that your problem is caused by some circumstances or bugs that you did not describe. Please do the following:

Create the smallest code that shows the problem.

This should always be the second step during debugging, if you are not able to find the answer in several minutes. If you are not able to reproduce your problem with smaller implementation it simply means that it is caused by some of your bugs that have not been described here. Otherwise we can help.

agsamek
A: 

I've tried many different approaches now and have not yet reached a conclusion. To try to isolate the problem I made a small test application and it can be downloaded here. This "Benchmarker" application will draw a single line from the mousedown location to the mouseup location (don't pay attention to the fact that it doesn't clean up the canvas properly, it doesn't matter), and if the line is drawn from corner to corner diagonally across a big screen and the mouse is moved around a lot, there is a small visible lag. The CPU also goes up to about 50%. The BitBlt version doesn't blit properly, but that doesn't matter either. In this case I invalidate the (whole) canvas using a 40ms timer and this seems to be even slower than using the native .Net methods.

So, at this point I'm proposing that invalidating a full HD canvas flawlessly can't be done with Windows Forms. I'm interested in any counteropinions.

Have any of you guys out there had any experience with i.e. DirectDraw? Could an alternative solution involve to manually update a drawing surface using DirectDraw or DirectX? Would this be quicker, even if that surface was still inside a Windows Form?

Thanks for all answers so far. I believe this is a topic that has affected many of us from time to time.

  • Peder -
Pedery
A: 

You can't really speed up Invalidate. The reason why it is slow is because it posts a WM_PAINT event onto the message queue. That then gets filtered down and eventually your OnPaint even is called. What you need to do is paint directly in your control during the MouseMove event.

In any control I do that requires some measure of fluid animation my OnPaint event generally only calls a PaintMe function. That way I can use that function to redraw the control at anytime.

Qberticus
Sure, and I can see what you're saying. You mean that I should Update() my control instead of invalidating, based on some fps scheme. This will probably strain the CPU even further though. Remember this is only an issue when I invalidate large portions of a HD screen. I was really looking for answers such as if this process could be successfully sped up by manual blitting or i.e. if painting on a GDI+ bmp and using DX to paint that image to a DX surface would make it quicker.
Pedery
A: 

Since this is a fairly popular thread, the following link is a pretty interesting discussion about how to speed up GDI+ in general:

http://www.gamedev.net/community/forums/topic.asp?topic_id=467752

Pedery