D3D various: "Safe texture cache" option, texture replace instead of destroy/create when possible, a commented out "optimization" that didn't speed things up (use DrawPrimitive instead of DrawIndexedPrimitive when possible), reduce code duplication in Flush(), don't periodically clean out the shader caches since it's not really beneficial - shaders are cheap to keep. some code cleanup.

git-svn-id: https://dolphin-emu.googlecode.com/svn/trunk@4302 8ced0084-cf51-0410-be5f-012b33b47a6e
This commit is contained in:
hrydgard
2009-09-19 13:14:55 +00:00
parent dae1a68bfc
commit 7a8f6bdd6d
17 changed files with 259 additions and 170 deletions

View File

@ -129,6 +129,13 @@ void ReplaceTexture2D(LPDIRECT3DTEXTURE9 pTexture, const u8* buffer, const int w
D3DLOCKED_RECT Lock;
pTexture->LockRect(level, &Lock, NULL, 0);
u32* pIn = pBuffer;
bool bExpand = false;
if (fmt == D3DFMT_A8P8) {
fmt = D3DFMT_A8L8;
bExpand = true;
}
switch(fmt)
{
case D3DFMT_A8R8G8B8:
@ -141,8 +148,59 @@ void ReplaceTexture2D(LPDIRECT3DTEXTURE9 pTexture, const u8* buffer, const int w
}
}
break;
case D3DFMT_L8:
case D3DFMT_A8:
case D3DFMT_A4L4:
{
const u8 *pIn = buffer;
for (int y = 0; y < height; y++)
{
u8* pBits = ((u8*)Lock.pBits + (y * Lock.Pitch));
memcpy(pBits, pIn, width);
pIn += pitch;
}
}
break;
case D3DFMT_R5G6B5:
{
const u16 *pIn = (u16*)buffer;
for (int y = 0; y < height; y++)
{
u16* pBits = (u16*)((u8*)Lock.pBits + (y * Lock.Pitch));
memcpy(pBits, pIn, width * 2);
pIn += pitch;
}
}
break;
case D3DFMT_A8L8:
{
if (bExpand) { // I8
const u8 *pIn = buffer;
// TODO(XK): Find a better way that does not involve either unpacking
// or downsampling (i.e. A4L4)
for (int y = 0; y < height; y++)
{
u8* pBits = ((u8*)Lock.pBits + (y * Lock.Pitch));
for(int i = 0; i < width * 2; i += 2) {
pBits[i] = pIn[i / 2];
pBits[i + 1] = pIn[i / 2];
}
pIn += pitch;
}
} else { // IA8
const u16 *pIn = (u16*)buffer;
for (int y = 0; y < height; y++)
{
u16* pBits = (u16*)((u8*)Lock.pBits + (y * Lock.Pitch));
memcpy(pBits, pIn, width * 2);
pIn += pitch;
}
}
}
break;
case D3DFMT_DXT1:
memcpy(Lock.pBits,buffer,(width/4)*(height/4)*8);
memcpy(Lock.pBits, buffer, (width/4) * (height/4) * 8);
break;
}
pTexture->UnlockRect(level);

View File

@ -92,6 +92,7 @@ struct TabDirect3D : public W32Util::Tab
CheckDlgButton(hDlg, IDC_ASPECT_16_9, g_Config.bKeepAR169 ? TRUE : FALSE);
CheckDlgButton(hDlg, IDC_ASPECT_4_3, g_Config.bKeepAR43 ? TRUE : FALSE);
CheckDlgButton(hDlg, IDC_WIDESCREEN_HACK, g_Config.bWidescreenHack ? TRUE : FALSE);
CheckDlgButton(hDlg, IDC_SAFE_TEXTURE_CACHE, g_Config.bSafeTextureCache ? TRUE : FALSE);
}
void Command(HWND hDlg,WPARAM wParam)
@ -114,6 +115,9 @@ struct TabDirect3D : public W32Util::Tab
case IDC_WIREFRAME:
g_Config.bWireFrame = Button_GetCheck(GetDlgItem(hDlg,IDC_WIREFRAME)) ? true : false;
break;
case IDC_SAFE_TEXTURE_CACHE:
g_Config.bSafeTextureCache = Button_GetCheck(GetDlgItem(hDlg, IDC_SAFE_TEXTURE_CACHE));
break;
default:
break;
}

