1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
3 | #include "Resources/BsResources.h" |
4 | #include "Resources/BsResource.h" |
5 | #include "Resources/BsResourceManifest.h" |
6 | #include "Error/BsException.h" |
7 | #include "Serialization/BsFileSerializer.h" |
8 | #include "FileSystem/BsFileSystem.h" |
9 | #include "Threading/BsTaskScheduler.h" |
10 | #include "Utility/BsUUID.h" |
11 | #include "Debug/BsDebug.h" |
12 | #include "Utility/BsUtility.h" |
13 | #include "Resources/BsSavedResourceData.h" |
14 | #include "Managers/BsResourceListenerManager.h" |
15 | #include "Serialization/BsMemorySerializer.h" |
16 | #include "Utility/BsCompression.h" |
17 | #include "FileSystem/BsDataStream.h" |
18 | #include "Serialization/BsBinarySerializer.h" |
19 | #include "Reflection/BsRTTIType.h" |
20 | #include "BsCoreApplication.h" |
21 | |
22 | namespace bs |
23 | { |
24 | Resources::Resources() |
25 | { |
26 | { |
27 | Lock lock(mDefaultManifestMutex); |
28 | mDefaultResourceManifest = ResourceManifest::create("Default" ); |
29 | mResourceManifests.push_back(mDefaultResourceManifest); |
30 | } |
31 | } |
32 | |
33 | Resources::~Resources() |
34 | { |
35 | unloadAll(); |
36 | } |
37 | |
38 | HResource Resources::load(const Path& filePath, ResourceLoadFlags loadFlags) |
39 | { |
40 | if (!FileSystem::isFile(filePath)) |
41 | { |
42 | LOGWRN("Cannot load resource. Specified file: " + filePath.toString() + " doesn't exist." ); |
43 | |
44 | return HResource(); |
45 | } |
46 | |
47 | UUID uuid; |
48 | bool foundUUID = getUUIDFromFilePath(filePath, uuid); |
49 | |
50 | if (!foundUUID) |
51 | uuid = UUIDGenerator::generateRandom(); |
52 | |
53 | return loadInternal(uuid, filePath, true, loadFlags).resource; |
54 | } |
55 | |
56 | HResource Resources::load(const WeakResourceHandle<Resource>& handle, ResourceLoadFlags loadFlags) |
57 | { |
58 | if (handle.mData == nullptr) |
59 | return HResource(); |
60 | |
61 | UUID uuid = handle.getUUID(); |
62 | return loadFromUUID(uuid, false, loadFlags); |
63 | } |
64 | |
65 | HResource Resources::loadAsync(const Path& filePath, ResourceLoadFlags loadFlags) |
66 | { |
67 | if (!FileSystem::isFile(filePath)) |
68 | { |
69 | LOGWRN("Cannot load resource. Specified file: " + filePath.toString() + " doesn't exist." ); |
70 | |
71 | return HResource(); |
72 | } |
73 | |
74 | UUID uuid; |
75 | bool foundUUID = getUUIDFromFilePath(filePath, uuid); |
76 | |
77 | if (!foundUUID) |
78 | uuid = UUIDGenerator::generateRandom(); |
79 | |
80 | return loadInternal(uuid, filePath, false, loadFlags).resource; |
81 | } |
82 | |
83 | HResource Resources::loadFromUUID(const UUID& uuid, bool async, ResourceLoadFlags loadFlags) |
84 | { |
85 | Path filePath; |
86 | getFilePathFromUUID(uuid, filePath); |
87 | |
88 | return loadInternal(uuid, filePath, !async, loadFlags).resource; |
89 | } |
90 | |
91 | Resources::LoadInfo Resources::loadInternal(const UUID& uuid, const Path& filePath, bool synchronous, |
92 | ResourceLoadFlags loadFlags) |
93 | { |
94 | LoadInfo output; |
95 | |
96 | // Retrieve/create resource handle, and register with the system |
97 | bool loadInProgress = false; |
98 | bool loadFailed = false; |
99 | bool initiateLoad = false; |
100 | Vector<UUID> dependenciesToLoad; |
101 | { |
102 | bool alreadyLoading = false; |
103 | |
104 | // Check if the resource is being loaded on a worker thread |
105 | Lock inProgressLock(mInProgressResourcesMutex); |
106 | Lock loadedLock(mLoadedResourceMutex); |
107 | |
108 | auto iterFind2 = mInProgressResources.find(uuid); |
109 | if (iterFind2 != mInProgressResources.end()) |
110 | { |
111 | LoadedResourceData& resData = iterFind2->second->resData; |
112 | output.resource = resData.resource.lock(); |
113 | output.state = LoadInfo::AlreadyInProgress; |
114 | output.size = resData.size; |
115 | |
116 | // Increase ref. count |
117 | if (loadFlags.isSet(ResourceLoadFlag::KeepInternalRef)) |
118 | { |
119 | resData.numInternalRefs++; |
120 | output.resource.addInternalRef(); |
121 | } |
122 | |
123 | loadInProgress = true; |
124 | alreadyLoading = true; |
125 | } |
126 | |
127 | // Check if the resource is already loaded |
128 | auto iterFind = mLoadedResources.find(uuid); |
129 | if (iterFind != mLoadedResources.end()) |
130 | { |
131 | LoadedResourceData& resData = iterFind->second; |
132 | output.resource = resData.resource.lock(); |
133 | output.state = LoadInfo::AlreadyLoaded; |
134 | output.size = resData.size; |
135 | |
136 | // Increase ref. count |
137 | if (loadFlags.isSet(ResourceLoadFlag::KeepInternalRef)) |
138 | { |
139 | resData.numInternalRefs++; |
140 | output.resource.addInternalRef(); |
141 | } |
142 | |
143 | alreadyLoading = true; |
144 | } |
145 | |
146 | // Not loaded and not in progress, register a new handle or find a pre-registered one |
147 | if(!alreadyLoading) |
148 | { |
149 | output.state = LoadInfo::Loading; |
150 | output.size = 0; |
151 | |
152 | auto iterFind = mHandles.find(uuid); |
153 | if (iterFind != mHandles.end()) |
154 | output.resource = iterFind->second.lock(); |
155 | else |
156 | { |
157 | output.resource = HResource(uuid); |
158 | mHandles[uuid] = output.resource.getWeak(); |
159 | } |
160 | } |
161 | |
162 | // If we have nowhere to load from, warn and complete load if a file path was provided, otherwise pass through |
163 | // as we might just want to complete a previously queued load |
164 | if (filePath.isEmpty()) |
165 | { |
166 | if (!alreadyLoading) |
167 | { |
168 | LOGWRN_VERBOSE("Cannot load resource. Resource with UUID '" + uuid.toString() + "' doesn't exist." ); |
169 | loadFailed = true; |
170 | } |
171 | } |
172 | else if (!FileSystem::isFile(filePath)) |
173 | { |
174 | LOGWRN_VERBOSE("Cannot load resource. Specified file: " + filePath.toString() + " doesn't exist." ); |
175 | loadFailed = true; |
176 | } |
177 | |
178 | bool loadDependencies = loadFlags.isSet(ResourceLoadFlag::LoadDependencies); |
179 | if(!loadFailed) |
180 | { |
181 | // Load dependency data if a file path is provided |
182 | SPtr<SavedResourceData> savedResourceData; |
183 | if (!filePath.isEmpty()) |
184 | { |
185 | // Note: Ideally this data gets cached eventually (e.g. as part of the manifest). When loading objects |
186 | // with a lot of dependencies (e.g. scenes) this will get called for every dependency, synchronously, |
187 | // which might take a while. It would be nice to just read it from a single location. Another option is |
188 | // to make this whole block asynchronous so every dependency does it on its own thread. |
189 | FileDecoder fs(filePath); |
190 | savedResourceData = std::static_pointer_cast<SavedResourceData>(fs.decode()); |
191 | output.size = fs.getSize(); |
192 | } |
193 | |
194 | // Register an in-progress load unless there is an existing load operation, or the resource is already |
195 | // loaded |
196 | if(!alreadyLoading) |
197 | { |
198 | ResourceLoadData* loadData = bs_new<ResourceLoadData>(output.resource.getWeak(), 0, output.size); |
199 | mInProgressResources[uuid] = loadData; |
200 | |
201 | if (loadFlags.isSet(ResourceLoadFlag::KeepInternalRef)) |
202 | { |
203 | loadData->resData.numInternalRefs++; |
204 | output.resource.addInternalRef(); |
205 | } |
206 | |
207 | loadData->remainingDependencies = 1; // Self |
208 | loadData->progress.store(0.0f, std::memory_order_relaxed); |
209 | |
210 | // Make resource listener trigger before exit if loading synchronously on the main thread |
211 | loadData->notifyImmediately = synchronous && BS_THREAD_CURRENT_ID == gCoreApplication().getSimThreadId(); |
212 | |
213 | // Register dependencies and count them so we know when the resource is fully loaded |
214 | if (loadDependencies && savedResourceData != nullptr) |
215 | { |
216 | for (auto& dependency : savedResourceData->getDependencies()) |
217 | { |
218 | if (dependency != uuid) |
219 | { |
220 | mDependantLoads[dependency].push_back(loadData); |
221 | loadData->remainingDependencies++; |
222 | dependenciesToLoad.push_back(dependency); |
223 | } |
224 | } |
225 | } |
226 | } |
227 | // The resource is already being loaded, or is loaded, but we might still need to load some dependencies |
228 | else if(loadDependencies && savedResourceData != nullptr) |
229 | { |
230 | const Vector<UUID>& dependencies = savedResourceData->getDependencies(); |
231 | if (!dependencies.empty()) |
232 | { |
233 | ResourceLoadData* loadData = nullptr; |
234 | |
235 | // If load not in progress, register the resource for load |
236 | if (!loadInProgress) |
237 | { |
238 | loadData = bs_new<ResourceLoadData>(output.resource.getWeak(), 0, output.size); |
239 | loadData->remainingDependencies = 0; |
240 | loadData->progress.store(0.0f, std::memory_order_relaxed); |
241 | |
242 | // Make resource listener trigger before exit if loading synchronously |
243 | loadData->notifyImmediately = synchronous && BS_THREAD_CURRENT_ID == gCoreApplication().getSimThreadId(); |
244 | } |
245 | else |
246 | loadData = mInProgressResources[uuid]; |
247 | |
248 | // Find dependencies that aren't already loaded or queued for loading |
249 | for (auto& dependency : dependencies) |
250 | { |
251 | if (dependency != uuid) |
252 | { |
253 | bool registerDependency = false; |
254 | |
255 | auto iterFind3 = mLoadedResources.find(dependency); |
256 | if(iterFind3 == mLoadedResources.end()) |
257 | { |
258 | registerDependency = true; |
259 | |
260 | auto iterFind2 = mDependantLoads.find(dependency); |
261 | if (iterFind2 != mDependantLoads.end()) |
262 | { |
263 | Vector<ResourceLoadData*>& dependantData = iterFind2->second; |
264 | auto iterFind3 = std::find_if(dependantData.begin(), dependantData.end(), |
265 | [&](ResourceLoadData* x) |
266 | { |
267 | return x->resData.resource.getUUID() == output.resource.getUUID(); |
268 | }); |
269 | |
270 | registerDependency = iterFind3 == dependantData.end(); |
271 | } |
272 | } |
273 | |
274 | if (registerDependency) |
275 | { |
276 | mDependantLoads[dependency].push_back(loadData); |
277 | loadData->remainingDependencies++; |
278 | dependenciesToLoad.push_back(dependency); |
279 | } |
280 | } |
281 | } |
282 | |
283 | if(!loadInProgress) |
284 | { |
285 | if(!dependenciesToLoad.empty()) |
286 | mInProgressResources[uuid] = loadData; |
287 | else |
288 | bs_delete(loadData); |
289 | } |
290 | } |
291 | } |
292 | |
293 | initiateLoad = !alreadyLoading && !filePath.isEmpty(); |
294 | |
295 | if(savedResourceData != nullptr) |
296 | synchronous = synchronous || !savedResourceData->allowAsyncLoading(); |
297 | } |
298 | } |
299 | |
300 | // Something went wrong, clean up and exit |
301 | if(loadFailed) |
302 | { |
303 | output.state = LoadInfo::Failed; |
304 | output.size = 0; |
305 | |
306 | // Clean up in-progress state |
307 | loadComplete(output.resource, true); |
308 | return output; |
309 | } |
310 | |
311 | // Load dependencies (before the main resource) |
312 | const auto numDependencies = (UINT32)dependenciesToLoad.size(); |
313 | if(numDependencies > 0) |
314 | { |
315 | ResourceLoadFlags depLoadFlags = ResourceLoadFlag::LoadDependencies; |
316 | if (loadFlags.isSet(ResourceLoadFlag::KeepSourceData)) |
317 | depLoadFlags |= ResourceLoadFlag::KeepSourceData; |
318 | |
319 | Vector<HResource> dependencies(numDependencies); |
320 | UINT32 dependencySize = 0; |
321 | for (UINT32 i = 0; i < numDependencies; i++) |
322 | { |
323 | const UUID& depUUID = dependenciesToLoad[i]; |
324 | |
325 | Path depFilePath; |
326 | getFilePathFromUUID(depUUID, depFilePath); |
327 | |
328 | LoadInfo loadInfo = loadInternal(depUUID, depFilePath, synchronous, depLoadFlags); |
329 | dependencies[i] = loadInfo.resource; |
330 | |
331 | // Calculate the size of dependencies that still need to be loaded, for progress reporting |
332 | if(loadInfo.state == LoadInfo::Loading || loadInfo.state == LoadInfo::AlreadyInProgress) |
333 | { |
334 | // Note: Technically, since we're queuing the dependency load with no locking, the load could complete |
335 | // before the size of the dependency has been registered, which means getLoadProgress() method would |
336 | // incorrectly report the progress to be higher than it should be. If that becomes an issue then this |
337 | // operation is better to be moved to the child loadInternal() call. |
338 | dependencySize += loadInfo.size; |
339 | } |
340 | } |
341 | |
342 | // Keep dependencies alive until the parent is done loading, and record total size of dependencies to load |
343 | { |
344 | Lock inProgressLock(mInProgressResourcesMutex); |
345 | |
346 | // If we're doing a dependency-only load (main resource itself was previously loaded), then the in-progress |
347 | // operation could have already finished when the last dependency was loaded (this will always be true for |
348 | // synchronous loads), and no need to register dependencies. |
349 | const auto iterFind = mInProgressResources.find(uuid); |
350 | if(iterFind != mInProgressResources.end()) |
351 | { |
352 | iterFind->second->dependencySize = dependencySize; |
353 | iterFind->second->dependencies = dependencies; |
354 | } |
355 | } |
356 | } |
357 | |
358 | // Previously being loaded as async but now we want it synced, so we wait |
359 | if (loadInProgress && synchronous) |
360 | output.resource.blockUntilLoaded(false); |
361 | |
362 | // Actually start the file read operation if not already loaded or in progress |
363 | if (initiateLoad) |
364 | { |
365 | // Synchronous or the resource doesn't support async, read the file immediately |
366 | if (synchronous) |
367 | { |
368 | loadCallback(filePath, output.resource, loadFlags.isSet(ResourceLoadFlag::KeepSourceData)); |
369 | } |
370 | else // Asynchronous, read the file on a worker thread |
371 | { |
372 | String fileName = filePath.getFilename(); |
373 | String taskName = "Resource load: " + fileName; |
374 | |
375 | bool keepSourceData = loadFlags.isSet(ResourceLoadFlag::KeepSourceData); |
376 | SPtr<Task> task = Task::create(taskName, |
377 | std::bind(&Resources::loadCallback, this, filePath, output.resource, keepSourceData)); |
378 | TaskScheduler::instance().addTask(task); |
379 | } |
380 | } |
381 | else |
382 | { |
383 | if(!loadInProgress) |
384 | { |
385 | // Already loaded, decrement dependency count |
386 | loadComplete(output.resource, false); |
387 | } |
388 | } |
389 | |
390 | output.state = LoadInfo::Loading; |
391 | return output; |
392 | } |
393 | |
394 | SPtr<Resource> Resources::loadFromDiskAndDeserialize(const Path& filePath, bool loadWithSaveData, |
395 | std::atomic<float>& progress) |
396 | { |
397 | Lock fileLock = FileScheduler::getLock(filePath); |
398 | |
399 | SPtr<DataStream> stream = FileSystem::openFile(filePath, true); |
400 | if (stream == nullptr) |
401 | return nullptr; |
402 | |
403 | if (stream->size() > std::numeric_limits<UINT32>::max()) |
404 | { |
405 | BS_EXCEPT(InternalErrorException, |
406 | "File size is larger that UINT32 can hold. Ask a programmer to use a bigger data type." ); |
407 | } |
408 | |
409 | CoreSerializationContext serzContext; |
410 | serzContext.flags = loadWithSaveData ? SF_KeepResourceSourceData : 0; |
411 | |
412 | // Read meta-data |
413 | SPtr<SavedResourceData> metaData; |
414 | { |
415 | if (!stream->eof()) |
416 | { |
417 | UINT32 objectSize = 0; |
418 | stream->read(&objectSize, sizeof(objectSize)); |
419 | |
420 | BinarySerializer bs; |
421 | metaData = std::static_pointer_cast<SavedResourceData>(bs.decode(stream, objectSize, &serzContext)); |
422 | } |
423 | } |
424 | |
425 | // Read resource data |
426 | SPtr<IReflectable> loadedData; |
427 | { |
428 | if(metaData && !stream->eof()) |
429 | { |
430 | UINT32 objectSize = 0; |
431 | stream->read(&objectSize, sizeof(objectSize)); |
432 | |
433 | if (metaData->getCompressionMethod() != 0) |
434 | { |
435 | stream = Compression::decompress(stream, [&progress](float val) |
436 | { |
437 | progress.exchange(val * 0.9f, std::memory_order_relaxed); |
438 | }); |
439 | |
440 | BinarySerializer bs; |
441 | loadedData = bs.decode(stream, objectSize, &serzContext, [&progress](float val) |
442 | { |
443 | progress.exchange(0.9f + val * 0.1f, std::memory_order_relaxed); |
444 | }); |
445 | } |
446 | else |
447 | { |
448 | BinarySerializer bs; |
449 | loadedData = bs.decode(stream, objectSize, &serzContext, [&progress](float val) |
450 | { |
451 | progress.exchange(val, std::memory_order_relaxed); |
452 | }); |
453 | } |
454 | } |
455 | } |
456 | |
457 | if (loadedData == nullptr) |
458 | { |
459 | LOGERR("Unable to load resource at path \"" + filePath.toString() + "\"" ); |
460 | } |
461 | else |
462 | { |
463 | if (!loadedData->isDerivedFrom(Resource::getRTTIStatic())) |
464 | BS_EXCEPT(InternalErrorException, "Loaded class doesn't derive from Resource." ); |
465 | } |
466 | |
467 | SPtr<Resource> resource = std::static_pointer_cast<Resource>(loadedData); |
468 | return resource; |
469 | } |
470 | |
471 | void Resources::release(ResourceHandleBase& resource) |
472 | { |
473 | const UUID& uuid = resource.getUUID(); |
474 | |
475 | { |
476 | bool loadInProgress = false; |
477 | |
478 | { |
479 | Lock inProgressLock(mInProgressResourcesMutex); |
480 | auto iterFind2 = mInProgressResources.find(uuid); |
481 | if (iterFind2 != mInProgressResources.end()) |
482 | loadInProgress = true; |
483 | } |
484 | |
485 | // Technically we should be able to just cancel a load in progress instead of blocking until it finishes. |
486 | // However that would mean the last reference could get lost on whatever thread did the loading, which |
487 | // isn't something that's supported. If this ends up being a problem either make handle counting atomic |
488 | // or add a separate queue for objects destroyed from the load threads. |
489 | if (loadInProgress) |
490 | resource.blockUntilLoaded(); |
491 | |
492 | bool lostLastRef = false; |
493 | { |
494 | Lock loadedLock(mLoadedResourceMutex); |
495 | auto iterFind = mLoadedResources.find(uuid); |
496 | if (iterFind != mLoadedResources.end()) |
497 | { |
498 | LoadedResourceData& resData = iterFind->second; |
499 | |
500 | assert(resData.numInternalRefs > 0); |
501 | resData.numInternalRefs--; |
502 | resource.removeInternalRef(); |
503 | |
504 | std::uint32_t refCount = resource.getHandleData()->mRefCount.load(std::memory_order_relaxed); |
505 | lostLastRef = refCount == 0; |
506 | } |
507 | } |
508 | |
509 | if(lostLastRef) |
510 | destroy(resource); |
511 | } |
512 | } |
513 | |
514 | void Resources::unloadAllUnused() |
515 | { |
516 | Vector<HResource> resourcesToUnload; |
517 | |
518 | { |
519 | Lock lock(mLoadedResourceMutex); |
520 | for(auto iter = mLoadedResources.begin(); iter != mLoadedResources.end(); ++iter) |
521 | { |
522 | const LoadedResourceData& resData = iter->second; |
523 | |
524 | std::uint32_t refCount = resData.resource.mData->mRefCount.load(std::memory_order_relaxed); |
525 | assert(refCount > 0); // No references but kept in mLoadedResources list? |
526 | |
527 | if (refCount == resData.numInternalRefs) // Only internal references exist, free it |
528 | resourcesToUnload.push_back(resData.resource.lock()); |
529 | } |
530 | } |
531 | |
532 | // Note: When unloading multiple resources it's possible that unloading one will also unload |
533 | // another resource in "resourcesToUnload". This is fine because "unload" deals with invalid |
534 | // handles gracefully. |
535 | for(auto iter = resourcesToUnload.begin(); iter != resourcesToUnload.end(); ++iter) |
536 | { |
537 | release(*iter); |
538 | } |
539 | } |
540 | |
541 | void Resources::unloadAll() |
542 | { |
543 | // Unload and invalidate all resources |
544 | UnorderedMap<UUID, LoadedResourceData> loadedResourcesCopy; |
545 | |
546 | { |
547 | Lock lock(mLoadedResourceMutex); |
548 | loadedResourcesCopy = mLoadedResources; |
549 | } |
550 | |
551 | for (auto& loadedResourcePair : loadedResourcesCopy) |
552 | destroy(loadedResourcePair.second.resource); |
553 | } |
554 | |
555 | void Resources::destroy(ResourceHandleBase& resource) |
556 | { |
557 | if (resource.mData == nullptr) |
558 | return; |
559 | |
560 | RecursiveLock lock(mDestroyMutex); |
561 | |
562 | // If load in progress, first wait until it completes |
563 | const UUID& uuid = resource.getUUID(); |
564 | if (!resource.isLoaded(false)) |
565 | { |
566 | bool loadInProgress = false; |
567 | { |
568 | Lock lock(mInProgressResourcesMutex); |
569 | auto iterFind2 = mInProgressResources.find(uuid); |
570 | if (iterFind2 != mInProgressResources.end()) |
571 | loadInProgress = true; |
572 | } |
573 | |
574 | if (loadInProgress) // If it's still loading wait until that finishes |
575 | resource.blockUntilLoaded(); |
576 | else |
577 | return; // Already unloaded |
578 | } |
579 | |
580 | // At this point resource is guaranteed to be loaded and this state cannot change by some other thread because of |
581 | // the mDestroyMutex lock |
582 | |
583 | // Notify external systems before we actually destroy it |
584 | onResourceDestroyed(uuid); |
585 | resource.mData->mPtr->destroy(); |
586 | |
587 | { |
588 | Lock lock(mLoadedResourceMutex); |
589 | auto iterFind = mLoadedResources.find(uuid); |
590 | if (iterFind != mLoadedResources.end()) |
591 | { |
592 | LoadedResourceData& resData = iterFind->second; |
593 | while (resData.numInternalRefs > 0) |
594 | { |
595 | resData.numInternalRefs--; |
596 | resData.resource.removeInternalRef(); |
597 | } |
598 | |
599 | mLoadedResources.erase(iterFind); |
600 | } |
601 | else |
602 | { |
603 | assert(false); // This should never happen but in case it does fail silently in release mode |
604 | } |
605 | } |
606 | |
607 | resource.clearHandleData(); |
608 | } |
609 | |
610 | void Resources::save(const HResource& resource, const Path& filePath, bool overwrite, bool compress) |
611 | { |
612 | if (resource == nullptr) |
613 | return; |
614 | |
615 | if (!resource.isLoaded(false)) |
616 | { |
617 | bool loadInProgress = false; |
618 | { |
619 | Lock lock(mInProgressResourcesMutex); |
620 | auto iterFind2 = mInProgressResources.find(resource.getUUID()); |
621 | if (iterFind2 != mInProgressResources.end()) |
622 | loadInProgress = true; |
623 | } |
624 | |
625 | if (loadInProgress) // If it's still loading wait until that finishes |
626 | resource.blockUntilLoaded(); |
627 | else |
628 | return; // Nothing to save |
629 | } |
630 | |
631 | const bool fileExists = FileSystem::isFile(filePath); |
632 | if(fileExists && !overwrite) |
633 | { |
634 | LOGERR("Another file exists at the specified location. Not saving." ); |
635 | return; |
636 | } |
637 | |
638 | { |
639 | Lock lock(mDefaultManifestMutex); |
640 | mDefaultResourceManifest->registerResource(resource.getUUID(), filePath); |
641 | } |
642 | |
643 | _save(resource.getInternalPtr(), filePath, compress); |
644 | } |
645 | |
646 | void Resources::save(const HResource& resource, bool compress) |
647 | { |
648 | if (resource == nullptr) |
649 | return; |
650 | |
651 | Path path; |
652 | if (getFilePathFromUUID(resource.getUUID(), path)) |
653 | save(resource, path, true, compress); |
654 | } |
655 | |
656 | void Resources::_save(const SPtr<Resource>& resource, const Path& filePath, bool compress) |
657 | { |
658 | if (!resource->mKeepSourceData) |
659 | { |
660 | LOGWRN("Saving a resource that was created/loaded without ResourceLoadFlag::KeepSourceData. Some data might " |
661 | "not be available for saving. File path: " + filePath.toString()); |
662 | } |
663 | |
664 | Vector<ResourceDependency> dependencyList = Utility::findResourceDependencies(*resource); |
665 | Vector<UUID> dependencyUUIDs(dependencyList.size()); |
666 | for (UINT32 i = 0; i < (UINT32)dependencyList.size(); i++) |
667 | dependencyUUIDs[i] = dependencyList[i].resource.getUUID(); |
668 | |
669 | UINT32 compressionMethod = (compress && resource->isCompressible()) ? 1 : 0; |
670 | SPtr<SavedResourceData> resourceData = bs_shared_ptr_new<SavedResourceData>(dependencyUUIDs, |
671 | resource->allowAsyncLoading(), compressionMethod); |
672 | |
673 | Path parentDir = filePath.getDirectory(); |
674 | if (!FileSystem::exists(parentDir)) |
675 | FileSystem::createDir(parentDir); |
676 | |
677 | Path savePath; |
678 | const bool fileExists = FileSystem::isFile(filePath); |
679 | if(fileExists) |
680 | { |
681 | // If a file exists, save to a temporary location, then copy over only after a save was successful. This guards |
682 | // against data loss in case the save process fails. |
683 | |
684 | // TODO: Temp directory should always be on this drive, as files moved from one drive to another will in fact |
685 | // be copied |
686 | savePath = FileSystem::getTempDirectoryPath(); |
687 | savePath.setFilename(UUIDGenerator::generateRandom().toString()); |
688 | |
689 | UINT32 safetyCounter = 0; |
690 | while(FileSystem::exists(savePath)) |
691 | { |
692 | if(safetyCounter > 10) |
693 | { |
694 | LOGERR("Internal error. Unable to save resource due to not being able to find a unique filename." ); |
695 | return; |
696 | } |
697 | |
698 | savePath.setFilename(UUIDGenerator::generateRandom().toString()); |
699 | safetyCounter++; |
700 | } |
701 | |
702 | } |
703 | else |
704 | savePath = filePath; |
705 | |
706 | Lock fileLock = FileScheduler::getLock(filePath); |
707 | |
708 | std::ofstream stream; |
709 | stream.open(savePath.toPlatformString().c_str(), std::ios::out | std::ios::binary); |
710 | if (stream.fail()) |
711 | LOGWRN("Failed to save file: \"" + filePath.toString() + "\". Error: " + strerror(errno) + "." ); |
712 | |
713 | // Write meta-data |
714 | { |
715 | MemorySerializer ms; |
716 | UINT32 numBytes = 0; |
717 | UINT8* bytes = ms.encode(resourceData.get(), numBytes); |
718 | |
719 | stream.write((char*)&numBytes, sizeof(numBytes)); |
720 | stream.write((char*)bytes, numBytes); |
721 | |
722 | bs_free(bytes); |
723 | } |
724 | |
725 | // Write object data |
726 | { |
727 | MemorySerializer ms; |
728 | UINT32 numBytes = 0; |
729 | UINT8* bytes = ms.encode(resource.get(), numBytes); |
730 | |
731 | SPtr<MemoryDataStream> objStream = bs_shared_ptr_new<MemoryDataStream>(bytes, numBytes); |
732 | if (compressionMethod != 0) |
733 | { |
734 | SPtr<DataStream> srcStream = std::static_pointer_cast<DataStream>(objStream); |
735 | objStream = Compression::compress(srcStream); |
736 | } |
737 | |
738 | stream.write((char*)&numBytes, sizeof(numBytes)); |
739 | stream.write((char*)objStream->getPtr(), objStream->size()); |
740 | } |
741 | |
742 | stream.close(); |
743 | stream.clear(); |
744 | |
745 | if (fileExists) |
746 | { |
747 | FileSystem::remove(filePath); |
748 | FileSystem::move(savePath, filePath); |
749 | } |
750 | } |
751 | |
752 | void Resources::update(HResource& handle, const SPtr<Resource>& resource) |
753 | { |
754 | const UUID& uuid = handle.getUUID(); |
755 | handle.setHandleData(resource, uuid); |
756 | handle.notifyLoadComplete(); |
757 | |
758 | if(resource) |
759 | { |
760 | Lock lock(mLoadedResourceMutex); |
761 | auto iterFind = mLoadedResources.find(uuid); |
762 | if (iterFind == mLoadedResources.end()) |
763 | { |
764 | LoadedResourceData& resData = mLoadedResources[uuid]; |
765 | resData.resource = handle.getWeak(); |
766 | } |
767 | } |
768 | |
769 | onResourceModified(handle); |
770 | |
771 | // This method is not thread safe due to this call (callable from main thread only) |
772 | ResourceListenerManager::instance().notifyListeners(uuid); |
773 | } |
774 | |
775 | Vector<UUID> Resources::getDependencies(const Path& filePath) |
776 | { |
777 | SPtr<SavedResourceData> savedResourceData; |
778 | if (!filePath.isEmpty()) |
779 | { |
780 | FileDecoder fs(filePath); |
781 | savedResourceData = std::static_pointer_cast<SavedResourceData>(fs.decode()); |
782 | } |
783 | |
784 | return savedResourceData->getDependencies(); |
785 | } |
786 | |
787 | void Resources::registerResourceManifest(const SPtr<ResourceManifest>& manifest) |
788 | { |
789 | auto findIter = std::find(mResourceManifests.begin(), mResourceManifests.end(), manifest); |
790 | if(findIter == mResourceManifests.end()) |
791 | mResourceManifests.push_back(manifest); |
792 | else |
793 | *findIter = manifest; |
794 | } |
795 | |
796 | void Resources::unregisterResourceManifest(const SPtr<ResourceManifest>& manifest) |
797 | { |
798 | if (manifest->getName() == "Default" ) |
799 | return; |
800 | |
801 | auto findIter = std::find(mResourceManifests.begin(), mResourceManifests.end(), manifest); |
802 | if (findIter != mResourceManifests.end()) |
803 | mResourceManifests.erase(findIter); |
804 | } |
805 | |
806 | SPtr<ResourceManifest> Resources::getResourceManifest(const String& name) const |
807 | { |
808 | for(auto iter = mResourceManifests.rbegin(); iter != mResourceManifests.rend(); ++iter) |
809 | { |
810 | if(name == (*iter)->getName()) |
811 | return (*iter); |
812 | } |
813 | |
814 | return nullptr; |
815 | } |
816 | |
817 | bool Resources::isLoaded(const UUID& uuid, bool checkInProgress) |
818 | { |
819 | if (checkInProgress) |
820 | { |
821 | Lock inProgressLock(mInProgressResourcesMutex); |
822 | auto iterFind2 = mInProgressResources.find(uuid); |
823 | if (iterFind2 != mInProgressResources.end()) |
824 | { |
825 | return true; |
826 | } |
827 | } |
828 | |
829 | { |
830 | Lock loadedLock(mLoadedResourceMutex); |
831 | auto iterFind = mLoadedResources.find(uuid); |
832 | if (iterFind != mLoadedResources.end()) |
833 | { |
834 | return true; |
835 | } |
836 | } |
837 | |
838 | return false; |
839 | } |
840 | |
841 | float Resources::getLoadProgress(const HResource& resource, bool includeDependencies) |
842 | { |
843 | const UUID& uuid = resource.getUUID(); |
844 | if(uuid.empty()) |
845 | return 0.0f; |
846 | |
847 | Lock inProgressLock(mInProgressResourcesMutex); |
848 | Lock loadedLock(mLoadedResourceMutex); |
849 | |
850 | // Fully loaded |
851 | auto iterFind = mLoadedResources.find(uuid); |
852 | if (iterFind != mLoadedResources.end()) |
853 | return 1.0f; |
854 | |
855 | // Not loaded nor being loaded |
856 | auto iterFind2 = mInProgressResources.find(uuid); |
857 | if (iterFind2 == mInProgressResources.end()) |
858 | return 0.0f; |
859 | |
860 | ResourceLoadData* loadData = iterFind2->second; |
861 | |
862 | // Don't care about dependencies, just report own progress directly |
863 | if(!includeDependencies) |
864 | return loadData->progress.load(std::memory_order_relaxed); |
865 | |
866 | // Dependencies that are already fully loaded will just have their loaded sizes in 'dependencyLoadedAmount', while |
867 | // for those still in progress we need to check their load data |
868 | float totalBytesLoaded = (float)loadData->dependencyLoadedAmount; |
869 | for(auto& entry : loadData->dependencies) |
870 | { |
871 | auto iterFind3 = mInProgressResources.find(entry.getUUID()); |
872 | if (iterFind3 == mInProgressResources.end()) |
873 | continue; |
874 | |
875 | ResourceLoadData* dependencyLoadData = iterFind3->second; |
876 | totalBytesLoaded += dependencyLoadData->resData.size * dependencyLoadData->progress.load(std::memory_order_relaxed); |
877 | } |
878 | |
879 | totalBytesLoaded += loadData->resData.size * loadData->progress.load(std::memory_order_relaxed); |
880 | |
881 | float totalBytesToLoad = (float)(loadData->dependencySize + loadData->resData.size); |
882 | assert(totalBytesLoaded <= totalBytesToLoad); |
883 | |
884 | return std::min(1.0f, totalBytesLoaded / totalBytesToLoad); |
885 | } |
886 | |
887 | HResource Resources::_createResourceHandle(const SPtr<Resource>& obj) |
888 | { |
889 | UUID uuid = UUIDGenerator::generateRandom(); |
890 | return _createResourceHandle(obj, uuid); |
891 | } |
892 | |
893 | HResource Resources::_createResourceHandle(const SPtr<Resource>& obj, const UUID& UUID) |
894 | { |
895 | HResource newHandle(obj, UUID); |
896 | |
897 | { |
898 | Lock lock(mLoadedResourceMutex); |
899 | |
900 | if(obj) |
901 | { |
902 | LoadedResourceData& resData = mLoadedResources[UUID]; |
903 | resData.resource = newHandle.getWeak(); |
904 | } |
905 | |
906 | mHandles[UUID] = newHandle.getWeak(); |
907 | } |
908 | |
909 | return newHandle; |
910 | } |
911 | |
912 | HResource Resources::_getResourceHandle(const UUID& uuid) |
913 | { |
914 | Lock lock(mLoadedResourceMutex); |
915 | auto iterFind3 = mHandles.find(uuid); |
916 | if (iterFind3 != mHandles.end()) // Not loaded, but handle does exist |
917 | { |
918 | return iterFind3->second.lock(); |
919 | } |
920 | |
921 | // Create new handle |
922 | HResource handle(uuid); |
923 | mHandles[uuid] = handle.getWeak(); |
924 | |
925 | return handle; |
926 | } |
927 | |
928 | bool Resources::getFilePathFromUUID(const UUID& uuid, Path& filePath) const |
929 | { |
930 | // Default manifest is at 0th index but all other take priority since Default manifest could |
931 | // contain obsolete data. |
932 | for(auto iter = mResourceManifests.rbegin(); iter != mResourceManifests.rend(); ++iter) |
933 | { |
934 | if((*iter)->uuidToFilePath(uuid, filePath)) |
935 | return true; |
936 | } |
937 | |
938 | return false; |
939 | } |
940 | |
941 | bool Resources::getUUIDFromFilePath(const Path& path, UUID& uuid) const |
942 | { |
943 | Path manifestPath = path; |
944 | if (!manifestPath.isAbsolute()) |
945 | manifestPath.makeAbsolute(FileSystem::getWorkingDirectoryPath()); |
946 | |
947 | for(auto iter = mResourceManifests.rbegin(); iter != mResourceManifests.rend(); ++iter) |
948 | { |
949 | if ((*iter)->filePathToUUID(manifestPath, uuid)) |
950 | return true; |
951 | } |
952 | |
953 | return false; |
954 | } |
955 | |
956 | void Resources::loadComplete(HResource& resource, bool notifyProgress) |
957 | { |
958 | UUID uuid = resource.getUUID(); |
959 | |
960 | ResourceLoadData* myLoadData = nullptr; |
961 | bool finishLoad = true; |
962 | Vector<ResourceLoadData*> dependantLoads; |
963 | { |
964 | Lock inProgresslock(mInProgressResourcesMutex); |
965 | |
966 | auto iterFind = mInProgressResources.find(uuid); |
967 | if (iterFind != mInProgressResources.end()) |
968 | { |
969 | myLoadData = iterFind->second; |
970 | finishLoad = myLoadData->remainingDependencies == 0; |
971 | |
972 | if (finishLoad) |
973 | mInProgressResources.erase(iterFind); |
974 | } |
975 | |
976 | auto iterFind2 = mDependantLoads.find(uuid); |
977 | |
978 | if (iterFind2 != mDependantLoads.end()) |
979 | dependantLoads = iterFind2->second; |
980 | |
981 | if (finishLoad) |
982 | { |
983 | mDependantLoads.erase(uuid); |
984 | |
985 | // If loadedData is null then we're probably completing load on an already loaded resource, triggered |
986 | // by its dependencies. |
987 | if (myLoadData != nullptr && myLoadData->loadedData != nullptr) |
988 | { |
989 | Lock loadedLock(mLoadedResourceMutex); |
990 | |
991 | mLoadedResources[uuid] = myLoadData->resData; |
992 | resource.setHandleData(myLoadData->loadedData, uuid); |
993 | } |
994 | |
995 | resource.notifyLoadComplete(); |
996 | |
997 | for (auto& dependantLoad : dependantLoads) |
998 | dependantLoad->remainingDependencies--; |
999 | } |
1000 | } |
1001 | |
1002 | for (auto& dependantLoad : dependantLoads) |
1003 | { |
1004 | if(notifyProgress && myLoadData) |
1005 | dependantLoad->dependencyLoadedAmount += myLoadData->resData.size; |
1006 | |
1007 | HResource dependant = dependantLoad->resData.resource.lock(); |
1008 | loadComplete(dependant, false); |
1009 | } |
1010 | |
1011 | if (finishLoad && myLoadData != nullptr) |
1012 | { |
1013 | onResourceLoaded(resource); |
1014 | |
1015 | // This should only ever be true on the main thread |
1016 | if (myLoadData->notifyImmediately) |
1017 | ResourceListenerManager::instance().notifyListeners(uuid); |
1018 | |
1019 | bs_delete(myLoadData); |
1020 | } |
1021 | } |
1022 | |
1023 | void Resources::loadCallback(const Path& filePath, HResource& resource, bool loadWithSaveData) |
1024 | { |
1025 | ResourceLoadData* myLoadData; |
1026 | { |
1027 | Lock lock(mInProgressResourcesMutex); |
1028 | myLoadData = mInProgressResources[resource.getUUID()]; |
1029 | } |
1030 | |
1031 | SPtr<Resource> rawResource = loadFromDiskAndDeserialize(filePath, loadWithSaveData, myLoadData->progress); |
1032 | |
1033 | { |
1034 | Lock lock(mInProgressResourcesMutex); |
1035 | |
1036 | myLoadData->loadedData = rawResource; |
1037 | myLoadData->remainingDependencies--; |
1038 | myLoadData->progress.exchange(1.0f, std::memory_order_relaxed); |
1039 | } |
1040 | |
1041 | loadComplete(resource, true); |
1042 | } |
1043 | |
1044 | BS_CORE_EXPORT Resources& gResources() |
1045 | { |
1046 | return Resources::instance(); |
1047 | } |
1048 | } |
1049 | |