Просмотр исходного кода

added stuff for resource management

Michele Caini 8 лет назад
Родитель
Сommit
7c7bcf80cf
4 измененных файлов с 440 добавлено и 0 удалено
  1. 183 0
      src/entt/resource/cache.hpp
  2. 115 0
      src/entt/resource/handle.hpp
  3. 62 0
      src/entt/resource/loader.hpp
  4. 80 0
      test/entt/resource/resource.cpp

+ 183 - 0
src/entt/resource/cache.hpp

@@ -0,0 +1,183 @@
+#ifndef ENTT_RESOURCE_CACHE_HPP
+#define ENTT_RESOURCE_CACHE_HPP
+
+
+#include <memory>
+#include <utility>
+#include <type_traits>
+#include <unordered_map>
+#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<typename Resource>
+class ResourceCache {
+    using container_type = std::unordered_map<HashedString::hash_type, std::shared_ptr<Resource>>;
+
+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<typename Loader, typename... Args>
+    bool load(resource_type id, Args&&... args) {
+        static_assert(std::is_base_of<ResourceLoader<Loader, Resource>, Loader>::value, "!");
+
+        bool loaded = true;
+
+        if(resources.find(id) == resources.cend()) {
+            std::shared_ptr<Resource> resource = Loader{}.get(std::forward<Args>(args)...);
+            loaded = (static_cast<bool>(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<typename Loader, typename... Args>
+    void reload(resource_type id, Args&&... args) {
+        return (discard(id), load(id, std::forward<Args>(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<Resource> 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

+ 115 - 0
src/entt/resource/handle.hpp

@@ -0,0 +1,115 @@
+#ifndef ENTT_RESOURCE_HANDLE_HPP
+#define ENTT_RESOURCE_HANDLE_HPP
+
+
+#include <memory>
+#include <utility>
+#include <cassert>
+
+
+namespace entt {
+
+
+template<typename Resource>
+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.<br/>
+ * 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<typename Resource>
+class ResourceHandle final {
+    /*! @brief Resource handles are friends of their caches. */
+    friend class ResourceCache<Resource>;
+
+    ResourceHandle(std::shared_ptr<Resource> 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.<br/>
+     * 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<bool>(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.<br/>
+     * 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.<br/>
+     * 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.<br/>
+     * 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<bool>(resource));
+        return resource.get();
+    }
+
+    /**
+     * @brief Returns true if the handle contains a resource, false otherwise.
+     */
+    explicit operator bool() const { return static_cast<bool>(resource); }
+
+private:
+    std::shared_ptr<Resource> resource;
+};
+
+
+}
+
+
+#endif // ENTT_RESOURCE_HANDLE_HPP

+ 62 - 0
src/entt/resource/loader.hpp

@@ -0,0 +1,62 @@
+#ifndef ENTT_RESOURCE_LOADER_HPP
+#define ENTT_RESOURCE_LOADER_HPP
+
+
+#include <memory>
+
+
+namespace entt {
+
+
+template<typename Resource>
+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.<br/>
+ * As an example:
+ *
+ * @code{.cpp}
+ * struct MyResource {};
+ *
+ * struct MyLoader: entt::ResourceLoader<MyLoader, MyResource> {
+ *     std::shared_ptr<MyResource> load(int) const {
+ *         // use the integer value somehow
+ *         return std::make_shared<MyResource>();
+ *     }
+ * };
+ * @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<typename Loader, typename Resource>
+class ResourceLoader {
+    /*! @brief Resource loaders are friends of their caches. */
+    friend class ResourceCache<Resource>;
+
+    template<typename... Args>
+    std::shared_ptr<Resource> get(Args&&... args) const {
+        return static_cast<const Loader *>(this)->load(std::forward<Args>(args)...);
+    }
+};
+
+
+}
+
+
+#endif // ENTT_RESOURCE_LOADER_HPP

+ 80 - 0
test/entt/resource/resource.cpp

@@ -0,0 +1,80 @@
+#include <gtest/gtest.h>
+#include <entt/resource/cache.hpp>
+
+struct Resource { const int value; };
+
+struct Loader: entt::ResourceLoader<Loader, Resource> {
+    std::shared_ptr<Resource> load(int value) const {
+        return std::shared_ptr<Resource>(new Resource{ value });
+    }
+};
+
+struct BrokenLoader: entt::ResourceLoader<BrokenLoader, Resource> {
+    std::shared_ptr<Resource> load(int) const {
+        return nullptr;
+    }
+};
+
+TEST(ResourceCache, Functionalities) {
+    entt::ResourceCache<Resource> cache;
+
+    constexpr auto hs1 = entt::HashedString{"res1"};
+    constexpr auto hs2 = entt::HashedString{"res2"};
+
+    ASSERT_EQ(cache.size(), entt::ResourceCache<Resource>::size_type{});
+    ASSERT_TRUE(cache.empty());
+    ASSERT_FALSE(cache.contains(hs1));
+    ASSERT_FALSE(cache.contains(hs2));
+
+    ASSERT_FALSE(cache.load<BrokenLoader>(hs1, 42));
+
+    ASSERT_EQ(cache.size(), entt::ResourceCache<Resource>::size_type{});
+    ASSERT_TRUE(cache.empty());
+    ASSERT_FALSE(cache.contains(hs1));
+    ASSERT_FALSE(cache.contains(hs2));
+
+    ASSERT_TRUE(cache.load<Loader>(hs1, 42));
+
+    ASSERT_NE(cache.size(), entt::ResourceCache<Resource>::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<Loader>(hs2, 42));
+
+    ASSERT_NE(cache.size(), entt::ResourceCache<Resource>::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<Loader>(hs1, 42));
+    ASSERT_NO_THROW(cache.clear());
+
+    ASSERT_EQ(cache.size(), entt::ResourceCache<Resource>::size_type{});
+    ASSERT_TRUE(cache.empty());
+    ASSERT_FALSE(cache.contains(hs1));
+    ASSERT_FALSE(cache.contains(hs2));
+
+    ASSERT_TRUE(cache.load<Loader>(hs1, 42));
+
+    ASSERT_NE(cache.size(), entt::ResourceCache<Resource>::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<const Resource &>(cache.handle(hs1)));
+    ASSERT_NO_THROW(cache.clear());
+
+    ASSERT_EQ(cache.size(), entt::ResourceCache<Resource>::size_type{});
+    ASSERT_TRUE(cache.empty());
+}