Friday, 13 June 2014

Manipulating WriteableBitmap's back buffer

Things I've found about WriteableBitmap's back buffer;
  • It's at a fixed location in memory for the lifetime of the object
  • Some of the methods take byte arrays, others will take struct arrays
I wanted to 'pan' the image.  I didn't want to use a Transform because I wanted the Image to remain at the same location, but the bitmap being displayed to appear to move about so that I could have the appearance of an infinite canvas to work with as I was working on a simple Mandelbrot viewer.

Pre-requisites

I know the bitmap is PixelFormats.Bgra32 because I created it in the first place which meant that I could represent each pixel in the buffer with the following struct:

  using System.Runtime.InteropServices;
 
  /// <summary>
  /// Represents a PixelFormats.Bgra32 format pixel
  /// </summary>
  [StructLayout(LayoutKind.Explicit)]
  public struct PixelColour
  {
    // 32 bit BGRA 
    [FieldOffset(0)]
    public readonly uint ColourBGRA;
 
    // 8 bit components
    [FieldOffset(0)]
    public byte Blue;
 
    [FieldOffset(1)]
    public byte Green;
 
    [FieldOffset(2)]
    public byte Red;
 
    [FieldOffset(3)]
    public byte Alpha;
  }

The back buffer has a 'stride'

This is the number of bytes that each row occupies.

Q: Why isn't this the same as the bitmap's pixel width?

A: For performance reasons the display layer wants the data being displayed in a form that's fastest to process. This means in our case that it wants as much as possible to be on well-known byte boundaries (e.g. 4-byte). If, for example, the bitmap used an 8-bit representation of each pixel the 'stride' may include some unused data at the end of each row to round the number of bytes for each row to be a multiple of 4, just so the maths for calculating pixel locations is significantly easier.

The location of a pixel in the back buffer of 'bmp' is therefore:

  bmp.BackBuffer + (bmp.BackBufferStride * Y) + X * (bmp.Format.BitsPerPixel / 8)

Copying pixels out of the back buffer

Here's a simple extension method to

/// <summary>
/// Take a copy of the pixel data from the indicated bitmap at the indicated pixel local.
/// </summary>
/// <param name="bmp">The bitmap to access</param>
/// <param name="sourceRect">A 0-based pixel aread to copy. If this is empty the entire back buffer is copied</param>
/// <param name="pixels">This will be updated with the pixels from the area requested.  NOTE: 1st dmension indicates the Y dimension, the 2nd dimension indicates the X dimension. e.g. var pixels = new PixelColour[maxY, maxX];</param>
public static void CopyPixels2(this WriteableBitmap bmp, Int32Rect sourceRect, PixelColour[,] pixels)
{
  Contract.Requires(bmp != null);
  Contract.Requires(pixels != null);
 
  // raise an exception if the bmp isn't the correct format
  CheckBmp(bmp);
 
  if (sourceRect.IsEmpty)
  {
    sourceRect = new Int32Rect(0, 0, bmp.PixelWidth, bmp.PixelHeight);
  }
  else
  {
    if (sourceRect.X < 0 
        || sourceRect.Y < 0 
        || (sourceRect.X + sourceRect.Width) > bmp.PixelWidth
        || (sourceRect.Y + sourceRect.Height) > bmp.PixelHeight)
    {
      throw new ArgumentOutOfRangeException("sourceRect",
        string.Format("Source rectangle is outside the bitmap pixel area (bitmap size: {0},{1}. SourceRect: {2}", bmp.PixelWidth, bmp.PixelHeight, sourceRect));
    }
  }
 
  {
    // we require the pixel dimensions to be of the form [x, y]
    var pixelsWidth = pixels.GetLength(1);
    var pixelsHeight = pixels.GetLength(0);
 
    if ((pixelsHeight < sourceRect.Height) || (pixelsWidth < sourceRect.Width))
    {
      throw new ArgumentOutOfRangeException(
        "pixels",
        string.Format(
          "The supplied pixel array is smaller than the source area being copied from (pixel buffer size: {0},{1}. Bitmap area to copy from: {2}",
          pixelsWidth,
          pixelsHeight,
          sourceRect));
    }
  }
 
  // we know from our previous checks that the bmp's bytes per pixel == sizeof(PixelColour)
  var bytesPerPixel = bmp.Format.BitsPerPixel / 8;
 
  // this is our starting point in the back puffer
  var pBackBuffer = bmp.BackBuffer + (bmp.BackBufferStride * sourceRect.Y) + sourceRect.X * bytesPerPixel;
 
  // prevent others writing to the back buffer whlile we take a copy
  bmp.Lock();
  try
  {
    unsafe
    {
      // simply loop through the requested source rectangle updating the resultant pixels
      for (var iy = 0; iy < sourceRect.Height; iy++)
      {
        var pPixels = (PixelColour*)pBackBuffer;
 
        // In production code I'd investigate using CopyMemory from kernel32.dll to perform the copy.
        // A 'for' loop seems clearer to the reader for now.
        for (var ix = 0; ix < sourceRect.Width; ix++)
        {
          pixels[iy, ix] = *(pPixels++);
        }
 
        // stride along to the next row
        pBackBuffer += bmp.BackBufferStride;
      }
    }
  }
  finally
  {
    bmp.Unlock();
  }
}




No comments: