Export (0) Print
Expand All

Dancing Zombies: Adding Bitmaps to the Managed Graphics Library

.NET Compact Framework 1.0
 

Geoff Schwab
Excell Data Corporation

Art provided by Douglas R. Albright III
www.bittertrain.com

December 2003

Applies to:
   
Microsoft® .NET Compact Framework 1.0
   Microsoft Visual Studio® .NET 2003
   Microsoft eMbedded Visual C++® 3.0

Summary: This article expands upon the "Dancing Rectangles" sample by implementing loading and displaying of bitmaps. It also implements some more advanced features such as animated bitmaps, source and destination key transparency, and alpha blending, i.e., translucency. (53 printed pages)

Download Sample or GAPINetDLLs
Download Dancing Rectangles Sample
Download GAPI
Download eMbedded Visual Tools 2002 Edition


Contents

Introduction
Installing GAPINet
The GAPI Wrapper
The Windows Wrapper
A Basic Graphics Framework
The Pixel Class
The GXGraphics Class
The GXBitmap Class
The GXAnimation Class
Test Application
    ImageSpriteTest
    The Form
Conclusion

Developers only concerned with using the graphics library in an application can download and install the sample, as described in the "Installing GAPINet" section and then skip to the "Test Application" section.

Introduction

The graphics library in this sample, defined in the GXGraphicsLibrary namespace, expands upon the previous Dancing Rectangles sample with the following additions to its functionality:

  • Loading, instancing, displaying, and saving bitmaps
  • Source and destination key transparency
  • Flipping the X and/or Y axis when drawing bitmaps
  • Alpha blending
  • Combining of ImageBuffer, BackBuffer, and DrawSurface into the GXBitmap class
  • GetPixel and SetPixel functionality for bitmaps
  • GXAnimation class supports instancing and displaying animated bitmaps

While the graphics library portion of the sample is only available in C#—due to the need for unsafe code blocks in order to optimize pixel copies with pointer operations—the test application is available in Visual Basic as well as C#. This demonstrates the flexibility of code interoperability under the .NET Compact Framework. Screenshots from the bitmap test application are shown below.

Figure 1. Frame 20 Screenshot of an ImageSpriteTest Instance.

Figure 2. Frame 80 Screenshot of an ImageSpriteTest Instance.

Note:   The test application that produced these screenshots was tested and verified on Smartphone, as well as Pocket PC devices.

Subsequent articles and samples in this series will demonstrate how to draw basic primitives, draw text, implement GAPI input, and play sounds.

This sample is based on a previous sample titled "Dancing Rectangles: Using GAPI to Create a Managed Graphics Library" which demonstrates how to wrap GAPI to create a .NET Compact Framework compatible DLL. These DLL's are required to run this sample and can be downloaded from the sample link at the top of this document. For more information on how these DLL's were created, refer to the "Dancing Rectangles" article.

Installing GAPINet

This sample relies on GAPI and thus requires that GAPI be downloaded and unzipped to a location of your choice. Because some functions return types that are not supported by the .NET Compact Framework, GAPINet is required as well. This DLL provides a wrapper around the incompatible functions. For detailed information on how GAPINet was created, refer to the "Creating GAPINet" section of the "Dancing Rectangles" article.

To properly install these components, follow these steps:

  1. Download GAPI
  2. Download GAPINet and this sample
  3. On the device, copy GAPINet.DLL and GX.DLL (from 1 above) from the appropriate processor directory to the device's Windows directory
Note:   There is no officially supported build of GAPI for the emulators.

The GAPI Wrapper

The GXGraphicsLibrary namespace contains a class named GAPI which encapsulates all functionality necessary to access the graphics portions of the GX and GAPINet DLL's. This class is found in GAPI.cs in the sample and contains the P/Invoke implementations of the original API. This class has not been modified from the previous sample, therefore, for detailed information regarding this class, refer to the "The GAPI Wrapper" section of the "Dancing Rectangle" article.

The Windows Wrapper

The graphics library uses several functions from coredll as well as GAPI and GAPINet. The GetCapture function is used to access a HWND handle of the parent form and LocalAlloc and LocalFree are used to allocate and free heap memory. The class responsible for encapsulating this functionality is defined within the GXGraphicsLibrary namespace as the Windows class and can be found in Windows.cs in the sample. This class has not been modified from the previous sample, therefore, for detailed information regarding this class, refer to the "The Windows Wrapper" section of the "Dancing Rectangle" article.

A Basic Graphics Framework

For information regarding the architecture of a basic game loop, refer to the "A Basic Graphics Framework" section of the "Dancing Rectangles" article.

The graphics engine in this sample is primarily made up of four files. These four files and their respective contents are described below.

  • Pixel.cs: This file contains the Pixel class and is internal to the GXGraphicsLibrary namespace. This class is responsible for maintaining pixel conversion utilities.
  • GXBitmap.cs: This file contains the GXBitmap class and is a public class available to users of the GXGraphicsLibrary. This class implements the functionality for loading, instancing (creating memory sharing copies), saving, and displaying both indexed and direct color bitmaps. When drawing bitmaps, the following draw modes are supported:
    • Source key transparency
    • Destination key transparency
    • Flip X axis
    • Flip Y axis
    • Alpha blending
  • GXAnimation.cs: This file contains the GXAnimation class and is a public class available to users of the GXGraphicsLibrary. This class provides functionality for creating, instancing, updating, and displaying an animated bitmap.
  • GXGraphics.cs: This file contains the GXGraphics class and is a public class available to users of GXGraphicsLibrary. This class acts as the interface for the user to the graphics library, providing functionality for accessing the display buffer and maintaining draw states, as well as drawing bitmaps and animations.

In order to fully understand the sample, some concepts and terminology must be explained. The following is a list of the features new to this sample and their descriptions. For more information on features from the previous sample, see the "A Basic Graphics Framework" section of the "Dancing Rectangles" article:

  • Bitmaps: The graphics engine supports the bitmap image (.bmp) file format for pixel bit depths of 8,16, and 24 bits per pixel. 8 bit bitmaps are indexed, meaning that they use a palette of 256 colors and each pixel is an 8 bit index into the palette. All other supported formats are direct color meaning that each pixel represents a color rather than an index and no palette is required.
  • Key Transparency: The graphics engine supports two types of transparency: source key, and destination key. For each type, a "key" is specified which is a color to be tested. For the following descriptions, it may help to consider an example where an image is the source and the screen is the destination. Source key transparency checks the source key against the color of the source pixel (from the image) and if they match then the pixel is considered transparent and not drawn to the destination (the screen). Destination key transparency checks the destination key against the color of the destination pixel (from the screen) and if they match then the pixel is not drawn to the destination (the screen).
  • Axis Flipping: When drawing a bitmap, the graphics engine supports the option of flipping the image on either the X axis, Y axis, or both.
  • Alpha Blending: Alpha blending allows pixels to appear translucent. The level of translucency is defined by the alpha value where the minimum value (0) is completely transparent, the maximum value (255) is completely opaque, and values between are proportional levels of translucency. The formula for an alpha blended pixel is given below where "alpha" represents the alpha blending factor (0-255) and "max" is the maximum alpha value (255).
destination = (source * alpha + destination * (max - alpha)) / max

Optimization Tip:  Throughout the sample, this formula is implemented as is without taking into account optimizations for certain alpha values such as 50%. Note that the formula for an alpha blended pixel with a 128/255 alpha value simplifies as follows (approximating with max as 256):

destination = (source * 128 + destination * 128) / 256
destination = 128 * (source + destination) / 256
destination = (source + destination) >> 1
  • Animation: An animation is a group of images that, when played in sequence, give the appearance of motion. This is similar to a flipbook. Each individual image in the animation is referred to in the engine as a cell and the rate of the animation is frame rate independent.

The Pixel Class

Due to the nature of the hardware that this graphics library has to support, pixel formats for the target device will vary. The Pixel class was designed to alleviate the burden of dealing with the various color pixel formats. The Pixel class is nearly the same as the previous sample with a few new delegates being added to support alpha blending. The pixel class was also modified to be implemented as an instantiated class rather than a static one. This was done so that the constructor could automate the assignment of the various delegates rather than forcing external code to maintain them. The Pixel class is listed in its entirety below.

public PixelToARGBDelegate PixelToARGB = null;
public delegate uint PixelToARGBDelegate(ushort pixel);

public ColorToPixelDelegate ColorToPixel = null;
public delegate ushort ColorToPixelDelegate(Color col);

public ARGBToPixelDelegate ARGBToPixel = null;
public delegate ushort ARGBToPixelDelegate(uint col);

public BGRToPixelNoShiftDelegate BGRToPixelNoShift = null;
public delegate ushort BGRToPixelNoShiftDelegate(byte blue, byte green, byte red);

