Pyrogenesis  13997
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
ColladaManager.cpp
Go to the documentation of this file.
1 /* Copyright (C) 2013 Wildfire Games.
2  * This file is part of 0 A.D.
3  *
4  * 0 A.D. is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * 0 A.D. is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "precompiled.h"
19 
20 #include "ColladaManager.h"
21 
22 #include <boost/algorithm/string.hpp>
23 
24 #include "graphics/ModelDef.h"
25 #include "lib/fnv_hash.h"
26 #include "maths/MD5.h"
27 #include "ps/CacheLoader.h"
28 #include "ps/CLogger.h"
29 #include "ps/CStr.h"
30 #include "ps/DllLoader.h"
31 #include "ps/Filesystem.h"
32 
33 namespace Collada
34 {
35  #include "collada/DLL.h"
36 }
37 
38 namespace
39 {
40  void ColladaLog(void* cb_data, int severity, const char* text)
41  {
42  VfsPath* path = static_cast<VfsPath*>(cb_data);
43 
44  if (severity == LOG_INFO)
45  LOGMESSAGE(L"%ls: %hs", path->string().c_str(), text);
46  else if (severity == LOG_WARNING)
47  LOGWARNING(L"%ls: %hs", path->string().c_str(), text);
48  else
49  LOGERROR(L"%ls: %hs", path->string().c_str(), text);
50  }
51 
52  void ColladaOutput(void* cb_data, const char* data, unsigned int length)
53  {
54  WriteBuffer* writeBuffer = static_cast<WriteBuffer*>(cb_data);
55  writeBuffer->Append(data, (size_t)length);
56  }
57 }
58 
60 {
62 
63  void (*set_logger)(Collada::LogFn logger, void* cb_data);
64  int (*set_skeleton_definitions)(const char* xml, int length);
65  int (*convert_dae_to_pmd)(const char* dae, Collada::OutputFn pmd_writer, void* cb_data);
66  int (*convert_dae_to_psa)(const char* dae, Collada::OutputFn psa_writer, void* cb_data);
67 
68 public:
70  : dll("Collada"), m_VFS(vfs), m_skeletonHashInvalidated(false)
71  {
72  // Support hotloading
74  }
75 
77  {
78  if (dll.IsLoaded())
79  set_logger(NULL, NULL); // unregister the log handler
81  }
82 
84  {
85  // Ignore files that aren't in the right path
86  if (!boost::algorithm::starts_with(path.string(), L"art/skeletons/"))
87  return INFO::OK;
88 
89  if (path.Extension() != L".xml")
90  return INFO::OK;
91 
93 
94  // If the file doesn't exist (e.g. it was deleted), don't bother reloading
95  // or 'unloading' since that isn't possible
96  if (!VfsFileExists(path))
97  return INFO::OK;
98 
99  if (!dll.IsLoaded() && !TryLoadDLL())
100  return ERR::FAIL;
101 
102  LOGMESSAGE(L"Hotloading skeleton definitions from '%ls'", path.string().c_str());
103  // Set the filename for the logger to report
104  set_logger(ColladaLog, const_cast<void*>(static_cast<const void*>(&path)));
105 
106  CVFSFile skeletonFile;
107  if (skeletonFile.Load(m_VFS, path) != PSRETURN_OK)
108  {
109  LOGERROR(L"Failed to read skeleton defintions from '%ls'", path.string().c_str());
110  return ERR::FAIL;
111  }
112 
113  int ok = set_skeleton_definitions((const char*)skeletonFile.GetBuffer(), (int)skeletonFile.GetBufferSize());
114  if (ok < 0)
115  {
116  LOGERROR(L"Failed to load skeleton definitions from '%ls'", path.string().c_str());
117  return ERR::FAIL;
118  }
119 
120  return INFO::OK;
121  }
122 
123  static Status ReloadChangedFileCB(void* param, const VfsPath& path)
124  {
125  return static_cast<CColladaManagerImpl*>(param)->ReloadChangedFile(path);
126  }
127 
128  bool Convert(const VfsPath& daeFilename, const VfsPath& pmdFilename, CColladaManager::FileType type)
129  {
130  // To avoid always loading the DLL when it's usually not going to be
131  // used (and to do the same on Linux where delay-loading won't help),
132  // and to avoid compile-time dependencies (because it's a minor pain
133  // to get all the right libraries to build the COLLADA DLL), we load
134  // it dynamically when it is required, instead of using the exported
135  // functions and binding at link-time.
136  if (!dll.IsLoaded())
137  {
138  if (!TryLoadDLL())
139  return false;
140 
142  {
143  dll.Unload(); // Error should have been logged already
144  return false;
145  }
146  }
147 
148  // Set the filename for the logger to report
149  set_logger(ColladaLog, const_cast<void*>(static_cast<const void*>(&daeFilename)));
150 
151  // We need to null-terminate the buffer, so do it (possibly inefficiently)
152  // by converting to a CStr
153  CStr daeData;
154  {
155  CVFSFile daeFile;
156  if (daeFile.Load(m_VFS, daeFilename) != PSRETURN_OK)
157  return false;
158  daeData = daeFile.GetAsString();
159  }
160 
161  // Do the conversion into a memory buffer
162  // We need to check the result, as archive builder needs to know if the source dae
163  // was sucessfully converted to .pmd/psa
164  int result = -1;
165  WriteBuffer writeBuffer;
166  switch (type)
167  {
169  result = convert_dae_to_pmd(daeData.c_str(), ColladaOutput, &writeBuffer);
170  break;
172  result = convert_dae_to_psa(daeData.c_str(), ColladaOutput, &writeBuffer);
173  break;
174  }
175 
176  // don't create zero-length files (as happens in test_invalid_dae when
177  // we deliberately pass invalid XML data) because the VFS caching
178  // logic warns when asked to load such.
179  if (writeBuffer.Size())
180  {
181  Status ret = m_VFS->CreateFile(pmdFilename, writeBuffer.Data(), writeBuffer.Size());
182  ENSURE(ret == INFO::OK);
183  }
184 
185  return (result == 0);
186  }
187 
188  bool TryLoadDLL()
189  {
190  if (!dll.LoadDLL())
191  {
192  LOGERROR(L"Failed to load COLLADA conversion DLL");
193  return false;
194  }
195 
196  try
197  {
198  dll.LoadSymbol("set_logger", set_logger);
199  dll.LoadSymbol("set_skeleton_definitions", set_skeleton_definitions);
200  dll.LoadSymbol("convert_dae_to_pmd", convert_dae_to_pmd);
201  dll.LoadSymbol("convert_dae_to_psa", convert_dae_to_psa);
202  }
203  catch (PSERROR_DllLoader&)
204  {
205  LOGERROR(L"Failed to load symbols from COLLADA conversion DLL");
206  dll.Unload();
207  return false;
208  }
209  return true;
210  }
211 
213  {
214  VfsPaths pathnames;
215  if (vfs::GetPathnames(m_VFS, L"art/skeletons/", L"*.xml", pathnames) < 0)
216  {
217  LOGERROR(L"No skeleton definition files present");
218  return false;
219  }
220 
221  bool loaded = false;
222  for (VfsPaths::const_iterator it = pathnames.begin(); it != pathnames.end(); ++it)
223  {
224  LOGMESSAGE(L"Loading skeleton definitions from '%ls'", it->string().c_str());
225  // Set the filename for the logger to report
226  set_logger(ColladaLog, const_cast<void*>(static_cast<const void*>(&(*it))));
227 
228  CVFSFile skeletonFile;
229  if (skeletonFile.Load(m_VFS, *it) != PSRETURN_OK)
230  {
231  LOGERROR(L"Failed to read skeleton defintions from '%ls'", it->string().c_str());
232  continue;
233  }
234 
235  int ok = set_skeleton_definitions((const char*)skeletonFile.GetBuffer(), (int)skeletonFile.GetBufferSize());
236  if (ok < 0)
237  {
238  LOGERROR(L"Failed to load skeleton definitions from '%ls'", it->string().c_str());
239  continue;
240  }
241 
242  loaded = true;
243  }
244 
245  if (!loaded)
246  LOGERROR(L"Failed to load any skeleton definitions");
247 
248  return loaded;
249  }
250 
251  /**
252  * Creates MD5 hash key from skeletons.xml info and COLLADA converter version,
253  * used to invalidate cached .pmd/psas
254  *
255  * @param[out] hash resulting MD5 hash
256  * @param[out] version version passed to CCacheLoader, used if code change should force
257  * cache invalidation
258  */
259  void PrepareCacheKey(MD5& hash, u32& version)
260  {
261  // Add converter version to the hash
262  version = COLLADA_CONVERTER_VERSION;
263 
264  // Cache the skeleton files hash data
266  {
267  VfsPaths paths;
268  if (vfs::GetPathnames(m_VFS, L"art/skeletons/", L".xml", paths) != INFO::OK)
269  {
270  LOGWARNING(L"Failed to load skeleton definitions");
271  return;
272  }
273 
274  // Sort the paths to not invalidate the cache if mods are mounted in different order
275  // (No need to stable_sort as the VFS gurantees that we have no duplicates)
276  std::sort(paths.begin(), paths.end());
277 
278  // We need two u64s per file
279  m_skeletonHashes.clear();
280  m_skeletonHashes.resize(paths.size()*2);
281 
282  CFileInfo fileInfo;
283  for (VfsPaths::const_iterator it = paths.begin(); it != paths.end(); ++it)
284  {
285  // This will cause an assertion failure if *it doesn't exist,
286  // because fileinfo is not a NULL pointer, which is annoying but that
287  // should never happen, unless there really is a problem
288  if (m_VFS->GetFileInfo(*it, &fileInfo) != INFO::OK)
289  {
290  LOGERROR(L"Failed to stat '%ls' for DAE caching", it->string().c_str());
291  }
292  else
293  {
294  m_skeletonHashes.push_back((u64)fileInfo.MTime() & ~1); //skip lowest bit, since zip and FAT don't preserve it
295  m_skeletonHashes.push_back((u64)fileInfo.Size());
296  }
297  }
298 
299  // Check if we were able to load any skeleton files
300  if (m_skeletonHashes.empty())
301  LOGERROR(L"Failed to stat any skeleton definitions for DAE caching");
302  // We can continue, something else will break if we try loading a skeletal model
303 
305  }
306 
307  for (std::vector<u64>::const_iterator it = m_skeletonHashes.begin(); it != m_skeletonHashes.end(); ++it)
308  hash.Update((const u8*)&(*it), sizeof(*it));
309  }
310 
311 private:
314  std::vector<u64> m_skeletonHashes;
315 };
316 
318 : m(new CColladaManagerImpl(vfs)), m_VFS(vfs)
319 {
320 }
321 
323 {
324  delete m;
325 }
326 
327 VfsPath CColladaManager::GetLoadablePath(const VfsPath& pathnameNoExtension, FileType type)
328 {
329  std::wstring extn;
330  switch (type)
331  {
332  case PMD: extn = L".pmd"; break;
333  case PSA: extn = L".psa"; break;
334  // no other alternatives
335  }
336 
337  /*
338 
339  Algorithm:
340  * Calculate hash of skeletons.xml and converter version.
341  * Use CCacheLoader to check for archived or loose cached .pmd/psa.
342  * If cached version exists:
343  * Return pathname of cached .pmd/psa.
344  * Else, if source .dae for this model exists:
345  * Convert it to cached .pmd/psa.
346  * If converter succeeded:
347  * Return pathname of cached .pmd/psa.
348  * Else, fail (return empty path).
349  * Else, if uncached .pmd/psa exists:
350  * Return pathname of uncached .pmd/psa.
351  * Else, fail (return empty path).
352 
353  Since we use CCacheLoader which automatically hashes file size and mtime,
354  and handles archived files and loose cache, when preparing the cache key
355  we add converter version number (so updates of the converter cause
356  regeneration of the .pmd/psa) and the global skeletons.xml file size and
357  mtime, as modelers frequently change the contents of skeletons.xml and get
358  perplexed if the in-game models haven't updated as expected (we don't know
359  which models were affected by the skeletons.xml change, if any, so we just
360  regenerate all of them)
361 
362  TODO (maybe): The .dae -> .pmd/psa conversion may fail (e.g. if the .dae is
363  invalid or unsupported), but it may take a long time to start the conversion
364  then realise it's not going to work. That will delay the loading of the game
365  every time, which is annoying, so maybe it should cache the error message
366  until the .dae is updated and fixed. (Alternatively, avoid having that many
367  broken .daes in the game.)
368 
369  */
370 
371  // Now we're looking for cached files
372  CCacheLoader cacheLoader(m_VFS, extn);
373  MD5 hash;
374  u32 version;
375  m->PrepareCacheKey(hash, version);
376 
377  VfsPath cachePath;
378  VfsPath sourcePath = pathnameNoExtension.ChangeExtension(L".dae");
379  Status ret = cacheLoader.TryLoadingCached(sourcePath, hash, version, cachePath);
380 
381  if (ret == INFO::OK)
382  // Found a valid cached version
383  return cachePath;
384 
385  // No valid cached version, check if we have a source .dae
386  if (ret != INFO::SKIPPED)
387  {
388  // No valid cached version was found, and no source .dae exists
389  ENSURE(ret < 0);
390 
391  // Check if source (uncached) .pmd/psa exists
392  sourcePath = pathnameNoExtension.ChangeExtension(extn);
393  if (m_VFS->GetFileInfo(sourcePath, NULL) != INFO::OK)
394  {
395  // Broken reference, the caller will need to handle this
396  return L"";
397  }
398  else
399  {
400  return sourcePath;
401  }
402  }
403 
404  // No valid cached version was found - but source .dae exists
405  // We'll try converting it
406 
407  // We have a source .dae and invalid cached version, so regenerate cached version
408  if (! m->Convert(sourcePath, cachePath, type))
409  {
410  // The COLLADA converter failed for some reason, this will need to be handled
411  // by the caller
412  return L"";
413  }
414 
415  return cachePath;
416 }
417 
418 bool CColladaManager::GenerateCachedFile(const VfsPath& sourcePath, FileType type, VfsPath& archiveCachePath)
419 {
420  std::wstring extn;
421  switch (type)
422  {
423  case PMD: extn = L".pmd"; break;
424  case PSA: extn = L".psa"; break;
425  // no other alternatives
426  }
427 
428  CCacheLoader cacheLoader(m_VFS, extn);
429 
430  archiveCachePath = cacheLoader.ArchiveCachePath(sourcePath);
431 
432  return m->Convert(sourcePath, VfsPath("cache") / archiveCachePath, type);
433 }
#define u8
Definition: types.h:39
VfsPath GetLoadablePath(const VfsPath &pathnameNoExtension, FileType type)
Returns the VFS path to a PMD/PSA file for the given source file.
size_t Size() const
Definition: write_buffer.h:42
Path VfsPath
VFS path of the form &quot;(dir/)*file?&quot;.
Definition: vfs_path.h:40
void Append(const void *data, size_t size)
const Status OK
Definition: status.h:386
#define LOGERROR
Definition: CLogger.h:35
void(* set_logger)(Collada::LogFn logger, void *cb_data)
const PSRETURN PSRETURN_OK
Definition: Errors.h:103
Reads a file, then gives read-only access to the contents.
Definition: Filesystem.h:69
void PrepareCacheKey(MD5 &hash, u32 &version)
Creates MD5 hash key from skeletons.xml info and COLLADA converter version, used to invalidate cached...
static Status ReloadChangedFileCB(void *param, const VfsPath &path)
void Unload()
Unload the library, if it has been loaded already.
Definition: DllLoader.cpp:172
shared_ptr< IVFS > PIVFS
Definition: vfs.h:226
#define LOGMESSAGE
Definition: CLogger.h:32
CColladaManagerImpl(const PIVFS &vfs)
int(* convert_dae_to_psa)(const char *dae, Collada::OutputFn psa_writer, void *cb_data)
void ColladaLog(void *cb_data, int severity, const char *text)
tuple xml
Definition: tests.py:119
Status ReloadChangedFile(const VfsPath &path)
void ColladaOutput(void *cb_data, const char *data, unsigned int length)
#define LOGWARNING
Definition: CLogger.h:34
CStr GetAsString() const
Returns contents of file as a string.
Definition: Filesystem.cpp:148
bool IsLoaded() const
Check whether the library has been loaded successfully.
Definition: DllLoader.cpp:147
#define ENSURE(expr)
ensure the expression &lt;expr&gt; evaluates to non-zero.
Definition: debug.h:282
void Update(const u8 *data, size_t len)
Definition: MD5.h:34
MD5 hashing algorithm.
Definition: MD5.h:27
void UnregisterFileReloadFunc(FileReloadFunc func, void *obj)
delete a callback function registered with RegisterFileReloadFunc (removes any with the same func and...
Definition: Filesystem.cpp:44
void RegisterFileReloadFunc(FileReloadFunc func, void *obj)
register a callback function to be called by ReloadChangedFiles
Definition: Filesystem.cpp:39
Definition: path.h:75
VfsPath ArchiveCachePath(const VfsPath &sourcePath)
Return the path of the archive cache for the given source file.
bool GenerateCachedFile(const VfsPath &sourcePath, FileType type, VfsPath &archiveCachePath)
Converts DAE to archive cached .pmd/psa and outputs the resulting path (used by archive builder) ...
shared_ptr< u8 > Data() const
Definition: write_buffer.h:37
const String & string() const
Definition: path.h:123
off_t Size() const
Definition: file_system.h:58
#define LOG_INFO
#define LOG_WARNING
const u8 * GetBuffer() const
Returns buffer of this file as a stream of bytes.
Definition: Filesystem.cpp:138
CColladaManager(const PIVFS &vfs)
i64 Status
Error handling system.
Definition: status.h:171
#define COLLADA_CONVERTER_VERSION
bool LoadDLL()
Attempt to load and initialise the library, if not already.
Definition: DllLoader.cpp:152
#define u64
Definition: types.h:42
void LoadSymbol(const char *name, T &fptr) const
Attempt to load a named symbol from the library.
Definition: DllLoader.h:86
const Status SKIPPED
Definition: status.h:392
#define u32
Definition: types.h:41
Path ChangeExtension(Path extension) const
Definition: path.h:185
CColladaManagerImpl * m
std::vector< u64 > m_skeletonHashes
time_t MTime() const
Definition: file_system.h:63
Path Extension() const
Definition: path.h:176
PSRETURN Load(const PIVFS &vfs, const VfsPath &filename)
Returns either PSRETURN_OK or PSRETURN_CVFSFile_LoadFailed.
Definition: Filesystem.cpp:117
int(* set_skeleton_definitions)(const char *xml, int length)
bool VfsFileExists(const VfsPath &pathname)
Definition: Filesystem.cpp:34
size_t GetBufferSize() const
Definition: Filesystem.cpp:143
std::vector< VfsPath > VfsPaths
Definition: vfs_path.h:42
const Status FAIL
Definition: status.h:406
bool Convert(const VfsPath &daeFilename, const VfsPath &pmdFilename, CColladaManager::FileType type)
void(* OutputFn)(void *cb_data, const char *data, unsigned int length)
Status TryLoadingCached(const VfsPath &sourcePath, const MD5 &initialHash, u32 version, VfsPath &loadPath)
Attempts to find a valid cached which can be loaded.
Definition: CacheLoader.cpp:32
void(* LogFn)(void *cb_data, int severity, const char *text)
Helper class for systems that have an expensive cacheable conversion process when loading files...
Definition: CacheLoader.h:40
Status GetPathnames(const PIVFS &fs, const VfsPath &path, const wchar_t *filter, VfsPaths &pathnames)
Definition: vfs_util.cpp:40
int(* convert_dae_to_pmd)(const char *dae, Collada::OutputFn pmd_writer, void *cb_data)