🛠️🐜 Antkeeper superbuild with dependencies included https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
7.0 KiB

  1. # Crash Course: resource management
  2. <!--
  3. @cond TURN_OFF_DOXYGEN
  4. -->
  5. # Table of Contents
  6. * [Introduction](#introduction)
  7. * [The resource, the loader and the cache](#the-resource-the-loader-and-the-cache)
  8. * [Resource handle](#resource-handle)
  9. * [Loaders](#loader)
  10. * [The cache class](#the-cache)
  11. <!--
  12. @endcond TURN_OFF_DOXYGEN
  13. -->
  14. # Introduction
  15. Resource management is usually one of the most critical part of a software like
  16. a game. Solutions are often tuned to the particular application. There exist
  17. several approaches and all of them are perfectly fine as long as they fit the
  18. requirements of the piece of software in which they are used.<br/>
  19. Examples are loading everything on start, loading on request, predictive
  20. loading, and so on.
  21. `EnTT` doesn't pretend to offer a _one-fits-all_ solution for the different
  22. cases.<br/>
  23. Instead, the library offers a minimal, general purpose resource cache that might
  24. be useful in many cases.
  25. # The resource, the loader and the cache
  26. Resource, loader and cache are the three main actors for the purpose.<br/>
  27. The _resource_ is an image, an audio, a video or any other type:
  28. ```cpp
  29. struct my_resource { const int value; };
  30. ```
  31. The _loader_ is a callable type the aim of which is to load a specific resource:
  32. ```cpp
  33. struct my_loader final {
  34. using result_type = std::shared_ptr<my_resource>;
  35. result_type operator()(int value) const {
  36. // ...
  37. return std::make_shared<my_resource>(value);
  38. }
  39. };
  40. ```
  41. Its function operator can accept any arguments and should return a value of the
  42. declared result type (`std::shared_ptr<my_resource>` in the example).<br/>
  43. A loader can also overload its function call operator to make it possible to
  44. construct the same or another resource from different lists of arguments.
  45. Finally, a cache is a specialization of a class template tailored to a specific
  46. resource and (optionally) a loader:
  47. ```cpp
  48. using my_cache = entt::resource_cache<my_resource, my_loader>;
  49. // ...
  50. my_cache cache{};
  51. ```
  52. The cache is meant to be used to create different caches for different types of
  53. resources and to manage each one independently in the most appropriate way.<br/>
  54. As a (very) trivial example, audio tracks can survive in most of the scenes of
  55. an application while meshes can be associated with a single scene only, then
  56. discarded when a player leaves it.
  57. ## Resource handle
  58. Resources aren't returned directly to the caller. Instead, they are wrapped in a
  59. _resource handle_ identified by the `entt::resource` class template.<br/>
  60. For those who know the _flyweight design pattern_ already, that's exactly what
  61. it is. To all others, this is the time to brush up on some notions instead.
  62. A shared pointer could have been used as a resource handle. In fact, the default
  63. handle mostly maps the interface of its standard counterpart and only adds a few
  64. things to it.<br/>
  65. However, the handle in `EnTT` is designed as a standalone class template named
  66. `resource`. It boils down to the fact that specializing a class in the standard
  67. is often undefined behavior while having the ability to specialize the handle
  68. for one, more or all resource types could help over time.
  69. ## Loaders
  70. A loader is a class that is responsible for _loading_ the resources.<br/>
  71. By default, it's just a callable object which forwards its arguments to the
  72. resource itself. That is, a _passthrough type_. All the work is demanded to the
  73. constructor(s) of the resource itself.<br/>
  74. Loaders also are fully customizable as expected.
  75. A custom loader is a class with at least one function call operator and a member
  76. type named `result_type`.<br/>
  77. The loader isn't required to return a resource handle. As long as `return_type`
  78. is suitable for constructing a handle, that's fine.
  79. When using the default handle, it expects a resource type which is convertible
  80. to or suitable for constructing an `std::shared_ptr<Type>` (where `Type` is the
  81. actual resource type).<br/>
  82. In other terms, the loader should return shared pointers to the given resource
  83. type. However, it isn't mandatory. Users can easily get around this constraint
  84. by specializing both the handle and the loader.
  85. A cache forwards all its arguments to the loader if required. This means that
  86. loaders can also support tag dispatching to offer different loading policies:
  87. ```cpp
  88. struct my_loader {
  89. using result_type = std::shared_ptr<my_resource>;
  90. struct from_disk_tag{};
  91. struct from_network_tag{};
  92. template<typename Args>
  93. result_type operator()(from_disk_tag, Args&&... args) {
  94. // ...
  95. return std::make_shared<my_resource>(std::forward<Args>(args)...);
  96. }
  97. template<typename Args>
  98. result_type operator()(from_network_tag, Args&&... args) {
  99. // ...
  100. return std::make_shared<my_resource>(std::forward<Args>(args)...);
  101. }
  102. }
  103. ```
  104. This makes the whole loading logic quite flexible and easy to extend over time.
  105. ## The cache class
  106. The cache is the class that is asked to _connect the dots_.<br/>
  107. It loads the resources, store them aside and returns handles as needed:
  108. ```cpp
  109. entt::resource_cache<my_resource, my_loader> cache{};
  110. ```
  111. Under the hood, a cache is nothing more than a map where the key value has type
  112. `entt::id_type` while the mapped value is whatever type its loader returns.<br/>
  113. For this reason, it offers most of the functionality a user would expect from a
  114. map, such as `empty` or `size` and so on. Similarly, it's an iterable type that
  115. also supports indexing by resource id:
  116. ```cpp
  117. for(entt::resource<my_resource> curr: cache) {
  118. // ...
  119. }
  120. if(entt::resource<my_resource> res = cache["resource/id"_hs]; res) {
  121. // ...
  122. }
  123. ```
  124. Please, refer to the inline documentation for all the details about the other
  125. functions (for example `contains` or `erase`).
  126. Set aside the part of the API that this class shares with a map, it also adds
  127. something on top of it in order to address the most common requirements of a
  128. resource cache.<br/>
  129. In particular, it doesn't have an `emplace` member function which is replaced by
  130. `load` and `force_load` instead (where the former loads a new resource only if
  131. not present while the second triggers a forced loading in any case):
  132. ```cpp
  133. auto ret = cache.load("resource/id"_hs);
  134. // true only if the resource was not already present
  135. const bool loaded = ret.second;
  136. // takes the resource handle pointed to by the returned iterator
  137. entt::resource<my_resource> res = *ret.first;
  138. ```
  139. Note that the hashed string is used for convenience in the example above.<br/>
  140. Resource identifiers are nothing more than integral values. Therefore, plain
  141. numbers as well as non-class enum value are accepted.
  142. Moreover, it's worth mentioning that both the iterators of a cache and its
  143. indexing operators return resource handles rather than instances of the mapped
  144. type.<br/>
  145. Since the cache has no control over the loader and a resource isn't required to
  146. also be convertible to bool, these handles can be invalid. This usually means an
  147. error in the user logic but it may also be an _expected_ event.<br/>
  148. It's therefore recommended to verify handles validity with a check in debug (for
  149. example, when loading) or an appropriate logic in retail.