Introduction
Over the last couple days, I have been researching managed 2D graphics APIs for the Windows Mobile platform. There are a lot of choices out there. Strictly for 2D, we have GDI, GAPI, DirectDraw, and the GetRawFramebuffer
function. On the 3D side, we have OpenGL ES and Direct3D. Each of these APIs has its pros and cons.
OpenGL ES (Embedded Systems)
OpenGL ES is a lightweight subset of OpenGL for small devices. There is no official managed OpenGL ES wrapper, but Koushik Dutta has created a managed wrapper which you can download off his blog. In order to get decent performance out of OpenGL ES, your hardware needs drivers. There is a software driver though.
Direct3D Mobile
Managed Direct3D has been in Compact Framework since 2.0. If your target device does not have hardware support with an accompanying driver (htcclassaction), then forget about Direct3D. The software driver is incredibly slow and is not really good for anything except debugging in the emulator.
You can use the code from this link to see if your device has a Direct3D driver.
GDI
GDI stands for Graphic Device Interface and is the primary API for 2D graphics in windows.
GAPI (Game API)
GAPI is an old API for CE that Microsoft created to ease game development. You can find a managed wrapper for GAPI on MSDN here.
GAPI is officially deprecated. All documentation has been removed from MSDN (except GXInput). You will still find GX.dll on your phone though, and the graphics related functions still exist. Microsoft is going to remove it in a future version.
Using an API that was going away didn't jive with me. The zombies sample application did not run too fast on my phone either.
DirectDraw
DirectDraw is a 2D API from Microsoft. DirectDraw gives us direct access to video memory, hardware blting, and flipping. Microsoft is pushing DirectDraw as the recommended API for fast 2Dd graphics in CE now that GAPI is being deprecated (see the "Have you migrated to DirectDraw yet?" link at the bottom of the page). Unfortunately, there is no managed wrapper for DirectDraw in the Compact Framework. Today, we will create one.
DirectDraw wrapper
DirectDraw is implemented as a COM library. The initial version of the Compact Framework did not support COM interop, but it was added in version 2.0. Using DirectDraw from managed code should be extremely simple. We should be able to run tlbimp to generate the stubs we need for the interop, but unfortunately, I had some issues, so I manually wrote the interop code.
Initializing DirectDraw
The first thing we need to is create an instance of the main DirectDraw object. We will use the DirectDrawCreate
method to create this object. Even though DirectDraw is implemented as a COM library, we need to call this method to pass in the device ID that we get using DirectDrawEnumerateEx
, or pass in null
for the default device. We will also need to pass in a pointer to a IDirectDraw
interface. The last argument is not used, and we can ignore and pass in null
.
[DllImport("ddraw.dll", CallingConvention = CallingConvention.Winapi)]
public static extern uint DirectDrawCreate(IntPtr lpGUID,
out IDirectDraw lplpDD, IntPtr pUnkOuter);
[Guid("9c59509a-39bd-11d1-8c4a-00c04fd930c5"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IDirectDraw
{
uint CreateClipper(uint dwFlags, out IDirectDrawClipper lplpDDClipper,
IntPtr pUnkOuter);
uint CreatePalette(CreatePaletteFlags dwFlags,
ref tagPALETTEENTRY lpDDColorArray,
out IDirectDrawPalette lplpDDPalette,
IntPtr pUnkOuter);
uint CreateSurface(ref DDSURFACEDESC lpDDSurfaceDesc,
out IDDrawSurface lplpDDSurface, IntPtr pUnkOuter);
uint EnumDisplayModes(uint dwFlags, ref DDSURFACEDESC lpDDSurfaceDesc2,
IntPtr lpContext, IntPtr lpEnumModesCallback);
uint EnumSurfaces(uint dwFlags, ref DDSURFACEDESC lpDDSD2,
IntPtr lpContext, IntPtr lpEnumSurfacesCallback);
uint FlipToGDISurface();
uint GetCaps(out DDCAPS halCaps, out DDCAPS helCaps);
uint GetDisplayMode(out DDSURFACEDESC lpDDSurfaceDesc2);
uint GetFourCCCodes(ref int lpNumCodes, IntPtr lpCodes);
uint GetGDISurface(out IDDrawSurface lplpGDIDDSSurface4);
uint GetMonitorFrequency(ref uint lpdwFrequency);
uint GetScanLine(ref uint lpdwScanLine);
uint GetVerticalBlankStatus(ref bool lpbIsInVB);
uint RestoreDisplayMode();
uint SetCooperativeLevel(IntPtr hWnd, CooperativeFlags flags);
uint SetDisplayMode(uint dwWidth, uint dwHeight, uint dwBPP,
uint dwRefreshRate, SetDisplayModeFlags dwFlags);
uint WaitForVerticalBlank(WaitForVBlankFlags flags, IntPtr handle);
uint GetAvailableVidMem(ref DDSCAPS lpDDSCaps2, ref uint lpdwTotal, ref uint lpdwFree);
uint GetSurfaceFromDC(IntPtr hdc, out IDDrawSurface lpDDS4);
uint RestoreAllSurfaces();
uint TestCooperativeLevel();
uint GetDeviceIdentifier(ref DDDEVICEIDENTIFIER lpDDDeviceIdentifier, uint dwFlags);
}
Before we can start, we need to set the cooperative level using the SetCooperativeLevel
method. The first parameter of this method is the control handle, and the second parameter is a value indicating if the application will be windowed or full screen.
Primary surface and backbuffers
Now, we must create our primary surface/framebuffer and any backbuffers. The primary surface is the screen itself. A backbuffer is another image in memory that is the same dimensions as the framebuffer. Applications draw the current frame to the backbuffer, and then copies the entire backbuffer to the framebuffer when the frame has been completely rendered. The video hardware is constantly drawing the screen based on the refresh rate. The screen will get updated while you are in the middle of drawing a frame, and your game graphics will appear to flicker. A backbuffer will prevent this flickering from occurring. Shown below is our initialization code; windowed mode is not really supported right now.
public DirectDrawGraphics(Control control, CooperativeFlags flags,
BackbufferMode backBufferMode)
{
if (control == null)
{
throw new ArgumentNullException("control");
}
IDirectDraw draw;
uint result = NativeMethods.DirectDrawCreate(IntPtr.Zero,
out draw, IntPtr.Zero);
_ddraw = draw;
if (result != 0)
{
throw ExceptionUtil.Create(result);
}
result = _ddraw.SetCooperativeLevel(control.Handle, flags);
if (result != 0)
{
throw ExceptionUtil.Create(result);
}
_hostControl = control;
DDCAPS halCaps = new DDCAPS(), helCaps = new DDCAPS();
halCaps.dwSize =(uint) Marshal.SizeOf(typeof(DDCAPS));
helCaps.dwSize = halCaps.dwSize;
result = _ddraw.GetCaps(out halCaps, out helCaps);
_supportHardwareFlip = (halCaps.ddsCaps.dwCaps &
SurfaceCapsFlags.BACKBUFFER) == SurfaceCapsFlags.BACKBUFFER;
_supportHardwareFlip &= ((halCaps.ddsCaps.dwCaps &
SurfaceCapsFlags.FLIP) == SurfaceCapsFlags.FLIP);
_supportHardwareFlip &= (backBufferMode != BackbufferMode.None);
if ( !_supportHardwareFlip && backBufferMode == BackbufferMode.Hardware )
{
throw new NotSupportedException("Device does not support " +
"the minimum hardware options.");
}
_surfaceFlags = ((halCaps.ddsCaps.dwCaps & SurfaceCapsFlags.VIDEOMEMORY) ==
SurfaceCapsFlags.VIDEOMEMORY) ?
SurfaceCapsFlags.VIDEOMEMORY : SurfaceCapsFlags.SYSTEMMEMORY;
DDSURFACEDESC desc = new DDSURFACEDESC();
desc.dwFlags = SurfaceDescFlags.CAPS;
desc.ddsCaps.dwCaps = SurfaceCapsFlags.PRIMARYSURFACE | _surfaceFlags;
if (_supportHardwareFlip && backBufferMode != BackbufferMode.None &&
backBufferMode != BackbufferMode.Software)
{
desc.dwFlags |= SurfaceDescFlags.BACKBUFFERCOUNT;
desc.ddsCaps.dwCaps |= SurfaceCapsFlags.FLIP;
desc.dwBackBufferCount = 1;
_primarySurface = CreateSurface(desc);
EnumSurfacesCallback callback =
new EnumSurfacesCallback(EnumAttachSurfacesCallback);
IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(callback);
_primarySurface._surface.EnumAttachedSurfaces(IntPtr.Zero, callbackPtr);
GC.KeepAlive(callback);
}
else
{
_primarySurface = CreateSurface(desc);
if (backBufferMode != BackbufferMode.None)
{
desc = new DDSURFACEDESC();
desc.ddsCaps.dwCaps |= _surfaceFlags;
desc.dwFlags = SurfaceDescFlags.CAPS | SurfaceDescFlags.WIDTH |
SurfaceDescFlags.HEIGHT;
desc.dwHeight = (uint)_primarySurface.Height;
desc.dwWidth = (uint)_primarySurface.Width;
_backbuffer = CreateSurface(desc);
}
}
if (flags == CooperativeFlags.Normal)
{
result = _ddraw.CreateClipper(0, out _clipper, IntPtr.Zero);
if (result != 0)
{
throw ExceptionUtil.Create(result);
}
result = _clipper.SetHWnd(_hostControl.Handle);
if (result != 0)
{
throw ExceptionUtil.Create(result);
}
result = _primarySurface._surface.SetClipper(_clipper);
if (result != 0)
{
throw ExceptionUtil.Create(result);
}
}
_screenArea = new Rectangle(0, 0, _primarySurface.Width, _primarySurface.Height);
}
Creating off screen surfaces
To create surfaces, we need to call the IDirectDraw.CreateSurface
method. This method takes a surface description as an input parameter and returns an IDirectDraw
surface as an output parameter. The surface description contains various information such as width, height, and the capabilities of the new surface. We need to indicate which fields we have set using the dwFlags
field. Shown below is our wrapper code.
public Surface CreateSurface(int width, int height)
{
DDSURFACEDESC desc = new DDSURFACEDESC();
desc.dwSize = (uint)Marshal.SizeOf(typeof(DDSURFACEDESC));
desc.dwHeight = (uint)height;
desc.dwWidth = (uint)width;
desc.ddsCaps.dwCaps = _surfaceFlags;
desc.dwFlags = SurfaceDescFlags.CAPS | SurfaceDescFlags.WIDTH |
SurfaceDescFlags.HEIGHT;
return CreateSurface(desc);
}
public Surface CreateSurface(DDSURFACEDESC surfaceDesc)
{
DDSURFACEDESC desc = surfaceDesc;
desc.dwSize = (uint)Marshal.SizeOf(typeof(DDSURFACEDESC));
IDDrawSurface surface;
_ddraw.CreateSurface(ref desc, out surface, IntPtr.Zero);
return new Surface(this, surface);
}
Loading a surface from a bitmap
To load a bitmap onto a surface, we need to use GDI. The GetDC
method on a surface will get us a pointer to a device context that we can use in various GDI methods that take an HDC. After performing any GDI work, we need to make sure we can use the surface ReleaseDC
method. In the example code below, we use the BitBlt
method to copy the image on to the surface.
internal void LoadImage(Bitmap image)
{
IntPtr hBitmap = image.GetHbitmap();
IntPtr hdcImage, hdc = IntPtr.Zero;
hdcImage = GDI.CreateCompatibleDC(IntPtr.Zero);
GDI.SelectObject(hdcImage, hBitmap);
_surface.GetDC(ref hdc);
GDI.BitBlt(hdc, 0, 0, image.Width, image.Height, hdcImage, 0, 0,
TernaryRasterOperations.SRCCOPY);
_surface.ReleaseDC(hdc);
GDI.DeleteDC(hdcImage);
}
Bliting
Bliting is short for bit block transfer. Bliting is basically copying some memory from one location to another. The term is used when referring to images that are combined in memory. When bliting, many libraries will use bitwise operators to combine images and perform effects like transparencies and overlays. In our example DirectDraw wrapper, we expose a color fill method and a DrawImage
method that supports color keys.
Flipping
There can be some performance issues copying the backbuffer on each frame. Hardware page flipping removes this step. Instead of copying the backbuffer to the framebuffer, the backbuffer becomes framebuffer and the framebuffer becomes the backbuffer. The flip method tells the video adapter to use a different video memory address for the framebuffer. You can chain multiple backbuffers. In our example, we will just be using one backbuffer.
public void Flip()
{
if (_supportsHardware)
{
_primarySurface._surface.Flip(IntPtr.Zero, FlipFlags.WAITNOTBUSY);
}
else
{
tagRECT dest = _screenArea;
_primarySurface.Draw(ref dest, _backbuffer, ref dest, BltFlags.DDBLT_WAITNOTBUSY);
}
}
Conclusions
In the emulator, I noticed DirectDraw is slightly slower than GDI. On a HTC TOUCH DUAL, DirectDraw is slightly slower than GDI only when we are not using hardware page flipping. With hardware page flipping enabled, it is 4x slower than GDI! This device does not have very good drivers (see the HTC class action) so I am going to wait to get a better hardware before drawing any conclusions.
Next time
After I get some better hardware, we will compare our wrapper against unmanaged DirectDraw, GDI, and some of the 3D APIs as well (if the phone has hardware support).