Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/GPU/Common/TextureReplacer.cpp
3186 views
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
20
#include <cstring>
21
#include <memory>
22
#include <png.h>
23
24
#include "ext/basis_universal/basisu_transcoder.h"
25
#include "ext/xxhash.h"
26
27
#include "Common/Data/Format/IniFile.h"
28
#include "Common/Data/Text/I18n.h"
29
#include "Common/Data/Text/Parsers.h"
30
#include "Common/File/VFS/DirectoryReader.h"
31
#include "Common/File/VFS/ZipFileReader.h"
32
#include "Common/File/FileUtil.h"
33
#include "Common/File/VFS/VFS.h"
34
#include "Common/StringUtils.h"
35
#include "Common/System/OSD.h"
36
#include "Common/Thread/ThreadManager.h"
37
#include "Common/TimeUtil.h"
38
#include "Core/Config.h"
39
#include "Core/System.h"
40
#include "Core/ELF/ParamSFO.h"
41
#include "GPU/Common/TextureReplacer.h"
42
#include "GPU/Common/TextureDecoder.h"
43
44
static const std::string INI_FILENAME = "textures.ini";
45
static const std::string ZIP_FILENAME = "textures.zip";
46
static const std::string NEW_TEXTURE_DIR = "new/";
47
static const int VERSION = 1;
48
static const double MAX_CACHE_SIZE = 4.0;
49
static bool basisu_initialized = false;
50
51
TextureReplacer::TextureReplacer(Draw::DrawContext *draw) {
52
if (!basisu_initialized) {
53
basist::basisu_transcoder_init();
54
basisu_initialized = true;
55
}
56
// We don't want to keep the draw object around, so extract the info we need.
57
if (draw->GetDataFormatSupport(Draw::DataFormat::BC3_UNORM_BLOCK)) formatSupport_.bc123 = true;
58
if (draw->GetDataFormatSupport(Draw::DataFormat::ASTC_4x4_UNORM_BLOCK)) formatSupport_.astc = true;
59
if (draw->GetDataFormatSupport(Draw::DataFormat::BC7_UNORM_BLOCK)) formatSupport_.bc7 = true;
60
if (draw->GetDataFormatSupport(Draw::DataFormat::ETC2_R8G8B8_UNORM_BLOCK)) formatSupport_.etc2 = true;
61
}
62
63
TextureReplacer::~TextureReplacer() {
64
for (auto iter : levelCache_) {
65
delete iter.second;
66
}
67
delete vfs_;
68
}
69
70
void TextureReplacer::NotifyConfigChanged() {
71
gameID_ = g_paramSFO.GetDiscID();
72
73
bool wasReplaceEnabled = replaceEnabled_;
74
replaceEnabled_ = g_Config.bReplaceTextures;
75
saveEnabled_ = g_Config.bSaveNewTextures;
76
if (replaceEnabled_ || saveEnabled_) {
77
basePath_ = GetSysDirectory(DIRECTORY_TEXTURES) / gameID_;
78
replaceEnabled_ = replaceEnabled_ && File::IsDirectory(basePath_);
79
newTextureDir_ = basePath_ / NEW_TEXTURE_DIR;
80
81
// If we're saving, auto-create the directory.
82
if (saveEnabled_ && !File::Exists(newTextureDir_)) {
83
INFO_LOG(Log::TexReplacement, "Creating new texture directory: '%s'", newTextureDir_.ToVisualString().c_str());
84
File::CreateFullPath(newTextureDir_);
85
// We no longer create a nomedia file here, since we put one
86
// in the TEXTURES root.
87
}
88
}
89
90
if (!replaceEnabled_ && wasReplaceEnabled) {
91
delete vfs_;
92
vfs_ = nullptr;
93
Decimate(ReplacerDecimateMode::ALL);
94
} else if (!wasReplaceEnabled && replaceEnabled_) {
95
std::string error;
96
replaceEnabled_ = LoadIni(&error);
97
if (!error.empty() && !replaceEnabled_) {
98
ERROR_LOG(Log::G3D, "ERROR: %s", error.c_str());
99
g_OSD.Show(OSDType::MESSAGE_ERROR, error, 5.0f);
100
}
101
} else if (saveEnabled_) {
102
// Even if just saving is enabled, it makes sense to reload the ini to get the correct
103
// settings for saving. See issue #19086. This can be expensive though.
104
std::string error;
105
bool result = LoadIni(&error, false);
106
if (!result) {
107
// Ignore errors here, just log if we successfully loaded an ini.
108
} else {
109
INFO_LOG(Log::G3D, "Loaded INI file for saving.");
110
}
111
}
112
}
113
114
bool TextureReplacer::LoadIni(std::string *error, bool notify) {
115
hash_ = ReplacedTextureHash::QUICK;
116
aliases_.clear();
117
hashranges_.clear();
118
filtering_.clear();
119
reducehashranges_.clear();
120
121
allowVideo_ = false;
122
ignoreAddress_ = false;
123
reduceHash_ = false;
124
reduceHashGlobalValue = 0.5;
125
// Prevents dumping the mipmaps.
126
ignoreMipmap_ = false;
127
128
delete vfs_;
129
vfs_ = nullptr;
130
131
Path zipPath = basePath_ / ZIP_FILENAME;
132
133
// First, check for textures.zip, which is used to reduce IO.
134
VFSBackend *dir = ZipFileReader::Create(zipPath, "", false);
135
if (!dir) {
136
INFO_LOG(Log::TexReplacement, "%s wasn't a zip file - opening the directory %s instead.", zipPath.c_str(), basePath_.c_str());
137
vfsIsZip_ = false;
138
dir = new DirectoryReader(basePath_);
139
} else {
140
if (!replaceEnabled_ && saveEnabled_) {
141
WARN_LOG(Log::TexReplacement, "Found zip file even though only saving is enabled! This is weird.");
142
}
143
vfsIsZip_ = true;
144
}
145
146
IniFile ini;
147
bool iniLoaded = ini.LoadFromVFS(*dir, INI_FILENAME);
148
149
if (iniLoaded) {
150
if (!LoadIniValues(ini, dir, false, error)) {
151
delete dir;
152
return false;
153
}
154
155
// Allow overriding settings per game id.
156
std::string overrideFilename;
157
if (ini.GetOrCreateSection("games")->Get(gameID_.c_str(), &overrideFilename, "")) {
158
if (overrideFilename == "true") {
159
// Ignore it
160
} else if (!overrideFilename.empty() && overrideFilename != INI_FILENAME) {
161
IniFile overrideIni;
162
iniLoaded = overrideIni.LoadFromVFS(*dir, overrideFilename);
163
if (!iniLoaded) {
164
*error = "Loading override ini failed: '" + overrideFilename + "'";
165
ERROR_LOG(Log::TexReplacement, "Failed to load extra texture ini: '%s'", overrideFilename.c_str());
166
// Since this error is most likely to occure for texture pack creators, let's just bail here
167
// so that the creator is more likely to look in the logs for what happened.
168
delete dir;
169
return false;
170
}
171
172
INFO_LOG(Log::TexReplacement, "Loading extra texture ini: %s", overrideFilename.c_str());
173
if (!LoadIniValues(overrideIni, nullptr, true, error)) {
174
*error = "Override: " + *error;
175
delete dir;
176
return false;
177
}
178
}
179
}
180
} else {
181
if (vfsIsZip_) {
182
// We don't accept zip files without inis.
183
ERROR_LOG(Log::TexReplacement, "Texture pack lacking ini file: %s", basePath_.c_str());
184
*error = "Zip files without ini files will not load";
185
delete dir;
186
return false;
187
} else {
188
if (replaceEnabled_) {
189
WARN_LOG(Log::TexReplacement, "Texture pack lacking ini file: %s Proceeding with only hash-named textures in the root.", basePath_.c_str());
190
}
191
// Do what we can do anyway: Scan for textures and build the map.
192
std::map<ReplacementCacheKey, std::map<int, std::string>> filenameMap;
193
ScanForHashNamedFiles(dir, filenameMap);
194
195
if (filenameMap.empty()) {
196
WARN_LOG(Log::TexReplacement, "No replacement textures found.");
197
return false;
198
}
199
200
ComputeAliasMap(filenameMap);
201
}
202
}
203
204
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
205
if (replaceEnabled_ && notify) {
206
g_OSD.Show(OSDType::MESSAGE_SUCCESS, gr->T("Texture replacement pack activated"), 3.0f);
207
}
208
209
vfs_ = dir;
210
211
// If we have stuff loaded from before, need to update the vfs pointers to avoid
212
// crash on exit. The actual problem is that we tend to call LoadIni a little too much...
213
for (auto &repl : levelCache_) {
214
repl.second->vfs_ = vfs_;
215
}
216
217
if (replaceEnabled_) {
218
if (vfsIsZip_) {
219
INFO_LOG(Log::TexReplacement, "Texture pack activated from '%s'", (basePath_ / ZIP_FILENAME).c_str());
220
} else {
221
INFO_LOG(Log::TexReplacement, "Texture pack activated from '%s'", basePath_.c_str());
222
}
223
}
224
225
// The ini doesn't have to exist for the texture directory or zip to be valid.
226
return true;
227
}
228
229
void TextureReplacer::ScanForHashNamedFiles(VFSBackend *dir, std::map<ReplacementCacheKey, std::map<int, std::string>> &filenameMap) {
230
// Scan the root of the texture folder/zip and preinitialize the hash map.
231
// TODO: Could put VFSFileReference into the map...
232
std::vector<File::FileInfo> filesInRoot;
233
dir->GetFileListing("", &filesInRoot, nullptr);
234
for (auto file : filesInRoot) {
235
if (file.isDirectory)
236
continue;
237
if (file.name.empty() || file.name[0] == '.')
238
continue;
239
Path path(file.name);
240
std::string ext = path.GetFileExtension();
241
242
std::string hash = file.name.substr(0, file.name.size() - ext.size());
243
if (!((hash.size() >= 26 && hash.size() <= 27 && hash[24] == '_') || hash.size() == 24)) {
244
continue;
245
}
246
// OK, it's hash-like enough to try to parse it into the map.
247
if (equalsNoCase(ext, ".ktx2") || equalsNoCase(ext, ".png") || equalsNoCase(ext, ".dds") || equalsNoCase(ext, ".zim")) {
248
ReplacementCacheKey key(0, 0);
249
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.
250
if (sscanf(hash.c_str(), "%16llx%8x_%d", &key.cachekey, &key.hash, &level) >= 1) {
251
// INFO_LOG(Log::TexReplacement, "hash-like file in root, adding: %s", file.name.c_str());
252
filenameMap[key][level] = file.name;
253
}
254
}
255
}
256
}
257
258
void TextureReplacer::ComputeAliasMap(const std::map<ReplacementCacheKey, std::map<int, std::string>> &filenameMap) {
259
for (auto &pair : filenameMap) {
260
std::string alias;
261
int mipIndex = 0;
262
for (auto &level : pair.second) {
263
if (level.first == mipIndex) {
264
alias += level.second + "|";
265
mipIndex++;
266
} else {
267
WARN_LOG(Log::TexReplacement, "Non-sequential mip index %d, breaking. filenames=%s", level.first, level.second.c_str());
268
break;
269
}
270
}
271
if (alias == "|") {
272
alias.clear(); // marker for no replacement
273
}
274
// Replace any '\' with '/', to be safe and consistent. Since these are from the ini file, we do this on all platforms.
275
for (auto &c : alias) {
276
if (c == '\\') {
277
c = '/';
278
}
279
}
280
aliases_[pair.first] = alias;
281
}
282
}
283
284
bool TextureReplacer::LoadIniValues(IniFile &ini, VFSBackend *dir, bool isOverride, std::string *error) {
285
INFO_LOG(Log::G3D, "Loading ini values...");
286
287
auto options = ini.GetOrCreateSection("options");
288
std::string hash;
289
if (!options->Get("hash", &hash, "")) {
290
*error = "textures.ini: Hash type not specified";
291
return false;
292
}
293
if (strcasecmp(hash.c_str(), "quick") == 0) {
294
hash_ = ReplacedTextureHash::QUICK;
295
} else if (strcasecmp(hash.c_str(), "xxh32") == 0) {
296
hash_ = ReplacedTextureHash::XXH32;
297
} else if (strcasecmp(hash.c_str(), "xxh64") == 0) {
298
hash_ = ReplacedTextureHash::XXH64;
299
} else if (!isOverride || !hash.empty()) {
300
*error = "textures.ini: Unsupported hash type: " + hash;
301
return false;
302
}
303
304
options->Get("video", &allowVideo_, allowVideo_);
305
options->Get("ignoreAddress", &ignoreAddress_, ignoreAddress_);
306
// Multiplies sizeInRAM/bytesPerLine in XXHASH by 0.5.
307
options->Get("reduceHash", &reduceHash_, reduceHash_);
308
options->Get("ignoreMipmap", &ignoreMipmap_, ignoreMipmap_);
309
options->Get("skipLastDXT1Blocks128x64", &skipLastDXT1Blocks128x64_, skipLastDXT1Blocks128x64_);
310
if (reduceHash_ && hash_ == ReplacedTextureHash::QUICK) {
311
reduceHash_ = false;
312
ERROR_LOG(Log::TexReplacement, "Texture Replacement: reduceHash option requires safer hash, use xxh32 or xxh64 instead.");
313
}
314
315
if (ignoreAddress_ && hash_ == ReplacedTextureHash::QUICK) {
316
ignoreAddress_ = false;
317
ERROR_LOG(Log::TexReplacement, "Texture Replacement: ignoreAddress option requires safer hash, use xxh32 or xxh64 instead.");
318
}
319
320
int version = 0;
321
if (options->Get("version", &version, 0) && version > VERSION) {
322
ERROR_LOG(Log::TexReplacement, "Unsupported texture replacement version %d, trying anyway", version);
323
}
324
325
int badFileNameCount = 0;
326
327
std::map<ReplacementCacheKey, std::map<int, std::string>> filenameMap;
328
329
if (dir) {
330
ScanForHashNamedFiles(dir, filenameMap);
331
}
332
333
std::string badFilenames;
334
335
if (ini.HasSection("hashes")) {
336
const Section *hashesSection = ini.GetOrCreateSection("hashes");
337
// Format: hashname = filename.png
338
bool checkFilenames = saveEnabled_ && !g_Config.bIgnoreTextureFilenames && !vfsIsZip_;
339
340
for (const auto &line : hashesSection->Lines()) {
341
if (line.Key().empty())
342
continue;
343
ReplacementCacheKey key(0, 0);
344
// sscanf might fail to pluck the level if omitted from the line, but that's ok, we default level to 0.
345
// sscanf doesn't write to non-matched outputs.
346
int level = 0;
347
char k[128];
348
truncate_cpy(k, line.Key());
349
std::string_view v = line.Value();
350
if (sscanf(k, "%16llx%8x_%d", &key.cachekey, &key.hash, &level) >= 1) {
351
// We allow empty filenames, to mark textures that we don't want to keep saving.
352
filenameMap[key][level] = v;
353
if (checkFilenames) {
354
// TODO: We should check for the union of these on all platforms, really.
355
#if PPSSPP_PLATFORM(WINDOWS)
356
bool bad = v.find_first_of("\\ABCDEFGHIJKLMNOPQRSTUVWXYZ:<>|?*") != std::string::npos;
357
// Uppercase probably means the filenames don't match.
358
// Avoiding an actual check of the filenames to avoid performance impact.
359
#else
360
bool bad = v.find_first_of("\\:<>|?*") != std::string::npos;
361
#endif
362
if (bad) {
363
badFileNameCount++;
364
if (badFileNameCount == 10) {
365
badFilenames.append("...");
366
} else if (badFileNameCount < 10) {
367
badFilenames.append(v);
368
badFilenames.push_back('\n');
369
}
370
}
371
}
372
} else {
373
ERROR_LOG(Log::TexReplacement, "Unsupported syntax under [hashes], ignoring: %s = ", k);
374
}
375
}
376
}
377
378
// Now, translate the filenameMap to the final aliasMap.
379
ComputeAliasMap(filenameMap);
380
381
if (badFileNameCount > 0) {
382
auto err = GetI18NCategory(I18NCat::ERRORS);
383
g_OSD.Show(OSDType::MESSAGE_WARNING, err->T("textures.ini filenames may not be cross - platform(banned characters)"), badFilenames, 6.0f);
384
WARN_LOG(Log::TexReplacement, "Potentially bad filenames: %s", badFilenames.c_str());
385
}
386
387
if (ini.HasSection("hashranges")) {
388
auto hashranges = ini.GetOrCreateSection("hashranges")->ToMap();
389
// Format: addr,w,h = newW,newH
390
for (const auto &[k, v] : hashranges) {
391
ParseHashRange(k, v);
392
}
393
}
394
395
if (ini.HasSection("filtering")) {
396
auto filters = ini.GetOrCreateSection("filtering")->ToMap();
397
// Format: hashname = nearest or linear
398
for (const auto &[k, v] : filters) {
399
ParseFiltering(k, v);
400
}
401
}
402
403
if (ini.HasSection("reducehashranges")) {
404
auto reducehashranges = ini.GetOrCreateSection("reducehashranges")->ToMap();
405
// Format: w,h = reducehashvalues
406
for (const auto &[k, v] : reducehashranges) {
407
ParseReduceHashRange(k, v);
408
}
409
}
410
411
return true;
412
}
413
414
void TextureReplacer::ParseHashRange(const std::string &key, const std::string &value) {
415
std::vector<std::string> keyParts;
416
SplitString(key, ',', keyParts);
417
std::vector<std::string> valueParts;
418
SplitString(value, ',', valueParts);
419
420
if (keyParts.size() != 3 || valueParts.size() != 2) {
421
ERROR_LOG(Log::TexReplacement, "Ignoring invalid hashrange %s = %s, expecting addr,w,h = w,h", key.c_str(), value.c_str());
422
return;
423
}
424
425
// Allow addr not starting with 0x, for consistency. TryParse requires 0x to parse as hex.
426
if (!startsWith(keyParts[0], "0x") && !startsWith(keyParts[0], "0X")) {
427
keyParts[0] = "0x" + keyParts[0];
428
}
429
430
u32 addr;
431
u32 fromW;
432
u32 fromH;
433
if (!TryParse(keyParts[0], &addr) || !TryParse(keyParts[1], &fromW) || !TryParse(keyParts[2], &fromH)) {
434
ERROR_LOG(Log::TexReplacement, "Ignoring invalid hashrange %s = %s, key format is 0x12345678,512,512", key.c_str(), value.c_str());
435
return;
436
}
437
438
u32 toW;
439
u32 toH;
440
if (!TryParse(valueParts[0], &toW) || !TryParse(valueParts[1], &toH)) {
441
ERROR_LOG(Log::TexReplacement, "Ignoring invalid hashrange %s = %s, value format is 512,512", key.c_str(), value.c_str());
442
return;
443
}
444
445
if (toW > fromW || toH > fromH) {
446
ERROR_LOG(Log::TexReplacement, "Ignoring invalid hashrange %s = %s, range bigger than source", key.c_str(), value.c_str());
447
return;
448
}
449
450
const u64 rangeKey = ((u64)addr << 32) | ((u64)fromW << 16) | fromH;
451
hashranges_[rangeKey] = WidthHeightPair(toW, toH);
452
}
453
454
void TextureReplacer::ParseFiltering(const std::string &key, const std::string &value) {
455
ReplacementCacheKey itemKey(0, 0);
456
if (sscanf(key.c_str(), "%16llx%8x", &itemKey.cachekey, &itemKey.hash) >= 1) {
457
if (!strcasecmp(value.c_str(), "nearest")) {
458
filtering_[itemKey] = TEX_FILTER_FORCE_NEAREST;
459
} else if (!strcasecmp(value.c_str(), "linear")) {
460
filtering_[itemKey] = TEX_FILTER_FORCE_LINEAR;
461
} else if (!strcasecmp(value.c_str(), "auto")) {
462
filtering_[itemKey] = TEX_FILTER_AUTO;
463
} else {
464
ERROR_LOG(Log::TexReplacement, "Unsupported syntax under [filtering]: %s", value.c_str());
465
}
466
} else {
467
ERROR_LOG(Log::TexReplacement, "Unsupported syntax under [filtering]: %s", key.c_str());
468
}
469
}
470
471
void TextureReplacer::ParseReduceHashRange(const std::string& key, const std::string& value) {
472
std::vector<std::string> keyParts;
473
SplitString(key, ',', keyParts);
474
std::vector<std::string> valueParts;
475
SplitString(value, ',', valueParts);
476
477
if (keyParts.size() != 2 || valueParts.size() != 1) {
478
ERROR_LOG(Log::TexReplacement, "Ignoring invalid reducehashrange %s = %s, expecting w,h = reducehashvalue", key.c_str(), value.c_str());
479
return;
480
}
481
482
u32 forW;
483
u32 forH;
484
if (!TryParse(keyParts[0], &forW) || !TryParse(keyParts[1], &forH)) {
485
ERROR_LOG(Log::TexReplacement, "Ignoring invalid reducehashrange %s = %s, key format is 512,512", key.c_str(), value.c_str());
486
return;
487
}
488
489
float rhashvalue;
490
if (!TryParse(valueParts[0], &rhashvalue)) {
491
ERROR_LOG(Log::TexReplacement, "Ignoring invalid reducehashrange %s = %s, value format is 0.5", key.c_str(), value.c_str());
492
return;
493
}
494
495
if (rhashvalue == 0) {
496
ERROR_LOG(Log::TexReplacement, "Ignoring invalid hashrange %s = %s, reducehashvalue can't be 0", key.c_str(), value.c_str());
497
return;
498
}
499
500
const u64 reducerangeKey = ((u64)forW << 16) | forH;
501
reducehashranges_[reducerangeKey] = rhashvalue;
502
}
503
504
u32 TextureReplacer::ComputeHash(u32 addr, int bufw, int w, int h, bool swizzled, GETextureFormat fmt, u16 maxSeenV) {
505
_dbg_assert_msg_(replaceEnabled_ || saveEnabled_, "Replacement not enabled");
506
507
// TODO: Take swizzled into account, like in QuickTexHash().
508
// Note: Currently, only the MLB games are known to need this.
509
510
if (!LookupHashRange(addr, w, h, &w, &h)) {
511
// There wasn't any hash range, let's fall back to maxSeenV logic.
512
if (h == 512 && maxSeenV < 512 && maxSeenV != 0) {
513
h = (int)maxSeenV;
514
}
515
}
516
517
const u8 *checkp = Memory::GetPointerUnchecked(addr);
518
519
float reduceHashSize = 1.0f;
520
if (reduceHash_) {
521
reduceHashSize = LookupReduceHashRange(w, h);
522
// default to reduceHashGlobalValue which default is 0.5
523
}
524
525
if (bufw <= w) {
526
// We can assume the data is contiguous. These are the total used pixels.
527
const u32 totalPixels = bufw * h + (w - bufw);
528
u32 sizeInRAM = (textureBitsPerPixel[fmt] * totalPixels) / 8 * reduceHashSize;
529
530
// Sanity check: Ignore textures that are at the end of RAM.
531
if (Memory::MaxSizeAtAddress(addr) < sizeInRAM) {
532
ERROR_LOG(Log::G3D, "Can't hash a %d bytes textures at %08x - end point is outside memory", sizeInRAM, addr);
533
return 0;
534
}
535
536
// Hack for Yu Gi Oh texture hashing problem. See issue #19714
537
if (skipLastDXT1Blocks128x64_ && fmt == GE_TFMT_DXT1 && w == 128 && h == 64) {
538
// Skip the last few blocks as specified.
539
sizeInRAM -= 8 * skipLastDXT1Blocks128x64_;
540
}
541
542
switch (hash_) {
543
case ReplacedTextureHash::QUICK:
544
return StableQuickTexHash(checkp, sizeInRAM);
545
case ReplacedTextureHash::XXH32:
546
return XXH32(checkp, sizeInRAM, 0xBACD7814);
547
case ReplacedTextureHash::XXH64:
548
return XXH64(checkp, sizeInRAM, 0xBACD7814);
549
default:
550
return 0;
551
}
552
} else {
553
// We have gaps. Let's hash each row and sum.
554
const u32 bytesPerLine = (textureBitsPerPixel[fmt] * w) / 8 * reduceHashSize;
555
const u32 stride = (textureBitsPerPixel[fmt] * bufw) / 8;
556
557
u32 result = 0;
558
switch (hash_) {
559
case ReplacedTextureHash::QUICK:
560
for (int y = 0; y < h; ++y) {
561
u32 rowHash = StableQuickTexHash(checkp, bytesPerLine);
562
result = (result * 11) ^ rowHash;
563
checkp += stride;
564
}
565
break;
566
567
case ReplacedTextureHash::XXH32:
568
for (int y = 0; y < h; ++y) {
569
u32 rowHash = XXH32(checkp, bytesPerLine, 0xBACD7814);
570
result = (result * 11) ^ rowHash;
571
checkp += stride;
572
}
573
break;
574
575
case ReplacedTextureHash::XXH64:
576
for (int y = 0; y < h; ++y) {
577
u32 rowHash = XXH64(checkp, bytesPerLine, 0xBACD7814);
578
result = (result * 11) ^ rowHash;
579
checkp += stride;
580
}
581
break;
582
583
default:
584
break;
585
}
586
587
return result;
588
}
589
}
590
591
ReplacedTexture *TextureReplacer::FindReplacement(u64 cachekey, u32 hash, int w, int h) {
592
// Only actually replace if we're replacing. We might just be saving.
593
if (!Enabled() || !g_Config.bReplaceTextures) {
594
return nullptr;
595
}
596
597
ReplacementCacheKey replacementKey(cachekey, hash);
598
auto it = cache_.find(replacementKey);
599
if (it != cache_.end()) {
600
return it->second.texture;
601
}
602
603
ReplacementDesc desc;
604
desc.newW = w;
605
desc.newH = h;
606
desc.w = w;
607
desc.h = h;
608
desc.cachekey = cachekey;
609
desc.hash = hash;
610
LookupHashRange(cachekey >> 32, w, h, &desc.newW, &desc.newH);
611
612
if (ignoreAddress_) {
613
cachekey = cachekey & 0xFFFFFFFFULL;
614
}
615
616
bool foundAlias = false;
617
bool ignored = false;
618
std::string hashfiles = LookupHashFile(cachekey, hash, &foundAlias, &ignored);
619
620
// Early-out for ignored textures, let's not bother even starting a thread task.
621
if (ignored) {
622
// WARN_LOG(Log::TexReplacement, "Not found/ignored: %s (%d, %d)", hashfiles.c_str(), (int)foundReplacement, (int)ignored);
623
// Insert an entry into the cache for faster lookup next time.
624
ReplacedTextureRef ref{};
625
cache_.emplace(std::make_pair(replacementKey, ref));
626
return nullptr;
627
}
628
629
desc.forceFiltering = (TextureFiltering)0; // invalid value
630
FindFiltering(cachekey, hash, &desc.forceFiltering);
631
632
if (!foundAlias) {
633
// We'll just need to generate the names for each level.
634
// By default, we look for png since that's also what's dumped.
635
// For other file formats, use the ini to create aliases.
636
desc.filenames.resize(MAX_REPLACEMENT_MIP_LEVELS);
637
for (int level = 0; level < desc.filenames.size(); level++) {
638
desc.filenames[level] = TextureReplacer::HashName(cachekey, hash, level) + ".png";
639
}
640
desc.logId = desc.filenames[0];
641
desc.hashfiles = desc.filenames[0]; // The generated filename of the top level is used as the key in the data cache.
642
hashfiles.clear();
643
hashfiles.reserve(desc.filenames[0].size() * (desc.filenames.size() + 1));
644
for (int level = 0; level < desc.filenames.size(); level++) {
645
hashfiles += desc.filenames[level];
646
hashfiles.push_back('|');
647
}
648
} else {
649
desc.logId = hashfiles;
650
SplitString(hashfiles, '|', desc.filenames);
651
desc.hashfiles = hashfiles;
652
}
653
654
_dbg_assert_(!hashfiles.empty());
655
// OK, we might already have a matching texture, we use hashfiles as a key. Look it up in the level cache.
656
auto iter = levelCache_.find(hashfiles);
657
if (iter != levelCache_.end()) {
658
// Insert an entry into the cache for faster lookup next time.
659
ReplacedTextureRef ref;
660
ref.hashfiles = hashfiles;
661
ref.texture = iter->second;
662
cache_.emplace(std::make_pair(replacementKey, ref));
663
return iter->second;
664
}
665
666
// Final path - we actually need a new replacement texture, because we haven't seen "hashfiles" before.
667
desc.basePath = basePath_;
668
desc.formatSupport = formatSupport_;
669
670
ReplacedTexture *texture = new ReplacedTexture(vfs_, desc);
671
672
ReplacedTextureRef ref;
673
ref.hashfiles = hashfiles;
674
ref.texture = texture;
675
cache_.emplace(std::make_pair(replacementKey, ref));
676
677
// Also, insert the level in the level cache so we can look up by desc_->hashfiles again.
678
levelCache_.emplace(std::make_pair(hashfiles, texture));
679
return texture;
680
}
681
682
static bool WriteTextureToPNG(png_imagep image, const Path &filename, int convert_to_8bit, const void *buffer, png_int_32 row_stride, const void *colormap) {
683
FILE *fp = File::OpenCFile(filename, "wb");
684
if (!fp) {
685
ERROR_LOG(Log::TexReplacement, "Save texture: Unable to open texture file '%s' for writing.", filename.c_str());
686
return false;
687
}
688
689
if (png_image_write_to_stdio(image, fp, convert_to_8bit, buffer, row_stride, colormap)) {
690
fclose(fp);
691
return true;
692
} else {
693
// This shouldn't really happen.
694
ERROR_LOG(Log::TexReplacement, "Texture PNG encode failed.");
695
fclose(fp);
696
remove(filename.c_str());
697
return false;
698
}
699
}
700
701
// We save textures on threadpool tasks since it's a fire-and-forget task, and both I/O and png compression
702
// can be pretty slow.
703
class SaveTextureTask : public Task {
704
public:
705
// malloc'd
706
u8 *rgbaData = nullptr;
707
708
int w = 0;
709
int h = 0;
710
711
Path filename;
712
Path saveFilename;
713
714
u32 replacedInfoHash = 0;
715
716
SaveTextureTask(u8 *_rgbaData) : rgbaData(_rgbaData) {}
717
~SaveTextureTask() {
718
free(rgbaData);
719
}
720
721
// This must be set to I/O blocking because of Android storage (so we attach the thread to JNI), while being CPU heavy too.
722
TaskType Type() const override { return TaskType::IO_BLOCKING; }
723
724
TaskPriority Priority() const override {
725
return TaskPriority::LOW;
726
}
727
728
void Run() override {
729
// Should we skip writing if the newly saved data already exists?
730
if (File::Exists(saveFilename)) {
731
return;
732
}
733
734
// And we always skip if the replace file already exists.
735
if (File::Exists(filename)) {
736
return;
737
}
738
739
Path saveDirectory = saveFilename.NavigateUp();
740
if (!File::Exists(saveDirectory)) {
741
// Previously, we created a .nomedia file here. This is unnecessary as they have recursive behavior.
742
// When initializing (see NotifyConfigChange above) we create one in the "root" of the "new" folder.
743
File::CreateFullPath(saveDirectory);
744
}
745
746
// Now that we've passed the checks, we change the file extension of the path we're actually
747
// going to write to to .png.
748
saveFilename = saveFilename.WithReplacedExtension(".png");
749
750
png_image png{};
751
png.version = PNG_IMAGE_VERSION;
752
png.format = PNG_FORMAT_RGBA;
753
png.width = w;
754
png.height = h;
755
bool success = WriteTextureToPNG(&png, saveFilename, 0, rgbaData, w * 4, nullptr);
756
png_image_free(&png);
757
if (png.warning_or_error >= 2) {
758
ERROR_LOG(Log::TexReplacement, "Saving texture to PNG produced errors.");
759
} else if (success) {
760
NOTICE_LOG(Log::TexReplacement, "Saving texture for replacement: %08x / %dx%d in '%s'", replacedInfoHash, w, h, saveFilename.ToVisualString().c_str());
761
} else {
762
ERROR_LOG(Log::TexReplacement, "Failed to write '%s'", saveFilename.c_str());
763
}
764
}
765
};
766
767
bool TextureReplacer::WillSave(const ReplacedTextureDecodeInfo &replacedInfo) const {
768
if (!saveEnabled_)
769
return false;
770
// Don't save the PPGe texture.
771
if (replacedInfo.addr > 0x05000000 && replacedInfo.addr < PSP_GetKernelMemoryEnd())
772
return false;
773
if (replacedInfo.isVideo && !allowVideo_)
774
return false;
775
776
return true;
777
}
778
779
void TextureReplacer::NotifyTextureDecoded(ReplacedTexture *texture, const ReplacedTextureDecodeInfo &replacedInfo, const void *data, int srcPitch, int level, int origW, int origH, int scaledW, int scaledH) {
780
_assert_msg_(saveEnabled_, "Texture saving not enabled");
781
_assert_(srcPitch >= 0);
782
_assert_(data);
783
_assert_(level >= 0);
784
785
if (!WillSave(replacedInfo)) {
786
// Ignore.
787
return;
788
}
789
790
if (ignoreMipmap_ && level > 0) {
791
// Not saving higher mips.
792
return;
793
}
794
795
u64 cachekey = replacedInfo.cachekey;
796
if (ignoreAddress_) {
797
cachekey = cachekey & 0xFFFFFFFFULL;
798
}
799
800
bool foundAlias = false;
801
bool ignored = false;
802
std::string replacedLevelNames = LookupHashFile(cachekey, replacedInfo.hash, &foundAlias, &ignored);
803
if (ignored) {
804
// The ini file entry was set to empty string. We can early-out.
805
return;
806
}
807
808
// Alright, get the specified filename for the level.
809
std::string hashfile;
810
if (!replacedLevelNames.empty()) {
811
// If the user has specified a name before, we get it here.
812
std::vector<std::string> names;
813
SplitString(replacedLevelNames, '|', names);
814
hashfile = names[std::min(level, (int)(names.size() - 1))];
815
} else {
816
// Generate a new PNG filename, complete with level.
817
hashfile = HashName(cachekey, replacedInfo.hash, level) + ".png";
818
}
819
820
ReplacementCacheKey replacementKey(cachekey, replacedInfo.hash);
821
auto it = savedCache_.find(replacementKey);
822
if (it != savedCache_.end()) {
823
// We've already saved this texture. Ignore it.
824
// We don't really care about changing the scale factor during runtime, only confusing.
825
return;
826
}
827
double now = time_now_d();
828
829
// Width/height of the image to save.
830
int w = scaledW;
831
int h = scaledH;
832
833
if (w == 0 || h == 0) {
834
return;
835
}
836
837
// Only save the hashed portion of the PNG.
838
int lookupW;
839
int lookupH;
840
if (LookupHashRange(replacedInfo.addr, origW, origH, &lookupW, &lookupH)) {
841
w = lookupW * (scaledW / origW);
842
h = lookupH * (scaledH / origH);
843
}
844
845
846
size_t saveBufSize = w * h * 4;
847
u8 *saveBuf = (u8 *)malloc(saveBufSize);
848
if (!saveBuf) {
849
ERROR_LOG(Log::TexReplacement, "Failed to allocated %d bytes of memory for saving a texture", (int)saveBufSize);
850
return;
851
}
852
853
// Copy data to a buffer so we can send it to the thread. Might as well compact-away the pitch
854
// while we're at it.
855
for (int y = 0; y < h; y++) {
856
memcpy(saveBuf + y * w * 4, (const u8 *)data + y * srcPitch, w * 4);
857
}
858
859
SaveTextureTask *task = new SaveTextureTask(std::move(saveBuf));
860
861
task->filename = basePath_ / hashfile;
862
task->saveFilename = newTextureDir_ / hashfile;
863
864
task->w = w;
865
task->h = h;
866
task->replacedInfoHash = replacedInfo.hash;
867
g_threadManager.EnqueueTask(task); // We don't care about waiting for the task. It'll be fine.
868
869
// Remember that we've saved this for next time.
870
// Should be OK that the actual disk write may not be finished yet.
871
SavedTextureCacheData &saveData = savedCache_[replacementKey];
872
saveData.levelW[level] = w;
873
saveData.levelH[level] = h;
874
saveData.levelSaved[level] = true;
875
saveData.lastTimeSaved = now;
876
}
877
878
void TextureReplacer::Decimate(ReplacerDecimateMode mode) {
879
// Allow replacements to be cached for a long time, although they're large.
880
double age = 1800.0;
881
if (mode == ReplacerDecimateMode::FORCE_PRESSURE) {
882
age = 90.0;
883
} else if (mode == ReplacerDecimateMode::ALL) {
884
age = 0.0;
885
} else if (lastTextureCacheSizeGB_ > 1.0) {
886
double pressure = std::min(MAX_CACHE_SIZE, lastTextureCacheSizeGB_) / MAX_CACHE_SIZE;
887
// Get more aggressive the closer we are to the max.
888
age = 90.0 + (1.0 - pressure) * 1710.0;
889
}
890
891
const double threshold = time_now_d() - age;
892
size_t totalSize = 0;
893
for (auto &item : levelCache_) {
894
// During decimation, it's fine to try-lock here to avoid blocking the main thread while
895
// the level is being loaded - in that case we don't want to decimate anyway.
896
if (item.second->lock_.try_lock()) {
897
item.second->PurgeIfNotUsedSinceTime(threshold);
898
totalSize += item.second->GetTotalDataSize(); // TODO: Make something better.
899
item.second->lock_.unlock();
900
}
901
// don't actually delete the items here, just clean out the data.
902
}
903
904
double totalSizeGB = totalSize / (1024.0 * 1024.0 * 1024.0);
905
if (totalSizeGB >= 1.0) {
906
WARN_LOG(Log::TexReplacement, "Decimated replacements older than %fs, currently using %f GB of RAM", age, totalSizeGB);
907
}
908
lastTextureCacheSizeGB_ = totalSizeGB;
909
}
910
911
template <typename Key, typename Value>
912
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) {
913
auto alias = map.find(key);
914
if (alias != map.end())
915
return alias;
916
917
// Also check for a few more aliases with zeroed portions:
918
// Only clut hash (very dangerous in theory, in practice not more than missing "just" data hash)
919
key.cachekey = cachekey & 0xFFFFFFFFULL;
920
key.hash = 0;
921
alias = map.find(key);
922
if (alias != map.end())
923
return alias;
924
925
if (!ignoreAddress) {
926
// No data hash.
927
key.cachekey = cachekey;
928
key.hash = 0;
929
alias = map.find(key);
930
if (alias != map.end())
931
return alias;
932
}
933
934
// No address.
935
key.cachekey = cachekey & 0xFFFFFFFFULL;
936
key.hash = hash;
937
alias = map.find(key);
938
if (alias != map.end())
939
return alias;
940
941
if (!ignoreAddress) {
942
// Address, but not clut hash (in case of garbage clut data.)
943
key.cachekey = cachekey & ~0xFFFFFFFFULL;
944
key.hash = hash;
945
alias = map.find(key);
946
if (alias != map.end())
947
return alias;
948
}
949
950
// Anything with this data hash (a little dangerous.)
951
key.cachekey = 0;
952
key.hash = hash;
953
return map.find(key);
954
}
955
956
bool TextureReplacer::FindFiltering(u64 cachekey, u32 hash, TextureFiltering *forceFiltering) {
957
if (!Enabled() || !g_Config.bReplaceTextures) {
958
return false;
959
}
960
961
ReplacementCacheKey replacementKey(cachekey, hash);
962
auto filter = LookupWildcard(filtering_, replacementKey, cachekey, hash, ignoreAddress_);
963
if (filter == filtering_.end()) {
964
// Allow a global wildcard.
965
replacementKey.cachekey = 0;
966
replacementKey.hash = 0;
967
filter = filtering_.find(replacementKey);
968
}
969
if (filter != filtering_.end()) {
970
*forceFiltering = filter->second;
971
return true;
972
}
973
return false;
974
}
975
976
std::string TextureReplacer::LookupHashFile(u64 cachekey, u32 hash, bool *foundAlias, bool *ignored) {
977
ReplacementCacheKey key(cachekey, hash);
978
auto alias = LookupWildcard(aliases_, key, cachekey, hash, ignoreAddress_);
979
if (alias != aliases_.end()) {
980
// Note: this will be blank if explicitly ignored.
981
*foundAlias = true;
982
*ignored = alias->second.empty();
983
return alias->second;
984
}
985
*foundAlias = false;
986
*ignored = false;
987
return "";
988
}
989
990
std::string TextureReplacer::HashName(u64 cachekey, u32 hash, int level) {
991
char hashname[16 + 8 + 1 + 11 + 1] = {};
992
if (level > 0) {
993
snprintf(hashname, sizeof(hashname), "%016llx%08x_%d", cachekey, hash, level);
994
} else {
995
snprintf(hashname, sizeof(hashname), "%016llx%08x", cachekey, hash);
996
}
997
998
return hashname;
999
}
1000
1001
bool TextureReplacer::LookupHashRange(u32 addr, int w, int h, int *newW, int *newH) {
1002
const u64 rangeKey = ((u64)addr << 32) | ((u64)w << 16) | h;
1003
auto range = hashranges_.find(rangeKey);
1004
if (range != hashranges_.end()) {
1005
const WidthHeightPair &wh = range->second;
1006
*newW = wh.first;
1007
*newH = wh.second;
1008
return true;
1009
} else {
1010
*newW = w;
1011
*newH = h;
1012
return false;
1013
}
1014
}
1015
1016
float TextureReplacer::LookupReduceHashRange(int w, int h) {
1017
const u64 reducerangeKey = ((u64)w << 16) | h;
1018
auto range = reducehashranges_.find(reducerangeKey);
1019
if (range != reducehashranges_.end()) {
1020
float rhv = range->second;
1021
return rhv;
1022
}
1023
else {
1024
return reduceHashGlobalValue;
1025
}
1026
}
1027
1028
bool TextureReplacer::IniExists(const std::string &gameID) {
1029
if (gameID.empty())
1030
return false;
1031
1032
Path texturesDirectory = GetSysDirectory(DIRECTORY_TEXTURES) / gameID;
1033
Path generatedFilename = texturesDirectory / INI_FILENAME;
1034
return File::Exists(generatedFilename);
1035
}
1036
1037
bool TextureReplacer::GenerateIni(const std::string &gameID, Path &generatedFilename) {
1038
if (gameID.empty())
1039
return false;
1040
1041
Path texturesDirectory = GetSysDirectory(DIRECTORY_TEXTURES) / gameID;
1042
if (!File::Exists(texturesDirectory)) {
1043
File::CreateFullPath(texturesDirectory);
1044
}
1045
1046
generatedFilename = texturesDirectory / INI_FILENAME;
1047
if (File::Exists(generatedFilename))
1048
return true;
1049
1050
FILE *f = File::OpenCFile(generatedFilename, "wb");
1051
if (f) {
1052
// Unicode byte order mark
1053
fwrite("\xEF\xBB\xBF", 1, 3, f);
1054
1055
// Let's also write some defaults.
1056
fprintf(f, R"(# This describes your textures and set up options for texture replacement.
1057
# Documentation about the options and syntax is available here:
1058
# https://www.ppsspp.org/docs/reference/texture-replacement
1059
1060
[options]
1061
version = 1
1062
hash = quick # options available: "quick", "xxh32" - more accurate, but slower, "xxh64" - more accurate and quite fast, but slower than xxh32 on 32 bit cpu's
1063
ignoreMipmap = true # Usually, can just generate them with basisu, no need to dump.
1064
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
1065
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.
1066
1067
[games]
1068
# Used to make it easier to install, and override settings for other regions.
1069
# Files still have to be copied to each TEXTURES folder.
1070
%s = %s
1071
1072
[hashes]
1073
# Use / for folders not \\, avoid special characters, and stick to lowercase.
1074
# See wiki for more info.
1075
1076
[hashranges]
1077
# 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.
1078
# Example: 08b31020,512,512 = 480,272
1079
# Example: 0x08b31020,512,512 = 480,272
1080
1081
[filtering]
1082
# You can enforce specific filtering modes with this. Available modes are linear, nearest, auto. See the docs.
1083
# Example: 08d3961000000909ba70b2af = nearest
1084
1085
[reducehashranges]
1086
# Lets you set texture sizes where the hash range is reduced by a factor. See the docs.
1087
# Example:
1088
512,512=0.5
1089
1090
)", gameID.c_str(), INI_FILENAME.c_str());
1091
fclose(f);
1092
}
1093
return File::Exists(generatedFilename);
1094
}
1095
1096