[Glide64] Get text cache to use char *
This commit is contained in:
parent
3d384808bd
commit
5ddc89571f
|
@ -168,8 +168,8 @@ boolean ext_ghq_init(int maxwidth, /* maximum texture width supported by hardwar
|
|||
int maxbpp, /* maximum texture bpp supported by hardware */
|
||||
int options, /* options */
|
||||
int cachesize,/* cache textures to system memory */
|
||||
const wchar_t *path, /* plugin directory. must be smaller than MAX_PATH */
|
||||
const wchar_t *ident, /* name of ROM. must be no longer than 64 in character. */
|
||||
const char *path, /* plugin directory. must be smaller than MAX_PATH */
|
||||
const char *ident, /* name of ROM. must be no longer than 64 in character. */
|
||||
dispInfoFuncExt callback /* callback function to display info */
|
||||
);
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ TxCache::~TxCache()
|
|||
delete _txUtil;
|
||||
}
|
||||
|
||||
TxCache::TxCache(int options, int cachesize, const wchar_t *path, const wchar_t *ident,
|
||||
TxCache::TxCache(int options, int cachesize, const char *path, const char *ident,
|
||||
dispInfoFuncExt callback)
|
||||
{
|
||||
_txUtil = new TxUtil();
|
||||
|
@ -51,20 +51,26 @@ TxCache::TxCache(int options, int cachesize, const wchar_t *path, const wchar_t
|
|||
|
||||
/* save path name */
|
||||
if (path)
|
||||
{
|
||||
_path.assign(path);
|
||||
}
|
||||
|
||||
/* save ROM name */
|
||||
if (ident)
|
||||
{
|
||||
_ident.assign(ident);
|
||||
}
|
||||
|
||||
/* zlib memory buffers to (de)compress hires textures */
|
||||
if (_options & (GZ_TEXCACHE | GZ_HIRESTEXCACHE)) {
|
||||
if (_options & (GZ_TEXCACHE | GZ_HIRESTEXCACHE))
|
||||
{
|
||||
_gzdest0 = TxMemBuf::getInstance()->get(0);
|
||||
_gzdest1 = TxMemBuf::getInstance()->get(1);
|
||||
_gzdestLen = (TxMemBuf::getInstance()->size_of(0) < TxMemBuf::getInstance()->size_of(1)) ?
|
||||
TxMemBuf::getInstance()->size_of(0) : TxMemBuf::getInstance()->size_of(1);
|
||||
|
||||
if (!_gzdest0 || !_gzdest1 || !_gzdestLen) {
|
||||
if (!_gzdest0 || !_gzdest1 || !_gzdestLen)
|
||||
{
|
||||
_options &= ~(GZ_TEXCACHE | GZ_HIRESTEXCACHE);
|
||||
_gzdest0 = NULL;
|
||||
_gzdest1 = NULL;
|
||||
|
@ -83,20 +89,24 @@ TxCache::add(uint64 checksum, GHQTexInfo *info, int dataSize)
|
|||
uint8 *dest = info->data;
|
||||
uint16 format = info->format;
|
||||
|
||||
if (!dataSize) {
|
||||
if (!dataSize)
|
||||
{
|
||||
dataSize = _txUtil->sizeofTx(info->width, info->height, info->format);
|
||||
|
||||
if (!dataSize) return 0;
|
||||
|
||||
if (_options & (GZ_TEXCACHE | GZ_HIRESTEXCACHE)) {
|
||||
if (_options & (GZ_TEXCACHE | GZ_HIRESTEXCACHE))
|
||||
{
|
||||
/* zlib compress it. compression level:1 (best speed) */
|
||||
uLongf destLen = _gzdestLen;
|
||||
dest = (dest == _gzdest0) ? _gzdest1 : _gzdest0;
|
||||
if (compress2(dest, &destLen, info->data, dataSize, 1) != Z_OK) {
|
||||
if (compress2(dest, &destLen, info->data, dataSize, 1) != Z_OK)
|
||||
{
|
||||
dest = info->data;
|
||||
DBG_INFO(80, L"Error: zlib compression failed!\n");
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
DBG_INFO(80, L"zlib compressed: %.02fkb->%.02fkb\n", (float)dataSize / 1000, (float)destLen / 1000);
|
||||
dataSize = destLen;
|
||||
format |= GR_TEXFMT_GZ;
|
||||
|
@ -105,15 +115,19 @@ TxCache::add(uint64 checksum, GHQTexInfo *info, int dataSize)
|
|||
}
|
||||
|
||||
/* if cache size exceeds limit, remove old cache */
|
||||
if (_cacheSize > 0) {
|
||||
if (_cacheSize > 0)
|
||||
{
|
||||
_totalSize += dataSize;
|
||||
if ((_totalSize > _cacheSize) && !_cachelist.empty()) {
|
||||
if ((_totalSize > _cacheSize) && !_cachelist.empty())
|
||||
{
|
||||
/* _cachelist is arranged so that frequently used textures are in the back */
|
||||
std::list<uint64>::iterator itList = _cachelist.begin();
|
||||
while (itList != _cachelist.end()) {
|
||||
while (itList != _cachelist.end())
|
||||
{
|
||||
/* find it in _cache */
|
||||
std::map<uint64, TXCACHE*>::iterator itMap = _cache.find(*itList);
|
||||
if (itMap != _cache.end()) {
|
||||
if (itMap != _cache.end())
|
||||
{
|
||||
/* yep we have it. remove it. */
|
||||
_totalSize -= (*itMap).second->size;
|
||||
free((*itMap).second->info.data);
|
||||
|
@ -136,9 +150,11 @@ TxCache::add(uint64 checksum, GHQTexInfo *info, int dataSize)
|
|||
|
||||
/* cache it */
|
||||
uint8 *tmpdata = (uint8*)malloc(dataSize);
|
||||
if (tmpdata) {
|
||||
if (tmpdata)
|
||||
{
|
||||
TXCACHE *txCache = new TXCACHE;
|
||||
if (txCache) {
|
||||
if (txCache)
|
||||
{
|
||||
/* we can directly write as we filter, but for now we get away
|
||||
* with doing memcpy after all the filtering is done.
|
||||
*/
|
||||
|
@ -151,7 +167,8 @@ TxCache::add(uint64 checksum, GHQTexInfo *info, int dataSize)
|
|||
txCache->size = dataSize;
|
||||
|
||||
/* add to cache */
|
||||
if (_cacheSize > 0) {
|
||||
if (_cacheSize > 0)
|
||||
{
|
||||
_cachelist.push_back(checksum);
|
||||
txCache->it = --(_cachelist.end());
|
||||
}
|
||||
|
@ -161,31 +178,34 @@ TxCache::add(uint64 checksum, GHQTexInfo *info, int dataSize)
|
|||
#ifdef DEBUG
|
||||
DBG_INFO(80, L"[%5d] added!! crc:%08X %08X %d x %d gfmt:%x total:%.02fmb\n",
|
||||
_cache.size(), (uint32)(checksum >> 32), (uint32)(checksum & 0xffffffff),
|
||||
info->width, info->height, info->format, (float)_totalSize/1000000);
|
||||
info->width, info->height, info->format, (float)_totalSize / 1000000);
|
||||
|
||||
DBG_INFO(80, L"smalllodlog2:%d largelodlog2:%d aspectratiolog2:%d\n",
|
||||
txCache->info.smallLodLog2, txCache->info.largeLodLog2, txCache->info.aspectRatioLog2);
|
||||
|
||||
if (info->tiles) {
|
||||
if (info->tiles)
|
||||
{
|
||||
DBG_INFO(80, L"tiles:%d un-tiled size:%d x %d\n", info->tiles, info->untiled_width, info->untiled_height);
|
||||
}
|
||||
|
||||
if (_cacheSize > 0) {
|
||||
DBG_INFO(80, L"cache max config:%.02fmb\n", (float)_cacheSize/1000000);
|
||||
if (_cacheSize > 0)
|
||||
{
|
||||
DBG_INFO(80, L"cache max config:%.02fmb\n", (float)_cacheSize / 1000000);
|
||||
|
||||
if (_cache.size() != _cachelist.size()) {
|
||||
if (_cache.size() != _cachelist.size())
|
||||
{
|
||||
DBG_INFO(80, L"Error: cache/cachelist mismatch! (%d/%d)\n", _cache.size(), _cachelist.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* total cache size */
|
||||
_totalSize += dataSize;
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
free(tmpdata);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -197,22 +217,26 @@ TxCache::get(uint64 checksum, GHQTexInfo *info)
|
|||
|
||||
/* find a match in cache */
|
||||
std::map<uint64, TXCACHE*>::iterator itMap = _cache.find(checksum);
|
||||
if (itMap != _cache.end()) {
|
||||
if (itMap != _cache.end())
|
||||
{
|
||||
/* yep, we've got it. */
|
||||
memcpy(info, &(((*itMap).second)->info), sizeof(GHQTexInfo));
|
||||
|
||||
/* push it to the back of the list */
|
||||
if (_cacheSize > 0) {
|
||||
if (_cacheSize > 0)
|
||||
{
|
||||
_cachelist.erase(((*itMap).second)->it);
|
||||
_cachelist.push_back(checksum);
|
||||
((*itMap).second)->it = --(_cachelist.end());
|
||||
}
|
||||
|
||||
/* zlib decompress it */
|
||||
if (info->format & GR_TEXFMT_GZ) {
|
||||
if (info->format & GR_TEXFMT_GZ)
|
||||
{
|
||||
uLongf destLen = _gzdestLen;
|
||||
uint8 *dest = (_gzdest0 == info->data) ? _gzdest1 : _gzdest0;
|
||||
if (uncompress(dest, &destLen, info->data, ((*itMap).second)->size) != Z_OK) {
|
||||
if (uncompress(dest, &destLen, info->data, ((*itMap).second)->size) != Z_OK)
|
||||
{
|
||||
DBG_INFO(80, L"Error: zlib decompression failed!\n");
|
||||
return 0;
|
||||
}
|
||||
|
@ -220,45 +244,27 @@ TxCache::get(uint64 checksum, GHQTexInfo *info)
|
|||
info->format &= ~GR_TEXFMT_GZ;
|
||||
DBG_INFO(80, L"zlib decompressed: %.02fkb->%.02fkb\n", (float)(((*itMap).second)->size) / 1000, (float)destLen / 1000);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
boolean
|
||||
TxCache::save(const wchar_t *path, const wchar_t *filename, int config)
|
||||
boolean TxCache::save(const char *path, const char *filename, int config)
|
||||
{
|
||||
if (!_cache.empty()) {
|
||||
/* dump cache to disk */
|
||||
char cbuf[MAX_PATH];
|
||||
if (!_cache.empty())
|
||||
{
|
||||
CPath(path, "").DirectoryCreate();
|
||||
|
||||
CPath cachepath(stdstr().FromUTF16(path), "");
|
||||
cachepath.DirectoryCreate();
|
||||
|
||||
/* Ugly hack to enable fopen/gzopen in Win9x */
|
||||
#ifdef _WIN32
|
||||
wchar_t curpath[MAX_PATH];
|
||||
GETCWD(MAX_PATH, curpath);
|
||||
cachepath.ChangeDirectory();
|
||||
#else
|
||||
char curpath[MAX_PATH];
|
||||
wcstombs(cbuf, cachepath.wstring().c_str(), MAX_PATH);
|
||||
GETCWD(MAX_PATH, curpath);
|
||||
CHDIR(cbuf);
|
||||
#endif
|
||||
|
||||
wcstombs(cbuf, filename, MAX_PATH);
|
||||
|
||||
gzFile gzfp = gzopen(cbuf, "wb1");
|
||||
gzFile gzfp = gzopen(CPath(path, filename), "wb1");
|
||||
DBG_INFO(80, L"gzfp:%x file:%ls\n", gzfp, filename);
|
||||
if (gzfp) {
|
||||
if (gzfp)
|
||||
{
|
||||
/* write header to determine config match */
|
||||
gzwrite(gzfp, &config, 4);
|
||||
|
||||
std::map<uint64, TXCACHE*>::iterator itMap = _cache.begin();
|
||||
while (itMap != _cache.end()) {
|
||||
while (itMap != _cache.end())
|
||||
{
|
||||
uint8 *dest = (*itMap).second->info.data;
|
||||
uint32 destLen = (*itMap).second->size;
|
||||
uint16 format = (*itMap).second->info.format;
|
||||
|
@ -280,7 +286,8 @@ TxCache::save(const wchar_t *path, const wchar_t *filename, int config)
|
|||
}
|
||||
}*/
|
||||
|
||||
if (dest && destLen) {
|
||||
if (dest && destLen)
|
||||
{
|
||||
/* texture checksum */
|
||||
gzwrite(gzfp, &((*itMap).first), 8);
|
||||
|
||||
|
@ -311,21 +318,19 @@ TxCache::save(const wchar_t *path, const wchar_t *filename, int config)
|
|||
}
|
||||
gzclose(gzfp);
|
||||
}
|
||||
|
||||
CHDIR(curpath);
|
||||
}
|
||||
return _cache.empty();
|
||||
}
|
||||
|
||||
boolean
|
||||
TxCache::load(const wchar_t *path, const wchar_t *filename, int config)
|
||||
boolean TxCache::load(const char *path, const char *filename, int config)
|
||||
{
|
||||
/* find it on disk */
|
||||
CPath cbuf(stdstr().FromUTF16(path).c_str(), stdstr().FromUTF16(filename).c_str());
|
||||
CPath cbuf(path, filename);
|
||||
|
||||
gzFile gzfp = gzopen(cbuf, "rb");
|
||||
DBG_INFO(80, L"gzfp:%x file:%ls\n", gzfp, filename);
|
||||
if (gzfp) {
|
||||
if (gzfp)
|
||||
{
|
||||
/* yep, we have it. load it into memory cache. */
|
||||
int dataSize;
|
||||
uint64 checksum;
|
||||
|
@ -334,8 +339,10 @@ TxCache::load(const wchar_t *path, const wchar_t *filename, int config)
|
|||
/* read header to determine config match */
|
||||
gzread(gzfp, &tmpconfig, 4);
|
||||
|
||||
if (tmpconfig == config) {
|
||||
do {
|
||||
if (tmpconfig == config)
|
||||
{
|
||||
do
|
||||
{
|
||||
memset(&tmpInfo, 0, sizeof(GHQTexInfo));
|
||||
|
||||
gzread(gzfp, &checksum, 8);
|
||||
|
@ -357,7 +364,8 @@ TxCache::load(const wchar_t *path, const wchar_t *filename, int config)
|
|||
gzread(gzfp, &dataSize, 4);
|
||||
|
||||
tmpInfo.data = (uint8*)malloc(dataSize);
|
||||
if (tmpInfo.data) {
|
||||
if (tmpInfo.data)
|
||||
{
|
||||
gzread(gzfp, tmpInfo.data, dataSize);
|
||||
|
||||
/* add to memory cache */
|
||||
|
@ -365,7 +373,8 @@ TxCache::load(const wchar_t *path, const wchar_t *filename, int config)
|
|||
|
||||
free(tmpInfo.data);
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
gzseek(gzfp, dataSize, SEEK_CUR);
|
||||
}
|
||||
|
||||
|
@ -376,17 +385,16 @@ TxCache::load(const wchar_t *path, const wchar_t *filename, int config)
|
|||
gzclose(gzfp);
|
||||
}
|
||||
}
|
||||
|
||||
return !_cache.empty();
|
||||
}
|
||||
|
||||
boolean
|
||||
TxCache::del(uint64 checksum)
|
||||
boolean TxCache::del(uint64 checksum)
|
||||
{
|
||||
if (!checksum || _cache.empty()) return 0;
|
||||
|
||||
std::map<uint64, TXCACHE*>::iterator itMap = _cache.find(checksum);
|
||||
if (itMap != _cache.end()) {
|
||||
if (itMap != _cache.end())
|
||||
{
|
||||
/* for texture cache (not hi-res cache) */
|
||||
if (!_cachelist.empty()) _cachelist.erase(((*itMap).second)->it);
|
||||
|
||||
|
@ -404,8 +412,7 @@ TxCache::del(uint64 checksum)
|
|||
return 0;
|
||||
}
|
||||
|
||||
boolean
|
||||
TxCache::is_cached(uint64 checksum)
|
||||
boolean TxCache::is_cached(uint64 checksum)
|
||||
{
|
||||
std::map<uint64, TXCACHE*>::iterator itMap = _cache.find(checksum);
|
||||
if (itMap != _cache.end()) return 1;
|
||||
|
@ -413,12 +420,13 @@ TxCache::is_cached(uint64 checksum)
|
|||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
TxCache::clear()
|
||||
void TxCache::clear()
|
||||
{
|
||||
if (!_cache.empty()) {
|
||||
if (!_cache.empty())
|
||||
{
|
||||
std::map<uint64, TXCACHE*>::iterator itMap = _cache.begin();
|
||||
while (itMap != _cache.end()) {
|
||||
while (itMap != _cache.end())
|
||||
{
|
||||
free((*itMap).second->info.data);
|
||||
delete (*itMap).second;
|
||||
itMap++;
|
||||
|
|
|
@ -39,8 +39,8 @@ private:
|
|||
uint32 _gzdestLen;
|
||||
protected:
|
||||
int _options;
|
||||
std::wstring _ident;
|
||||
std::wstring _path;
|
||||
std::string _ident;
|
||||
std::string _path;
|
||||
dispInfoFuncExt _callback;
|
||||
TxUtil *_txUtil;
|
||||
struct TXCACHE {
|
||||
|
@ -51,14 +51,14 @@ protected:
|
|||
int _totalSize;
|
||||
int _cacheSize;
|
||||
std::map<uint64, TXCACHE*> _cache;
|
||||
boolean save(const wchar_t *path, const wchar_t *filename, const int config);
|
||||
boolean load(const wchar_t *path, const wchar_t *filename, const int config);
|
||||
boolean save(const char *path, const char *filename, const int config);
|
||||
boolean load(const char *path, const char *filename, const int config);
|
||||
boolean del(uint64 checksum); /* checksum hi:palette low:texture */
|
||||
boolean is_cached(uint64 checksum); /* checksum hi:palette low:texture */
|
||||
void clear();
|
||||
public:
|
||||
~TxCache();
|
||||
TxCache(int options, int cachesize, const wchar_t *path, const wchar_t *ident,
|
||||
TxCache(int options, int cachesize, const char *path, const char *ident,
|
||||
dispInfoFuncExt callback);
|
||||
boolean add(uint64 checksum, /* checksum hi:palette low:texture */
|
||||
GHQTexInfo *info, int dataSize = 0);
|
||||
|
|
|
@ -54,7 +54,7 @@ TxFilter::~TxFilter()
|
|||
}
|
||||
|
||||
TxFilter::TxFilter(int maxwidth, int maxheight, int maxbpp, int options,
|
||||
int cachesize, const wchar_t *path, const wchar_t *ident,
|
||||
int cachesize, const char *path, const char *ident,
|
||||
dispInfoFuncExt callback) :
|
||||
_numcore(0),
|
||||
_tex1(NULL),
|
||||
|
@ -72,7 +72,7 @@ TxFilter::TxFilter(int maxwidth, int maxheight, int maxbpp, int options,
|
|||
_initialized(false)
|
||||
{
|
||||
/* HACKALERT: the emulator misbehaves and sometimes forgets to shutdown */
|
||||
if ((ident && wcscmp(ident, L"DEFAULT") != 0 && _ident.compare(ident) == 0) &&
|
||||
if ((ident && strcmp(ident, "DEFAULT") != 0 && _ident.compare(ident) == 0) &&
|
||||
_maxwidth == maxwidth &&
|
||||
_maxheight == maxheight &&
|
||||
_maxbpp == maxbpp &&
|
||||
|
@ -123,7 +123,7 @@ TxFilter::TxFilter(int maxwidth, int maxheight, int maxbpp, int options,
|
|||
_path.assign(path);
|
||||
|
||||
/* save ROM name */
|
||||
if (ident && wcscmp(ident, L"DEFAULT") != 0)
|
||||
if (ident && strcmp(ident, "DEFAULT") != 0)
|
||||
_ident.assign(ident);
|
||||
|
||||
/* check for dxtn extensions */
|
||||
|
@ -220,9 +220,9 @@ TxFilter::filter(uint8 *src, int srcwidth, int srcheight, uint16 srcformat, uint
|
|||
}
|
||||
texture = tmptex;
|
||||
destformat = GR_TEXFMT_ARGB_8888;
|
||||
}
|
||||
}
|
||||
#if !_16BPP_HACK
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (destformat) {
|
||||
|
@ -422,7 +422,7 @@ TxFilter::filter(uint8 *src, int srcwidth, int srcheight, uint16 srcformat, uint
|
|||
break;
|
||||
#endif /* _16BPP_HACK */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* fill in the texture info. */
|
||||
info->data = texture;
|
||||
|
@ -566,8 +566,10 @@ TxFilter::hirestex(uint64 g64crc, uint64 r_crc64, uint16 *palette, GHQTexInfo *i
|
|||
#endif
|
||||
|
||||
/* check if we have it in memory cache */
|
||||
if (_cacheSize && g64crc) {
|
||||
if (_txTexCache->get(g64crc, info)) {
|
||||
if (_cacheSize && g64crc)
|
||||
{
|
||||
if (_txTexCache->get(g64crc, info))
|
||||
{
|
||||
DBG_INFO(80, L"cache hit: %d x %d gfmt:%x\n", info->width, info->height, info->format);
|
||||
return 1; /* yep, we've got it */
|
||||
}
|
||||
|
@ -582,8 +584,9 @@ uint64
|
|||
TxFilter::checksum64(uint8 *src, int width, int height, int size, int rowStride, uint8 *palette)
|
||||
{
|
||||
if (_options & (HIRESTEXTURES_MASK | DUMP_TEX))
|
||||
{
|
||||
return _txUtil->checksum64(src, width, height, size, rowStride, palette);
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -591,52 +594,60 @@ boolean
|
|||
TxFilter::dmptx(uint8 *src, int width, int height, int rowStridePixel, uint16 gfmt, uint16 n64fmt, uint64 r_crc64)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return 0;
|
||||
|
||||
}
|
||||
if (!(_options & DUMP_TEX))
|
||||
{
|
||||
return 0;
|
||||
|
||||
}
|
||||
DBG_INFO(80, L"gfmt = %02x n64fmt = %02x\n", gfmt, n64fmt);
|
||||
DBG_INFO(80, L"hirestex: r_crc64:%08X %08X\n",
|
||||
(uint32)(r_crc64 >> 32), (uint32)(r_crc64 & 0xffffffff));
|
||||
|
||||
if (!_txQuantize->quantize(src, _tex1, rowStridePixel, height, (gfmt & 0x00ff), GR_TEXFMT_ARGB_8888))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
src = _tex1;
|
||||
|
||||
if (!_path.empty() && !_ident.empty()) {
|
||||
if (!_path.empty() && !_ident.empty())
|
||||
{
|
||||
/* dump it to disk */
|
||||
FILE *fp = NULL;
|
||||
CPath tmpbuf(stdstr().FromUTF16(_path.c_str()).c_str(), "");
|
||||
CPath tmpbuf(_path.c_str(), "");
|
||||
|
||||
/* create directories */
|
||||
tmpbuf.AppendDirectory("texture_dump");
|
||||
|
||||
if (!tmpbuf.DirectoryExists() && !tmpbuf.DirectoryCreate())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
tmpbuf.AppendDirectory(stdstr().FromUTF16(_ident.c_str()).c_str());
|
||||
tmpbuf.AppendDirectory(_ident.c_str());
|
||||
if (!tmpbuf.DirectoryExists() && !tmpbuf.DirectoryCreate())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
tmpbuf.AppendDirectory("GlideHQ");
|
||||
if (!tmpbuf.DirectoryExists() && !tmpbuf.DirectoryCreate())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((n64fmt >> 8) == 0x2) {
|
||||
if ((n64fmt >> 8) == 0x2)
|
||||
{
|
||||
tmpbuf.SetNameExtension(stdstr_f("%ls#%08X#%01X#%01X#%08X_ciByRGBA.png", _ident.c_str(), (uint32)(r_crc64 & 0xffffffff), (n64fmt >> 8), (n64fmt & 0xf), (uint32)(r_crc64 >> 32)).c_str());
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
tmpbuf.SetNameExtension(stdstr_f("%ls#%08X#%01X#%01X_all.png", _ident.c_str(), (uint32)(r_crc64 & 0xffffffff), (n64fmt >> 8), (n64fmt & 0xf)).c_str());
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if ((fp = fopen(tmpbuf, "wb")) != NULL) {
|
||||
#else
|
||||
char cbuf[MAX_PATH];
|
||||
wcstombs(cbuf, tmpbuf.c_str(), MAX_PATH);
|
||||
if ((fp = fopen(cbuf, "wb")) != NULL) {
|
||||
#endif
|
||||
if ((fp = fopen(tmpbuf, "wb")) != NULL)
|
||||
{
|
||||
_txImage->writePNG(src, fp, width, height, (rowStridePixel << 2), 0x0003, 0);
|
||||
fclose(fp);
|
||||
return 1;
|
||||
|
@ -646,8 +657,7 @@ TxFilter::dmptx(uint8 *src, int width, int height, int rowStridePixel, uint16 gf
|
|||
return 0;
|
||||
}
|
||||
|
||||
boolean
|
||||
TxFilter::reloadhirestex()
|
||||
boolean TxFilter::reloadhirestex()
|
||||
{
|
||||
DBG_INFO(80, L"Reload hires textures from texture pack.\n");
|
||||
|
||||
|
|
|
@ -44,8 +44,8 @@ private:
|
|||
int _maxbpp;
|
||||
int _options;
|
||||
int _cacheSize;
|
||||
std::wstring _ident;
|
||||
std::wstring _path;
|
||||
std::string _ident;
|
||||
std::string _path;
|
||||
TxQuantize *_txQuantize;
|
||||
TxTexCache *_txTexCache;
|
||||
TxHiResCache *_txHiResCache;
|
||||
|
@ -60,8 +60,8 @@ public:
|
|||
int maxbpp,
|
||||
int options,
|
||||
int cachesize,
|
||||
const wchar_t *path,
|
||||
const wchar_t *ident,
|
||||
const char *path,
|
||||
const char *ident,
|
||||
dispInfoFuncExt callback);
|
||||
boolean filter(uint8 *src,
|
||||
int srcwidth,
|
||||
|
|
|
@ -35,7 +35,7 @@ extern "C"{
|
|||
|
||||
TAPI boolean TAPIENTRY
|
||||
txfilter_init(int maxwidth, int maxheight, int maxbpp, int options, int cachesize,
|
||||
const wchar_t *path, const wchar_t*ident,
|
||||
const char *path, const char * ident,
|
||||
dispInfoFuncExt callback)
|
||||
{
|
||||
if (txFilter) return 0;
|
||||
|
|
|
@ -62,20 +62,23 @@
|
|||
#include <string>
|
||||
#include <Common/path.h>
|
||||
#include <Common/StdString.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
TxHiResCache::~TxHiResCache()
|
||||
{
|
||||
#if DUMP_CACHE
|
||||
if ((_options & DUMP_HIRESTEXCACHE) && !_haveCache && !_abortLoad) {
|
||||
if ((_options & DUMP_HIRESTEXCACHE) && !_haveCache && !_abortLoad)
|
||||
{
|
||||
/* dump cache to disk */
|
||||
std::wstring filename = _ident + L"_HIRESTEXTURES.dat";
|
||||
std::string filename = _ident + "_HIRESTEXTURES.dat";
|
||||
|
||||
CPath cachepath(stdstr().FromUTF16(_path.c_str()).c_str(), "");
|
||||
CPath cachepath(_path.c_str(), "");
|
||||
cachepath.AppendDirectory("cache");
|
||||
int config = _options & (HIRESTEXTURES_MASK | COMPRESS_HIRESTEX | COMPRESSION_MASK | TILE_HIRESTEX | FORCE16BPP_HIRESTEX | GZ_HIRESTEXCACHE | LET_TEXARTISTS_FLY);
|
||||
|
||||
TxCache::save(stdstr((std::string &)cachepath).ToUTF16().c_str(), filename.c_str(), config);
|
||||
TxCache::save(cachepath, filename.c_str(), config);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -84,10 +87,8 @@ TxHiResCache::~TxHiResCache()
|
|||
delete _txReSample;
|
||||
}
|
||||
|
||||
TxHiResCache::TxHiResCache(int maxwidth, int maxheight, int maxbpp, int options,
|
||||
const wchar_t *path, const wchar_t *ident,
|
||||
dispInfoFuncExt callback
|
||||
) : TxCache((options & ~GZ_TEXCACHE), 0, path, ident, callback)
|
||||
TxHiResCache::TxHiResCache(int maxwidth, int maxheight, int maxbpp, int options, const char *path, const char *ident, dispInfoFuncExt callback) :
|
||||
TxCache((options & ~GZ_TEXCACHE), 0, path, ident, callback)
|
||||
{
|
||||
_txImage = new TxImage();
|
||||
_txQuantize = new TxQuantize();
|
||||
|
@ -101,23 +102,26 @@ TxHiResCache::TxHiResCache(int maxwidth, int maxheight, int maxbpp, int options,
|
|||
|
||||
/* assert local options */
|
||||
if (!(_options & COMPRESS_HIRESTEX))
|
||||
{
|
||||
_options &= ~COMPRESSION_MASK;
|
||||
|
||||
if (_path.empty() || _ident.empty()) {
|
||||
}
|
||||
if (_path.empty() || _ident.empty())
|
||||
{
|
||||
_options &= ~DUMP_HIRESTEXCACHE;
|
||||
return;
|
||||
}
|
||||
|
||||
#if DUMP_CACHE
|
||||
/* read in hires texture cache */
|
||||
if (_options & DUMP_HIRESTEXCACHE) {
|
||||
if (_options & DUMP_HIRESTEXCACHE)
|
||||
{
|
||||
/* find it on disk */
|
||||
std::wstring filename = _ident + L"_HIRESTEXTURES.dat";
|
||||
CPath cachepath(stdstr().FromUTF16(_path.c_str()).c_str(), "");
|
||||
std::string filename = _ident + "_HIRESTEXTURES.dat";
|
||||
CPath cachepath(_path.c_str(), "");
|
||||
cachepath.AppendDirectory("cache");
|
||||
int config = _options & (HIRESTEXTURES_MASK | COMPRESS_HIRESTEX | COMPRESSION_MASK | TILE_HIRESTEX | FORCE16BPP_HIRESTEX | GZ_HIRESTEXCACHE | LET_TEXARTISTS_FLY);
|
||||
|
||||
_haveCache = TxCache::load(stdstr((std::string &)cachepath).ToUTF16().c_str(), filename.c_str(), config);
|
||||
_haveCache = TxCache::load(cachepath, filename.c_str(), config);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -134,12 +138,14 @@ TxHiResCache::empty()
|
|||
boolean
|
||||
TxHiResCache::load(boolean replace) /* 0 : reload, 1 : replace partial */
|
||||
{
|
||||
if (!_path.empty() && !_ident.empty()) {
|
||||
if (!_path.empty() && !_ident.empty())
|
||||
{
|
||||
if (!replace) TxCache::clear();
|
||||
|
||||
CPath dir_path(stdstr().FromUTF16(_path.c_str()).c_str(), "");
|
||||
CPath dir_path(_path.c_str(), "");
|
||||
|
||||
switch (_options & HIRESTEXTURES_MASK) {
|
||||
switch (_options & HIRESTEXTURES_MASK)
|
||||
{
|
||||
case GHQ_HIRESTEXTURES:
|
||||
break;
|
||||
case RICE_HIRESTEXTURES:
|
||||
|
@ -154,7 +160,7 @@ TxHiResCache::load(boolean replace) /* 0 : reload, 1 : replace partial */
|
|||
INFO(80, L" usage of only 2) and 3) highly recommended!\n");
|
||||
INFO(80, L" folder names must be in US-ASCII characters!\n");
|
||||
|
||||
dir_path.AppendDirectory(stdstr().FromUTF16(_ident.c_str()).c_str());
|
||||
dir_path.AppendDirectory(_ident.c_str());
|
||||
loadHiResTextures(dir_path, replace);
|
||||
break;
|
||||
case JABO_HIRESTEXTURES:
|
||||
|
@ -163,12 +169,12 @@ TxHiResCache::load(boolean replace) /* 0 : reload, 1 : replace partial */
|
|||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
||||
boolean TxHiResCache::loadHiResTextures(const char * dir_path, boolean replace)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
DBG_INFO(80, L"-----\n");
|
||||
DBG_INFO(80, L"path: %s\n", stdstr(dir_path).ToUTF16().c_str());
|
||||
|
||||
|
@ -196,7 +202,8 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
{
|
||||
do
|
||||
{
|
||||
if (KBHIT(0x1B)) {
|
||||
if (KBHIT(0x1B))
|
||||
{
|
||||
_abortLoad = 1;
|
||||
if (_callback) (*_callback)(L"Aborted loading hiresolution texture!\n");
|
||||
INFO(80, L"Error: aborted loading hiresolution texture!\n");
|
||||
|
@ -222,7 +229,7 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
std::string ident;
|
||||
FILE *fp = NULL;
|
||||
|
||||
wcstombs(fname, _ident.c_str(), MAX_PATH);
|
||||
strcpy(fname, _ident.c_str());
|
||||
/* XXX case sensitivity fiasco!
|
||||
* files must use _a, _rgb, _all, _allciByRGBA, _ciByRGBA, _ci
|
||||
* and file extensions must be in lower case letters! */
|
||||
|
@ -417,16 +424,16 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
/* use libpng style grayscale conversion */
|
||||
uint32 texel = ((uint32*)tmptex)[i];
|
||||
uint32 acomp = (((texel >> 16) & 0xff) * 6969 +
|
||||
((texel >> 8) & 0xff) * 23434 +
|
||||
((texel ) & 0xff) * 2365) / 32768;
|
||||
((texel >> 8) & 0xff) * 23434 +
|
||||
((texel)& 0xff) * 2365) / 32768;
|
||||
((uint32*)tex)[i] = (acomp << 24) | (((uint32*)tex)[i] & 0x00ffffff);
|
||||
#endif
|
||||
#if 0
|
||||
/* use the standard NTSC gray scale conversion */
|
||||
uint32 texel = ((uint32*)tmptex)[i];
|
||||
uint32 acomp = (((texel >> 16) & 0xff) * 299 +
|
||||
((texel >> 8) & 0xff) * 587 +
|
||||
((texel ) & 0xff) * 114) / 1000;
|
||||
((texel >> 8) & 0xff) * 587 +
|
||||
((texel)& 0xff) * 114) / 1000;
|
||||
((uint32*)tex)[i] = (acomp << 24) | (((uint32*)tex)[i] & 0x00ffffff);
|
||||
#endif
|
||||
}
|
||||
|
@ -838,8 +845,8 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
tex = NULL;
|
||||
DBG_INFO(80, L"Error: minification failed!\n");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* TEXTURE_TILING */
|
||||
|
||||
|
@ -940,8 +947,8 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
free(tmptex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
#if POW2_TEXTURES
|
||||
#if (POW2_TEXTURES == 2)
|
||||
|
@ -957,7 +964,7 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
continue;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/* quantize */
|
||||
{
|
||||
|
@ -1004,7 +1011,7 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* last minute validations */
|
||||
if (!tex || !chksum || !width || !height || !format || width > _maxwidth || height > _maxheight) {
|
||||
|
@ -1025,51 +1032,52 @@ boolean TxHiResCache::loadHiResTextures(LPCSTR dir_path, boolean replace)
|
|||
}
|
||||
|
||||
/* load it into hires texture cache. */
|
||||
{
|
||||
uint64 chksum64 = (uint64)palchksum;
|
||||
chksum64 <<= 32;
|
||||
chksum64 |= (uint64)chksum;
|
||||
{
|
||||
uint64 chksum64 = (uint64)palchksum;
|
||||
chksum64 <<= 32;
|
||||
chksum64 |= (uint64)chksum;
|
||||
|
||||
GHQTexInfo tmpInfo;
|
||||
memset(&tmpInfo, 0, sizeof(GHQTexInfo));
|
||||
GHQTexInfo tmpInfo;
|
||||
memset(&tmpInfo, 0, sizeof(GHQTexInfo));
|
||||
|
||||
tmpInfo.data = tex;
|
||||
tmpInfo.width = width;
|
||||
tmpInfo.height = height;
|
||||
tmpInfo.format = format;
|
||||
tmpInfo.largeLodLog2 = _txUtil->grLodLog2(width, height);
|
||||
tmpInfo.smallLodLog2 = tmpInfo.largeLodLog2;
|
||||
tmpInfo.aspectRatioLog2 = _txUtil->grAspectRatioLog2(width, height);
|
||||
tmpInfo.is_hires_tex = 1;
|
||||
tmpInfo.data = tex;
|
||||
tmpInfo.width = width;
|
||||
tmpInfo.height = height;
|
||||
tmpInfo.format = format;
|
||||
tmpInfo.largeLodLog2 = _txUtil->grLodLog2(width, height);
|
||||
tmpInfo.smallLodLog2 = tmpInfo.largeLodLog2;
|
||||
tmpInfo.aspectRatioLog2 = _txUtil->grAspectRatioLog2(width, height);
|
||||
tmpInfo.is_hires_tex = 1;
|
||||
|
||||
#if TEXTURE_TILING
|
||||
/* Glide64 style texture tiling. */
|
||||
if (untiled_width && untiled_height) {
|
||||
tmpInfo.tiles = ((untiled_width - 1) >> 8) + 1;
|
||||
tmpInfo.untiled_width = untiled_width;
|
||||
tmpInfo.untiled_height = untiled_height;
|
||||
}
|
||||
/* Glide64 style texture tiling. */
|
||||
if (untiled_width && untiled_height) {
|
||||
tmpInfo.tiles = ((untiled_width - 1) >> 8) + 1;
|
||||
tmpInfo.untiled_width = untiled_width;
|
||||
tmpInfo.untiled_height = untiled_height;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* remove redundant in cache */
|
||||
if (replace && TxCache::del(chksum64)) {
|
||||
DBG_INFO(80, L"removed duplicate old cache.\n");
|
||||
}
|
||||
|
||||
/* add to cache */
|
||||
if (TxCache::add(chksum64, &tmpInfo)) {
|
||||
/* Callback to display hires texture info.
|
||||
* Gonetz <gonetz(at)ngs.ru> */
|
||||
if (_callback) {
|
||||
wchar_t tmpbuf[MAX_PATH];
|
||||
mbstowcs(tmpbuf, fname, MAX_PATH);
|
||||
(*_callback)(L"[%d] total mem:%.2fmb - %ls\n", _cache.size(), (float)_totalSize / 1000000, tmpbuf);
|
||||
}
|
||||
DBG_INFO(80, L"texture loaded!\n");
|
||||
}
|
||||
free(tex);
|
||||
}
|
||||
} while (TextureDir.FindNext());
|
||||
/* remove redundant in cache */
|
||||
if (replace && TxCache::del(chksum64)) {
|
||||
DBG_INFO(80, L"removed duplicate old cache.\n");
|
||||
}
|
||||
|
||||
/* add to cache */
|
||||
if (TxCache::add(chksum64, &tmpInfo)) {
|
||||
/* Callback to display hires texture info.
|
||||
* Gonetz <gonetz(at)ngs.ru> */
|
||||
if (_callback) {
|
||||
wchar_t tmpbuf[MAX_PATH];
|
||||
mbstowcs(tmpbuf, fname, MAX_PATH);
|
||||
(*_callback)(L"[%d] total mem:%.2fmb - %ls\n", _cache.size(), (float)_totalSize / 1000000, tmpbuf);
|
||||
}
|
||||
DBG_INFO(80, L"texture loaded!\n");
|
||||
}
|
||||
free(tex);
|
||||
}
|
||||
} while (TextureDir.FindNext());
|
||||
}
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -38,22 +38,22 @@
|
|||
class TxHiResCache : public TxCache
|
||||
{
|
||||
private:
|
||||
int _maxwidth;
|
||||
int _maxheight;
|
||||
int _maxbpp;
|
||||
boolean _haveCache;
|
||||
boolean _abortLoad;
|
||||
TxImage *_txImage;
|
||||
TxQuantize *_txQuantize;
|
||||
TxReSample *_txReSample;
|
||||
boolean loadHiResTextures(LPCSTR dir_path, boolean replace);
|
||||
int _maxwidth;
|
||||
int _maxheight;
|
||||
int _maxbpp;
|
||||
boolean _haveCache;
|
||||
boolean _abortLoad;
|
||||
TxImage *_txImage;
|
||||
TxQuantize *_txQuantize;
|
||||
TxReSample *_txReSample;
|
||||
boolean loadHiResTextures(const char * dir_path, boolean replace);
|
||||
public:
|
||||
~TxHiResCache();
|
||||
TxHiResCache(int maxwidth, int maxheight, int maxbpp, int options,
|
||||
const wchar_t *path, const wchar_t *ident,
|
||||
dispInfoFuncExt callback);
|
||||
boolean empty();
|
||||
boolean load(boolean replace);
|
||||
~TxHiResCache();
|
||||
TxHiResCache(int maxwidth, int maxheight, int maxbpp, int options,
|
||||
const char *path, const char *ident,
|
||||
dispInfoFuncExt callback);
|
||||
boolean empty();
|
||||
boolean load(boolean replace);
|
||||
};
|
||||
|
||||
#endif /* __TXHIRESCACHE_H__ */
|
||||
|
|
|
@ -38,44 +38,46 @@
|
|||
TxTexCache::~TxTexCache()
|
||||
{
|
||||
#if DUMP_CACHE
|
||||
if (_options & DUMP_TEXCACHE) {
|
||||
/* dump cache to disk */
|
||||
std::wstring filename = _ident + L"_MEMORYCACHE.dat";
|
||||
CPath cachepath(stdstr().FromUTF16(_path.c_str()).c_str(),"");
|
||||
cachepath.AppendDirectory("cache");
|
||||
if (_options & DUMP_TEXCACHE)
|
||||
{
|
||||
/* dump cache to disk */
|
||||
std::string filename = _ident + "_MEMORYCACHE.dat";
|
||||
CPath cachepath(_path.c_str(), "");
|
||||
cachepath.AppendDirectory("cache");
|
||||
|
||||
int config = _options & (FILTER_MASK|ENHANCEMENT_MASK|COMPRESS_TEX|COMPRESSION_MASK|FORCE16BPP_TEX|GZ_TEXCACHE);
|
||||
int config = _options & (FILTER_MASK | ENHANCEMENT_MASK | COMPRESS_TEX | COMPRESSION_MASK | FORCE16BPP_TEX | GZ_TEXCACHE);
|
||||
|
||||
TxCache::save(stdstr((std::string &)cachepath).ToUTF16().c_str(), filename.c_str(), config);
|
||||
}
|
||||
TxCache::save(cachepath, filename.c_str(), config);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TxTexCache::TxTexCache(int options, int cachesize, const wchar_t *path, const wchar_t *ident,
|
||||
dispInfoFuncExt callback
|
||||
) : TxCache((options & ~GZ_HIRESTEXCACHE), cachesize, path, ident, callback)
|
||||
TxTexCache::TxTexCache(int options, int cachesize, const char *path, const char *ident, dispInfoFuncExt callback) :
|
||||
TxCache((options & ~GZ_HIRESTEXCACHE), cachesize, path, ident, callback)
|
||||
{
|
||||
/* assert local options */
|
||||
if (_path.empty() || _ident.empty() || !_cacheSize)
|
||||
_options &= ~DUMP_TEXCACHE;
|
||||
/* assert local options */
|
||||
if (_path.empty() || _ident.empty() || !_cacheSize)
|
||||
{
|
||||
_options &= ~DUMP_TEXCACHE;
|
||||
}
|
||||
|
||||
#if DUMP_CACHE
|
||||
if (_options & DUMP_TEXCACHE) {
|
||||
/* find it on disk */
|
||||
std::wstring filename = _ident + L"_MEMORYCACHE.dat";
|
||||
CPath cachepath(stdstr().FromUTF16(_path.c_str()),"");
|
||||
cachepath.AppendDirectory("cache");
|
||||
int config = _options & (FILTER_MASK|ENHANCEMENT_MASK|COMPRESS_TEX|COMPRESSION_MASK|FORCE16BPP_TEX|GZ_TEXCACHE);
|
||||
if (_options & DUMP_TEXCACHE)
|
||||
{
|
||||
/* find it on disk */
|
||||
std::string filename = _ident + "_MEMORYCACHE.dat";
|
||||
CPath cachepath(_path.c_str(), "");
|
||||
cachepath.AppendDirectory("cache");
|
||||
int config = _options & (FILTER_MASK | ENHANCEMENT_MASK | COMPRESS_TEX | COMPRESSION_MASK | FORCE16BPP_TEX | GZ_TEXCACHE);
|
||||
|
||||
TxCache::load(stdstr((std::string &)cachepath).ToUTF16().c_str(), filename.c_str(), config);
|
||||
}
|
||||
TxCache::load(cachepath, filename.c_str(), config);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
boolean
|
||||
TxTexCache::add(uint64 checksum, GHQTexInfo *info)
|
||||
boolean TxTexCache::add(uint64 checksum, GHQTexInfo *info)
|
||||
{
|
||||
if (_cacheSize <= 0) return 0;
|
||||
if (_cacheSize <= 0) return 0;
|
||||
|
||||
return TxCache::add(checksum, info);
|
||||
return TxCache::add(checksum, info);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class TxTexCache : public TxCache
|
|||
{
|
||||
public:
|
||||
~TxTexCache();
|
||||
TxTexCache(int options, int cachesize, const wchar_t *path, const wchar_t *ident,
|
||||
TxTexCache(int options, int cachesize, const char *path, const char *ident,
|
||||
dispInfoFuncExt callback);
|
||||
boolean add(uint64 checksum, /* checksum hi:palette low:texture */
|
||||
GHQTexInfo *info);
|
||||
|
|
Loading…
Reference in New Issue