public BGRToPixelDelegate BGRToPixel = null;
public delegate ushort BGRToPixelDelegate(byte blue, byte green, byte red);

public PixelToBGRDelegate PixelToBGR = null;
public delegate void PixelToBGRDelegate(ushort pixel, ref ushort blue, ref ushort green, ref ushort red);

public Pixel(uint pixelFormat)
{
    if ((pixelFormat & (uint)GAPI.kfDirect565) == (uint)GAPI.kfDirect565)
    {
        ColorToPixel = new ColorToPixelDelegate(ColorToPixel565);
        ARGBToPixel = new ARGBToPixelDelegate(ARGBToPixel565);
        BGRToPixel = new BGRToPixelDelegate(BGRToPixel565);
        BGRToPixelNoShift = new BGRToPixelNoShiftDelegate(BGRToPixelNoShift565);
        PixelToBGR = new PixelToBGRDelegate(PixelToBGR565);
        PixelToARGB = new PixelToARGBDelegate(PixelToARGB565);
    }
    else
    {
        ColorToPixel = new ColorToPixelDelegate(ColorToPixel555);
        ARGBToPixel = new ARGBToPixelDelegate(ARGBToPixel555);
        BGRToPixel = new BGRToPixelDelegate(BGRToPixel555);
        BGRToPixelNoShift = new BGRToPixelNoShiftDelegate(BGRToPixelNoShift555);
        PixelToBGR = new PixelToBGRDelegate(PixelToBGR555);
        PixelToARGB = new PixelToARGBDelegate(PixelToARGB555);
    }
}

public ushort BGRToPixel565(byte blue, byte green, byte red)
{
    return (ushort)((((ushort)red >> 3) << 11) | (((ushort)green >> 2) << 5) | ((ushort)blue >> 3));
}

public ushort BGRToPixelNoShift565(byte blue, byte green, byte red)
{
    return (ushort)((((ushort)red) << 11) | (((ushort)green) << 5) | ((ushort)blue));
}

public void PixelToBGR565(ushort pixel, ref ushort blue, ref ushort green, ref ushort red)
{
    red = (ushort)((pixel & 0xf800) >> 11);
    green = (ushort)((pixel & 0x07e0) >> 5);
    blue = (ushort)((pixel & 0x001f));
}

public ushort BGRToPixel555(byte blue, byte green, byte red)
{
    return (ushort)((((ushort)red >> 3) << 10) | (((ushort)green >> 3) << 5) | ((ushort)blue >> 3));
}

public ushort BGRToPixelNoShift555(byte blue, byte green, byte red)
{
    return (ushort)((((ushort)red) << 10) | (((ushort)green) << 5) | ((ushort)blue));
}

public void PixelToBGR555(ushort pixel, ref ushort blue, ref ushort green, ref ushort red)
{
    red = (ushort)((pixel & 0x7c00) >> 10);
    green = (ushort)((pixel & 0x03e0) >> 5);
    blue = (ushort)((pixel & 0x001f));
}

public ushort ColorToPixel565(Color col)
{
    return ((ushort)((((ushort)col.R >> 3) << 11) | (((ushort)col.G >> 2) << 5) | ((ushort)col.B >> 3)));
}

public ushort ColorToPixel555(Color col)
{
    return ((ushort)((((ushort)col.R >> 3) << 10) | (((ushort)col.G >> 3) << 5) | ((ushort)col.B >> 3)));
}

public ushort ARGBToPixel565(uint col)
{
    return ((ushort)(((col >> 8) & 0xf800) | ((col >> 5) & 0x07e0) | ((col >> 3) & 0x001f)));
}

public uint PixelToARGB565(ushort pixel)
{
    uint bigPixel = (uint)pixel;
    return 0x00f8fcf8 & ((bigPixel << 8) | (bigPixel << 5) | (bigPixel << 3));
}

public uint PixelToARGB555(ushort pixel)
{
    uint bigPixel = (uint)pixel;
    return 0x00f8f8f8 & ((bigPixel << 9) | (bigPixel << 6) | (bigPixel << 3));
}

public ushort ARGBToPixel555(uint col)
{
    return ((ushort)(((col >> 9) & 0x7c00) | ((col >> 6) & 0x03d0) | ((col >> 3) & 0x001f)));
}

The GXGraphics Class

The GXGraphics class acts as the primary interface for the graphics library. GXGraphics is responsible for providing drawing interfaces, maintaining current draw states, maintaining the draw surface (back buffer or screen), and encapsulating GAPI functionality.

GXGraphics maintains several states related to the current draw mode. These states are maintained in the form of flags. The first set refers to the buffer mode, whereas the others relate to drawing.

The buffer mode can be either direct display drawing or back buffered. Back buffering designates memory to be used as an off-screen surface for drawing. Once drawing is complete, the back buffer is copied to the display. The following code defines and maintains the buffer mode in GXGraphics which can only be set in the constructor.

public enum DisplayBufferModes
{
    kDoubleBuffer,
    kNoBuffer,
}

protected DisplayBufferModes m_bufferMode;

Draw states are maintained internally by GXGraphics and can be accessed and modified by a set of associated functions.

public enum DrawFlags
{
    kModeNoBoundsChecking = 0x0001,
    kModeSrcKeyTransparency = 0x0002,
    kModeDstKeyTransparency = 0x0004,
    kModeAlphaBlending = 0x0008,
    kModeFlipX = 0x0010,
    kModeFlipY = 0x0020
}

protected uint m_drawFlags = 0;

public bool CheckDrawModes(DrawFlags flags)
{
    if ((m_drawFlags & (uint)flags) == (uint)flags)
        return true;

    return false;
}

public void SetDrawModes(DrawFlags flags)
{
    m_drawFlags |= (uint)flags;
}

public void ClearDrawModes(DrawFlags flags)
{
    m_drawFlags &= ~(uint)flags;
}

Some draw states require additional information. Specifically, the source and destination key transparency modes require a key value and the alpha blending mode requires an alpha value. GXGraphics provides methods that maintain this information. Note that the key transparency, unlike alpha, cannot be completely maintained with properties because they accept various forms of input.

public byte Alpha
{
    get { return m_alpha; } 
    set { m_alpha = value; }
}
byte m_alpha = 128;

public ushort SourceKey { get { return m_sourceKey; } }
protected ushort m_sourceKey;

public void SetSourceKey(ushort keyColor)
{
    m_sourceKey = keyColor;
}

public void SetSourceKey(Color keyColor)
{
    m_sourceKey = m_pixel.ColorToPixel(keyColor);
}

public void SetSourceKey(byte red, byte green, byte blue)
{
    m_sourceKey = m_pixel.BGRToPixel(blue, green, red);
}

public ushort DestinationKey { get { return m_destinationKey; } }
protected ushort m_destinationKey;

public void SetDestinationKey(ushort keyColor)
{
    m_destinationKey = keyColor;
}

public void SetDestinationKey(Color keyColor)
{
    m_destinationKey = m_pixel.ColorToPixel(keyColor);
}

public void SetDestinationKey(byte red, byte green, byte blue)
{
    m_destinationKey = m_pixel.BGRToPixel(blue, green, red);
}

Several properties are defined for returning information retrieved from GAPI about the device display.

public int ScreenWidth { get { return (int)m_displayProps.cxWidth; } }
public int ScreenHeight { get { return (int)m_displayProps.cyHeight; } }
public int PixelByteDepth { get { return (m_displayProps.cBPP >> 3); } }
public uint PixelFormat { get { return m_displayProps.ffFormat; } }
public int DisplayXPixelPitch { get { return (m_displayProps.cbxPitch >> 1); } } 
public int DisplayYPixelPitch { get { return (m_displayProps.cbyPitch >> 1); } }

Because GXGraphics has to access the handle of its parent window, it keeps a copy of it in case the parent needs it again.

public IntPtr hWnd { get { return m_hWnd; } }
protected IntPtr m_hWnd = IntPtr.Zero;

Probably the most substantial contribution of GXGraphics is that it maintains a draw surface. The draw surface is the target of all draw functions and can either be a back buffer or the actual display. Several methods and members are defined to allow GXGraphics to keep track of which draw surface is currently active.

Note:  One major change from the previous sample is that the back buffer and display are instances of GXBitmap. With the addition of the GXBitmap class, it was possible to centralize the functionality that was originally duplicated in the definitions of the various classes that were used to support images and draw surfaces.

The code that supports the draw surface is listed below.

internal int DrawSurfaceXPixelPitch
{
    get 
    {
        if (m_bufferMode == DisplayBufferModes.kNoBuffer)
            return m_displayProps.cbxPitch >> 1;

        return 1;
    }
}

internal int DrawSurfaceYPixelPitch
{
    get
    {
        if (m_bufferMode == DisplayBufferModes.kNoBuffer)
            return m_displayProps.cbyPitch >> 1;

        return ScreenWidth;
    }
}

