Sign in to follow this  

Need help with DDS loading

This topic is 4224 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I am trying to write a DDS loader but I am having problems. After parsing the file the image data appears to be corrupted (as shown below). I am pretty sure that I parse the header properly but I cannot figure how to properly access the image data. broken dds loader I upload the texture with the following code.
unsigned char *pixels;
int width, height, compressFormat;
bool mipmapped;
LoadDDS("wall.dds", pixels, width, height, mipmapped, compressFormat);
int imgSize = std::max(1, width / 4) * std::max(1, height / 4) * 8; //DXT1

GLuint texId;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glCompressedTexImage2DARB(GL_TEXTURE_2D, 0, compressFormat, width, height, 0, imgSize, pixels);
delete [] pixels;

The code for the LoadDDS function is shown below.
struct DDSHeader_t
{
    unsigned magic;
    
    unsigned surfaceSize;
    unsigned surfaceFlags;
    unsigned surfaceHeight;
    unsigned surfaceWidth;
    unsigned surfacePitchOrLinearSize;
    unsigned surfaceDepth;
    unsigned surfaceMipMapCount;
    unsigned surfaceReserved[11];
    
    unsigned PFSize;
    unsigned PFFlags;
    unsigned PFFourCC;
    unsigned PFBPP;
    unsigned PFRedMask;
    unsigned PFGreenMask;
    unsigned PFBlueMask;
    unsigned PFAlphaMask;

    unsigned caps1;
    unsigned caps2;
    unsigned capsReserved[2];
    
    unsigned reserved;
};

void LoadDDS(const char *filename, unsigned char *&pixels,
             int &width, int &height, bool &mipmapped, int &compressFormat)
{
    std::ifstream inFile(filename);
    unsigned char *buffer;
    unsigned char *data;
    DDSHeader_t ddsHeader;

    bool compressed;

    pixels = NULL;
    compressFormat = 0;
        
    inFile.seekg(0, std::ios::end);
    int length = inFile.tellg();
    inFile.seekg(0, std::ios::beg);

    data = new unsigned char[length];
    inFile.read((char *)data, length);
    inFile.close();
    inFile.clear();
    
    buffer = data;
    
    ddsHeader.magic = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfaceSize = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfaceFlags = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfaceHeight = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfaceWidth = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfacePitchOrLinearSize = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfaceDepth = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.surfaceMipMapCount = *(unsigned *)buffer;
    buffer += 4;

    //Skip over reserved bytes.
    buffer += 44;
    
    ddsHeader.PFSize = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFFlags = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFFourCC = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFBPP = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFRedMask = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFGreenMask = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFBlueMask = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.PFAlphaMask = *(unsigned *)buffer;
    buffer += 4;
    
    ddsHeader.caps1 = *(unsigned *)buffer;
    buffer += 4;
    ddsHeader.caps2 = *(unsigned *)buffer;
    buffer += 4;
    
    //Skip reserved stuff.
    buffer += 12;

    switch (ddsHeader.PFFourCC)
    {
        case DXT1:
            if (ddsHeader.PFFlags & DDPF_ALPHAPIXELS)
                compressFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
            else
                compressFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
            break;
        case DXT2:
            delete [] data;
            return;
        case DXT3:
            compressFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
            break;
        case DXT4:
            delete [] data;
            return;
        case DXT5:
            compressFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
            break;
        default:
            delete [] data;
            return;
    }

    mipmapped = (ddsHeader.surfaceMipMapCount > 1);
    compressed = ((ddsHeader.PFFlags & DDPF_FOURCC) && (ddsHeader.surfaceFlags & DDSD_LINEARSIZE));
    width = ddsHeader.surfaceWidth;
    height = ddsHeader.surfaceHeight;    
    
    if (compressed )
    {
        unsigned pSize = 0;
        int factor = 16;
        int w = width;
        int h = height;
        
        if (ddsHeader.PFFourCC == DXT1)
            factor = 8;
        
        //Compute the buffer size.
        for (unsigned i = 0; i < ddsHeader.surfaceMipMapCount; i++)
        {
            pSize += std::max(1, w / 4) * std::max(1, h / 4) * factor;
            w /= 2;
            h /= 2;
        }
        pixels = new unsigned char[pSize];

        //Copy the image data?
        memcpy(pixels, buffer, pSize);
    }
    else
    {
        //Ignore uncompressed files for now.
    }

    delete [] data;
}


