From 7c7bcf80cf7d641a8dfdf34d4c0db3970254d6cf Mon Sep 17 00:00:00 2001 From: Michele Caini Date: Sat, 11 Nov 2017 23:46:10 +0100 Subject: [PATCH] added stuff for resource management --- src/entt/resource/cache.hpp | 183 ++++++++++++++++++++++++++++++++ src/entt/resource/handle.hpp | 115 ++++++++++++++++++++ src/entt/resource/loader.hpp | 62 +++++++++++ test/entt/resource/resource.cpp | 80 ++++++++++++++ 4 files changed, 440 insertions(+) create mode 100644 src/entt/resource/cache.hpp create mode 100644 src/entt/resource/handle.hpp create mode 100644 src/entt/resource/loader.hpp create mode 100644 test/entt/resource/resource.cpp diff --git a/src/entt/resource/cache.hpp b/src/entt/resource/cache.hpp new file mode 100644 index 000000000..02aefcbf1 --- /dev/null +++ b/src/entt/resource/cache.hpp @@ -0,0 +1,183 @@ +#ifndef ENTT_RESOURCE_CACHE_HPP +#define ENTT_RESOURCE_CACHE_HPP + + +#include +#include +#include +#include +#include "../core/hashed_string.hpp" +#include "handle.hpp" +#include "loader.hpp" + + +namespace entt { + + +/** + * @brief Simple cache for resources of a given type. + * + * Minimal implementation of a cache for resources of a given type. It doesn't + * offer much functionalities but it's suitable for small or medium sized + * applications and can be freely inherited to add targeted functionalities for + * large sized applications. + * + * @tparam Resource Type of resources managed by a cache. + */ +template +class ResourceCache { + using container_type = std::unordered_map>; + +public: + /*! @brief Unsigned integer type. */ + using size_type = typename container_type::size_type; + /*! @brief Type of resources managed by a cache. */ + using resource_type = HashedString; + + /*! @brief Default constructor. */ + ResourceCache() = default; + + /*! @brief Copying a cache isn't allowed. */ + ResourceCache(const ResourceCache &) noexcept = delete; + /*! @brief Default move constructor. */ + ResourceCache(ResourceCache &&) noexcept = default; + + /*! @brief Copying a cache isn't allowed. @return This cache. */ + ResourceCache & operator=(const ResourceCache &) noexcept = delete; + /*! @brief Default move assignment operator. @return This cache. */ + ResourceCache & operator=(ResourceCache &&) noexcept = default; + + /** + * @brief Number of resources managed by a cache. + * @return Number of resources currently stored. + */ + size_type size() const noexcept { + return resources.size(); + } + + /** + * @brief Returns true if a cache contains no resources, false otherwise. + * @return True if the cache contains no resources, false otherwise. + */ + bool empty() const noexcept { + return resources.empty(); + } + + /** + * @brief Clears a cache and discards all its resources. + * + * Handles are not invalidated and the memory used by a resource isn't + * freed as long as at least a handle keeps the resource itself alive. + */ + void clear() noexcept { + resources.clear(); + } + + /** + * @brief Loads the resource that corresponds to the given identifier. + * + * In case an identifier isn't already present in the cache, it loads its + * resource and stores it aside for future uses. Arguments are forwarded + * directly to the loader in order to construct properly the requested + * resource. + * + * @note + * If the identifier is already present in the cache, this function does + * nothing and the arguments are simply discarded. + * + * @tparam Loader Type of loader to use to load the resource if required. + * @tparam Args Types of arguments to use to load the resource if required. + * @param id Unique resource identifier. + * @param args Arguments to use to load the resource if required. + * @return True if the resource is ready to use, false otherwise. + */ + template + bool load(resource_type id, Args&&... args) { + static_assert(std::is_base_of, Loader>::value, "!"); + + bool loaded = true; + + if(resources.find(id) == resources.cend()) { + std::shared_ptr resource = Loader{}.get(std::forward(args)...); + loaded = (static_cast(resource) ? (resources[id] = std::move(resource), loaded) : false); + } + + return loaded; + } + + /** + * @brief Reloads a resource or loads it for the first time if not present. + * + * Equivalent to the following snippet (pseudocode): + * + * @code{.cpp} + * cache.discard(id); + * cache.load(id, args...); + * @endcode + * + * Arguments are forwarded directly to the loader in order to construct + * properly the requested resource. + * + * @tparam Loader Type of loader to use to load the resource. + * @tparam Args Types of arguments to use to load the resource. + * @param id Unique resource identifier. + * @param args Arguments to use to load the resource. + * @return True if the resource is ready to use, false otherwise. + */ + template + void reload(resource_type id, Args&&... args) { + return (discard(id), load(id, std::forward(args)...)); + } + + /** + * @brief Creates a handle for the given resource identifier. + * + * A resource handle can be in a either valid or invalid state. In other + * terms, a resource handle is properly initialized with a resource if the + * cache contains the resource itself. Otherwise the returned handle is + * uninitialized and accessing it results in undefined behavior. + * + * @sa ResourceHandle + * + * @param id Unique resource identifier. + * @return A handle for the given resource. + */ + ResourceHandle handle(resource_type id) const { + auto it = resources.find(id); + return { it == resources.end() ? nullptr : it->second }; + } + + /** + * @brief Checks if a cache contains the given identifier. + * @param id Unique resource identifier. + * @return True if the cache contains the resource, false otherwise. + */ + bool contains(resource_type id) const noexcept { + return !(resources.find(id) == resources.cend()); + } + + /** + * @brief Discards the resource that corresponds to the given identifier. + * + * Handles are not invalidated and the memory used by the resource isn't + * freed as long as at least a handle keeps the resource itself alive. + * + * @param id Unique resource identifier. + */ + void discard(resource_type id) noexcept { + auto it = resources.find(id); + + if(it != resources.end()) { + resources.erase(it); + } + } + +private: + container_type resources; +}; + + +} + + +#endif // ENTT_RESOURCE_CACHE_HPP diff --git a/src/entt/resource/handle.hpp b/src/entt/resource/handle.hpp new file mode 100644 index 000000000..875434835 --- /dev/null +++ b/src/entt/resource/handle.hpp @@ -0,0 +1,115 @@ +#ifndef ENTT_RESOURCE_HANDLE_HPP +#define ENTT_RESOURCE_HANDLE_HPP + + +#include +#include +#include + + +namespace entt { + + +template +struct ResourceCache; + + +/** + * @brief Shared resource handle. + * + * A shared resource handle is a small class that wraps a resource and keeps it + * alive even if it's deleted from the cache. It can be either copied or + * moved. A handle shares a reference to the same resource with all the other + * handles constructed for the same identifier.
+ * As a rule of thumb, resources should never be copied nor moved. Handles are + * the way to go to keep references to them. + * + * @tparam Resource Type of resource managed by a handle. + */ +template +class ResourceHandle final { + /*! @brief Resource handles are friends of their caches. */ + friend class ResourceCache; + + ResourceHandle(std::shared_ptr res) noexcept + : resource{std::move(res)} + {} + +public: + /*! @brief Default copy constructor. */ + ResourceHandle(const ResourceHandle &) noexcept = default; + /*! @brief Default move constructor. */ + ResourceHandle(ResourceHandle &&) noexcept = default; + + /*! @brief Default copy assignment operator. @return This handle. */ + ResourceHandle & operator=(const ResourceHandle &) noexcept = default; + /*! @brief Default move assignment operator. @return This handle. */ + ResourceHandle & operator=(ResourceHandle &&) noexcept = default; + + /** + * @brief Gets a reference to the managed resource. + * + * @warning + * The behavior is undefined if the handle doesn't contain a resource.
+ * An assertion will abort the execution at runtime in debug mode if the + * handle is empty. + * + * @return A reference to the managed resource. + */ + const Resource & get() const noexcept { + assert(static_cast(resource)); + return *resource; + } + + /** + * @brief Casts a handle and gets a reference to the managed resource. + * + * @warning + * The behavior is undefined if the handle doesn't contain a resource.
+ * An assertion will abort the execution at runtime in debug mode if the + * handle is empty. + */ + inline operator const Resource & () const noexcept { return get(); } + + /** + * @brief Dereferences a handle to obtain the managed resource. + * + * @warning + * The behavior is undefined if the handle doesn't contain a resource.
+ * An assertion will abort the execution at runtime in debug mode if the + * handle is empty. + * + * @return A reference to the managed resource. + */ + inline const Resource & operator *() const noexcept { return get(); } + + /** + * @brief Gets a pointer to the managed resource from a handle . + * + * @warning + * The behavior is undefined if the handle doesn't contain a resource.
+ * An assertion will abort the execution at runtime in debug mode if the + * handle is empty. + * + * @return A pointer to the managed resource or `nullptr` if the handle + * contains no resource at all. + */ + inline const Resource * operator ->() const noexcept { + assert(static_cast(resource)); + return resource.get(); + } + + /** + * @brief Returns true if the handle contains a resource, false otherwise. + */ + explicit operator bool() const { return static_cast(resource); } + +private: + std::shared_ptr resource; +}; + + +} + + +#endif // ENTT_RESOURCE_HANDLE_HPP diff --git a/src/entt/resource/loader.hpp b/src/entt/resource/loader.hpp new file mode 100644 index 000000000..160449013 --- /dev/null +++ b/src/entt/resource/loader.hpp @@ -0,0 +1,62 @@ +#ifndef ENTT_RESOURCE_LOADER_HPP +#define ENTT_RESOURCE_LOADER_HPP + + +#include + + +namespace entt { + + +template +struct ResourceCache; + + +/** + * @brief Base class for resource loaders. + * + * Resource loaders must inherit from this class and stay true to the CRTP + * idiom. Moreover, a resource loader must expose a public, const member + * function named `load` that accepts a variable number of arguments and return + * a shared pointer to the resource just created.
+ * As an example: + * + * @code{.cpp} + * struct MyResource {}; + * + * struct MyLoader: entt::ResourceLoader { + * std::shared_ptr load(int) const { + * // use the integer value somehow + * return std::make_shared(); + * } + * }; + * @endcode + * + * In general, resource loaders should not have a state or retain data of any + * type. They should let the cache manage their resources instead. + * + * @note + * Base class and CRTP idiom aren't strictly required with the current + * implementation. One could argue that a cache can easily work with loaders of + * any type. However, future changes won't be breaking ones by forcing the use + * of a base class today and that's why the model is already in its place. + * + * @tparam Loader Type of the derived class. + * @tparam Resource Type of resource for which to use the loader. + */ +template +class ResourceLoader { + /*! @brief Resource loaders are friends of their caches. */ + friend class ResourceCache; + + template + std::shared_ptr get(Args&&... args) const { + return static_cast(this)->load(std::forward(args)...); + } +}; + + +} + + +#endif // ENTT_RESOURCE_LOADER_HPP diff --git a/test/entt/resource/resource.cpp b/test/entt/resource/resource.cpp new file mode 100644 index 000000000..9f713b798 --- /dev/null +++ b/test/entt/resource/resource.cpp @@ -0,0 +1,80 @@ +#include +#include + +struct Resource { const int value; }; + +struct Loader: entt::ResourceLoader { + std::shared_ptr load(int value) const { + return std::shared_ptr(new Resource{ value }); + } +}; + +struct BrokenLoader: entt::ResourceLoader { + std::shared_ptr load(int) const { + return nullptr; + } +}; + +TEST(ResourceCache, Functionalities) { + entt::ResourceCache cache; + + constexpr auto hs1 = entt::HashedString{"res1"}; + constexpr auto hs2 = entt::HashedString{"res2"}; + + ASSERT_EQ(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_TRUE(cache.empty()); + ASSERT_FALSE(cache.contains(hs1)); + ASSERT_FALSE(cache.contains(hs2)); + + ASSERT_FALSE(cache.load(hs1, 42)); + + ASSERT_EQ(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_TRUE(cache.empty()); + ASSERT_FALSE(cache.contains(hs1)); + ASSERT_FALSE(cache.contains(hs2)); + + ASSERT_TRUE(cache.load(hs1, 42)); + + ASSERT_NE(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_FALSE(cache.empty()); + ASSERT_TRUE(cache.contains(hs1)); + ASSERT_FALSE(cache.contains(hs2)); + ASSERT_EQ((*cache.handle(hs1)).value, 42); + + ASSERT_TRUE(cache.load(hs2, 42)); + + ASSERT_NE(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_FALSE(cache.empty()); + ASSERT_TRUE(cache.contains(hs1)); + ASSERT_TRUE(cache.contains(hs2)); + ASSERT_EQ((*cache.handle(hs1)).value, 42); + ASSERT_EQ(cache.handle(hs2)->value, 42); + + ASSERT_NO_THROW(cache.discard(hs1)); + + ASSERT_FALSE(cache.contains(hs1)); + ASSERT_TRUE(cache.contains(hs2)); + ASSERT_EQ(cache.handle(hs2)->value, 42); + + ASSERT_TRUE(cache.load(hs1, 42)); + ASSERT_NO_THROW(cache.clear()); + + ASSERT_EQ(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_TRUE(cache.empty()); + ASSERT_FALSE(cache.contains(hs1)); + ASSERT_FALSE(cache.contains(hs2)); + + ASSERT_TRUE(cache.load(hs1, 42)); + + ASSERT_NE(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_FALSE(cache.empty()); + ASSERT_TRUE(cache.handle(hs1)); + ASSERT_FALSE(cache.handle(hs2)); + + ASSERT_TRUE(cache.handle(hs1)); + ASSERT_EQ(&cache.handle(hs1).get(), &static_cast(cache.handle(hs1))); + ASSERT_NO_THROW(cache.clear()); + + ASSERT_EQ(cache.size(), entt::ResourceCache::size_type{}); + ASSERT_TRUE(cache.empty()); +}