internal GXBitmap DrawSurface
{
    get
    {
        return m_drawSurface;
    }
}
protected GXBitmap m_drawSurface = null;

internal IntPtr Display { get { return m_screenImage.Pixels; } }

protected GXBitmap m_screenImage = null;

internal GXBitmap m_backBuffer = null;

The display properties returned by GAPI are maintained by GXGraphics as well.

internal GAPI.GXDisplayProperties m_displayProps = new
    GAPI.GXDisplayProperties();

With the Pixel class now being an instantiated class, an instance of this is maintained by GXGraphics with the pixel format being provided so the constructor can determine how to initialize the pixel conversion delegates.

public Pixel GetPixel { get { return m_pixel; } }
protected Pixel m_pixel = null;

The drawing functions used by GXGraphics sometimes require a Rectangle so two are cached to minimize overhead.

protected Rectangle m_srcRect = new Rectangle(0, 0, 0, 0);
protected Rectangle m_backBufferRegion = new Rectangle(0,0,0,0);

Because the class initialization is done in the constructor, a mechanism for determining the success of the initialization is provided by the Initialized property.

public bool Inititialized { get { return m_init; } }
protected bool m_init = false;

The constructor of the GXGraphics class is responsible for initializing GAPI, accessing the hardware display properties through GAPI, setting up the buffer mode and screen, and initializing the Pixel class instance.

public GXGraphics(Control owner, DisplayBufferModes bufferMode)
{
    if (owner == null)
        return;

    owner.Capture = true;
    m_hWnd = Windows.GetCapture();
    owner.Capture = false;

    if (m_hWnd == IntPtr.Zero)
        return;

    if (GAPI.GXOpenDisplay(m_hWnd, GAPI.GX_FULLSCREEN) == GAPI.GX_FAIL)
        return;

    GAPI.GXGetDisplayProperties(m_displayProps);

    m_pixel = new Pixel(m_displayProps.ffFormat);

    m_bufferMode = bufferMode;

    if (m_bufferMode == DisplayBufferModes.kDoubleBuffer)
    {
        m_backBuffer = new GXBitmap(ScreenWidth, ScreenHeight);
        m_backBufferRegion.Width = m_backBuffer.Width;
        m_backBufferRegion.Height = m_backBuffer.Height;
    }

    m_screenImage = new GXBitmap(ScreenWidth, ScreenHeight,
        DisplayXPixelPitch, DisplayYPixelPitch, GAPI.GXBeginDraw());
    if (m_screenImage == null)
        return;

    m_init = true;
}

There are two methods provided by GXGraphics that bookend a draw frame. All draws must take place within a valid draw frame. These functions are BeginDraw and EndDraw. BeginDraw sets the active draw surface depending on the buffer mode, while EndDraw clears the active draw surface. If the buffer mode is set to double buffering then EndDraw also flips (copies) the back buffer to the screen.

public void BeginDraw()
{
    if (m_bufferMode == DisplayBufferModes.kNoBuffer)
        m_drawSurface = m_screenImage;
    else
        m_drawSurface = m_backBuffer;
}

public void EndDraw()
{
    if (m_bufferMode == DisplayBufferModes.kDoubleBuffer)
    {
        m_backBuffer.Draw(this, m_backBufferRegion, 0, 0, m_screenImage);
    }

    GAPI.GXEndDraw();
    m_drawSurface = null;
}

GXGraphics provides a method for capturing the screen to a GXBitmap object. In the test application, this is used to provide a background image that mimics the last screen the OS shows before the application starts.

public void CaptureScreen(GXBitmap image)
{
    if (image == null)
    {
        if (this.m_bufferMode == DisplayBufferModes.kDoubleBuffer)
        {
            m_srcRect.X = 0;
            m_srcRect.Y = 0;
            m_srcRect.Width = m_screenImage.Width;
            m_srcRect.Height = m_screenImage.Height;
            m_screenImage.Draw(this, m_srcRect, 0, 0, m_backBuffer);
        }
    }
    else
    {
        m_srcRect.X = 0;
        m_srcRect.Y = 0;
        m_srcRect.Width = m_screenImage.Width;
        m_srcRect.Height = m_screenImage.Height;
        m_screenImage.Draw(this, m_srcRect, 0, 0, image);
    }
}

Two methods are provided for drawing bitmaps to the screen. Because the draw function of GXBitmap requires a draw region, GXGraphics provides a function for drawing the entire bitmap that constructs the region. Each function provides the current draw surface as the destination of the draw.

public void DrawBitmap(int x, int y, GXBitmap srcBitmap)
{
    m_srcRect.X = 0;
    m_srcRect.Y = 0;
    m_srcRect.Width = srcBitmap.Width;
    m_srcRect.Height = srcBitmap.Height;
    srcBitmap.Draw(this, m_srcRect, x, y, m_drawSurface);
}

public void DrawBitmap(int x, int y, Rectangle srcRect, GXBitmap srcBitmap)
{
    srcBitmap.Draw(this, srcRect, x, y, m_drawSurface);
}

The DrawAnimation method calls the GXAnimation's Draw method with the current draw surface as the supplied destination of the draw.

public void DrawAnimation(int x, int y, GXAnimation srcAnimation)
{
    srcAnimation.Draw(this, x, y, m_drawSurface);
}

FillRect provides a method for drawing a filled rectangle to the screen. This is the same method as the FillRect3 method of the previous sample with the addition of transparency and alpha blending. The optimizations of FillRect4 were taken out for code readability. The first optimization tip describes how to add it back in.

Optimization Tip 1:  Performance can be increased by unrolling the inner loop in the same way as that of the FillRect4 optimization in the previous sample. For more information see the "FillRect4" section of the "Dancing Rectangles" article.
Optimization Tip 2:  Notice that a check for various draw modes is done on a per pixel basis. An opportunity for significant performance gain would be to move this check to a per draw basis and have separate functions for each combination of draw modes. For example: FillRectWithAlpha, FillRectWithSrcKey, FillRectWithDstKey, etc. This was not done in this sample due to the size and repetition of the code required.
public void FillRect(Rectangle fillRegion, Color fillColor)
{
    if ((m_drawFlags & (uint)DrawFlags.kModeNoBoundsChecking) != 0)
    {
        if (fillRegion.Right > this.ScreenWidth)
            fillRegion.Width = this.ScreenWidth - fillRegion.X;

        if (fillRegion.Bottom > this.ScreenHeight)
            fillRegion.Height = this.ScreenHeight - fillRegion.Y;

        if (fillRegion.X < 0)
        {
            fillRegion.Width += fillRegion.X;
            fillRegion.X = 0;
        }

        if (fillRegion.Y < 0)
        {
            fillRegion.Height += fillRegion.Y;
            fillRegion.Y = 0;
        }

        if (fillRegion.X >= this.ScreenWidth || fillRegion.Y >=
            this.ScreenHeight || fillRegion.Right < 0 ||
            fillRegion.Bottom < 0 || fillRegion.Width < 0 ||
            fillRegion.Height < 0)
        {
            return;
        }
}

    ushort rgb = m_pixel.ColorToPixel(fillColor);

    bool bAlpha = CheckDrawModes(DrawFlags.kModeAlphaBlending);
    bool bSrcKey = CheckDrawModes(DrawFlags.kModeSrcKeyTransparency);
    bool bDstKey = CheckDrawModes(DrawFlags.kModeDstKeyTransparency);

    ushort rSrc = 0;
    ushort gSrc = 0;
    ushort bSrc = 0;
    ushort rDst = 0;
    ushort gDst = 0;
    ushort bDst = 0;
    ushort dAlpha = (ushort)(255 - m_alpha);

    if (bAlpha)
    {
        m_pixel.PixelToBGR(rgb, ref bSrc, ref gSrc, ref rSrc);
        rSrc = (ushort)(m_alpha * rSrc);
        gSrc = (ushort)(m_alpha * gSrc);
        bSrc = (ushort)(m_alpha * bSrc);
    }

    unsafe 
    {
        int xPitch = DrawSurfaceXPixelPitch;
        int yPitch = DrawSurfaceYPixelPitch;

        ushort* pCurLine = (ushort*)this.DrawSurface.Pixels +
            fillRegion.X * xPitch + fillRegion.Y * yPitch;

        int xOffset = fillRegion.Width * xPitch;

        ushort* pLastLine = pCurLine + (fillRegion.Height) * yPitch;
        while (pCurLine != pLastLine)
        {
            ushort* pCurPixel = pCurLine;
            ushort* pLastPixel = pCurPixel + xOffset;
            while (pCurPixel != pLastPixel)
            {
                if (!(bSrcKey && rgb == m_sourceKey) &&
                    !(bDstKey && *pCurPixel == m_destinationKey))
                {
                    if (bAlpha)
                    {
                        m_pixel.PixelToBGR(*pCurPixel, ref bDst,
                            ref gDst, ref rDst);

                        rDst = (ushort)((rSrc + rDst * dAlpha) >> 8);
                        gDst = (ushort)((gSrc + gDst * dAlpha) >> 8);
                        bDst = (ushort)((bSrc + bDst * dAlpha) >> 8);

                        *pCurPixel = m_pixel.BGRToPixelNoShift
                        (
                            (byte)bDst, (byte)gDst, (byte)rDst
                         );
                    }
                    else
                    {
                        *pCurPixel = rgb;
                    }
                }
                pCurPixel += xPitch;

            }

            pCurLine += yPitch;
        }
    }
}

