LCOV - code coverage report
Current view: top level - source/graphics - ColladaManager.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 111 163 68.1 %
Date: 2023-01-19 00:18:29 Functions: 13 16 81.2 %

          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 : }

Generated by: LCOV version 1.13