Native and Managed Interoperability: DirectDraw via CLI / C++

In my previous post on Native and Managed Interoperability I briefly mentioned that “Managed C++” has it’s own unique interoperability features. Technically, the “Managed C++” syntax is officially depreciated and has been superseded by CLI / C++, or the Common Language Infrastructure.

However, the same underlying concepts still apply. CLI / C++ is a very powerful language that allows developers to bridge the gap between managed BCL components and native components under a single managed context.

DirectDraw: An oldie, but a goodie

DirectDraw is a perfect example of a native Win32 component that has very little to no direct managed support, this is because since the introduction of DirectX 8.0, the main interface to DirectX rendering is with the Direct3D component.

While undoubtedly incredibly powerful and robust, Direct3D can be very overwhelming when starting out with graphics development. I’m a big advocate of “working your way up”, building a solid foundation on the fundamentals before moving on to complex frameworks that abstract the core concepts away.

This is where DirectDraw really excels. DirectDraw is as simple as it gets in the modern rendering world, it’s essentially just an abstraction of the video hardware that allows a unified access to video memory where you’re free to modify it however you see fit.

However, in the land of managed code, if you’re wanting a straightforward interface to do some simple rendering with hardware acceleration you’re required to use things like Managed DirectX, which is now depreciated in favor of an even higher level of abstraction, the XNA Framework.

The aforementioned solutions I found to be unnecessarily complicated and bloated when all I wanted was simple access to a hardware accelerated rendering surface, so I decided to write a very simple CLI / C++ wrapper around the DirectDraw7 component that I can use in my managed applications with minimal overhead.

Also, I should mention that I wrote this wrapper before I discovered SlimDX and SharpDX, both of which are excellent managed wrappers that even have support for the new Direct2D API, the successor to DirectDraw. So, I didn’t reinvent the wheel just to do it, however I did learn quite a bit from doing this little project and it brought back a lot of nostalgic memories.

A quick word on COM

One of the things Microsoft got absolutely right with COM is that it’s completely downward compatible, meaning that even if an end user has DirectX 11 installed and your application is built to run against interfaces exposed through a previous version, like DirectX 7 for example, your application can request an instance of the 7.0 objects you built against, and they’re guaranteed to be there and behave exactly as you expect them to.

This very flexible development model this is accomplished at a very high level by enforcing all objects to implement the IUnknown interface, which exposes two crucial methods – QueryInterface and Release. These methods are used to get instances of requested objects dynamically at run-time that implement the interface you’re expecting and then release them when you’re done. Since the object binding is done at run-time the underlying components can actually be upgraded or enhanced independently of the implementing application as long as they keep their exposed interface contract unchanged.

Of course, with this astounding amount of flexibly comes with a quirky and complex underpinning built on layers and layers of abstraction that are implemented with virtual function pointers and dynamic instantiation of objects which are mapped to unique identifiers in the system which requires a lot of standard pluming before you can get up and running. Thankfully, we’ll use some built-in COM wrappers added in DirectX 7 to retrieve our objects so it’ll be as clean as possible.

CLI Video Memory Wrapper

The DirectDraw wrapper is exposed through a very simple CLI / C++ class called “VideoMemory”. Let’s take a look at the class definition first, then I’ll go over the interesting parts.


public ref class VideoMemory
{
    public:

        VideoMemory( IntPtr );

        void CreateDevice();
        void ResetDevice();
        void Release();

        void SetBackgroundColor( unsigned int );
        void SetColorFormat( PixelSize );

        bool ReadyFrame( bool );
        void RenderFrame();

        IntPtr VideoMemoryPtr;
        int Width, Height, Stride;

    protected:

        ~VideoMemory();
        !VideoMemory();

    private:
        DirectDrawWrapper * pDDW;
};

It’s a pretty straightforward public interface much like a traditional C++ class, except for the CLI finalizer – !VideoMemory() which is a non-deterministic deconstructor. Since our object is built to be used under a managed context with automatic memory management, i.e., garbage collection, the compiler will automatically implement the CLR disposable pattern on it, which will call our finalizer before the objects memory gets re-allocated.

There is also a private backing field to a “DirectDrawWrapper” – this is our native C++ class that does all the “heavy lifting” for working with DirectDraw. This is the traditional Opaque Pointer (Pimpl) pattern, which creates an instance of the native wrapper object in the managed object’s constructor, then deletes it when CLR finalizer is called.

The rest of the class is simply constructed out of wrapper methods that forward calls to the native DirectDraw wrapper class.

Native DirectDraw Implementation

