mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-28 00:35:34 +00:00
a2b45a4b82
People who make texture packs usually release them using a specific ID (for instance SX4E01). Users who have a different version of the game (like the PAL version SX4P01) then need to rename the custom texture folder to match. This is a lot simpler than renaming every texture file, as was required with the old texture format, but it's still something that users can forget to do. To make that unnecessary, this change makes it possible to use three-character region-free IDs for custom texture folders, similarly to how game INIs can use three-character IDs. Once most people have updated to Dolphin versions that include this change, those who make texture packs will be able to name them with three-character IDs, removing the need for users to rename anything.
450 lines
12 KiB
C++
450 lines
12 KiB
C++
// Copyright 2009 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
#include <algorithm>
|
|
#include <cinttypes>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <xxhash.h>
|
|
#include <SOIL/SOIL.h>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/FileSearch.h"
|
|
#include "Common/FileUtil.h"
|
|
#include "Common/Flag.h"
|
|
#include "Common/Hash.h"
|
|
#include "Common/MemoryUtil.h"
|
|
#include "Common/StringUtil.h"
|
|
#include "Common/Thread.h"
|
|
#include "Common/Timer.h"
|
|
|
|
#include "Core/ConfigManager.h"
|
|
|
|
#include "VideoCommon/HiresTextures.h"
|
|
#include "VideoCommon/OnScreenDisplay.h"
|
|
#include "VideoCommon/VideoConfig.h"
|
|
|
|
static std::unordered_map<std::string, std::string> s_textureMap;
|
|
static std::unordered_map<std::string, std::shared_ptr<HiresTexture>> s_textureCache;
|
|
static std::mutex s_textureCacheMutex;
|
|
static std::mutex s_textureCacheAquireMutex; // for high priority access
|
|
static Common::Flag s_textureCacheAbortLoading;
|
|
static bool s_check_native_format;
|
|
static bool s_check_new_format;
|
|
|
|
static std::thread s_prefetcher;
|
|
|
|
static const std::string s_format_prefix = "tex1_";
|
|
|
|
HiresTexture::Level::Level()
|
|
: data(nullptr, SOIL_free_image_data)
|
|
{
|
|
}
|
|
|
|
void HiresTexture::Init()
|
|
{
|
|
s_check_native_format = false;
|
|
s_check_new_format = false;
|
|
|
|
Update();
|
|
}
|
|
|
|
void HiresTexture::Shutdown()
|
|
{
|
|
if (s_prefetcher.joinable())
|
|
{
|
|
s_textureCacheAbortLoading.Set();
|
|
s_prefetcher.join();
|
|
}
|
|
|
|
s_textureMap.clear();
|
|
s_textureCache.clear();
|
|
}
|
|
|
|
void HiresTexture::Update()
|
|
{
|
|
if (s_prefetcher.joinable())
|
|
{
|
|
s_textureCacheAbortLoading.Set();
|
|
s_prefetcher.join();
|
|
}
|
|
|
|
if (!g_ActiveConfig.bHiresTextures)
|
|
{
|
|
s_textureMap.clear();
|
|
s_textureCache.clear();
|
|
return;
|
|
}
|
|
|
|
if (!g_ActiveConfig.bCacheHiresTextures)
|
|
{
|
|
s_textureCache.clear();
|
|
}
|
|
|
|
const std::string& game_id = SConfig::GetInstance().m_strUniqueID;
|
|
std::string texture_directory = GetTextureFolder(game_id);
|
|
|
|
// If there's no directory with the region-specific ID, look for a 3-character region-free one
|
|
if (!File::Exists(texture_directory))
|
|
texture_directory = GetTextureFolder(game_id.substr(0, 3));
|
|
|
|
std::vector<std::string> extensions {
|
|
".png",
|
|
".bmp",
|
|
".tga",
|
|
".dds",
|
|
".jpg" // Why not? Could be useful for large photo-like textures
|
|
};
|
|
|
|
std::vector<std::string> filenames = DoFileSearch(extensions, {texture_directory}, /*recursive*/ true);
|
|
|
|
const std::string code = game_id + "_";
|
|
|
|
for (auto& rFilename : filenames)
|
|
{
|
|
std::string FileName;
|
|
SplitPath(rFilename, nullptr, &FileName, nullptr);
|
|
|
|
if (FileName.substr(0, code.length()) == code)
|
|
{
|
|
s_textureMap[FileName] = rFilename;
|
|
s_check_native_format = true;
|
|
}
|
|
|
|
if (FileName.substr(0, s_format_prefix.length()) == s_format_prefix)
|
|
{
|
|
s_textureMap[FileName] = rFilename;
|
|
s_check_new_format = true;
|
|
}
|
|
}
|
|
|
|
if (g_ActiveConfig.bCacheHiresTextures)
|
|
{
|
|
// remove cached but deleted textures
|
|
auto iter = s_textureCache.begin();
|
|
while (iter != s_textureCache.end())
|
|
{
|
|
if (s_textureMap.find(iter->first) == s_textureMap.end())
|
|
{
|
|
iter = s_textureCache.erase(iter);
|
|
}
|
|
else
|
|
{
|
|
iter++;
|
|
}
|
|
}
|
|
|
|
s_textureCacheAbortLoading.Clear();
|
|
s_prefetcher = std::thread(Prefetch);
|
|
}
|
|
}
|
|
|
|
void HiresTexture::Prefetch()
|
|
{
|
|
Common::SetCurrentThreadName("Prefetcher");
|
|
|
|
size_t size_sum = 0;
|
|
size_t sys_mem = MemPhysical();
|
|
size_t recommended_min_mem = 2 * size_t(1024 * 1024 * 1024);
|
|
// keep 2GB memory for system stability if system RAM is 4GB+ - use half of memory in other cases
|
|
size_t max_mem = (sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem);
|
|
u32 starttime = Common::Timer::GetTimeMs();
|
|
for (const auto& entry : s_textureMap)
|
|
{
|
|
const std::string& base_filename = entry.first;
|
|
|
|
if (base_filename.find("_mip") == std::string::npos)
|
|
{
|
|
{
|
|
// try to get this mutex first, so the video thread is allow to get the real mutex faster
|
|
std::unique_lock<std::mutex> lk(s_textureCacheAquireMutex);
|
|
}
|
|
std::unique_lock<std::mutex> lk(s_textureCacheMutex);
|
|
|
|
auto iter = s_textureCache.find(base_filename);
|
|
if (iter == s_textureCache.end())
|
|
{
|
|
// unlock while loading a texture. This may result in a race condition where we'll load a texture twice,
|
|
// but it reduces the stuttering a lot. Notice: The loading library _must_ be thread safe now.
|
|
// But bad luck, SOIL isn't, so TODO: remove SOIL usage here and use libpng directly
|
|
// Also TODO: remove s_textureCacheAquireMutex afterwards. It won't be needed as the main mutex will be locked rarely
|
|
//lk.unlock();
|
|
std::unique_ptr<HiresTexture> texture = Load(base_filename, 0, 0);
|
|
//lk.lock();
|
|
if (texture)
|
|
{
|
|
std::shared_ptr<HiresTexture> ptr(std::move(texture));
|
|
iter = s_textureCache.insert(iter, std::make_pair(base_filename, ptr));
|
|
}
|
|
}
|
|
if (iter != s_textureCache.end())
|
|
{
|
|
for (const Level& l : iter->second->m_levels)
|
|
{
|
|
size_sum += l.data_size;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (s_textureCacheAbortLoading.IsSet())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (size_sum > max_mem)
|
|
{
|
|
g_Config.bCacheHiresTextures = false;
|
|
|
|
OSD::AddMessage(StringFromFormat("Custom Textures prefetching after %.1f MB aborted, not enough RAM available", size_sum / (1024.0 * 1024.0)), 10000);
|
|
return;
|
|
}
|
|
}
|
|
u32 stoptime = Common::Timer::GetTimeMs();
|
|
OSD::AddMessage(StringFromFormat("Custom Textures loaded, %.1f MB in %.1f s", size_sum / (1024.0 * 1024.0), (stoptime - starttime) / 1000.0), 10000);
|
|
}
|
|
|
|
std::string HiresTexture::GenBaseName(const u8* texture, size_t texture_size, const u8* tlut, size_t tlut_size, u32 width, u32 height, int format, bool has_mipmaps, bool dump)
|
|
{
|
|
std::string name = "";
|
|
bool convert = false;
|
|
if (!dump && s_check_native_format)
|
|
{
|
|
// try to load the old format first
|
|
u64 tex_hash = GetHashHiresTexture(texture, (int)texture_size, g_ActiveConfig.iSafeTextureCache_ColorSamples);
|
|
u64 tlut_hash = tlut_size ? GetHashHiresTexture(tlut, (int)tlut_size, g_ActiveConfig.iSafeTextureCache_ColorSamples) : 0;
|
|
name = StringFromFormat("%s_%08x_%i", SConfig::GetInstance().m_strUniqueID.c_str(), (u32)(tex_hash ^ tlut_hash), (u16)format);
|
|
if (s_textureMap.find(name) != s_textureMap.end())
|
|
{
|
|
if (g_ActiveConfig.bConvertHiresTextures)
|
|
convert = true;
|
|
else
|
|
return name;
|
|
}
|
|
}
|
|
|
|
if (dump || s_check_new_format || convert)
|
|
{
|
|
// checking for min/max on paletted textures
|
|
u32 min = 0xffff;
|
|
u32 max = 0;
|
|
switch(tlut_size)
|
|
{
|
|
case 0: break;
|
|
case 16 * 2:
|
|
for (size_t i = 0; i < texture_size; i++)
|
|
{
|
|
min = std::min<u32>(min, texture[i] & 0xf);
|
|
min = std::min<u32>(min, texture[i] >> 4);
|
|
max = std::max<u32>(max, texture[i] & 0xf);
|
|
max = std::max<u32>(max, texture[i] >> 4);
|
|
}
|
|
break;
|
|
case 256 * 2:
|
|
for (size_t i = 0; i < texture_size; i++)
|
|
{
|
|
min = std::min<u32>(min, texture[i]);
|
|
max = std::max<u32>(max, texture[i]);
|
|
}
|
|
break;
|
|
case 16384 * 2:
|
|
for (size_t i = 0; i < texture_size/2; i++)
|
|
{
|
|
min = std::min<u32>(min, Common::swap16(((u16*)texture)[i]) & 0x3fff);
|
|
max = std::max<u32>(max, Common::swap16(((u16*)texture)[i]) & 0x3fff);
|
|
}
|
|
break;
|
|
}
|
|
if (tlut_size > 0)
|
|
{
|
|
tlut_size = 2 * (max + 1 - min);
|
|
tlut += 2 * min;
|
|
}
|
|
|
|
u64 tex_hash = XXH64(texture, texture_size, 0);
|
|
u64 tlut_hash = tlut_size ? XXH64(tlut, tlut_size, 0) : 0;
|
|
|
|
std::string basename = s_format_prefix + StringFromFormat("%dx%d%s_%016" PRIx64, width, height, has_mipmaps ? "_m" : "", tex_hash);
|
|
std::string tlutname = tlut_size ? StringFromFormat("_%016" PRIx64, tlut_hash) : "";
|
|
std::string formatname = StringFromFormat("_%d", format);
|
|
std::string fullname = basename + tlutname + formatname;
|
|
|
|
for (int level = 0; level < 10 && convert; level++)
|
|
{
|
|
std::string oldname = name;
|
|
if (level)
|
|
oldname += StringFromFormat("_mip%d", level);
|
|
|
|
// skip not existing levels
|
|
if (s_textureMap.find(oldname) == s_textureMap.end())
|
|
continue;
|
|
|
|
for (int i = 0;; i++)
|
|
{
|
|
// for hash collisions, padd with an integer
|
|
std::string newname = fullname;
|
|
if (level)
|
|
newname += StringFromFormat("_mip%d", level);
|
|
if (i)
|
|
newname += StringFromFormat(".%d", i);
|
|
|
|
// new texture
|
|
if (s_textureMap.find(newname) == s_textureMap.end())
|
|
{
|
|
std::string src = s_textureMap[oldname];
|
|
size_t postfix = src.find_last_of('.');
|
|
std::string dst = src.substr(0, postfix - oldname.length()) + newname + src.substr(postfix, src.length() - postfix);
|
|
if (File::Rename(src, dst))
|
|
{
|
|
s_textureMap.erase(oldname);
|
|
s_textureMap[newname] = dst;
|
|
s_check_new_format = true;
|
|
OSD::AddMessage(StringFromFormat("Rename custom texture %s to %s", oldname.c_str(), newname.c_str()), 5000);
|
|
}
|
|
else
|
|
{
|
|
ERROR_LOG(VIDEO, "rename failed");
|
|
}
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// dst fail already exist, compare content
|
|
std::string a, b;
|
|
File::ReadFileToString(s_textureMap[oldname], a);
|
|
File::ReadFileToString(s_textureMap[newname], b);
|
|
|
|
if (a == b && a != "")
|
|
{
|
|
// equal, so remove
|
|
if (File::Delete(s_textureMap[oldname]))
|
|
{
|
|
s_textureMap.erase(oldname);
|
|
OSD::AddMessage(StringFromFormat("Delete double old custom texture %s", oldname.c_str()), 5000);
|
|
}
|
|
else
|
|
{
|
|
ERROR_LOG(VIDEO, "delete failed");
|
|
}
|
|
break;
|
|
}
|
|
|
|
// else continue in this loop with the next higher padding variable
|
|
}
|
|
}
|
|
}
|
|
|
|
// try to match a wildcard template
|
|
if (!dump && s_textureMap.find(basename + "_*" + formatname) != s_textureMap.end())
|
|
return basename + "_*" + formatname;
|
|
|
|
// else generate the complete texture
|
|
if (dump || s_textureMap.find(fullname) != s_textureMap.end())
|
|
return fullname;
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
std::shared_ptr<HiresTexture> HiresTexture::Search(const u8* texture, size_t texture_size, const u8* tlut, size_t tlut_size, u32 width, u32 height, int format, bool has_mipmaps)
|
|
{
|
|
std::string base_filename = GenBaseName(texture, texture_size, tlut, tlut_size, width, height, format, has_mipmaps);
|
|
|
|
std::lock_guard<std::mutex> lk2(s_textureCacheAquireMutex);
|
|
std::lock_guard<std::mutex> lk(s_textureCacheMutex);
|
|
|
|
auto iter = s_textureCache.find(base_filename);
|
|
if (iter != s_textureCache.end())
|
|
{
|
|
return iter->second;
|
|
}
|
|
|
|
std::shared_ptr<HiresTexture> ptr(Load(base_filename, width, height));
|
|
|
|
if (ptr && g_ActiveConfig.bCacheHiresTextures)
|
|
{
|
|
s_textureCache[base_filename] = ptr;
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
std::unique_ptr<HiresTexture> HiresTexture::Load(const std::string& base_filename, u32 width, u32 height)
|
|
{
|
|
std::unique_ptr<HiresTexture> ret;
|
|
for (int level = 0;; level++)
|
|
{
|
|
std::string filename = base_filename;
|
|
if (level)
|
|
{
|
|
filename += StringFromFormat("_mip%u", level);
|
|
}
|
|
|
|
if (s_textureMap.find(filename) != s_textureMap.end())
|
|
{
|
|
Level l;
|
|
|
|
File::IOFile file;
|
|
file.Open(s_textureMap[filename], "rb");
|
|
std::vector<u8> buffer(file.GetSize());
|
|
file.ReadBytes(buffer.data(), file.GetSize());
|
|
|
|
int channels;
|
|
l.data = SOILPointer(SOIL_load_image_from_memory(buffer.data(), (int)buffer.size(), (int*)&l.width, (int*)&l.height, &channels, SOIL_LOAD_RGBA), SOIL_free_image_data);
|
|
l.data_size = (size_t)l.width * l.height * 4;
|
|
|
|
if (l.data == nullptr)
|
|
{
|
|
ERROR_LOG(VIDEO, "Custom texture %s failed to load", filename.c_str());
|
|
break;
|
|
}
|
|
|
|
if (!level)
|
|
{
|
|
if (l.width * height != l.height * width)
|
|
ERROR_LOG(VIDEO, "Invalid custom texture size %dx%d for texture %s. The aspect differs from the native size %dx%d.",
|
|
l.width, l.height, filename.c_str(), width, height);
|
|
if (width && height && (l.width % width || l.height % height))
|
|
WARN_LOG(VIDEO, "Invalid custom texture size %dx%d for texture %s. Please use an integer upscaling factor based on the native size %dx%d.",
|
|
l.width, l.height, filename.c_str(), width, height);
|
|
width = l.width;
|
|
height = l.height;
|
|
}
|
|
else if (width != l.width || height != l.height)
|
|
{
|
|
ERROR_LOG(VIDEO, "Invalid custom texture size %dx%d for texture %s. This mipmap layer _must_ be %dx%d.",
|
|
l.width, l.height, filename.c_str(), width, height);
|
|
l.data.reset();
|
|
break;
|
|
}
|
|
|
|
// calculate the size of the next mipmap
|
|
width >>= 1;
|
|
height >>= 1;
|
|
|
|
if (!ret)
|
|
ret = std::unique_ptr<HiresTexture>(new HiresTexture);
|
|
ret->m_levels.push_back(std::move(l));
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string HiresTexture::GetTextureFolder(const std::string& game_id)
|
|
{
|
|
return File::GetUserPath(D_HIRESTEXTURES_IDX) + game_id;
|
|
}
|
|
|
|
HiresTexture::~HiresTexture()
|
|
{
|
|
}
|