The final method of the GXGraphics class is Dispose. This method is required because it is possible that memory was allocated to create a back buffer.

public void Dispose()
{
    GAPI.GXCloseDisplay();

    if (m_backBuffer != null)
        m_backBuffer.Dispose();
}

The GXBitmap Class

The GXBitmap class is the backbone for the graphics engine. This class provides numerous methods for manipulating bitmaps and the majority of the complexity of the overall code lies in this class. The primary methods and classes provided by GXBitmap are described below.

  • BITMAPFILEHEADER: This is a class that is internal to the GXBitmap class. The data represented by this class is the first data that appears in a .bmp file.
  • BITMAPINFOHEADER: This is a class that is also internal to the GXBitmap class. The data this class encapsulates appears in the .bmp file immediately following the BITMAPFILEHEADER.
  • GXBitmap: There are four different constructors provided for the GXBitmap class.
    • GXBitmap(int width, int height) provides a method for allocating memory for an empty bitmap.
    • GXBitmap(int width, int height, int xPitch, int yPitch, IntPtr buffer) provides a method for creating a bitmap with the specified parameters using the supplied buffer as the pixel data.
    • GXBitmap(Rectangle srcRect, GXBitmap srcBitmap) provides a method for creating a bitmap that is a region of another bitmap. This bitmap will share the memory of the original.
    • GXBitmap(Stream bmpData, GXGraphics gx) provides a method for creating a bitmap from a stream containing .bmp file data.
Note:  The first two constructors can only create 16 bpp bitmaps since they are assumed to be created specifically for use on the device.
  • ReadPixels: This method reads in the pixel data from a given stream. The supported pixel bit depths are: 8, 16, and 24.
  • WritePixels: This method writes out the pixel data from the bitmap to the specified stream. The supported pixel bit depths are: 8, 16, and 24.
  • DrawDirect: This method is assigned to the Draw delegate if the bitmap pixel format is direct, i.e., 16, or 24 bits per pixel.
  • DrawIndexed: This method is assigned to the Draw delegate if the bitmap pixel format is indexed, i.e., 8 bits per pixel.
  • Draw: The Draw delegate is assigned either DrawDirect or DrawIndexed bases on the pixel format. This function is responsible for drawing a region of the bitmap to a destination bitmap with the draw modes specified by a supplied instance of GXGraphics.
  • GetPixel: This function gets the color of a specific pixel in the bitmap.
  • SetPixel: This function sets the color of a specific pixel in the bitmap. For indexed bitmaps it will check the palette for the entry with the color closest to the specified color and set the pixel to that palette index.

The BITMAPFILEHEADER and BITMAPINFOHEADER classes correspond to the Windows GDI structures of the same name. These headers are the first data contained in a bitmap file, starting with BITMAPFILEHEADER, and the classes are responsible for reading, writing, and initializing the data associated with the headers. The methods ReadStream and WriteStream are provided for loading and saving bitmap information respectively, whereas the Init method provides the ability to create an original bitmap and initialize its data.

protected class BITMAPFILEHEADER
{
    protected const ushort fBitmapFileDesignator = 19778;
    protected const uint fBitmapFileOffsetToData = 54;

    public ushort bfType;
    public uint bfSize;
    public short bfReserved1;
    public short bfReserved2;
    public uint bfOffBits;

    public void ReadStream(BinaryReader rdr)
    {
        bfType = rdr.ReadUInt16();
        bfSize = rdr.ReadUInt32();
        bfReserved1 = rdr.ReadInt16();
        bfReserved2 = rdr.ReadInt16();
        bfOffBits = rdr.ReadUInt32();
    }

    public void WriteStream(BinaryWriter bw)
    {
        bw.Write(bfType);
        bw.Write(bfSize);
        bw.Write(bfReserved1);
        bw.Write(bfReserved2);
        bw.Write(bfOffBits);
    }

    public void Init(int width, int height)
    {
        bfType = fBitmapFileDesignator;
        bfOffBits = fBitmapFileOffsetToData;
        bfReserved1 = 0;
        bfReserved2 = 0;
        bfSize = (uint)(bfOffBits + height * (((width + ((width << 1) %
            4)) << 1)));
    }
}

protected class BITMAPINFOHEADER
{
    const uint kBitmapInfoHeaderSize = 40;

    public uint biSize;
    public int biWidth;
    public int biHeight;
    public short biPlanes;
    public short biBitCount;
    public uint biCompression;
    public uint biSizeImage;
    public int biXPelsPerMeter;
    public int biYPelsPerMeter;
    public uint biClrUsed;
    public uint biClrImportant;

    public void ReadStream(BinaryReader rdr)
    {
        biSize = rdr.ReadUInt32();
        biWidth = rdr.ReadInt32();
        biHeight = rdr.ReadInt32();
        biPlanes = rdr.ReadInt16();
        biBitCount = rdr.ReadInt16();
        biCompression = rdr.ReadUInt32();
        biSizeImage = rdr.ReadUInt32();
        biXPelsPerMeter = rdr.ReadInt32();
        biYPelsPerMeter = rdr.ReadInt32();
        biClrUsed = rdr.ReadUInt32();
        biClrImportant = rdr.ReadUInt32();
    }

    public void WriteStream(BinaryWriter bw)
    {
        bw.Write(biSize);
        bw.Write(biWidth);
        bw.Write(biHeight);
        bw.Write(biPlanes);
        bw.Write(biBitCount);
        bw.Write(biCompression);
        bw.Write(biSizeImage);
        bw.Write(biXPelsPerMeter);
        bw.Write(biYPelsPerMeter);
        bw.Write(biClrUsed);
        bw.Write(biClrImportant);
    }

    public void Init(int width, int height)
    {
        biSize = kBitmapInfoHeaderSize;
        biWidth = width;
        biHeight = height;
        biPlanes = 1;
        biBitCount = 16;
        biCompression = 0;
        biSizeImage = 0;
        biXPelsPerMeter = 0;
        biYPelsPerMeter = 0;
        biClrUsed = 0;
        biClrImportant = 0;
    }
}

In addition to instances of the headers, the GXBitmap class maintains some pixel information. The pixel information varies depending on the format of the bitmap. Indexed (8 bit) bitmaps require a palette and each pixel is an 8 bit index into the palette. Therefore, for 8 bit bitmaps, m_pixels will contain a 256 entry palette, whereas m_indices will contain the pixel indices that correspond to entries in the palette. In contrast, a direct color bitmap (16 or 24 bit) will only utilize m_pixels. In this case, m_pixels will contain 16 bit pixels that are formatted to be compatible with the device's display.

public IntPtr Pixels { get { return m_pixels; } }
protected IntPtr m_pixels = IntPtr.Zero;
protected IntPtr m_indices = IntPtr.Zero;
protected BITMAPFILEHEADER m_fileHdr = new BITMAPFILEHEADER();
protected BITMAPINFOHEADER m_infoHdr = new BITMAPINFOHEADER();

The GXBitmap class contains a Draw delegate. This is because there are two separate draw functions based on whether the pixels are direct or indexed. The delegate is set in the constructor and is the only publicly accessible draw function. The methods that are assigned to Draw cannot be called outside of the GXBitmap class. This is to protect the class from the wrong function being called.

internal DrawDelegate Draw = null;
internal delegate void DrawDelegate(GXGraphics gx,
    Rectangle drawRegion, int x, int y, GXBitmap bmDest);

There are several properties associated with the GXBitmap class that allow access to the header and pitch information. The pitch of the bitmap is maintained because the source of a bitmap can be anything, including the physical screen, and may therefore have non-standard pitches. The pitch can also take into account the case where a GXBitmap is a region of another GXBitmap instance. In this case, the y pitch of the derived GXBitmap would be that of the original even though the widths may be different.

public int BPP    { get { return m_infoHdr.biBitCount; } }
public int Width  { get { return m_infoHdr.biWidth;  } }
public int Height { get { return m_infoHdr.biHeight; } } 

public int xPixelPitch { get { return m_xPixelPitch; } }
protected int m_xPixelPitch = 0;

public int yPixelPitch { get { return m_yPixelPitch; } }
protected int m_yPixelPitch = 0;

