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