Geoff Schwab
Excell Data Corporation
December 2003
Applies to:
Microsoft® .NET Compact Framework 1.0
Microsoft Visual Studio® .NET 2003
Microsoft eMbedded Visual C++® 3.0
Summary: This article describes how to create a DLL that wraps GAPI (Game API), such that it is .NET Compact Framework compliant, and use it to create and optimize a basic graphics library in managed code. (42 printed pages)
Download Sample or GAPINetDLLs
Download GAPI
Download eMbedded Visual Tools 2002 Edition
Contents
Introduction
Creating GAPINet
The GAPI Wrapper
The Windows Wrapper
A Basic Graphics Framework
The Pixel Class
The GXGraphics Class
The GXBackBuffer Class
The GXImageBuffer Class
FillRect Optimizations
FillRect1
FillRect2
FillRect3
FillRect4
Test Applications
ImageSpriteTest
RectSpriteTest
The Form
Conclusion
Introduction
The graphics library in this sample, defined in the GXGraphicsLibrary
namespace, implements the following functionality.
- Encapsulating GAPI (gx.dll) and GAPINet (gapinet.dll)
- Drawing a filled rectangle to the screen
- Capturing the screen image to a buffer
- Drawing an image buffer to the screen
- Using back buffering to provide a smooth display
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 image and rectangle filling test applications are shown below.
Figure 1. ImageSpriteTest screenshot
Figure 2. RectSpriteTest screenshot
Subsequent articles and samples in this series will demonstrate how to load and display bitmaps, implement source and destination key transparency, create special effects with translucency, set/get individual pixels, and draw basic primitives. GAPI contains an input element for handling button presses which will be explored in a future article as well.
Developers purely interested in the managed graphics library can download the GAPINet and GAPI DLL's from the link at the top of this article, place them on the device, and skip to the section titled The GAPI Wrapper. Developers only interested in creating a GAPI wrapper DLL in native code can go straight to the section titled Creating GAPINet and skip all other sections. Developers whose only interested is in using the graphics library as an API can skip straight to the section titled Test Applications for a demonstration of how to use the library, although the library must still be built to create the appropriate DLL's.
GAPI is a small API that makes reading and writing directly from and to the device's display memory possible. The API consists of not much more than access to a pointer to the display memory, as well as information about the display properties. Detailed documentation regarding the API can be found online at the following locations.
- Game API Functions
- Game API Structures
Most of the functions provided by GAPI can be P/Invoked directly in a managed application, however there are two that return structures and therefore must be wrapped and imported in a separate DLL. This is because the .NET Compact Framework restricts P/Invoked function return values to a 32-bit maximum size.
The first major decision to make when creating a GAPI based graphics engine is whether to write the engine in native code and P/Invoke its functions, or wrap the GAPI DLL and write the engine in managed code. This article and its associated sample will minimize the amount of native code by limiting it to wrapping the GAPI DLL and implementing the graphics engine itself in managed code.
Creating GAPINet
This sample relies on GAPI and thus requires that GAPI be downloaded and unzipped to a location of your choice (use the link at the top of this article to download the latest version of GAPI). The rest of this article will assume that, after installing both of these, your GAPI directory structure is as follows:
Note: There is no build of GAPI that is officially supported on the emulator.
<GAPIROOT>/INC – contains gx.h <GAPIROOT>/ARM – contains gx.dll and gx.lib for ARM processors <GAPIROOT>/MIPS – contains gx.dll and gx.lib for MIPS processors <GAPIROOT>/SH – contains gx.dll and gx.lib for SH processors
The first step in creating a graphics engine for .NET Compact Framework-based applications is to access the full functionality of GAPI. There are only two functions which cannot be accessed using P/Invoke and this is because they return structures, which violate the P/Invoke return size maximum of 32-bits. To minimize the impact on existing applications, the GAPINet DLL project only re-implements the two necessary functions rather than wrapping the entire API. It also maintains the same function names provided as overloads to the existing ones.
Start by opening Microsoft eMbedded Visual C++ 3.0 and creating a new "WCE Dynamic-Link Library" project, with all of the CPU types that you will be supporting, named "GAPINet". Select the "A simple Windows CE DLL project" option from step 1 and press "Finish". In the file view, open the project's GAPINet.cpp file and add the following header declaration and code to the file.
#include "gx.h" extern "C" __declspec(dllexport) GXGetDisplayProperties ( GXDisplayProperties* pProps ) { *pProps = GXGetDisplayProperties(); } extern "C" __declspec(dllexport) GXGetDefaultKeys ( GXKeyList* pList, int iOptions ) { *pList = GXGetDefaultKeys(iOptions); }
Next, add the GAPI library to the project. Select Project->Settings from the main menu of eMbedded Visual C++ and then select "All Configurations" in the "Settings For:" selection list. Select the "Link" tab and enter gx.lib into the "Object/library modules" text box.
Make sure that GAPI is in your code paths by adding the include and library paths to the appropriate folders. This can be done on a per project basis but I prefer to do it once for all projects—in "Tools->Options" select the "Directories" tab and add the proper folder for each build type. To set up the include path, select "Pocket PC" in the "Platform" selection list and then select "Include Files" in the "Show directories for:" selection list and add the "INC" folder from <GAPIROOT>/INC. This must be done for each build target in the "CPUs:" selection list.
Note To add a new directory, click on the empty line below the existing paths (sometimes multiple clicks to get focus and select) and then a "…" button will appear that allows you to browse to the directory.
To set up a library path, select "Library files" in the "Show directories for:" selection list and add the appropriate path for each CPU, e.g., <GAPIROOT>/ARM for an ARM CPU build.
Target the "PocketPC" platform for all device processors to ensure compatibility with all devices running PocketPC 2000 or newer operating systems.
Note If you get an error regarding targeting the standard SDK and not being able to run on the emulator after a successful build then you will want to turn off some download settings. In the main menu select Tools->Options and select the "Download" tab of the options dialog. Uncheck all of the download options and the error should stop.
Once the project is built, place the corresponding GX.dll and GAPINet.dll file in the device's Windows directory or include them as content in the application's project.
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 is listed in its entirety below. The contents of this class are quite simply the P/Invoke implementations of the original API.
The first section of the GAPI class consists of constants used by GAPI to describe the display properties of the hardware. GX_FULLSCREEN
is provided as a flag to the GAPI initialization function and specifies that the application should have exclusive access to the display buffer. The rest of the constants describe the pixel format as reported by GXDisplayProperties.ffFormat
. The only exception being GX_FAIL
and GX_SUCCESS
which I added to help check return values from GAPI functions.
public const int GX_FAIL = 0; public const int GX_SUCCESS = 1; public const uint GX_FULLSCREEN = 0x01; public const uint kfLandscape = 0x8; public const uint kfPalette = 0x10; public const uint kfDirect = 0x20; public const uint kfDirect5550 = 0x40; public const uint kfDirect565 = 0x80; public const uint kfDirect888 = 0x100; public const uint kfDirect444 = 0x200; public const uint kfDirectInverted = 0x400;
The class GXDisplayProperties
is a map of the structure of the same name provided by GAPI to describe the display hardware of the device. The display size is specified by cxWidth
and cyHeight
with the pixel depth and color format being specified in cBPP
and ffFormat
, respectively. The pitch, or stride, of the pixels is specified in the members cbxPitch
and cbyPitch
. These values are used to determine how many bytes away the next pixel is. For example, a char*
pointer that is iterating through the display memory would need to be incremented by cbxPitch
to access the next pixel in the current row. To access the pixel below the current one, cbyPitch
would be added to the pointer. The pitch values are not necessarily linear and can be negative values so it is not safe to iterate through pixels assuming they are contiguous in memory.
public class GXDisplayProperties { public uint cxWidth; public uint cyHeight; public int cbxPitch; public int cbyPitch; public int cBPP; public uint ffFormat; }
The last section of the class consists of P/Invoke declarations of the GAPI and GAPINet functions. Because the GAPI functions are not exported with the extern "C"
declaration, the names are mangled on export and must be designated by entry points. For more information on this see the .NET Compact Framework FAQ entry on using dumpbin. The two GAPINet functions, however, are exported with their names intact and can be accessed directly. Only one of these functions is used in this class, as the other pertains to input.
[DllImport("gx.dll", EntryPoint="#1")] extern public static IntPtr GxBeginDraw(); [DllImport("gx.dll", EntryPoint="#8")] extern public static int GXOpenDisplay(IntPtr hWnd, uint dwFlags); [DllImport("gx.dll", EntryPoint="#2")] extern public static int GXCloseDisplay(); [DllImport("gx.dll", EntryPoint="#4")] extern public static int GXEndDraw(); [DllImport("gx.dll", EntryPoint="#7")] extern public static bool GXIsDisplayDRAMBuffer(); [DllImport("gx.dll", EntryPoint="#10")] extern public static int GXResume(); [DllImport("gx.dll", EntryPoint="#11")] extern public static int GXSetViewport(uint dwTop, uint dwHeight, uint dwReserved1, uint dwReserved2); [DllImport("gx.dll", EntryPoint="#12")] extern public static int GXSuspend(); [DllImport("gapinet.dll")] extern public static void GXGetDisplayProperties(GXDisplayProperties displayProps);
Note You must either build the GAPINet project included with the sample or download the DLL installation and place the appropriate processor version of GAPI.DLL and GAPINet.DLL in the device's /Windows directory.
The Windows Wrapper
The graphics library will use 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 needed for double buffering the display memory, as well as creating image buffers. This will be discussed in greater detail later in this document.
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. The P/Invoke and constant declarations in this class are listed below.
[DllImport("coredll.dll")] extern public static IntPtr GetCapture(); public const uint LMEM_FIXED = 0; public const uint LMEM_MOVEABLE = 2; public const uint LMEM_ZEROINIT = 0x0040; [DllImport("coredll.dll")] extern public static IntPtr LocalAlloc(uint uFlags, uint uBytes); [DllImport("coredll.dll")] extern public static IntPtr LocalFree(IntPtr hMem);
A Basic Graphics Framework
In order to understand the architecture of GXGraphicsLibrary
, it is important to understand how it will be used in a typical application. In normal operation, an application will contain a single draw cycle per event loop, framed by begin and end, with drawing and state settings between. A typical game loop focusing on this functionality is described by the pseudo-code below:
GXGraphics m_gx = new GXGraphics(…) m_gx.SetDrawModes(DoubleBuffer) while (!done) { m_gx.BeginDraw() // … Draw some stuff m_gx.SetDrawModes(Transparency) m_gx.SetAlphaValue(128) // … Draw more stuff m_gx.ClearDrawModes(Transparency) m_gx.EndDraw() }
Our engine will not support alpha blended transparency, that is a discussion for the next article, but I wanted to demonstrate how a typical draw cycle occurs. A well designed game engine would track draw states in the datapipe and group all objects with similar states in order to minimize state changes but again, that is a more advanced topic than this article covers.
The graphics engine in this sample is primarily made up of four files. Although it may sound intimidating, the code is relatively small and is simply broken up to make it more easily scalable for future additions to the functionality. These four files and their respective contents are described below.
- Pixel.cs: This file contains the
Pixel
class and is internal to theGXGraphicsLibrary
namespace. This class is responsible for maintaining pixel conversion utilities. - GXBackBuffer.cs: This file contains the
GXBackBuffer
class and is internal to theGXGraphicsLibrary
namespace. This class implements the functionality of a back buffer that can be used by theGXGraphics
object to create a back buffer draw surface, the details of which are described in greater detail below. - GXImageBuffer.cs: This file contains the
GXImageBuffer
class and is a public class available to users of theGXGraphicsLibrary
. This class provides functionality for creating a buffer to hold pixel information and draw it to the screen. In this sample, this class is used to capture the contents of the screen. - GXGraphics.cs: This file contains the
GXGraphics
class and is a public class available to users ofGXGraphicsLibrary
. This class acts as the interface for the user to the graphics library, providing functionality for accessing the display buffer and maintaining draw states.
In order to fully understand the sample, some concepts and terminology must be explained. The following is a list of the features implemented by the sample engine and their descriptions:
- Support for double buffering and direct drawing. Direct drawing simply executes all draw requests directly to the screen, whereas double buffering sets up a secondary buffer the same size as the screen where all drawing within a frame is executed. At the end of the frame, this buffer is copied in full to the screen. Despite the extra buffer copy, double buffering can actually be more efficient than direct drawing due to the slow nature of accessing display memory on some devices. Double buffering also provides an advantage by reducing the effects of "tearing" when drawing images. Tearing is an effect where the user can see part of the current frame being drawn with part of the previous frame still visible. This is particularly apparent when a lot of drawing is happening.
- Support for bounds checking. Our engine will be kind as to how it deals with attempted draws that are outside of the screen extents. However, some applications, such as the sample test application, already do bounds checking so this code would be redundant. Therefore, our engine's first and only draw mode will allow the user to turn off bounds checking.
- Support for both 555 and 565 pixel formats. While the sample only supports 16 bit pixels (due to some optimizations using
ushort
pointers and for simplicity of the sample), delegates are defined in thePixel
class which are used to convert various pixel formats andSystem.Drawing.Color
instances to the current target's format. - Screen capture. The engine in the sample will allow the user to provide a
GXImageBuffer
instance that will be the destination of a copy of the screen image. The contents of the screen can be captured at any point inside or outside of a draw frame. - Draw filled rectangles. The sample contains four versions of a function that will draw a filled rectangle to the current draw surface (back buffer or display). The user specifies a location, size, and color for the rectangle and the engine handles drawing it to the appropriate buffer. The four versions are iterative optimizations that I made as I profiled their performance, where
FillRect1
is the slowest andFillRect4
is the final version. GXImageBuffer
provides the user with the ability to create a buffer for displaying to the screen. In this sample, there is no access to the pixel data because it is used exclusively for screen captures, however, in subsequent articles it will be expanded to support Bitmap files and direct access of the pixels, as well as the basis for the screen and back buffers.
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. To help alleviate this, a Pixel
class was added to the GXGraphicsLibrary
namespace. In the sample, this class is defined in Pixel.cs. This class encapsulates all pixel conversion utilities, as well as defines several pixel conversion delegates. The delegates are provided so that once the GXGraphics
object has determined the display pixel format, a delegate can be assigned the function that corresponds to the target format. This saves a lot complexity and improves performance because without the delegates, a switch or if/else combination would be required to determine the display format and call the appropriate function for each pixel operation that requires a conversion. The code in this class is listed below.
public delegate ushort ColorToPixelDelegate(Color col); public delegate ushort ARGBToPixelDelegate(uint col); public delegate ushort BGRToPixelDelegate(byte blue, byte green, byte red); public static ushort BGRToPixel565(byte blue, byte green, byte red) { return (ushort)((((ushort)red >> 3) << 11) | (((ushort)green >> 2) << 5) | ((ushort)blue >> 3)); } public static ushort BGRToPixel555(byte blue, byte green, byte red) { return (ushort)((((ushort)red >> 3) << 10) | (((ushort)green >> 3) << 5) | ((ushort)blue >> 3)); } public static ushort ColorToPixel565(Color col) { return ((ushort)((((ushort)col.R >> 3) << 11) | (((ushort)col.G >> 2) << 5) | ((ushort)col.B >> 3))); } public static ushort ColorToPixel555(Color col) { return ((ushort)((((ushort)col.R >> 3) << 10) | (((ushort)col.G >> 3) << 5) | ((ushort)col.B >> 3))); } public static ushort ARGBToPixel565(uint col) { return ((ushort)(((col >> 8) & 0xf800) | ((col >> 5) & 0x07e0) | ((col >> 3) & 0x001f))); } public static ushort ARGBToPixel555(uint col) { return ((ushort)(((col >> 9) & 0x7c00) | ((col >> 6) & 0x03d0) | ((col >> 3) & 0x001f))); }
The GXGraphics Class
The GXGraphics
class is the primary interface to the graphics library. This class provides the basic functionality for accessing and drawing to the display. Because this class may allocate memory, it is derived from IDisposable
. Implementations of the various members of this class are described in detail below. For clarity, comments and debugging asserts have been removed from the code.
The first code to appear in the GXGraphics
class is an enum
which defines which buffer mode to use. This enum
is passed to the constructor and cannot be changed. The next enum
is a definition of the various draw flags stored in m_drawFlags
. Future iterations of the engine will expand on these flags to include various other draw states.
public enum DisplayBufferModes { kDoubleBuffer, kNoBuffer, } public enum DrawFlags { kModeNoBoundsChecking = 0x1 }
The library user is also granted access to some of the hardware properties which can be accessed through the following public properties. hWnd
is a handle to the owner control of the graphics library. Because the library obtains this handle itself, it keeps it internally and allows access through a property in case the owner needs it.
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 IntPtr hWnd { get { return m_hWnd; } } protected IntPtr m_hWnd = IntPtr.Zero;
There are also several properties that are used internally by the GXGraphicsLibrary
namespace. These include information about the hardware display, as well as the current draw surface. Properties named Display*
relate to the hardware display, whereas properties named DrawSurface*
relate to the current draw surface, either the display or the back buffer depending on the current draw mode.
The pitch of a pixel is essentially the number of bytes to the next pixel. The properties below are defined as "pixel pitch" rather than byte pitch so they are the number pixels to the next pixel. This might sound strange but on many devices the pixels are not ordered from left to right starting at the top corner. For example, a device that returns 240 as DisplayXPixelPitch
requires the engine to move 240 pixels forward in memory to get to the next pixel in the row as it is displayed. These properties return the value as pixel pitch as opposed to byte pitch because the calling functions use ushort
pointers rather than byte
pointers to iterate through the pixels.
internal int DisplayXPixelPitch { get { return (m_displayProps.cbxPitch >> 1); } } internal int DisplayYPixelPitch { get { return (m_displayProps.cbyPitch >> 1); } } 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; } }
The next two properties are used to access the actual display memory. As before, the draw surface is the target of current draw routines, while the display is the actual hardware display buffer.
internal IntPtr DrawSurface { get { return m_drawSurface; } } protected IntPtr m_drawSurface = IntPtr.Zero; internal IntPtr Display { get { return m_cachedDisplay; } } protected IntPtr m_cachedDisplay = IntPtr.Zero;
The graphics engine has to track the state of the current draw modes as specified by the user. For now, this only consists of the buffer mode and bounds checking but in subsequent articles we will add modes including transparency and alpha blending.
protected DisplayBufferModes m_bufferMode; protected uint m_drawFlags = 0;
Of course, GXGraphics
will store the display properties as returned by GAPI, as well as a GXBackBuffer
object which will only be instanced if the user specifies to use back buffering.
internal GAPI.GXDisplayProperties m_displayProps = new GAPI.GXDisplayProperties(); internal GXBackBuffer m_backBuffer = null;
Three delegates provided by the Pixel
class are stored locally for conversion of various pixel formats. While ColorToPixel
is the only delegate in use in this sample, the others will be used to load bitmap files in subsequent samples.
internal Pixel.ColorToPixelDelegate ColorToPixel = null; internal Pixel.ARGBToPixelDelegate ARGBToPixel = null; internal Pixel.BGRToPixelDelegate BGRToPixel = null;
Because all initialization happens in the constructor, a method for determining if any errors occurred during initialization is provided as a property. Production level code could make use of error codes rather than a simple bool
.
public bool Inititialized { get { return m_init; } } protected bool m_init = false;
Appropriately, the first method in the sample file is the constructor. The constructor is responsible for accessing the owner's handle, initializing GAPI, constructing a back buffer (if requested), initializing the pixel conversion delegates, and caching the display buffer address. As far as I know, no devices have retargetable display memory so we can reduce the overhead of calling GXBeginDraw
by only calling it once and caching the display pointer that it returns.
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); if ((m_displayProps.ffFormat & GAPI.kfDirect565) != 0) { ColorToPixel = new Pixel.ColorToPixelDelegate(Pixel.ColorToPixel565); ARGBToPixel = new Pixel.ARGBToPixelDelegate(Pixel.ARGBToPixel565); BGRToPixel = new Pixel.BGRToPixelDelegate(Pixel.BGRToPixel565); } else { ColorToPixel = new Pixel.ColorToPixelDelegate(Pixel.ColorToPixel555); ARGBToPixel = new Pixel.ARGBToPixelDelegate(Pixel.ARGBToPixel555); BGRToPixel = new Pixel.BGRToPixelDelegate(Pixel.BGRToPixel555); } m_bufferMode = bufferMode; if (m_bufferMode == DisplayBufferModes.kDoubleBuffer) { m_backBuffer = new GXBackBuffer(this); if (m_backBuffer == null || m_backBuffer.Initialized == false) return; } m_cachedDisplay = GAPI.GxBeginDraw(); if (m_cachedDisplay == IntPtr.Zero) return; m_init = true; }
The next suite of methods provides access to the draw modes set by the user. CheckDrawModes
determines whether all of the specified modes are set, SetDrawModes
sets all of the specified modes, and ClearDrawModes
clears all of the specified flags. The flags passed to these functions are specified in the enum DrawFlags
.
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; }
BeginDraw
is the function the signals the start of a draw cycle. GXGraphics
maintains a pointer to the current draw surface which is only valid inside of a framed draw cycle, i.e., between BeginDraw
and EndDraw
. This pointer is stored internally as m_drawSurface
and gets set in the BeginDraw
function. Because we cached the hardware display memory pointer in m_cachedDisplay
in the constructor, there is no need to call BeginDraw
at this point regardless of the buffer mode. Notice that the current display pointer depends on the buffer mode.
public void BeginDraw() { if (m_bufferMode == DisplayBufferModes.kNoBuffer) m_drawSurface = m_cachedDisplay; else m_drawSurface = m_backBuffer.Buffer; }
The EndDraw
method has a bit more work to do than BeginDraw
. If the mode is set to double buffering then this is the point where the buffer will be flipped to the screen. Because some devices, may need to do some work during EndDraw
to get the display memory to the actual hardware display, a call to GXEndDraw
is required at this point. Also, we set the current draw surface to an invalid pointer value since we are no long in a framed draw cycle.
public void EndDraw() { if (m_bufferMode == DisplayBufferModes.kDoubleBuffer) m_backBuffer.Flip(this); GAPI.GXEndDraw(); m_drawSurface = IntPtr.Zero; }
CaptureScreen
copies the contents of the display to the specified GXImageBuffer
instance. It is possible to provide a null value for the GXImageBuffer
reference. This will signal to the method that the user wants the back buffer to be filled with the screen contents.
public void CaptureScreen(GXImageBuffer image) { if (image == null) { if (this.m_bufferMode == DisplayBufferModes.kDoubleBuffer) ScreenToBuffer(m_backBuffer.Buffer); } else { ScreenToBuffer(image.Buffer); } }
The ScreenToBuffer
method is used internally to copy the contents of the screen to a generic buffer. It is up to the calling function to verify that the buffer is the proper size. This function is optimized so it may be best to ignore how it works until you read the FillRect Optimization section below. In short, it loops through each row and column of pixels and copies the screen pixels to the buffer. The primary optimizations that make it a bit difficult to read are the replacement of for loops with while loops and the unrolling of the loops to write 8 pixels per iteration.
protected void ScreenToBuffer(IntPtr buffer) { unsafe { int xPitch = m_displayProps.cbxPitch >> 1; int yPitch = m_displayProps.cbyPitch >> 1; int xExtra = (ScreenWidth % 8) * xPitch; int xOffset = ScreenWidth * xPitch - xExtra; ushort* pBackBuffer = (ushort*)buffer; ushort* pScreen = (ushort*)m_cachedDisplay; ushort* pLastLine = pScreen + ScreenHeight * yPitch; while (pScreen != pLastLine) { ushort* pCurScreenPixel = pScreen; ushort* pLastPixel = pCurScreenPixel + xOffset; while (pCurScreenPixel != pLastPixel) { *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; } pLastPixel += xExtra; while (pCurScreenPixel != pLastPixel) { *pBackBuffer = *pCurScreenPixel; pCurScreenPixel += xPitch; pBackBuffer++; } pScreen += yPitch; } } }
The Dispose
function closes GAPI and cleans up the back buffer if it was allocated in the constructor.
public void Dispose() { GAPI.GXCloseDisplay(); if (m_backBuffer != null) m_backBuffer.Dispose(); }
The remaining methods implement drawing routines for drawing a filled rectangle to the screen. These functions are described in detail in the Optimizing FillRect section so their code is omitted from this section.
The GXBackBuffer Class
The GXBackBuffer
class is internal to the GXGraphicsLibrary
namespace and is defined in GXBackBuffer.cs. This class is responsible for maintaining a back buffer and implementing all functionality associated with it. It has dependencies on GXGraphics
, in that it requires some methods for determining display properties and accessing the display.
The class implements only two properties. The first is public and used to determine if the constructor initialized properly. The other property is local and used to access the actual buffer.
public bool Initialized { get { return m_init; } } protected bool m_init = false; public IntPtr Buffer { get { return m_buffer; } } protected IntPtr m_buffer = IntPtr.Zero;
The constructor for GXBackBuffer
allocates the memory for the buffer. The buffer size is determined by the instance of GXGraphics
passed to the constructor.
public GXBackBuffer(GXGraphics gx) { m_buffer = Windows.LocalAlloc(Windows.LMEM_FIXED, (uint)(gx.ScreenWidth * gx.ScreenHeight * gx.PixelByteDepth)); if (m_buffer == IntPtr.Zero) return; m_init = true; }
The Flip
function copies the buffer to the display. This requires an instance of a GXGraphics
object for accessing display properties and memory. This function is optimized in the same manner as described for GXGraphics.ScreenToBuffer
.
public void Flip(GXGraphics gx) { unsafe { int xPitch = gx.DisplayXPixelPitch; int yPitch = gx.DisplayYPixelPitch; int xExtra = (gx.ScreenWidth % 8) * xPitch; int xOffset = gx.ScreenWidth * xPitch - xExtra; ushort* pCurLine = (ushort*)gx.Display; ushort* pPixels = (ushort*)m_buffer; ushort* pSource = pPixels; ushort* pLastLine = pCurLine + gx.ScreenHeight * yPitch; while (pCurLine != pLastLine) { ushort* pCurPixel = pCurLine; ushort* pLastPixel = pCurPixel + xOffset; while (pCurPixel != pLastPixel) { *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; } pLastPixel += xExtra; while (pCurPixel != pLastPixel) { *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; } pCurLine += yPitch; } } }
The final function in the class is Dispose
. This function frees the buffer that was allocated for the back buffer.
public void Dispose() { if (m_buffer != IntPtr.Zero) Windows.LocalFree(m_buffer); m_buffer = IntPtr.Zero; }
The GXImageBuffer Class
The GXImageBuffer
class stores a buffer that can be used to draw an image to the screen. In this sample, this class is used to capture the screen and then redraw bits of it as moving sprites. In future samples, this class will be the basis for the back buffer, screen buffer, loading and displaying bitmaps, and allow users to access and modify pixels directly.
As a memory optimization, two constructors are provided by the class. The first will allocate a buffer to be used for drawing operations, while the second allows the image to be based on another instance of GXImageBuffer
. In the latter case no buffer is allocated, instead the buffer pointer is directed to a region of an already existing buffer. It is therefore up to the calling application to ensure that any image buffers created as copies are only valid as long as the original.
In order to keep track of whether a buffer was allocated or not, the m_allocated
member is set in the constructor.
protected bool m_allocated;
The m_srcYOffset
member is needed for image buffers that are copies of original image buffers. Consider the case where the image is a 16x16 region of an original 32x32 image. If the buffer points into the original 32x32 image then how would the methods of the class know how to increment the point to get to the next row of pixels when they know nothing about the original image? Thanks to m_srcYOffset
, the methods do know how far to offset into the original buffer to get to the next row of pixels. This value is actually the difference between the right edge of the original image and the right edge of the copy.
protected int m_srcYOffset = 0;
The class also provides properties for accessing the size of the image and the data buffer, the latter of which is internal only.
public int Width { get { return m_width; } } protected int m_width; public int Height { get { return m_height; } } protected int m_height; internal IntPtr Buffer { get { return m_buffer; } } protected IntPtr m_buffer = IntPtr.Zero;
The final property of the class is used to determine whether the constructor was able to initialize the instance properly.
public bool Initialized { get { return m_init; } } protected bool m_init = false;
The first of the constructors requires information regarding the size of the image buffer to be created, as well as an instance to a GXGraphics
object used to determine the pixel byte depth of the target draw surface. The constructor allocates memory for the buffer and initializes all members.
public GXImageBuffer(int width, int height, GXGraphics gx) { m_width = width; m_height = height; if (m_width < 0 || m_height < 0) return; m_buffer = Windows.LocalAlloc(Windows.LMEM_FIXED, (uint)(width * height * gx.PixelByteDepth)); if (m_buffer == IntPtr.Zero) return; m_allocated = true; m_init = true; m_srcYOffset = 0; }
The second constructor is used to create a copy of an original image buffer. The parameters passed to this constructor are the region of the original image that will constitute the copy, an instance of an image buffer to be used as the original, as well as a GXGraphics
object used to determine the display format. This constructor will initialize all members and set the buffer pointer to the starting offset in the original as specified by the region.
public GXImageBuffer(Rectangle region, GXImageBuffer image, GXGraphics gx) { if (region.X < 0) region.X = 0; if (region.Y < 0) region.Y = 0; if (region.Right >= image.Width) region.Width = image.Width - region.X; if (region.Bottom >= image.Height) region.Height = image.Height - region.Y; if (region.X >= image.Width || region.Y >= image.Height || region.Right < 0 || region.Bottom < 0) return; m_width = region.Width; m_height = region.Height; m_buffer = (IntPtr)(image.Buffer.ToInt32() + (region.X * gx.PixelByteDepth) + (region.Y * image.Width * gx.PixelByteDepth)); m_allocated = false; m_srcYOffset = (image.Width - m_width) * gx.DrawSurfaceXPixelPitch; }
The Draw
method of GXImageBuffer
draws the image buffer to the current draw surface provided by GXGraphics
. If you are thinking the loop that copies pixels in this procedure looks redundant then you are correct. In the next article, we will examine how to leverage the GXImageBuffer
class to use it as a representation of the screen and back buffer so that the buffer copy function is centralized. For now, it is explicitly defined for each case since the sources and destinations have differing formats.
public void Draw(int x, int y, Rectangle drawRegion, GXGraphics gx) { if (gx.CheckDrawModes(GXGraphics.DrawFlags.kModeNoBoundsChecking)) { if (drawRegion.Right > gx.ScreenWidth) drawRegion.Width = gx.ScreenWidth - drawRegion.X; if (drawRegion.Bottom > gx.ScreenHeight) drawRegion.Height = gx.ScreenHeight - drawRegion.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 >= gx.ScreenWidth || drawRegion.Y >= gx.ScreenHeight || drawRegion.Right < 0 || drawRegion.Bottom < 0 || drawRegion.Width < 0 || drawRegion.Height < 0) { return; } } unsafe { int xPitch = gx.DrawSurfaceXPixelPitch; int yPitch = gx.DrawSurfaceYPixelPitch; int xExtra = (drawRegion.Width % 8) * xPitch; int xOffset = drawRegion.Width * xPitch - xExtra; ushort* pCurLine = (ushort*)gx.DrawSurface + (x * xPitch) + (y * yPitch); ushort* pPixels = (ushort*)m_buffer + (drawRegion.X * gx.PixelByteDepth) + (drawRegion.Y * gx.PixelByteDepth); ushort* pSource = pPixels; ushort* pLastLine = pCurLine + drawRegion.Height * yPitch; while (pCurLine != pLastLine) { ushort* pCurPixel = pCurLine; ushort* pLastPixel = pCurPixel + xOffset; while (pCurPixel != pLastPixel) { *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; } pLastPixel += xExtra; while (pCurPixel != pLastPixel) { *pCurPixel = *pSource; pCurPixel += xPitch; pSource++; } pCurLine += yPitch; pSource += m_srcYOffset; } } }
The final method for this class is the Dispose
method. This method frees the buffer if it was allocated by this instance of the GXImageBuffer
. Remember that the image buffer can be a copy of another buffer. If the buffer is a copy then it shares the same memory as the original so we do not want to try and free it.
public void Dispose() { if (m_allocated && m_buffer != IntPtr.Zero) Windows.LocalFree(m_buffer); m_buffer = IntPtr.Zero; }
FillRect Optimizations
The FillRect
function fills a region of the screen or back buffer with a specified color. The creation and optimization of this function went through various levels of iterative enhancement so I felt it was worthwhile to provide four versions of the function which demonstrate the various levels of optimization. I always start with a non-optimized, simplest version of a function and then optimize iteratively as I go. The optimization steps and initial test results for each function are outlined below. The test drew a single full screen rectangle.
Note Code for all versions of this function share a common bounds checking routine which was left out for brevity's sake.
All profiling of the following methods was done on a Compaq iPAQ H3600 series Pocket PC with a 206MHz ARM SA1110 processor and 32 MB of RAM. This device was running the .NET Compact Framework Service Pack 2 Beta.
FillRect1
This is the first and simplest implementation of the rectangle drawing function. This version uses for
loops to loop through each row, and each column, writing pixel by pixel. Initial testing showed this function to run at 13 frames per second (fps). The code for this function is listed below.
This code starts by accessing a pointer to the current draw surface (either the screen or back buffer) as a ushort*
since we assume pixels to be 16 bits. This pointer is then offset by the starting location of the fill. Once the start location is determined, the code can draw each row of pixels by looping through the number of columns, copying the color to the contents of the destination pointer, and updating the position of the destination pointer by one pixel. Once a row is complete, the pointer that designated the start of the line can be updated to point to the next line and the column iteration is repeated.
public void FillRect1(Rectangle fillRegion, Color fillColor) { ushort rgb = ColorToPixel(fillColor); unsafe { ushort* pCurLine = (ushort*)this.DrawSurface.Buffer + fillRegion.X * DrawSurfaceXPixelPitch + fillRegion.Y * DrawSurfaceYPixelPitch; for (int row = 0; row < fillRegion.Height; row++) { ushort* pCurPixel = pCurLine; for (int col = 0; col < fillRegion.Width; col++) { *pCurPixel = rgb; pCurPixel += DrawSurfaceXPixelPitch; } pCurLine += DrawSurfaceYPixelPitch; } } }
FillRect2
The only optimization made to this function was to cache the variables that are accessed in the loop comparisons and pixel pointer operations. This had a significant affect on performance because some of the properties being accessed actually performed operations, as well as structure dereferencing to calculate values. One would also hope that by caching the variables, the compiler may allocate registers to these variables. FillRect2
proved to be over 6 times faster than FillRect1
and ran at 83 fps.
The code for FillRect2
is very similar to that of FillRect1
. The one important difference being that the calls to DrawSufaceYPixelPitch
, DrawSufaceXPixelPitch
, fillRegion.Width
, and fillRegion.Height
have all been cached in local variables yPitch
, xPitch
, colMax
, and rowMax
respectively.
public void FillRect2(Rectangle fillRegion, Color fillColor) { ushort rgb = ColorToPixel(fillColor); unsafe { int xPitch = DrawSurfaceXPixelPitch; int yPitch = DrawSurfaceYPixelPitch; int rowMax = fillRegion.Height; int colMax = fillRegion.Width; ushort* pCurLine = (ushort*)this.DrawSurface.Buffer + fillRegion.X * xPitch + fillRegion.Y * yPitch; for (int row = 0; row < rowMax; row++) { ushort* pCurPixel = pCurLine; for (int col = 0; col < colMax; col++) { *pCurPixel = rgb; pCurPixel += xPitch; } pCurLine += yPitch; } } }
FillRect3
FillRect3
takes FillRect2
one step further by eliminating the for
loop comparisons and counter incrementing by replacing the for
loops with while
loops. In order to properly use the while
loops, a start pointer and end pointer are initialized and then compared to determine when to exit. FillRect2
tested at almost 8 times faster than FillRect1
and ran at 100 fps.
The code for FillRect3
eliminates the need for determining the number of rows and columns through which to iterate by using pointer values and while
loops. This is an advantage in performance because the overhead of maintaining a loop counter, incrementing it, and checking it is replaced with one comparison of two pointers. To achieve this, the location of the pointer after the last pixel fill is calculated and the current location is compared against that. In this version of the function, the pitches are still cached, as is the offset in the x direction that is used to determine when the pixel pointer is done with a single row.
public void FillRect3(Rectangle fillRegion, Color fillColor) { ushort rgb = ColorToPixel(fillColor); unsafe { int xPitch = DrawSurfaceXPixelPitch; int yPitch = DrawSurfaceYPixelPitch; ushort* pCurLine = (ushort*)this.DrawSurface.Buffer + 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) { *pCurPixel = rgb; pCurPixel += xPitch; } pCurLine += yPitch; } } }
FillRect4
The final version of the function is FillRect4
which improves on FillRect3
by unrolling the inner while loop. This is achieved by writing 8 pixels per loop instead of 1. Unfortunately, it is not possible to expand on this by writing 32 bit values (use uint*
for pixels instead of ushort*
) because the x direction pitch of a pixel is not guaranteed to be 1. This function improved the performance of FillRect1
by almost 13 times and ran at 166 fps.
Because the code segments in FillRect3
and FillRect4
use pointer comparisons to determine when they are done drawing, it does not matter how many pixels are drawn in the inner loop as long as the pointer is updated and checked before running over the boundary. To ensure this overrun condition does not occur, as well as account for rectangles that are not even multiples of 8, the inner loop is modified to only draw to the nearest multiple of 8 less than or equal to the size of the rectangle e.g., if the rectangle is 18 pixels wide then the first inner loop will draw to 16. Anything left over is then drawn as individual pixels in a second inner loop.
Several factors must be considered when determining how far to unroll the loops. Unrolling the loops too far can actually degrade performance. Two factors come immediately to mind, first is the size of the rectangles that an application draws. For example, if the loop is unrolled to draw 32 pixels per iteration but the application typically draws rectangles that are only 20 pixels wide then there will be no performance gain at all, as the entire draw will have to default to the single pixel drawing loop. A second factor is code size. While it is not likely to have an affect on your code, it is possible that by increasing the size of the code you could hit low memory situations where JIT'd code is pitched thus causing the JIT to recompile code between draw operations.
public void FillRect4(Rectangle fillRegion, Color fillColor) { ushort rgb = ColorToPixel(fillColor); unsafe { int xPitch = DrawSurfaceXPixelPitch; int yPitch = DrawSurfaceYPixelPitch; ushort* pCurLine = (ushort*)this.DrawSurface.Buffer + fillRegion.X * xPitch + fillRegion.Y * yPitch; int xExtra = (fillRegion.Width % 8) * xPitch; int xOffset = fillRegion.Width * xPitch - xExtra; ushort* pLastLine = pCurLine + (fillRegion.Height) * yPitch; while (pCurLine != pLastLine) { ushort* pCurPixel = pCurLine; ushort* pLastPixel = pCurPixel + xOffset; while (pCurPixel != pLastPixel) { *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; *pCurPixel = rgb; pCurPixel += xPitch; } pLastPixel += xExtra; while (pCurPixel != pLastPixel) { *pCurPixel = rgb; pCurPixel += xPitch; } pCurLine += yPitch; } } }
Test Applications
Two source files are provided for building applications to test the functionality of the graphics library. The first will run a test of the GXImageBuffer
class by capturing the screen into a buffer and then creating randomly sized rectangles that point to random locations within the screen image. These images are created as sprites, a common term in the game industry for a moving image, which move at a random velocity until they collide with the edge of the screen and bounce back in the opposite direction. The second test verifies the optimization assumptions made in the FillRect
methods. This test is the same as the first test only it uses filled rectangles instead of images.
The test application is available in both C# and VB and is named TestApp and TestAppVB respectively. In the sample, the bulk of the test code is found in source files separate from the form. The image test is located in ImageSpriteTest.cs/vb, while the rectangle test code is in RectSpriteTest.cs/vb.
In order to properly build the test applications, they need to know that they are dependant on the graphics library. To accomplish this in the sample solution, the test projects contain a reference to the GXGraphics project. The two easiest methods for creating a reference are to either
- select the TestApp project in the solution explorer, right-click and select "Add Reference" or
- select the TestApp project in the solution explorer and select Project->Add Reference… from the main menu.
From the Add Reference dialog select the Projects tab and double-click on GXGraphics. This will cause GXGraphics to appear in the Selected Components window. Press OK to add the project as a reference. To see how this affects the solution, select Project->Project Dependencies from the main menu and examine the dependencies and build order for each project.
The test applications make use of the QueryPerformanceCount
API for precision profiling in the StopWatch
class. More information on this topic can be found in the article, HOWTO: Use a Performance Counter.
ImageSpriteTest
The class ImageSpriteTest
creates an array of sprites with an associated GXImageBuffer
and iterates through them, updating their location and blitting (derived from bit-block-transfer, simply means copying) them to the screen every frame. In order to track the sprites, this class maintains an internal representation of a sprite class.
The ImageSprite
class maintains an instance of GXImageBuffer
which represents the piece of the screen that it draws, as two dimensional velocity and location information. The location is redundantly held as an integer and a floating point value in the sprite. The floating point position is necessary because it is possible to move less than one pixel per draw frame – especially with slow objects and a fast frame-rate. If the position were only kept as an integer, the preceding case would cause the position to be truncated and therefore never result in any movement, i.e., pixel location x = 1.2 is still pixel location 1. The integer location could be eliminated and the floating point location could be cast every time it is used but this would be inefficient since casting from floats
to ints
is fairly expensive. Therefore, the floating point location is cast once, immediately after being calculated, to an integer location and the integer location is accessed from then on.
protected class ImageSprite { public GXImageBuffer m_image = null; public float m_velX; public float m_velY; public float m_curX; public float m_curY; public int m_locX; public int m_locY; }
The class contains several members relating to maintaining the list of sprites, as well as the original screen capture image and the stop watch timer. The member m_screenImage
is an instance of an image buffer that stores the capture of the screen at the time the test is started. m_sprites
is an array of ImageSprites
that will be drawn and updated every frame. m_sw
is an instance of the StopWatch
class. This instance will be used for tracking the frame-rate of the test, but more importantly, will also track the time of each frame in milliseconds which will be used to determine the frame-rate independent movement of each sprite. m_numSprites
is clearly the number sprites in the array, as specified in the constructor.
protected GXImageBuffer m_screenImage = null; protected ImageSprite[] m_sprites = null; protected StopWatch sw = new StopWatch(); protected int m_numSprites = 0;
The constructor for the test initializes the sprite array with randomly sized and located sprites with random velocities. Initial parameters for testing are supplied by the calling routine to the constructor.
public ImageSpriteTest(int numSprites, int maxWidth, int maxHeight, int maxVel, GXGraphics gx) { m_numSprites = numSprites; Random r = new Random(987654321); m_sprites = new ImageSprite[m_numSprites]; m_screenImage = new GXImageBuffer(gx.ScreenWidth, gx.ScreenHeight, 1, gx.ScreenWidth, gx); gx.CaptureScreen(m_screenImage); Rectangle imageRect = new Rectangle(0,0,0,0); for (int i = 0; i < numSprites; i++) { m_sprites[i] = new ImageSprite(); ImageSprite imSpr = m_sprites[i]; imageRect.Width = r.Next(maxWidth - 1) + 1; imageRect.Height = r.Next(maxHeight - 1) + 1; imageRect.X = r.Next(gx.ScreenWidth - imageRect.Width); imageRect.Y = r.Next(gx.ScreenHeight - imageRect.Height); imSpr.m_image = new GXImageBuffer(imageRect, m_screenImage, gx); imSpr.m_velX = (float)r.Next(maxVel * 2) - maxVel; imSpr.m_velY = (float)r.Next(maxVel * 2) - maxVel; imSpr.m_curX = (float)imageRect.X; imSpr.m_curY = (float)imageRect.Y; imSpr.m_locX = (int)imSpr.m_curX; imSpr.m_locY = (int)imSpr.m_curY; } }
The Draw
method in the ImageSpriteTest
class is responsible for running the test. This function loops 100 times, updating and drawing the sprites every frame. Once the test is complete, a MessageBox
is displayed with the frame-rate results as the average of each frame. The result of this MessageBox
is propagated to the calling procedure so it can determine whether to continue with testing or exit the application.
public DialogResult Draw(GXGraphics gx) { Rectangle backRect = new Rectangle(0,0,gx.ScreenWidth,gx.ScreenHeight); Int64 prevStart = 0; m_sw.Clear(); for (int i = 0; i < 100; i++) { m_sw.Start(); gx.BeginDraw(); float delta_ms = (float)((1000 * (m_sw.StartTime - prevStart)) / m_sw.Freq); m_screenImage.Draw(0, 0, backRect, gx); Rectangle drawRegion = new Rectangle(0,0,0,0); for (int j = 0; j < m_numSprites; j++) { ImageSprite s = m_sprites[j]; drawRegion.Width = s.m_image.Width; drawRegion.Height = s.m_image.Height; s.m_image.Draw(s.m_locX, s.m_locY, drawRegion, gx); if (i > 0) { s.m_curX += (delta_ms * s.m_velX / 1000.0f); s.m_curY += (delta_ms * s.m_velY / 1000.0f); s.m_locX = (int)s.m_curX; s.m_locY = (int)s.m_curY; if (s.m_locX + s.m_image.Width > gx.ScreenWidth) { s.m_locX = gx.ScreenWidth - s.m_image.Width; s.m_velX = - s.m_velX; } else if (s.m_locX < 0) { s.m_locX = 0; s.m_velX = - s.m_velX; } if (s.m_locY + s.m_image.Height > gx.ScreenHeight) { s.m_locY = gx.ScreenHeight - s.m_image.Height; s.m_velY = - s.m_velY; } else if (s.m_locY < 0) { s.m_locY = 0; s.m_velY = - s.m_velY; } } } prevStart = m_sw.StartTime; gx.EndDraw(); m_sw.Stop(); } if (m_sw.MeanTime_ms > 0) return MessageBox.Show(String.Format("Images: {0} fps", 1000 / m_sw.MeanTime_ms), "Results", MessageBoxButtons.OKCancel,MessageBoxIcon.None, MessageBoxDefaultButton.Button1); return MessageBox.Show("Images: Inf. fps", "Results", MessageBoxButtons.OKCancel,MessageBoxIcon.None, MessageBoxDefaultButton.Button1); }
The final function in ImageSpriteTest
is the Dispose
method which frees any memory allocated by the screen image.
public void Dispose() { if (m_screenImage != null) m_screenImage.Dispose(); }
RectSpriteTest
The class RectSpriteTest
creates an array of sprites with an associated Rectangle
and iterates through them, updating their location and drawing them to the screen every frame. In order to track the sprites, this class maintains an internal representation of a sprite class.
The code for this class will not be displayed in full, as it is very similar to that of the ImageSpriteTest
class, with the primary difference being the structure of the sprite class and the addition of a delegate. Instead of a GXImageBuffer
instance, the RectSprite
class maintains a Rectangle
object representing the rectangle of the screen to be drawn. Because the internal representation of the Rectangle
object has an X,Y component, there is no need to have an additional location as there is in the ImageSprite
class.
A delegate was added to the RectSpriteTest
class in order to easily facilitate the testing of all four FillRect
functions. The delegate, named m_fillRect
, is initialized in the function SetFillRectVersion
by passing an enumerated value representing the function that is to be tested.
public enum FillRectVersion { kFillRect1, kFillRect2, kFillRect3, kFillRect4 } protected delegate void FillRectDelegate(Rectangle fillRegion, Color fillColor); protected FillRectDelegate m_fillRect = null; public void SetFillRectVersion(FillRectVersion ver, GXGraphics gx) { m_ver = ver; switch (ver) { case FillRectVersion.kFillRect1: m_fillRect = new FillRectDelegate(gx.FillRect1); break; case FillRectVersion.kFillRect2: m_fillRect = new FillRectDelegate(gx.FillRect2); break; case FillRectVersion.kFillRect3: m_fillRect = new FillRectDelegate(gx.FillRect3); break; case FillRectVersion.kFillRect4: m_fillRect = new FillRectDelegate(gx.FillRect4); break; } }
The Form
The Form
used for testing is a simple form with a minimal amount of interaction with the graphics library since most of the interaction is done in the two test classes. The form code overrides the paint methods so that the form is not drawing during graphics operations.
protected override void OnPaint(PaintEventArgs e){} protected override void OnPaintBackground(PaintEventArgs e){}
All of the test code is located in the Form1_Load
function. This code first formats the form so that it will not interfere with drawing.
this.ControlBox = false; this.Menu = null; this.WindowState = FormWindowState.Maximized; this.FormBorderStyle = FormBorderStyle.None;
The code declares the GXGraphics
and test class instances at the top of the function so that they can be initialized in a try/finally
block to ensure that any allocated resources are properly freed when exiting the test procedures.
GXGraphics gx = null; ImageSpriteTest test1 = null; RectSpriteTest test2 = null;
The following initialization code creates a GXGraphics
instance which utilizes double buffering and sets the draw mode to not do bounds checking since the test code already handles this.
gx = new GXGraphics(this, GXGraphics.DisplayBufferModes.kDoubleBuffer); gx.SetDrawModes(GXGraphics.DrawFlags.kModeNoBoundsChecking);
In the sample, the test attempts to push the limits of the engine be testing it with 400 sprites of sizes varying from 1 to 40 pixels in height and width. The tests are then run with calls to their corresponding Draw methods. If the result of the test is the user pressing the cancel button then a given test will return from the Form1_Load
method. This will result in the finally branch being executed.
If the user presses OK after the results of each test are displayed then the testing will continue for each version of FillRect
. The version is changed with a call to the SetFillRectVersion
method.
int numSprites = 400; int maxVel = 75; int maxWidth = 40; int maxHeight = 40; test1 = new ImageSpriteTest(numSprites, maxWidth, maxHeight, maxVel, gx); if (test1.Draw(gx) == DialogResult.Cancel) return; test2 = new RectSpriteTest(RectSpriteTest.FillRectVersion.kFillRect1, numSprites, maxWidth, maxHeight, maxVel, gx); if (test2.Draw(gx) == DialogResult.Cancel) return; test2.SetFillRectVersion(RectSpriteTest.FillRectVersion.kFillRect2, gx); if (test2.Draw(gx) == DialogResult.Cancel) return; test2.SetFillRectVersion(RectSpriteTest.FillRectVersion.kFillRect3, gx); if (test2.Draw(gx) == DialogResult.Cancel) return; test2.SetFillRectVersion(RectSpriteTest.FillRectVersion.kFillRect4, gx); if (test2.Draw(gx) == DialogResult.Cancel) return;
The finally code branch of Form1_Load
frees any allocated resources and exits the application.
finally { if (gx != null) gx.Dispose(); if (test1 != null) test1.Dispose(); this.Close(); }
Conclusion
The sample provided supplies developers with a starting point for creating a robust and richly featured graphics engine in managed code that works within the .NET Compact Framework. Future articles will expand on this engine providing bitmap support, transparency, translucency, animation, pixel access, primitive rendering, and an input library.