10

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 opposite 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?

Pedery
  • 3,632
  • 1
  • 27
  • 39
  • Word and Powerpoint don't use GDI+... I don't think GDI+ was ever praised for its speed (over plain GDI). – H H Jun 06 '09 at 12:07
  • Have you tried using a profiler to see where the slowdown actually occurs? I've done a heap of this kind of thing (ages ago, in Delphi) and never found Invalidate() to take a discernable amount of time. Painting, on the other hand, was "fun" to get running fast enough for rubber-banding a line. – Bevan Jun 08 '09 at 06:59
  • Well, I believe Delphi to be way faster for this kind of work in any case. Like Henk says, GDI+ is not known for speed at all, more likely lack thereof. However, the actual drawing code is not the bottleneck. The invalidation of the large area seems to be. I'm considering using my own backbuffer and manually blit the image to see if I can get a performance gain. Could you recomend any profiler in particular for VS2005? – Pedery Jun 08 '09 at 15:22

4 Answers4

9

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
  • 6,595
  • 1
  • 23
  • 49
  • 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 Jun 08 '09 at 03:10
  • 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 Jun 08 '09 at 03:13
  • 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 Jun 08 '09 at 13:48
  • Also, at this link (a snippet from the C#.NET 2003 developer's cookbook), they recommend setting ControlStyles.AllPaintingInWmPaint and ControlStyles.UserPaint in conjunction with ControlStyles.DoubleBuffer . The Former should do all the work of suppressing WM_ERASEBKGND for you without requiring you to do WndProc work. http://books.google.com/books?id=sygW0iEw2zkC&pg=PA291&lpg=PA291&dq=WM_ERASEBKGND+GDI%2B&source=bl&ots=A1ifDtDoGQ&sig=B6n7YgeiwXz5QMnqcDXbAPM0Nbo&hl=en&ei=gRUtSrSGNI6gMsT8uOUJ&sa=X&oi=book_result&ct=result&resnum=5#PPA291,M1 – meklarian Jun 08 '09 at 14:09
  • 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 Jun 08 '09 at 15:29
  • 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 Jun 08 '09 at 18:11
  • ... handle the work of tracking Invalidated areas, of course. – meklarian Jun 08 '09 at 18:12
  • 1
    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.zip Here 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 Jun 10 '09 at 23:17
  • 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 Jun 11 '09 at 17:54
  • 1
    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 Jun 11 '09 at 18:10
  • 1
    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 Jun 11 '09 at 18:18
  • 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 Jun 11 '09 at 18:54
  • 1
    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 Jun 11 '09 at 19:47
2

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.

Rich Schuler
  • 41,814
  • 6
  • 72
  • 59
  • 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 Jun 11 '09 at 17:44
1

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
  • 8,002
  • 1
  • 34
  • 48
  • 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 Jun 05 '09 at 19:21
1

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
  • 17,669
  • 6
  • 70
  • 85
  • 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 Jun 06 '09 at 02:05
  • Hm, if it's the "Invalidate()" that is slow, how about just calling OnPaint(new PaintEventArgs(CreateGraphics(), null || constRectangle)? – Patrick Jun 06 '09 at 23:00