dolphin/Source/Core/VideoCommon/HiresTextures.cpp
JosJuice a2b45a4b82 HiresTextures: Also look for directories with 3-character IDs
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.
2016-01-06 14:33:36 +01:00

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()
{
}