Share this post


Link to post
Share on other sites
Hi this is the source of a loader I found somewhere in these forums a few days ago


//-----------------------------------------------------------------------------
// Name: ogl_dds_texture_loader.cpp
// Author: Kevin Harris
// Last Modified: 02/01/05
// Description: This sample demonstrates how to use ARB_texture_compression
// to load .dds compressed texture files.
//
// Control Keys: Left Mouse Button - Spin the view
// F1 - Toggle between using a compressed texture and a
// regular texture.
// Up Arrow - View moves forward
// Down Arrow - View moves backward
//
// NOTE: To create the compressed version of the original "lena.bmp" texture,
// I opened the texture file in Photoshop and flipped the image
// vertically and saved it out as "lena_flipped.bmp". This is to
// compensate for the .dds file format which is inverted.
// Some programmers opt not to flip the image itself. Instead they
// either flip it in their texture loader or adjust the texture
// coordinates to flip it. I chose to flip the actual image since
// I only had to fix one file. I've been tolds that this is how most
// programmers choose to deal with this problem. If you have a large
// collection of files to convert, there are command-line tools which
// can automate the process.
//
// I then used ATI's tool called "TheCompressonator" to generate mip-map
// levels and compress the file using DXT1 compression. I originally
// tried to use the "DirectX Texture Tool" that ships with DirectX 9.0c,
// but I had problems loading the resulting .dds files. The
// dwLinearSize member of its DDSURFACEDESC2 struct always returns 0,
// which causes problems since I can't load 0!
//
// You can get ATI's "TheCompressonator" tool from here:
//
// http://www.ati.com/developer/compressonator.html
//
//-----------------------------------------------------------------------------

#define STRICT
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <stdio.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
#include <ddraw.h> // Required for DirectX's DDSURFACEDESC2 structure definition
#include "resource.h"

//-----------------------------------------------------------------------------
// FUNCTION POINTERS FOR OPENGL EXTENSIONS
//-----------------------------------------------------------------------------

// For convenience, this project ships with its own "glext.h" extension header
// file. If you have trouble running this sample, it may be that this "glext.h"
// file is defining something that your hardware doesn’t actually support.
// Try recompiling the sample using your own local, vendor-specific "glext.h"
// header file.

#include "glext.h" // Sample's header file
//#include <GL/glext.h> // Your local header file

// ARB_texture_compression
PFNGLCOMPRESSEDTEXIMAGE2DARBPROC glCompressedTexImage2DARB;

//-----------------------------------------------------------------------------
// GLOBALS
//-----------------------------------------------------------------------------
HWND g_hWnd = NULL;
HDC g_hDC = NULL;
HGLRC g_hRC = NULL;
GLuint g_textureID = -1;
GLuint g_compressedTextureID = -1;

bool g_bUseCompressedTexture = true;

float g_fDistance = -3.0f;
float g_fSpinX = 0.0f;
float g_fSpinY = 0.0f;

struct Vertex
{
float tu, tv;
float x, y, z;
};

Vertex g_quadVertices[] =
{
{ 0.0f,0.0f, -1.0f,-1.0f, 0.0f },
{ 1.0f,0.0f, 1.0f,-1.0f, 0.0f },
{ 1.0f,1.0f, 1.0f, 1.0f, 0.0f },
{ 0.0f,1.0f, -1.0f, 1.0f, 0.0f }
};

struct DDS_IMAGE_DATA
{
GLsizei width;
GLsizei height;
GLint components;
GLenum format;
int numMipMaps;
GLubyte *pixels;
};

//-----------------------------------------------------------------------------
// PROTOTYPES
//-----------------------------------------------------------------------------
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow);
LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
void loadTexture(void);
void loadCompressedTexture(void);
DDS_IMAGE_DATA* loadDDSTextureFile(const char *filename);
void init(void);
void render(void);
void shutDown(void);

//-----------------------------------------------------------------------------
// Name: WinMain()
// Desc: The application's entry point
//-----------------------------------------------------------------------------
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
WNDCLASSEX winClass;
MSG uMsg;

