• 11/24/99 02:08 PM
    Sign in to follow this  
    Followers 0

    Direct3D 7 IM Framework Programming 1: The Basics

    Graphics and GPU Programming

    Myopic Rhino
    [size="5"]The Basic Sample:

    Smashing a texture at the background


    With the advent of DirectX 7 two new interfaces have been added as part of Direct3D: IDirect3D7 and IDirect3Ddevice7. These objects provide several new features over their counterparts from previous versions of Direct3D. To mention a few: Hardware-accelerated transformation and lighting, Environment mapping with cubic environment maps, Geometry Blending, Device-state blocks, the texture manager can handle texture priorities etc.

    This tutorial series focuses on using the new Direct3D features with the Direct3D IM Framework provided by Microsoft. Since its advent in DirectX 6, the Direct3D IM Framework has changed dramatically. It's easier to use and easier to code ... that's great work.

    There's also a new Utility Library: Direct3DX, which sits on Top of Direct3D IM (Tutorial: http://iki.fi/sol/tut2.html). It provides helper functionality for enumerating device configurations, setting up a device, running full-screen or windowed mode uniformly, running resizing operations, calculating vector and matrix operations, and simplifying image file loading and texture creation. In addition, it provides functions for drawing simple shapes, sprites, and cube maps. However, Direct3DX does not provide a scene hierarchy or support X Files. This way the whole complexity of Direct3D IM is encapsulated. There's one big drawback ... we haven't received the source of this library. I recommend creating your own Utility Library. There are a lot of open source libraries to learn from.

    A good place to start diving into Direct3D is the DirectX Software Development Kit (SDK). All the D3D samples use the Direct3D IM Framework. This common framework gives you a common ground on which you can implement your individual features. As a beginner, you avoid a lot of basic mistakes. The framework code is fast and very well tested. You can concentrate your energy on learning. As am intermediate or professional programmer, you have a good testing platform. Perhaps a professional programmer would write his own framework, which suits his needs better, by looking in the Direct3D IM Framework Source.
    [bquote]For example ATI has a slightly modified version of this framework. NVIDIA has built a completely new framework.[/bquote]
    The SDK HTML Help is the primary resource for understanding DirectX in general and D3D specifically. You can find it online in the MSDN library. Sample code is the next best vehicle for learning Direct3D. The Direct3D Immediate Mode sample code lives in \mssdk\samples\multimedia\d3dim. Beneath \d3dim are \bin, \include, \lib, \media, and \src. All samples in ready-to-build form can be found in \src, depend on \include and \lib, and use art content in \media. The SDK installer plants prebuilt executables in \bin. Play around with the examples in the bin directory. Microsoft provides a discussion forum and a FAW for like-minded developers to share information. They provide an an excellent article on starting with the framework at http://msdn.microsof...ctx05152000.asp.

    Get the source attached to this article.

    [size="5"]Compiling The Source

    In our first example, we will take a look at the basic functionality provided by the framework and at one of the most simplified programs I can think of to use it. We will see a texture thrown at the back of a window and a timer, which shows the frames per seconds. This little program will work windowed and fullscreen. Press Alt-F4 to switch between these modes. F1 will show you the about box. F2 will give you a selection of useable drivers and ESC will shutdown the app.

    First let's talk about the files you need to compile the program. The best place to copy the files is in your Direct3D Immediate Mode source file Directory of the DirectX 7 SDK, which has to be installed. On my computer that would be for tex1: D:\mssdk\samples\Multimedia\D3DIM\src\tex7_1.

    There are only four program files:
    • tex1.cpp: the main file
    • winmain.rc: the resource file (Menu, Accelerator, Icon information etc.)
    • resource.h: header of winmain.rc
    • directx.ico: the icon you can see if the program is minimized or if maximized in the left corner of the title of the window. If you'd like to compile everything, you have to link it with the following *.lib files:
      ddraw.lib dxguid.lib ..\..\lib\d3dframe.lib kernel32.lib user32.lib gdi32.lib comdlg32.lib advapi32.lib shell32.lib uuid.lib winmm.lib

      d3dframe.lib is the static library which holds the Direct3D 7 Framework. You can find the Framework source in the D3Dframe directory in D:\mssdk\samples\Multimedia\D3DIM\src\D3DFrame. We will walk now through a high-level view of the Framework to understand and use the Direct3D 7 Framework. The Framework consists of seven *.cpp files. These files encapsulate the basic functionality you need to start programming a Direct3D game.
      • d3dapp.cpp exposes the application interface used for samples
      • d3dframe.cpp provides the framework that the application interface (in tex1.cpp) uses "under the covers"
      • d3denum.cpp contains enumeration support for drivers, devices, and modes
      • d3dtextr.cpp supplies texture support
      • d3dfile.cpp furnishes x-file support
      • d3dmath.cpp delivers math utility functions
      • d3dutil.cpp is a catchall for whatever miscellaneous useful functions remain The d3dApp.cpp module contains the class CD3DApplication. This class publishes the interface for sample programs, in the file D3Dapp.h. It provides seven functions:

        virtual HRESULT OneTimeSceneInit() { return S_OK; }
        virtual HRESULT InitDeviceObjects() { return S_OK; }
        virtual HRESULT DeleteDeviceObjects() { return S_OK; }
        virtual HRESULT Render() { return S_OK; }
        virtual HRESULT FrameMove( FLOAT ) { return S_OK; }
        virtual HRESULT RestoreSurfaces() { return S_OK; }
        virtual HRESULT FinalCleanup() { return S_OK; }

        These are the functions we'll use in tex1.cpp to create our D3D app. They could be called the "public interface" for the Direct3D IM Framework. Now, let's dive into the source.

        [size="5"]The Application Class

        The application class in tex1.cpp:

        class CMyD3DApplication : public CD3DApplication
        D3DTLVERTEX m_Background[4]; // Vertices used to render the backdrop
        FLOAT m_fStartTimeKey; // Time reference for calculations

        static HRESULT ConfirmDevice( DDCAPS* pddDriverCaps,
        D3DDEVICEDESC7* pd3dDeviceDesc );
        HRESULT OneTimeSceneInit();
        HRESULT InitDeviceObjects();
        HRESULT DeleteDeviceObjects();
        HRESULT Render();
        HRESULT FrameMove( FLOAT fTimeKey );
        HRESULT FinalCleanup();


        The first two variables hold the vertices used to render the background and the reference for calculating the time. The method [font="Courier New"][color="#000080"]ConfirmDevice()[/color][/font] checks the device for some minimum set of capabilities.
        [bquote]If you like to use only devices which really support multiple textures, you can use the following code in this method:[font="Courier New"][color="#000080"]if( pd3dDeviceDesc->wMaxTextureBlendStages > 1 )[/color][/font]
        [font="Courier New"][color="#000080"]if( pd3dDeviceDesc->wMaxSimultaneousTextures > 1 )
        if( pd3dDeviceDesc->dwTextureOpCaps & D3DTEXOPCAPS_MODULATE )
        return S_OK;[/color][/font][/bquote]
        For the permanent initialization the function [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font] is invoked once per application execution cycle. You can load textures and x-files, setup calculated values etc.. Basically any one-time resource allocation should be performed here. [font="Courier New"][color="#000080"]InitDeviceObjects()[/color][/font] is used to initialize per-device objects such as loading texture bits onto a device surface, setting matrices and populating vertex buffers. The method [font="Courier New"][color="#000080"]DeleteDeviceObjects()[/color][/font] is called when the app exits, or the device is being changed. It deletes any device dependant objects, which are intialized in InitDeviceObjects().
        [bquote]These two functions are matched pairs; be sure your device-specific resource allocations are matched with deletions, or you will be leaking memory every time a device change happens.[/bquote]
        The [font="Courier New"][color="#000080"]Render()[/color][/font] method is self-explaining. It is called once per frame and is the entry point for 3d rendering. It could set up render states, clear the viewport, and render a scene. In an animated program, the method [font="Courier New"][color="#000080"]FrameMove()[/color][/font] is used to hold the whole animation code such as updating matrices, texture coordinates, object coordinates, and other time-varying activities. This example doesn't use any animation ... just a texture, which is thrown at the background. So it's not used really. Well ... [font="Courier New"][color="#000080"]FinalCleanup()[/color][/font] as the last protected method destroys, for example, the allocated memory for the textures, deletes the file objects etc. It's the counterpart to [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font] and destroys any per-application objects. Watch out for memory leaks.


        The first method, which is exported by the framework is [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font]:

        HRESULT CMyD3DApplication::OneTimeSceneInit()
        // Initializes vertices used to render the background
        D3DVECTOR vFar = D3DVECTOR( 0.0f, 0.0f, 0.5f );
        m_Background[0] = D3DTLVERTEX( vFar, 0.5f, 0xffffffff, 0, 0.0f, 1.0f );
        m_Background[1] = D3DTLVERTEX( vFar, 0.5f, 0xffffffff, 0, 0.0f, 0.0f );
        m_Background[2] = D3DTLVERTEX( vFar, 0.5f, 0xffffffff, 0, 1.0f, 1.0f );
        m_Background[3] = D3DTLVERTEX( vFar, 0.5f, 0xffffffff, 0, 1.0f, 0.0f );

        // Load in textures
        D3DTextr_CreateTextureFromFile( "lake.bmp" );

        return S_OK;
        The background is held by four vertices. The "TL" indicates that these are vertices that your application - not Direct3D - should light and transform. These vertices skip the transformation and lightning pipeline altogether and pass straight to the rasterizer.
        [bquote]It wouldn't make any sense to transform or light the vertices which "hold" the background texture.[/bquote]
        The D3D_OVERLOADS constructors defined in row 11 offers a convenient way for C++ programmers to create transformed and lit vertices with D3DTLVERTEX.

        _D3DTLVERTEX(const D3DVECTOR& v, float _rhw, D3DCOLOR _color,
        D3DCOLOR _specular, float _tu, float _tv)
        sx = v.x;
        sy = v.y;
        sz = v.z;
        rhw = _rhw;
        color = _color;
        specular = _specular;
        tu = _tu;
        tv = _tv;
        The system requires a vertex position that has already been transformed. So the x and y values must be in screen coordinates, and z must be the depth value of the pixel, which could be used in a z-buffer (we won't use a z-buffer here). Z values can range from 0.0 to 1.0, where 0.0 is the closest possible position to the viewer, and 1.0 is the farthest position still visible within the viewing area. Immediately following the position, transformed and lit vertices must include an RHW (reciprocal of homogeneous W) value.
        [bquote]Before rasterizing the vertices, they have to be converted from homogeneous vertices to non-homogeneous vertices, because the rasterizer expects them this way. Direct3D converts the homogeneous vertices to non-homogeneous vertices by dividing the x-, y-, and z-coordinates by the w-coordinate, and produces an RHW value by inverting the w-coordinate. This is only done for vertices which are transformed and lit by Direct3D.

        The RHW value is used in multiple ways: for calculating fog, for performing perspective-correct texture mapping, and for w-buffering (an alternate form of depth buffering).[/bquote]

        With D3D_OVERLOADS defined, D3DVECTOR is declared as

        _D3DVECTOR(D3DVALUE _x, D3DVALUE _y, D3DVALUE _z);
        D3DVALUE is the fundamental Direct3D fractional data type. It's declared in d3dtypes.h as

        typedef float D3DVALUE, *LPD3DVALUE;
        The source shows that the x and y values for the D3DVECTOR are always 0.0f (this will be changed in [font="Courier New"][color="#000080"]InitDeviceObjects()[/color][/font]). rhw is always 0.5f, [font="Courier New"][color="#000080"]color[/color][/font] is 0xfffffff and [font="Courier New"][color="#000080"]specular[/color][/font] is set to 0. Only the tu1 and tv1 values are differing between the four vertices. These are the coordinates of the background texture.
        [bquote]In order to map texels onto primitives, Direct3D requires a uniform address range for all texels in all textures. Therefore, it uses a generic addressing scheme in which all texel addresses are in the range of 0.0 to 1.0 inclusive.

        If, instead, you decide to assign texture coordinates to make Direct3D use the bottom half of the texture, the texture coordinates your application would assign to the vertices of the primitive in this example are (0.0,0.0), (1.0,0.0), (1.0,0.5), and (0.0,0.5). Direct3D will apply the bottom half of the texture as the background.

        Note: By assigning texture coordinates outside that range, you can create certain special texturing effects.
        You will find the declaration of [font="Courier New"][color="#000080"]D3DTextr_CreateTextureFromFile()[/color][/font] in the Framework source in d3dtextr.cpp. It creates a local bitmap from a passed file. Textures could be created from *.bmp and *.tga files. Textures are managed in the framework in a linked list, which holds the info per texture, called texture container.

        struct TextureContainer
        TextureContainer* m_pNext; // Linked list ptr

        TCHAR m_strName[80]; // Name of texture (doubles as image filename)
        DWORD m_dwWidth;
        DWORD m_dwHeight;
        DWORD m_dwStage; // Texture stage (for multitexture devices)
        DWORD m_dwBPP;
        DWORD m_dwFlags;
        BOOL m_bHasAlpha;

        LPDIRECTDRAWSURFACE7 m_pddsSurface; // Surface of the texture
        HBITMAP m_hbmBitmap; // Bitmap containing texture image
        DWORD* m_pRGBAData;

        HRESULT LoadImageData();
        HRESULT LoadBitmapFile( TCHAR* strPathname );
        HRESULT LoadTargaFile( TCHAR* strPathname );
        HRESULT Restore( LPDIRECT3DDEVICE7 pd3dDevice );
        HRESULT CopyBitmapToSurface();
        HRESULT CopyRGBADataToSurface();

        TextureContainer( TCHAR* strName, DWORD dwStage, DWORD dwFlags );


        Any texture has to be restored before it can be used. This is done inside of [font="Courier New"][color="#000080"]InitDeviceObjects()[/color][/font] by a call to [font="Courier New"][color="#000080"]D3DTextr_RestoreAllTextures()[/color][/font]:

        HRESULT CMyD3DApplication::InitDeviceObjects()
        D3DTextr_RestoreAllTextures( m_pd3dDevice );

        // Set up the dimensions for the background image
        D3DVIEWPORT7 vp;
        m_Background[0].sy = (FLOAT)vp.dwHeight;
        m_Background[2].sy = (FLOAT)vp.dwHeight;
        m_Background[2].sx = (FLOAT)vp.dwWidth;
        m_Background[3].sx = (FLOAT)vp.dwWidth;

        return S_OK;
        The [font="Courier New"][color="#000080"]D3DTextr_RestoreAllTextures()[/color][/font] method calls at least the method [font="Courier New"][color="#000080"]TextureContainer::Restore()[/color][/font] which checks, for example, the device caps, sets up a new surface for the texture with these device caps and adjusts the texture size to be a power of 2 (take a look at that method in d3dtextr.cpp).
        [bquote]It turns on the texture management for hardware devices, limits the texture size, if the driver can't handle large textures (for example the Voodoo 2 and 3 boards can't handle textures bigger than 256x256). There's also a routine which makes the texture square, if the driver requires it . Another routine enumerate the texture formats and finds the closest format, which is supported by the device etc.[/bquote]
        The [font="Courier New"][color="#000080"]IDirect3DDevice7::GetViewport()[/color][/font] method retrieves the viewport parameters currently set for the device in a D3DVIEWPORT7 structure. dwWidth and dwHeight are the dimensions of the viewport on the render target surface, in pixels. Unless you are rendering only to a subset of the surface, these members should be set to the dimensions of the render target surface.


        The next method the framework calls is [font="Courier New"][color="#000080"]FrameMove()[/color][/font]:

        HRESULT CMyD3DApplication::FrameMove( FLOAT fTimeKey )
        return S_OK;

        Because we're not animating a scene, there doesn't have to be any code inside of this method.


        Now one of the key methods is called:

        HRESULT CMyD3DApplication::Render()
        // Begin the scene
        if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )
        // Draw the background
        m_pd3dDevice->SetTexture( 0, D3DTextr_GetSurface("lake.bmp") );
        m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX,
        m_Background, 4, 0 );

        // End the scene.

        return S_OK;

        This function calls the BeginScene()/EndScene() pair. The first function is called before performing rendering, the second after that. BeginScene causes the system to check its internal data structures, the availability and validity of rendering surfaces, and sets an internal flag to signal that a scene is in progress. Attempts to call rendering methods when a scene is not in progress fail, returning D3DERR_SCENE_NOT_IN_SCENE. Once your rendering is complete, you need to call EndScene(). It clears the internal flag that indicates that a scene is in progress, flushes the cached data and makes sure the rendering surfaces are OK.
        [bquote]If you like to use GDI functions, make sure that all GDI calls are made outside of the scene functions.[/bquote]
        The [font="Courier New"][color="#000080"]IDirect3DDevice7::SetTexture()[/color][/font] method assigns a texture to a given stage for a device. The first parameter must be a number in the range of 0-7 inclusive. Pass the texture interface pointer as the second parameter. This method method increments the reference count of the texture surface being assigned. When the texture is no longer needed, you should set the texture at the appropriate stage to NULL. If you fail to do this, the surface will not be released, resulting in a memory leak. Since Version 6, Direct3D maintains a list of up to eight current textures. So Direct3D supports the blending of up to eight textures onto a primitive at once. It blends these textures onto all of the primitives it renders. Only textures created as texture interface pointers can be used in the set of current textures.
        [bquote]Note: Textures under the [font="Courier New"][color="#000080"]IDirect3D2[/color][/font] interface were manipulated using texture handles. With the [font="Courier New"][color="#000080"]IDirect3D7[/color][/font] interface (and the legacy [font="Courier New"][color="#000080"]IDirect3D3[/color][/font] interface), you create and use textures through interface pointers to the texture surfaces. You obtain a texture surface interface pointer when you create the texture surface by calling the [font="Courier New"][color="#000080"]IDirectDraw7::CreateSurface()[/color][/font] which is called in the framework by [font="Courier New"][color="#000080"]D3Dtextr_CreateTextureFromFile()[/color][/font] method.[/bquote]
        When your application selects a texture as the current texture, it instructs the Direct3D device to apply the texture to all primitives that are rendered from that time until the current texture is changed again. If each primitive in a 3-D scene has its own texture, the texture must be set before each primitive is rendered.
        [bquote]Software devices do not support assigning a texture to more than one texture stage at a time.[/bquote]
        The [font="Courier New"][color="#000080"]IDirect3DDevice7::DrawPrimitive()[/color][/font] method renders the specified array of vertices as a sequence of geometric primitives of the specified type:

        HRESULT DrawPrimitive (
        D3DPRIMITIVETYPE dptPrimitiveType,
        DWORD dwVertexTypeDesc,
        LPVOID lpvVertices,
        DWORD dwVertexCount,
        DWORD dwFlags
        The first parameter is dptPrimitiveType. These are the types of primitive which could be rendered by this command:

        typedef enum _D3DPRIMITIVETYPE {
        D3DPT_POINTLIST = 1,
        D3DPT_LINELIST = 2,
        D3DPT_LINESTRIP = 3,
        D3DPT_TRIANGLEFAN = 6,
        D3DPT_FORCE_DWORD = 0x7fffffff,
        You can see the different primitives as pictures in the DirectX SDK. The second parameter of [font="Courier New"][color="#000080"]DrawPrimitive()[/color][/font], D3DFVF_TLVERTEX, describes the vertex format used for this set of primitives. The d3dtypes.h header file declares these flags to explicitly describe a vertex format and provides helper macros that act as common combinations of such flags. Each of the rendering methods of IDirect3Ddevice7 accepts a combination of these flags, and uses them to determine how to render primitives. Basically, these flags tell the system which vertex components--position, normal, colors, and the number of texture coordinates-your application uses and, indirectly, which parts of the rendering pipeline you want Direct3D to apply to them. In addition, the presence or absence of a particular vertex format flag communicates to the system which vertex component fields are present in memory, and which you've omitted. By using only the needed vertex components, your application can conserve memory and minimize the processing bandwidth required to render models. The d3dtypes.h header file defines the following helper macro that you can use to describe the vertex format declared by the D3DTLVERTEX structure:

        #define D3DFVF_TLVERTEX ( D3DFVF_XYZRHW | \
        D3DFVF_XYZRHW tells your system that your application is using transformed and lit vertices. Therefore, Direct3D doesn't transform your vertices with the world, view, or projection matrices, nor does it perform any lighting calculations. It passes them directly to the driver to be rasterized. D3DFVF_DIFFUSE indicates, that the Vertex format includes a diffuse color component. D3DFVF_SPECULAR indicates, that the vertex format includes a specular color component. D3DFVF_TEX1 shows us the number of texture coordinate sets for this vertex.

        // The vertex format description for this vertex
        // would be: (D3DFVF_XYZRHW |
        typedef struct _D3DTLVERTEX {
        D3DVALUE sx, sy, sz; /* Screen coordinates */
        D3DVALUE rhw; /* Reciprocal of homogeneous w */
        D3DCOLOR color; /* Vertex color */
        D3DCOLOR specular; /* Specular component of vertex */
        float tu1,tv1; // texture coordinates
        I've described an overloaded version in the [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font] section above.
        [bquote]Prior to DirectX 6.0, applications were required to use one of three vertex types - D3DVERTEX, D3DLVERTEX, and D3DTLVERTEX - depending on which parts of the Direct3D geometry pipeline were being used. With the introduction of more flexible vertex formats in DirectX 6.0, you can declare vertices in many more ways than before, but you can still use the predefined structures to describe untransformed and unlit vertices, untransformed but lit vertices, and vertices that are both transformed and lit.

        With the new flexible vertex format, vertices may contain both vertex color and vertex normal information. By default, Direct3D uses this information when it calculates lighting.
        The third parameter of [font="Courier New"][color="#000080"]DrawPrimitive()[/color][/font] is a pointer to the array of vertices to be used in the primitive sequence. After that the number of vertices in the array can be provided.
        [bquote]Indexed and nonindexed primitives: There are two ways of grouping the vertices that define a primitive: using nonidexed primitives and using indexed primitves. To create a nonindexed primitve, you fill an array with an ordered list of vertices. Ordered means that the order of the vertices in the array indicates how to build the triangles. The first triangle consists of the first three vertices, the second triangle consists of the next three vertices and so on. If you have two triangles that are connected, you'll have to specify the same vertices multiple times. To create an indexed primitive, you fill an array with an unordered list of vertices and specify the order with a second array (index array). This means that vertices can be shared by multiple triangles, simply by having multiple entries in the index array refer to the same vertex. Most 3D models share a number of vertices. Therefore, you can save bandwith and CPU time sharing these vertices among multiple triangles. Indexed primitves are called f.e. by

        // Display the object
        m_pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, D3DFVF_VERTEX,
        m_pCubeVertices, NUM_CUBE_VERTICES,
        m_pCubeIndices, NUM_CUBE_INDICES, 0 );
        The maximum number of vertices allowed is D3DMAXNUMVERTICES (0xFFFF). The last parameter is a flag, which has to be zero to render the primitive without waiting, or D3DDP_WAIT, which causes the method to wait until the polygons have been rendered before it returns, instead of returning as soon as the polygons have been sent to the card. (On scene-capture cards, the method returns as soon as the card responds.) This flag is typically used for debugging. Applications should not attempt to use this flag to ensure that a scene is up to date before continuing.


        When the app is finishing, the [font="Courier New"][color="#000080"]DeleteDeviceObjects()[/color][/font] is called:

        HRESULT CMyD3DApplication::DeleteDeviceObjects() { D3DTextr_InvalidateAllTextures(); return S_OK; }
        The call to [font="Courier New"][color="#000080"]D3DTextr_InvalidateAllTextures()[/color][/font] releases the allocated surfaces for the textures.


        The framework sends its last call to [font="Courier New"][color="#000080"]FinalCleanup()[/color][/font].

        HRESULT CMyD3DApplication::FinalCleanup()
        D3DTextr_DestroyTexture( "lake.bmp" );

        return S_OK;
        [font="Courier New"][color="#000080"] D3DTextr_DestroyTexture()[/color][/font] releases the allocated memory for the texture container.


        I hope you enjoyed our small trip into the word of the Direct3D 7 IM Framework. This was a very simple sample to show the new Direct3D 7 Framework. The next step is building a more complex sample...

        If you dislike or like it, give me a sign at [email="wolf@direct3d.net"]wolf@direct3d.net[/email]

    Sign in to follow this  
    Followers 0

    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

    There are no reviews to display.