1 // Copyright (c) 2016- PPSSPP Project.
2 
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, version 2.0 or later versions.
6 
7 // This program is distributed in the hope that it will be useful,
8 // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10 // GNU General Public License 2.0 for more details.
11 
12 // A copy of the GPL 2.0 should have been included with the program.
13 // If not, see http://www.gnu.org/licenses/
14 
15 // Official git repository and contact information can be found at
16 // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17 
18 #include "ppsspp_config.h"
19 #include <algorithm>
20 #include <cstring>
21 #include <memory>
22 #include <png.h>
23 
24 #include "ext/xxhash.h"
25 
26 #include "Common/Data/Convert/ColorConv.h"
27 #include "Common/Data/Format/IniFile.h"
28 #include "Common/Data/Format/ZIMLoad.h"
29 #include "Common/Data/Text/I18n.h"
30 #include "Common/Data/Text/Parsers.h"
31 #include "Common/File/FileUtil.h"
32 #include "Common/StringUtils.h"
33 #include "Common/Thread/ParallelLoop.h"
34 #include "Core/Config.h"
35 #include "Core/Host.h"
36 #include "Core/System.h"
37 #include "Core/TextureReplacer.h"
38 #include "Core/ThreadPools.h"
39 #include "Core/ELF/ParamSFO.h"
40 #include "GPU/Common/TextureDecoder.h"
41 
42 static const std::string INI_FILENAME = "textures.ini";
43 static const std::string NEW_TEXTURE_DIR = "new/";
44 static const int VERSION = 1;
45 static const int MAX_MIP_LEVELS = 12;  // 12 should be plenty, 8 is the max mip levels supported by the PSP.
46 
TextureReplacer()47 TextureReplacer::TextureReplacer() {
48 	none_.alphaStatus_ = ReplacedTextureAlpha::UNKNOWN;
49 }
50 
~TextureReplacer()51 TextureReplacer::~TextureReplacer() {
52 }
53 
Init()54 void TextureReplacer::Init() {
55 	NotifyConfigChanged();
56 }
57 
NotifyConfigChanged()58 void TextureReplacer::NotifyConfigChanged() {
59 	gameID_ = g_paramSFO.GetDiscID();
60 
61 	enabled_ = g_Config.bReplaceTextures || g_Config.bSaveNewTextures;
62 	if (enabled_) {
63 		basePath_ = GetSysDirectory(DIRECTORY_TEXTURES) / gameID_;
64 
65 		Path newTextureDir = basePath_ / NEW_TEXTURE_DIR;
66 
67 		// If we're saving, auto-create the directory.
68 		if (g_Config.bSaveNewTextures && !File::Exists(newTextureDir)) {
69 			File::CreateFullPath(newTextureDir);
70 			File::CreateEmptyFile(newTextureDir / ".nomedia");
71 		}
72 
73 		enabled_ = File::Exists(basePath_) && File::IsDirectory(basePath_);
74 	}
75 
76 	if (enabled_) {
77 		enabled_ = LoadIni();
78 	}
79 }
80 
LoadIni()81 bool TextureReplacer::LoadIni() {
82 	// TODO: Use crc32c?
83 	hash_ = ReplacedTextureHash::QUICK;
84 	aliases_.clear();
85 	hashranges_.clear();
86 	filtering_.clear();
87 	reducehashranges_.clear();
88 
89 	allowVideo_ = false;
90 	ignoreAddress_ = false;
91 	reduceHash_ = false;
92 	reduceHashGlobalValue = 0.5;
93 	// Prevents dumping the mipmaps.
94 	ignoreMipmap_ = false;
95 
96 	if (File::Exists(basePath_ / INI_FILENAME)) {
97 		IniFile ini;
98 		ini.LoadFromVFS((basePath_ / INI_FILENAME).ToString());
99 
100 		if (!LoadIniValues(ini)) {
101 			return false;
102 		}
103 
104 		// Allow overriding settings per game id.
105 		std::string overrideFilename;
106 		if (ini.GetOrCreateSection("games")->Get(gameID_.c_str(), &overrideFilename, "")) {
107 			if (!overrideFilename.empty() && overrideFilename != INI_FILENAME) {
108 				INFO_LOG(G3D, "Loading extra texture ini: %s", overrideFilename.c_str());
109 				IniFile overrideIni;
110 				overrideIni.LoadFromVFS((basePath_ / overrideFilename).ToString());
111 
112 				if (!LoadIniValues(overrideIni, true)) {
113 					return false;
114 				}
115 			}
116 		}
117 	}
118 
119 	// The ini doesn't have to exist for it to be valid.
120 	return true;
121 }
122 
LoadIniValues(IniFile & ini,bool isOverride)123 bool TextureReplacer::LoadIniValues(IniFile &ini, bool isOverride) {
124 	auto options = ini.GetOrCreateSection("options");
125 	std::string hash;
126 	options->Get("hash", &hash, "");
127 	// TODO: crc32c.
128 	if (strcasecmp(hash.c_str(), "quick") == 0) {
129 		hash_ = ReplacedTextureHash::QUICK;
130 	} else if (strcasecmp(hash.c_str(), "xxh32") == 0) {
131 		hash_ = ReplacedTextureHash::XXH32;
132 	} else if (strcasecmp(hash.c_str(), "xxh64") == 0) {
133 		hash_ = ReplacedTextureHash::XXH64;
134 	} else if (!isOverride || !hash.empty()) {
135 		ERROR_LOG(G3D, "Unsupported hash type: %s", hash.c_str());
136 		return false;
137 	}
138 
139 	options->Get("video", &allowVideo_, allowVideo_);
140 	options->Get("ignoreAddress", &ignoreAddress_, ignoreAddress_);
141 	// Multiplies sizeInRAM/bytesPerLine in XXHASH by 0.5.
142 	options->Get("reduceHash", &reduceHash_, reduceHash_);
143 	options->Get("ignoreMipmap", &ignoreMipmap_, ignoreMipmap_);
144 	if (reduceHash_ && hash_ == ReplacedTextureHash::QUICK) {
145 		reduceHash_ = false;
146 		ERROR_LOG(G3D, "Texture Replacement: reduceHash option requires safer hash, use xxh32 or xxh64 instead.");
147 	}
148 
149 	if (ignoreAddress_ && hash_ == ReplacedTextureHash::QUICK) {
150 		ignoreAddress_ = false;
151 		ERROR_LOG(G3D, "Texture Replacement: ignoreAddress option requires safer hash, use xxh32 or xxh64 instead.");
152 	}
153 
154 	int version = 0;
155 	if (options->Get("version", &version, 0) && version > VERSION) {
156 		ERROR_LOG(G3D, "Unsupported texture replacement version %d, trying anyway", version);
157 	}
158 
159 	bool filenameWarning = false;
160 	if (ini.HasSection("hashes")) {
161 		auto hashes = ini.GetOrCreateSection("hashes")->ToMap();
162 		// Format: hashname = filename.png
163 		bool checkFilenames = g_Config.bSaveNewTextures && !g_Config.bIgnoreTextureFilenames;
164 		for (const auto &item : hashes) {
165 			ReplacementAliasKey key(0, 0, 0);
166 			if (sscanf(item.first.c_str(), "%16llx%8x_%d", &key.cachekey, &key.hash, &key.level) >= 1) {
167 				aliases_[key] = item.second;
168 				if (checkFilenames) {
169 #if PPSSPP_PLATFORM(WINDOWS)
170 					// Uppercase probably means the filenames don't match.
171 					// Avoiding an actual check of the filenames to avoid performance impact.
172 					filenameWarning = filenameWarning || item.second.find_first_of("\\ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos;
173 #else
174 					filenameWarning = filenameWarning || item.second.find_first_of("\\:<>|?*") != std::string::npos;
175 #endif
176 				}
177 			} else {
178 				ERROR_LOG(G3D, "Unsupported syntax under [hashes]: %s", item.first.c_str());
179 			}
180 		}
181 	}
182 
183 	if (filenameWarning) {
184 		auto err = GetI18NCategory("Error");
185 		host->NotifyUserMessage(err->T("textures.ini filenames may not be cross-platform"), 6.0f);
186 	}
187 
188 	if (ini.HasSection("hashranges")) {
189 		auto hashranges = ini.GetOrCreateSection("hashranges")->ToMap();
190 		// Format: addr,w,h = newW,newH
191 		for (const auto &item : hashranges) {
192 			ParseHashRange(item.first, item.second);
193 		}
194 	}
195 
196 	if (ini.HasSection("filtering")) {
197 		auto filters = ini.GetOrCreateSection("filtering")->ToMap();
198 		// Format: hashname = nearest or linear
199 		for (const auto &item : filters) {
200 			ParseFiltering(item.first, item.second);
201 		}
202 	}
203 
204 	if (ini.HasSection("reducehashranges")) {
205 		auto reducehashranges = ini.GetOrCreateSection("reducehashranges")->ToMap();
206 		// Format: w,h = reducehashvalues
207 		for (const auto& item : reducehashranges) {
208 			ParseReduceHashRange(item.first, item.second);
209 		}
210 	}
211 
212 	return true;
213 }
214 
ParseHashRange(const std::string & key,const std::string & value)215 void TextureReplacer::ParseHashRange(const std::string &key, const std::string &value) {
216 	std::vector<std::string> keyParts;
217 	SplitString(key, ',', keyParts);
218 	std::vector<std::string> valueParts;
219 	SplitString(value, ',', valueParts);
220 
221 	if (keyParts.size() != 3 || valueParts.size() != 2) {
222 		ERROR_LOG(G3D, "Ignoring invalid hashrange %s = %s, expecting addr,w,h = w,h", key.c_str(), value.c_str());
223 		return;
224 	}
225 
226 	u32 addr;
227 	u32 fromW;
228 	u32 fromH;
229 	if (!TryParse(keyParts[0], &addr) || !TryParse(keyParts[1], &fromW) || !TryParse(keyParts[2], &fromH)) {
230 		ERROR_LOG(G3D, "Ignoring invalid hashrange %s = %s, key format is 0x12345678,512,512", key.c_str(), value.c_str());
231 		return;
232 	}
233 
234 	u32 toW;
235 	u32 toH;
236 	if (!TryParse(valueParts[0], &toW) || !TryParse(valueParts[1], &toH)) {
237 		ERROR_LOG(G3D, "Ignoring invalid hashrange %s = %s, value format is 512,512", key.c_str(), value.c_str());
238 		return;
239 	}
240 
241 	if (toW > fromW || toH > fromH) {
242 		ERROR_LOG(G3D, "Ignoring invalid hashrange %s = %s, range bigger than source", key.c_str(), value.c_str());
243 		return;
244 	}
245 
246 	const u64 rangeKey = ((u64)addr << 32) | ((u64)fromW << 16) | fromH;
247 	hashranges_[rangeKey] = WidthHeightPair(toW, toH);
248 }
249 
ParseFiltering(const std::string & key,const std::string & value)250 void TextureReplacer::ParseFiltering(const std::string &key, const std::string &value) {
251 	ReplacementCacheKey itemKey(0, 0);
252 	if (sscanf(key.c_str(), "%16llx%8x", &itemKey.cachekey, &itemKey.hash) >= 1) {
253 		if (!strcasecmp(value.c_str(), "nearest")) {
254 			filtering_[itemKey] = TEX_FILTER_FORCE_NEAREST;
255 		} else if (!strcasecmp(value.c_str(), "linear")) {
256 			filtering_[itemKey] = TEX_FILTER_FORCE_LINEAR;
257 		} else if (!strcasecmp(value.c_str(), "auto")) {
258 			filtering_[itemKey] = TEX_FILTER_AUTO;
259 		} else {
260 			ERROR_LOG(G3D, "Unsupported syntax under [filtering]: %s", value.c_str());
261 		}
262 	} else {
263 		ERROR_LOG(G3D, "Unsupported syntax under [filtering]: %s", key.c_str());
264 	}
265 }
266 
ParseReduceHashRange(const std::string & key,const std::string & value)267 void TextureReplacer::ParseReduceHashRange(const std::string& key, const std::string& value) {
268 	std::vector<std::string> keyParts;
269 	SplitString(key, ',', keyParts);
270 	std::vector<std::string> valueParts;
271 	SplitString(value, ',', valueParts);
272 
273 	if (keyParts.size() != 2 || valueParts.size() != 1) {
274 		ERROR_LOG(G3D, "Ignoring invalid reducehashrange %s = %s, expecting w,h = reducehashvalue", key.c_str(), value.c_str());
275 		return;
276 	}
277 
278 	u32 forW;
279 	u32 forH;
280 	if (!TryParse(keyParts[0], &forW) || !TryParse(keyParts[1], &forH)) {
281 		ERROR_LOG(G3D, "Ignoring invalid reducehashrange %s = %s, key format is 512,512", key.c_str(), value.c_str());
282 		return;
283 	}
284 
285 	float rhashvalue;
286 	if (!TryParse(valueParts[0], &rhashvalue)) {
287 		ERROR_LOG(G3D, "Ignoring invalid reducehashrange %s = %s, value format is 0.5", key.c_str(), value.c_str());
288 		return;
289 	}
290 
291 	if (rhashvalue == 0) {
292 		ERROR_LOG(G3D, "Ignoring invalid hashrange %s = %s, reducehashvalue can't be 0", key.c_str(), value.c_str());
293 		return;
294 	}
295 
296 	const u64 reducerangeKey = ((u64)forW << 16) | forH;
297 	reducehashranges_[reducerangeKey] = rhashvalue;
298 }
299 
ComputeHash(u32 addr,int bufw,int w,int h,GETextureFormat fmt,u16 maxSeenV)300 u32 TextureReplacer::ComputeHash(u32 addr, int bufw, int w, int h, GETextureFormat fmt, u16 maxSeenV) {
301 	_dbg_assert_msg_(enabled_, "Replacement not enabled");
302 
303 	if (!LookupHashRange(addr, w, h)) {
304 		// There wasn't any hash range, let's fall back to maxSeenV logic.
305 		if (h == 512 && maxSeenV < 512 && maxSeenV != 0) {
306 			h = (int)maxSeenV;
307 		}
308 	}
309 
310 	const u8 *checkp = Memory::GetPointer(addr);
311 	if (reduceHash_) {
312 		reduceHashSize = LookupReduceHashRange(w, h);
313 		// default to reduceHashGlobalValue which default is 0.5
314 	}
315 	if (bufw <= w) {
316 		// We can assume the data is contiguous.  These are the total used pixels.
317 		const u32 totalPixels = bufw * h + (w - bufw);
318 		const u32 sizeInRAM = (textureBitsPerPixel[fmt] * totalPixels) / 8 * reduceHashSize;
319 
320 		switch (hash_) {
321 		case ReplacedTextureHash::QUICK:
322 			return StableQuickTexHash(checkp, sizeInRAM);
323 		case ReplacedTextureHash::XXH32:
324 			return XXH32(checkp, sizeInRAM, 0xBACD7814);
325 		case ReplacedTextureHash::XXH64:
326 			return XXH64(checkp, sizeInRAM, 0xBACD7814);
327 		default:
328 			return 0;
329 		}
330 	} else {
331 		// We have gaps.  Let's hash each row and sum.
332 		const u32 bytesPerLine = (textureBitsPerPixel[fmt] * w) / 8 * reduceHashSize;
333 		const u32 stride = (textureBitsPerPixel[fmt] * bufw) / 8;
334 
335 		u32 result = 0;
336 		switch (hash_) {
337 		case ReplacedTextureHash::QUICK:
338 			for (int y = 0; y < h; ++y) {
339 				u32 rowHash = StableQuickTexHash(checkp, bytesPerLine);
340 				result = (result * 11) ^ rowHash;
341 				checkp += stride;
342 			}
343 			break;
344 
345 		case ReplacedTextureHash::XXH32:
346 			for (int y = 0; y < h; ++y) {
347 				u32 rowHash = XXH32(checkp, bytesPerLine, 0xBACD7814);
348 				result = (result * 11) ^ rowHash;
349 				checkp += stride;
350 			}
351 			break;
352 
353 		case ReplacedTextureHash::XXH64:
354 			for (int y = 0; y < h; ++y) {
355 				u32 rowHash = XXH64(checkp, bytesPerLine, 0xBACD7814);
356 				result = (result * 11) ^ rowHash;
357 				checkp += stride;
358 			}
359 			break;
360 
361 		default:
362 			break;
363 		}
364 
365 		return result;
366 	}
367 }
368 
FindReplacement(u64 cachekey,u32 hash,int w,int h)369 ReplacedTexture &TextureReplacer::FindReplacement(u64 cachekey, u32 hash, int w, int h) {
370 	// Only actually replace if we're replacing.  We might just be saving.
371 	if (!Enabled() || !g_Config.bReplaceTextures) {
372 		return none_;
373 	}
374 
375 	ReplacementCacheKey replacementKey(cachekey, hash);
376 	auto it = cache_.find(replacementKey);
377 	if (it != cache_.end()) {
378 		return it->second;
379 	}
380 
381 	// Okay, let's construct the result.
382 	ReplacedTexture &result = cache_[replacementKey];
383 	result.alphaStatus_ = ReplacedTextureAlpha::UNKNOWN;
384 	PopulateReplacement(&result, cachekey, hash, w, h);
385 	return result;
386 }
387 
PopulateReplacement(ReplacedTexture * result,u64 cachekey,u32 hash,int w,int h)388 void TextureReplacer::PopulateReplacement(ReplacedTexture *result, u64 cachekey, u32 hash, int w, int h) {
389 	int newW = w;
390 	int newH = h;
391 	LookupHashRange(cachekey >> 32, newW, newH);
392 
393 	if (ignoreAddress_) {
394 		cachekey = cachekey & 0xFFFFFFFFULL;
395 	}
396 
397 	for (int i = 0; i < MAX_MIP_LEVELS; ++i) {
398 		const std::string hashfile = LookupHashFile(cachekey, hash, i);
399 		const Path filename = basePath_ / hashfile;
400 		if (hashfile.empty() || !File::Exists(filename)) {
401 			// Out of valid mip levels.  Bail out.
402 			break;
403 		}
404 
405 		ReplacedTextureLevel level;
406 		level.fmt = ReplacedTextureFormat::F_8888;
407 		level.file = filename;
408 		bool good = PopulateLevel(level);
409 
410 		// We pad files that have been hashrange'd so they are the same texture size.
411 		level.w = (level.w * w) / newW;
412 		level.h = (level.h * h) / newH;
413 
414 		if (good && i != 0) {
415 			// Check that the mipmap size is correct.  Can't load mips of the wrong size.
416 			if (level.w != (result->levels_[0].w >> i) || level.h != (result->levels_[0].h >> i)) {
417 				 WARN_LOG(G3D, "Replacement mipmap invalid: size=%dx%d, expected=%dx%d (level %d, '%s')", level.w, level.h, result->levels_[0].w >> i, result->levels_[0].h >> i, i, filename.c_str());
418 				 good = false;
419 			}
420 		}
421 
422 		if (good)
423 			result->levels_.push_back(level);
424 		// Otherwise, we're done loading mips (bad PNG or bad size, either way.)
425 		else
426 			break;
427 	}
428 
429 	result->alphaStatus_ = ReplacedTextureAlpha::UNKNOWN;
430 }
431 
432 enum class ReplacedImageType {
433 	PNG,
434 	ZIM,
435 	INVALID,
436 };
437 
Identify(FILE * fp)438 static ReplacedImageType Identify(FILE *fp) {
439 	uint8_t magic[4];
440 	if (fread(magic, 1, 4, fp) != 4)
441 		return ReplacedImageType::INVALID;
442 	rewind(fp);
443 
444 	if (strncmp((const char *)magic, "ZIMG", 4) == 0)
445 		return ReplacedImageType::ZIM;
446 	if (magic[0] == 0x89 && strncmp((const char *)&magic[1], "PNG", 3) == 0)
447 		return ReplacedImageType::PNG;
448 	return ReplacedImageType::INVALID;
449 }
450 
PopulateLevel(ReplacedTextureLevel & level)451 bool TextureReplacer::PopulateLevel(ReplacedTextureLevel &level) {
452 	bool good = false;
453 
454 	FILE *fp = File::OpenCFile(level.file, "rb");
455 	auto imageType = Identify(fp);
456 	if (imageType == ReplacedImageType::ZIM) {
457 		fseek(fp, 4, SEEK_SET);
458 		good = fread(&level.w, 4, 1, fp) == 1;
459 		good = good && fread(&level.h, 4, 1, fp) == 1;
460 		int flags;
461 		if (good && fread(&flags, 4, 1, fp) == 1) {
462 			good = (flags & ZIM_FORMAT_MASK) == ZIM_RGBA8888;
463 		}
464 	} else if (imageType == ReplacedImageType::PNG) {
465 		png_image png = {};
466 		png.version = PNG_IMAGE_VERSION;
467 		if (png_image_begin_read_from_stdio(&png, fp)) {
468 			// We pad files that have been hashrange'd so they are the same texture size.
469 			level.w = png.width;
470 			level.h = png.height;
471 			good = true;
472 		} else {
473 			ERROR_LOG(G3D, "Could not load texture replacement info: %s - %s", level.file.ToVisualString().c_str(), png.message);
474 		}
475 		png_image_free(&png);
476 	} else {
477 		ERROR_LOG(G3D, "Could not load texture replacement info: %s - unsupported format", level.file.ToVisualString().c_str());
478 	}
479 	fclose(fp);
480 
481 	return good;
482 }
483 
WriteTextureToPNG(png_imagep image,const Path & filename,int convert_to_8bit,const void * buffer,png_int_32 row_stride,const void * colormap)484 static bool WriteTextureToPNG(png_imagep image, const Path &filename, int convert_to_8bit, const void *buffer, png_int_32 row_stride, const void *colormap) {
485 	FILE *fp = File::OpenCFile(filename, "wb");
486 	if (!fp) {
487 		ERROR_LOG(IO, "Unable to open texture file for writing.");
488 		return false;
489 	}
490 
491 	if (png_image_write_to_stdio(image, fp, convert_to_8bit, buffer, row_stride, colormap)) {
492 		fclose(fp);
493 		return true;
494 	} else {
495 		ERROR_LOG(SYSTEM, "Texture PNG encode failed.");
496 		fclose(fp);
497 		remove(filename.c_str());
498 		return false;
499 	}
500 }
501 
NotifyTextureDecoded(const ReplacedTextureDecodeInfo & replacedInfo,const void * data,int pitch,int level,int w,int h)502 void TextureReplacer::NotifyTextureDecoded(const ReplacedTextureDecodeInfo &replacedInfo, const void *data, int pitch, int level, int w, int h) {
503 	_assert_msg_(enabled_, "Replacement not enabled");
504 	if (!g_Config.bSaveNewTextures) {
505 		// Ignore.
506 		return;
507 	}
508 	if (replacedInfo.addr > 0x05000000 && replacedInfo.addr < PSP_GetKernelMemoryEnd()) {
509 		// Don't save the PPGe texture.
510 		return;
511 	}
512 	if (replacedInfo.isVideo && !allowVideo_) {
513 		return;
514 	}
515 	u64 cachekey = replacedInfo.cachekey;
516 	if (ignoreAddress_) {
517 		cachekey = cachekey & 0xFFFFFFFFULL;
518 	}
519 	if (ignoreMipmap_ && level > 0) {
520 		return;
521 	}
522 
523 	std::string hashfile = LookupHashFile(cachekey, replacedInfo.hash, level);
524 	const Path filename = basePath_ / hashfile;
525 	const Path saveFilename = basePath_ / NEW_TEXTURE_DIR / hashfile;
526 
527 	// If it's empty, it's an ignored hash, we intentionally don't save.
528 	if (hashfile.empty() || File::Exists(filename)) {
529 		// If it exists, must've been decoded and saved as a new texture already.
530 		return;
531 	}
532 
533 	ReplacementCacheKey replacementKey(cachekey, replacedInfo.hash);
534 	auto it = savedCache_.find(replacementKey);
535 	if (it != savedCache_.end() && File::Exists(saveFilename)) {
536 		// We've already saved this texture.  Let's only save if it's bigger (e.g. scaled now.)
537 		if (it->second.w >= w && it->second.h >= h) {
538 			return;
539 		}
540 	}
541 
542 #ifdef _WIN32
543 	size_t slash = hashfile.find_last_of("/\\");
544 #else
545 	size_t slash = hashfile.find_last_of("/");
546 #endif
547 	if (slash != hashfile.npos) {
548 		// Create any directory structure as needed.
549 		const Path saveDirectory = basePath_ / NEW_TEXTURE_DIR / hashfile.substr(0, slash);
550 		if (!File::Exists(saveDirectory)) {
551 			File::CreateFullPath(saveDirectory);
552 			File::CreateEmptyFile(saveDirectory / ".nomedia");
553 		}
554 	}
555 
556 	// Only save the hashed portion of the PNG.
557 	int lookupW = w / replacedInfo.scaleFactor;
558 	int lookupH = h / replacedInfo.scaleFactor;
559 	if (LookupHashRange(replacedInfo.addr, lookupW, lookupH)) {
560 		w = lookupW * replacedInfo.scaleFactor;
561 		h = lookupH * replacedInfo.scaleFactor;
562 	}
563 
564 	if (replacedInfo.fmt != ReplacedTextureFormat::F_8888) {
565 		saveBuf.resize((pitch * h) / sizeof(u16));
566 		switch (replacedInfo.fmt) {
567 		case ReplacedTextureFormat::F_5650:
568 			ConvertRGB565ToRGBA8888(saveBuf.data(), (const u16 *)data, (pitch * h) / sizeof(u16));
569 			break;
570 		case ReplacedTextureFormat::F_5551:
571 			ConvertRGBA5551ToRGBA8888(saveBuf.data(), (const u16 *)data, (pitch * h) / sizeof(u16));
572 			break;
573 		case ReplacedTextureFormat::F_4444:
574 			ConvertRGBA4444ToRGBA8888(saveBuf.data(), (const u16 *)data, (pitch * h) / sizeof(u16));
575 			break;
576 		case ReplacedTextureFormat::F_0565_ABGR:
577 			ConvertBGR565ToRGBA8888(saveBuf.data(), (const u16 *)data, (pitch * h) / sizeof(u16));
578 			break;
579 		case ReplacedTextureFormat::F_1555_ABGR:
580 			ConvertABGR1555ToRGBA8888(saveBuf.data(), (const u16 *)data, (pitch * h) / sizeof(u16));
581 			break;
582 		case ReplacedTextureFormat::F_4444_ABGR:
583 			ConvertABGR4444ToRGBA8888(saveBuf.data(), (const u16 *)data, (pitch * h) / sizeof(u16));
584 			break;
585 		case ReplacedTextureFormat::F_8888_BGRA:
586 			ConvertBGRA8888ToRGBA8888(saveBuf.data(), (const u32 *)data, (pitch * h) / sizeof(u32));
587 			break;
588 		case ReplacedTextureFormat::F_8888:
589 			// Impossible.  Just so we can get warnings on other missed formats.
590 			break;
591 		}
592 
593 		data = saveBuf.data();
594 		if (replacedInfo.fmt != ReplacedTextureFormat::F_8888_BGRA) {
595 			// We doubled our pitch.
596 			pitch *= 2;
597 		}
598 	}
599 
600 	png_image png;
601 	memset(&png, 0, sizeof(png));
602 	png.version = PNG_IMAGE_VERSION;
603 	png.format = PNG_FORMAT_RGBA;
604 	png.width = w;
605 	png.height = h;
606 	bool success = WriteTextureToPNG(&png, saveFilename, 0, data, pitch, nullptr);
607 	png_image_free(&png);
608 
609 	if (png.warning_or_error >= 2) {
610 		ERROR_LOG(COMMON, "Saving screenshot to PNG produced errors.");
611 	} else if (success) {
612 		NOTICE_LOG(G3D, "Saving texture for replacement: %08x / %dx%d", replacedInfo.hash, w, h);
613 	}
614 
615 	// Remember that we've saved this for next time.
616 	ReplacedTextureLevel saved;
617 	saved.fmt = ReplacedTextureFormat::F_8888;
618 	saved.file = filename;
619 	saved.w = w;
620 	saved.h = h;
621 	savedCache_[replacementKey] = saved;
622 }
623 
624 template <typename Key, typename Value>
LookupWildcard(const std::unordered_map<Key,Value> & map,Key & key,u64 cachekey,u32 hash,bool ignoreAddress)625 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) {
626 	auto alias = map.find(key);
627 	if (alias != map.end())
628 		return alias;
629 
630 	// Also check for a few more aliases with zeroed portions:
631 	// Only clut hash (very dangerous in theory, in practice not more than missing "just" data hash)
632 	key.cachekey = cachekey & 0xFFFFFFFFULL;
633 	key.hash = 0;
634 	alias = map.find(key);
635 	if (alias != map.end())
636 		return alias;
637 
638 	if (!ignoreAddress) {
639 		// No data hash.
640 		key.cachekey = cachekey;
641 		key.hash = 0;
642 		alias = map.find(key);
643 		if (alias != map.end())
644 			return alias;
645 	}
646 
647 	// No address.
648 	key.cachekey = cachekey & 0xFFFFFFFFULL;
649 	key.hash = hash;
650 	alias = map.find(key);
651 	if (alias != map.end())
652 		return alias;
653 
654 	if (!ignoreAddress) {
655 		// Address, but not clut hash (in case of garbage clut data.)
656 		key.cachekey = cachekey & ~0xFFFFFFFFULL;
657 		key.hash = hash;
658 		alias = map.find(key);
659 		if (alias != map.end())
660 			return alias;
661 	}
662 
663 	// Anything with this data hash (a little dangerous.)
664 	key.cachekey = 0;
665 	key.hash = hash;
666 	return map.find(key);
667 }
668 
FindFiltering(u64 cachekey,u32 hash,TextureFiltering * forceFiltering)669 bool TextureReplacer::FindFiltering(u64 cachekey, u32 hash, TextureFiltering *forceFiltering) {
670 	if (!Enabled() || !g_Config.bReplaceTextures) {
671 		return false;
672 	}
673 
674 	ReplacementCacheKey replacementKey(cachekey, hash);
675 	auto filter = LookupWildcard(filtering_, replacementKey, cachekey, hash, ignoreAddress_);
676 	if (filter == filtering_.end()) {
677 		// Allow a global wildcard.
678 		replacementKey.cachekey = 0;
679 		replacementKey.hash = 0;
680 		filter = filtering_.find(replacementKey);
681 	}
682 	if (filter != filtering_.end()) {
683 		*forceFiltering = filter->second;
684 		return true;
685 	}
686 	return false;
687 }
688 
LookupHashFile(u64 cachekey,u32 hash,int level)689 std::string TextureReplacer::LookupHashFile(u64 cachekey, u32 hash, int level) {
690 	ReplacementAliasKey key(cachekey, hash, level);
691 	auto alias = LookupWildcard(aliases_, key, cachekey, hash, ignoreAddress_);
692 	if (alias != aliases_.end()) {
693 		// Note: this will be blank if explicitly ignored.
694 		return alias->second;
695 	}
696 
697 	return HashName(cachekey, hash, level) + ".png";
698 }
699 
HashName(u64 cachekey,u32 hash,int level)700 std::string TextureReplacer::HashName(u64 cachekey, u32 hash, int level) {
701 	char hashname[16 + 8 + 1 + 11 + 1] = {};
702 	if (level > 0) {
703 		snprintf(hashname, sizeof(hashname), "%016llx%08x_%d", cachekey, hash, level);
704 	} else {
705 		snprintf(hashname, sizeof(hashname), "%016llx%08x", cachekey, hash);
706 	}
707 
708 	return hashname;
709 }
710 
LookupHashRange(u32 addr,int & w,int & h)711 bool TextureReplacer::LookupHashRange(u32 addr, int &w, int &h) {
712 	const u64 rangeKey = ((u64)addr << 32) | ((u64)w << 16) | h;
713 	auto range = hashranges_.find(rangeKey);
714 	if (range != hashranges_.end()) {
715 		const WidthHeightPair &wh = range->second;
716 		w = wh.first;
717 		h = wh.second;
718 		return true;
719 	}
720 
721 	return false;
722 }
723 
LookupReduceHashRange(int & w,int & h)724 float TextureReplacer::LookupReduceHashRange(int& w, int& h) {
725 	const u64 reducerangeKey = ((u64)w << 16) | h;
726 	auto range = reducehashranges_.find(reducerangeKey);
727 	if (range != reducehashranges_.end()) {
728 		float rhv = range->second;
729 		return rhv;
730 	}
731 	else {
732 		return reduceHashGlobalValue;
733 	}
734 }
735 
Load(int level,void * out,int rowPitch)736 bool ReplacedTexture::Load(int level, void *out, int rowPitch) {
737 	_assert_msg_((size_t)level < levels_.size(), "Invalid miplevel");
738 	_assert_msg_(out != nullptr && rowPitch > 0, "Invalid out/pitch");
739 
740 	const ReplacedTextureLevel &info = levels_[level];
741 
742 	FILE *fp = File::OpenCFile(info.file, "rb");
743 	if (!fp) {
744 		return false;
745 	}
746 
747 	auto imageType = Identify(fp);
748 	if (imageType == ReplacedImageType::ZIM) {
749 		size_t zimSize = File::GetFileSize(fp);
750 		std::unique_ptr<uint8_t[]> zim(new uint8_t[zimSize]);
751 		if (!zim) {
752 			ERROR_LOG(G3D, "Failed to allocate memory for texture replacement");
753 			fclose(fp);
754 			return false;
755 		}
756 
757 		if (fread(&zim[0], 1, zimSize, fp) != zimSize) {
758 			ERROR_LOG(G3D, "Could not load texture replacement: %s - failed to read ZIM", info.file.c_str());
759 			fclose(fp);
760 			return false;
761 		}
762 
763 		int w, h, f;
764 		uint8_t *image;
765 		const int MIN_LINES_PER_THREAD = 4;
766 		if (LoadZIMPtr(&zim[0], zimSize, &w, &h, &f, &image)) {
767 			ParallelRangeLoop(&g_threadManager, [&](int l, int h) {
768 				for (int y = l; y < h; ++y) {
769 					memcpy((uint8_t *)out + rowPitch * y, image + w * 4 * y, w * 4);
770 				}
771 			}, 0, h, MIN_LINES_PER_THREAD);
772 			free(image);
773 		}
774 
775 		// This will only check the hashed bits.
776 		CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)out, rowPitch / sizeof(u32), w, h);
777 		if (res == CHECKALPHA_ANY || level == 0) {
778 			alphaStatus_ = ReplacedTextureAlpha(res);
779 		}
780 	} else if (imageType == ReplacedImageType::PNG) {
781 		png_image png = {};
782 		png.version = PNG_IMAGE_VERSION;
783 
784 		if (!png_image_begin_read_from_stdio(&png, fp)) {
785 			ERROR_LOG(G3D, "Could not load texture replacement info: %s - %s", info.file.c_str(), png.message);
786 			fclose(fp);
787 			return false;
788 		}
789 
790 		bool checkedAlpha = false;
791 		if ((png.format & PNG_FORMAT_FLAG_ALPHA) == 0) {
792 			// Well, we know for sure it doesn't have alpha.
793 			if (level == 0) {
794 				alphaStatus_ = ReplacedTextureAlpha::FULL;
795 			}
796 			checkedAlpha = true;
797 		}
798 		png.format = PNG_FORMAT_RGBA;
799 
800 		if (!png_image_finish_read(&png, nullptr, out, rowPitch, nullptr)) {
801 			ERROR_LOG(G3D, "Could not load texture replacement: %s - %s", info.file.c_str(), png.message);
802 			fclose(fp);
803 			return false;
804 		}
805 		png_image_free(&png);
806 
807 		if (!checkedAlpha) {
808 			// This will only check the hashed bits.
809 			CheckAlphaResult res = CheckAlphaRGBA8888Basic((u32 *)out, rowPitch / sizeof(u32), png.width, png.height);
810 			if (res == CHECKALPHA_ANY || level == 0) {
811 				alphaStatus_ = ReplacedTextureAlpha(res);
812 			}
813 		}
814 	}
815 
816 	fclose(fp);
817 	return true;
818 }
819 
GenerateIni(const std::string & gameID,Path & generatedFilename)820 bool TextureReplacer::GenerateIni(const std::string &gameID, Path &generatedFilename) {
821 	if (gameID.empty())
822 		return false;
823 
824 	Path texturesDirectory = GetSysDirectory(DIRECTORY_TEXTURES) / gameID;
825 	if (!File::Exists(texturesDirectory)) {
826 		File::CreateFullPath(texturesDirectory);
827 	}
828 
829 	generatedFilename = texturesDirectory / INI_FILENAME;
830 	if (File::Exists(generatedFilename))
831 		return true;
832 
833 	FILE *f = File::OpenCFile(generatedFilename, "wb");
834 	if (f) {
835 		fwrite("\xEF\xBB\xBF", 1, 3, f);
836 
837 		// Let's also write some defaults.
838 		fprintf(f, "# This file is optional and describes your textures.\n");
839 		fprintf(f, "# Some information on syntax available here:\n");
840 		fprintf(f, "# https://github.com/hrydgard/ppsspp/wiki/Texture-replacement-ini-syntax \n");
841 		fprintf(f, "[options]\n");
842 		fprintf(f, "version = 1\n");
843 		fprintf(f, "hash = quick\n");
844 		fprintf(f, "ignoreMipmap = false\n");
845 		fprintf(f, "\n");
846 		fprintf(f, "[games]\n");
847 		fprintf(f, "# Used to make it easier to install, and override settings for other regions.\n");
848 		fprintf(f, "# Files still have to be copied to each TEXTURES folder.");
849 		fprintf(f, "%s = %s\n", gameID.c_str(), INI_FILENAME.c_str());
850 		fprintf(f, "\n");
851 		fprintf(f, "[hashes]\n");
852 		fprintf(f, "# Use / for folders not \\, avoid special characters, and stick to lowercase.\n");
853 		fprintf(f, "# See wiki for more info.\n");
854 		fprintf(f, "\n");
855 		fprintf(f, "[hashranges]\n");
856 		fprintf(f, "\n");
857 		fprintf(f, "[filtering]\n");
858 		fprintf(f, "\n");
859 		fprintf(f, "[reducehashranges]\n");
860 		fclose(f);
861 	}
862 	return File::Exists(generatedFilename);
863 }
864