memset(&uMsg,0,sizeof(uMsg));

winClass.lpszClassName = "MY_WINDOWS_CLASS";
winClass.cbSize = sizeof(WNDCLASSEX);
winClass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
winClass.lpfnWndProc = WindowProc;
winClass.hInstance = hInstance;
winClass.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_OPENGL_ICON);
winClass.hIconSm = LoadIcon(hInstance, (LPCTSTR)IDI_OPENGL_ICON);
winClass.hCursor = LoadCursor(NULL, IDC_ARROW);
winClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
winClass.lpszMenuName = NULL;
winClass.cbClsExtra = 0;
winClass.cbWndExtra = 0;

if( !RegisterClassEx(&winClass) )
return E_FAIL;

g_hWnd = CreateWindowEx( NULL, "MY_WINDOWS_CLASS",
"OpenGL - Texture Compression Using .DDS Files",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0, 0, 640, 480, NULL, NULL, hInstance, NULL );

if( g_hWnd == NULL )
return E_FAIL;

ShowWindow( g_hWnd, nCmdShow );
UpdateWindow( g_hWnd );

init();

while( uMsg.message != WM_QUIT )
{
if( PeekMessage( &uMsg, NULL, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &uMsg );
DispatchMessage( &uMsg );
}
else
render();
}

shutDown();

UnregisterClass( "MY_WINDOWS_CLASS", hInstance );

return uMsg.wParam;
}

//-----------------------------------------------------------------------------
// Name: WindowProc()
// Desc: The window's message handler
//-----------------------------------------------------------------------------
LRESULT CALLBACK WindowProc( HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam )
{
static POINT ptLastMousePosit;
static POINT ptCurrentMousePosit;
static bool bMousing;

switch( msg )
{
case WM_KEYDOWN:
{
switch( wParam )
{
case VK_ESCAPE:
PostQuitMessage(0);
break;

case VK_F1:
g_bUseCompressedTexture = !g_bUseCompressedTexture;
break;

case 38: // Up Arrow Key
g_fDistance += 1.0f;
break;

case 40: // Down Arrow Key
g_fDistance -= 1.0f;
break;
}
}
break;

case WM_LBUTTONDOWN:
{
ptLastMousePosit.x = ptCurrentMousePosit.x = LOWORD (lParam);
ptLastMousePosit.y = ptCurrentMousePosit.y = HIWORD (lParam);
bMousing = true;
}
break;

case WM_LBUTTONUP:
{
bMousing = false;
}
break;

case WM_MOUSEMOVE:
{
ptCurrentMousePosit.x = LOWORD (lParam);
ptCurrentMousePosit.y = HIWORD (lParam);

if( bMousing )
{
g_fSpinX -= (ptCurrentMousePosit.x - ptLastMousePosit.x);
g_fSpinY -= (ptCurrentMousePosit.y - ptLastMousePosit.y);
}

ptLastMousePosit.x = ptCurrentMousePosit.x;
ptLastMousePosit.y = ptCurrentMousePosit.y;
}
break;

case WM_SIZE:
{
int nWidth = LOWORD(lParam);
int nHeight = HIWORD(lParam);
glViewport(0, 0, nWidth, nHeight);

glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 45.0, (GLdouble)nWidth / (GLdouble)nHeight, 0.1, 100.0);
}
break;

case WM_CLOSE:
{
PostQuitMessage(0);
}

case WM_DESTROY:
{
PostQuitMessage(0);
}
break;

default:
{
return DefWindowProc( hWnd, msg, wParam, lParam );
}
break;
}

return 0;
}

//-----------------------------------------------------------------------------
// Name: loadTexture()
// Desc:
//-----------------------------------------------------------------------------
void loadTexture( void )
{
AUX_RGBImageRec *pTextureImage = auxDIBImageLoad( ".\\lena.bmp" );

if( pTextureImage != NULL )
{
glGenTextures( 1, &g_textureID );

glBindTexture( GL_TEXTURE_2D, g_textureID );

glTexParameteri( GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR );

glTexImage2D( GL_TEXTURE_2D, 0, 3, pTextureImage->sizeX, pTextureImage->sizeY, 0,
GL_RGB, GL_UNSIGNED_BYTE, pTextureImage->data );
}

if( pTextureImage )
{
if( pTextureImage->data )
free( pTextureImage->data );

free( pTextureImage );
}
}

