Dancing Rectangles: Using GAPI to Create a Managed Graphics Library

.NET Compact Framework
Dancing Rectangles: Using GAPI to Create a Managed Graphics Library
 

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 the GXGraphicsLibrary namespace. This class is responsible for maintaining pixel conversion utilities.
  • GXBackBuffer.cs: This file contains the GXBackBuffer class and is internal to the GXGraphicsLibrary namespace. This class implements the functionality of a back buffer that can be used by the GXGraphics 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 the GXGraphicsLibrary. 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 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.

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 the Pixel class which are used to convert various pixel formats and System.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 and FillRect4 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.

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
解释如下代码: length_list = [list(range(32, 1025, 16)) + list(range(1056, 8193, 16))] width_list = [list(range(16, 145, 1))] length_max = max(length_list[0]) width_max = max(width_list[0]) def cut_rectangle(length, width): if length > length_max and width > width_max: rectangles = [] a_length = length_max b_length = length - length_max a_rectangle = (a_length, width) b_rectangle = (b_length, width) if b_length > length_max: a_rectangles, b_rectangles = cut_rectangle(b_length, width) rectangles.extend(a_rectangles) rectangles.extend(b_rectangles) else: rectangles.append(b_rectangle) if a_length > width_max: new_a_rectangles = [a_rectangle] while new_a_rectangles: a_rectangles = new_a_rectangles new_a_rectangles = [] for rectangle in a_rectangles: a_width = rectangle[1] if a_width > width_max: half_width = math.ceil(a_width / 2) if half_width > width_max: new_a_rectangle = (a_length, half_width) b_length = rectangle[0] b_rectangle = (b_length, a_width - half_width) if b_length > length_max: a_rectangles, b_rectangles = cut_rectangle(b_length, a_width - half_width) rectangles.extend(a_rectangles) rectangles.extend(b_rectangles) else: rectangles.append(b_rectangle) new_a_rectangles.append(new_a_rectangle) else: new_a_rectangles.append(rectangle) else: rectangles.append(rectangle) else: rectangles.append(a_rectangle) return rectangles, [] else: return [(length, width)], [] length = int(input("请输入被切割矩形的长度值:")) width = int(input("请输入被切割矩形的宽度值:")) rectangles, _ = cut_rectangle(length, width) print("全部切割后的矩形尺寸的列表:") for rectangle in rectangles: print(f"{rectangle[0]} x {rectangle[1]}")
03-10

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值