Sign in to follow this  

D3DPRESENT_INTERVAL_ONE doesn't work for 100%

This topic is 3585 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 have application which shows 2D animation in Direct3D scene with 30 fps. To prevent image tearing, I use D3DPRESENT_INTERVAL_ONE parameter in PresentationInterval. It has some effect. If I set D3DPRESENT_INTERVAL_IMMEDIATE, I see strong image tearing, and time intervals between frames are 30 ms +/- 2 ms. With D3DPRESENT_INTERVAL_ONE, time intervals are not equal: 30, 27, 23, 35, 23 ... So, this flag makes something. However, I still see slight image tearing in the top part of the screen. I try to guess what else can cause this behavior. Window doesn't draw its background. Synchronization is not necessary, since everything is done in main application thread. What can be a reason of this? DirectX version: October 2005 Application: Win32, VC++ 2005. Display adapter: NVIDIA GeForce 8500 GT Processor: Pentium IV Screen refresh rate: 85 Hertz It looks like I made 90% of work, but still missing somethimg.

Share this post


Link to post
Share on other sites
This is full application code (it is not too complicated). Basically, program draws 2D bitmap in textured rectangle. Texture is created by D3DXCreateTextureFromFileInMemoryEx function, and information in memory bitmap file is updates once per 30 ms. To see better tearing effect, I use two buffers with different gray values, which gives strong difference between every two images. Time counting (TimeCounter.h) doesn't affect program logic and can be ignored.


//-----------------------------------------------------------------------------
// File: CreateDevice.cpp
//
// Desc: This is the first tutorial for using Direct3D. In this tutorial, all
// we are doing is creating a Direct3D device and using it to clear the
// window.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#include <d3d9.h>
#include <d3dx9.h>
#include <stdio.h>

#define BLACK_AND_WHITE

#define BLACK_LEVEL 200
#define WHITE_LEVEL 100



#pragma warning( disable : 4996 ) // disable deprecated warning
#pragma warning( disable : 4995 ) // disable deprecated warning
#include <strsafe.h>
//#pragma warning( default : 4996 )

#define TIME_COUNT_EX_ENABLED
#include "TimeCounter.h"

#include <Mmsystem.h>

void PostInitialize(HWND hWnd);
void Render2D();
void CreateMemoryBmpFile();
void CreateTexture();
void OnSize(HWND hWnd);
void SetViewParameters(HWND hWnd);
void ReleaseVerticesAndTexture();
void CreateVertices();
void CALLBACK TimeProc(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2);
void DrawFrame();
void FillD3Dpp(HWND hWnd);


#define WM_APP_FRAME (WM_APP + 100)


struct PANELVERTEX
{
FLOAT x, y, z;
DWORD color;
FLOAT u, v;
};

#define D3DFVF_PANELVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)

LPDIRECT3DVERTEXBUFFER9 g_pVertices = NULL;
LPDIRECT3DTEXTURE9 g_pTexture = NULL;

#define IMAGE_SIZE 1024
#define FULL_IMAGE_HEIGHT (IMAGE_SIZE+256)
#define MAX_OFFSET 256
BYTE* g_pMemoryBmpFile = NULL;
BYTE* g_pFullImage = NULL;
RGBQUAD g_Palette[256];
PALETTEENTRY g_PaletteEntry[256];
int g_nFileSize;
int g_nOffset = 0;
BYTE* g_pPixelsPtr;
DWORD g_nTickCount = 0;
DWORD g_nFrameCounter = 0;
D3DPRESENT_PARAMETERS d3dpp;
D3DDISPLAYMODE d3ddm;

CTimeCounter* g_pTimeCounter = NULL;
HWND g_hWnd = NULL;
MMRESULT g_nTimerID = 0;
BYTE* g_pWhiteImage = NULL;
BYTE* g_pBlackImage = NULL;
BOOL g_bWhite = FALSE; // TRUE - next image to draw is white

#define TIMER_ANIMATION 101
#define TIMER_FPS 102



//-----------------------------------------------------------------------------
// Global variables
//-----------------------------------------------------------------------------
LPDIRECT3D9 g_pD3D = NULL; // Used to create the D3DDevice
LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; // Our rendering device