The constructor to the VideoMemory object takes an argument to an “IntPtr”, which is simply a managed platform independent pointer, basically the equivalent of “void *” in traditional C/C++, which is the handle to the window (hWnd in the code below) that DirectDraw will be attached to. Here is a look at the native code that is used to create our DirectDraw device.


DirectDrawCreateEx
(
    NULL,
    ( VOID **) &lpDirectDraw,
    IID_IDirectDraw7,
    NULL
);

lpDirectDraw->SetCooperativeLevel
(
    hWnd,
    DDSCL_NORMAL
);

DirectDrawCreateEx is used to do all the nasty COM stuff, which will give us a nice DirectDraw object to work with. Also note that the IID_IDirectDraw7 is the unique identifier for the DirectDraw7 COM object. The SetCooperationLevel call is used to tell DirectX that we’re going to be sharing our resources with the rest of the desktop (Normal), the alternative is Exclusive Mode which would allow us to change the display resolution and give us exclusive access to system resources.

The CreateDevice method is used to create the primary surface, which will be the same color depth as the desktop, and we will need to attach a clipper to our window so that we don’t accidentally write over video memory that we don’t own. Create device also forwards a call to CreateBackBuffer that will setup our video memory buffer. This allows us to specify a custom size and bit depth properties for our video memory buffer, so we can write code optimized for say, a 16 BPP 565 color format surface when working with it, then just “Blt” it over to the primary surface (even if the primary surface is in a higher color depth).

Here is a look at how to create back buffer surfaces of a a specific color depth.


void DirectDrawWrapper::InitializeRealColorSurface( LPDDSURFACEDESC2 ddsd )
{
	ddsd->ddsCaps.dwCaps |= DDSCAPS_SYSTEMMEMORY;

	ddsd->ddpfPixelFormat.dwSize = sizeof( DDPIXELFORMAT );

	ddsd->ddpfPixelFormat.dwFlags = DDPF_RGB | DDPF_PALETTEINDEXED8;
	ddsd->ddpfPixelFormat.dwRGBBitCount = 8;

	static bool paletteInitialized = false;

	if( !paletteInitialized )
	{
		int BlueValues[] = { 0, 85, 170, 255 };
		int RedValues[] = { 0, 36, 73, 108, 144, 181, 216, 255 };
		int GreenValues[] = { 0, 36, 73, 108, 144, 181, 216, 255 };

		int colorMapIndex = 0;

		for( int redIndex = 0; redIndex < 8; redIndex++ )
		{
			for( int greenIndex = 0; greenIndex < 8; greenIndex++ )
			{
				for( int blueIndex = 0; blueIndex < 4; blueIndex++ )
				{
					paletteEntries[ colorMapIndex ].peRed = RedValues[ redIndex ];
					paletteEntries[ colorMapIndex ].peGreen = RedValues[ greenIndex ];
					paletteEntries[ colorMapIndex ].peBlue = RedValues[ blueIndex ];

					colorMapIndex++;
				}
			}
		}

		lpDirectDraw->CreatePalette
                (
                    DDPCAPS_8BIT | DDPCAPS_ALLOW256 | DDPCAPS_INITIALIZE,
                    paletteEntries,
                    &lpDDPalette,
                    NULL
                );

		paletteInitialized = true;
	}
}

void DirectDrawWrapper::InitializeHiColorSurface( LPDDSURFACEDESC2 ddsd )
{
	ddsd->ddsCaps.dwCaps |= DDSCAPS_VIDEOMEMORY;

	ddsd->ddpfPixelFormat.dwSize = sizeof( DDPIXELFORMAT );

	ddsd->ddpfPixelFormat.dwFlags = DDPF_RGB;
	ddsd->ddpfPixelFormat.dwRGBBitCount = 16;

	ddsd->ddpfPixelFormat.dwRBitMask = 0xf800;
	ddsd->ddpfPixelFormat.dwGBitMask = 0x07e0;
	ddsd->ddpfPixelFormat.dwBBitMask = 0x001f;
}

void DirectDrawWrapper::InitializeTrueColorSurface( LPDDSURFACEDESC2 ddsd )
{
	ddsd->ddsCaps.dwCaps |= DDSCAPS_VIDEOMEMORY;

	ddsd->ddpfPixelFormat.dwSize = sizeof( DDPIXELFORMAT );

	ddsd->ddpfPixelFormat.dwFlags = DDPF_RGB;
	ddsd->ddpfPixelFormat.dwRGBBitCount = 32;

	ddsd->ddpfPixelFormat.dwRBitMask = 0x00ff0000;
	ddsd->ddpfPixelFormat.dwGBitMask = 0x0000ff00;
	ddsd->ddpfPixelFormat.dwBBitMask = 0x000000ff;
	ddsd->ddpfPixelFormat.dwRGBAlphaBitMask = 0xff000000;
}