The GXBitmap class can be an instance of another GXBitmap, derived from an IntPtr, or created as an original bitmap. Because of this, it is necessary to keep track of whether or not a particular instance allocated any memory. The member m_allocated is provided for this purpose.

protected bool m_allocated = false;

The first constructor to GXBitmap is provided as a method to create an instance of an empty bitmap. The only pixel format supported for this type of bitmap is 16 bit. In this constructor, memory is allocated for the pixel data.

public GXBitmap(int width, int height)
{
    m_xPixelPitch = 1;
    m_yPixelPitch = width;

    m_pixels = Windows.LocalAlloc(Windows.LMEM_FIXED,
        (uint)(2 * width * height));
    m_allocated = true;

    Draw = new DrawDelegate(DrawDirect);

    m_fileHdr.Init(width, height);
    m_infoHdr.Init(width, height);
}

The second constructor allows the user to specify the parameters of the bitmap and supply an arbitrary buffer that represents the pixel information. The GXBitmap instance will assume that the memory is allocated and is the proper size. The user can specify any pitch for the bitmap as well. This comes in handle, in particular, when using this constructor to create a GXBitmap that represents the physical screen.

public GXBitmap(int width, int height, int xPitch, int yPitch, IntPtr buffer)
{
    m_xPixelPitch = xPitch;
    m_yPixelPitch = yPitch;

    m_pixels = buffer;
    m_allocated = false;

    Draw = new DrawDelegate(DrawDirect);

    m_fileHdr.Init(width, height);
    m_infoHdr.Init(width, height);
}

The third constructor allows the user to specify a region and source GXBitmap that will be used to create this instance. This instance will use the memory allocated by the original source GXBitmap.

public GXBitmap(Rectangle srcRect, GXBitmap srcBitmap)
{
    m_xPixelPitch = srcBitmap.m_xPixelPitch;
    m_yPixelPitch = srcBitmap.m_yPixelPitch;

    if (srcBitmap.BPP == 8)
    {
        m_indices = (IntPtr)(srcBitmap.m_indices.ToInt32() +
            srcRect.X * srcBitmap.m_xPixelPitch + srcRect.Y *
            srcBitmap.m_yPixelPitch);
        m_pixels = srcBitmap.m_pixels;
    }
    else
    {
        m_pixels = (IntPtr)(srcBitmap.m_pixels.ToInt32() + srcRect.X *
           (srcBitmap.m_xPixelPitch << 1) + srcRect.Y *
           (srcBitmap.m_yPixelPitch << 1));
    }

    m_allocated = false;

    if (srcBitmap.BPP == 8)
        Draw = new DrawDelegate(DrawIndexed);
    else
        Draw = new DrawDelegate(DrawDirect);

    m_fileHdr.Init(srcRect.Width, srcRect.Height);
    m_infoHdr.Init(srcRect.Width, srcRect.Height);
    m_infoHdr.biBitCount = srcBitmap.m_infoHdr.biBitCount;
}

The final constructor loads a .bmp file from a supplied stream. An instance of GXGraphics is required by this constructor in order to properly convert the pixels as they are loaded. This method will allocate the necessary memory for loading the bitmaps and supports both indexed and direct pixel formats.

public GXBitmap(Stream bmpData, GXGraphics gx)
{
    BinaryReader rdr = new BinaryReader(bmpData);
    m_fileHdr.ReadStream(rdr);
    m_infoHdr.ReadStream(rdr);

    this.m_allocated = true;
    this.m_xPixelPitch = 1;
    this.m_yPixelPitch = this.Width;

    ReadPixels(rdr, gx);

    if (m_infoHdr.biBitCount == 8)
        Draw = new DrawDelegate(DrawIndexed);
    else
        Draw = new DrawDelegate(DrawDirect);

    rdr.Close();
}

The ReadPixels method is responsible for reading the pixel information from a stream. This method allocates the appropriate memory for the pixel data and the palette, if a palette is required. This method supports the loading of 8, 16, and 24 bit bitmaps. There is also a WritePixels method in the sample but due to the similarity of the code, it is not shown in the code listings below.

Note:  Pixels are converted at load time as an optimization to the draw routines. Therefore, 32 bit palette entries, 24 bit pixel data, and 16 bit pixels are all converted to 16 bit pixels that match the format of the current device's display. For 16 bit bitmaps this may require a conversion from 565 to 555 or vice versa.

A note of interest when reviewing the following code is that the .bmp file format is padded to 32 bit boundaries for each row of pixels. Thus, the cExtra variable is used to determine if there are any extra bytes at the end of each row.

Optimization Tip:  This code is not optimized in the same way as draw functions. To increase load performance, consider using while loops instead of for loops and unrolling the inner loop to read multiple pixels at once.
protected void ReadPixels(BinaryReader rdr, GXGraphics gx)
{
    if (m_infoHdr.biBitCount == 8)
    {
        m_pixels = Windows.LocalAlloc(Windows.LMEM_FIXED, (uint)512);
        m_indices = Windows.LocalAlloc(Windows.LMEM_FIXED,
            (uint)(this.Width * this.Height));

        unsafe
        {
            ushort* pPixels = (ushort*)m_pixels;
            for (int i = 0; i < 256; i++)
            {
                *pPixels = gx.GetPixel.ARGBToPixel(rdr.ReadUInt32());
                pPixels++;
            }
        }
    }
    else
    {
        m_pixels = Windows.LocalAlloc(Windows.LMEM_FIXED,
           (uint)(2 * this.Width * this.Height));
    }

    int rMax = this.Height;
    int cMax = this.Width;
    int cExtra = 0;

    if (m_infoHdr.biBitCount == 24)
        cExtra = (3 * this.Width) % 4;
    else if (m_infoHdr.biBitCount == 8)
        cExtra = (this.Width) % 4;
    else
        cExtra = (2 * this.Width) % 4;

    int yOffset = 2 * this.Width;

    unsafe
    {
        ushort* pCurPixel = null;
        byte* pIndices = null;

        if (m_infoHdr.biBitCount == 8)
            pIndices = (byte*)m_indices + (this.Height - 1) *
                this.Width;
        else
            pCurPixel = (ushort*)m_pixels + (this.Height - 1) *
                this.Width;

        for (int r = 0; r < rMax; r++)
        {
            for (int c = 0; c < cMax; c++)
            {
                if (m_infoHdr.biBitCount == 24)
                {
                    byte blue = rdr.ReadByte();
                    byte green = rdr.ReadByte();
                    byte red = rdr.ReadByte();
                    *pCurPixel =
                        gx.GetPixel.BGRToPixel(blue,green,red);
                    pCurPixel++;
                }
                else if (m_infoHdr.biBitCount == 16)
                {
                    *pCurPixel = rdr.ReadUInt16();
                    if ((gx.PixelFormat & GAPI.kfDirect565) != 0)
                    {
                        ushort pixel = *pCurPixel;
                        *pCurPixel = (ushort)(((pixel << 1) & 0xffc0) |
                            (pixel & 0x1f));
                    }
                    pCurPixel++;
                }
                else
                {
                    *pIndices = rdr.ReadByte();
                    pIndices++;
                }
            }

            pCurPixel -= yOffset;
            pIndices -= yOffset;

            for (int c = 0; c < cExtra; c++)
            {
                rdr.ReadByte();
            }
        }
    }
}

DrawDirect is the method assigned to the Draw delegate if the pixel format is not indexed. This function is responsible for drawing the current GXBitmap instance onto another GXBitmap instance. The user specifies a region of the source bitmap to be drawn, as well as the x,y pixel location of the destination draw. An instance of GXGraphics is also provided to allow for the checking of draw states and conversion of pixels if needed. DrawIndexed is not shown below due to the similarity of the code. The primary difference being that DrawIndexed cycles through m_indices as a byte pointer and uses the value of each pixel as an index into m_pixels. Otherwise, the code is the same.

