daedalus/Source/HLEGraphics/CachedTexture.cpp
2023-04-24 15:51:14 +10:00

382 lines
11 KiB
C++

/*
Copyright (C) 2001 StrmnNrmn
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "BuildOptions.h"
#include "Base/Types.h"
#include <vector>
#include <random>
#include "Config/ConfigOptions.h"
#include "Core/ROM.h"
#include "Debug/DBGConsole.h"
#include "Debug/Dump.h"
#include "HLEGraphics/CachedTexture.h"
#include "HLEGraphics/ConvertImage.h"
#include "HLEGraphics/ConvertTile.h"
#include "HLEGraphics/TextureInfo.h"
#include "Graphics/ColourValue.h"
#include "Graphics/NativePixelFormat.h"
#include "Graphics/NativeTexture.h"
#include "Graphics/PngUtil.h"
#include "Graphics/TextureTransform.h"
#include "Math/Math.h"
#include "Base/MathUtil.h"
#include "Ultra/ultra_gbi.h"
#include "System/IO.h"
#include "Utility/Profiler.h"
#include <array>
static std::vector<u8> gTexelBuffer;
// std::array<NativePf8888, 256> gPaletteBuffer;
static NativePf8888 gPaletteBuffer[ 256 ];
// NB: On the PSP we generate a lightweight hash of the texture data before
// updating the native texture. This avoids some expensive work where possible.
// On other platforms (e.g. OSX) updating textures is relatively inexpensive, so
// we just skip the hashing process entirely, and update textures every frame
// regardless of whether they've actually changed.
#ifdef DAEDALUS_PSP
static const bool kUpdateTexturesEveryFrame = false;
#else
static const bool kUpdateTexturesEveryFrame = true;
#endif
#if defined(DAEDALUS_GL) || defined(DAEDALUS_ACCURATE_TMEM)
static ETextureFormat SelectNativeFormat(const TextureInfo & ti)
{
// On OSX, always use RGBA 8888 textures.
return TexFmt_8888;
}
#else
#define DEFTEX TexFmt_8888
// static const ETextureFormat TFmt[ 32 ] =
std::array<const ETextureFormat, 32> TFmt =
{
// 4bpp 8bpp 16bpp 32bpp
DEFTEX, DEFTEX, TexFmt_5551, TexFmt_8888, // RGBA
DEFTEX, DEFTEX, DEFTEX, DEFTEX, // YUV
TexFmt_CI4_8888, TexFmt_CI8_8888, DEFTEX, DEFTEX, // CI
TexFmt_4444, TexFmt_4444, TexFmt_8888, DEFTEX, // IA
TexFmt_4444, TexFmt_8888, DEFTEX, DEFTEX, // I
DEFTEX, DEFTEX, DEFTEX, DEFTEX, // ?
DEFTEX, DEFTEX, DEFTEX, DEFTEX, // ?
DEFTEX, DEFTEX, DEFTEX, DEFTEX // ?
};
// static const ETextureFormat TFmt_hack[ 32 ] =
std::array<const ETextureFormat, 32> TFmt_hack =
{
// 4bpp 8bpp 16bpp 32bpp
DEFTEX, DEFTEX, TexFmt_4444, TexFmt_8888, // RGBA
DEFTEX, DEFTEX, DEFTEX, DEFTEX, // YUV
TexFmt_CI4_8888, TexFmt_CI8_8888, DEFTEX, DEFTEX, // CI
TexFmt_4444, TexFmt_4444, TexFmt_8888, DEFTEX, // IA
TexFmt_4444, TexFmt_4444, DEFTEX, DEFTEX, // I
DEFTEX, DEFTEX, DEFTEX, DEFTEX, // ?
DEFTEX, DEFTEX, DEFTEX, DEFTEX, // ?
DEFTEX, DEFTEX, DEFTEX, DEFTEX // ?
};
static ETextureFormat SelectNativeFormat(const TextureInfo & ti)
{
u32 idx = (ti.GetFormat() << 2) | ti.GetSize();
return g_ROM.LOAD_T1_HACK ? TFmt_hack[idx] : TFmt[idx];
}
#endif
static bool GenerateTexels(void ** p_texels,
void ** p_palette,
const TextureInfo & ti,
ETextureFormat texture_format,
u32 pitch,
u32 buffer_size)
{
if( gTexelBuffer.size() < buffer_size ) //|| gTexelBuffer.size() > (128 * 1024))//Cut off for downsizing may need to be adjusted to prevent some thrashing
{
#ifdef DAEDALUS_DEBUG_DISPLAYLIST
printf( "Resizing texel buffer to %d bytes. Texture is %dx%d\n", buffer_size, ti.GetWidth(), ti.GetHeight() );
#endif
gTexelBuffer.resize( buffer_size );
}
void * texels = &gTexelBuffer[0];
NativePf8888 * palette = IsTextureFormatPalettised( texture_format ) ? gPaletteBuffer : nullptr;
#ifdef DAEDALUS_ACCURATE_TMEM
// NB: if line is 0, it implies this is a direct load from ram (e.g. S2DEX and Sprite2D ucodes)
// Some games set ti.Line = 0 on LoadTile, ex SSV and Paper Mario
if (ti.GetLine() > 0)
{
if (ConvertTile(ti, texels, palette, texture_format, pitch))
{
*p_texels = texels;
*p_palette = palette;
return true;
}
else
{
return false;
}
}
#endif
if (ConvertTexture(ti, texels, palette, texture_format, pitch))
{
*p_texels = texels;
*p_palette = palette;
return true;
}
return false;
}
static void UpdateTexture( const TextureInfo & ti, CNativeTexture * texture )
{
#ifdef DAEDALUS_PROFILE
DAEDALUS_PROFILE( "Texture Conversion" );
#endif
#ifdef DAEDALUS_ENABLE_ASSERTS
DAEDALUS_ASSERT( texture != nullptr, "No texture" );
#endif
if ( texture != nullptr && texture->HasData() )
{
ETextureFormat format = texture->GetFormat();
u32 stride = texture->GetStride();
void * texels;
void * palette;
if( GenerateTexels( &texels, &palette, ti, format, stride, texture->GetBytesRequired() ) )
{
//
// Recolour the texels
//
if( ti.GetWhite() )
{
Recolour( texels, palette, ti.GetWidth(), ti.GetHeight(), stride, format, c32::White );
}
//
// Clamp edges. We do this so that non power-of-2 textures whose whose width/height
// is less than the mask value clamp correctly. It still doesn't fix those
// textures with a width which is greater than the power-of-2 size.
//
ClampTexels( texels, ti.GetWidth(), ti.GetHeight(), texture->GetCorrectedWidth(), texture->GetCorrectedHeight(), stride, format );
//
// Mirror the texels if required (in-place)
//
bool mirror_s = ti.GetEmulateMirrorS();
bool mirror_t = ti.GetEmulateMirrorT();
if( mirror_s || mirror_t )
{
MirrorTexels( mirror_s, mirror_t, texels, stride, texels, stride, format, ti.GetWidth(), ti.GetHeight() );
}
texture->SetData( texels, palette );
}
}
}
CachedTexture * CachedTexture::Create( const TextureInfo & ti )
{
if( ti.GetWidth() == 0 || ti.GetHeight() == 0 )
{
#ifdef DAEDALUS_DEBUG_CONSOLE
DAEDALUS_ERROR( "Trying to create 0 width/height texture" );
#endif
return nullptr;
}
CachedTexture * texture = new CachedTexture( ti );
if (!texture->Initialise())
{
return nullptr;
}
return texture;
}
CachedTexture::CachedTexture( const TextureInfo & ti )
: mTextureInfo( ti )
, mpTexture(nullptr)
, mTextureContentsHash( 0 )
, mFrameLastUpToDate( gRDPFrame )
, mFrameLastUsed( gRDPFrame )
{
}
CachedTexture::~CachedTexture()
{
}
bool CachedTexture::Initialise()
{
#ifdef DAEDALUS_ENABLE_ASSERTS
DAEDALUS_ASSERT_Q(mpTexture == nullptr);
#endif
u32 width = mTextureInfo.GetWidth();
u32 height = mTextureInfo.GetHeight();
std::default_random_engine FastRando;
if (mTextureInfo.GetEmulateMirrorS()) width *= 2;
if (mTextureInfo.GetEmulateMirrorT()) height *= 2;
mpTexture = CNativeTexture::Create( width, height, SelectNativeFormat(mTextureInfo) );
if( mpTexture != nullptr )
{
// If this we're performing Texture updated checks, randomly offset the
// 'FrameLastUpToDate' time. This ensures when lots of textures are
// created on the same frame we update them over a nice distribution of frames.
if(gCheckTextureHashFrequency > 0)
{
mFrameLastUpToDate = gRDPFrame + (FastRando() & (gCheckTextureHashFrequency - 1));
}
UpdateTextureHash();
UpdateTexture( mTextureInfo, mpTexture );
}
return mpTexture != nullptr;
}
// Update the hash of the texture. Returns true if the texture should be updated.
bool CachedTexture::UpdateTextureHash()
{
if (kUpdateTexturesEveryFrame)
{
// NB always assume we need updating.
return true;
}
u32 new_hash_value = mTextureInfo.GenerateHashValue();
bool changed = new_hash_value != mTextureContentsHash;
mTextureContentsHash = new_hash_value;
return changed;
}
void CachedTexture::UpdateIfNecessary()
{
if( !IsFresh() )
{
if (UpdateTextureHash())
{
UpdateTexture( mTextureInfo, mpTexture );
}
// FIXME(strmnrmn): should probably recreate mpWhiteTexture if it exists, else it may have stale data.
mFrameLastUpToDate = gRDPFrame;
}
mFrameLastUsed = gRDPFrame;
}
// IsFresh - has this cached texture been updated recently?
bool CachedTexture::IsFresh() const
{
if (gRDPFrame == mFrameLastUsed)
return true;
// If we're not updating textures every frame, check how long it's been
// since we last updated it.
if (!kUpdateTexturesEveryFrame)
{
return (gCheckTextureHashFrequency == 0 ||
gRDPFrame < mFrameLastUpToDate + gCheckTextureHashFrequency);
}
return false;
}
bool CachedTexture::HasExpired() const
{
std::default_random_engine FastRand;
if (!kUpdateTexturesEveryFrame)
{
if (!IsFresh())
{
//Hack to make WONDER PROJECT J2 work (need to reload some textures every frame!) //Corn
if( (g_ROM.GameHacks == WONDER_PROJECTJ2) && (mTextureInfo.GetTLutFormat() == kTT_RGBA16) && (mTextureInfo.GetSize() == G_IM_SIZ_8b) ) return true;
//Hack for Worms Armageddon
if( (g_ROM.GameHacks == WORMS_ARMAGEDDON) && (mTextureInfo.GetSize() == G_IM_SIZ_8b) && (mTextureContentsHash != mTextureInfo.GenerateHashValue()) ) return true;
//Hack for Zelda OOT & MM text (only needed if there is not a general hash check) //Corn
if( g_ROM.ZELDA_HACK && (mTextureInfo.GetSize() == G_IM_SIZ_4b) && mTextureContentsHash != mTextureInfo.GenerateHashValue() ) return true;
//Check if texture has changed
//if( mTextureContentsHash != mTextureInfo.GenerateHashValue() ) return true;
}
}
//Otherwise we wait 20+random(0-3) frames before trashing the texture if unused
//Spread trashing them over time so not all get killed at once (lower value uses less VRAM) //Corn
return gRDPFrame - mFrameLastUsed > (20 + (FastRand() & 0x3));
}
#ifdef DAEDALUS_DEBUG_DISPLAYLIST
void CachedTexture::DumpTexture( const TextureInfo & ti, const CNativeTexture * texture )
{
DAEDALUS_ASSERT(texture != nullptr, "Should have a texture");
if( texture != nullptr && texture->HasData() )
{
IO::Filename filename;
IO::Filename filepath;
IO::Filename dumpdir;
IO::Path::Combine( dumpdir, g_ROM.settings.GameName.c_str(), "Textures" );
Dump_GetDumpDirectory( filepath, dumpdir );
sprintf( filename, "%08x-%s_%dbpp-%dx%d-%dx%d.png",
ti.GetLoadAddress(), ti.GetFormatName(), ti.GetSizeInBits(),
0, 0, // Left/Top
ti.GetWidth(), ti.GetHeight() );
IO::Path::Append( filepath, filename );
void * texels;
void * palette;
// Note that we re-convert the texels because those in the native texture may well already
// be swizzle. Maybe we should just have an unswizzle routine?
if( GenerateTexels( &texels, &palette, ti, texture->GetFormat(), texture->GetStride(), texture->GetBytesRequired() ) )
{
// NB - this does not include the mirrored texels
// NB we use the palette from the native texture. This is a total hack.
// We have to do this because the palette texels come from emulated tmem, rather
// than ram. This means that when we dump out the texture here, tmem won't necessarily
// contain our pixels.
const void * native_palette = texture->GetPalette();
PngSaveImage( filepath, texels, native_palette, texture->GetFormat(), texture->GetStride(), ti.GetWidth(), ti.GetHeight(), true );
}
}
}
#endif // DAEDALUS_DEBUG_DISPLAYLIST