//-----------------------------------------------------------------------------
// Name: InitD3D()
// Desc: Initializes Direct3D
//-----------------------------------------------------------------------------
HRESULT InitD3D( HWND hWnd )
{

// Create the D3D object, which is needed to create the D3DDevice.
if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
return E_FAIL;

// *****************************

if ( FAILED(g_pD3D->GetAdapterDisplayMode (D3DADAPTER_DEFAULT, &d3ddm)) )
{
return E_FAIL;
}

D3DCAPS9 d3dCaps;

if (FAILED(g_pD3D->GetDeviceCaps (D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &d3dCaps)))
return E_FAIL;

// *****************************

FillD3Dpp(hWnd);

// Create the Direct3D device. Here we are using the default adapter (most
// systems only have one, unless they have multiple graphics hardware cards
// installed) and requesting the HAL (which is saying we want the hardware
// device rather than a software one). Software vertex processing is
// specified since we know it will work on all cards. On cards that support
// hardware vertex processing, though, we would see a big performance gain
// by specifying hardware vertex processing.

if (d3dCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT)
{
// Create device with hardware vertex processing
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_HARDWARE_VERTEXPROCESSING,
&d3dpp, &g_pd3dDevice ) ) )
{
return E_FAIL;
}
}
else
{
// Create device with software vertex processing
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, &g_pd3dDevice ) ) )
{
return E_FAIL;
}
}


// Device state would normally be set here

return S_OK;
}

void FillD3Dpp(HWND hWnd)
{
RECT rect;
GetClientRect(hWnd, &rect);

// Set up the structure used to create the D3DDevice. Most parameters are
// zeroed out. We set Windowed to TRUE, since we want to do D3D in a
// window, and then set the SwapEffect to "discard", which is the most
// efficient method of presenting the back buffer to the display. And
// we request a back buffer format that matches the current desktop display
// format.
ZeroMemory( &d3dpp, sizeof(d3dpp) );
d3dpp.Windowed = TRUE;
d3dpp.BackBufferFormat = d3ddm.Format;
d3dpp.BackBufferWidth = rect.right - rect.left;
d3dpp.BackBufferHeight = rect.bottom - rect.top;
d3dpp.BackBufferCount = 1;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; //D3DSWAPEFFECT_COPY //D3DSWAPEFFECT_FLIP;
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; //D3DPRESENT_INTERVAL_IMMEDIATE;
}


void ReleaseVerticesAndTexture()
{
if( g_pVertices != NULL)
g_pVertices->Release();

if( g_pTexture != NULL)
g_pTexture->Release();
}


//-----------------------------------------------------------------------------
// Name: Cleanup()
// Desc: Releases all previously initialized objects
//-----------------------------------------------------------------------------
VOID Cleanup()
{
ReleaseVerticesAndTexture();

if( g_pd3dDevice != NULL)
g_pd3dDevice->Release();

if( g_pD3D != NULL)
g_pD3D->Release();

if ( g_pMemoryBmpFile )
{
delete[] g_pMemoryBmpFile;
g_pMemoryBmpFile = NULL;
}

if ( g_pFullImage )
{
delete[] g_pFullImage;
g_pFullImage = NULL;
}

if ( g_pWhiteImage )
{
delete[] g_pWhiteImage;
g_pWhiteImage = NULL;
}

if ( g_pBlackImage )
{
delete[] g_pBlackImage;
g_pBlackImage = NULL;
}

if ( g_pTimeCounter )
{
delete g_pTimeCounter;
g_pTimeCounter = NULL;
}

if ( g_nTimerID )
{
timeKillEvent(g_nTimerID);
g_nTimerID = 0;
}
}




//-----------------------------------------------------------------------------
// Name: Render()
// Desc: Draws the scene
//-----------------------------------------------------------------------------
VOID Render()
{
if( NULL == g_pd3dDevice )
return;

// Clear the backbuffer to a blue color
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );

// Begin the scene
if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )
{
// Rendering of scene objects can happen here

Render2D();

// End the scene
g_pd3dDevice->EndScene();
}

// Present the backbuffer contents to the display
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}


void CALLBACK TimeProc(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2)
{
PostMessage(g_hWnd, WM_APP_FRAME, 0, 0);
}


void DrawFrame()
{
#ifdef BLACK_AND_WHITE

g_bWhite = ! g_bWhite;

if ( g_bWhite )
{
memcpy(g_pPixelsPtr, g_pWhiteImage, IMAGE_SIZE*IMAGE_SIZE);
}
else
{
memcpy(g_pPixelsPtr, g_pBlackImage, IMAGE_SIZE*IMAGE_SIZE);
}
#else
g_nOffset++;

if ( g_nOffset >= MAX_OFFSET )
{
g_nOffset = 0;
}


memcpy(g_pPixelsPtr, g_pFullImage + g_nOffset*IMAGE_SIZE, IMAGE_SIZE*IMAGE_SIZE);
#endif

TIME_START(t, L"Create Texture");
CreateTexture();
TIME_END(t);

TIME_START(t1, L"Draw image");


InvalidateRect(g_hWnd, NULL, FALSE);
UpdateWindow(g_hWnd);

TIME_END(t1);

g_nFrameCounter++;


if ( g_pTimeCounter != NULL )
{
delete g_pTimeCounter;
g_pTimeCounter = NULL;
}

g_pTimeCounter = new CTimeCounter(L"Interval");
g_pTimeCounter->SetShowTime();
}

