2016-04-30 20:44:31 +00:00
// Copyright (c) 2016- PPSSPP Project.
// 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, version 2.0 or later versions.
// 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 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
2021-03-03 04:42:55 +00:00
# include "ppsspp_config.h"
2023-03-08 10:06:29 +00:00
2016-05-01 04:10:48 +00:00
# include <algorithm>
2021-05-16 16:30:33 +00:00
# include <cstring>
# include <memory>
2021-03-03 04:42:55 +00:00
# include <png.h>
2020-09-29 10:19:22 +00:00
2023-03-13 15:20:01 +00:00
# include "ext/basis_universal/basisu_transcoder.h"
2016-04-30 21:19:23 +00:00
# include "ext/xxhash.h"
2020-10-05 22:16:23 +00:00
2021-05-01 14:15:04 +00:00
# include "Common/Data/Convert/ColorConv.h"
2020-10-03 22:25:21 +00:00
# include "Common/Data/Format/IniFile.h"
2021-05-16 16:30:33 +00:00
# include "Common/Data/Format/ZIMLoad.h"
2023-03-07 21:20:56 +00:00
# include "Common/Data/Format/PNGLoad.h"
2021-05-01 14:15:04 +00:00
# include "Common/Data/Text/I18n.h"
2020-10-01 11:05:04 +00:00
# include "Common/Data/Text/Parsers.h"
2023-03-07 19:53:50 +00:00
# include "Common/File/VFS/DirectoryReader.h"
# include "Common/File/VFS/ZipFileReader.h"
2020-10-04 18:48:47 +00:00
# include "Common/File/FileUtil.h"
2023-03-07 09:02:59 +00:00
# include "Common/File/VFS/VFS.h"
2022-12-31 14:31:45 +00:00
# include "Common/LogReporting.h"
2020-09-29 10:19:22 +00:00
# include "Common/StringUtils.h"
2023-03-25 09:29:18 +00:00
# include "Common/System/System.h"
2020-11-27 23:12:06 +00:00
# include "Common/Thread/ParallelLoop.h"
2022-04-08 10:04:34 +00:00
# include "Common/Thread/Waitable.h"
2022-04-17 22:25:59 +00:00
# include "Common/Thread/ThreadManager.h"
2021-10-17 16:16:54 +00:00
# include "Common/TimeUtil.h"
2016-04-30 21:05:03 +00:00
# include "Core/Config.h"
# include "Core/System.h"
2021-05-16 16:30:33 +00:00
# include "Core/ThreadPools.h"
2016-04-30 21:05:03 +00:00
# include "Core/ELF/ParamSFO.h"
2023-03-09 20:11:53 +00:00
# include "GPU/Common/TextureReplacer.h"
2016-04-30 21:19:23 +00:00
# include "GPU/Common/TextureDecoder.h"
2016-04-30 20:44:31 +00:00
2016-04-30 23:21:16 +00:00
static const std : : string INI_FILENAME = " textures.ini " ;
2022-10-30 17:34:25 +00:00
static const std : : string ZIP_FILENAME = " textures.zip " ;
2016-05-01 03:21:08 +00:00
static const std : : string NEW_TEXTURE_DIR = " new/ " ;
2016-04-30 23:21:16 +00:00
static const int VERSION = 1 ;
2022-10-30 15:15:37 +00:00
static const double MAX_CACHE_SIZE = 4.0 ;
2023-03-13 15:20:01 +00:00
static bool basisu_initialized = false ;
2016-04-30 23:21:16 +00:00
2023-03-08 23:05:24 +00:00
TextureReplacer : : TextureReplacer ( Draw : : DrawContext * draw ) {
2023-03-13 15:20:01 +00:00
if ( ! basisu_initialized ) {
basist : : basisu_transcoder_init ( ) ;
basisu_initialized = true ;
}
2023-03-12 12:19:01 +00:00
// We don't want to keep the draw object around, so extract the info we need.
if ( draw - > GetDataFormatSupport ( Draw : : DataFormat : : BC3_UNORM_BLOCK ) ) formatSupport_ . bc123 = true ;
if ( draw - > GetDataFormatSupport ( Draw : : DataFormat : : ASTC_4x4_UNORM_BLOCK ) ) formatSupport_ . astc = true ;
if ( draw - > GetDataFormatSupport ( Draw : : DataFormat : : BC7_UNORM_BLOCK ) ) formatSupport_ . bc7 = true ;
if ( draw - > GetDataFormatSupport ( Draw : : DataFormat : : ETC2_R8G8B8_UNORM_BLOCK ) ) formatSupport_ . etc2 = true ;
2023-03-08 23:05:24 +00:00
}
2016-04-30 20:44:31 +00:00
TextureReplacer : : ~ TextureReplacer ( ) {
2023-03-16 10:44:38 +00:00
for ( auto iter : levelCache_ ) {
2023-03-08 10:31:32 +00:00
delete iter . second ;
}
2023-03-07 19:53:50 +00:00
delete vfs_ ;
2016-04-30 20:44:31 +00:00
}
void TextureReplacer : : Init ( ) {
2016-04-30 21:05:03 +00:00
NotifyConfigChanged ( ) ;
2016-04-30 20:44:31 +00:00
}
void TextureReplacer : : NotifyConfigChanged ( ) {
2017-06-03 03:54:28 +00:00
gameID_ = g_paramSFO . GetDiscID ( ) ;
2016-04-30 21:05:03 +00:00
2022-07-28 03:27:56 +00:00
bool wasEnabled = enabled_ ;
2017-05-26 21:15:04 +00:00
enabled_ = g_Config . bReplaceTextures | | g_Config . bSaveNewTextures ;
2016-04-30 21:05:03 +00:00
if ( enabled_ ) {
2021-05-11 07:50:28 +00:00
basePath_ = GetSysDirectory ( DIRECTORY_TEXTURES ) / gameID_ ;
2016-04-30 21:05:03 +00:00
2023-03-10 13:58:44 +00:00
newTextureDir_ = basePath_ / NEW_TEXTURE_DIR ;
2021-05-09 13:02:46 +00:00
2016-04-30 21:05:03 +00:00
// If we're saving, auto-create the directory.
2023-03-10 13:58:44 +00:00
if ( g_Config . bSaveNewTextures & & ! File : : Exists ( newTextureDir_ ) ) {
File : : CreateFullPath ( newTextureDir_ ) ;
File : : CreateEmptyFile ( newTextureDir_ / " .nomedia " ) ;
2016-04-30 21:05:03 +00:00
}
2022-10-30 14:55:17 +00:00
enabled_ = File : : IsDirectory ( basePath_ ) ;
2022-07-28 03:27:56 +00:00
} else if ( wasEnabled ) {
2023-03-07 19:53:50 +00:00
delete vfs_ ;
vfs_ = nullptr ;
2022-07-28 03:27:56 +00:00
Decimate ( ReplacerDecimateMode : : ALL ) ;
2016-04-30 21:05:03 +00:00
}
2016-04-30 23:21:16 +00:00
if ( enabled_ ) {
enabled_ = LoadIni ( ) ;
}
}
bool TextureReplacer : : LoadIni ( ) {
hash_ = ReplacedTextureHash : : QUICK ;
aliases_ . clear ( ) ;
hashranges_ . clear ( ) ;
2021-02-28 01:16:16 +00:00
filtering_ . clear ( ) ;
2021-04-01 09:18:06 +00:00
reducehashranges_ . clear ( ) ;
2016-04-30 23:21:16 +00:00
2019-07-14 23:01:37 +00:00
allowVideo_ = false ;
ignoreAddress_ = false ;
reduceHash_ = false ;
2021-04-01 09:18:06 +00:00
reduceHashGlobalValue = 0.5 ;
2020-04-13 14:29:07 +00:00
// Prevents dumping the mipmaps.
ignoreMipmap_ = false ;
2019-07-14 23:01:37 +00:00
2023-03-07 19:53:50 +00:00
delete vfs_ ;
vfs_ = nullptr ;
2022-10-30 17:34:25 +00:00
2023-03-16 21:59:26 +00:00
Path zipPath = basePath_ / ZIP_FILENAME ;
2022-10-30 17:34:25 +00:00
// First, check for textures.zip, which is used to reduce IO.
2023-03-16 21:59:26 +00:00
VFSBackend * dir = ZipFileReader : : Create ( zipPath , " " , false ) ;
2023-03-07 19:53:50 +00:00
if ( ! dir ) {
2023-03-16 21:59:26 +00:00
INFO_LOG ( G3D , " %s wasn't a zip file - opening the directory %s instead. " , zipPath . c_str ( ) , basePath_ . c_str ( ) ) ;
2023-03-08 08:29:19 +00:00
vfsIsZip_ = false ;
2023-03-07 19:53:50 +00:00
dir = new DirectoryReader ( basePath_ ) ;
2023-03-08 08:29:19 +00:00
} else {
vfsIsZip_ = true ;
2022-10-30 17:34:25 +00:00
}
2023-03-07 19:53:50 +00:00
IniFile ini ;
2023-03-14 16:52:40 +00:00
bool iniLoaded = ini . LoadFromVFS ( * dir , INI_FILENAME ) ;
2022-10-30 17:34:25 +00:00
if ( iniLoaded ) {
2023-05-01 21:20:54 +00:00
if ( ! LoadIniValues ( ini , dir ) ) {
2023-03-07 19:53:50 +00:00
delete dir ;
2016-04-30 23:21:16 +00:00
return false ;
}
2019-07-14 23:01:37 +00:00
// Allow overriding settings per game id.
std : : string overrideFilename ;
if ( ini . GetOrCreateSection ( " games " ) - > Get ( gameID_ . c_str ( ) , & overrideFilename , " " ) ) {
if ( ! overrideFilename . empty ( ) & & overrideFilename ! = INI_FILENAME ) {
IniFile overrideIni ;
2023-03-07 19:53:50 +00:00
iniLoaded = overrideIni . LoadFromVFS ( * dir , overrideFilename ) ;
2022-10-30 17:34:25 +00:00
if ( ! iniLoaded ) {
2022-10-30 14:55:17 +00:00
ERROR_LOG ( G3D , " Failed to load extra texture ini: %s " , overrideFilename . c_str ( ) ) ;
2023-03-08 07:23:13 +00:00
// Since this error is most likely to occure for texture pack creators, let's just bail here
// so that the creator is more likely to look in the logs for what happened.
2023-03-07 19:53:50 +00:00
delete dir ;
2022-10-30 14:55:17 +00:00
return false ;
}
2016-05-01 15:58:14 +00:00
2022-10-30 14:55:17 +00:00
INFO_LOG ( G3D , " Loading extra texture ini: %s " , overrideFilename . c_str ( ) ) ;
2023-05-01 21:20:54 +00:00
if ( ! LoadIniValues ( overrideIni , dir , true ) ) {
2023-03-07 19:53:50 +00:00
delete dir ;
2019-07-14 23:01:37 +00:00
return false ;
}
}
2017-05-06 22:12:20 +00:00
}
2023-03-08 08:29:19 +00:00
} else {
if ( vfsIsZip_ ) {
// We don't accept zip files without inis.
ERROR_LOG ( G3D , " Texture pack lacking ini file: %s " , basePath_ . c_str ( ) ) ;
delete dir ;
return false ;
} else {
WARN_LOG ( G3D , " Texture pack lacking ini file: %s " , basePath_ . c_str ( ) ) ;
}
2019-07-14 23:01:37 +00:00
}
2016-05-01 15:58:14 +00:00
2023-03-07 19:53:50 +00:00
vfs_ = dir ;
2023-03-10 22:58:15 +00:00
// If we have stuff loaded from before, need to update the vfs pointers to avoid
// crash on exit. The actual problem is that we tend to call LoadIni a little too much...
2023-03-16 10:44:38 +00:00
for ( auto & repl : levelCache_ ) {
2023-03-10 22:58:15 +00:00
repl . second - > vfs_ = vfs_ ;
}
2023-03-14 16:52:40 +00:00
if ( vfsIsZip_ ) {
INFO_LOG ( G3D , " Texture pack activated from '%s' " , ( basePath_ / ZIP_FILENAME ) . c_str ( ) ) ;
} else {
INFO_LOG ( G3D , " Texture pack activated from '%s' " , basePath_ . c_str ( ) ) ;
}
2023-03-07 19:53:50 +00:00
// The ini doesn't have to exist for the texture directory or zip to be valid.
2019-07-14 23:01:37 +00:00
return true ;
}
2023-05-01 21:20:54 +00:00
bool TextureReplacer : : LoadIniValues ( IniFile & ini , VFSBackend * dir , bool isOverride ) {
2019-07-14 23:01:37 +00:00
auto options = ini . GetOrCreateSection ( " options " ) ;
std : : string hash ;
options - > Get ( " hash " , & hash , " " ) ;
if ( strcasecmp ( hash . c_str ( ) , " quick " ) = = 0 ) {
hash_ = ReplacedTextureHash : : QUICK ;
} else if ( strcasecmp ( hash . c_str ( ) , " xxh32 " ) = = 0 ) {
hash_ = ReplacedTextureHash : : XXH32 ;
} else if ( strcasecmp ( hash . c_str ( ) , " xxh64 " ) = = 0 ) {
hash_ = ReplacedTextureHash : : XXH64 ;
} else if ( ! isOverride | | ! hash . empty ( ) ) {
ERROR_LOG ( G3D , " Unsupported hash type: %s " , hash . c_str ( ) ) ;
return false ;
}
options - > Get ( " video " , & allowVideo_ , allowVideo_ ) ;
options - > Get ( " ignoreAddress " , & ignoreAddress_ , ignoreAddress_ ) ;
// Multiplies sizeInRAM/bytesPerLine in XXHASH by 0.5.
options - > Get ( " reduceHash " , & reduceHash_ , reduceHash_ ) ;
2020-04-13 14:29:07 +00:00
options - > Get ( " ignoreMipmap " , & ignoreMipmap_ , ignoreMipmap_ ) ;
2019-07-14 23:01:37 +00:00
if ( reduceHash_ & & hash_ = = ReplacedTextureHash : : QUICK ) {
reduceHash_ = false ;
ERROR_LOG ( G3D , " Texture Replacement: reduceHash option requires safer hash, use xxh32 or xxh64 instead. " ) ;
}
if ( ignoreAddress_ & & hash_ = = ReplacedTextureHash : : QUICK ) {
ignoreAddress_ = false ;
ERROR_LOG ( G3D , " Texture Replacement: ignoreAddress option requires safer hash, use xxh32 or xxh64 instead. " ) ;
}
int version = 0 ;
if ( options - > Get ( " version " , & version , 0 ) & & version > VERSION ) {
ERROR_LOG ( G3D , " Unsupported texture replacement version %d, trying anyway " , version ) ;
}
2016-04-30 23:21:16 +00:00
2019-07-14 23:01:37 +00:00
bool filenameWarning = false ;
2023-05-01 21:20:54 +00:00
std : : map < ReplacementCacheKey , std : : map < int , std : : string > > filenameMap ;
2019-07-14 23:01:37 +00:00
if ( ini . HasSection ( " hashes " ) ) {
auto hashes = ini . GetOrCreateSection ( " hashes " ) - > ToMap ( ) ;
// Format: hashname = filename.png
2023-03-09 09:51:15 +00:00
bool checkFilenames = g_Config . bSaveNewTextures & & ! g_Config . bIgnoreTextureFilenames & & ! vfsIsZip_ ;
2023-03-08 23:01:38 +00:00
2019-07-14 23:01:37 +00:00
for ( const auto & item : hashes ) {
2023-03-08 23:01:38 +00:00
ReplacementCacheKey key ( 0 , 0 ) ;
2023-03-28 09:18:45 +00:00
int level = 0 ; // sscanf might fail to pluck the level, but that's ok, we default to 0. sscanf doesn't write to non-matched outputs.
2023-03-08 23:01:38 +00:00
if ( sscanf ( item . first . c_str ( ) , " %16llx%8x_%d " , & key . cachekey , & key . hash , & level ) > = 1 ) {
filenameMap [ key ] [ level ] = item . second ;
2019-07-14 23:01:37 +00:00
if ( checkFilenames ) {
2018-10-01 00:55:51 +00:00
# if PPSSPP_PLATFORM(WINDOWS)
2019-07-14 23:01:37 +00:00
// Uppercase probably means the filenames don't match.
// Avoiding an actual check of the filenames to avoid performance impact.
2023-03-09 09:51:15 +00:00
filenameWarning = filenameWarning | | item . second . find_first_of ( " \\ ABCDEFGHIJKLMNOPQRSTUVWXYZ:<>|?* " ) ! = std : : string : : npos ;
2018-10-01 00:55:51 +00:00
# else
2019-07-14 23:01:37 +00:00
filenameWarning = filenameWarning | | item . second . find_first_of ( " \\ :<>|?* " ) ! = std : : string : : npos ;
2018-10-01 00:55:51 +00:00
# endif
2016-05-31 05:31:02 +00:00
}
2019-07-14 23:01:37 +00:00
} else {
ERROR_LOG ( G3D , " Unsupported syntax under [hashes]: %s " , item . first . c_str ( ) ) ;
2016-04-30 23:21:16 +00:00
}
}
2023-05-01 21:20:54 +00:00
}
2023-03-08 23:01:38 +00:00
2023-05-01 21:20:54 +00:00
// Scan the root of the texture folder/zip and preinitialize the hash map.
std : : vector < File : : FileInfo > filesInRoot ;
dir - > GetFileListing ( " / " , & filesInRoot , nullptr ) ;
for ( auto file : filesInRoot ) {
if ( file . isDirectory )
continue ;
if ( file . name . empty ( ) | | file . name [ 0 ] = = ' . ' )
continue ;
Path path ( file . name ) ;
std : : string ext = path . GetFileExtension ( ) ;
std : : string hash = file . name . substr ( 0 , file . name . size ( ) - ext . size ( ) ) ;
if ( ! ( ( hash . size ( ) > = 26 & & hash . size ( ) < = 27 & & hash [ 24 ] = = ' _ ' ) | | hash . size ( ) = = 24 ) ) {
continue ;
}
// OK, it's hash-like enough to try to parse it into the map.
if ( equalsNoCase ( ext , " .ktx2 " ) | | equalsNoCase ( ext , " .png " ) | | equalsNoCase ( ext , " .dds " ) ) {
ReplacementCacheKey key ( 0 , 0 ) ;
int level = 0 ; // sscanf might fail to pluck the level, but that's ok, we default to 0. sscanf doesn't write to non-matched outputs.
if ( sscanf ( hash . c_str ( ) , " %16llx%8x_%d " , & key . cachekey , & key . hash , & level ) > = 1 ) {
INFO_LOG ( G3D , " hash-like file in root, adding: %s " , file . name . c_str ( ) ) ;
filenameMap [ key ] [ level ] = file . name ;
2023-03-08 23:01:38 +00:00
}
2023-05-01 21:20:54 +00:00
}
}
// Now, translate the filenameMap to the final aliasMap.
for ( auto & pair : filenameMap ) {
std : : string alias ;
int mipIndex = 0 ;
for ( auto & level : pair . second ) {
if ( level . first = = mipIndex ) {
alias + = level . second + " | " ;
mipIndex + + ;
} else {
WARN_LOG ( G3D , " Non-sequential mip index %d, breaking. filenames=%s " , level . first , level . second . c_str ( ) ) ;
break ;
2023-03-08 23:01:38 +00:00
}
2023-05-01 21:20:54 +00:00
}
if ( alias = = " | " ) {
alias = " " ; // marker for no replacement
}
// Replace any '\' with '/', to be safe and consistent. Since these are from the ini file, we do this on all platforms.
for ( auto & c : alias ) {
if ( c = = ' \\ ' ) {
c = ' / ' ;
2023-03-28 09:18:45 +00:00
}
2023-03-08 23:01:38 +00:00
}
2023-05-01 21:20:54 +00:00
aliases_ [ pair . first ] = alias ;
2019-07-14 23:01:37 +00:00
}
2016-04-30 23:21:16 +00:00
2019-07-14 23:01:37 +00:00
if ( filenameWarning ) {
2023-04-05 22:34:50 +00:00
auto err = GetI18NCategory ( I18NCat : : ERRORS ) ;
2023-03-25 09:29:18 +00:00
System_NotifyUserMessage ( err - > T ( " textures.ini filenames may not be cross-platform (banned characters) " ) , 6.0f ) ;
2019-07-14 23:01:37 +00:00
}
2018-10-01 00:55:51 +00:00
2019-07-14 23:01:37 +00:00
if ( ini . HasSection ( " hashranges " ) ) {
auto hashranges = ini . GetOrCreateSection ( " hashranges " ) - > ToMap ( ) ;
// Format: addr,w,h = newW,newH
for ( const auto & item : hashranges ) {
ParseHashRange ( item . first , item . second ) ;
2016-04-30 23:21:16 +00:00
}
}
2021-02-28 01:16:16 +00:00
if ( ini . HasSection ( " filtering " ) ) {
auto filters = ini . GetOrCreateSection ( " filtering " ) - > ToMap ( ) ;
// Format: hashname = nearest or linear
for ( const auto & item : filters ) {
ParseFiltering ( item . first , item . second ) ;
}
}
2021-04-01 09:18:06 +00:00
if ( ini . HasSection ( " reducehashranges " ) ) {
auto reducehashranges = ini . GetOrCreateSection ( " reducehashranges " ) - > ToMap ( ) ;
2023-03-07 09:02:59 +00:00
// Format: w,h = reducehashvalues
2021-04-01 09:18:06 +00:00
for ( const auto & item : reducehashranges ) {
ParseReduceHashRange ( item . first , item . second ) ;
}
}
2016-04-30 23:21:16 +00:00
return true ;
}
void TextureReplacer : : ParseHashRange ( const std : : string & key , const std : : string & value ) {
std : : vector < std : : string > keyParts ;
SplitString ( key , ' , ' , keyParts ) ;
std : : vector < std : : string > valueParts ;
SplitString ( value , ' , ' , valueParts ) ;
if ( keyParts . size ( ) ! = 3 | | valueParts . size ( ) ! = 2 ) {
ERROR_LOG ( G3D , " Ignoring invalid hashrange %s = %s, expecting addr,w,h = w,h " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
2023-03-18 23:00:59 +00:00
// Allow addr not starting with 0x, for consistency. TryParse requires 0x to parse as hex.
if ( ! startsWith ( keyParts [ 0 ] , " 0x " ) & & ! startsWith ( keyParts [ 0 ] , " 0X " ) ) {
keyParts [ 0 ] = " 0x " + keyParts [ 0 ] ;
}
2016-04-30 23:21:16 +00:00
u32 addr ;
u32 fromW ;
u32 fromH ;
if ( ! TryParse ( keyParts [ 0 ] , & addr ) | | ! TryParse ( keyParts [ 1 ] , & fromW ) | | ! TryParse ( keyParts [ 2 ] , & fromH ) ) {
ERROR_LOG ( G3D , " Ignoring invalid hashrange %s = %s, key format is 0x12345678,512,512 " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
u32 toW ;
u32 toH ;
if ( ! TryParse ( valueParts [ 0 ] , & toW ) | | ! TryParse ( valueParts [ 1 ] , & toH ) ) {
ERROR_LOG ( G3D , " Ignoring invalid hashrange %s = %s, value format is 512,512 " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
2016-05-01 01:39:30 +00:00
if ( toW > fromW | | toH > fromH ) {
ERROR_LOG ( G3D , " Ignoring invalid hashrange %s = %s, range bigger than source " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
2017-11-25 20:34:52 +00:00
const u64 rangeKey = ( ( u64 ) addr < < 32 ) | ( ( u64 ) fromW < < 16 ) | fromH ;
2016-04-30 23:21:16 +00:00
hashranges_ [ rangeKey ] = WidthHeightPair ( toW , toH ) ;
2016-04-30 20:44:31 +00:00
}
2021-02-28 01:16:16 +00:00
void TextureReplacer : : ParseFiltering ( const std : : string & key , const std : : string & value ) {
ReplacementCacheKey itemKey ( 0 , 0 ) ;
if ( sscanf ( key . c_str ( ) , " %16llx%8x " , & itemKey . cachekey , & itemKey . hash ) > = 1 ) {
if ( ! strcasecmp ( value . c_str ( ) , " nearest " ) ) {
filtering_ [ itemKey ] = TEX_FILTER_FORCE_NEAREST ;
} else if ( ! strcasecmp ( value . c_str ( ) , " linear " ) ) {
filtering_ [ itemKey ] = TEX_FILTER_FORCE_LINEAR ;
} else if ( ! strcasecmp ( value . c_str ( ) , " auto " ) ) {
filtering_ [ itemKey ] = TEX_FILTER_AUTO ;
} else {
ERROR_LOG ( G3D , " Unsupported syntax under [filtering]: %s " , value . c_str ( ) ) ;
}
} else {
ERROR_LOG ( G3D , " Unsupported syntax under [filtering]: %s " , key . c_str ( ) ) ;
}
}
2021-04-01 09:18:06 +00:00
void TextureReplacer : : ParseReduceHashRange ( const std : : string & key , const std : : string & value ) {
std : : vector < std : : string > keyParts ;
SplitString ( key , ' , ' , keyParts ) ;
std : : vector < std : : string > valueParts ;
SplitString ( value , ' , ' , valueParts ) ;
if ( keyParts . size ( ) ! = 2 | | valueParts . size ( ) ! = 1 ) {
ERROR_LOG ( G3D , " Ignoring invalid reducehashrange %s = %s, expecting w,h = reducehashvalue " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
u32 forW ;
u32 forH ;
if ( ! TryParse ( keyParts [ 0 ] , & forW ) | | ! TryParse ( keyParts [ 1 ] , & forH ) ) {
ERROR_LOG ( G3D , " Ignoring invalid reducehashrange %s = %s, key format is 512,512 " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
float rhashvalue ;
if ( ! TryParse ( valueParts [ 0 ] , & rhashvalue ) ) {
ERROR_LOG ( G3D , " Ignoring invalid reducehashrange %s = %s, value format is 0.5 " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
if ( rhashvalue = = 0 ) {
ERROR_LOG ( G3D , " Ignoring invalid hashrange %s = %s, reducehashvalue can't be 0 " , key . c_str ( ) , value . c_str ( ) ) ;
return ;
}
const u64 reducerangeKey = ( ( u64 ) forW < < 16 ) | forH ;
reducehashranges_ [ reducerangeKey ] = rhashvalue ;
2023-03-07 09:02:59 +00:00
}
2021-04-01 09:18:06 +00:00
2016-04-30 20:44:31 +00:00
u32 TextureReplacer : : ComputeHash ( u32 addr , int bufw , int w , int h , GETextureFormat fmt , u16 maxSeenV ) {
2020-07-19 15:47:02 +00:00
_dbg_assert_msg_ ( enabled_ , " Replacement not enabled " ) ;
2016-04-30 21:19:23 +00:00
2023-03-16 08:55:49 +00:00
if ( ! LookupHashRange ( addr , w , h , & w , & h ) ) {
2016-04-30 21:19:23 +00:00
// There wasn't any hash range, let's fall back to maxSeenV logic.
if ( h = = 512 & & maxSeenV < 512 & & maxSeenV ! = 0 ) {
h = ( int ) maxSeenV ;
}
}
2023-01-01 20:12:04 +00:00
const u8 * checkp = Memory : : GetPointerUnchecked ( addr ) ;
2021-04-01 09:18:06 +00:00
if ( reduceHash_ ) {
reduceHashSize = LookupReduceHashRange ( w , h ) ;
// default to reduceHashGlobalValue which default is 0.5
}
2016-05-01 00:30:32 +00:00
if ( bufw < = w ) {
// We can assume the data is contiguous. These are the total used pixels.
const u32 totalPixels = bufw * h + ( w - bufw ) ;
2017-05-07 04:03:21 +00:00
const u32 sizeInRAM = ( textureBitsPerPixel [ fmt ] * totalPixels ) / 8 * reduceHashSize ;
2016-05-01 00:30:32 +00:00
switch ( hash_ ) {
case ReplacedTextureHash : : QUICK :
return StableQuickTexHash ( checkp , sizeInRAM ) ;
2017-05-06 12:51:24 +00:00
case ReplacedTextureHash : : XXH32 :
2020-08-28 08:15:22 +00:00
return XXH32 ( checkp , sizeInRAM , 0xBACD7814 ) ;
2017-05-06 12:51:24 +00:00
case ReplacedTextureHash : : XXH64 :
2020-08-28 08:15:22 +00:00
return XXH64 ( checkp , sizeInRAM , 0xBACD7814 ) ;
2016-05-01 00:30:32 +00:00
default :
return 0 ;
}
} else {
// We have gaps. Let's hash each row and sum.
2017-05-07 04:03:21 +00:00
const u32 bytesPerLine = ( textureBitsPerPixel [ fmt ] * w ) / 8 * reduceHashSize ;
2016-05-01 00:30:32 +00:00
const u32 stride = ( textureBitsPerPixel [ fmt ] * bufw ) / 8 ;
u32 result = 0 ;
switch ( hash_ ) {
case ReplacedTextureHash : : QUICK :
for ( int y = 0 ; y < h ; + + y ) {
u32 rowHash = StableQuickTexHash ( checkp , bytesPerLine ) ;
result = ( result * 11 ) ^ rowHash ;
checkp + = stride ;
}
break ;
2017-05-06 12:51:24 +00:00
case ReplacedTextureHash : : XXH32 :
for ( int y = 0 ; y < h ; + + y ) {
2020-08-28 08:15:22 +00:00
u32 rowHash = XXH32 ( checkp , bytesPerLine , 0xBACD7814 ) ;
2017-05-06 12:51:24 +00:00
result = ( result * 11 ) ^ rowHash ;
checkp + = stride ;
}
break ;
case ReplacedTextureHash : : XXH64 :
for ( int y = 0 ; y < h ; + + y ) {
2020-08-28 08:15:22 +00:00
u32 rowHash = XXH64 ( checkp , bytesPerLine , 0xBACD7814 ) ;
2017-05-06 12:51:24 +00:00
result = ( result * 11 ) ^ rowHash ;
2016-05-01 00:30:32 +00:00
checkp + = stride ;
}
break ;
default :
break ;
}
return result ;
2016-04-30 23:21:16 +00:00
}
2016-04-30 20:44:31 +00:00
}
2023-03-16 10:44:38 +00:00
ReplacedTexture * TextureReplacer : : FindReplacement ( u64 cachekey , u32 hash , int w , int h ) {
2016-04-30 21:05:03 +00:00
// Only actually replace if we're replacing. We might just be saving.
2016-05-01 15:04:15 +00:00
if ( ! Enabled ( ) | | ! g_Config . bReplaceTextures ) {
2023-03-08 10:31:32 +00:00
return nullptr ;
2016-05-01 02:35:35 +00:00
}
ReplacementCacheKey replacementKey ( cachekey , hash ) ;
auto it = cache_ . find ( replacementKey ) ;
if ( it ! = cache_ . end ( ) ) {
2023-03-16 10:44:38 +00:00
return it - > second . texture ;
2022-08-22 05:07:05 +00:00
}
2016-05-01 01:39:30 +00:00
2023-03-16 10:44:38 +00:00
ReplacementDesc desc ;
desc . newW = w ;
desc . newH = h ;
desc . w = w ;
desc . h = h ;
desc . cachekey = cachekey ;
desc . hash = hash ;
LookupHashRange ( cachekey > > 32 , w , h , & desc . newW , & desc . newH ) ;
2016-05-01 01:39:30 +00:00
2017-05-05 19:40:40 +00:00
if ( ignoreAddress_ ) {
cachekey = cachekey & 0xFFFFFFFFULL ;
}
2023-03-16 10:44:38 +00:00
bool foundAlias = false ;
2023-03-09 09:51:15 +00:00
bool ignored = false ;
2023-03-16 10:44:38 +00:00
std : : string hashfiles = LookupHashFile ( cachekey , hash , & foundAlias , & ignored ) ;
2023-03-08 23:01:38 +00:00
2023-03-10 16:43:12 +00:00
// Early-out for ignored textures, let's not bother even starting a thread task.
2023-03-10 13:58:44 +00:00
if ( ignored ) {
2023-03-10 13:16:14 +00:00
// WARN_LOG(G3D, "Not found/ignored: %s (%d, %d)", hashfiles.c_str(), (int)foundReplacement, (int)ignored);
2023-03-16 10:44:38 +00:00
// Insert an entry into the cache for faster lookup next time.
ReplacedTextureRef ref { } ;
cache_ . emplace ( std : : make_pair ( replacementKey , ref ) ) ;
return nullptr ;
2023-03-08 23:01:38 +00:00
}
2023-03-18 12:03:05 +00:00
desc . forceFiltering = ( TextureFiltering ) 0 ; // invalid value
FindFiltering ( cachekey , hash , & desc . forceFiltering ) ;
2023-03-16 10:44:38 +00:00
if ( ! foundAlias ) {
2023-03-10 13:58:44 +00:00
// We'll just need to generate the names for each level.
// By default, we look for png since that's also what's dumped.
// For other file formats, use the ini to create aliases.
2023-03-16 10:44:38 +00:00
desc . filenames . resize ( MAX_REPLACEMENT_MIP_LEVELS ) ;
for ( int level = 0 ; level < desc . filenames . size ( ) ; level + + ) {
desc . filenames [ level ] = TextureReplacer : : HashName ( cachekey , hash , level ) + " .png " ;
2023-03-10 13:58:44 +00:00
}
2023-03-16 10:44:38 +00:00
desc . logId = desc . filenames [ 0 ] ;
desc . hashfiles = desc . filenames [ 0 ] ; // The generated filename of the top level is used as the key in the data cache.
2023-03-10 13:58:44 +00:00
} else {
2023-03-16 10:44:38 +00:00
desc . logId = hashfiles ;
SplitString ( hashfiles , ' | ' , desc . filenames ) ;
desc . hashfiles = hashfiles ;
2023-03-10 13:58:44 +00:00
}
2023-03-08 23:01:38 +00:00
2023-03-16 10:44:38 +00:00
// OK, we might already have a matching texture, we use hashfiles as a key. Look it up in the level cache.
auto iter = levelCache_ . find ( hashfiles ) ;
if ( iter ! = levelCache_ . end ( ) ) {
// Insert an entry into the cache for faster lookup next time.
ReplacedTextureRef ref ;
ref . hashfiles = hashfiles ;
ref . texture = iter - > second ;
cache_ . emplace ( std : : make_pair ( replacementKey , ref ) ) ;
return iter - > second ;
}
2023-03-10 16:43:12 +00:00
2023-03-16 10:44:38 +00:00
// Final path - we actually need a new replacement texture, because we haven't seen "hashfiles" before.
desc . basePath = basePath_ ;
desc . formatSupport = formatSupport_ ;
ReplacedTexture * texture = new ReplacedTexture ( vfs_ , desc ) ;
ReplacedTextureRef ref ;
ref . hashfiles = hashfiles ;
ref . texture = texture ;
cache_ . emplace ( std : : make_pair ( replacementKey , ref ) ) ;
// Also, insert the level in the level cache so we can look up by desc_->hashfiles again.
levelCache_ . emplace ( std : : make_pair ( hashfiles , texture ) ) ;
return texture ;
2023-03-10 16:43:12 +00:00
}
2021-05-05 23:31:38 +00:00
static bool WriteTextureToPNG ( png_imagep image , const Path & filename , int convert_to_8bit , const void * buffer , png_int_32 row_stride , const void * colormap ) {
2016-04-30 22:03:39 +00:00
FILE * fp = File : : OpenCFile ( filename , " wb " ) ;
if ( ! fp ) {
2023-04-21 22:05:07 +00:00
ERROR_LOG ( IO , " Unable to open texture file '%s' for writing. " , filename . c_str ( ) ) ;
2016-04-30 22:03:39 +00:00
return false ;
}
if ( png_image_write_to_stdio ( image , fp , convert_to_8bit , buffer , row_stride , colormap ) ) {
2020-08-15 18:02:07 +00:00
fclose ( fp ) ;
2016-04-30 22:03:39 +00:00
return true ;
} else {
2017-03-06 12:10:23 +00:00
ERROR_LOG ( SYSTEM , " Texture PNG encode failed. " ) ;
2016-04-30 22:03:39 +00:00
fclose ( fp ) ;
remove ( filename . c_str ( ) ) ;
return false ;
}
}
2023-03-09 09:51:15 +00:00
// We save textures on threadpool tasks since it's a fire-and-forget task, and both I/O and png compression
// can be pretty slow.
class SaveTextureTask : public Task {
2022-04-17 22:25:59 +00:00
public :
2023-03-08 23:01:38 +00:00
std : : vector < u8 > rgbaData ;
2022-04-17 22:25:59 +00:00
int w = 0 ;
int h = 0 ;
int pitch = 0 ; // bytes
2023-03-10 14:39:45 +00:00
Path filename ;
Path saveFilename ;
2023-03-09 09:51:15 +00:00
u32 replacedInfoHash = 0 ;
2022-04-17 22:25:59 +00:00
2023-03-09 09:51:15 +00:00
SaveTextureTask ( std : : vector < u8 > & & _rgbaData ) : rgbaData ( std : : move ( _rgbaData ) ) { }
2022-04-17 22:25:59 +00:00
2023-03-09 09:51:15 +00:00
// This must be set to I/O blocking because of Android storage (so we attach the thread to JNI), while being CPU heavy too.
2023-01-15 16:15:59 +00:00
TaskType Type ( ) const override { return TaskType : : IO_BLOCKING ; }
2023-01-15 15:55:07 +00:00
TaskPriority Priority ( ) const override {
return TaskPriority : : LOW ;
}
2022-04-17 22:25:59 +00:00
void Run ( ) override {
2022-04-18 03:25:41 +00:00
// Should we skip writing if the newly saved data already exists?
2023-03-27 20:09:10 +00:00
if ( File : : Exists ( saveFilename ) ) {
2022-04-18 03:25:41 +00:00
return ;
2023-03-09 09:51:15 +00:00
}
2022-04-18 03:25:41 +00:00
2023-03-27 20:09:10 +00:00
// And we always skip if the replace file already exists.
2023-03-27 13:43:18 +00:00
if ( File : : Exists ( filename ) ) {
2022-04-18 03:25:41 +00:00
return ;
2023-03-27 13:43:18 +00:00
}
2023-03-28 13:44:35 +00:00
Path saveDirectory = saveFilename . NavigateUp ( ) ;
2023-03-28 09:18:45 +00:00
if ( ! File : : Exists ( saveDirectory ) ) {
// Previously, we created a .nomedia file here. This is unnecessary as they have recursive behavior.
// When initializing (see NotifyConfigChange above) we create one in the "root" of the "new" folder.
2023-03-10 14:39:45 +00:00
File : : CreateFullPath ( saveDirectory ) ;
2022-04-18 03:25:41 +00:00
}
2023-03-27 20:09:10 +00:00
// Now that we've passed the checks, we change the file extension of the path we're actually
// going to write to to .png.
saveFilename = saveFilename . WithReplacedExtension ( " .png " ) ;
2022-04-23 20:52:28 +00:00
png_image png { } ;
2022-04-17 22:25:59 +00:00
png . version = PNG_IMAGE_VERSION ;
png . format = PNG_FORMAT_RGBA ;
png . width = w ;
png . height = h ;
2023-03-08 23:01:38 +00:00
bool success = WriteTextureToPNG ( & png , saveFilename , 0 , rgbaData . data ( ) , pitch , nullptr ) ;
2022-04-17 22:25:59 +00:00
png_image_free ( & png ) ;
if ( png . warning_or_error > = 2 ) {
2023-03-09 09:51:15 +00:00
ERROR_LOG ( G3D , " Saving screenshot to PNG produced errors. " ) ;
2022-04-17 22:25:59 +00:00
} else if ( success ) {
2022-09-01 12:31:52 +00:00
NOTICE_LOG ( G3D , " Saving texture for replacement: %08x / %dx%d in '%s' " , replacedInfoHash , w , h , saveFilename . ToVisualString ( ) . c_str ( ) ) ;
2023-03-09 09:51:15 +00:00
} else {
ERROR_LOG ( G3D , " Failed to write '%s' " , saveFilename . c_str ( ) ) ;
2022-04-17 22:25:59 +00:00
}
}
} ;
2022-04-18 03:25:41 +00:00
bool TextureReplacer : : WillSave ( const ReplacedTextureDecodeInfo & replacedInfo ) {
_assert_msg_ ( enabled_ , " Replacement not enabled " ) ;
if ( ! g_Config . bSaveNewTextures )
return false ;
// Don't save the PPGe texture.
if ( replacedInfo . addr > 0x05000000 & & replacedInfo . addr < PSP_GetKernelMemoryEnd ( ) )
return false ;
if ( replacedInfo . isVideo & & ! allowVideo_ )
return false ;
return true ;
}
2022-04-17 22:25:59 +00:00
2023-03-27 13:43:18 +00:00
void TextureReplacer : : NotifyTextureDecoded ( ReplacedTexture * texture , const ReplacedTextureDecodeInfo & replacedInfo , const void * data , int pitch , int level , int origW , int origH , int scaledW , int scaledH ) {
2020-07-19 15:47:02 +00:00
_assert_msg_ ( enabled_ , " Replacement not enabled " ) ;
2023-03-27 13:43:18 +00:00
2022-04-18 03:25:41 +00:00
if ( ! WillSave ( replacedInfo ) ) {
2016-04-30 21:05:03 +00:00
// Ignore.
return ;
}
2023-03-27 20:09:10 +00:00
2022-04-18 03:25:41 +00:00
if ( ignoreMipmap_ & & level > 0 ) {
2023-03-27 20:09:10 +00:00
// Not saving higher mips.
2016-05-01 15:58:14 +00:00
return ;
}
2022-04-18 03:25:41 +00:00
2017-05-05 19:40:40 +00:00
u64 cachekey = replacedInfo . cachekey ;
if ( ignoreAddress_ ) {
cachekey = cachekey & 0xFFFFFFFFULL ;
}
2016-04-30 21:05:03 +00:00
2023-03-27 20:09:10 +00:00
bool foundAlias = false ;
bool ignored = false ;
2023-03-27 13:43:18 +00:00
std : : string replacedLevelNames = LookupHashFile ( cachekey , replacedInfo . hash , & foundAlias , & ignored ) ;
if ( ignored ) {
2023-03-27 20:09:10 +00:00
// The ini file entry was set to empty string. We can early-out.
2016-04-30 22:03:39 +00:00
return ;
}
2023-03-27 20:09:10 +00:00
// Alright, get the specified filename for the level.
std : : string hashfile ;
if ( ! replacedLevelNames . empty ( ) ) {
// If the user has specified a name before, we get it here.
std : : vector < std : : string > names ;
SplitString ( replacedLevelNames , ' | ' , names ) ;
hashfile = names [ std : : min ( level , ( int ) ( names . size ( ) - 1 ) ) ] ;
} else {
// Generate a new PNG filename, complete with level.
hashfile = HashName ( cachekey , replacedInfo . hash , level ) + " .png " ;
}
2023-03-09 09:51:15 +00:00
2017-05-05 19:40:40 +00:00
ReplacementCacheKey replacementKey ( cachekey , replacedInfo . hash ) ;
2016-05-01 03:21:08 +00:00
auto it = savedCache_ . find ( replacementKey ) ;
2022-04-18 03:30:20 +00:00
double now = time_now_d ( ) ;
2022-04-18 03:25:41 +00:00
if ( it ! = savedCache_ . end ( ) ) {
2023-03-16 09:21:57 +00:00
// We've already saved this texture. Ignore it.
// We don't really care about changing the scale factor during runtime, only confusing.
return ;
2016-05-31 05:49:41 +00:00
}
2023-03-16 09:21:57 +00:00
// Width/height of the image to save.
int w = scaledW ;
int h = scaledH ;
2016-04-30 23:33:09 +00:00
// Only save the hashed portion of the PNG.
2023-03-16 08:55:49 +00:00
int lookupW ;
int lookupH ;
2023-03-16 09:21:57 +00:00
if ( LookupHashRange ( replacedInfo . addr , origW , origH , & lookupW , & lookupH ) ) {
w = lookupW * ( scaledW / origW ) ;
h = lookupH * ( scaledH / origH ) ;
2016-05-01 03:33:14 +00:00
}
2016-04-30 23:33:09 +00:00
2023-03-08 23:01:38 +00:00
std : : vector < u8 > saveBuf ;
2022-04-17 22:25:59 +00:00
2022-07-29 16:27:52 +00:00
// Copy data to a buffer so we can send it to the thread. Might as well compact-away the pitch
// while we're at it.
2023-03-08 23:01:38 +00:00
saveBuf . resize ( w * h * 4 ) ;
2022-07-29 16:27:52 +00:00
for ( int y = 0 ; y < h ; y + + ) {
memcpy ( ( u8 * ) saveBuf . data ( ) + y * w * 4 , ( const u8 * ) data + y * pitch , w * sizeof ( u32 ) ) ;
2016-04-30 22:03:39 +00:00
}
2022-07-29 16:27:52 +00:00
pitch = w * 4 ;
2016-04-30 22:03:39 +00:00
2023-03-09 09:51:15 +00:00
SaveTextureTask * task = new SaveTextureTask ( std : : move ( saveBuf ) ) ;
2023-03-10 14:39:45 +00:00
task - > filename = basePath_ / hashfile ;
task - > saveFilename = newTextureDir_ / hashfile ;
2022-04-17 22:25:59 +00:00
task - > w = w ;
task - > h = h ;
task - > pitch = pitch ;
task - > replacedInfoHash = replacedInfo . hash ;
g_threadManager . EnqueueTask ( task ) ; // We don't care about waiting for the task. It'll be fine.
2016-05-01 03:21:08 +00:00
// Remember that we've saved this for next time.
2022-04-17 22:25:59 +00:00
// Should be OK that the actual disk write may not be finished yet.
2023-03-08 23:24:30 +00:00
SavedTextureCacheData & saveData = savedCache_ [ replacementKey ] ;
saveData . levelW [ level ] = w ;
saveData . levelH [ level ] = h ;
saveData . levelSaved [ level ] = true ;
saveData . lastTimeSaved = now ;
2016-04-30 20:44:31 +00:00
}
2022-07-28 03:27:56 +00:00
void TextureReplacer : : Decimate ( ReplacerDecimateMode mode ) {
2021-10-17 16:16:54 +00:00
// Allow replacements to be cached for a long time, although they're large.
2022-07-28 03:27:56 +00:00
double age = 1800.0 ;
2022-10-30 15:15:37 +00:00
if ( mode = = ReplacerDecimateMode : : FORCE_PRESSURE ) {
2022-07-28 03:27:56 +00:00
age = 90.0 ;
2022-10-30 15:15:37 +00:00
} else if ( mode = = ReplacerDecimateMode : : ALL ) {
2022-07-28 03:27:56 +00:00
age = 0.0 ;
2022-10-30 15:15:37 +00:00
} else if ( lastTextureCacheSizeGB_ > 1.0 ) {
double pressure = std : : min ( MAX_CACHE_SIZE , lastTextureCacheSizeGB_ ) / MAX_CACHE_SIZE ;
// Get more aggressive the closer we are to the max.
age = 90.0 + ( 1.0 - pressure ) * 1710.0 ;
}
2022-07-28 03:27:56 +00:00
2021-10-17 16:16:54 +00:00
const double threshold = time_now_d ( ) - age ;
2022-10-30 17:44:33 +00:00
size_t totalSize = 0 ;
for ( auto & item : levelCache_ ) {
2023-03-16 10:44:38 +00:00
std : : lock_guard < std : : mutex > guard ( item . second - > lock_ ) ;
2023-03-16 10:53:39 +00:00
item . second - > PurgeIfNotUsedSinceTime ( threshold ) ;
2023-03-16 10:44:38 +00:00
totalSize + = item . second - > GetTotalDataSize ( ) ; // TODO: Make something better.
// don't actually delete the items here, just clean out the data.
2022-10-30 15:15:37 +00:00
}
double totalSizeGB = totalSize / ( 1024.0 * 1024.0 * 1024.0 ) ;
if ( totalSizeGB > = 1.0 ) {
WARN_LOG ( G3D , " Decimated replacements older than %fs, currently using %f GB of RAM " , age , totalSizeGB ) ;
2021-10-17 16:16:54 +00:00
}
2022-10-30 15:15:37 +00:00
lastTextureCacheSizeGB_ = totalSizeGB ;
2021-10-17 16:16:54 +00:00
}
2021-02-28 01:16:16 +00:00
template < typename Key , typename Value >
static typename std : : unordered_map < Key , Value > : : const_iterator LookupWildcard ( const std : : unordered_map < Key , Value > & map , Key & key , u64 cachekey , u32 hash , bool ignoreAddress ) {
auto alias = map . find ( key ) ;
if ( alias ! = map . end ( ) )
return alias ;
// Also check for a few more aliases with zeroed portions:
// Only clut hash (very dangerous in theory, in practice not more than missing "just" data hash)
key . cachekey = cachekey & 0xFFFFFFFFULL ;
key . hash = 0 ;
alias = map . find ( key ) ;
if ( alias ! = map . end ( ) )
return alias ;
if ( ! ignoreAddress ) {
// No data hash.
key . cachekey = cachekey ;
2016-05-31 05:31:02 +00:00
key . hash = 0 ;
2021-02-28 01:16:16 +00:00
alias = map . find ( key ) ;
if ( alias ! = map . end ( ) )
return alias ;
}
// No address.
key . cachekey = cachekey & 0xFFFFFFFFULL ;
key . hash = hash ;
alias = map . find ( key ) ;
if ( alias ! = map . end ( ) )
return alias ;
if ( ! ignoreAddress ) {
// Address, but not clut hash (in case of garbage clut data.)
key . cachekey = cachekey & ~ 0xFFFFFFFFULL ;
key . hash = hash ;
alias = map . find ( key ) ;
if ( alias ! = map . end ( ) )
return alias ;
}
// Anything with this data hash (a little dangerous.)
key . cachekey = 0 ;
key . hash = hash ;
return map . find ( key ) ;
}
2016-05-31 05:31:02 +00:00
2021-02-28 01:16:16 +00:00
bool TextureReplacer : : FindFiltering ( u64 cachekey , u32 hash , TextureFiltering * forceFiltering ) {
if ( ! Enabled ( ) | | ! g_Config . bReplaceTextures ) {
return false ;
}
2016-05-31 05:31:02 +00:00
2021-02-28 01:16:16 +00:00
ReplacementCacheKey replacementKey ( cachekey , hash ) ;
auto filter = LookupWildcard ( filtering_ , replacementKey , cachekey , hash , ignoreAddress_ ) ;
if ( filter = = filtering_ . end ( ) ) {
// Allow a global wildcard.
replacementKey . cachekey = 0 ;
replacementKey . hash = 0 ;
filter = filtering_ . find ( replacementKey ) ;
}
if ( filter ! = filtering_ . end ( ) ) {
* forceFiltering = filter - > second ;
return true ;
2016-04-30 23:21:16 +00:00
}
2021-02-28 01:16:16 +00:00
return false ;
}
2016-04-30 23:21:16 +00:00
2023-03-10 14:39:45 +00:00
std : : string TextureReplacer : : LookupHashFile ( u64 cachekey , u32 hash , bool * foundAlias , bool * ignored ) {
2023-03-08 23:01:38 +00:00
ReplacementCacheKey key ( cachekey , hash ) ;
2021-02-28 01:16:16 +00:00
auto alias = LookupWildcard ( aliases_ , key , cachekey , hash , ignoreAddress_ ) ;
2016-05-31 05:31:02 +00:00
if ( alias ! = aliases_ . end ( ) ) {
2016-05-01 03:55:30 +00:00
// Note: this will be blank if explicitly ignored.
2023-03-10 14:39:45 +00:00
* foundAlias = true ;
2023-03-09 09:51:15 +00:00
* ignored = alias - > second . empty ( ) ;
2016-05-31 05:31:02 +00:00
return alias - > second ;
2016-05-01 03:55:30 +00:00
}
2023-03-10 14:39:45 +00:00
* foundAlias = false ;
2023-03-09 09:51:15 +00:00
* ignored = false ;
2023-03-08 23:01:38 +00:00
return " " ;
2016-04-30 22:41:12 +00:00
}
2021-04-03 15:56:28 +00:00
std : : string TextureReplacer : : HashName ( u64 cachekey , u32 hash , int level ) {
2016-04-30 22:41:12 +00:00
char hashname [ 16 + 8 + 1 + 11 + 1 ] = { } ;
if ( level > 0 ) {
2016-05-01 01:39:30 +00:00
snprintf ( hashname , sizeof ( hashname ) , " %016llx%08x_%d " , cachekey , hash , level ) ;
2016-04-30 22:41:12 +00:00
} else {
2016-05-01 01:39:30 +00:00
snprintf ( hashname , sizeof ( hashname ) , " %016llx%08x " , cachekey , hash ) ;
2016-04-30 22:41:12 +00:00
}
return hashname ;
}
2023-03-16 08:55:49 +00:00
bool TextureReplacer : : LookupHashRange ( u32 addr , int w , int h , int * newW , int * newH ) {
2017-11-25 20:34:52 +00:00
const u64 rangeKey = ( ( u64 ) addr < < 32 ) | ( ( u64 ) w < < 16 ) | h ;
2016-04-30 23:21:16 +00:00
auto range = hashranges_ . find ( rangeKey ) ;
if ( range ! = hashranges_ . end ( ) ) {
const WidthHeightPair & wh = range - > second ;
2023-03-16 08:55:49 +00:00
* newW = wh . first ;
* newH = wh . second ;
2016-04-30 23:21:16 +00:00
return true ;
2023-03-16 08:55:49 +00:00
} else {
* newW = w ;
* newH = h ;
return false ;
2016-04-30 23:21:16 +00:00
}
2016-04-30 21:19:23 +00:00
}
2023-03-18 23:00:59 +00:00
float TextureReplacer : : LookupReduceHashRange ( int w , int h ) {
2021-04-01 09:18:06 +00:00
const u64 reducerangeKey = ( ( u64 ) w < < 16 ) | h ;
auto range = reducehashranges_ . find ( reducerangeKey ) ;
if ( range ! = reducehashranges_ . end ( ) ) {
float rhv = range - > second ;
return rhv ;
}
else {
return reduceHashGlobalValue ;
}
}
2022-07-10 20:34:44 +00:00
bool TextureReplacer : : IniExists ( const std : : string & gameID ) {
if ( gameID . empty ( ) )
return false ;
Path texturesDirectory = GetSysDirectory ( DIRECTORY_TEXTURES ) / gameID ;
Path generatedFilename = texturesDirectory / INI_FILENAME ;
return File : : Exists ( generatedFilename ) ;
}
2021-05-11 07:50:28 +00:00
bool TextureReplacer : : GenerateIni ( const std : : string & gameID , Path & generatedFilename ) {
2018-10-01 00:00:05 +00:00
if ( gameID . empty ( ) )
return false ;
2021-05-15 05:48:04 +00:00
Path texturesDirectory = GetSysDirectory ( DIRECTORY_TEXTURES ) / gameID ;
2018-10-01 00:00:05 +00:00
if ( ! File : : Exists ( texturesDirectory ) ) {
File : : CreateFullPath ( texturesDirectory ) ;
}
2021-05-11 07:50:28 +00:00
generatedFilename = texturesDirectory / INI_FILENAME ;
if ( File : : Exists ( generatedFilename ) )
2018-10-01 00:00:05 +00:00
return true ;
2021-05-11 07:50:28 +00:00
FILE * f = File : : OpenCFile ( generatedFilename , " wb " ) ;
2018-10-01 00:00:05 +00:00
if ( f ) {
2023-03-08 09:57:26 +00:00
// Unicode byte order mark
2021-05-09 22:48:36 +00:00
fwrite ( " \xEF \xBB \xBF " , 1 , 3 , f ) ;
2018-10-01 00:00:05 +00:00
// Let's also write some defaults.
2023-03-18 14:07:45 +00:00
fprintf ( f , R " (# This describes your textures and set up options for texture replacement.
2023-03-17 11:47:12 +00:00
# Documentation about the options and syntax is available here:
# https: //www.ppsspp.org/docs/reference/texture-replacement
2023-03-18 14:07:45 +00:00
2023-03-08 09:57:26 +00:00
[ options ]
version = 1
2023-03-18 23:00:59 +00:00
hash = quick # options available : " quick " , xxh32 - more accurate , but much slower , xxh64 - more accurate and quite fast , but slower than xxh32 on 32 bit cpu ' s
ignoreMipmap = true # Usually , can just generate them with basisu , no need to dump .
reduceHash = false # Unsafe and can cause glitches in some cases , but allows to skip garbage data in some textures reducing endless duplicates as a side effect speeds up hashing as well , requires stronger hash like xxh32 or xxh64
ignoreAddress = false # Reduces duplicates at the cost of making hash less reliable , requires stronger hash like xxh32 or xxh64 . Basically automatically sets the address to 0 in the dumped filenames .
2023-03-08 09:57:26 +00:00
[ games ]
# Used to make it easier to install, and override settings for other regions.
# Files still have to be copied to each TEXTURES folder.
% s = % s
[ hashes ]
# Use / for folders not \\, avoid special characters, and stick to lowercase.
# See wiki for more info.
[ hashranges ]
2023-03-18 23:00:59 +00:00
# This is useful for images that very clearly have smaller dimensions, like 480x272 image. They'll need to be redumped, since the hash will change. See the documentation.
2023-03-17 11:47:12 +00:00
# Example: 08b31020,512,512 = 480,272
2023-03-18 23:00:59 +00:00
# Example: 0x08b31020,512,512 = 480,272
2023-03-08 09:57:26 +00:00
[ filtering ]
2023-03-18 23:00:59 +00:00
# You can enforce specific filtering modes with this. Available modes are linear, nearest, auto. See the docs.
# Example: 08d3961000000909ba70b2af = nearest
2023-03-08 09:57:26 +00:00
[ reducehashranges ]
2023-03-18 23:00:59 +00:00
# Lets you set texture sizes where the hash range is reduced by a factor. See the docs.
# Example:
512 , 512 = 0.5
2023-03-08 09:57:26 +00:00
) " , gameID.c_str(), INI_FILENAME.c_str());
2021-05-09 22:48:36 +00:00
fclose ( f ) ;
2018-10-01 00:00:05 +00:00
}
2021-05-11 07:50:28 +00:00
return File : : Exists ( generatedFilename ) ;
2018-10-01 00:00:05 +00:00
}