Optimization Tip 1:  Performance can be increased by unrolling the inner loop in the same way as that of the FillRect4 optimization in the previous sample. For more information see the "FillRect4" section of the "Dancing Rectangles" article.
Optimization Tip 2:  Notice that a check for various draw modes is done on a per pixel basis. An opportunity for significant performance gain would be to move this check to a per draw basis and have separate functions for each combination of draw modes. For example: DrawDirectWithAlpha, DrawDirectWithSrcKey, DrawDirectWithDstKey, etc. This was not done in this sample due to the size and repetition of the code required.
internal void DrawDirect(GXGraphics gx, Rectangle drawRegion, int x,
    int y, GXBitmap bmDest)
{
    if (!gx.CheckDrawModes(GXGraphics.DrawFlags.kModeNoBoundsChecking))
    {
        if (drawRegion.Right > this.Width)
            drawRegion.Width = this.Width - drawRegion.X;

        if (drawRegion.Bottom > this.Height)
            drawRegion.Height = this.Height - drawRegion.Y;

        if (x + drawRegion.Right > bmDest.Width)
            drawRegion.Width = bmDest.Width - drawRegion.X - x;

        if (y + drawRegion.Bottom > bmDest.Height)
            drawRegion.Height = bmDest.Height - drawRegion.Y - y;

        if (drawRegion.X < 0)
        {
            drawRegion.Width += drawRegion.X;
            drawRegion.X = 0;
        }

        if (drawRegion.Y < 0)
        {
            drawRegion.Height += drawRegion.Y;
            drawRegion.Y = 0;
        }

        if (drawRegion.X >= this.Width || drawRegion.Y >= 
            this.Height || drawRegion.Right < 0 || 
            drawRegion.Bottom < 0 || drawRegion.Width < 0 ||
            drawRegion.Height < 0 || x >= bmDest.Width || x < 0 ||
            y >= bmDest.Height || y < 0)
        {
            return;
        }

    }

    int xDstPitch = bmDest.xPixelPitch;
    int yDstPitch = bmDest.yPixelPitch;
    int xSrcPitch = this.xPixelPitch;
    int ySrcPitch = this.yPixelPitch;

    int xSrcOffset = drawRegion.Width * xSrcPitch;

    bool bSrcKey = gx.CheckDrawModes(GXGraphics.DrawFlags.kModeSrcKeyTransparency);
    bool bDstKey = gx.CheckDrawModes(GXGraphics.DrawFlags.kModeDstKeyTransparency);
    bool bFlipY = gx.CheckDrawModes(GXGraphics.DrawFlags.kModeFlipY);
    bool bFlipX = gx.CheckDrawModes(GXGraphics.DrawFlags.kModeFlipX);
    bool bAlpha = gx.CheckDrawModes(GXGraphics.DrawFlags.kModeAlphaBlending);

    ushort rSrc = 0;
    ushort gSrc = 0;
    ushort bSrc = 0;
    ushort rDst = 0;
    ushort gDst = 0;
    ushort bDst = 0;
    ushort alpha = gx.Alpha;
    ushort dAlpha = (ushort)(255 - alpha);

    unsafe
    {
        ushort* pCurDstLine = (ushort*)bmDest.Pixels.ToInt32();

        if (bFlipY)
        {
            pCurDstLine += (y + drawRegion.Height - 1) * yDstPitch;
            yDstPitch = -yDstPitch;
        }
        else
        {
            pCurDstLine += y * yDstPitch;
        }

        if (bFlipX)
        {
            pCurDstLine += (x + drawRegion.Width - 1) * xDstPitch;
            xDstPitch = -xDstPitch;
        }
        else
        {
            pCurDstLine += x * xDstPitch;
        }

        ushort* pCurSrcLine = (ushort*)m_pixels + drawRegion.X *
            xSrcPitch  + drawRegion.Y * ySrcPitch;

        ushort srcKey = gx.SourceKey;
        ushort dstKey = gx.DestinationKey;

        ushort* pLastSrcLine = pCurSrcLine + drawRegion.Height *
            ySrcPitch;
        while (pCurSrcLine != pLastSrcLine)
        {
            ushort* pCurDstPixel = pCurDstLine;
            ushort* pCurSrcPixel = pCurSrcLine;

            ushort* pLastSrcPixel = pCurSrcPixel + xSrcOffset;
            while (pCurSrcPixel != pLastSrcPixel)
            {
                if (!(bSrcKey && *pCurSrcPixel == srcKey) &&
                    !(bDstKey && *pCurDstPixel == dstKey))
                {
                    if (bAlpha)
                    {
                        gx.GetPixel.PixelToBGR(*pCurSrcPixel, ref bSrc,
                            ref gSrc, ref rSrc);
                        gx.GetPixel.PixelToBGR(*pCurDstPixel, ref bDst,
                            ref gDst, ref rDst);

                        rDst = (ushort)((alpha * rSrc + rDst * dAlpha)
                            >> 8);
                        gDst = (ushort)((alpha * gSrc + gDst * dAlpha)
                            >> 8);
                        bDst = (ushort)((alpha * bSrc + bDst * dAlpha)
                            >> 8);

                        *pCurDstPixel =
                             gx.GetPixel.BGRToPixelNoShift((byte)bDst,
                             (byte)gDst, (byte)rDst);
                    }
                    else
                        *pCurDstPixel = *pCurSrcPixel;
                }
                pCurSrcPixel += xSrcPitch;
                pCurDstPixel += xDstPitch;
            }

            pCurDstLine += yDstPitch;
            pCurSrcLine += ySrcPitch;
        }
    }
}

The GetPixel function returns the color of the pixel at the specified location. Remember that for indexed bitmaps, the pixel value is an index into the palette.

public ushort GetPixel(int x, int y)
{
    if (x >= this.Width || x < 0 || y >= this.Height || y < 0)
        return 0;

    unsafe
    {
        byte* pIndices = (byte*)m_indices;
        ushort* pPixels = (ushort*)m_pixels;

        if (this.BPP == 8)
        {
            return pPixels[pIndices[x + y * this.Width]];
        }

        return pPixels[x + y * this.Width];
    }
}

The SetPixel method sets the color of the pixel at the specified location. For indexed bitmaps, the palette is searched for the color that is closest to the designated color. The pixel value is then set to the closest palette entry's index.

public void SetPixel(int x, int y, ushort pixelColor, GXGraphics gx)
{
    if (x >= this.Width || x < 0 || y >= this.Height || y < 0)
        return;

    unsafe
    {
        byte* pIndices = (byte*)m_indices;
        ushort* pPixels = (ushort*)m_pixels;

        if (this.BPP == 8)
        {
            byte index = 0;
            uint distSquared = uint.MaxValue;
            ushort dstBlue = 0;
            ushort dstGreen = 0;
            ushort dstRed = 0;

            gx.GetPixel.PixelToBGR(pixelColor, ref dstBlue,
                ref dstGreen, ref dstRed);

            for (int i = 0; i < 256; i++)
            {
                ushort srcBlue = 0;
                ushort srcGreen = 0;
                ushort srcRed = 0;
                gx.GetPixel.PixelToBGR(*pPixels, ref srcBlue,
                    ref srcGreen, ref srcRed);
                pPixels++;

                uint deltaR = (uint)Math.Abs(srcRed-dstRed);
                uint deltaG = (uint)Math.Abs(srcGreen-dstGreen);
                uint deltaB = (uint)Math.Abs(srcBlue-dstBlue);
                uint curDistSquared = deltaR*deltaR + deltaG*deltaG +
                    deltaB*deltaB;
                if (curDistSquared < distSquared)
                {
                    index = (byte)i;
                    distSquared = curDistSquared;
                }
            }

            pPixels[pIndices[x + y * this.Width]] = index;
        }
        else
        {
            pPixels[x + y * this.Width] = pixelColor;
        }
    }
}

The Save function saves the GXBitmap as a .bmp file to the specified file location.

public void Save(string fileName, GXGraphics gx)
{
    FileStream fs = null;
    BinaryWriter bw = null;

    try
    {
        fs = new FileStream(fileName, FileMode.OpenOrCreate);
        bw = new BinaryWriter(fs);

        m_fileHdr.WriteStream(bw);
        m_infoHdr.WriteStream(bw);

        WritePixels(bw, gx);
    }
    finally
    {
        if (bw != null)
            bw.Close();

        if (fs != null)
            fs.Close();
    }
}

Dispose frees any memory that was allocated for the palette and pixel array.

public void Dispose()
{
    if (!m_allocated)
        return;

    if (m_pixels != IntPtr.Zero)
        Windows.LocalFree(m_pixels);

    if (m_indices != IntPtr.Zero)
        Windows.LocalFree(m_indices);
}

The GXAnimation Class

The GXAnimation class provides an interface for automating the updating and displaying of animated bitmaps. The user can specify an image and information about the size and speed of the animation and the GXAnimation class provides methods for updating and displaying the current cell.

The cell width and height specify the size of a single animation cell within the bitmap.

public int CellWidth { get { return m_cellWidth; } }
protected int m_cellWidth;
public int CellHeight { get { return m_cellHeight; } }
protected int m_cellHeight;

The GXAnimation class maintains several members associated with the cells of the animation.

  • m_startCell is the first cell to play in the animation cycle
  • m_numCells is the total number of cells to be played sequentially
  • m_curCell is the cell that is currently displayed
  • m_startTime_ms is the time the animation started in milliseconds
  • m_cellsPerSecond is the rate that the animation is updated
protected int m_startCell;
protected int m_numCells;
protected int m_curCell;
public Int64 StartTime { set { m_startTime_ms = value; } }
protected Int64 m_startTime_ms;
protected Int64 m_cellsPerSecond;

GXAnimation provides a property for accessing the image associated with it.

public GXBitmap Image { get { return m_bmp; } }
protected GXBitmap m_bmp;