View File

@ -151,6 +151,7 @@ bool PixelShaderCache::SetShader(bool dstAlpha)
void PixelShaderCache::Cleanup()
{
/*
PSCache::iterator iter;
iter = PixelShaders.begin();
while (iter != PixelShaders.end())
@ -167,6 +168,7 @@ void PixelShaderCache::Cleanup()
}
}
SETSTAT(stats.numPixelShadersAlive, (int)PixelShaders.size());
*/
}
#if defined(_DEBUG) || defined(DEBUGFAST)

View File

@ -49,7 +49,7 @@ void TextureCache::TCacheEntry::Destroy(bool shutdown)
if (texture)
texture->Release();
texture = 0;
if (!isRenderTarget && !shutdown)
if (!isRenderTarget && !shutdown && !g_ActiveConfig.bSafeTextureCache)
{
u32 *ptr = (u32*)g_VideoInitialize.pGetMemoryPointer(addr);
if (ptr && *ptr == hash)
@ -103,88 +103,78 @@ void TextureCache::Cleanup()
}
}
TextureCache::TCacheEntry *TextureCache::Load(int stage, u32 address, int width, int height, int format, int tlutaddr, int tlutfmt)
TextureCache::TCacheEntry *TextureCache::Load(int stage, u32 address, int width, int height, int tex_format, int tlutaddr, int tlutfmt)
{
if (address == 0)
return NULL;
u8 *ptr = g_VideoInitialize.pGetMemoryPointer(address);
int bsw = TexDecoder_GetBlockWidthInTexels(tex_format) - 1; //TexelSizeInNibbles(format)*width*height/16;
int bsh = TexDecoder_GetBlockHeightInTexels(tex_format) - 1; //TexelSizeInNibbles(format)*width*height/16;
int expandedWidth = (width + bsw) & (~bsw);
int expandedHeight = (height + bsh) & (~bsh);
int palSize = TexDecoder_GetPaletteSize(format);
u32 palhash = 0xc0debabe;
if (palSize)
u32 hash_value;
u32 texID = address;
u32 texHash;
if (g_ActiveConfig.bSafeTextureCache || g_ActiveConfig.bDumpTextures)
{
// TODO: Share this code with the GL plugin.
if (palSize > 32)
palSize = 32; //let's not do excessive amount of checking
u8 *pal = g_VideoInitialize.pGetMemoryPointer(tlutaddr);
if (pal != 0)
texHash = TexDecoder_GetSafeTextureHash(ptr, expandedWidth, expandedHeight, tex_format, 0);
if (g_ActiveConfig.bSafeTextureCache)
hash_value = texHash;
if ((tex_format == GX_TF_C4) || (tex_format == GX_TF_C8) || (tex_format == GX_TF_C14X2))
{
for (int i = 0; i < palSize; i++)
{
palhash = _rotl(palhash,13);
palhash ^= pal[i];
palhash += 31;
}
}
}
static LPDIRECT3DTEXTURE9 lastTexture[8] = {0,0,0,0,0,0,0,0};
int bs = TexDecoder_GetBlockWidthInTexels(format)-1; //TexelSizeInNibbles(format)*width*height/16;
int expandedWidth = (width+bs) & (~bs);
u32 hash_value = TexDecoder_GetSafeTextureHash(ptr, expandedWidth, height, format, 0);
u32 tex_hash = 0;
u32 texID = address;
if (g_ActiveConfig.bDumpTextures || g_ActiveConfig.bSafeTextureCache)
{
tex_hash = hash_value;
if ((format == GX_TF_C4) || (format == GX_TF_C8) || (format == GX_TF_C14X2))
{
u32 tlutHash = TexDecoder_GetTlutHash(&texMem[tlutaddr], (format == GX_TF_C4) ? 32 : 128);
tex_hash ^= tlutHash;
// WARNING! texID != address now => may break CopyRenderTargetToTexture (cf. TODO up)
// tlut size can be up to 32768B (GX_TF_C14X2) but Safer == Slower.
// This trick (to change the texID depending on the TLUT addr) is a trick to get around
// an issue with metroid prime's fonts, where it has multiple sets of fonts on top of
// each other stored in a single texture, and uses the palette to make different characters
// visible or invisible. Thus, unless we want to recreate the textures for every drawn character,
// we must make sure that texture with different tluts get different IDs.
u32 tlutHash = TexDecoder_GetTlutHash(&texMem[tlutaddr], (tex_format == GX_TF_C4) ? 32 : 128);
texHash ^= tlutHash;
if (g_ActiveConfig.bSafeTextureCache)
texID ^= tlutHash;
}
}
bool skip_texture_create = false;
TexCache::iterator iter = textures.find(texID);
if (iter != textures.end())
{
TCacheEntry &entry = iter->second;
if (!g_ActiveConfig.bSafeTextureCache)
hash_value = ((u32 *)ptr)[0];
if (entry.isRenderTarget || ((address == entry.addr) && (hash_value == entry.hash)))
{
entry.frameCount = frameCount;
if (lastTexture[stage] == entry.texture)
{
return &entry;
}
lastTexture[stage] = entry.texture;
D3D::SetTexture(stage, entry.texture);
return &entry;
}
else
{
/* if (width == iter->second.w && height==entry.h && format==entry.fmt)
// Let's reload the new texture data into the same texture,
// instead of destroying it and having to create a new one.
// Might speed up movie playback very, very slightly.
if (width == entry.w && height==entry.h && tex_format == entry.fmt)
{
LPDIRECT3DTEXTURE9 tex = entry.texture;
int bs = TexDecoder_GetBlockWidthInTexels(format)-1; //TexelSizeInNibbles(format)*width*height/16;
int expandedWidth = (width+bs) & (~bs);
D3DFORMAT dfmt = TexDecoder_Decode(temp,ptr,expandedWidth,height,format, tlutaddr, tlutfmt);
D3D::ReplaceTexture2D(tex,temp,width,height,expandedWidth,dfmt);
D3D::dev->SetTexture(stage,tex);
return;
skip_texture_create = true;
}
else
{*/
{
entry.Destroy(false);
textures.erase(iter);
//}
}
}
}
PC_TexFormat pcfmt = TexDecoder_Decode(temp, ptr, expandedWidth, height, format, tlutaddr, tlutfmt);
PC_TexFormat pcfmt = TexDecoder_Decode(temp, ptr, expandedWidth, height, tex_format, tlutaddr, tlutfmt);
D3DFORMAT d3d_fmt;
switch (pcfmt) {
case PC_TEX_FMT_BGRA32:
@ -213,23 +203,32 @@ TextureCache::TCacheEntry *TextureCache::Load(int stage, u32 address, int width,
//Make an entry in the table
TCacheEntry& entry = textures[texID];
entry.hash = hash_value;
//entry.hash = (u32)(((double)rand() / RAND_MAX) * 0xFFFFFFFF);
entry.paletteHash = palhash;
entry.oldpixel = ((u32 *)ptr)[0];
//((u32 *)ptr)[entry.hashoffset] = entry.hash;
if (g_ActiveConfig.bSafeTextureCache)
entry.hash = hash_value;
else
{
entry.hash = (u32)(((double)rand() / RAND_MAX) * 0xFFFFFFFF);
((u32 *)ptr)[0] = entry.hash;
}
entry.addr = address;
entry.size_in_bytes = TexDecoder_GetTextureSizeInBytes(width, height, tex_format);
entry.isRenderTarget = false;
entry.isNonPow2 = ((width & (width - 1)) || (height & (height - 1)));
entry.texture = D3D::CreateTexture2D((BYTE*)temp, width, height, expandedWidth, d3d_fmt);
if (!skip_texture_create) {
entry.texture = D3D::CreateTexture2D((BYTE*)temp, width, height, expandedWidth, d3d_fmt);
} else {
D3D::ReplaceTexture2D(entry.texture, (BYTE*)temp, width, height, expandedWidth, d3d_fmt);
}
entry.frameCount = frameCount;
entry.w = width;
entry.h = height;
entry.fmt = format;
entry.fmt = tex_format;
if (g_ActiveConfig.bDumpTextures)
{ // dump texture to file
{
// dump texture to file
char szTemp[MAX_PATH];
char szDir[MAX_PATH];
const char* uniqueId = globals->unique_id;
@ -242,7 +241,7 @@ TextureCache::TCacheEntry *TextureCache::Load(int stage, u32 address, int width,
bCheckedDumpDir = true;
}
sprintf(szTemp, "%s/%s_%08x_%i.png", szDir, uniqueId, tex_hash, format);
sprintf(szTemp, "%s/%s_%08x_%i.png", szDir, uniqueId, texHash, tex_format);
//sprintf(szTemp, "%s\\txt_%04i_%i.png", g_Config.texDumpPath.c_str(), counter++, format); <-- Old method
if (!File::Exists(szTemp))
D3DXSaveTextureToFileA(szTemp,D3DXIFF_BMP,entry.texture,0);
@ -254,8 +253,6 @@ TextureCache::TCacheEntry *TextureCache::Load(int stage, u32 address, int width,
//Set the texture!
D3D::SetTexture(stage, entry.texture);
lastTexture[stage] = entry.texture;
DEBUGGER_PAUSE_LOG_AT(NEXT_NEW_TEXTURE,true,{printf("A new texture (%d x %d) is loaded", width, height);});
return &entry;
}

View File

@ -56,9 +56,18 @@ enum Collection
C_POINTS,
};
const D3DPRIMITIVETYPE pts[3] =
{
D3DPT_POINTLIST, //DUMMY
D3DPT_TRIANGLELIST,
D3DPT_LINELIST,
};
static IndexGenerator indexGen;
static Collection collection;
int lastPrimitive;
static u8 *fakeVBuffer; // format undefined - NativeVertexFormat takes care of the declaration.
static u16 *fakeIBuffer; // These are just straightforward 16-bit indices.
@ -67,16 +76,29 @@ static u16 *fakeIBuffer; // These are just straightforward 16-bit indices.
const Collection collectionTypeLUT[8] =
{
C_TRIANGLES,//quads
C_NOTHING, //nothing
C_TRIANGLES,//triangles
C_TRIANGLES,//strip
C_TRIANGLES,//fan
C_LINES, //lines
C_LINES, //linestrip
C_POINTS //guess :P
C_TRIANGLES, //quads
C_NOTHING, //nothing
C_TRIANGLES, //triangles
C_TRIANGLES, //strip
C_TRIANGLES, //fan
C_LINES, //lines
C_LINES, //linestrip
C_POINTS //guess :P
};
const D3DPRIMITIVETYPE gxPrimToD3DPrim[8] = {
(D3DPRIMITIVETYPE)0, // not supported
(D3DPRIMITIVETYPE)0, // nothing
D3DPT_TRIANGLELIST,
D3DPT_TRIANGLESTRIP,
D3DPT_TRIANGLEFAN,
D3DPT_LINELIST,
D3DPT_LINESTRIP,
};
void CreateDeviceObjects();
void DestroyDeviceObjects();
@ -116,7 +138,7 @@ void AddIndices(int _primitive, int _numVertices)
case GX_DRAW_TRIANGLE_FAN: indexGen.AddFan(_numVertices); return;
case GX_DRAW_LINE_STRIP: indexGen.AddLineStrip(_numVertices); return;
case GX_DRAW_LINES: indexGen.AddLineList(_numVertices); return;
case GX_DRAW_POINTS: indexGen.AddPointList(_numVertices); return;
case GX_DRAW_POINTS: indexGen.AddPoints(_numVertices); return;
}
}
@ -129,7 +151,8 @@ void AddVertices(int _primitive, int _numVertices)
{
if (_numVertices <= 0) //This check is pretty stupid...
return;
lastPrimitive = _primitive;
Collection type = collectionTypeLUT[_primitive];
if (type == C_NOTHING)
return;
@ -166,12 +189,55 @@ void AddVertices(int _primitive, int _numVertices)
}
}
const D3DPRIMITIVETYPE pts[3] =
inline void Draw(int numVertices, int stride)
{
D3DPT_POINTLIST, //DUMMY
D3DPT_TRIANGLELIST,
D3DPT_LINELIST,
};
if (collection != C_POINTS)
{
int numPrimitives = indexGen.GetNumPrims();
/* For some reason, this makes things slower!
if ((indexGen.GetNumAdds() == 1 || indexGen.GetOnlyLists()) && lastPrimitive != GX_DRAW_QUADS && gxPrimToD3DPrim[lastPrimitive])
{
if (FAILED(D3D::dev->DrawPrimitiveUP(
gxPrimToD3DPrim[lastPrimitive],
numPrimitives,
fakeVBuffer,
stride))) {
#if defined(_DEBUG) || defined(DEBUGFAST)
std::string error_shaders;
error_shaders.append(VertexShaderCache::GetCurrentShaderCode());
error_shaders.append(PixelShaderCache::GetCurrentShaderCode());
File::WriteStringToFile(true, error_shaders, "bad_shader_combo.txt");
PanicAlert("DrawPrimitiveUP failed. Shaders written to bad_shader_combo.txt.");
#endif
}
INCSTAT(stats.thisFrame.numDrawCalls);
} else*/ {
if (FAILED(D3D::dev->DrawIndexedPrimitiveUP(
pts[(int)collection],
0, numVertices, numPrimitives,
fakeIBuffer,
D3DFMT_INDEX16,
fakeVBuffer,
stride))) {
#if defined(_DEBUG) || defined(DEBUGFAST)
std::string error_shaders;
error_shaders.append(VertexShaderCache::GetCurrentShaderCode());
error_shaders.append(PixelShaderCache::GetCurrentShaderCode());
File::WriteStringToFile(true, error_shaders, "bad_shader_combo.txt");
PanicAlert("DrawIndexedPrimitiveUP failed. Shaders written to bad_shader_combo.txt.");
#endif
}
INCSTAT(stats.thisFrame.numIndexedDrawCalls);
}
}
else
{
D3D::dev->SetIndices(0);
D3D::dev->DrawPrimitiveUP(D3DPT_POINTLIST, numVertices, fakeVBuffer, stride);
INCSTAT(stats.thisFrame.numDrawCalls);
}
}
void Flush()
{
@ -219,73 +285,26 @@ void Flush()
VertexShaderManager::SetConstants();
PixelShaderManager::SetConstants();
if (!PixelShaderCache::SetShader(false))
goto shader_fail;
if (!VertexShaderCache::SetShader(g_nativeVertexFmt->m_components))
goto shader_fail;
if (!PixelShaderCache::SetShader(false))
goto shader_fail;
int stride = g_nativeVertexFmt->GetVertexStride();
g_nativeVertexFmt->SetupVertexPointers();
if (collection != C_POINTS)
{
int numPrimitives = indexGen.GetNumPrims();
if (FAILED(D3D::dev->DrawIndexedPrimitiveUP(
pts[(int)collection],
0, numVertices, numPrimitives,
fakeIBuffer,
D3DFMT_INDEX16,
fakeVBuffer,
stride))) {
#if defined(_DEBUG) || defined(DEBUGFAST)
std::string error_shaders;
error_shaders.append(VertexShaderCache::GetCurrentShaderCode());
error_shaders.append(PixelShaderCache::GetCurrentShaderCode());
File::WriteStringToFile(true, error_shaders, "bad_shader_combo.txt");
PanicAlert("DrawIndexedPrimitiveUP failed. Shaders written to bad_shader_combo.txt.");
#endif
}
}
else
{
D3D::dev->SetIndices(0);
D3D::dev->DrawPrimitiveUP(D3DPT_POINTLIST, numVertices, fakeVBuffer, stride);
}
Draw(numVertices, stride);
if (bpmem.dstalpha.enable && bpmem.blendmode.alphaupdate)
{
DWORD write = 0;
if (!PixelShaderCache::SetShader(true))
goto shader_fail;
// update alpha only
D3D::SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_ALPHA);
D3D::SetRenderState(D3DRS_ALPHABLENDENABLE, false);
g_nativeVertexFmt->SetupVertexPointers();
if (collection != C_POINTS)
{
int numPrimitives = indexGen.GetNumPrims();
if (FAILED(D3D::dev->DrawIndexedPrimitiveUP(
pts[(int)collection],
0, numVertices, numPrimitives,
fakeIBuffer,
D3DFMT_INDEX16,
fakeVBuffer,
stride))) {
#if defined(_DEBUG) || defined(DEBUGFAST)
std::string error_shaders;
error_shaders.append(VertexShaderCache::GetCurrentShaderCode());
error_shaders.append(PixelShaderCache::GetCurrentShaderCode());
File::WriteStringToFile(true, error_shaders, "bad_shader_combo.txt");
PanicAlert("DrawIndexedPrimitiveUP failed (dstalpha). Shaders written to bad_shader_combo.txt.");
#endif
}
}
else
{
D3D::dev->SetIndices(0);
D3D::dev->DrawPrimitiveUP(D3DPT_POINTLIST, numVertices, fakeVBuffer, stride);
}
Draw(numVertices, stride);
if (bpmem.blendmode.alphaupdate)
write = D3DCOLORWRITEENABLE_ALPHA;
@ -295,11 +314,7 @@ void Flush()
D3D::SetRenderState(D3DRS_ALPHABLENDENABLE, true);
D3D::SetRenderState(D3DRS_COLORWRITEENABLE, write);
INCSTAT(stats.thisFrame.numDrawCalls);
}
INCSTAT(stats.thisFrame.numDrawCalls);
}
shader_fail:
collection = C_NOTHING;