The interesting things to note in the above sample code is a DDSURFACEDESC2 is simply a description of how you want a surface to be created. The interesting fields are the PixelFormat structure, which is used to specify the color depth and the dwCaps, which is used to tell DirectX where you want the surface memory allocated from. Obviously, we want video memory, however 8 bit indexed color is not supported anymore on modern video hardware, so we have to default back to system memory.

Now that we have a surface to manipulate, let’s take a look at the process of locking our surface memory for writing and then “flipping” the result back onto our primary surface. Since the actual memory manipulation is designed to take place in outside of our control (managed application) we lock the surface in one step, which validates the address of an exposed pointer in the managed wrapper called, unimaginatively, “VideoMemoryPtr”.


bool DirectDrawWrapper::Lock( bool clearFrame )
{
	if( lpDDSBack == NULL )
		return false;

	DDSURFACEDESC2 LockedSurface;

	DDRAW_INIT_STRUCT( LockedSurface );

	HRESULT hResult;

	if( clearFrame )
	{
		DDBLTFX ddbltfx;
		RECT fillArea;

		memset( &ddbltfx, 0, sizeof( DDBLTFX ) );
		ddbltfx.dwSize = sizeof( DDBLTFX );

		ddbltfx.dwFillColor = BackgroundColor;

		fillArea.top = 0; fillArea.left = 0;
		fillArea.right = Width; fillArea.bottom = Height;

		hResult = lpDDSBack->Blt( &fillArea, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx );

		if( hResult != DD_OK )
			DisplayError( hResult, TEXT( "Error Clearing Secondary Surface" ) );
	}

	hResult = lpDDSBack->Lock( NULL, &LockedSurface, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT | DDLOCK_NOSYSLOCK, NULL );

	if( hResult == DD_OK )
	{
		VideoMemory = ( unsigned int * ) LockedSurface.lpSurface;

		Stride = LockedSurface.lPitch >> PixelSize;

		return true;
	}
	else if( hResult == DDERR_SURFACELOST )
	{
		lpDDSBack->Restore();
	}

	return false;
}

void DirectDrawWrapper::Render( void )
{
	lpDDSBack->Unlock( NULL );

	ZeroMemory( &clientArea, sizeof( clientArea ) );
	GetClientRect( hWnd, &clientArea );

	p1.x = clientArea.left; p1.y = clientArea.top;
	p2.x = clientArea.right; p2.y = clientArea.bottom;

	ClientToScreen( hWnd, &p1 );
	ClientToScreen( hWnd, &p2 );

	clientArea.left = p1.x; clientArea.top = p1.y;
	clientArea.right = p2.x; clientArea.bottom = p2.y;

	if( lpDDSPrimary->Blt( &clientArea, lpDDSBack, NULL, DDBLT_WAIT, NULL ) != DD_OK )
		throw;
}

In between the calls to Lock() and Render() VideoMemoryPtr is a valid address to the back buffer surface. You’re free to manipulate it at will, and then Render will take that memory and copy it to the display surface.

Managed Context Usage

You can use the DirectDraw wrapper in your managed application like so:


bool ClearEveryFrame = true;

Random random = new Random( DateTime.Now.Ticks );

VideoMemory videoMemory = new VideoMemory( FrameworkForm.Handle );

videoMemory.SetColorFormat( ( PixelSize ) 4 ); // 32 bits per pixel
videoMemory.SetBackgroundColor( 0 ); // Black

videoMemory.CreateDevice();

while( true )
{
	Point point;

	if( videoMemory.ReadyFrame( ClearEveryFrame ) )
	{
		for( int i = 0; i < 1000; i++ )
		{
			point = new Point( videoMemory.Width, videoMemory.Height );

			*( ( ( uint * ) videoMemory.VideoMemoryPtr ) + ( point.Y * videoMemory.Stride ) + point.X ) =
				new Color( random.Next( 255 ), random.Next( 255 ), random.Next( 255 ) );
		}

		videoMemory.RenderFrame();
	}
}

Source Code & DirectDrawWrapper Binary

The full project source code is available on my RenderingFramework.DirectX repository on GitHub, or the compiled binary can downloaded directly here.

In order to compile the project you’ll need the August 2007 DirectX SDK, which is the latest version of the DirectX SDK that ships with the necessary headers and libraries referenced by the project for DirectDraw support. If you just want to use the binary in your own application simply download it and add a reference to the DLL then you’re free to use it like any other managed object demonstrated in the sample code above.

Also feel free to checkout my Rendering Framework project which uses this library for DirectDraw support.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s