From 425e7ca780abef80bd646fce9a6541101bd5593d Mon Sep 17 00:00:00 2001 From: gingerBill Date: Mon, 26 Jan 2026 11:46:59 +0000 Subject: [PATCH] Add `core:container/handle_map` --- core/container/handle_map/handle_map.odin | 206 ++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 core/container/handle_map/handle_map.odin diff --git a/core/container/handle_map/handle_map.odin b/core/container/handle_map/handle_map.odin new file mode 100644 index 000000000..da816d288 --- /dev/null +++ b/core/container/handle_map/handle_map.odin @@ -0,0 +1,206 @@ +/* +Handle-based map using fixed-length arrays. + +Example: + import hm "core:container/handle_map" + + Handle :: hm.Handle32 + + Entity :: struct { + handle: Handle, + pos: [2]f32, + } + + entities: hm.Handle_Map(1024, Entity, Handle) + + h1 := hm.add(&entities, Entity{pos = {1, 4}}) + h2 := hm.add(&entities, Entity{pos = {9, 16}}) + + if e, ok := hm.get(&entities, h2); ok { + e.pos.x += 32 + } + + hm.remove(&entities, h1) + + h3 := hm.add(&entities, Entity{pos = {6, 7}}) + + it := hm.iterator_make(&entities) + for e, h in hm.iterate(&it) { + e.pos += {1, 2} + } +*/ +package container_handle_map + +import "base:builtin" +import "base:intrinsics" + +// Default 16-bit Handle type which can be used for handle maps which only need a maximum of 254 (1<<8 - 2) items +Handle16 :: struct { + idx: u8, + gen: u8, +} + +// Default 32-bit Handle type which can be used for handle maps which only need a maximum of 65534 (1<<16 - 2) items +Handle32 :: struct { + idx: u16, + gen: u16, +} + +// Default 64-bit Handle type which can be used for handle maps which only need a maximum of 4294967294 (1<<32 - 2) items +Handle64 :: struct { + idx: u32, + gen: u32, +} + +Handle_Map :: struct($N: uint, $T: typeid, $Handle_Type: typeid) + where + 0 < N, N < uint(1<<31 - 1), + + intrinsics.type_has_field(Handle_Type, "idx"), + intrinsics.type_has_field(Handle_Type, "gen"), + intrinsics.type_is_unsigned(intrinsics.type_field_type(Handle_Type, "idx")), + intrinsics.type_is_unsigned(intrinsics.type_field_type(Handle_Type, "gen")), + intrinsics.type_field_type(Handle_Type, "idx") == intrinsics.type_field_type(Handle_Type, "gen"), + + N < uint(max(intrinsics.type_field_type(Handle_Type, "idx"))), + + intrinsics.type_has_field (T, "handle"), + intrinsics.type_field_type(T, "handle") == Handle_Type +{ + + // The zero element represent a zero-value sentinel (dummy value), allowing for `idx == 0` to mean a no-handle. + // This means the capacity is actually N-1 items. + items: [N]T, + + used_len: u32, // How many of the items are in use + unused_len: u32, // Use to calculate the number of valid items + unused_items: [N]u32, + next_unused: u32, +} + + +// `add` a value of type `T` to the handle map. This will return a pointer to the item and an optional boolean to check for validity. +@(require_results) +add :: proc "contextless" (m: ^$H/Handle_Map($N, $T, $Handle_Type), item: T) -> (handle: Handle_Type, ok: bool) #optional_ok { + if i := m.next_unused; i != 0 { + ptr := &m.items[i] + + m.next_unused = m.unused_items[i] + m.unused_items[i] = 0 + + prev_gen := ptr.handle.gen + ptr^ = item + + ptr.handle.idx = auto_cast i + ptr.handle.gen = auto_cast (prev_gen + 1) + m.unused_len -= 1 + return ptr.handle, true + } + + if m.used_len == 0 { + // initialize the zero-value sentinel + m.items[0] = {} + m.used_len += 1 + } + + if m.used_len == builtin.len(m.items) { + return {}, false + } + + ptr := &m.items[m.used_len] + ptr^ = item + + ptr.handle.idx = auto_cast m.used_len + ptr.handle.gen = 1 + m.used_len += 1 + return ptr.handle, true +} + +// `get` a stable pointer of type `^T` by resolving the handle `h`. If the handle is not valid, then `nil, false` is returned. +@(require_results) +get :: proc "contextless" (m: ^$H/Handle_Map($N, $T, $Handle_Type), h: Handle_Type) -> (^T, bool) #optional_ok { + if h.idx <= 0 || u32(h.idx) >= m.used_len { + return nil, false + } + if e := &m.items[h.idx]; e.handle == h { + return e, true + } + return nil, false +} + +// `remove` an item from the handle map from the handle `h`. +remove :: proc "contextless" (m: ^$H/Handle_Map($N, $T, $Handle_Type), h: Handle_Type) -> bool { + if h.idx <= 0 || u32(h.idx) >= m.used_len { + return false + } + + if item := &m.items[h.idx]; item.handle == h { + m.unused_items[h.idx] = m.next_unused + m.next_unused = u32(h.idx) + m.unused_len += 1 + item.handle.idx = 0 + return true + } + + return false +} + +// Returns true when the handle `h` is valid relating to the handle map. +@(require_results) +is_valid :: proc "contextless" (m: $H/Handle_Map($N, $T, $Handle_Type), h: Handle_Type) -> bool { + return h.idx > 0 && u32(h.idx) < m.used_len && m.items[h.idx].handle == h +} + +// Returns the number of possibly valid items in the handle map. +@(require_results) +len :: proc "contextless" (m: $H/Handle_Map($N, $T, $Handle_Type)) -> uint { + n := uint(m.used_len) - uint(m.unused_len) + return n-1 if n > 0 else 0 +} + +// Returns the capacity of the items in a handle map. +// This is equivalent to `N-1` as the zero value is reserved for the zero-value sentinel. +@(require_results) +cap :: proc "contextless" (m: $H/Handle_Map($N, $T, $Handle_Type)) -> uint { + // We could just return `N` but I am doing this for clarity + return builtin.len(m.items)-1 +} + +// `clear` the handle map by zeroing all of the memory. +// Internally this does not do `m^ = {}` but rather uses `intrinsics.mem_zero` explicitly improve performance. +clear :: proc "contextless" (m: ^$H/Handle_Map($N, $T, $Handle_Type)) { + intrinsics.mem_zero(m, size_of(m^)) +} + +// An iterator for a handle map. +Handle_Map_Iterator :: struct($H: typeid) { + m: ^H, + index: u32, +} + +// Makes an iterator from a handle map. +@(require_results) +iterator_make :: proc "contextless" (m: ^$H/Handle_Map($N, $T, $Handle_Type)) -> Handle_Map_Iterator(H) { + return {m, 1} +} + +/* + Iterate over a handle map. It will skip over unused item slots (e.g. handle.idx == 0). + Usage: + it := hm.iterator_make(&the_handle_map) + for item, handle in hm.iterate(&it) { + ... + } +*/ +@(require_results) +iterate :: proc "contextless" (it: ^$HI/Handle_Map_Iterator($H/Handle_Map($N, $T, $Handle_Type))) -> (val: ^T, h: Handle_Type, ok: bool) { + for _ in it.index..