
This chapter will introduce the DirectX.NET basic graphics classes, from the Sunlight.DirectX.Graphics namespace: the Direct3D, Device and Texture classes.
This chapter introduces .NET events (and the C++.NET extensions for events), properties, string conversion between native and .NET strings and String.Format.
The Direct3D class wraps an IDirect3D8 object.
namespace Sunlight
{
namespace DirectX
{
namespace Graphics
{
__gc public
class Direct3D : public Component
{
protected:
IDirect3D8 __nogc
*m_pD3D;
public:
Direct3D();
~Direct3D();
IDirect3D8 __nogc
*GetDirect3D();
};
}
}
}
We have defined the class using the __gc modifier, to specify this is a managed class. It is set to 'public', so it is visible outside its containing assembly.
The inner pointer to an IDirect3D8 object is defined as __nogc, to prevent .NET from attempting to manage the object (since it comes from DirectX, not .NET).
The implementation of this class is fairly simple.
Direct3D::Direct3D()
{
m_pD3D = Direct3DCreate8(D3D_SDK_VERSION);
if (m_pD3D == NULL)
throw new
Sunlight::DirectX::DirectXException(S"::Direct3DCreate8", E_FAIL);
}
Direct3D::~Direct3D()
{
if (m_pD3D != NULL)
{
m_pD3D->Release();
m_pD3D = NULL;
}
}
IDirect3D8 *Direct3D::GetDirect3D()
{
return m_pD3D;
}
This class is not terribly exciting. The next one is far more so...
__gc public class Device
{
protected:
D3DFORMAT m_nFormat;
bool
m_bCreated;
IDirect3DDevice8 __nogc *m_pDevice;
System::Windows::Forms::Form *m_pParentWindow;
double
m_nFrameRate;
DWORD
m_dwFrameStartTime;
// Perform the device initialisation or reset.
void Create(bool bReset);
// Fills a D3DPRESENT_PARAMETERS for the device.
virtual void FillD3DPP(D3DPRESENT_PARAMETERS
*pd3dpp);
// Sets the device render states.
virtual void SetupDevice();
// Sets the window styles.
virtual void SetupWindow();
// Called when the ParentWindow is closed.
void OnFormClosed(Object *sender, EventArgs *e);
public:
Device();
~Device();
int Width;
int Height;
int BitsPerPixel;
bool Windowed;
Direct3D *Direct3DObject;
__property System::Windows::Forms::Form *get_ParentWindow();
__property void
set_ParentWindow(System::Windows::Forms::Form *);
__property DWORD get_Format();
__property IDirect3DDevice8 __nogc
*get_Direct3DDevice();
__property bool get_Paused();
__property bool get_IsCreated();
__property double get_FrameRate();
__property int get_RefreshRate();
__event EventHandler *OnCreate;
__event EventHandler *OnInitialize;
__event EventHandler *OnLost;
__event EventHandler *OnDestroy;
void Create();
void Reset();
void Destroy();
void BeginScene();
void EndScene();
void Flip();
};
Now, this is slightly more interesting, isn't it?
A Device object has to contain several fields: Width, Height, BitsPerPixel and Windowed specify the screen size, colour depth and windowed/fullscreen mode. Direct3DObject references the Direct3D object used to create this device. ParentWindow is a property which specifies the window used for this device; a property is a field that, instead of using a data member to specify its value, uses a pair of functions. We will investigate why ParentWindow is a property, rather than a simple field, later on. The properties Format, Direct3DDevice, Paused, IsCreated, FrameRate and RefreshRate are properties for a simpler reason; they are intended to be read-only by other classes. Format specifies the surface format of the device. Direct3DDevice returns a pointer to the internal IDirect3DDevice8 instance. Paused returns whether the device is paused (i.e. the device is lost and can't be restored yet). IsCreated returns whether the device has been created (and thus the IDirect3DDevice8 pointer is valid). RefreshRate returns the display refresh rate, and FrameRate returns the actual current frame rate (the rate at which Flip is called).
Next, we have four events. Events allow other classes to 'listen' for things happening in the class. In traditional programming languages, this might have been done with function pointers. In .NET, there is a special type of function pointer: a delegate. Delegates are actually classes in themselves, and store pointers to class methods, together with a reference to the class itself; thus, they are strongly typed and more flexible than other function pointers. Delegates can be unicast, in which case only one function can be called, or they can be multicast, so that many functions may be called (but the return values are ignored). Events are simply multicast delegates.
These events handle the cases when the device is created, when it is initialised (the mode set, e.g. when the device is restored after loss), when it is lost, and when it is destroyed.
Finally, we have six other methods. Create is called to initially create the device. Reset causes the device to be reset (as if lost, then restored). This allows the device to reassess its parameters (such as Windowed). Destroy releases the device object (and other objects associated with it). BeginScene and EndScene serve much the same function as their IDirect3DDevice8 equivalents. Flip calls IDirect3DDevice8::Present to show the back buffer contents on the screen.
The first segment of interesting code lies in Create. This function is called to create or reset the Direct3D device. This is all fairly straightforward until near the end:
// Attach to the parent window's Closed event, to
shut down the thread.
__hook(&System::Windows::Forms::Form::Closed, m_pParentWindow, &Device::OnFormClosed,
this);
// OnCreate triggered when device is first created
__raise OnCreate(this, new
EventArgs());
m_bCreated = true;
}
// OnInitialize triggered when device is created or reset
__raise OnInitialize(this, new
EventArgs());
}
This shows off the C++.NET extensions for events. Device attaches to the Closed event of its parent window, to run the OnFormClosed method of this object when the window is closed. It uses the C++ extension __hook, which takes the address of the event object, a pointer to the source object, the address of the method to be called, and the object to call it on. The advantages of the .NET event model can immediately be seen - the device can be destroyed in tandem with the window, without any code required by the application program.
Next, we raise two events of our own: OnCreate and OnInitialize. This is done by the C++ extension __raise, which takes the event object, plus any parameters it requires. We have chosen to use the standard event class EventHandler, so we must give them a pointer to the event sender, together with an EventArgs object. We have not defined a EventArgs-derived class, so we merely pass a new EventArgs object.
The Destroy method holds the corresponding pair of destroy event notifications:
// Destroys this device and related video-memory objects.
void Device::Destroy()
{
if (m_pDevice != NULL)
{
// OnLost triggered when device lost
__raise OnLost(this, new
EventArgs());
// OnDestroy triggered when device released
__raise OnDestroy(this, new
EventArgs());
m_pDevice->Release();
m_pDevice = NULL;
ParentWindow->Hide();
}
m_bCreated = false;
}
The ParentWindow property holds the next code snippet of interest:
// The window which will represent this device.
System::Windows::Forms::Form *Device::get_ParentWindow()
{
return m_pParentWindow;
}
void Device::set_ParentWindow(System::Windows::Forms::Form *pParent)
{
if (m_pDevice != NULL)
{
Destroy();
m_pParentWindow = pParent;
Create(false);
}
else
m_pParentWindow = pParent;
}
When the ParentWindow property is changed, in order to change the assigned window, we must destroy and recreate the device. We do not need to destroy any other objects; other objects will have hooked the OnCreate and OnDestroy events, and will destroy and recreate themselves together with the device.
This shows part of the power of properties; being able to trap what happens when a field is assigned a value. The Paused property shows a further advantage:
// Returns if it is safe for the application to execute its logic and drawing.
bool Device::get_Paused()
{
HRESULT h = m_pDevice->TestCooperativeLevel();
switch (h)
{
case D3D_OK:
return false;
case D3DERR_DEVICELOST:
return true;
case D3DERR_DEVICENOTRESET:
Reset();
return true;
}
throw new DirectXException(S"IDirect3DDevice8::TestCooperativeLevel",
h);
}
In this case, there is no underlying data member. Paused calls IDirect3DDevice8::TestCooperativeLevel to find out whether the device has been lost, and if so, whether it can be restored. If it can be restored, it attempts to reset the device using Reset.
void Device::Reset()
{
__raise OnLost(this, new
EventArgs());
Create(true);
__raise OnInitialize(this, new
EventArgs());
}
Since Reset raises the events OnLost and OnInitialize, video-memory objects which have hooked these events can release and re-create themselves without application intervention.
Frame rate calculation is done in Flip:
// Draw the current scene to the device.
void Device::Flip()
{
DWORD dwFrameEndTime = timeGetTime();
m_nFrameRate = 1000.0 / (double)(dwFrameEndTime -
m_dwFrameStartTime);
m_dwFrameStartTime = dwFrameEndTime;
if (m_nFrameRate < 1)
m_nFrameRate = 1;
m_pDevice->Present(NULL, NULL, NULL, NULL);
}
This merely computes the frame rate based on the time between calls to Flip.
__gc public class Texture : public
Component, public ISupportInitialize
{
protected:
bool m_bCreated;
IDirect3DTexture8 __nogc *m_pTexture;
// Called when the parent device is created.
void OnDeviceCreated(Object *sender, EventArgs *e);
// Called when the parent device is destroyed.
void OnDeviceDestroyed(Object *sender, EventArgs *e);
public:
Texture();
~Texture();
Device *DeviceObject;
String *Filename;
__property IDirect3DTexture8 __nogc
*get_Direct3DTexture();
__property int get_Width();
__property int get_Height();
// Create the texture object.
virtual void Create();
// Destroy the texture object.
virtual void Destroy();
};
Texture is primarily interesting because it shows off the utility of the Device object's events.
Texture requires only the DeviceObject field, which specifies the device for which to create this texture, and Filename, which specifies the source filename, to create the texture. Filename can specify any graphic file format allowed by Direct3DX, including BMP, TGA, PNG, JPG, DIB, PPM, and DDS files.
The Direct3DTexture property provides an interface to the IDirect3DTexture8 object this class wraps. The Width and Height properties give the width and height of the loaded texture.
The Create and Destroy methods should be fairly self-explanatory. For simple applications, in which all the textures are loaded into memory at startup, these methods will be automatically called.
By far the most interesting thing about Texture is the way it hooks into Device's events to create and release the texture at the appropriate moments. Texture creates managed textures, so there is no need to release them on OnLost and reload on OnInitialize, but textures do need to be created, and released when the device is destroyed.
void Texture::set_DeviceObject(Device *pDevice)
{
m_pDeviceObject = pDevice;
// Hook the device's OnCreate event so we can (re-)create our texture
// when it is created
__hook(&Device::OnCreate, m_pDeviceObject, &Texture::OnDeviceCreated,
this);
// Hook the device's OnDestroy event so we can release our texture
// when it is destroyed
__hook(&Device::OnDestroy, m_pDeviceObject, &Texture::OnDeviceDestroyed,
this);
}
Note that this means there is no need for the application to call Create; the application need only set the device object and the filename, and the texture will automatically load with the device.
The next interesting attribute of Texture is how it obtains the path to the texture file in Create:
#ifdef _UNICODE
LPWSTR tszFilename = (LPWSTR)(void *)Marshal::StringToCoTaskMemUni(Filename);
#else
LPSTR tszFilename = (LPSTR)(void *)Marshal::StringToCoTaskMemAnsi(Filename);
#endif
HRESULT h = ::D3DXCreateTextureFromFileEx(m_pDeviceObject->Direct3DDevice,
tszFilename, D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT,
0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_FILTER_NONE,
D3DX_DEFAULT,
0, NULL, NULL, &pTexture);
CoTaskMemFree(tszFilename);
Create first obtains the filename. This is a String object. It then calls System.Runtime.InteropServices.Marshal.StringToCoTaskMemUni or StringToCoTaskMemAnsi (depending on the application character set) to convert from a String object to a character array, allocated using the COM task allocator. It creates the texture using this string with D3DXCreateTextureFromFileEx. Finally, it frees the allocated memory using the COM task allocation function CoTaskMemFree. This poorly-documented method of conversion is by far the most preferable way of converting from String objects to ordinary C++ strings.
If this fails, Create throws a variety of exceptions. It uses the absolutely wonderful function String.Format to do so:
if (FAILED(h))
{
if (h == ERROR_FILE_NOT_FOUND)
throw new
IO::FileNotFoundException(
String::Format(S"Texture file {0} not
found.", Filename), Filename);
if (h == D3DXERR_INVALIDDATA)
throw new IO::FileLoadException(
String::Format(S"Texture file {0} could
not be loaded.", Filename),
Filename);
throw new
Sunlight::DirectX::DirectXException(
S"IDirect3DDevice8::CreateVertexBuffer",
h);
}
String.Format takes a format string and a list of parameters, much like MFC's CString::Format, or sprintf. It returns a new String object. It's much nicer than the printf functions, however, in that you can specify arguments in any order, it's type-safe, and it accepts any object you like. Objects that implements the IFormattable interface can even take custom format strings (for things like printing hex values, setting the number of decimal places, etc.). After such a monumental leap forward in string formatting, the various exception classes are far too mundane and obvious - no hidden depths at all. Note the use of the 'S' prefix before the string constant, to denote a managed string.
Hopefully, the superior architecture of these objects, mostly enabled by the event support of .NET, will allow us to use Direct3D devices and textures with far less application-specific code than before. In the next section, we will find out whether this is true, by using these classes to build a 2-D sprite handling system.