To reduce overhead, a Rectangle representing the source region of the draw is maintained. This is the region of the bitmap, based on the current cell, that is currently being displayed.

protected Rectangle m_srcRegion = new Rectangle(0, 0, 0, 0);

Because many GXAnimation instances can share the same GXBitmap instance, each instance has to keep track of whether the GXBitmap instance was allocated or shared.

protected bool m_allocated = false;

The first constructor to GXAnimation allocates and loads a GXBitmap from a stream and initializes the animation information.

public GXAnimation
(
    Stream bmpData,
    GXGraphics gx,
    int numCells,
    int startCell,
    int cellWidth,
    int cellHeight,
    int cellsPerSecond,
    Int64 curTime_ms
)
{
    m_cellWidth = cellWidth;
    m_cellHeight = cellHeight;
    m_numCells = numCells;
    m_startCell = startCell;

    m_bmp = new GXBitmap(bmpData, gx);
    m_allocated = true;

    m_cellsPerSecond = (Int64)cellsPerSecond;
    m_startTime_ms = curTime_ms;

    m_srcRegion.Width = m_cellWidth;
    m_srcRegion.Height = m_cellHeight;

    Update(curTime_ms);
}

The second constructor allows the user to create an instance of another GXAnimation with a unique animation rate, starting cell, and starting time. This allows the same original GXAnimation to be instanced with unique animations that are not tied to the original's cell playback information.

public GXAnimation
(
    GXAnimation anim,
    int startCell,
    int cellsPerSecond,
    Int64 curTime_ms
)
{
    m_cellWidth = anim.m_cellWidth;
    m_cellHeight = anim.m_cellHeight;
    m_numCells = anim.m_numCells;
    m_startCell = startCell;

    m_bmp = anim.m_bmp;
    m_allocated = false;

    m_cellsPerSecond = (Int64)cellsPerSecond;
    m_startTime_ms = curTime_ms;

    m_srcRegion.Width = m_cellWidth;
    m_srcRegion.Height = m_cellHeight;

    Update(curTime_ms);
}

The Draw function utilizes the current cell information to draw the region of the bitmap that it represents. m_srcRegion is set in the Update function when the current cell is determined.

public void Draw(GXGraphics gx, int x, int y, GXBitmap bmDest)
{
    m_bmp.Draw(gx, m_srcRegion, x, y, bmDest);
}

The Update function uses the current time to determine the current cell. Absolute times are used in this function rather than delta times in order to make it easier to determine the current cell without having to use floating point math or track overflow time when a cell is updated.

public void Update(Int64 curTime_ms)
{
    m_curCell = (int)((m_startCell + (m_cellsPerSecond *
        (curTime_ms  - m_startTime_ms) / 1000)) % m_numCells);
    m_srcRegion.X = (int)(m_curCell % (m_bmp.Width / m_cellWidth)*
        m_cellWidth);
    m_srcRegion.Y = (int)(m_curCell / (m_bmp.Width / m_cellWidth) *
        m_cellHeight);
}

The Dispose method frees the GXBitmap instance if it was allocated for this animation.

public void Dispose()
{
    if (!m_allocated)
        return;

    if (m_bmp != null)
        m_bmp.Dispose();
}

Test Application

The test application for this sample is provided in both C# and VB. The sample demonstrates the following functionality of the graphics engine:

  • Double buffering
  • Alpha blending
  • Source key transparency
  • Animation
  • Screen capture
  • Bitmap loading and displaying
  • Getting the color of a pixel in a bitmap
  • Animation instancing
  • Flipping of the X Axis

The test accomplish the verification of these elements with the following features

  • Captures the screen before the test begins and draws it in every frame as the background.
  • Uses animations with a frame rate of 0 (not animating) to draw randomly moving tiles that make up a picture. This picture is displayed with the tiles in their original position for the first 40 frames of the test. Only tile 0,0 is an original bitmap, the rest are instances.
  • Draws animated zombies with varying animation rates, velocities, and starting animation cells using source key transparency based off of the color of pixel 0,0. When these zombies are moving to the left, the animation is flipped about the x axis so as to animate in the correct direction. Only zombie 0 is an original bitmap, the rest are instances.
  • Draws a randomly moving alpha blended rectangle over the top of all other draws.

ImageSpriteTest

The ImageSpriteTest class encapsulates the entire test such that the Form code is greatly simplified. The test is started with a call to Draw and ends with a call to Quit. The test calls Application.DoEvents every frame in order to allow the Form to process messages.

The ImageSpriteTest class defines a base class Sprite that is used to derive the sprites used for testing. This class defines members that track the screen location and velocity of the sprite. It also defines a method, Update, responsible for updating the location given a time interval since the previous update. The Update method also checks the sprite against the bounds of the screen and reverses the velocity of the proper axis if it collided.

protected class Sprite
{
    public float m_velX;
    public float m_velY;
    private float m_curX;
    private float m_curY;
    private int m_locX;
    private int m_locY;

    public int PositionX
    {
        get { return m_locX; }
        set
        {
            m_locX = value;
            m_curX = (float)m_locX;
        }
    }

    public int PositionY
    {
        get { return m_locY; }
        set
        {
            m_locY = value;
            m_curY = (float)m_locY;
        }
    }

    protected void Update
    (
        float delta_ms, GXGraphics gx, int imageWidth, int imageHeight
    )
    {
        m_curX += (delta_ms * m_velX / 1000.0f);
        m_curY += (delta_ms * m_velY / 1000.0f);
        m_locX = (int)m_curX;
        m_locY = (int)m_curY;

        if (PositionX + imageWidth > gx.ScreenWidth)
        {
            PositionX = gx.ScreenWidth - imageWidth;
            m_velX = - m_velX;
        }
        else if (PositionX < 0)
        {
            PositionX = 0;
            m_velX = - m_velX;
        }

        if (PositionY + imageHeight > gx.ScreenHeight)
        {
            PositionY = gx.ScreenHeight - imageHeight;
            m_velY = - m_velY;
        }
        else if (PositionY < 0)
        {
            PositionY = 0;
            m_velY = - m_velY;
        }
    }
}

The AnimatedSprite class is derived from Sprite and implements a GXAnimation as a sprite. This class overloads the Update function to update the animation.

protected class AnimatedSprite : Sprite
{
    public GXAnimation m_anim;

    public void Update(float delta_ms, Int64 curTime_ms, GXGraphics gx)
    {
        base.Update(delta_ms, gx, m_anim.CellWidth, m_anim.CellHeight);
        m_anim.Update(curTime_ms);
    }
}

The AlphaSprite class is also derived from Sprite and implements a rectangle that will be drawn with alpha blending on the top of each frame. This class defines a constructor which allows the user to specify the color of the rectangle. The Update function of the AlphaSprite class is overloaded to allow the location of the rectangle to be updated. This class also provides a Draw method that draws the rectangle with alpha blending enabled.

protected class AlphaSprite : Sprite
{
    protected Rectangle m_rect = new Rectangle(0,0,60,60);
    protected Color m_color;

    public AlphaSprite(Color rectColor)
    {
        m_color = rectColor;
    }

    public void Update(float delta_ms, GXGraphics gx)
    {
        base.Update(delta_ms, gx, m_rect.Width, m_rect.Height);
        m_rect.X = PositionX;
        m_rect.Y = PositionY;
    }

    public void Draw(GXGraphics gx)
    {
        gx.SetDrawModes(GXGraphics.DrawFlags.kModeAlphaBlending);
        gx.Alpha = 128;
        gx.FillRect(m_rect, m_color);
        gx.ClearDrawModes(GXGraphics.DrawFlags.kModeAlphaBlending);
    }
}

Zombies appear on the screen and walk back and forth, switching directions when they collide with the edges of the screen. The following code defines the number of zombies and creates an array to hold them.

const int kNumZombies = 20;
protected AnimatedSprite[] m_zombies = new AnimatedSprite[kNumZombies];

The m_screenImage member is the background for the test. This background is a capture of the device's screen before the test is run.

protected GXBitmap m_screenImage = null;

The background image that appears in front of the screen capture is a bitmap that is tiled using animation cells. Think of each tile as an individual cell of the animation. Each cell is displayed but the animation is never updated so it remains static (in terms of the animation not the location). The GXAnimation class provides an easy mechanism for tiling the image to create sprites without having to write any new code.

protected int m_numSprites;
protected AnimatedSprite[] m_sprites = null;

One instance of the AlphaSprite class will be created to draw the alpha blended rectangle on the top layer of the frame.

protected AlphaSprite m_alphaSprite = null;

A stop watch is used to keep track of frame rate and provide the animations and sprites with timing information.

protected StopWatch sw = new StopWatch();

The test will run infinitely unless interrupted so the m_done member is provided as a means for determining when to exit the draw loop.

protected bool m_done = false;