//-----------------------------------------------------------------------------
// Name: MsgProc()
// Desc: The window's message handler
//-----------------------------------------------------------------------------
LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
TCHAR s[50];
int fps;
DWORD tickCount;

switch( msg )
{
case WM_CREATE:
//SetTimer(hWnd, TIMER_ANIMATION, 30, NULL);

g_nTimerID = timeSetEvent(30, 3, TimeProc, NULL, TIME_PERIODIC);

SetTimer(hWnd, TIMER_FPS, 1000, NULL);
return 0;

case WM_APP_FRAME:
DrawFrame();
break;

case WM_TIMER:
if ( wParam == TIMER_FPS )
{
if ( g_nTickCount == 0 )
{
g_nTickCount = GetTickCount();
g_nFrameCounter = 0;
}
else
{
tickCount = GetTickCount();

fps = g_nFrameCounter * 1000 / (tickCount - g_nTickCount);
_stprintf(s, L"%d", fps);
SetWindowText(hWnd, s);

g_nTickCount = tickCount;
g_nFrameCounter = 0;
}
}

return 0;

case WM_DESTROY:
Cleanup();
PostQuitMessage( 0 );
return 0;

case WM_PAINT:
Render();
ValidateRect( hWnd, NULL );
return 0;

case WM_SIZE:
if ( wParam != SIZE_MINIMIZED ) // don't handle minimized window
{
OnSize(hWnd);
//InvalidateRect(hWnd, NULL, FALSE);
}
return 0;

case WM_ERASEBKGND:
return TRUE;
}

return DefWindowProc( hWnd, msg, wParam, lParam );
}




//-----------------------------------------------------------------------------
// Name: wWinMain()
// Desc: The application's entry point
//-----------------------------------------------------------------------------
INT WINAPI wWinMain( HINSTANCE hInst, HINSTANCE, LPWSTR, INT )
{
// Register the window class
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC | CS_HREDRAW | CS_VREDRAW,
MsgProc, 0L, 0L,
GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
L"D3D Tutorial", NULL };
RegisterClassEx( &wc );



// Create the application's window
HWND hWnd = CreateWindow( L"D3D Tutorial", L"D3D Tutorial 01: CreateDevice",
WS_OVERLAPPEDWINDOW,
250, 150, 800, 700,
NULL, NULL, wc.hInstance, NULL );


g_hWnd = hWnd;

// Initialize Direct3D
if( SUCCEEDED( InitD3D( hWnd ) ) )
{
PostInitialize(hWnd);

// Show the window
ShowWindow( hWnd, SW_SHOWDEFAULT );
UpdateWindow( hWnd );

// Enter the message loop
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}

UnregisterClass( L"D3D Tutorial", wc.hInstance );
return 0;
}

void CreateTexture()
{
if( g_pTexture != NULL)
g_pTexture->Release();

HRESULT hResult = D3DXCreateTextureFromFileInMemoryEx(
g_pd3dDevice,
g_pMemoryBmpFile,
g_nFileSize,
D3DX_DEFAULT,
D3DX_DEFAULT,
1,
0,
D3DFMT_UNKNOWN,
D3DPOOL_MANAGED,
D3DX_DEFAULT,
D3DX_DEFAULT,
0,
NULL,
NULL, //g_PaletteEntry, // can be NULL - doesn't matter
&g_pTexture );

if (hResult != D3D_OK)
{
OutputDebugStringW(L"D3DXCreateTextureFromFileInMemoryEx failed.");
}

}


void OnSize(HWND hWnd)
{
FillD3Dpp(hWnd);

ReleaseVerticesAndTexture();

g_pd3dDevice->Reset(&d3dpp);

CreateVertices();
CreateTexture();

g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
g_pd3dDevice->SetVertexShader(NULL);
g_pd3dDevice->SetFVF(D3DFVF_PANELVERTEX);

g_pd3dDevice->SetStreamSource(0, g_pVertices, 0, sizeof(PANELVERTEX));

SetViewParameters(hWnd);
}