//-----------------------------------------------------------------------------
// Name: loadCompressedTexture()
// Desc:
//-----------------------------------------------------------------------------
void loadCompressedTexture( void )
{
// NOTE: Unlike "lena.bmp", "lena.dds" actually contains its own mip-map
// levels, which are also compressed.
DDS_IMAGE_DATA *pDDSImageData = loadDDSTextureFile( "lena.dds" );

if( pDDSImageData != NULL )
{
int nHeight = pDDSImageData->height;
int nWidth = pDDSImageData->width;
int nNumMipMaps = pDDSImageData->numMipMaps;

int nBlockSize;

if( pDDSImageData->format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT )
nBlockSize = 8;
else
nBlockSize = 16;

glGenTextures( 1, &g_compressedTextureID );
glBindTexture( GL_TEXTURE_2D, g_compressedTextureID );

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

int nSize;
int nOffset = 0;

// Load the mip-map levels

for( int i = 0; i < nNumMipMaps; ++i )
{
if( nWidth == 0 ) nWidth = 1;
if( nHeight == 0 ) nHeight = 1;

nSize = ((nWidth+3)/4) * ((nHeight+3)/4) * nBlockSize;

glCompressedTexImage2DARB( GL_TEXTURE_2D,
i,
pDDSImageData->format,
nWidth,
nHeight,
0,
nSize,
pDDSImageData->pixels + nOffset );

nOffset += nSize;

// Half the image size for the next mip-map level...
nWidth = (nWidth / 2);
nHeight = (nHeight / 2);
}
}

if( pDDSImageData != NULL )
{
if( pDDSImageData->pixels != NULL )
free( pDDSImageData->pixels );

free( pDDSImageData );
}
}

//-----------------------------------------------------------------------------
// Name: loadDDSTextureFile()
// Desc:
//-----------------------------------------------------------------------------
DDS_IMAGE_DATA* loadDDSTextureFile( const char *filename )
{
DDS_IMAGE_DATA *pDDSImageData;
DDSURFACEDESC2 ddsd;
char filecode[4];
FILE *pFile;
int factor;
int bufferSize;

// Open the file
pFile = fopen( filename, "rb" );

if( pFile == NULL )
{
char str[255];
sprintf( str, "loadDDSTextureFile couldn't find, or failed to load \"%s\"", filename );
MessageBox( NULL, str, "ERROR", MB_OK|MB_ICONEXCLAMATION );
return NULL;
}

// Verify the file is a true .dds file
fread( filecode, 1, 4, pFile );

if( strncmp( filecode, "DDS ", 4 ) != 0 )
{
char str[255];
sprintf( str, "The file \"%s\" doesn't appear to be a valid .dds file!", filename );
MessageBox( NULL, str, "ERROR", MB_OK|MB_ICONEXCLAMATION );
fclose( pFile );
return NULL;
}

// Get the surface descriptor
fread( &ddsd, sizeof(ddsd), 1, pFile );

pDDSImageData = (DDS_IMAGE_DATA*) malloc(sizeof(DDS_IMAGE_DATA));

memset( pDDSImageData, 0, sizeof(DDS_IMAGE_DATA) );

//
// This .dds loader supports the loading of compressed formats DXT1, DXT3
// and DXT5.
//

switch( ddsd.ddpfPixelFormat.dwFourCC )
{
case FOURCC_DXT1:
// DXT1's compression ratio is 8:1
pDDSImageData->format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
factor = 2;
break;

case FOURCC_DXT3:
// DXT3's compression ratio is 4:1
pDDSImageData->format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
factor = 4;
break;

case FOURCC_DXT5:
// DXT5's compression ratio is 4:1
pDDSImageData->format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
factor = 4;
break;

default:
char str[255];
sprintf( str, "The file \"%s\" doesn't appear to be compressed "
"using DXT1, DXT3, or DXT5!", filename );
MessageBox( NULL, str, "ERROR", MB_OK|MB_ICONEXCLAMATION );
return NULL;
}

//
// How big will the buffer need to be to load all of the pixel data
// including mip-maps?
//

if( ddsd.dwLinearSize == 0 )
{
MessageBox( NULL, "dwLinearSize is 0!","ERROR",
MB_OK|MB_ICONEXCLAMATION);
}

if( ddsd.dwMipMapCount > 1 )
bufferSize = ddsd.dwLinearSize * factor;
else
bufferSize = ddsd.dwLinearSize;

pDDSImageData->pixels = (unsigned char*)malloc(bufferSize * sizeof(unsigned char));

fread( pDDSImageData->pixels, 1, bufferSize, pFile );

// Close the file
fclose( pFile );

pDDSImageData->width = ddsd.dwWidth;
pDDSImageData->height = ddsd.dwHeight;
pDDSImageData->numMipMaps = ddsd.dwMipMapCount;

if( ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 )
pDDSImageData->components = 3;
else
pDDSImageData->components = 4;

return pDDSImageData;
}

