| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- /*
- Simple DirectMedia Layer
- Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
- This software is provided 'as-is', without any express or implied
- warranty. In no event will the authors be held liable for any damages
- arising from the use of this software.
- Permission is granted to anyone to use this software for any purpose,
- including commercial applications, and to alter it and redistribute it
- freely, subject to the following restrictions:
- 1. The origin of this software must not be misrepresented; you must not
- claim that you wrote the original software. If you use this software
- in a product, an acknowledgment in the product documentation would be
- appreciated but is not required.
- 2. Altered source versions must be plainly marked as such, and must not be
- misrepresented as being the original software.
- 3. This notice may not be removed or altered from any source distribution.
- */
- #include "SDL_internal.h"
- #include "SDL_hashtable.h"
- // XXX: We can't use SDL_assert here because it's going to call into hashtable code
- #ifdef NDEBUG
- #define HT_ASSERT(x) (void)(0)
- #else
- #if (defined(_WIN32) || defined(SDL_PLATFORM_CYGWIN)) && !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES)
- #include <windows.h>
- #endif
- /* This is not declared in any header, although it is shared between some
- parts of SDL, because we don't want anything calling it without an
- extremely good reason. */
- extern SDL_NORETURN void SDL_ExitProcess(int exitcode);
- static void HT_ASSERT_FAIL(const char *msg)
- {
- const char *caption = "SDL_HashTable Assertion Failure!";
- (void)caption;
- #if (defined(_WIN32) || defined(SDL_PLATFORM_CYGWIN)) && !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES)
- MessageBoxA(NULL, msg, caption, MB_OK | MB_ICONERROR);
- #elif defined(HAVE_STDIO_H)
- fprintf(stderr, "\n\n%s\n%s\n\n", caption, msg);
- fflush(stderr);
- #endif
- SDL_TriggerBreakpoint();
- SDL_ExitProcess(-1);
- }
- #define HT_ASSERT(x) if (!(x)) HT_ASSERT_FAIL("SDL_HashTable Assertion Failure: " #x)
- #endif
- typedef struct SDL_HashItem
- {
- // TODO: Splitting off values into a separate array might be more cache-friendly
- const void *key;
- const void *value;
- Uint32 hash;
- Uint32 probe_len : 31;
- Uint32 live : 1;
- } SDL_HashItem;
- // Must be a power of 2 >= sizeof(SDL_HashItem)
- #define MAX_HASHITEM_SIZEOF 32u
- SDL_COMPILE_TIME_ASSERT(sizeof_SDL_HashItem, sizeof(SDL_HashItem) <= MAX_HASHITEM_SIZEOF);
- // Anything larger than this will cause integer overflows
- #define MAX_HASHTABLE_SIZE (0x80000000u / (MAX_HASHITEM_SIZEOF))
- struct SDL_HashTable
- {
- SDL_RWLock *lock;
- SDL_HashItem *table;
- SDL_HashTable_HashFn hash;
- SDL_HashTable_KeyMatchFn keymatch;
- SDL_HashTable_NukeFn nuke;
- void *data;
- Uint32 hash_mask;
- Uint32 max_probe_len;
- Uint32 num_occupied_slots;
- bool stackable;
- };
- SDL_HashTable *SDL_CreateHashTable(void *data,
- Uint32 num_buckets,
- SDL_HashTable_HashFn hashfn,
- SDL_HashTable_KeyMatchFn keymatchfn,
- SDL_HashTable_NukeFn nukefn,
- bool threadsafe,
- bool stackable)
- {
- SDL_HashTable *table;
- // num_buckets must be a power of two so we can derive the bucket index with just a bit-and.
- if ((num_buckets < 1) || !SDL_HasExactlyOneBitSet32(num_buckets)) {
- SDL_SetError("num_buckets must be a power of two");
- return NULL;
- }
- if (num_buckets > MAX_HASHTABLE_SIZE) {
- SDL_SetError("num_buckets is too large");
- return NULL;
- }
- table = (SDL_HashTable *)SDL_calloc(1, sizeof(SDL_HashTable));
- if (!table) {
- return NULL;
- }
- if (threadsafe) {
- // Don't fail if we can't create a lock (single threaded environment?)
- table->lock = SDL_CreateRWLock();
- }
- table->table = (SDL_HashItem *)SDL_calloc(num_buckets, sizeof(SDL_HashItem));
- if (!table->table) {
- SDL_DestroyHashTable(table);
- return NULL;
- }
- table->hash_mask = num_buckets - 1;
- table->stackable = stackable;
- table->data = data;
- table->hash = hashfn;
- table->keymatch = keymatchfn;
- table->nuke = nukefn;
- return table;
- }
- static SDL_INLINE Uint32 calc_hash(const SDL_HashTable *table, const void *key)
- {
- const Uint32 BitMixer = 0x9E3779B1u;
- return table->hash(key, table->data) * BitMixer;
- }
- static SDL_INLINE Uint32 get_probe_length(Uint32 zero_idx, Uint32 actual_idx, Uint32 num_buckets)
- {
- // returns the probe sequence length from zero_idx to actual_idx
- if (actual_idx < zero_idx) {
- return num_buckets - zero_idx + actual_idx;
- }
- return actual_idx - zero_idx;
- }
- static SDL_HashItem *find_item(const SDL_HashTable *ht, const void *key, Uint32 hash, Uint32 *i, Uint32 *probe_len)
- {
- Uint32 hash_mask = ht->hash_mask;
- Uint32 max_probe_len = ht->max_probe_len;
- SDL_HashItem *table = ht->table;
- for (;;) {
- SDL_HashItem *item = table + *i;
- Uint32 item_hash = item->hash;
- if (!item->live) {
- return NULL;
- }
- if (item_hash == hash && ht->keymatch(item->key, key, ht->data)) {
- return item;
- }
- Uint32 item_probe_len = item->probe_len;
- HT_ASSERT(item_probe_len == get_probe_length(item_hash & hash_mask, (Uint32)(item - table), hash_mask + 1));
- if (*probe_len > item_probe_len) {
- return NULL;
- }
- if (++*probe_len > max_probe_len) {
- return NULL;
- }
- *i = (*i + 1) & hash_mask;
- }
- }
- static SDL_HashItem *find_first_item(const SDL_HashTable *ht, const void *key, Uint32 hash)
- {
- Uint32 i = hash & ht->hash_mask;
- Uint32 probe_len = 0;
- return find_item(ht, key, hash, &i, &probe_len);
- }
- static SDL_HashItem *insert_item(SDL_HashItem *item_to_insert, SDL_HashItem *table, Uint32 hash_mask, Uint32 *max_probe_len_ptr)
- {
- Uint32 idx = item_to_insert->hash & hash_mask;
- SDL_HashItem temp_item, *target = NULL;
- Uint32 num_buckets = hash_mask + 1;
- for (;;) {
- SDL_HashItem *candidate = table + idx;
- if (!candidate->live) {
- // Found an empty slot. Put it here and we're done.
- *candidate = *item_to_insert;
- if (target == NULL) {
- target = candidate;
- }
- Uint32 probe_len = get_probe_length(candidate->hash & hash_mask, idx, num_buckets);
- candidate->probe_len = probe_len;
- if (*max_probe_len_ptr < probe_len) {
- *max_probe_len_ptr = probe_len;
- }
- break;
- }
- Uint32 candidate_probe_len = candidate->probe_len;
- HT_ASSERT(candidate_probe_len == get_probe_length(candidate->hash & hash_mask, idx, num_buckets));
- Uint32 new_probe_len = get_probe_length(item_to_insert->hash & hash_mask, idx, num_buckets);
- if (candidate_probe_len < new_probe_len) {
- // Robin Hood hashing: the item at idx has a better probe length than our item would at this position.
- // Evict it and put our item in its place, then continue looking for a new spot for the displaced item.
- // This algorithm significantly reduces clustering in the table, making lookups take very few probes.
- temp_item = *candidate;
- *candidate = *item_to_insert;
- if (target == NULL) {
- target = candidate;
- }
- *item_to_insert = temp_item;
- HT_ASSERT(new_probe_len == get_probe_length(candidate->hash & hash_mask, idx, num_buckets));
- candidate->probe_len = new_probe_len;
- if (*max_probe_len_ptr < new_probe_len) {
- *max_probe_len_ptr = new_probe_len;
- }
- }
- idx = (idx + 1) & hash_mask;
- }
- return target;
- }
- static void delete_item(SDL_HashTable *ht, SDL_HashItem *item)
- {
- Uint32 hash_mask = ht->hash_mask;
- SDL_HashItem *table = ht->table;
- if (ht->nuke) {
- ht->nuke(item->key, item->value, ht->data);
- }
- ht->num_occupied_slots--;
- Uint32 idx = (Uint32)(item - ht->table);
- for (;;) {
- idx = (idx + 1) & hash_mask;
- SDL_HashItem *next_item = table + idx;
- if (next_item->probe_len < 1) {
- SDL_zerop(item);
- return;
- }
- *item = *next_item;
- item->probe_len -= 1;
- HT_ASSERT(item->probe_len < ht->max_probe_len);
- item = next_item;
- }
- }
- static bool resize(SDL_HashTable *ht, Uint32 new_size)
- {
- SDL_HashItem *old_table = ht->table;
- Uint32 old_size = ht->hash_mask + 1;
- Uint32 new_hash_mask = new_size - 1;
- SDL_HashItem *new_table = SDL_calloc(new_size, sizeof(*new_table));
- if (!new_table) {
- return false;
- }
- ht->max_probe_len = 0;
- ht->hash_mask = new_hash_mask;
- ht->table = new_table;
- for (Uint32 i = 0; i < old_size; ++i) {
- SDL_HashItem *item = old_table + i;
- if (item->live) {
- insert_item(item, new_table, new_hash_mask, &ht->max_probe_len);
- }
- }
- SDL_free(old_table);
- return true;
- }
- static bool maybe_resize(SDL_HashTable *ht)
- {
- Uint32 capacity = ht->hash_mask + 1;
- if (capacity >= MAX_HASHTABLE_SIZE) {
- return false;
- }
- Uint32 max_load_factor = 217; // range: 0-255; 217 is roughly 85%
- Uint32 resize_threshold = (Uint32)((max_load_factor * (Uint64)capacity) >> 8);
- if (ht->num_occupied_slots > resize_threshold) {
- return resize(ht, capacity * 2);
- }
- return true;
- }
- bool SDL_InsertIntoHashTable(SDL_HashTable *table, const void *key, const void *value)
- {
- SDL_HashItem *item;
- Uint32 hash;
- bool result = false;
- if (!table) {
- return false;
- }
- if (table->lock) {
- SDL_LockRWLockForWriting(table->lock);
- }
- hash = calc_hash(table, key);
- item = find_first_item(table, key, hash);
- if (item && !table->stackable) {
- // Allow overwrites, this might have been inserted on another thread
- delete_item(table, item);
- }
- SDL_HashItem new_item;
- new_item.key = key;
- new_item.value = value;
- new_item.hash = hash;
- new_item.live = true;
- new_item.probe_len = 0;
- table->num_occupied_slots++;
- if (!maybe_resize(table)) {
- table->num_occupied_slots--;
- goto done;
- }
- // This never returns NULL
- insert_item(&new_item, table->table, table->hash_mask, &table->max_probe_len);
- result = true;
- done:
- if (table->lock) {
- SDL_UnlockRWLock(table->lock);
- }
- return result;
- }
- bool SDL_FindInHashTable(const SDL_HashTable *table, const void *key, const void **value)
- {
- Uint32 hash;
- SDL_HashItem *i;
- bool result = false;
- if (!table) {
- if (value) {
- *value = NULL;
- }
- return false;
- }
- if (table->lock) {
- SDL_LockRWLockForReading(table->lock);
- }
- hash = calc_hash(table, key);
- i = find_first_item(table, key, hash);
- if (i) {
- if (value) {
- *value = i->value;
- }
- result = true;
- }
- if (table->lock) {
- SDL_UnlockRWLock(table->lock);
- }
- return result;
- }
- bool SDL_RemoveFromHashTable(SDL_HashTable *table, const void *key)
- {
- Uint32 hash;
- SDL_HashItem *item;
- bool result = false;
- if (!table) {
- return false;
- }
- if (table->lock) {
- SDL_LockRWLockForWriting(table->lock);
- }
- // FIXME: what to do for stacking hashtables?
- // The original code removes just one item.
- // This hashtable happens to preserve the insertion order of multi-value keys,
- // so deleting the first one will always delete the least-recently inserted one.
- // But maybe it makes more sense to remove all matching items?
- hash = calc_hash(table, key);
- item = find_first_item(table, key, hash);
- if (!item) {
- goto done;
- }
- delete_item(table, item);
- result = true;
- done:
- if (table->lock) {
- SDL_UnlockRWLock(table->lock);
- }
- return result;
- }
- bool SDL_IterateHashTableKey(const SDL_HashTable *table, const void *key, const void **_value, void **iter)
- {
- SDL_HashItem *item = (SDL_HashItem *)*iter;
- if (!table) {
- return false;
- }
- Uint32 i, probe_len, hash;
- if (item) {
- HT_ASSERT(item >= table->table);
- HT_ASSERT(item < table->table + (table->hash_mask + 1));
- hash = item->hash;
- probe_len = item->probe_len + 1;
- i = ((Uint32)(item - table->table) + 1) & table->hash_mask;
- item = table->table + i;
- } else {
- hash = calc_hash(table, key);
- i = hash & table->hash_mask;
- probe_len = 0;
- }
- item = find_item(table, key, hash, &i, &probe_len);
- if (!item) {
- *_value = NULL;
- return false;
- }
- *_value = item->value;
- *iter = item;
- return true;
- }
- bool SDL_IterateHashTable(const SDL_HashTable *table, const void **_key, const void **_value, void **iter)
- {
- SDL_HashItem *item = (SDL_HashItem *)*iter;
- if (!table) {
- return false;
- }
- if (!item) {
- item = table->table;
- } else {
- item++;
- }
- HT_ASSERT(item >= table->table);
- SDL_HashItem *end = table->table + (table->hash_mask + 1);
- while (item < end && !item->live) {
- ++item;
- }
- HT_ASSERT(item <= end);
- if (item == end) {
- if (_key) {
- *_key = NULL;
- }
- if (_value) {
- *_value = NULL;
- }
- return false;
- }
- if (_key) {
- *_key = item->key;
- }
- if (_value) {
- *_value = item->value;
- }
- *iter = item;
- return true;
- }
- bool SDL_HashTableEmpty(SDL_HashTable *table)
- {
- return !(table && table->num_occupied_slots);
- }
- static void nuke_all(SDL_HashTable *table)
- {
- void *data = table->data;
- SDL_HashItem *end = table->table + (table->hash_mask + 1);
- SDL_HashItem *i;
- for (i = table->table; i < end; ++i) {
- if (i->live) {
- table->nuke(i->key, i->value, data);
- }
- }
- }
- void SDL_EmptyHashTable(SDL_HashTable *table)
- {
- if (table) {
- SDL_LockRWLockForWriting(table->lock);
- {
- if (table->nuke) {
- nuke_all(table);
- }
- SDL_memset(table->table, 0, sizeof(*table->table) * (table->hash_mask + 1));
- table->num_occupied_slots = 0;
- }
- SDL_UnlockRWLock(table->lock);
- }
- }
- void SDL_DestroyHashTable(SDL_HashTable *table)
- {
- if (table) {
- SDL_EmptyHashTable(table);
- SDL_DestroyRWLock(table->lock);
- SDL_free(table->table);
- SDL_free(table);
- }
- }
- // this is djb's xor hashing function.
- static SDL_INLINE Uint32 hash_string_djbxor(const char *str, size_t len)
- {
- Uint32 hash = 5381;
- while (len--) {
- hash = ((hash << 5) + hash) ^ *(str++);
- }
- return hash;
- }
- Uint32 SDL_HashPointer(const void *key, void *unused)
- {
- (void)unused;
- return SDL_murmur3_32(&key, sizeof(key), 0);
- }
- bool SDL_KeyMatchPointer(const void *a, const void *b, void *unused)
- {
- (void)unused;
- return (a == b);
- }
- Uint32 SDL_HashString(const void *key, void *unused)
- {
- (void)unused;
- const char *str = (const char *)key;
- return hash_string_djbxor(str, SDL_strlen(str));
- }
- bool SDL_KeyMatchString(const void *a, const void *b, void *unused)
- {
- const char *a_string = (const char *)a;
- const char *b_string = (const char *)b;
- (void)unused;
- if (a == b) {
- return true; // same pointer, must match.
- } else if (!a || !b) {
- return false; // one pointer is NULL (and first test shows they aren't the same pointer), must not match.
- } else if (a_string[0] != b_string[0]) {
- return false; // we know they don't match
- }
- return (SDL_strcmp(a_string, b_string) == 0); // Check against actual string contents.
- }
- // We assume we can fit the ID in the key directly
- SDL_COMPILE_TIME_ASSERT(SDL_HashID_KeySize, sizeof(Uint32) <= sizeof(const void *));
- Uint32 SDL_HashID(const void *key, void *unused)
- {
- (void)unused;
- return (Uint32)(uintptr_t)key;
- }
- bool SDL_KeyMatchID(const void *a, const void *b, void *unused)
- {
- (void)unused;
- return (a == b);
- }
- void SDL_NukeFreeKey(const void *key, const void *value, void *unused)
- {
- (void)value;
- (void)unused;
- SDL_free((void *)key);
- }
- void SDL_NukeFreeValue(const void *key, const void *value, void *unused)
- {
- (void)key;
- (void)unused;
- SDL_free((void *)value);
- }
|