LCOV - code coverage report
Current view: top level - ivc - ivc.hpp (source / functions) Coverage Total Hit
Test: coverage_clean.info Lines: 97.2 % 72 70
Test Date: 2026-03-30 03:13:12 Functions: 100.0 % 17 17

            Line data    Source code
       1              : #pragma once
       2              : /**
       3              :  * @file ivc.hpp
       4              :  * @brief Index Vector Core (IVC) — stable ID management for Struct-of-Arrays collections.
       5              :  *
       6              :  * This is the SoA equivalent of siv::Vector.  Instead of owning the data, it
       7              :  * only owns the two bookkeeping tables (indexes + metadata) that make IDs
       8              :  * stable across insertions and deletions.  Your collection owns its own data
       9              :  * arrays and calls the IVC helpers to keep them in sync.
      10              :  *
      11              :  * ── Core idea ────────────────────────────────────────────────────────────────
      12              :  *
      13              :  *  indexes[id]        → data_index   (stable ID  → current position in arrays)
      14              :  *  metadata[data_idx] → {rid, vid}   (position   → {its ID, validity counter})
      15              :  *
      16              :  *  On erase(id):
      17              :  *    1. Increment metadata[data_idx].validity_id  (invalidates all Handles)
      18              :  *    2. Swap-and-pop:  move last element into the freed slot.
      19              :  *       User does the same swap on their own data arrays, then pop_back.
      20              :  *    3. Decrement n_items.
      21              :  *
      22              :  *  Free slots are reused automatically (metadata + indexes arrays grow
      23              :  *  monotonically, never shrink).
      24              :  *
      25              :  * ── Usage ────────────────────────────────────────────────────────────────────
      26              :  *
      27              :  *  struct BodyCollection {
      28              :  *      IVC_CORE;                         // embeds ivc::Core _ivc
      29              :  *      std::vector<vec3>  position;
      30              :  *      std::vector<quat>  orientation;
      31              :  *      // ...
      32              :  *  };
      33              :  *
      34              :  *  // --- Adding ---
      35              :  *  ivc::ID id = ivc::add(bc._ivc);      // reserve a stable ID
      36              :  *  bc.position.push_back(p);            // keep all arrays in sync
      37              :  *  bc.orientation.push_back(q);
      38              :  *
      39              :  *  // --- Accessing ---
      40              :  *  size_t idx = ivc::index(bc._ivc, id);
      41              :  *  bc.position[idx].x = 1.0f;
      42              :  *
      43              :  *  // --- Handles (safe references) ---
      44              :  *  ivc::Handle h = ivc::make_handle(bc._ivc, id);
      45              :  *  if (h) { auto idx = h.index(); }     // checks validity automatically
      46              :  *
      47              :  *  // --- Erasing ---
      48              :  *  ivc::erase(bc._ivc, id, [&](size_t a, size_t b) {
      49              :  *      std::swap(bc.position[a],    bc.position[b]);
      50              :  *      std::swap(bc.orientation[a], bc.orientation[b]);
      51              :  *      // ... every array in the collection
      52              :  *  });
      53              :  *  bc.position.pop_back();              // after erase, pop all arrays
      54              :  *  bc.orientation.pop_back();
      55              :  *
      56              :  * ── Fixed-size arrays ────────────────────────────────────────────────────────
      57              :  *
      58              :  *  If Capacity > 0 the bookkeeping tables are stack-allocated and no dynamic
      59              :  *  allocation occurs.  Your own data arrays must also be fixed-size then.
      60              :  *  Use IVC_CORE_FIXED(N) in the struct and the same free functions work.
      61              :  *
      62              :  *      struct BodyCollection {
      63              :  *          IVC_CORE_FIXED(256);
      64              :  *          vec3 position[256];
      65              :  *          // ...
      66              :  *      };
      67              :  */
      68              : 
      69              : #include <cstdint>
      70              : #include <cstddef>
      71              : #include <cassert>
      72              : #include <limits>
      73              : #include <algorithm>   // std::swap
      74              : #include <vector>
      75              : 
      76              : namespace ivc
      77              : {
      78              : 
      79              : // ─── Types ────────────────────────────────────────────────────────────────────
      80              : 
      81              : using ID = uint64_t;
      82              : static constexpr ID InvalidID = std::numeric_limits<ID>::max();
      83              : 
      84              : struct Metadata {
      85              :     ID rid         = 0; ///< reverse-ID: data_index → the ID that owns this slot
      86              :     ID validity_id = 0; ///< bumped on every erase; used to detect stale Handles
      87              : };
      88              : 
      89              : // ─── Core struct (two flavours) ───────────────────────────────────────────────
      90              : 
      91              : /**
      92              :  * @brief Dynamic bookkeeping tables (heap-allocated, unlimited capacity).
      93              :  *
      94              :  * Embed via the IVC_CORE macro.
      95              :  */
      96              : struct Core
      97              : {
      98              :     size_t              n_items  = 0; ///< number of live items
      99              :     std::vector<ID>     indexes;      ///< indexes[id]       → data_index
     100              :     std::vector<Metadata> metadata;   ///< metadata[data_idx] → {rid, validity_id}
     101              : };
     102              : 
     103              : /**
     104              :  * @brief Fixed-size bookkeeping tables (stack-allocated, compile-time capacity).
     105              :  *
     106              :  * Embed via the IVC_CORE_FIXED(N) macro.
     107              :  * @tparam Capacity  Maximum number of live items at any one time.
     108              :  */
     109              : template<size_t Capacity>
     110              : struct CoreFixed
     111              : {
     112              :     size_t   n_items             = 0;
     113              :     size_t   n_slots             = 0; ///< total allocated slots (≥ n_items)
     114              :     ID       indexes[Capacity]   = {};
     115              :     Metadata metadata[Capacity]  = {};
     116              : };
     117              : 
     118              : // ─── Handle ───────────────────────────────────────────────────────────────────
     119              : 
     120              : /**
     121              :  * @brief A safe, stable reference to an item inside a Core-bearing collection.
     122              :  *
     123              :  * Stores (id, validity_id, Core*).  Checking the handle re-reads the current
     124              :  * validity counter from the Core, so it automatically becomes invalid after
     125              :  * the item is erased.
     126              :  *
     127              :  * The handle does NOT know about your data arrays; call ivc::index(h) to get
     128              :  * the current data_index and then index into your arrays yourself.
     129              :  */
     130              : struct Handle
     131              : {
     132              :     ID    id          = InvalidID;
     133              :     ID    validity_id = 0;
     134              :     Core* core        = nullptr;
     135              : 
     136              :     /// True iff the item still exists in the collection.
     137           10 :     [[nodiscard]] bool isValid() const
     138              :     {
     139           10 :         return core
     140           10 :             && id < core->indexes.size()
     141           20 :             && core->metadata[core->indexes[id]].validity_id == validity_id;
     142              :     }
     143              : 
     144            2 :     explicit operator bool() const { return isValid(); }
     145              : 
     146              :     /// Current data_index.  Only call when isValid() == true.
     147            1 :     [[nodiscard]] size_t index() const
     148              :     {
     149            1 :         assert(isValid());
     150            1 :         return static_cast<size_t>(core->indexes[id]);
     151              :     }
     152              : 
     153            1 :     [[nodiscard]] ID getID() const { return id; }
     154              : };
     155              : 
     156              : // ─── Internal helpers (not part of the public API) ───────────────────────────
     157              : 
     158              : namespace detail
     159              : {
     160              :     /** Obtain a free ID, reusing freed slots when available.
     161              :      *  Grows the bookkeeping tables by one slot when needed.
     162              :      *  Does NOT touch n_items. */
     163           51 :     inline ID get_free_id(Core& c)
     164              :     {
     165           51 :         if (c.metadata.size() > c.n_items)
     166              :         {
     167              :             // A previously freed slot is available; bump its validity_id so
     168              :             // any old handles to the same ID are invalidated immediately.
     169            7 :             ++c.metadata[c.n_items].validity_id;
     170            7 :             return c.metadata[c.n_items].rid;
     171              :         }
     172              :         // No free slot — allocate a brand-new one.
     173           44 :         const ID new_id = static_cast<ID>(c.n_items);
     174           44 :         c.metadata.push_back({new_id, 0});
     175           44 :         c.indexes.push_back(new_id);
     176           44 :         return new_id;
     177              :     }
     178              : 
     179              :     template<size_t Capacity>
     180            5 :     inline ID get_free_id(CoreFixed<Capacity>& c)
     181              :     {
     182            5 :         assert(c.n_items < Capacity && "CoreFixed capacity exceeded");
     183            5 :         if (c.n_slots > c.n_items)
     184              :         {
     185            0 :             ++c.metadata[c.n_items].validity_id;
     186            0 :             return c.metadata[c.n_items].rid;
     187              :         }
     188            5 :         const ID new_id = static_cast<ID>(c.n_items);
     189            5 :         c.indexes[new_id]  = new_id;
     190            5 :         c.metadata[new_id] = {new_id, 0};
     191            5 :         ++c.n_slots;
     192            5 :         return new_id;
     193              :     }
     194              : 
     195              :     /** Core erase logic — works for both Core (dynamic) and CoreFixed (fixed). */
     196              :     template<typename TCore, typename TSwapFn>
     197           20 :     inline void erase_impl(TCore& c, ID id, TSwapFn&& swap_fn)
     198              :     {
     199           20 :         assert(c.n_items > 0 && "erase called on empty collection");
     200           20 :         const size_t data_idx = static_cast<size_t>(c.indexes[id]);
     201           20 :         const size_t last_idx = c.n_items - 1;
     202              : 
     203              :         // Invalidate the erased slot *before* the swap so that the
     204              :         // incremented validity_id ends up in the free-slot position.
     205           20 :         ++c.metadata[data_idx].validity_id;
     206              : 
     207           20 :         if (data_idx != last_idx)
     208              :         {
     209           13 :             const ID last_rid = c.metadata[last_idx].rid;
     210              : 
     211              :             // 1. User swaps their data arrays (swap-and-pop pattern).
     212           13 :             swap_fn(data_idx, last_idx);
     213              : 
     214              :             // 2. Update bookkeeping so the moved item is still reachable.
     215           13 :             std::swap(c.metadata[data_idx], c.metadata[last_idx]);
     216           13 :             std::swap(c.indexes[id],        c.indexes[last_rid]);
     217              :         }
     218              : 
     219           20 :         --c.n_items;
     220              :         // NOTE: metadata and indexes arrays keep their allocated size;
     221              :         //       the freed slot at position n_items is reused on next add().
     222              :         //       Caller must pop_back on every SoA data array.
     223           20 :     }
     224              : }
     225              : 
     226              : // ─── Public API ───────────────────────────────────────────────────────────────
     227              : 
     228              : // ----- reserve / capacity ----------------------------------------------------
     229              : 
     230              : inline void reserve(Core& c, size_t n)
     231              : {
     232              :     c.indexes.reserve(n);
     233              :     c.metadata.reserve(n);
     234              : }
     235              : 
     236              : template<size_t Capacity>
     237              : inline void reserve(CoreFixed<Capacity>&, size_t) { /* no-op */ }
     238              : 
     239              : // ----- add -------------------------------------------------------------------
     240              : 
     241              : /**
     242              :  * @brief Register a new item and return its stable ID.
     243              :  *
     244              :  * After calling this, push_back (or assign at n_items-1) to EVERY data array
     245              :  * in your collection.  The ID is valid immediately.
     246              :  */
     247           51 : inline ID add(Core& c)
     248              : {
     249           51 :     const ID id   = detail::get_free_id(c);
     250           51 :     c.indexes[id] = static_cast<ID>(c.n_items);
     251           51 :     ++c.n_items;
     252           51 :     return id;
     253              : }
     254              : 
     255              : template<size_t Capacity>
     256            5 : inline ID add(CoreFixed<Capacity>& c)
     257              : {
     258            5 :     const ID id   = detail::get_free_id(c);
     259            5 :     c.indexes[id] = static_cast<ID>(c.n_items);
     260            5 :     ++c.n_items;
     261            5 :     return id;
     262              : }
     263              : 
     264              : // ----- erase -----------------------------------------------------------------
     265              : 
     266              : /**
     267              :  * @brief Erase the item with the given ID.
     268              :  *
     269              :  * @param c        The Core embedded in your collection.
     270              :  * @param id       Stable ID of the item to remove.
     271              :  * @param swap_fn  Callable  void(size_t a, size_t b)  that swaps the
     272              :  *                 elements at indices a and b in ALL your SoA data arrays.
     273              :  *
     274              :  * After this call, pop_back on every data array in your collection.
     275              :  *
     276              :  * Example:
     277              :  *   ivc::erase(bc._ivc, id, [&](size_t a, size_t b) {
     278              :  *       std::swap(bc.position[a],    bc.position[b]);
     279              :  *       std::swap(bc.orientation[a], bc.orientation[b]);
     280              :  *       std::swap(bc.mass[a],        bc.mass[b]);
     281              :  *       // ... every array
     282              :  *   });
     283              :  *   bc.position.pop_back();
     284              :  *   bc.orientation.pop_back();
     285              :  *   bc.mass.pop_back();
     286              :  */
     287              : template<typename TSwapFn>
     288           18 : inline void erase(Core& c, ID id, TSwapFn&& swap_fn)
     289              : {
     290           18 :     detail::erase_impl(c, id, std::forward<TSwapFn>(swap_fn));
     291           18 : }
     292              : 
     293              : template<size_t Capacity, typename TSwapFn>
     294            2 : inline void erase(CoreFixed<Capacity>& c, ID id, TSwapFn&& swap_fn)
     295              : {
     296            2 :     detail::erase_impl(c, id, std::forward<TSwapFn>(swap_fn));
     297            2 : }
     298              : 
     299              : /** Convenience overload: erase via a Handle. */
     300              : template<typename TSwapFn>
     301              : inline void erase(Core& c, const Handle& h, TSwapFn&& swap_fn)
     302              : {
     303              :     assert(h.core == &c && h.isValid());
     304              :     erase(c, h.id, std::forward<TSwapFn>(swap_fn));
     305              : }
     306              : 
     307              : // ----- index access ----------------------------------------------------------
     308              : 
     309              : /** Returns the current data index for a live ID. */
     310           21 : inline size_t index(const Core& c, ID id)
     311              : {
     312           21 :     assert(id < c.indexes.size());
     313           21 :     return static_cast<size_t>(c.indexes[id]);
     314              : }
     315              : 
     316              : template<size_t Capacity>
     317            3 : inline size_t index(const CoreFixed<Capacity>& c, ID id)
     318              : {
     319            3 :     assert(id < c.n_slots);
     320            3 :     return static_cast<size_t>(c.indexes[id]);
     321              : }
     322              : 
     323              : /** Returns the current data index from a Handle (validity-checked). */
     324              : inline size_t index(const Handle& h)
     325              : {
     326              :     return h.index();
     327              : }
     328              : 
     329              : // ----- handles ---------------------------------------------------------------
     330              : 
     331              : /** Create a Handle to an existing item. */
     332            7 : inline Handle make_handle(Core& c, ID id)
     333              : {
     334            7 :     assert(id < c.indexes.size() && c.indexes[id] < c.n_items);
     335            7 :     return {id, c.metadata[c.indexes[id]].validity_id, &c};
     336              : }
     337              : 
     338              : /** Check validity of an id + snapshot pair (e.g. stored externally). */
     339              : inline bool is_valid(const Core& c, ID id, ID validity_id)
     340              : {
     341              :     return id < c.indexes.size()
     342              :         && c.metadata[c.indexes[id]].validity_id == validity_id;
     343              : }
     344              : 
     345              : template<size_t Capacity>
     346            2 : inline bool is_valid(const CoreFixed<Capacity>& c, ID id, ID validity_id)
     347              : {
     348            2 :     return id < c.n_slots
     349            2 :         && c.metadata[c.indexes[id]].validity_id == validity_id;
     350              : }
     351              : 
     352              : // ----- utilities -------------------------------------------------------------
     353              : 
     354              : /** The ID that would be returned by the NEXT call to add(). */
     355              : inline ID next_id(const Core& c)
     356              : {
     357              :     if (c.metadata.size() > c.n_items)
     358              :         return c.metadata[c.n_items].rid;
     359              :     return static_cast<ID>(c.n_items);
     360              : }
     361              : 
     362              : /**
     363              :  * @brief Clear all items and invalidate all existing IDs / handles.
     364              :  *
     365              :  * Bumps every validity_id so outstanding Handles become stale.
     366              :  * Caller must also clear() or resize(0) every SoA data array.
     367              :  */
     368            2 : inline void clear(Core& c)
     369              : {
     370            2 :     c.n_items = 0;
     371            5 :     for (auto& m : c.metadata)
     372            3 :         ++m.validity_id;
     373              :     // Caller: bc.position.clear(); bc.orientation.clear(); etc.
     374            2 : }
     375              : 
     376              : template<size_t Capacity>
     377              : inline void clear(CoreFixed<Capacity>& c)
     378              : {
     379              :     c.n_items = 0;
     380              :     for (size_t i = 0; i < c.n_slots; ++i)
     381              :         ++c.metadata[i].validity_id;
     382              : }
     383              : 
     384              : } // namespace ivc
     385              : 
     386              : // ─── Convenience macros ───────────────────────────────────────────────────────
     387              : 
     388              : /**
     389              :  * @brief Embed dynamic stable-ID bookkeeping into a SoA collection struct.
     390              :  *
     391              :  * The embedded member is named _ivc so all ivc:: free functions can be called
     392              :  * as  ivc::add(myCollection._ivc).
     393              :  *
     394              :  * Usage:
     395              :  *   struct BodyCollection {
     396              :  *       IVC_CORE;
     397              :  *       std::vector<vec3> position;
     398              :  *       ...
     399              :  *   };
     400              :  */
     401              : #define IVC_CORE  ::ivc::Core _ivc
     402              : 
     403              : /**
     404              :  * @brief Embed fixed-size stable-ID bookkeeping (no heap allocation).
     405              :  *
     406              :  * @param N  Maximum number of live items (compile-time constant).
     407              :  *
     408              :  * Usage:
     409              :  *   struct BodyCollection {
     410              :  *       IVC_CORE_FIXED(256);
     411              :  *       vec3 position[256];
     412              :  *       ...
     413              :  *   };
     414              :  */
     415              : #define IVC_CORE_FIXED(N)  ::ivc::CoreFixed<(N)> _ivc
        

Generated by: LCOV version 2.0-1