It is a well-known fact that when drawing images to the screen that are a different pixel format than the screen's pixel format, format conversion must be performed. GDI+ provides the
CachedBitmap class to facilitate easy caching of a converted version of a bitmap. However, this functionality is not exposed in .NET, so normally bitmaps that do not match the screen format are converted on each drawing call.
At first glance, it would seem that it shouldn't be too performance-degrading, even for large images, to just draw the area of the bitmap that is exposed or invalidated using
Graphics.DrawImage(). However, a further performance examination reveals that the speed of
Graphics.DrawImage() is always related to the size of the whole bitmap, not the size of the area being drawn. If you tell GDI+ to draw even a 1-pixel-by-1-pixel section of a bitmap that is not in the same pixel format as the screen, it will convert the entire bitmap to the screen pixel format before completing the drawing call!
This is clearly unacceptable, especially since digital camera images are typically 24bpp. So, I set out to write a routine that would create a new bitmap that is the size of the area that needs to be drawn. It would then copy the correct section of the source bitmap into that area and draw that to the screen. This would give much better performance.
However, I also wanted to see if I could make GDI+ convert only the relevant area of the bitmap directly, rather than having the overhead of the extra copying routine. I realized that with my
EditableBitmap class, I could create an
EditableBitmap as a view on another
EditableBitmap's bits. Then I could just report to GDI+ that the "stride" is the size of the source bitmap's stride. So, there would be a distinct GDI+ bitmap information class that GDI+ could work with, but there would not be any more memory overhead related to the bits for an extra bitmap. There would also not be a performance reduction, however minor, associated with copying bits over to a copied bitmap section with each drawing call. I added a method called
CreateView(), which took a rectangle that specified the bounds of the view:
public EditableBitmap CreateView(Rectangle viewArea)
throw new ObjectDisposedException("this");
return new EditableBitmap(this, viewArea);
It delegates the "magic" to a protected constructor:
protected EditableBitmap(EditableBitmap source, Rectangle viewArea)
stride = source.stride;
bitmap = new Bitmap(viewArea.Width, viewArea.Height,
The constructor copies all of the properties from the source bitmap that will be the same. It stores a reference to the bitmap that it is a view on in the Owner property. It then calculates a byte offset from the start of the owner's byte array to the first pixel of the view bitmap. It then creates a GDI+ Bitmap object, passing in the offset byte pointer and the view's width and height. So far, so good! We have a bitmap object that tricks GDI+ into thinking that it is a standalone bitmap, when it really points to part of a larger bitmap.
However, we still have to worry about those pesky resource management issues. The
EditableBitmap class depends on a pinned byte array to store the pixel data. With bitmap views, that byte array is shared between multiple bitmaps. We want the views to still be operational when the root bitmap is disposed and definitely vice versa, as well. So, we have to make it so that the bit array is only destroyed when there are no more EditableBitmap instances that use it. To do this, I added a new class,
SharedPinnedByteArray, that manages the byte array and keeps a reference count. When the reference count reaches zero or it is finalized, it unpins the byte array.
The end result is a much faster rendering time for large, non-screen-format bitmaps. The demo included demonstrates the speed difference. When you run it, it will ask you to choose a bitmap using a file dialog. Choose a large bitmap. Try scrolling around, especially dragging the scrollbar thumbs. You will most likely see "tearing:" areas that paint slowly enough that you can easily see the missing area. Now click on the bitmap display area so that the title bar of the form says "New Method." Try scrolling again and it should be very smooth.
- 5 June, 2007 -- Article edited and posted to the main CodeProject.com article base
- 15 November, 2006 -- Original version posted