CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
hrydgard

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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