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