void SetViewParameters(HWND hWnd)
{
RECT rect;
GetClientRect(hWnd, &rect);

float WindowWidth = (float)(rect.right - rect.left);
float WindowHeight = (float)(rect.bottom - rect.top);

float viewWidth = IMAGE_SIZE;
float viewHeight = IMAGE_SIZE;

if ( WindowWidth > WindowHeight )
{
viewWidth *= (WindowWidth/WindowHeight);
}
else
{
viewHeight *= (WindowHeight/WindowWidth);
}


D3DXMATRIX Ortho2D;
D3DXMATRIX Identity;

D3DXMatrixOrthoLH(&Ortho2D, viewWidth, viewHeight, 0.0f, 1.0f);
D3DXMatrixIdentity(&Identity);

g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &Ortho2D);
g_pd3dDevice->SetTransform(D3DTS_WORLD, &Identity);
g_pd3dDevice->SetTransform(D3DTS_VIEW, &Identity);
}

void PostInitialize(HWND hWnd)
{
SetViewParameters(hWnd);

g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

CreateVertices();

CreateMemoryBmpFile();

CreateTexture();

g_pd3dDevice->SetVertexShader(NULL);
g_pd3dDevice->SetFVF(D3DFVF_PANELVERTEX);

g_pd3dDevice->SetStreamSource(0, g_pVertices, 0, sizeof(PANELVERTEX));

}

void CreateVertices()
{
float PanelWidth = IMAGE_SIZE;
float PanelHeight = IMAGE_SIZE;


g_pd3dDevice->CreateVertexBuffer(4 * sizeof(PANELVERTEX), D3DUSAGE_WRITEONLY,
D3DFVF_PANELVERTEX, D3DPOOL_MANAGED, &g_pVertices, NULL);

PANELVERTEX* pVertices = NULL;
g_pVertices->Lock(0, 4 * sizeof(PANELVERTEX), (void**)&pVertices, 0);

//Set all the colors to white
pVertices[0].color = pVertices[1].color = pVertices[2].color = pVertices[3].color = 0xffffffff;

//Set positions and texture coordinates
pVertices[0].x = pVertices[3].x = -PanelWidth / 2.0f;
pVertices[1].x = pVertices[2].x = PanelWidth / 2.0f;

pVertices[0].y = pVertices[1].y = PanelHeight / 2.0f;
pVertices[2].y = pVertices[3].y = -PanelHeight / 2.0f;

pVertices[0].z = pVertices[1].z = pVertices[2].z = pVertices[3].z = 1.0f;

pVertices[1].u = pVertices[2].u = 1.0f;
pVertices[0].u = pVertices[3].u = 0.0f;

pVertices[0].v = pVertices[1].v = 0.0f;
pVertices[2].v = pVertices[3].v = 1.0f;

g_pVertices->Unlock();
}

void Render2D()
{
g_pd3dDevice->SetTexture(0, g_pTexture);

g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
}

void CreateMemoryBmpFile()
{
g_pBlackImage = new BYTE[IMAGE_SIZE*IMAGE_SIZE];
memset(g_pBlackImage, BLACK_LEVEL, IMAGE_SIZE*IMAGE_SIZE);

g_pWhiteImage = new BYTE[IMAGE_SIZE*IMAGE_SIZE];
memset(g_pWhiteImage, WHITE_LEVEL, IMAGE_SIZE*IMAGE_SIZE);

g_pFullImage = new BYTE[IMAGE_SIZE*FULL_IMAGE_HEIGHT];
BYTE* ptr = g_pFullImage;

for(int i = 0; i < FULL_IMAGE_HEIGHT; i++)
{
for(int j = 0; j < IMAGE_SIZE; j++)
{
if ( j < 10 || j > IMAGE_SIZE - 10 )
{
*ptr++ = 255; // frame
}
else
{
*ptr++ = (BYTE)i;
}

}
}

g_nFileSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) +
sizeof(g_Palette) + IMAGE_SIZE*IMAGE_SIZE;

g_pMemoryBmpFile = new BYTE[g_nFileSize];

BYTE* p = g_pMemoryBmpFile;

// BITMAPFILEHEADER
BITMAPFILEHEADER FileHeader;

FileHeader.bfType = 0x4D42;
FileHeader.bfSize = g_nFileSize;
FileHeader.bfReserved1 = 0;
FileHeader.bfReserved2 = 0;
FileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) +
sizeof(g_Palette);

memcpy(p, &FileHeader, sizeof(FileHeader));
p += sizeof(FileHeader);

BITMAPINFOHEADER InfoHeader;

