#pragma once #include #include #include #include namespace unify { /// Generational handle: 24-bit slot index + 8-bit generation, packed in 32 bits. /// Handles are what cross subsystem and script boundaries; raw pointers never do. /// A handle whose generation no longer matches its slot is stale and fails validation. template struct Handle { uint32_t bits = 0; // 0 == null (generation 0 is never issued) static constexpr uint32_t kIndexBits = 24; static constexpr uint32_t kIndexMask = (1u << kIndexBits) - 1; static Handle make(uint32_t index, uint8_t gen) { return Handle{(uint32_t(gen) << kIndexBits) | (index & kIndexMask)}; } uint32_t index() const { return bits & kIndexMask; } uint8_t generation() const { return uint8_t(bits >> kIndexBits); } bool is_null() const { return bits == 0; } explicit operator bool() const { return bits != 0; } bool operator==(const Handle& o) const { return bits == o.bits; } bool operator!=(const Handle& o) const { return bits != o.bits; } bool operator<(const Handle& o) const { return bits < o.bits; } }; struct EntityTag {}; struct TextureTag {}; struct AudioTag {}; struct PhysicsBodyTag {}; struct BufferTag {}; struct KernelTag {}; struct AssetTag {}; struct ClipTag {}; using EntityHandle = Handle; using TextureHandle = Handle; using AudioHandle = Handle; // a playing voice using PhysicsBodyHandle = Handle; using BufferHandle = Handle; using KernelHandle = Handle; using AssetHandle = Handle; /// Issues and validates generational handles over a dense slot range with a free list. /// Free order is LIFO and fully deterministic, so the same sequence of alloc/free calls /// always yields the same handles (required for deterministic replay and save states). template class HandlePool { public: H allocate() { uint32_t index; if (!free_.empty()) { index = free_.back(); free_.pop_back(); } else { index = uint32_t(gens_.size()); assert(index <= H::kIndexMask); gens_.push_back(1); live_.push_back(0); } live_[index] = 1; alive_++; return H::make(index, gens_[index]); } bool valid(H h) const { uint32_t i = h.index(); return !h.is_null() && i < gens_.size() && live_[i] && gens_[i] == h.generation(); } bool release(H h) { if (!valid(h)) return false; uint8_t& g = gens_[h.index()]; g = uint8_t(g + 1); if (g == 0) g = 1; // skip 0 so a recycled slot never re-issues null live_[h.index()] = 0; free_.push_back(h.index()); alive_--; return true; } /// Handle currently occupying a slot (for iteration); null if the slot is free. H at(uint32_t index) const { return index < gens_.size() && live_[index] ? H::make(index, gens_[index]) : H{}; } uint32_t alive() const { return alive_; } uint32_t capacity() const { return uint32_t(gens_.size()); } void reserve(size_t n) { gens_.reserve(n); free_.reserve(n); live_.reserve(n); } /// Raw state for save/restore: generations, liveness and free-list order. struct State { std::vector gens; std::vector live; std::vector free; uint32_t alive = 0; }; State save() const { return {gens_, live_, free_, alive_}; } void load(const State& s) { gens_ = s.gens; live_ = s.live; free_ = s.free; alive_ = s.alive; } void clear() { gens_.clear(); live_.clear(); free_.clear(); alive_ = 0; } private: std::vector gens_; std::vector live_; std::vector free_; uint32_t alive_ = 0; }; } // namespace unify namespace std { template struct hash> { size_t operator()(const unify::Handle& h) const noexcept { return std::hash()(h.bits); } }; } // namespace std