Tuesday 8 June 2021

WinForms Fast Graphics

Most graphic operations using the Graphics class in a WinForms application are fairly quick. Not the fastest by any means. Still, a WinForms app has to jump through many hoops to draw to the screen. Examples likely include, the C# runtime, the graphics driver, the operating system (shared graphics) and so on.

One area where graphics in WinForms falls short is direct pixel manipulation. It is incredibly slow! In this post I will show basic pixel manipulation and a faster, more-efficent approach. Both approaches will use the PictureBox control. The picture box control display an image. The image may be loaded from a file or from memory.

For the examples I will be using Visual Studio 2019, community edition (FREE).

Top

Bitmap graphics

Bitmap or raster graphics is a well-known concept. The basic idea is to use a portion of memory to represent the video display. The display is broken down into a number of bits. For example 1024 X 768 would equate to 1024 pixels for each line (left to right) and 768 total lines (top to bottom). The four bytes can be broken down into four channels as follows...

  • Alpha channel

    The Alpha channel determines the opacity. Zero indicates no opacity (essentially invisible). 255 indicates full opacity (original data is overwritten). A value of 128 is approx midway between invisible and full opacity. The alpha channel essentially allows simple blending of image data.

  • The red channel

    The red channel specifies the strength of the red component. Zero indicates no red component, 255 equates to maximum red.

  • The green channel

    The green channel specifies the strength of the green component. Zero indicates no green component, 255 equates to maximum green.

  • The blue channel

    The blue channel specifies the strength of the blue component. Zero indicates no blue component, 255 equates to maximum blue.

The ammount of memory required for the display can be calculated by take the display dimensions and including the bpp. If we consider 32 bits per pixel (32 bpp), 4 bytes are required to display a single pixel. So, for a display size of 1024 * 768, the required memory is..

  • 1024 * 768 - (786,432 pixels) pixel count.
  • 786,432 * 4 bytes - each pixel requires 4 bytes to specify alpha and colour channels.
  • 3,145,728 bytes required.
  • This equates to approx 3 megabytes to display an image, of dimensions 1024 by 768 pixels using 32-bit colour with an alpha channel.
Top

Improving bitmap graphics performance

The bitmap class contains methods to get pixel data and to set pixel data. Both methods utilise the Color structure. The first performance issue is that these are both functions. Function calls have an overhead, creating the call stack, calling the function and restoring the call stack. Setting pixels for say, 1024 by 768 display will result in 1024 * 768 function calls, 786,432 in total.

To improve performance we need direct access to the array that stores bitmap information. That is, we need to manipulate pixels without having to use functions. The best way to achieve this in WinForms is to create new control that derives from the PictureBox control.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace FastGraphicsApp.Views
{
  public class FastBitmap : PictureBox
  {
    private GCHandle _handle;
    private IntPtr _addr;
    private UInt32[] _pixels;    

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);
      Create();
    }

    private void Create()
    {
      Cleanup();

      int width = ClientSize.Width;
      int height = ClientSize.Height;

      int bitsPerPixel = ((int)PixelFormat.Format32bppArgb & 0xff00) >> 8;
      int bytesPerPixel = (bitsPerPixel + 7) / 8;
      int stride = 4 * ((width * bytesPerPixel + 3) / 4);

      _pixels = new UInt32[width * height];
      _handle = GCHandle.Alloc(_pixels, GCHandleType.Pinned);
      _addr = Marshal.UnsafeAddrOfPinnedArrayElement(_pixels, 0);
      Image = new Bitmap(width, height, stride, PixelFormat.Format32bppArgb, _addr);
    }

    private void Cleanup()
    {
      if (null != Image)
      {
        Image.Dispose();
        Image = null;
      }

      if (_handle.IsAllocated)
        _handle.Free();
    }
  }
}

The above code will allow direct pixel manipulation. This is possible via the _pixels array member. However, any modifications to the _pixels array must be followed by an update call to the control. This ensures that modified pixel data is displayed correctly.

Top

No comments:

Post a Comment