diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 651eabbdb..debd4c69e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,6 +58,7 @@ set(Magnum_HEADERS Profiler.h Query.h Renderbuffer.h + ResourceManager.h Shader.h SizeTraits.h Swizzle.h diff --git a/src/ResourceManager.h b/src/ResourceManager.h new file mode 100644 index 000000000..06ac7a99c --- /dev/null +++ b/src/ResourceManager.h @@ -0,0 +1,540 @@ +#ifndef Magnum_ResourceManager_h +#define Magnum_ResourceManager_h +/* + Copyright © 2010, 2011, 2012 Vladimír Vondruš + + This file is part of Magnum. + + Magnum is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 3 + only, as published by the Free Software Foundation. + + Magnum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License version 3 for more details. +*/ + +/** @file + * @brief Class Magnum::ResourceManager, Magnum::ResourceKey, Magnum::Resource, enum Magnum::ResourceState, Magnum::ResourceDataState, Magnum::ResourcePolicy + */ + +#include +#include + +namespace Magnum { + +/** + * @brief %Resource state + * + * @see Resource::state(), ResourceManager::state() + */ +enum class ResourceState { + /** The resource is not yet loaded. */ + NotLoaded, + + /** The resource is not yet loaded and fallback resource is used instead. */ + Fallback, + + /** The resource is loaded, but can be changed by the manager at any time. */ + Mutable, + + /** The resource is loaded and won't be changed by the manager anymore. */ + Final +}; + +/** + * @brief %Resource data state + * + * @see ResourceManager::set() + */ +enum class ResourceDataState { + /** + * The resource can be changed by the manager in the future. This is + * slower, as Resource needs to ask the manager for new version every time + * the data are accessed, but allows changing the data for e.g. debugging + * purposes. + */ + Mutable = ResourceState::Mutable, + + /** + * The resource cannot be changed by the manager in the future. This is + * faster, as Resource instances will ask for the data only one time, thus + * suitable for production code. + */ + Final = ResourceState::Final +}; + +/** +@brief %Resource policy + +@see ResourceManager::set(), ResourceManager::free() + */ +enum class ResourcePolicy { + /** The resource will stay resident for whole lifetime of resource manager. */ + Resident, + + /** + * The resource will be unloaded when manually calling + * ResourceManager::free() if nothing references it. + */ + Manual, + + /** The resource will be unloaded when last reference to it is gone. */ + ReferenceCounted +}; + +/** +@brief Key for accessing resource + +@see ResourceManager::referenceCount(), ResourceManager::state(), + ResourceManager::get(), ResourceManager::set() +*/ +class ResourceKey: public Corrade::Utility::MurmurHash2::Digest { + public: + /** + * @brief Default constructor + * + * The same as calling other constructors with empty string. + */ + inline ResourceKey(): Corrade::Utility::MurmurHash2::Digest(Corrade::Utility::MurmurHash2()("")) {} + + /** @brief Constructor */ + inline ResourceKey(const std::string& key): Corrade::Utility::MurmurHash2::Digest(Corrade::Utility::MurmurHash2()(key)) {} + + /** + * @brief Constructor + * @todo constexpr + */ + template inline constexpr ResourceKey(const char(&key)[size]): Corrade::Utility::MurmurHash2::Digest(Corrade::Utility::MurmurHash2()(key)) {} +}; + +template class Resource; + +#ifndef DOXYGEN_GENERATING_OUTPUT +namespace Implementation { + struct ResourceKeyHash { + inline size_t operator()(ResourceKey key) const { + return *reinterpret_cast(key.byteArray()); + } + }; + + template class ResourceManagerData { + ResourceManagerData(const ResourceManagerData&) = delete; + ResourceManagerData(ResourceManagerData&&) = delete; + ResourceManagerData& operator=(const ResourceManagerData&) = delete; + ResourceManagerData& operator=(ResourceManagerData&&) = delete; + + public: + struct Data { + Data(const Data&) = delete; + Data& operator=(const Data&) = delete; + Data& operator=(Data&&) = delete; + + inline Data(): data(nullptr), state(ResourceDataState::Mutable), policy(ResourcePolicy::Manual), referenceCount(0) {} + + Data(Data&& other): data(other.data), state(other.state), policy(other.policy), referenceCount(other.referenceCount) { + other.data = nullptr; + other.referenceCount = 0; + } + + ~Data() { + CORRADE_ASSERT(referenceCount == 0, "ResourceManager: cannot destruct it while data are still referenced", ); + delete data; + } + + T* data; + ResourceDataState state; + ResourcePolicy policy; + size_t referenceCount; + }; + + inline virtual ~ResourceManagerData() { + delete _fallback; + } + + inline size_t lastChange() const { return _lastChange; } + + inline size_t count() const { return _data.size(); } + + size_t referenceCount(ResourceKey key) const { + auto it = _data.find(key); + if(it == _data.end()) return 0; + return it->second.referenceCount; + } + + ResourceState state(ResourceKey key) const { + auto it = _data.find(key); + if(it == _data.end() || !it->second.data) + return _fallback ? ResourceState::Fallback : ResourceState::NotLoaded; + else + return static_cast(it->second.state); + } + + template inline Resource get(ResourceKey key) { + return Resource(this, key); + } + + void set(ResourceKey key, T* data, ResourceDataState state, ResourcePolicy policy) { + auto it = _data.find(key); + + /* Cannot change resource with already final state */ + CORRADE_ASSERT(it == _data.end() || it->second.state != ResourceDataState::Final, "ResourceManager: cannot change already final resource", ); + + /* If nothing is referencing reference-counted resource, we're done */ + if(policy == ResourcePolicy::ReferenceCounted && (it == _data.end() || it->second.referenceCount == 0)) { + Corrade::Utility::Warning() << "ResourceManager: Reference-counted resource with key" << key << "isn't referenced from anywhere, deleting it immediately"; + delete data; + + /* Delete also already present resource (it could be here + because previous policy could be other than + ReferenceCounted) */ + if(it != _data.end()) _data.erase(it); + + return; + + /* Insert it, if not already here */ + } else if(it == _data.end()) + it = _data.insert(std::make_pair(key, Data())).first; + + /* Replace previous data */ + delete it->second.data; + it->second.data = data; + it->second.state = state; + it->second.policy = policy; + ++_lastChange; + } + + inline void setFallback(T* data) { + delete _fallback; + _fallback = data; + } + + void free() { + /* Delete all non-referenced non-resident resources */ + for(auto it = _data.begin(); it != _data.end(); ) { + if(it->second.policy != ResourcePolicy::Resident && !it->second.referenceCount) + it = _data.erase(it); + else ++it; + } + } + + inline T* fallback() const { return _fallback; } + + inline const Data& data(ResourceKey key) { + return _data[key]; + } + + inline void incrementReferenceCount(ResourceKey key) { + ++_data[key].referenceCount; + } + + inline void decrementReferenceCount(ResourceKey key) { + auto it = _data.find(key); + + /* Free the resource if it is reference counted */ + if(--it->second.referenceCount == 0 && it->second.policy == ResourcePolicy::ReferenceCounted) + _data.erase(it); + } + + protected: + inline ResourceManagerData(): _fallback(nullptr), _lastChange(0) {} + + private: + std::unordered_map _data; + T* _fallback; + size_t _lastChange; + }; +} +#endif + +/** +@brief %Resource reference + +See ResourceManager for more information. +*/ +template class Resource { + friend class Implementation::ResourceManagerData; + + public: + /** + * @brief Default constructor + * + * Creates empty resource. Resources are acquired from the manager by + * calling ResourceManager::get(). + */ + inline Resource(): manager(nullptr), lastCheck(0), _state(ResourceState::Final), data(nullptr) {} + + /** @brief Copy constructor */ + inline Resource(const Resource& other): manager(other.manager), key(other.key), lastCheck(other.lastCheck), _state(other._state), data(other.data) { + if(manager) manager->incrementReferenceCount(key); + } + + /** @brief Destructor */ + inline ~Resource() { + if(manager) manager->decrementReferenceCount(key); + } + + /** @brief Assignment operator */ + Resource& operator=(const Resource& other) { + if(manager) manager->decrementReferenceCount(key); + + manager = other.manager; + key = other.key; + lastCheck = other.lastCheck; + _state = other._state; + data = other.data; + + if(manager) manager->incrementReferenceCount(key); + return *this; + } + + /** + * @brief %Resource state + * + * @see operator bool() + */ + inline ResourceState state() { + acquire(); + return _state; + } + + /** + * @brief Whether the resource is available + * @return False when resource is not loaded and no fallback is + * available, true otherwise. + * + * @see state() + */ + inline operator bool() { + acquire(); + return data; + } + + /** @brief %Resource data */ + inline U& operator*() { + acquire(); + return *static_cast(data); + } + + /** @brief %Resource data */ + inline U* operator->() { + acquire(); + return static_cast(data); + } + + private: + inline Resource(Implementation::ResourceManagerData* manager, ResourceKey key): manager(manager), key(key), lastCheck(0) { + manager->incrementReferenceCount(key); + } + + void acquire() { + /* The data are already final, nothing to do */ + if(_state == ResourceState::Final) return; + + /* Nothing changed since last check */ + if(manager->lastChange() < lastCheck) return; + + /* Acquire new data and save last check time */ + const typename Implementation::ResourceManagerData::Data& d = manager->data(key); + lastCheck = manager->lastChange(); + + /* Try to get the data */ + if((data = d.data)) + _state = static_cast(d.state); + else if((data = manager->fallback())) + _state = ResourceState::Fallback; + else + _state = ResourceState::NotLoaded; + } + + Implementation::ResourceManagerData* manager; + ResourceKey key; + size_t lastCheck; + ResourceState _state; + T* data; +}; + +/** +@brief %Resource manager + +Provides storage for arbitrary set of types, accessible globally using +instance(). + +Each resource is referenced from Resource class. For optimizing performance, +each resource can be set as mutable or final. Mutable resources can be +modified by the manager and thus each %Resource instance asks the manager for +modifications on each access. On the other hand, final resources cannot be +modified by the manager, so %Resource instances don't have to ask the manager +every time, which is faster. + +It's possible to provide fallback for resources which are not available using +setFallback(). Accessing data of such resources will access the fallback +instead of failing on null pointer dereference. Availability and state of each +resource can be queried through function state() on the manager or +Resource::state() on each resource. + +The resources can be managed in three ways - resident resources, which stay in +memory for whole lifetime of the manager, manually managed resources, which +can be deleted by calling free() if nothing references them anymore, and +reference counted resources, which are deleted as soon as the last reference +to them is removed. + +%Resource state and policy is configured when setting the resource data in +set() and can be changed each time the data are updated, although already +final resources cannot obviously be set as mutable again. + +Basic usage is: +- Typedef'ing manager of desired types, creating its instance. +@code +typedef ResourceManager MyResourceManager; +MyResourceManager manager; +@endcode +- Filling the manager with resource data and acquiring the resources. Note + that a resource can be acquired with get() even before the manager contains + the data for it, as long as the resource data are not accessed (or fallback + is provided). +@code +MyResourceManager* manager = MyResourceManager::instance(); +Resource texture(manager->get("texture")); +Resource shader(manager->get("shader")); +Resource cube(manager->get("cube")); + +// The manager doesn't have data for the mesh yet, add them +if(!mesh) { + Mesh* mesh = new Mesh; + // ... + manager->set("cube", mesh, ResourceDataState::Final, ResourcePolicy::Resident); +} +@endcode +- Using the resource data. +@code +shader->use(); +texture->bind(); +cube->draw(); +@endcode +- Destroying resource references and deleting manager instance when nothing + references the resources anymore. +*/ +template class ResourceManager: protected Implementation::ResourceManagerData... { + public: + /** @brief Global instance */ + inline static ResourceManager* instance() { return _instance; } + + /** + * @brief Constructor + * + * Sets global instance pointer to itself. + * @attention Only one instance of given ResourceManager type can be + * created. + * @see instance() + */ + inline ResourceManager() { + CORRADE_ASSERT(!_instance, "ResourceManager: another instance is already created!", ); + _instance = this; + } + + /** + * @brief Destructor + * + * Sets global instance pointer to `nullptr`. + * @see instance() + */ + inline ~ResourceManager() { _instance = nullptr; } + + /** @brief Count of resources of given type */ + template inline size_t count() { + return this->Implementation::ResourceManagerData::count(); + } + + /** + * @brief Get resource reference + * + * In some cases it's desirable to store various different types under + * one base type for memory efficiency reasons. To avoid putting the + * responsibility of proper casting on the user, the acquired resource + * can be defined to cast the type automatically when accessing the + * data. This is commonly used for shaders, e.g.: + * @code + * Resource shader = manager->get("shader"); + * @endcode + */ + template inline Resource get(ResourceKey key) { + return this->Implementation::ResourceManagerData::template get(key); + } + + /** + * @brief Reference count of given resource + * + * @see set() + */ + template inline size_t referenceCount(ResourceKey key) const { + return this->Implementation::ResourceManagerData::referenceCount(key); + } + + /** + * @brief %Resource state + * + * @see set(), Resource::state() + */ + template inline ResourceState state(ResourceKey key) const { + return this->Implementation::ResourceManagerData::state(key); + } + + /** + * @brief Set resource data + * + * If @p policy is set to `ResourcePolicy::ReferenceCounted`, there + * must be already at least one reference to given resource, otherwise + * the data will be deleted immediately and no resource will be + * added. To avoid spending unnecessary loading time, add + * reference-counted resources only if they are already referenced: + * @code + * if(manager.referenceCount("myresource")) { + * // load data... + * manager.set("myresource", data, state, ResourcePolicy::ReferenceCounted); + * } + * @endcode + * @attention If resource state is already `ResourceState::Final`, + * subsequent updates are not possible. + * @see referenceCount(), state() + */ + template inline void set(ResourceKey key, T* data, ResourceDataState state, ResourcePolicy policy) { + this->Implementation::ResourceManagerData::set(key, data, state, policy); + } + + /** @brief Set fallback for not found resources */ + template inline void setFallback(T* data) { + return this->Implementation::ResourceManagerData::setFallback(data); + } + + /** @brief Free all resources of given type which are not referenced */ + template inline void free() { + return this->Implementation::ResourceManagerData::free(); + } + + /** @brief Free all resources which are not referenced */ + inline void free() { + freeInternal(std::common_type()...); + } + + private: + template inline void freeInternal(std::common_type, std::common_type... t) { + free(); + freeInternal(t...); + } + inline void freeInternal() const {} + + static ResourceManager* _instance; +}; + +/** @debugoperator{Magnum::ResourceKey} */ +template Corrade::Utility::Debug operator<<(Corrade::Utility::Debug debug, const ResourceKey& value) { + return debug << static_cast&>(value); +} + +template ResourceManager* ResourceManager::_instance(nullptr); + +} + +#endif diff --git a/src/Test/CMakeLists.txt b/src/Test/CMakeLists.txt index f86d239e8..87e9f71e2 100644 --- a/src/Test/CMakeLists.txt +++ b/src/Test/CMakeLists.txt @@ -1,2 +1,5 @@ corrade_add_test2(ColorTest ColorTest.cpp) +corrade_add_test2(ResourceManagerTest ResourceManagerTest.cpp) corrade_add_test2(SwizzleTest SwizzleTest.cpp) + +set_target_properties(ResourceManagerTest PROPERTIES COMPILE_FLAGS -DCORRADE_GRACEFUL_ASSERT) diff --git a/src/Test/ResourceManagerTest.cpp b/src/Test/ResourceManagerTest.cpp new file mode 100644 index 000000000..872dfefa1 --- /dev/null +++ b/src/Test/ResourceManagerTest.cpp @@ -0,0 +1,119 @@ +/* + Copyright © 2010, 2011, 2012 Vladimír Vondruš + + This file is part of Magnum. + + Magnum is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 3 + only, as published by the Free Software Foundation. + + Magnum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License version 3 for more details. +*/ + +#include "ResourceManagerTest.h" + +#include + +#include "ResourceManager.h" + +using namespace std; +using namespace Corrade::Utility; + +CORRADE_TEST_MAIN(Magnum::Test::ResourceManagerTest) + +namespace Magnum { namespace Test { + +size_t Data::count = 0; + +ResourceManagerTest::ResourceManagerTest() { + rm = new ResourceManager; + + addTests(&ResourceManagerTest::state, + &ResourceManagerTest::referenceCountedPolicy, + &ResourceManagerTest::manualPolicy, + &ResourceManagerTest::destroy); +} + +void ResourceManagerTest::state() { + ResourceKey questionKey("the-question"); + rm->set(questionKey, new int(10), ResourceDataState::Mutable, ResourcePolicy::Resident); + Resource theQuestion = rm->get(questionKey); + CORRADE_VERIFY(theQuestion.state() == ResourceState::Mutable); + CORRADE_COMPARE(*theQuestion, 10); + + /* Check that hash function is working properly */ + ResourceKey answerKey("the-answer"); + rm->set(answerKey, new int(42), ResourceDataState::Final, ResourcePolicy::Resident); + Resource theAnswer = rm->get(answerKey); + CORRADE_VERIFY(theAnswer.state() == ResourceState::Final); + CORRADE_COMPARE(*theAnswer, 42); + + CORRADE_COMPARE(rm->count(), 2); + + /* Cannot change already final resource */ + stringstream out; + Error::setOutput(&out); + rm->set(answerKey, new int(43), ResourceDataState::Mutable, ResourcePolicy::Resident); + CORRADE_COMPARE(*theAnswer, 42); + CORRADE_COMPARE(out.str(), "ResourceManager: cannot change already final resource\n"); + + /* Check non-final resource changes */ + rm->set(questionKey, new int(20), ResourceDataState::Final, ResourcePolicy::Resident); + CORRADE_VERIFY(theQuestion.state() == ResourceState::Final); + CORRADE_COMPARE(*theQuestion, 20); +} + +void ResourceManagerTest::referenceCountedPolicy() { + ResourceKey dataRefCountKey("dataRefCount"); + + /* Reference counted resources must be requested first */ + { + rm->set(dataRefCountKey, new Data(), ResourceDataState::Final, ResourcePolicy::ReferenceCounted); + CORRADE_COMPARE(rm->count(), 0); + Resource data = rm->get(dataRefCountKey); + CORRADE_VERIFY(data.state() == ResourceState::NotLoaded); + CORRADE_COMPARE(Data::count, 0); + } { + Resource data = rm->get(dataRefCountKey); + CORRADE_COMPARE(rm->count(), 1); + CORRADE_VERIFY(data.state() == ResourceState::NotLoaded); + rm->set(dataRefCountKey, new Data(), ResourceDataState::Final, ResourcePolicy::ReferenceCounted); + CORRADE_VERIFY(data.state() == ResourceState::Final); + CORRADE_COMPARE(Data::count, 1); + } + + CORRADE_COMPARE(rm->count(), 0); + CORRADE_COMPARE(Data::count, 0); +} + +void ResourceManagerTest::manualPolicy() { + ResourceKey dataKey("data"); + + /* Manual free */ + { + rm->set(dataKey, new Data(), ResourceDataState::Mutable, ResourcePolicy::Manual); + Resource data = rm->get(dataKey); + rm->free(); + } + + CORRADE_COMPARE(rm->count(), 1); + CORRADE_COMPARE(Data::count, 1); + rm->free(); + CORRADE_COMPARE(rm->count(), 0); + CORRADE_COMPARE(Data::count, 0); + + rm->set(dataKey, new Data(), ResourceDataState::Mutable, ResourcePolicy::Manual); + CORRADE_COMPARE(rm->count(), 1); + CORRADE_COMPARE(Data::count, 1); +} + +void ResourceManagerTest::destroy() { + delete rm; + rm = nullptr; + CORRADE_COMPARE(Data::count, 0); +} + +}} diff --git a/src/Test/ResourceManagerTest.h b/src/Test/ResourceManagerTest.h new file mode 100644 index 000000000..2f5ba5243 --- /dev/null +++ b/src/Test/ResourceManagerTest.h @@ -0,0 +1,51 @@ +#ifndef Magnum_Test_ResourceManagerTest_h +#define Magnum_Test_ResourceManagerTest_h +/* + Copyright © 2010, 2011, 2012 Vladimír Vondruš + + This file is part of Magnum. + + Magnum is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 3 + only, as published by the Free Software Foundation. + + Magnum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License version 3 for more details. +*/ + +#include + +namespace Magnum { + +template class ResourceManager; + +namespace Test { + +class Data { + public: + static size_t count; + + inline Data() { ++count; } + inline ~Data() { --count; } +}; + +typedef Magnum::ResourceManager ResourceManager; + +class ResourceManagerTest: public Corrade::TestSuite::Tester { + public: + ResourceManagerTest(); + + void state(); + void referenceCountedPolicy(); + void manualPolicy(); + void destroy(); + + private: + ResourceManager* rm; +}; + +}} + +#endif