//-----------------------------------------------------------------------------
// Name: init()
// Desc:
//-----------------------------------------------------------------------------
void init( void )
{
GLuint PixelFormat;

PIXELFORMATDESCRIPTOR pfd;
memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));

pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW |PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 16;
pfd.cDepthBits = 16;

g_hDC = GetDC( g_hWnd );
PixelFormat = ChoosePixelFormat( g_hDC, &pfd );
SetPixelFormat( g_hDC, PixelFormat, &pfd);
g_hRC = wglCreateContext( g_hDC );
wglMakeCurrent( g_hDC, g_hRC );

glClearColor( 0.35f, 0.53f, 0.7f, 1.0f );
glEnable(GL_TEXTURE_2D);

glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 45.0f, 640.0f / 480.0f, 0.1f, 100.0f);

//
// If the required extension is present, get the addresses of its
// functions that we wish to use...
//

char *ext = (char*)glGetString( GL_EXTENSIONS );

if( strstr( ext, "ARB_texture_compression" ) == NULL )
{
MessageBox(NULL,"ARB_texture_compression extension was not found",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return;
}
else
{
glCompressedTexImage2DARB = (PFNGLCOMPRESSEDTEXIMAGE2DARBPROC)wglGetProcAddress("glCompressedTexImage2DARB");

if( !glCompressedTexImage2DARB )
{
MessageBox(NULL,"One or more ARB_texture_compression functions were not found",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return;
}
}

loadTexture();
loadCompressedTexture();
}

//-----------------------------------------------------------------------------
// Name: shutDown()
// Desc:
//-----------------------------------------------------------------------------
void shutDown( void )
{
glDeleteTextures( 1, &g_textureID );
glDeleteTextures( 1, &g_compressedTextureID );

if( g_hRC != NULL )
{
wglMakeCurrent( NULL, NULL );
wglDeleteContext( g_hRC );
g_hRC = NULL;
}

if( g_hDC != NULL )
{
ReleaseDC( g_hWnd, g_hDC );
g_hDC = NULL;
}
}

//-----------------------------------------------------------------------------
// Name: render()
// Desc:
//-----------------------------------------------------------------------------
void render( void )
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glTranslatef( 0.0f, 0.0f, g_fDistance );
glRotatef( -g_fSpinY, 1.0f, 0.0f, 0.0f );
glRotatef( -g_fSpinX, 0.0f, 1.0f, 0.0f );

if( g_bUseCompressedTexture == true )
glBindTexture( GL_TEXTURE_2D, g_compressedTextureID );
else
glBindTexture( GL_TEXTURE_2D, g_textureID );

glInterleavedArrays( GL_T2F_V3F, 0, g_quadVertices );
glDrawArrays( GL_QUADS, 0, 4 );

glDisable(GL_BLEND);

SwapBuffers( g_hDC );
}


Share this post


Link to post
Share on other sites
I found the problem. I did not open the file in binary mode.
Replacing
std::ifstream inFile(filename);
with
std::ifstream inFile(filename, std::ios::binary);
fixed the problem.

Share this post


Link to post
Share on other sites

This topic is 4224 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

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

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

Sign in to follow this