You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

699 lines
18 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2015-2016 Cirilo Bernardo <cirilo.bernardo@gmail.com>
  5. * Copyright (C) 2018-2022 KiCad Developers, see AUTHORS.txt for contributors.
  6. * Copyright (C) 2022 CERN
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU General Public License
  10. * as published by the Free Software Foundation; either version 2
  11. * of the License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, you may find one here:
  20. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  21. * or you may search the http://www.gnu.org website for the version 2 license,
  22. * or you may write to the Free Software Foundation, Inc.,
  23. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  24. */
  25. #define GLM_FORCE_RADIANS
  26. #include <mutex>
  27. #include <utility>
  28. #include <wx/datetime.h>
  29. #include <wx/dir.h>
  30. #include <wx/log.h>
  31. #include <wx/stdpaths.h>
  32. #include <boost/version.hpp>
  33. #if BOOST_VERSION >= 106800
  34. #include <boost/uuid/detail/sha1.hpp>
  35. #else
  36. #include <boost/uuid/sha1.hpp>
  37. #endif
  38. #include "3d_cache.h"
  39. #include "3d_info.h"
  40. #include "3d_plugin_manager.h"
  41. #include "sg/scenegraph.h"
  42. #include "plugins/3dapi/ifsg_api.h"
  43. #include <advanced_config.h>
  44. #include <common.h> // For ExpandEnvVarSubstitutions
  45. #include <filename_resolver.h>
  46. #include <paths.h>
  47. #include <pgm_base.h>
  48. #include <project.h>
  49. #include <settings/common_settings.h>
  50. #include <settings/settings_manager.h>
  51. #include <wx_filename.h>
  52. #define MASK_3D_CACHE "3D_CACHE"
  53. static std::mutex mutex3D_cache;
  54. static bool isSHA1Same( const unsigned char* shaA, const unsigned char* shaB ) noexcept
  55. {
  56. for( int i = 0; i < 20; ++i )
  57. {
  58. if( shaA[i] != shaB[i] )
  59. return false;
  60. }
  61. return true;
  62. }
  63. static bool checkTag( const char* aTag, void* aPluginMgrPtr )
  64. {
  65. if( nullptr == aTag || nullptr == aPluginMgrPtr )
  66. return false;
  67. S3D_PLUGIN_MANAGER *pp = (S3D_PLUGIN_MANAGER*) aPluginMgrPtr;
  68. return pp->CheckTag( aTag );
  69. }
  70. static const wxString sha1ToWXString( const unsigned char* aSHA1Sum )
  71. {
  72. unsigned char uc;
  73. unsigned char tmp;
  74. char sha1[41];
  75. int j = 0;
  76. for( int i = 0; i < 20; ++i )
  77. {
  78. uc = aSHA1Sum[i];
  79. tmp = uc / 16;
  80. if( tmp > 9 )
  81. tmp += 87;
  82. else
  83. tmp += 48;
  84. sha1[j++] = tmp;
  85. tmp = uc % 16;
  86. if( tmp > 9 )
  87. tmp += 87;
  88. else
  89. tmp += 48;
  90. sha1[j++] = tmp;
  91. }
  92. sha1[j] = 0;
  93. return wxString::FromUTF8Unchecked( sha1 );
  94. }
  95. class S3D_CACHE_ENTRY
  96. {
  97. public:
  98. S3D_CACHE_ENTRY();
  99. ~S3D_CACHE_ENTRY();
  100. void SetSHA1( const unsigned char* aSHA1Sum );
  101. const wxString GetCacheBaseName();
  102. wxDateTime modTime; // file modification time
  103. unsigned char sha1sum[20];
  104. std::string pluginInfo; // PluginName:Version string
  105. SCENEGRAPH* sceneData;
  106. S3DMODEL* renderData;
  107. private:
  108. // prohibit assignment and default copy constructor
  109. S3D_CACHE_ENTRY( const S3D_CACHE_ENTRY& source );
  110. S3D_CACHE_ENTRY& operator=( const S3D_CACHE_ENTRY& source );
  111. wxString m_CacheBaseName; // base name of cache file (a SHA1 digest)
  112. };
  113. S3D_CACHE_ENTRY::S3D_CACHE_ENTRY()
  114. {
  115. sceneData = nullptr;
  116. renderData = nullptr;
  117. memset( sha1sum, 0, 20 );
  118. }
  119. S3D_CACHE_ENTRY::~S3D_CACHE_ENTRY()
  120. {
  121. delete sceneData;
  122. if( nullptr != renderData )
  123. S3D::Destroy3DModel( &renderData );
  124. }
  125. void S3D_CACHE_ENTRY::SetSHA1( const unsigned char* aSHA1Sum )
  126. {
  127. if( nullptr == aSHA1Sum )
  128. {
  129. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [BUG] NULL passed for aSHA1Sum" ),
  130. __FILE__, __FUNCTION__, __LINE__ );
  131. return;
  132. }
  133. memcpy( sha1sum, aSHA1Sum, 20 );
  134. }
  135. const wxString S3D_CACHE_ENTRY::GetCacheBaseName()
  136. {
  137. if( m_CacheBaseName.empty() )
  138. m_CacheBaseName = sha1ToWXString( sha1sum );
  139. return m_CacheBaseName;
  140. }
  141. S3D_CACHE::S3D_CACHE()
  142. {
  143. m_FNResolver = new FILENAME_RESOLVER;
  144. m_project = nullptr;
  145. m_Plugins = new S3D_PLUGIN_MANAGER;
  146. }
  147. S3D_CACHE::~S3D_CACHE()
  148. {
  149. FlushCache();
  150. delete m_FNResolver;
  151. delete m_Plugins;
  152. }
  153. SCENEGRAPH* S3D_CACHE::load( const wxString& aModelFile, const wxString& aBasePath,
  154. S3D_CACHE_ENTRY** aCachePtr, const EMBEDDED_FILES* aEmbeddedFiles )
  155. {
  156. if( aCachePtr )
  157. *aCachePtr = nullptr;
  158. wxString full3Dpath = m_FNResolver->ResolvePath( aModelFile, aBasePath, aEmbeddedFiles );
  159. if( full3Dpath.empty() )
  160. {
  161. // the model cannot be found; we cannot proceed
  162. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [3D model] could not find model '%s'\n" ),
  163. __FILE__, __FUNCTION__, __LINE__, aModelFile );
  164. return nullptr;
  165. }
  166. // check cache if file is already loaded
  167. std::lock_guard<std::mutex> lock( mutex3D_cache );
  168. std::map< wxString, S3D_CACHE_ENTRY*, rsort_wxString >::iterator mi;
  169. mi = m_CacheMap.find( full3Dpath );
  170. if( mi != m_CacheMap.end() )
  171. {
  172. wxFileName fname( full3Dpath );
  173. if( fname.FileExists() ) // Only check if file exists. If not, it will
  174. { // use the same model in cache.
  175. bool reload = ADVANCED_CFG::GetCfg().m_Skip3DModelMemoryCache;
  176. wxDateTime fmdate = fname.GetModificationTime();
  177. if( fmdate != mi->second->modTime )
  178. {
  179. unsigned char hashSum[20];
  180. getSHA1( full3Dpath, hashSum );
  181. mi->second->modTime = fmdate;
  182. if( !isSHA1Same( hashSum, mi->second->sha1sum ) )
  183. {
  184. mi->second->SetSHA1( hashSum );
  185. reload = true;
  186. }
  187. }
  188. if( reload )
  189. {
  190. if( nullptr != mi->second->sceneData )
  191. {
  192. S3D::DestroyNode( mi->second->sceneData );
  193. mi->second->sceneData = nullptr;
  194. }
  195. if( nullptr != mi->second->renderData )
  196. S3D::Destroy3DModel( &mi->second->renderData );
  197. mi->second->sceneData = m_Plugins->Load3DModel( full3Dpath,
  198. mi->second->pluginInfo );
  199. }
  200. }
  201. if( nullptr != aCachePtr )
  202. *aCachePtr = mi->second;
  203. return mi->second->sceneData;
  204. }
  205. // a cache item does not exist; search the Filename->Cachename map
  206. return checkCache( full3Dpath, aCachePtr );
  207. }
  208. SCENEGRAPH* S3D_CACHE::Load( const wxString& aModelFile, const wxString& aBasePath, const EMBEDDED_FILES* aEmbeddedFiles )
  209. {
  210. return load( aModelFile, aBasePath, nullptr, aEmbeddedFiles );
  211. }
  212. SCENEGRAPH* S3D_CACHE::checkCache( const wxString& aFileName, S3D_CACHE_ENTRY** aCachePtr )
  213. {
  214. if( aCachePtr )
  215. *aCachePtr = nullptr;
  216. unsigned char sha1sum[20];
  217. S3D_CACHE_ENTRY* ep = new S3D_CACHE_ENTRY;
  218. m_CacheList.push_back( ep );
  219. wxFileName fname( aFileName );
  220. ep->modTime = fname.GetModificationTime();
  221. if( !getSHA1( aFileName, sha1sum ) || m_CacheDir.empty() )
  222. {
  223. // just in case we can't get a hash digest (for example, on access issues)
  224. // or we do not have a configured cache file directory, we create an
  225. // entry to prevent further attempts at loading the file
  226. if( m_CacheMap.emplace( aFileName, ep ).second == false )
  227. {
  228. wxLogTrace( MASK_3D_CACHE,
  229. wxT( "%s:%s:%d\n * [BUG] duplicate entry in map file; key = '%s'" ),
  230. __FILE__, __FUNCTION__, __LINE__, aFileName );
  231. m_CacheList.pop_back();
  232. delete ep;
  233. }
  234. else
  235. {
  236. if( aCachePtr )
  237. *aCachePtr = ep;
  238. }
  239. return nullptr;
  240. }
  241. if( m_CacheMap.emplace( aFileName, ep ).second == false )
  242. {
  243. wxLogTrace( MASK_3D_CACHE,
  244. wxT( "%s:%s:%d\n * [BUG] duplicate entry in map file; key = '%s'" ),
  245. __FILE__, __FUNCTION__, __LINE__, aFileName );
  246. m_CacheList.pop_back();
  247. delete ep;
  248. return nullptr;
  249. }
  250. if( aCachePtr )
  251. *aCachePtr = ep;
  252. ep->SetSHA1( sha1sum );
  253. wxString bname = ep->GetCacheBaseName();
  254. wxString cachename = m_CacheDir + bname + wxT( ".3dc" );
  255. if( !ADVANCED_CFG::GetCfg().m_Skip3DModelFileCache && wxFileName::FileExists( cachename )
  256. && loadCacheData( ep ) )
  257. return ep->sceneData;
  258. ep->sceneData = m_Plugins->Load3DModel( aFileName, ep->pluginInfo );
  259. if( !ADVANCED_CFG::GetCfg().m_Skip3DModelFileCache && nullptr != ep->sceneData )
  260. saveCacheData( ep );
  261. return ep->sceneData;
  262. }
  263. bool S3D_CACHE::getSHA1( const wxString& aFileName, unsigned char* aSHA1Sum )
  264. {
  265. if( aFileName.empty() )
  266. {
  267. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [BUG] empty filename" ),
  268. __FILE__, __FUNCTION__, __LINE__ );
  269. return false;
  270. }
  271. if( nullptr == aSHA1Sum )
  272. {
  273. wxLogTrace( MASK_3D_CACHE, wxT( "%s\n * [BUG] NULL pointer passed for aMD5Sum" ),
  274. __FILE__, __FUNCTION__, __LINE__ );
  275. return false;
  276. }
  277. #ifdef _WIN32
  278. FILE* fp = _wfopen( aFileName.wc_str(), L"rb" );
  279. #else
  280. FILE* fp = fopen( aFileName.ToUTF8(), "rb" );
  281. #endif
  282. if( nullptr == fp )
  283. return false;
  284. boost::uuids::detail::sha1 dblock;
  285. unsigned char block[4096];
  286. size_t bsize = 0;
  287. while( ( bsize = fread( &block, 1, 4096, fp ) ) > 0 )
  288. dblock.process_bytes( block, bsize );
  289. fclose( fp );
  290. unsigned int digest[5];
  291. dblock.get_digest( digest );
  292. // ensure MSB order
  293. for( int i = 0; i < 5; ++i )
  294. {
  295. int idx = i << 2;
  296. unsigned int tmp = digest[i];
  297. aSHA1Sum[idx+3] = tmp & 0xff;
  298. tmp >>= 8;
  299. aSHA1Sum[idx+2] = tmp & 0xff;
  300. tmp >>= 8;
  301. aSHA1Sum[idx+1] = tmp & 0xff;
  302. tmp >>= 8;
  303. aSHA1Sum[idx] = tmp & 0xff;
  304. }
  305. return true;
  306. }
  307. bool S3D_CACHE::loadCacheData( S3D_CACHE_ENTRY* aCacheItem )
  308. {
  309. wxString bname = aCacheItem->GetCacheBaseName();
  310. if( bname.empty() )
  311. {
  312. wxLogTrace( MASK_3D_CACHE,
  313. wxT( " * [3D model] cannot load cached model; no file hash available" ) );
  314. return false;
  315. }
  316. if( m_CacheDir.empty() )
  317. {
  318. wxLogTrace( MASK_3D_CACHE,
  319. wxT( " * [3D model] cannot load cached model; config directory unknown" ) );
  320. return false;
  321. }
  322. wxString fname = m_CacheDir + bname + wxT( ".3dc" );
  323. if( !wxFileName::FileExists( fname ) )
  324. {
  325. wxLogTrace( MASK_3D_CACHE, wxT( " * [3D model] cannot open file '%s'" ), fname.GetData() );
  326. return false;
  327. }
  328. if( nullptr != aCacheItem->sceneData )
  329. S3D::DestroyNode( (SGNODE*) aCacheItem->sceneData );
  330. aCacheItem->sceneData = (SCENEGRAPH*)S3D::ReadCache( fname.ToUTF8(), m_Plugins, checkTag );
  331. if( nullptr == aCacheItem->sceneData )
  332. return false;
  333. return true;
  334. }
  335. bool S3D_CACHE::saveCacheData( S3D_CACHE_ENTRY* aCacheItem )
  336. {
  337. if( nullptr == aCacheItem )
  338. {
  339. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * NULL passed for aCacheItem" ),
  340. __FILE__, __FUNCTION__, __LINE__ );
  341. return false;
  342. }
  343. if( nullptr == aCacheItem->sceneData )
  344. {
  345. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * aCacheItem has no valid scene data" ),
  346. __FILE__, __FUNCTION__, __LINE__ );
  347. return false;
  348. }
  349. wxString bname = aCacheItem->GetCacheBaseName();
  350. if( bname.empty() )
  351. {
  352. wxLogTrace( MASK_3D_CACHE,
  353. wxT( " * [3D model] cannot load cached model; no file hash available" ) );
  354. return false;
  355. }
  356. if( m_CacheDir.empty() )
  357. {
  358. wxLogTrace( MASK_3D_CACHE,
  359. wxT( " * [3D model] cannot load cached model; config directory unknown" ) );
  360. return false;
  361. }
  362. wxString fname = m_CacheDir + bname + wxT( ".3dc" );
  363. if( wxFileName::Exists( fname ) )
  364. {
  365. if( !wxFileName::FileExists( fname ) )
  366. {
  367. wxLogTrace( MASK_3D_CACHE,
  368. wxT( " * [3D model] path exists but is not a regular file '%s'" ), fname );
  369. return false;
  370. }
  371. }
  372. return S3D::WriteCache( fname.ToUTF8(), true, (SGNODE*)aCacheItem->sceneData,
  373. aCacheItem->pluginInfo.c_str() );
  374. }
  375. bool S3D_CACHE::Set3DConfigDir( const wxString& aConfigDir )
  376. {
  377. if( !m_ConfigDir.empty() )
  378. return false;
  379. wxFileName cfgdir( ExpandEnvVarSubstitutions( aConfigDir, m_project ), wxEmptyString );
  380. cfgdir.Normalize( FN_NORMALIZE_FLAGS );
  381. if( !cfgdir.DirExists() )
  382. {
  383. cfgdir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
  384. if( !cfgdir.DirExists() )
  385. {
  386. wxLogTrace( MASK_3D_CACHE,
  387. wxT( "%s:%s:%d\n * failed to create 3D configuration directory '%s'" ),
  388. __FILE__, __FUNCTION__, __LINE__, cfgdir.GetPath() );
  389. return false;
  390. }
  391. }
  392. m_ConfigDir = cfgdir.GetPath();
  393. // inform the file resolver of the config directory
  394. if( !m_FNResolver->Set3DConfigDir( m_ConfigDir ) )
  395. {
  396. wxLogTrace( MASK_3D_CACHE,
  397. wxT( "%s:%s:%d\n * could not set 3D Config Directory on filename resolver\n"
  398. " * config directory: '%s'" ),
  399. __FILE__, __FUNCTION__, __LINE__, m_ConfigDir );
  400. }
  401. // 3D cache data must go to a user's cache directory;
  402. // unfortunately wxWidgets doesn't seem to provide
  403. // functions to retrieve such a directory.
  404. //
  405. // 1. OSX: ~/Library/Caches/kicad/3d/
  406. // 2. Linux: ${XDG_CACHE_HOME}/kicad/3d ~/.cache/kicad/3d/
  407. // 3. MSWin: AppData\Local\kicad\3d
  408. wxFileName cacheDir;
  409. cacheDir.AssignDir( PATHS::GetUserCachePath() );
  410. cacheDir.AppendDir( wxT( "3d" ) );
  411. if( !cacheDir.DirExists() )
  412. {
  413. cacheDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
  414. if( !cacheDir.DirExists() )
  415. {
  416. wxLogTrace( MASK_3D_CACHE,
  417. wxT( "%s:%s:%d\n * failed to create 3D cache directory '%s'" ),
  418. __FILE__, __FUNCTION__, __LINE__, cacheDir.GetPath() );
  419. return false;
  420. }
  421. }
  422. m_CacheDir = cacheDir.GetPathWithSep();
  423. return true;
  424. }
  425. bool S3D_CACHE::SetProject( PROJECT* aProject )
  426. {
  427. m_project = aProject;
  428. bool hasChanged = false;
  429. if( m_FNResolver->SetProject( aProject, &hasChanged ) && hasChanged )
  430. {
  431. m_CacheMap.clear();
  432. std::list< S3D_CACHE_ENTRY* >::iterator sL = m_CacheList.begin();
  433. std::list< S3D_CACHE_ENTRY* >::iterator eL = m_CacheList.end();
  434. while( sL != eL )
  435. {
  436. delete *sL;
  437. ++sL;
  438. }
  439. m_CacheList.clear();
  440. return true;
  441. }
  442. return false;
  443. }
  444. void S3D_CACHE::SetProgramBase( PGM_BASE* aBase )
  445. {
  446. m_FNResolver->SetProgramBase( aBase );
  447. }
  448. FILENAME_RESOLVER* S3D_CACHE::GetResolver() noexcept
  449. {
  450. return m_FNResolver;
  451. }
  452. std::list< wxString > const* S3D_CACHE::GetFileFilters() const
  453. {
  454. return m_Plugins->GetFileFilters();
  455. }
  456. void S3D_CACHE::FlushCache( bool closePlugins )
  457. {
  458. std::list< S3D_CACHE_ENTRY* >::iterator sCL = m_CacheList.begin();
  459. std::list< S3D_CACHE_ENTRY* >::iterator eCL = m_CacheList.end();
  460. while( sCL != eCL )
  461. {
  462. delete *sCL;
  463. ++sCL;
  464. }
  465. m_CacheList.clear();
  466. m_CacheMap.clear();
  467. if( closePlugins )
  468. ClosePlugins();
  469. }
  470. void S3D_CACHE::ClosePlugins()
  471. {
  472. if( m_Plugins )
  473. m_Plugins->ClosePlugins();
  474. }
  475. S3DMODEL* S3D_CACHE::GetModel( const wxString& aModelFileName, const wxString& aBasePath,
  476. const EMBEDDED_FILES* aEmbeddedFiles )
  477. {
  478. S3D_CACHE_ENTRY* cp = nullptr;
  479. SCENEGRAPH* sp = load( aModelFileName, aBasePath, &cp, aEmbeddedFiles );
  480. if( !sp )
  481. return nullptr;
  482. if( !cp )
  483. {
  484. wxLogTrace( MASK_3D_CACHE,
  485. wxT( "%s:%s:%d\n * [BUG] model loaded with no associated S3D_CACHE_ENTRY" ),
  486. __FILE__, __FUNCTION__, __LINE__ );
  487. return nullptr;
  488. }
  489. if( cp->renderData )
  490. return cp->renderData;
  491. S3DMODEL* mp = S3D::GetModel( sp );
  492. cp->renderData = mp;
  493. return mp;
  494. }
  495. void S3D_CACHE::CleanCacheDir( int aNumDaysOld )
  496. {
  497. wxDir dir;
  498. wxString fileSpec = wxT( "*.3dc" );
  499. wxArrayString fileList; // Holds list of ".3dc" files found in cache directory
  500. size_t numFilesFound = 0;
  501. wxFileName thisFile;
  502. wxDateTime lastAccess, thresholdDate;
  503. wxDateSpan durationInDays;
  504. // Calc the threshold date above which we delete cache files
  505. durationInDays.SetDays( aNumDaysOld );
  506. thresholdDate = wxDateTime::Now() - durationInDays;
  507. // If the cache directory can be found and opened, then we'll try and clean it up
  508. if( dir.Open( m_CacheDir ) )
  509. {
  510. thisFile.SetPath( m_CacheDir ); // Set the base path to the cache folder
  511. // Get a list of all the ".3dc" files in the cache directory
  512. numFilesFound = dir.GetAllFiles( m_CacheDir, &fileList, fileSpec );
  513. for( unsigned int i = 0; i < numFilesFound; i++ )
  514. {
  515. // Completes path to specific file so we can get its "last access" date
  516. thisFile.SetFullName( fileList[i] );
  517. // Only get "last access" time to compare against. Don't need the other 2 timestamps.
  518. if( thisFile.GetTimes( &lastAccess, nullptr, nullptr ) )
  519. {
  520. if( lastAccess.IsEarlierThan( thresholdDate ) )
  521. {
  522. // This file is older than the threshold so delete it
  523. wxRemoveFile( thisFile.GetFullPath() );
  524. }
  525. }
  526. }
  527. }
  528. }