View File

@ -203,6 +203,7 @@ bool VertexShaderCache::SetShader(u32 components)
void VertexShaderCache::Cleanup()
{
/*
for (VSCache::iterator iter = vshaders.begin(); iter != vshaders.end();)
{
VSCacheEntry &entry = iter->second;
@ -216,7 +217,8 @@ void VertexShaderCache::Cleanup()
++iter;
}
}
SETSTAT(stats.numVertexShadersAlive, (int)vshaders.size());
SETSTAT(stats.numVertexShadersAlive, (int)vshaders.size());*/
}
#if defined(_DEBUG) || defined(DEBUGFAST)

View File

@ -1224,6 +1224,8 @@
#define IDC_WIDESCREEN_HACK 1036
#define IDC_DUMPFRAMES 1037
#define psh14 0x040d
#define IDC_ASPECT_16_10 1037
#define IDC_SAFE_TEXTURE_CACHE 1037
#define psh15 0x040e
#define psh16 0x040f
#define _WIN32_WINDOWS 0x0410
@ -1532,8 +1534,8 @@
#define SPVERSION_MASK 0x0000FF00
#define HTERROR -2
#define IDC_STATIC -1
#define PWR_FAIL -1
#define UNICODE_NOCHAR 0xFFFF
#define PWR_FAIL -1
#define HTTRANSPARENT -1
// Next default values for new objects