InfoHeader.biSize = sizeof(InfoHeader);
InfoHeader.biWidth = IMAGE_SIZE;
InfoHeader.biHeight = IMAGE_SIZE;
InfoHeader.biPlanes = 1;
InfoHeader.biSizeImage = 0;
InfoHeader.biCompression = BI_RGB;
InfoHeader.biYPelsPerMeter = 0;
InfoHeader.biXPelsPerMeter = 0;
InfoHeader.biClrImportant = 0;
InfoHeader.biClrUsed = 0;
InfoHeader.biBitCount = 8;

memcpy(p, &InfoHeader, sizeof(InfoHeader));
p += sizeof(InfoHeader);

// Palette
for(int i = 0; i < 256; i++)
{
g_Palette[i].rgbBlue = i;
g_Palette[i].rgbGreen = i;
g_Palette[i].rgbRed = i;
g_Palette[i].rgbReserved = 0;

g_PaletteEntry[i].peBlue = i;
g_PaletteEntry[i].peRed = i;
g_PaletteEntry[i].peGreen = i;
g_PaletteEntry[i].peFlags = 0;
}

memcpy(p, g_Palette, sizeof(g_Palette));
p += sizeof(g_Palette);

// Pixels

#ifdef BLACK_AND_WHITE

memcpy(p, g_pBlackImage, IMAGE_SIZE*IMAGE_SIZE);
g_bWhite = TRUE;

#else

memcpy(p, g_pFullImage, IMAGE_SIZE*IMAGE_SIZE);

#endif

g_pPixelsPtr = p;
}


Share this post


Link to post
Share on other sites
I can't explain your tearing, but I can explain the timing.

When you call present, D3D is placing a command into the GPU's command stream that says "swap frames on vsync". Often you're processing your game logic faster than the GPU can draw. If there are 3 frames of data buffered, D3D will stall for a bit to let the GPU catch up, otherwise your game and display would become further and further out of sync. If fewer than 3 frames are buffered, Present will return immediately.

When timing, you'll see several short time intervals (no stall), followed by a large time interval (stall). The pattern depends on how quickly you're catching up to the GPU and how long the driver stalls your app when you've buffered too much.

You can help smooth out the timing by ensuring the GPU and CPU remain fairly synced, by forcing a stall. nVidia provides two simple methods of doing this.

Share this post


Link to post
Share on other sites
A few things here:

1. The regular Windows timer functions (GetTickCount, WM_TIMER and friends) simply aren't accurate enough for you to be using for game-related purposes. Look up QueryPerformanceCounter in the Win API for high-resolution timing.

2. In a Direct3D app, you shouldn't be attempting to manually manage the framerate in the manner that you're doing it. The CPU and GPU are meant to work in parallel, and trying to tell the GPU "render once every 30fps" simply won't work the way you might expect it to. The only real reason to control how many frames per second are rendered is to sync with the monitor's refresh, and that's easily accomplished by specifying D3DPRESENT_INTERVAL_ONE. If you only want the contents of the screen to be altered at a rate of 30fps, then only do the portion of the code that alters the texture every 33 ms. But keep calling Present every frame, so that you stay at or above the refresh rate (and thus prevent tearing).

3. There's a much much easier way to simply alternate between two textures. See in D3D programming, you have to get used to the idea that you want your data kept on the GPU as much as possible, with as little intervention from the CPU (AKA your app code) as possible. So instead of loading two bitmaps into memory, manually swapping the memory around, and then creating a texture, you could simply load both bitmaps as textures directly (using D3DXCreateTextureFromFile) and then draw whichever one you need to draw based on the current time.

Share this post


Link to post
Share on other sites
Actually, I don't write a game. I want to use Direct3D to show images from digital camera. This application is small prototype which generates synthetic images.
BTW, I don't use GetTickCount and WM_TIMER. I use multimedia timer to emulate digital camera.
Thinking about D3DPRESENT_INTERVAL_ONE effect, I try to understand what happens. If Present command is called when beam is drawing, it waits. But if Present is called at vertical retrace period, Present is executed immediately. What is Present is called in the end of retrace period? This can explain tearing in the top of my window. D3DPRESENT_INTERVAL_TWO is not supported in windowing mode, in any case, I cannot use it because of performance considerations.
When I move window over the screen by vertical, tearing changes. In some positions it almost disappears. In some positions it becomes completely unsynchronized. I am trying to understand this in order to fix the problem.

Maybe I need to change the whole algoritm? To call present with fixed frequency, and to fill back buffer when new image arrived?

Share this post


Link to post
Share on other sites
Quote:
Original post by MJP
Keep calling Present every frame, so that you stay at or above the refresh rate (and thus prevent tearing).


Thank you, this solved my problem. Now I call Present by timer with interval 1000/screen refresh rate in Hertz, and change the image by another timer emulating digital camera.

Share this post


Link to post
Share on other sites

This topic is 3585 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