// // Copyright (c) 2008-2014 the Urho3D project. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #include "Precompiled.h" #include "Context.h" #include "CoreEvents.h" #include "FileSystem.h" #include "FileWatcher.h" #include "Image.h" #include "JSONFile.h" #include "Log.h" #include "PackageFile.h" #include "ResourceCache.h" #include "ResourceEvents.h" #include "XMLFile.h" #include "DebugNew.h" namespace Urho3D { static const char* checkDirs[] = { "Fonts", "Materials", "Models", "Music", "Objects", "Particle", "PostProcess", "RenderPaths", "Scenes", "Scripts", "Sounds", "Shaders", "Techniques", "Textures", "UI", 0 }; static const SharedPtr noResource; ResourceCache::ResourceCache(Context* context) : Object(context), autoReloadResources_(false), returnFailedResources_(false), searchPackagesFirst_(true) { // Register Resource library object factories RegisterResourceLibrary(context_); } ResourceCache::~ResourceCache() { } bool ResourceCache::AddResourceDir(const String& pathName, unsigned int priority) { FileSystem* fileSystem = GetSubsystem(); if (!fileSystem || !fileSystem->DirExists(pathName)) { LOGERROR("Could not open directory " + pathName); return false; } // Convert path to absolute String fixedPath = SanitateResourceDirName(pathName); // Check that the same path does not already exist for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { if (!resourceDirs_[i].Compare(fixedPath, false)) return true; } // If the priority isn't last or greater than size insert at position otherwise push. if (priority > PRIORITY_LAST && priority < resourceDirs_.Size()) resourceDirs_.Insert(priority, fixedPath); else resourceDirs_.Push(fixedPath); // If resource auto-reloading active, create a file watcher for the directory if (autoReloadResources_) { SharedPtr watcher(new FileWatcher(context_)); watcher->StartWatching(fixedPath, true); fileWatchers_.Push(watcher); } LOGINFO("Added resource path " + fixedPath); return true; } void ResourceCache::AddPackageFile(PackageFile* package, unsigned int priority) { // Do not add packages that failed to load if (!package || !package->GetNumFiles()) return; // If the priority isn't last or greater than size insert at position otherwise push. if (priority > PRIORITY_LAST && priority < packages_.Size()) packages_.Insert(priority, SharedPtr(package)); else packages_.Push(SharedPtr(package)); LOGINFO("Added resource package " + package->GetName()); } bool ResourceCache::AddManualResource(Resource* resource) { if (!resource) { LOGERROR("Null manual resource"); return false; } const String& name = resource->GetName(); if (name.Empty()) { LOGERROR("Manual resource with empty name, can not add"); return false; } resource->ResetUseTimer(); resourceGroups_[resource->GetType()].resources_[resource->GetNameHash()] = resource; UpdateResourceGroup(resource->GetType()); return true; } void ResourceCache::RemoveResourceDir(const String& pathName) { String fixedPath = SanitateResourceDirName(pathName); for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { if (!resourceDirs_[i].Compare(fixedPath, false)) { resourceDirs_.Erase(i); // Remove the filewatcher with the matching path for (unsigned j = 0; j < fileWatchers_.Size(); ++j) { if (!fileWatchers_[j]->GetPath().Compare(fixedPath, false)) { fileWatchers_.Erase(j); break; } } LOGINFO("Removed resource path " + fixedPath); return; } } } void ResourceCache::RemovePackageFile(PackageFile* package, bool releaseResources, bool forceRelease) { for (Vector >::Iterator i = packages_.Begin(); i != packages_.End(); ++i) { if (*i == package) { if (releaseResources) ReleasePackageResources(*i, forceRelease); LOGINFO("Removed resource package " + (*i)->GetName()); packages_.Erase(i); return; } } } void ResourceCache::RemovePackageFile(const String& fileName, bool releaseResources, bool forceRelease) { // Compare the name and extension only, not the path String fileNameNoPath = GetFileNameAndExtension(fileName); for (Vector >::Iterator i = packages_.Begin(); i != packages_.End(); ++i) { if (!GetFileNameAndExtension((*i)->GetName()).Compare(fileNameNoPath, false)) { if (releaseResources) ReleasePackageResources(*i, forceRelease); LOGINFO("Removed resource package " + (*i)->GetName()); packages_.Erase(i); return; } } } void ResourceCache::ReleaseResource(StringHash type, const String& name, bool force) { StringHash nameHash(name); const SharedPtr& existingRes = FindResource(type, nameHash); if (!existingRes) return; // If other references exist, do not release, unless forced if ((existingRes.Refs() == 1 && existingRes.WeakRefs() == 0) || force) { resourceGroups_[type].resources_.Erase(nameHash); UpdateResourceGroup(type); } } void ResourceCache::ReleaseResources(StringHash type, bool force) { bool released = false; HashMap::Iterator i = resourceGroups_.Find(type); if (i != resourceGroups_.End()) { for (HashMap >::Iterator j = i->second_.resources_.Begin(); j != i->second_.resources_.End();) { HashMap >::Iterator current = j++; // If other references exist, do not release, unless forced if ((current->second_.Refs() == 1 && current->second_.WeakRefs() == 0) || force) { i->second_.resources_.Erase(current); released = true; } } } if (released) UpdateResourceGroup(type); } void ResourceCache::ReleaseResources(StringHash type, const String& partialName, bool force) { bool released = false; HashMap::Iterator i = resourceGroups_.Find(type); if (i != resourceGroups_.End()) { for (HashMap >::Iterator j = i->second_.resources_.Begin(); j != i->second_.resources_.End();) { HashMap >::Iterator current = j++; if (current->second_->GetName().Contains(partialName)) { // If other references exist, do not release, unless forced if ((current->second_.Refs() == 1 && current->second_.WeakRefs() == 0) || force) { i->second_.resources_.Erase(current); released = true; } } } } if (released) UpdateResourceGroup(type); } void ResourceCache::ReleaseResources(const String& partialName, bool force) { // Some resources refer to others, like materials to textures. Release twice to ensure these get released. // This is not necessary if forcing release unsigned repeat = force ? 1 : 2; while (repeat--) { for (HashMap::Iterator i = resourceGroups_.Begin(); i != resourceGroups_.End(); ++i) { bool released = false; for (HashMap >::Iterator j = i->second_.resources_.Begin(); j != i->second_.resources_.End();) { HashMap >::Iterator current = j++; if (current->second_->GetName().Contains(partialName)) { // If other references exist, do not release, unless forced if ((current->second_.Refs() == 1 && current->second_.WeakRefs() == 0) || force) { i->second_.resources_.Erase(current); released = true; } } } if (released) UpdateResourceGroup(i->first_); } } } void ResourceCache::ReleaseAllResources(bool force) { unsigned repeat = force ? 1 : 2; while (repeat--) { for (HashMap::Iterator i = resourceGroups_.Begin(); i != resourceGroups_.End(); ++i) { bool released = false; for (HashMap >::Iterator j = i->second_.resources_.Begin(); j != i->second_.resources_.End();) { HashMap >::Iterator current = j++; // If other references exist, do not release, unless forced if ((current->second_.Refs() == 1 && current->second_.WeakRefs() == 0) || force) { i->second_.resources_.Erase(current); released = true; } } if (released) UpdateResourceGroup(i->first_); } } } bool ResourceCache::ReloadResource(Resource* resource) { if (!resource) return false; resource->SendEvent(E_RELOADSTARTED); bool success = false; SharedPtr file = GetFile(resource->GetName()); if (file) success = resource->Load(*(file.Get())); if (success) { resource->ResetUseTimer(); UpdateResourceGroup(resource->GetType()); resource->SendEvent(E_RELOADFINISHED); return true; } // If reloading failed, do not remove the resource from cache, to allow for a new live edit to // attempt loading again resource->SendEvent(E_RELOADFAILED); return false; } void ResourceCache::SetMemoryBudget(StringHash type, unsigned budget) { resourceGroups_[type].memoryBudget_ = budget; } void ResourceCache::SetAutoReloadResources(bool enable) { if (enable != autoReloadResources_) { if (enable) { for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { SharedPtr watcher(new FileWatcher(context_)); watcher->StartWatching(resourceDirs_[i], true); fileWatchers_.Push(watcher); } SubscribeToEvent(E_BEGINFRAME, HANDLER(ResourceCache, HandleBeginFrame)); } else { UnsubscribeFromEvent(E_BEGINFRAME); fileWatchers_.Clear(); } autoReloadResources_ = enable; } } void ResourceCache::SetReturnFailedResources(bool enable) { returnFailedResources_ = enable; } SharedPtr ResourceCache::GetFile(const String& nameIn, bool sendEventOnFailure) { String name = SanitateResourceName(nameIn); File* file = 0; if (searchPackagesFirst_) { file = SearchPackages(name); if (!file) file = SearchResourceDirs(name); } else { file = SearchResourceDirs(name); if (!file) file = SearchPackages(name); } if (file) return SharedPtr(file); if (sendEventOnFailure) { LOGERROR("Could not find resource " + name); using namespace ResourceNotFound; VariantMap& eventData = GetEventDataMap(); eventData[P_RESOURCENAME] = name; SendEvent(E_RESOURCENOTFOUND, eventData); } return SharedPtr(); } Resource* ResourceCache::GetResource(StringHash type, const String& name, bool sendEventOnFailure) { return GetResource(type, name.CString(), sendEventOnFailure); } Resource* ResourceCache::GetResource(StringHash type, const char* nameIn, bool sendEventOnFailure) { String name = SanitateResourceName(nameIn); // If empty name, return null pointer immediately if (name.Empty()) return 0; StringHash nameHash(name); const SharedPtr& existing = FindResource(type, nameHash); if (existing) return existing; SharedPtr resource; // Make sure the pointer is non-null and is a Resource subclass resource = DynamicCast(context_->CreateObject(type)); if (!resource) { LOGERROR("Could not load unknown resource type " + String(type)); using namespace UnknownResourceType; VariantMap& eventData = GetEventDataMap(); eventData[P_RESOURCETYPE] = type; SendEvent(E_UNKNOWNRESOURCETYPE, eventData); return 0; } // Attempt to load the resource SharedPtr file = GetFile(name, sendEventOnFailure); if (!file) return 0; // Error is already logged LOGDEBUG("Loading resource " + name); resource->SetName(file->GetName()); if (!resource->Load(*(file.Get()))) { // Error should already been logged by corresponding resource descendant class using namespace LoadFailed; VariantMap& eventData = GetEventDataMap(); eventData[P_RESOURCENAME] = name; SendEvent(E_LOADFAILED, eventData); if (!returnFailedResources_) return 0; } // Store to cache resource->ResetUseTimer(); resourceGroups_[type].resources_[nameHash] = resource; UpdateResourceGroup(type); return resource; } void ResourceCache::GetResources(PODVector& result, StringHash type) const { result.Clear(); HashMap::ConstIterator i = resourceGroups_.Find(type); if (i != resourceGroups_.End()) { for (HashMap >::ConstIterator j = i->second_.resources_.Begin(); j != i->second_.resources_.End(); ++j) result.Push(j->second_); } } bool ResourceCache::Exists(const String& nameIn) const { String name = SanitateResourceName(nameIn); if (name.Empty()) return false; for (unsigned i = 0; i < packages_.Size(); ++i) { if (packages_[i]->Exists(name)) return true; } FileSystem* fileSystem = GetSubsystem(); for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { if (fileSystem->FileExists(resourceDirs_[i] + name)) return true; } // Fallback using absolute path if (fileSystem->FileExists(name)) return true; return false; } unsigned ResourceCache::GetMemoryBudget(StringHash type) const { HashMap::ConstIterator i = resourceGroups_.Find(type); if (i != resourceGroups_.End()) return i->second_.memoryBudget_; else return 0; } unsigned ResourceCache::GetMemoryUse(StringHash type) const { HashMap::ConstIterator i = resourceGroups_.Find(type); if (i != resourceGroups_.End()) return i->second_.memoryUse_; else return 0; } unsigned ResourceCache::GetTotalMemoryUse() const { unsigned total = 0; for (HashMap::ConstIterator i = resourceGroups_.Begin(); i != resourceGroups_.End(); ++i) total += i->second_.memoryUse_; return total; } String ResourceCache::GetResourceFileName(const String& name) const { FileSystem* fileSystem = GetSubsystem(); for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { if (fileSystem->FileExists(resourceDirs_[i] + name)) return resourceDirs_[i] + name; } return String(); } String ResourceCache::GetPreferredResourceDir(const String& path) const { String fixedPath = AddTrailingSlash(path); bool pathHasKnownDirs = false; bool parentHasKnownDirs = false; FileSystem* fileSystem = GetSubsystem(); for (unsigned i = 0; checkDirs[i] != 0; ++i) { if (fileSystem->DirExists(fixedPath + checkDirs[i])) { pathHasKnownDirs = true; break; } } if (!pathHasKnownDirs) { String parentPath = GetParentPath(fixedPath); for (unsigned i = 0; checkDirs[i] != 0; ++i) { if (fileSystem->DirExists(parentPath + checkDirs[i])) { parentHasKnownDirs = true; break; } } // If path does not have known subdirectories, but the parent path has, use the parent instead if (parentHasKnownDirs) fixedPath = parentPath; } return fixedPath; } String ResourceCache::SanitateResourceName(const String& nameIn) const { // Sanitate unsupported constructs from the resource name String name = GetInternalPath(nameIn); name.Replace("../", ""); name.Replace("./", ""); // If the path refers to one of the resource directories, normalize the resource name FileSystem* fileSystem = GetSubsystem(); if (resourceDirs_.Size()) { String namePath = GetPath(name); String exePath = fileSystem->GetProgramDir(); for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { String relativeResourcePath = resourceDirs_[i]; if (relativeResourcePath.StartsWith(exePath)) relativeResourcePath = relativeResourcePath.Substring(exePath.Length()); if (namePath.StartsWith(resourceDirs_[i], false)) namePath = namePath.Substring(resourceDirs_[i].Length()); else if (namePath.StartsWith(relativeResourcePath, false)) namePath = namePath.Substring(relativeResourcePath.Length()); } name = namePath + GetFileNameAndExtension(name); } return name.Trimmed(); } String ResourceCache::SanitateResourceDirName(const String& nameIn) const { String fixedPath = AddTrailingSlash(nameIn); if (!IsAbsolutePath(fixedPath)) fixedPath = GetSubsystem()->GetCurrentDir() + fixedPath; // Sanitate away /./ construct fixedPath.Replace("/./", "/"); return fixedPath.Trimmed(); } void ResourceCache::StoreResourceDependency(Resource* resource, const String& dependency) { // If resource reloading is not on, do not create the dependency data structure (saves memory) if (!resource || !autoReloadResources_) return; StringHash nameHash(resource->GetName()); HashSet& dependents = dependentResources_[dependency]; dependents.Insert(nameHash); } void ResourceCache::ResetDependencies(Resource* resource) { if (!resource || !autoReloadResources_) return; StringHash nameHash(resource->GetName()); for (HashMap >::Iterator i = dependentResources_.Begin(); i != dependentResources_.End();) { HashSet& dependents = i->second_; dependents.Erase(nameHash); if (dependents.Empty()) i = dependentResources_.Erase(i); else ++i; } } const SharedPtr& ResourceCache::FindResource(StringHash type, StringHash nameHash) { HashMap::Iterator i = resourceGroups_.Find(type); if (i == resourceGroups_.End()) return noResource; HashMap >::Iterator j = i->second_.resources_.Find(nameHash); if (j == i->second_.resources_.End()) return noResource; return j->second_; } const SharedPtr& ResourceCache::FindResource(StringHash nameHash) { for (HashMap::Iterator i = resourceGroups_.Begin(); i != resourceGroups_.End(); ++i) { HashMap >::Iterator j = i->second_.resources_.Find(nameHash); if (j != i->second_.resources_.End()) return j->second_; } return noResource; } void ResourceCache::ReleasePackageResources(PackageFile* package, bool force) { HashSet affectedGroups; const HashMap& entries = package->GetEntries(); for (HashMap::ConstIterator i = entries.Begin(); i != entries.End(); ++i) { StringHash nameHash(i->first_); // We do not know the actual resource type, so search all type containers for (HashMap::Iterator j = resourceGroups_.Begin(); j != resourceGroups_.End(); ++j) { HashMap >::Iterator k = j->second_.resources_.Find(nameHash); if (k != j->second_.resources_.End()) { // If other references exist, do not release, unless forced if ((k->second_.Refs() == 1 && k->second_.WeakRefs() == 0) || force) { j->second_.resources_.Erase(k); affectedGroups.Insert(j->first_); } break; } } } for (HashSet::Iterator i = affectedGroups.Begin(); i != affectedGroups.End(); ++i) UpdateResourceGroup(*i); } void ResourceCache::UpdateResourceGroup(StringHash type) { HashMap::Iterator i = resourceGroups_.Find(type); if (i == resourceGroups_.End()) return; for (;;) { unsigned totalSize = 0; unsigned oldestTimer = 0; HashMap >::Iterator oldestResource = i->second_.resources_.End(); for (HashMap >::Iterator j = i->second_.resources_.Begin(); j != i->second_.resources_.End(); ++j) { totalSize += j->second_->GetMemoryUse(); unsigned useTimer = j->second_->GetUseTimer(); if (useTimer > oldestTimer) { oldestTimer = useTimer; oldestResource = j; } } i->second_.memoryUse_ = totalSize; // If memory budget defined and is exceeded, remove the oldest resource and loop again // (resources in use always return a zero timer and can not be removed) if (i->second_.memoryBudget_ && i->second_.memoryUse_ > i->second_.memoryBudget_ && oldestResource != i->second_.resources_.End()) { LOGDEBUG("Resource group " + oldestResource->second_->GetTypeName() + " over memory budget, releasing resource " + oldestResource->second_->GetName()); i->second_.resources_.Erase(oldestResource); } else break; } } void ResourceCache::HandleBeginFrame(StringHash eventType, VariantMap& eventData) { for (unsigned i = 0; i < fileWatchers_.Size(); ++i) { String fileName; while (fileWatchers_[i]->GetNextChange(fileName)) { StringHash fileNameHash(fileName); // If the filename is a resource we keep track of, reload it const SharedPtr& resource = FindResource(fileNameHash); if (resource) { LOGDEBUG("Reloading changed resource " + fileName); ReloadResource(resource); } // Always perform dependency resource check for resource loaded from XML file as it could be used in inheritance if (!resource || GetExtension(resource->GetName()) == ".xml") { // Check if this is a dependency resource, reload dependents HashMap >::ConstIterator j = dependentResources_.Find(fileNameHash); if (j != dependentResources_.End()) { // Reloading a resource may modify the dependency tracking structure. Therefore collect the // resources we need to reload first Vector > dependents; dependents.Reserve(j->second_.Size()); for (HashSet::ConstIterator k = j->second_.Begin(); k != j->second_.End(); ++k) { const SharedPtr& dependent = FindResource(*k); if (dependent) dependents.Push(dependent); } for (unsigned k = 0; k < dependents.Size(); ++k) { LOGDEBUG("Reloading resource " + dependents[k]->GetName() + " depending on " + fileName); ReloadResource(dependents[k]); } } } // Finally send a general file changed event even if the file was not a tracked resource using namespace FileChanged; VariantMap& eventData = GetEventDataMap(); eventData[P_FILENAME] = fileWatchers_[i]->GetPath() + fileName; eventData[P_RESOURCENAME] = fileName; SendEvent(E_FILECHANGED, eventData); } } } File* ResourceCache::SearchResourceDirs(const String& nameIn) { FileSystem* fileSystem = GetSubsystem(); for (unsigned i = 0; i < resourceDirs_.Size(); ++i) { if (fileSystem->FileExists(resourceDirs_[i] + nameIn)) { // Construct the file first with full path, then rename it to not contain the resource path, // so that the file's name can be used in further GetFile() calls (for example over the network) File* file(new File(context_, resourceDirs_[i] + nameIn)); file->SetName(nameIn); return file; } } // Fallback using absolute path if (fileSystem->FileExists(nameIn)) return new File(context_, nameIn); return 0; } File* ResourceCache::SearchPackages(const String& nameIn) { for (unsigned i = 0; i < packages_.Size(); ++i) { if (packages_[i]->Exists(nameIn)) return new File(context_, packages_[i], nameIn); } return 0; } void RegisterResourceLibrary(Context* context) { Image::RegisterObject(context); JSONFile::RegisterObject(context); XMLFile::RegisterObject(context); } }