628 lines
17 KiB
C++
628 lines
17 KiB
C++
/* Copyright (C) 2010 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "TextureManager.h"
|
|
|
|
#include "graphics/TextureConverter.h"
|
|
#include "lib/allocators/shared_ptr.h"
|
|
#include "lib/res/h_mgr.h"
|
|
#include "lib/file/vfs/vfs_tree.h"
|
|
#include "lib/res/graphics/ogl_tex.h"
|
|
#include "lib/timer.h"
|
|
#include "maths/MD5.h"
|
|
#include "ps/CacheLoader.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/Filesystem.h"
|
|
|
|
#include <iomanip>
|
|
#include <boost/unordered_map.hpp>
|
|
#include <boost/unordered_set.hpp>
|
|
#include <boost/functional/hash.hpp>
|
|
|
|
struct TPhash
|
|
: std::unary_function<CTextureProperties, std::size_t>,
|
|
std::unary_function<CTexturePtr, std::size_t>
|
|
{
|
|
std::size_t operator()(CTextureProperties const& a) const
|
|
{
|
|
std::size_t seed = 0;
|
|
boost::hash_combine(seed, a.m_Path.string());
|
|
boost::hash_combine(seed, a.m_Filter);
|
|
boost::hash_combine(seed, a.m_Wrap);
|
|
boost::hash_combine(seed, a.m_Aniso);
|
|
return seed;
|
|
}
|
|
std::size_t operator()(CTexturePtr const& a) const
|
|
{
|
|
return (*this)(a->m_Properties);
|
|
}
|
|
};
|
|
struct TPequal_to
|
|
: std::binary_function<CTextureProperties, CTextureProperties, bool>,
|
|
std::binary_function<CTexturePtr, CTexturePtr, bool>
|
|
{
|
|
bool operator()(CTextureProperties const& a, CTextureProperties const& b) const
|
|
{
|
|
return a.m_Path == b.m_Path && a.m_Filter == b.m_Filter
|
|
&& a.m_Wrap == b.m_Wrap && a.m_Aniso == b.m_Aniso;
|
|
}
|
|
bool operator()(CTexturePtr const& a, CTexturePtr const& b) const
|
|
{
|
|
return (*this)(a->m_Properties, b->m_Properties);
|
|
}
|
|
};
|
|
|
|
|
|
class CTextureManagerImpl
|
|
{
|
|
friend class CTexture;
|
|
public:
|
|
CTextureManagerImpl(PIVFS vfs, bool highQuality, bool disableGL) :
|
|
m_VFS(vfs), m_CacheLoader(vfs, L".dds"), m_DisableGL(disableGL), m_TextureConverter(vfs, highQuality),
|
|
m_DefaultHandle(0), m_ErrorHandle(0)
|
|
{
|
|
// Initialise some textures that will always be available,
|
|
// without needing to load any files
|
|
|
|
// Default placeholder texture (grey)
|
|
if (!m_DisableGL)
|
|
{
|
|
// Construct 1x1 24-bit texture
|
|
shared_ptr<u8> data(new u8[3], ArrayDeleter());
|
|
data.get()[0] = 64;
|
|
data.get()[1] = 64;
|
|
data.get()[2] = 64;
|
|
Tex t;
|
|
(void)tex_wrap(1, 1, 24, 0, data, 0, &t);
|
|
|
|
m_DefaultHandle = ogl_tex_wrap(&t, m_VFS, L"(default texture)");
|
|
(void)ogl_tex_set_filter(m_DefaultHandle, GL_LINEAR);
|
|
if (!m_DisableGL)
|
|
(void)ogl_tex_upload(m_DefaultHandle);
|
|
}
|
|
|
|
// Error texture (magenta)
|
|
if (!m_DisableGL)
|
|
{
|
|
// Construct 1x1 24-bit texture
|
|
shared_ptr<u8> data(new u8[3], ArrayDeleter());
|
|
data.get()[0] = 255;
|
|
data.get()[1] = 0;
|
|
data.get()[2] = 255;
|
|
Tex t;
|
|
(void)tex_wrap(1, 1, 24, 0, data, 0, &t);
|
|
|
|
m_ErrorHandle = ogl_tex_wrap(&t, m_VFS, L"(error texture)");
|
|
(void)ogl_tex_set_filter(m_ErrorHandle, GL_LINEAR);
|
|
if (!m_DisableGL)
|
|
(void)ogl_tex_upload(m_ErrorHandle);
|
|
|
|
// Construct a CTexture to return to callers who want an error texture
|
|
CTextureProperties props(L"(error texture)");
|
|
m_ErrorTexture = CTexturePtr(new CTexture(m_ErrorHandle, props, this));
|
|
m_ErrorTexture->m_State = CTexture::LOADED;
|
|
m_ErrorTexture->m_Self = m_ErrorTexture;
|
|
}
|
|
|
|
// Allow hotloading of textures
|
|
RegisterFileReloadFunc(ReloadChangedFileCB, this);
|
|
}
|
|
|
|
~CTextureManagerImpl()
|
|
{
|
|
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
|
|
|
|
(void)ogl_tex_free(m_DefaultHandle);
|
|
(void)ogl_tex_free(m_ErrorHandle);
|
|
}
|
|
|
|
CTexturePtr GetErrorTexture()
|
|
{
|
|
return m_ErrorTexture;
|
|
}
|
|
|
|
/**
|
|
* See CTextureManager::CreateTexture
|
|
*/
|
|
CTexturePtr CreateTexture(const CTextureProperties& props)
|
|
{
|
|
// Construct a new default texture with the given properties to use as the search key
|
|
CTexturePtr texture(new CTexture(m_DefaultHandle, props, this));
|
|
|
|
// Try to find an existing texture with the given properties
|
|
TextureCache::iterator it = m_TextureCache.find(texture);
|
|
if (it != m_TextureCache.end())
|
|
return *it;
|
|
|
|
// Can't find an existing texture - finish setting up this new texture
|
|
texture->m_Self = texture;
|
|
m_TextureCache.insert(texture);
|
|
m_HotloadFiles[props.m_Path].insert(texture);
|
|
|
|
return texture;
|
|
}
|
|
|
|
/**
|
|
* Load the given file into the texture object and upload it to OpenGL.
|
|
* Assumes the file already exists.
|
|
*/
|
|
void LoadTexture(const CTexturePtr& texture, const VfsPath& path)
|
|
{
|
|
if (m_DisableGL)
|
|
return;
|
|
|
|
Handle h = ogl_tex_load(m_VFS, path, RES_UNIQUE);
|
|
if (h <= 0)
|
|
{
|
|
LOGERROR(L"Texture failed to load; \"%ls\"", texture->m_Properties.m_Path.string().c_str());
|
|
|
|
// Replace with error texture to make it obvious
|
|
texture->SetHandle(m_ErrorHandle);
|
|
return;
|
|
}
|
|
|
|
// Get some flags for later use
|
|
size_t flags = 0;
|
|
(void)ogl_tex_get_format(h, &flags, NULL);
|
|
|
|
// Initialise base colour from the texture
|
|
(void)ogl_tex_get_average_colour(h, &texture->m_BaseColour);
|
|
|
|
// Set GL upload properties
|
|
(void)ogl_tex_set_wrap(h, texture->m_Properties.m_Wrap);
|
|
(void)ogl_tex_set_anisotropy(h, texture->m_Properties.m_Aniso);
|
|
|
|
// Prevent ogl_tex automatically generating mipmaps (which is slow and unwanted),
|
|
// by avoiding mipmapped filters unless the source texture already has mipmaps
|
|
GLint filter = texture->m_Properties.m_Filter;
|
|
if (!(flags & TEX_MIPMAPS))
|
|
{
|
|
switch (filter)
|
|
{
|
|
case GL_NEAREST_MIPMAP_NEAREST:
|
|
case GL_NEAREST_MIPMAP_LINEAR:
|
|
filter = GL_NEAREST;
|
|
break;
|
|
case GL_LINEAR_MIPMAP_NEAREST:
|
|
case GL_LINEAR_MIPMAP_LINEAR:
|
|
filter = GL_LINEAR;
|
|
break;
|
|
}
|
|
}
|
|
(void)ogl_tex_set_filter(h, filter);
|
|
|
|
// Upload to GL
|
|
if (!m_DisableGL && ogl_tex_upload(h) < 0)
|
|
{
|
|
LOGERROR(L"Texture failed to upload: \"%ls\"", texture->m_Properties.m_Path.string().c_str());
|
|
|
|
ogl_tex_free(h);
|
|
|
|
// Replace with error texture to make it obvious
|
|
texture->SetHandle(m_ErrorHandle);
|
|
return;
|
|
}
|
|
|
|
// Let the texture object take ownership of this handle
|
|
texture->SetHandle(h, true);
|
|
}
|
|
|
|
/**
|
|
* Set up some parameters for the loose cache filename code.
|
|
*/
|
|
void PrepareCacheKey(const CTexturePtr& texture, MD5& hash, u32& version)
|
|
{
|
|
// Hash the settings, so we won't use an old loose cache file if the
|
|
// settings have changed
|
|
CTextureConverter::Settings settings = GetConverterSettings(texture);
|
|
settings.Hash(hash);
|
|
|
|
// Arbitrary version number - change this if we update the code and
|
|
// need to invalidate old users' caches
|
|
version = 1;
|
|
}
|
|
|
|
/**
|
|
* Attempts to load a cached version of a texture.
|
|
* If the texture is loaded (or there was an error), returns true.
|
|
* Otherwise, returns false to indicate the caller should generate the cached version.
|
|
*/
|
|
bool TryLoadingCached(const CTexturePtr& texture)
|
|
{
|
|
MD5 hash;
|
|
u32 version;
|
|
PrepareCacheKey(texture, hash, version);
|
|
|
|
VfsPath loadPath;
|
|
LibError ret = m_CacheLoader.TryLoadingCached(texture->m_Properties.m_Path, hash, version, loadPath);
|
|
|
|
if (ret == INFO::OK)
|
|
{
|
|
// Found a cached texture - load it
|
|
LoadTexture(texture, loadPath);
|
|
return true;
|
|
}
|
|
else if (ret == INFO::SKIPPED)
|
|
{
|
|
// No cached version was found - we'll need to create it
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
debug_assert(ret < 0);
|
|
|
|
// No source file or archive cache was found, so we can't load the
|
|
// real texture at all - return the error texture instead
|
|
texture->SetHandle(m_ErrorHandle);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiates an asynchronous conversion process, from the texture's
|
|
* source file to the corresponding loose cache file.
|
|
*/
|
|
void ConvertTexture(const CTexturePtr& texture)
|
|
{
|
|
VfsPath sourcePath = texture->m_Properties.m_Path;
|
|
|
|
MD5 hash;
|
|
u32 version;
|
|
PrepareCacheKey(texture, hash, version);
|
|
VfsPath looseCachePath = m_CacheLoader.LooseCachePath(sourcePath, hash, version);
|
|
|
|
// LOGWARNING(L"Converting texture \"%ls\"", srcPath.string().c_str());
|
|
|
|
CTextureConverter::Settings settings = GetConverterSettings(texture);
|
|
|
|
m_TextureConverter.ConvertTexture(texture, sourcePath, looseCachePath, settings);
|
|
}
|
|
|
|
bool GenerateCachedTexture(const VfsPath& sourcePath, VfsPath& archiveCachePath)
|
|
{
|
|
archiveCachePath = m_CacheLoader.ArchiveCachePath(sourcePath);
|
|
|
|
CTextureProperties textureProps(sourcePath);
|
|
CTexturePtr texture = CreateTexture(textureProps);
|
|
CTextureConverter::Settings settings = GetConverterSettings(texture);
|
|
|
|
if (!m_TextureConverter.ConvertTexture(texture, sourcePath, L"cache"/archiveCachePath, settings))
|
|
return false;
|
|
|
|
while (true)
|
|
{
|
|
CTexturePtr textureOut;
|
|
VfsPath dest;
|
|
bool ok;
|
|
if (m_TextureConverter.Poll(textureOut, dest, ok))
|
|
return ok;
|
|
|
|
// Spin-loop is dumb but it works okay for now
|
|
SDL_Delay(0);
|
|
}
|
|
}
|
|
|
|
bool MakeProgress()
|
|
{
|
|
// Process any completed conversion tasks
|
|
{
|
|
CTexturePtr texture;
|
|
VfsPath dest;
|
|
bool ok;
|
|
if (m_TextureConverter.Poll(texture, dest, ok))
|
|
{
|
|
if (ok)
|
|
{
|
|
LoadTexture(texture, dest);
|
|
}
|
|
else
|
|
{
|
|
LOGERROR(L"Texture failed to convert: \"%ls\"", texture->m_Properties.m_Path.string().c_str());
|
|
texture->SetHandle(m_ErrorHandle);
|
|
}
|
|
texture->m_State = CTexture::LOADED;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// We'll only push new conversion requests if it's not already busy
|
|
bool converterBusy = m_TextureConverter.IsBusy();
|
|
|
|
if (!converterBusy)
|
|
{
|
|
// Look for all high-priority textures needing conversion.
|
|
// (Iterating over all textures isn't optimally efficient, but it
|
|
// doesn't seem to be a problem yet and it's simpler than maintaining
|
|
// multiple queues.)
|
|
for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
{
|
|
if ((*it)->m_State == CTexture::HIGH_NEEDS_CONVERTING)
|
|
{
|
|
// Start converting this texture
|
|
(*it)->m_State = CTexture::HIGH_IS_CONVERTING;
|
|
ConvertTexture(*it);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try loading prefetched textures from their cache
|
|
for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
{
|
|
if ((*it)->m_State == CTexture::PREFETCH_NEEDS_LOADING)
|
|
{
|
|
if (TryLoadingCached(*it))
|
|
{
|
|
(*it)->m_State = CTexture::LOADED;
|
|
}
|
|
else
|
|
{
|
|
(*it)->m_State = CTexture::PREFETCH_NEEDS_CONVERTING;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If we've got nothing better to do, then start converting prefetched textures.
|
|
if (!converterBusy)
|
|
{
|
|
for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it)
|
|
{
|
|
if ((*it)->m_State == CTexture::PREFETCH_NEEDS_CONVERTING)
|
|
{
|
|
(*it)->m_State = CTexture::PREFETCH_IS_CONVERTING;
|
|
ConvertTexture(*it);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Compute the conversion settings that apply to a given texture, by combining
|
|
* the textures.xml files from its directory and all parent directories
|
|
* (up to the VFS root).
|
|
*/
|
|
CTextureConverter::Settings GetConverterSettings(const CTexturePtr& texture)
|
|
{
|
|
VfsPath srcPath = texture->m_Properties.m_Path;
|
|
|
|
std::vector<CTextureConverter::SettingsFile*> files;
|
|
VfsPath p;
|
|
for (VfsPath::iterator it = srcPath.begin(); it != srcPath.end(); ++it)
|
|
{
|
|
VfsPath settingsPath = p/L"textures.xml";
|
|
m_HotloadFiles[settingsPath].insert(texture);
|
|
CTextureConverter::SettingsFile* f = GetSettingsFile(settingsPath);
|
|
if (f)
|
|
files.push_back(f);
|
|
p /= *it;
|
|
}
|
|
return m_TextureConverter.ComputeSettings(srcPath.leaf(), files);
|
|
}
|
|
|
|
/**
|
|
* Return the (cached) settings file with the given filename,
|
|
* or NULL if it doesn't exist.
|
|
*/
|
|
CTextureConverter::SettingsFile* GetSettingsFile(const VfsPath& path)
|
|
{
|
|
SettingsFilesMap::iterator it = m_SettingsFiles.find(path);
|
|
if (it != m_SettingsFiles.end())
|
|
return it->second.get();
|
|
|
|
if (m_VFS->GetFileInfo(path, NULL) >= 0)
|
|
{
|
|
shared_ptr<CTextureConverter::SettingsFile> settings(m_TextureConverter.LoadSettings(path));
|
|
m_SettingsFiles.insert(std::make_pair(path, settings));
|
|
return settings.get();
|
|
}
|
|
else
|
|
{
|
|
m_SettingsFiles.insert(std::make_pair(path, shared_ptr<CTextureConverter::SettingsFile>()));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static LibError ReloadChangedFileCB(void* param, const VfsPath& path)
|
|
{
|
|
return static_cast<CTextureManagerImpl*>(param)->ReloadChangedFile(path);
|
|
}
|
|
|
|
LibError ReloadChangedFile(const VfsPath& path)
|
|
{
|
|
// Uncache settings file, if this is one
|
|
m_SettingsFiles.erase(path);
|
|
|
|
// Find all textures using this file
|
|
HotloadFilesMap::iterator files = m_HotloadFiles.find(path);
|
|
if (files != m_HotloadFiles.end())
|
|
{
|
|
// Flag all textures using this file as needing reloading
|
|
for (std::set<boost::weak_ptr<CTexture> >::iterator it = files->second.begin(); it != files->second.end(); ++it)
|
|
{
|
|
if (shared_ptr<CTexture> texture = it->lock())
|
|
{
|
|
texture->m_State = CTexture::UNLOADED;
|
|
texture->SetHandle(m_DefaultHandle);
|
|
}
|
|
}
|
|
}
|
|
|
|
return INFO::OK;
|
|
}
|
|
|
|
private:
|
|
PIVFS m_VFS;
|
|
CCacheLoader m_CacheLoader;
|
|
bool m_DisableGL;
|
|
CTextureConverter m_TextureConverter;
|
|
|
|
Handle m_DefaultHandle;
|
|
Handle m_ErrorHandle;
|
|
CTexturePtr m_ErrorTexture;
|
|
|
|
// Cache of all loaded textures
|
|
typedef boost::unordered_set<CTexturePtr, TPhash, TPequal_to > TextureCache;
|
|
TextureCache m_TextureCache;
|
|
// TODO: we ought to expire unused textures from the cache eventually
|
|
|
|
// Store the set of textures that need to be reloaded when the given file
|
|
// (a source file or settings.xml) is modified
|
|
typedef boost::unordered_map<VfsPath, std::set<boost::weak_ptr<CTexture> > > HotloadFilesMap;
|
|
HotloadFilesMap m_HotloadFiles;
|
|
|
|
// Cache for the conversion settings files
|
|
typedef boost::unordered_map<VfsPath, shared_ptr<CTextureConverter::SettingsFile> > SettingsFilesMap;
|
|
SettingsFilesMap m_SettingsFiles;
|
|
};
|
|
|
|
CTexture::CTexture(Handle handle, const CTextureProperties& props, CTextureManagerImpl* textureManager) :
|
|
m_Handle(handle), m_BaseColour(0), m_State(UNLOADED), m_Properties(props), m_TextureManager(textureManager)
|
|
{
|
|
// Add a reference to the handle (it might be shared by multiple CTextures
|
|
// so we can't take ownership of it)
|
|
if (m_Handle)
|
|
h_add_ref(m_Handle);
|
|
}
|
|
|
|
CTexture::~CTexture()
|
|
{
|
|
if (m_Handle)
|
|
ogl_tex_free(m_Handle);
|
|
}
|
|
|
|
void CTexture::Bind(size_t unit)
|
|
{
|
|
// TODO: TryLoad might call ogl_tex_upload which enables GL_TEXTURE_2D
|
|
// on texture unit 0, regardless of 'unit', which callers might
|
|
// not be expecting. Ideally that wouldn't happen.
|
|
|
|
TryLoad();
|
|
|
|
ogl_tex_bind(m_Handle, unit);
|
|
}
|
|
|
|
bool CTexture::TryLoad()
|
|
{
|
|
// If we haven't started loading, then try loading, and if that fails then request conversion.
|
|
// If we have already tried prefetch loading, and it failed, bump the conversion request to HIGH priority.
|
|
if (m_State == UNLOADED || m_State == PREFETCH_NEEDS_LOADING || m_State == PREFETCH_NEEDS_CONVERTING)
|
|
{
|
|
if (shared_ptr<CTexture> self = m_Self.lock())
|
|
{
|
|
if (m_State != PREFETCH_NEEDS_CONVERTING && m_TextureManager->TryLoadingCached(self))
|
|
m_State = LOADED;
|
|
else
|
|
m_State = HIGH_NEEDS_CONVERTING;
|
|
}
|
|
}
|
|
|
|
return (m_State == LOADED);
|
|
}
|
|
|
|
void CTexture::Prefetch()
|
|
{
|
|
if (m_State == UNLOADED)
|
|
{
|
|
if (shared_ptr<CTexture> self = m_Self.lock())
|
|
{
|
|
m_State = PREFETCH_NEEDS_LOADING;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CTexture::IsLoaded()
|
|
{
|
|
return (m_State == LOADED);
|
|
}
|
|
|
|
void CTexture::SetHandle(Handle handle, bool takeOwnership)
|
|
{
|
|
if (handle == m_Handle)
|
|
return;
|
|
|
|
if (!takeOwnership)
|
|
h_add_ref(handle);
|
|
|
|
ogl_tex_free(m_Handle);
|
|
m_Handle = handle;
|
|
}
|
|
|
|
size_t CTexture::GetWidth() const
|
|
{
|
|
size_t w = 0;
|
|
(void)ogl_tex_get_size(m_Handle, &w, 0, 0);
|
|
return w;
|
|
}
|
|
|
|
size_t CTexture::GetHeight() const
|
|
{
|
|
size_t h = 0;
|
|
(void)ogl_tex_get_size(m_Handle, 0, &h, 0);
|
|
return h;
|
|
}
|
|
|
|
bool CTexture::HasAlpha() const
|
|
{
|
|
size_t flags = 0;
|
|
(void)ogl_tex_get_format(m_Handle, &flags, 0);
|
|
return (flags & TEX_ALPHA) != 0;
|
|
}
|
|
|
|
u32 CTexture::GetBaseColour() const
|
|
{
|
|
return m_BaseColour;
|
|
}
|
|
|
|
|
|
// CTextureManager: forward all calls to impl:
|
|
|
|
CTextureManager::CTextureManager(PIVFS vfs, bool highQuality, bool disableGL) :
|
|
m(new CTextureManagerImpl(vfs, highQuality, disableGL))
|
|
{
|
|
}
|
|
|
|
CTextureManager::~CTextureManager()
|
|
{
|
|
delete m;
|
|
}
|
|
|
|
CTexturePtr CTextureManager::CreateTexture(const CTextureProperties& props)
|
|
{
|
|
return m->CreateTexture(props);
|
|
}
|
|
|
|
CTexturePtr CTextureManager::GetErrorTexture()
|
|
{
|
|
return m->GetErrorTexture();
|
|
}
|
|
|
|
bool CTextureManager::MakeProgress()
|
|
{
|
|
return m->MakeProgress();
|
|
}
|
|
|
|
bool CTextureManager::GenerateCachedTexture(const VfsPath& path, VfsPath& outputPath)
|
|
{
|
|
return m->GenerateCachedTexture(path, outputPath);
|
|
}
|