The constructor for the ImageSpriteTest class initializes all of the sprites and loads the images used for the test.

public ImageSpriteTest(GXGraphics gx)
{
    Random rnd = new Random();

    Assembly asm = Assembly.GetExecutingAssembly();

    for (int i = 0; i < kNumZombies; i++)
    {
        m_zombies[i] = new AnimatedSprite();

        if (i == 0)
        {
            Stream strm = asm.GetManifestResourceStream
            (
              asm.GetName().Name + ".test2.bmp"
            );
            m_zombies[i].m_anim = new GXAnimation(strm, gx, 8, 0, 40,
              40, 8, 0);
        }
        else
        {
            m_zombies[i].m_anim = new GXAnimation(m_zombies[0].m_anim,
              rnd.Next(9), 8, 0);
        }

        m_zombies[i].PositionX = rnd.Next(gx.ScreenWidth / 8,
          gx.ScreenWidth - gx.ScreenWidth / 8);
        m_zombies[i].PositionY = rnd.Next(0, gx.ScreenHeight - 40);
        m_zombies[i].m_velX = rnd.Next(10,20);
        if (rnd.Next(2) < 1)
        {
            m_zombies[i].m_velX = - m_zombies[i].m_velX;
        }
        m_zombies[i].m_velY = 0;
    }

    m_alphaSprite = new AlphaSprite(Color.Fuchsia);
    m_alphaSprite.PositionX = 0;
    m_alphaSprite.PositionY = 0;
    m_alphaSprite.m_velX = rnd.Next(30,60);
    m_alphaSprite.m_velY = rnd.Next(30,60);

    m_screenImage = new GXBitmap(gx.ScreenWidth, gx.ScreenHeight);
    gx.CaptureScreen(m_screenImage);

    int imageWidth = 20;
    int imageHeight = 20;

    int numRows = gx.ScreenHeight / imageHeight;
    int numCols = gx.ScreenWidth / imageWidth;

    m_sprites = new AnimatedSprite[numRows * numCols];

    m_numSprites = 0;
    for (int r = 0; r < numRows; r++)
    {
        for (int c = 0; c < numCols; c++)
        {
            Rectangle srcRect = new Rectangle(c*imageWidth,
              r*imageHeight,imageWidth,imageHeight);
            m_sprites[m_numSprites] = new AnimatedSprite();

            if (m_numSprites == 0)
            {
                Stream strm = asm.GetManifestResourceStream
                (
                  asm.GetName().Name + ".test.bmp"
                );
                m_sprites[m_numSprites].m_anim = new GXAnimation(strm,
                  gx, numRows*numCols, 0, imageWidth, imageHeight,
                  0, 0);
            }
            else
            {
                m_sprites[m_numSprites].m_anim = new GXAnimation
                (
                  m_sprites[0].m_anim, m_numSprites, 0, 0
                );
            }

            m_sprites[m_numSprites].PositionX = srcRect.X;
            m_sprites[m_numSprites].PositionY = srcRect.Y;

            m_sprites[m_numSprites].m_velX = (float)rnd.Next(-50,50);
            m_sprites[m_numSprites].m_velY = (float)rnd.Next(-50,50);

            m_numSprites++;
        }
    }
}

The Quit method provides a means for setting m_done to exit the draw loop.

public void Quit()
{
    m_done = true;
}

The Draw function runs the test. This function loops until m_done is set to false. Each loop is a single draw frame which is responsible for updating and drawing all of the images in the test. Once the test is exited, some basic statistics about the test are displayed in a MessageBox.

public void Draw(GXGraphics gx)
{
    sw.Clear();

    for (int i = 0; i < kNumZombies; i++)
        m_zombies[i].m_anim.StartTime = sw.CurrentTime_ms();

    Int64 prevStart = 0;

    sw.Start();
    prevStart = sw.StartTime;

    UInt64 numLoops = 0;

    while (!m_done)
    {
        sw.Start();

        gx.BeginDraw();

        float delta_ms = (float)((1000 * (sw.StartTime - prevStart)) /
          sw.Freq);

        gx.DrawBitmap(0, 0, m_screenImage);

        for (int j = 0; j < m_numSprites; j++)
        {
            if (numLoops > 40)
                m_sprites[j].Update(delta_ms, 0, gx);

            gx.DrawAnimation(m_sprites[j].PositionX,
              m_sprites[j].PositionY, m_sprites[j].m_anim);
        }

        for (int i = 0; i < kNumZombies; i++)
        {
            m_zombies[i].Update(delta_ms, sw.StartTime_ms(), gx);

            gx.SetDrawModes
            (
              GXGraphics.DrawFlags.kModeSrcKeyTransparency
            );
            gx.SetSourceKey(m_zombies[i].m_anim.Image.GetPixel(0,0));

            if (m_zombies[i].m_velX < 0.0f)
                gx.SetDrawModes(GXGraphics.DrawFlags.kModeFlipX);

            gx.DrawAnimation(m_zombies[i].PositionX,
              m_zombies[i].PositionY, m_zombies[i].m_anim);

            if (m_zombies[i].m_velX < 0.0f)
                gx.ClearDrawModes(GXGraphics.DrawFlags.kModeFlipX);

            gx.ClearDrawModes
            (
              GXGraphics.DrawFlags.kModeSrcKeyTransparency
            );
        }

        m_alphaSprite.Update(delta_ms, gx);
        m_alphaSprite.Draw(gx);

        prevStart = sw.StartTime;

        gx.EndDraw();

        sw.Stop();

        Application.DoEvents();

        numLoops++;
    }

    if (sw.MeanTime_ms > 0)
        MessageBox.Show(String.Format("{0} Images, {1} Loops: {2} fps",
          m_numSprites+2+kNumZombies, numLoops, 1000 / sw.MeanTime_ms),
          "Results", MessageBoxButtons.OKCancel,MessageBoxIcon.None,
          MessageBoxDefaultButton.Button1);
    else
        MessageBox.Show(String.Format("{0} Images, {1} Loops: Inf. fps",
          m_numSprites+2+kNumZombies, numLoops), "Results",
          MessageBoxButtons.OKCancel,MessageBoxIcon.None,
          MessageBoxDefaultButton.Button1);
}

The Dispose function is provided to clean up any memory allocated for the test.

public void Dispose()
{
    if (m_screenImage != null)
        m_screenImage.Dispose();

    for (int i = 0; i < kNumZombies; i++)
        m_zombies[i].m_anim.Dispose();

    for (int i = 0; i < m_numSprites; i++)
        m_sprites[i].m_anim.Dispose();
}

The Form

The Form of the test application is fairly simple since the majority of the test code is encapsulated in the ImageSpriteTest class.

We do not want the form to interfere with the drawing of the test application so the OnPaint and OnPaintBackground methods are nullified.

protected override void OnPaint(PaintEventArgs e){}
protected override void OnPaintBackground(PaintEventArgs e){}

Allow the user to exit the application by tapping on the screen or pressing a button.

protected override void OnMouseDown(MouseEventArgs e)
{
    if (m_test != null)
        m_test.Quit();

    base.OnMouseDown (e);
}

protected override void OnKeyDown(KeyEventArgs e)
{
    if (m_test != null)
        m_test.Quit();

    base.OnKeyDown (e);
}

Of course, the Form has to maintain an instance of the test.

private ImageSpriteTest m_test = null;

The Form1_Load method initializes the Form to ensure that it is full screen and then creates an instance of the graphics engine through GXGraphics. The instance of GXGraphics is then used to create an instance of the test application which is started with a call to Draw. The finally block ensures that all memory is cleaned up properly and the application is closed down.

private void Form1_Load(object sender, System.EventArgs e)
{
    this.ControlBox = false;
    this.Menu = null;
    this.WindowState = FormWindowState.Maximized;
    this.FormBorderStyle = FormBorderStyle.None;

    GXGraphics gx = null;

    try
    {
        gx = new GXGraphics(this,
          GXGraphics.DisplayBufferModes.kDoubleBuffer);

        if (gx.Inititialized == false)
        {
            MessageBox.Show("Failed to create GXGraphics object",
              "ERROR", MessageBoxButtons.OK,
              MessageBoxIcon.Exclamation,
              MessageBoxDefaultButton.Button1);

            return;
        }

        gx.SetDrawModes(GXGraphics.DrawFlags.kModeNoBoundsChecking);

        m_test = new ImageSpriteTest(gx);
        m_test.Draw(gx);
    }
    finally
    {
        if (gx != null)
            gx.Dispose();

        if (m_test != null)
        {
            m_test.Dispose();
            m_test = null;
        }

        this.Close();
    }
}

Conclusion

This sample provides a nearly complete graphics engine written entirely in managed code, with the exception of a small set of functions contained in the GAPI API. Performance is relatively good and following all of the optimization tips found throughout this article will increase performance by 50-100%.

Show:
© 2014 Microsoft