View File

@ -2,15 +2,6 @@
//
#include "resource.h"
#include <windows.h>
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources
@ -36,7 +27,7 @@ BEGIN
LTEXT "Will not work correctly on older GPU:s.",IDC_STATIC,7,47,170,8
END
IDD_SETTINGS DIALOGEX 0, 0, 231, 174
IDD_SETTINGS DIALOGEX 0, 0, 231, 194
STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
@ -57,6 +48,7 @@ BEGIN
CONTROL "4:3",IDC_ASPECT_4_3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,68,66,59,11
CONTROL "16:9",IDC_ASPECT_16_9,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,68,80,49,11
CONTROL "&Widescreen Hack",IDC_WIDESCREEN_HACK,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,128,81,73,10
CONTROL "&Safe Texture Cache",IDC_SAFE_TEXTURE_CACHE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,68,172,85,11
END
IDD_DEBUGGER DIALOGEX 0, 0, 234, 254
@ -128,7 +120,7 @@ BEGIN
VERTGUIDE, 81
VERTGUIDE, 87
TOPMARGIN, 7
BOTTOMMARGIN, 167
BOTTOMMARGIN, 187
END
IDD_DEBUGGER, DIALOG
@ -198,7 +190,7 @@ END
// Generated from the TEXTINCLUDE 3 resource.
//
<EFBFBD>
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED