item_catalog.cpp 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. #include "item_catalog.h"
  2. #include <algorithm>
  3. #include <array>
  4. #include <limits>
  5. #include <mutex>
  6. #include <shared_mutex>
  7. #include "../table.h"
  8. #include "core/threading/srw_lock.h"
  9. namespace sunrise::state::build_data::items {
  10. namespace {
  11. /** A lookup table twice the row count keeps the load factor under 50 percent. */
  12. constexpr std::size_t kLookupCapacity = kDefinitionCapacity * 2;
  13. /** An all-one row cannot clash with a real native definition index. */
  14. constexpr std::uint16_t kEmptyLookupRow = (std::numeric_limits<std::uint16_t>::max)();
  15. /** The standard 64-bit FNV-1a offset basis starts each item lookup hash. */
  16. constexpr std::uint64_t kHashOffsetBasis = 14695981039346656037ULL;
  17. /** The standard 64-bit FNV-1a prime mixes the item hash and bucket bytes. */
  18. constexpr std::uint64_t kHashPrime = 1099511628211ULL;
  19. /** Four definition-hash bytes precede the bucket byte in the lookup key. */
  20. constexpr std::size_t kDefinitionHashByteCount = sizeof(std::uint32_t);
  21. core::threading::SrwLock g_lock;
  22. Table<Definition, kDefinitionCapacity> g_definitions;
  23. // Open-addressed probes into the dense rows, rebuilt with them under the same exclusive hold.
  24. std::array<std::uint16_t, kLookupCapacity> g_lookup{};
  25. std::array<std::uint16_t, kLookupCapacity> g_hashLookup{};
  26. static_assert((kLookupCapacity & (kLookupCapacity - 1)) == 0);
  27. /** @return FNV lookup value mixed from the definition hash. */
  28. [[nodiscard]] std::uint64_t mix_definition_hash(std::uint32_t definitionHash) noexcept {
  29. std::uint64_t hash = kHashOffsetBasis;
  30. std::uint32_t value = definitionHash;
  31. for (std::size_t index = 0; index < kDefinitionHashByteCount; ++index) {
  32. hash ^= static_cast<std::uint8_t>(value);
  33. hash *= kHashPrime;
  34. value >>= 8U;
  35. }
  36. return hash;
  37. }
  38. /** @return Start slot for the hash-and-bucket lookup. */
  39. [[nodiscard]] std::size_t start_slot(const Definition& definition) noexcept {
  40. std::uint64_t hash = mix_definition_hash(definition.definitionHash);
  41. hash ^= definition.bucketId;
  42. hash *= kHashPrime;
  43. return static_cast<std::size_t>(hash) & (kLookupCapacity - 1);
  44. }
  45. /** @return Start slot for the hash-only lookup. */
  46. [[nodiscard]] std::size_t start_hash_slot(std::uint32_t definitionHash) noexcept {
  47. return static_cast<std::size_t>(mix_definition_hash(definitionHash)) & (kLookupCapacity - 1);
  48. }
  49. /**
  50. * Inserts one checked row with a known inventory bucket into the empty lookup table.
  51. * @param definition Dense installed-build mapping to index.
  52. */
  53. void insert_lookup(const Definition& definition) noexcept {
  54. const std::size_t start = start_slot(definition);
  55. for (std::size_t probe = 0; probe < g_lookup.size(); ++probe) {
  56. std::uint16_t& row = g_lookup[(start + probe) & (g_lookup.size() - 1)];
  57. if (row == kEmptyLookupRow) {
  58. row = definition.definitionIndex;
  59. return;
  60. }
  61. }
  62. }
  63. /**
  64. * Inserts one checked row into the empty hash-only lookup table.
  65. * @param definition Dense installed-build mapping to index.
  66. */
  67. void insert_hash_lookup(const Definition& definition) noexcept {
  68. const std::size_t start = start_hash_slot(definition.definitionHash);
  69. for (std::size_t probe = 0; probe < g_hashLookup.size(); ++probe) {
  70. std::uint16_t& row = g_hashLookup[(start + probe) & (g_hashLookup.size() - 1)];
  71. if (row == kEmptyLookupRow) {
  72. row = definition.definitionIndex;
  73. return;
  74. }
  75. }
  76. }
  77. } // namespace
  78. /** Clears every generated item mapping under the catalog lock. */
  79. void clear() noexcept {
  80. const std::lock_guard guard(g_lock);
  81. g_definitions.clear();
  82. std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
  83. std::fill(g_hashLookup.begin(), g_hashLookup.end(), kEmptyLookupRow);
  84. }
  85. /** Checks that the native indices cover the whole range once each, in any input order. */
  86. bool valid(std::span<const Definition> definitions) noexcept {
  87. if (definitions.empty() || definitions.size() > kDefinitionCapacity) {
  88. return false;
  89. }
  90. std::array<bool, kDefinitionCapacity> occupied{};
  91. for (const Definition& definition : definitions) {
  92. if (definition.definitionIndex >= definitions.size() || occupied[definition.definitionIndex]
  93. || !valid(definition.questInitialization)
  94. || (definition.questInitialization.scope != QuestInitialization::Scope::none
  95. && definition.bucketId != kPursuitBucketId)) {
  96. return false;
  97. }
  98. occupied[definition.definitionIndex] = true;
  99. }
  100. return true;
  101. }
  102. /** Rebuilds the dense rows and the lookups, only after the whole input passes the checks. */
  103. bool replace(std::span<const Definition> definitions) noexcept {
  104. if (!valid(definitions)) {
  105. return false;
  106. }
  107. const std::lock_guard guard(g_lock);
  108. std::fill(g_lookup.begin(), g_lookup.end(), kEmptyLookupRow);
  109. std::fill(g_hashLookup.begin(), g_hashLookup.end(), kEmptyLookupRow);
  110. // valid() proved each index appears once, so every row lands in its own slot.
  111. const std::span<Definition> storage = g_definitions.reset(definitions.size());
  112. if (storage.size() != definitions.size()) {
  113. return false;
  114. }
  115. for (const Definition& definition : definitions) {
  116. storage[definition.definitionIndex] = definition;
  117. insert_hash_lookup(definition);
  118. // A row with no bucket is still reachable by native index, but not by bucket lookup.
  119. if (definition.bucketId != kUnresolvedBucketId) {
  120. insert_lookup(definition);
  121. }
  122. }
  123. return true;
  124. }
  125. /** Probes one hash-only key and rejects duplicate installed mappings. */
  126. bool find_hash(std::uint32_t definitionHash, Definition& definition) noexcept {
  127. definition = {};
  128. const std::size_t start = start_hash_slot(definitionHash);
  129. std::uint16_t match = kEmptyLookupRow;
  130. bool ambiguous = false;
  131. const std::shared_lock guard(g_lock);
  132. const std::span<const Definition> rows = g_definitions.rows();
  133. for (std::size_t probe = 0; probe < g_hashLookup.size(); ++probe) {
  134. const std::uint16_t row = g_hashLookup[(start + probe) & (g_hashLookup.size() - 1)];
  135. if (row == kEmptyLookupRow) {
  136. break;
  137. }
  138. if (rows[row].definitionHash == definitionHash) {
  139. ambiguous = match != kEmptyLookupRow;
  140. match = row;
  141. }
  142. }
  143. if (!ambiguous && match != kEmptyLookupRow) {
  144. definition = rows[match];
  145. }
  146. return !ambiguous && match != kEmptyLookupRow;
  147. }
  148. /** Probes one hash-and-bucket key and rejects duplicate installed mappings. */
  149. bool find(std::uint32_t definitionHash, std::uint8_t bucketId, Definition& definition) noexcept {
  150. definition = {};
  151. if (bucketId == kUnresolvedBucketId) {
  152. return false;
  153. }
  154. const Definition key{definitionHash, 0, bucketId};
  155. const std::size_t start = start_slot(key);
  156. std::uint16_t match = kEmptyLookupRow;
  157. bool ambiguous = false;
  158. const std::shared_lock guard(g_lock);
  159. const std::span<const Definition> rows = g_definitions.rows();
  160. for (std::size_t probe = 0; probe < g_lookup.size(); ++probe) {
  161. const std::uint16_t row = g_lookup[(start + probe) & (g_lookup.size() - 1)];
  162. if (row == kEmptyLookupRow) {
  163. break;
  164. }
  165. const Definition& candidate = rows[row];
  166. if (candidate.definitionHash == definitionHash && candidate.bucketId == bucketId) {
  167. ambiguous = match != kEmptyLookupRow;
  168. match = row;
  169. }
  170. }
  171. if (!ambiguous && match != kEmptyLookupRow) {
  172. definition = rows[match];
  173. }
  174. return !ambiguous && match != kEmptyLookupRow;
  175. }
  176. /** Finds one dense installed-build row by its native definition index. */
  177. bool find_index(std::uint16_t definitionIndex, Definition& definition) noexcept {
  178. definition = {};
  179. const std::shared_lock guard(g_lock);
  180. const std::span<const Definition> rows = g_definitions.rows();
  181. const bool found = static_cast<std::size_t>(definitionIndex) < rows.size();
  182. if (found) {
  183. definition = rows[definitionIndex];
  184. }
  185. return found;
  186. }
  187. /** Copies the dense rows in native-index order, without exposing the catalog storage. */
  188. bool snapshot(std::span<Definition> output, std::size_t& count) noexcept {
  189. const std::shared_lock guard(g_lock);
  190. return g_definitions.snapshot(output, count);
  191. }
  192. /** @return Number of installed-build item mappings, read under the lock. */
  193. std::size_t count() noexcept {
  194. const std::shared_lock guard(g_lock);
  195. return g_definitions.count();
  196. }
  197. } // namespace sunrise::state::build_data::items