diff --git a/Quake/common.make b/Quake/common.make index 539ae0b33..8eb9054b0 100644 --- a/Quake/common.make +++ b/Quake/common.make @@ -219,6 +219,7 @@ OBJS := strlcat.o \ world.o \ mem.o \ tasks.o \ + hash_map.o \ embedded_pak.o \ $(SYSOBJ_SYS) $(SYSOBJ_MAIN) diff --git a/Quake/hash_map.c b/Quake/hash_map.c new file mode 100644 index 000000000..264be8e36 --- /dev/null +++ b/Quake/hash_map.c @@ -0,0 +1,358 @@ +/* +Copyright (C) 2023 Axel Gneiting +*/ + +#include "quakedef.h" + +#define LOAD_FACTOR 0.75f +#define MIN_KEY_VALUE_STORAGE_SIZE 16 +#define MIN_HASH_SIZE 32 + +typedef struct hash_map_s +{ + uint32_t num_entries; + uint32_t hash_size; + uint32_t key_value_storage_size; + uint32_t key_size; + uint32_t value_size; + uint32_t (*hasher) (const void *const); + uint32_t *hash_to_index; + uint32_t *index_chain; + void *keys; + void *values; +} hash_map_t; + +/* +================= +HashMap_GetKeyImpl +================= +*/ +void *HashMap_GetKeyImpl (hash_map_t *map, uint32_t index) +{ + return (byte *)map->keys + (map->key_size * index); +} + +/* +================= +HashMap_GetValueImpl +================= +*/ +void *HashMap_GetValueImpl (hash_map_t *map, uint32_t index) +{ + return (byte *)map->values + (map->value_size * index); +} + +/* +================= +HashMap_Rehash +================= +*/ +static void HashMap_Rehash (hash_map_t *map, const uint32_t new_size) +{ + if (map->hash_size >= new_size) + return; + map->hash_size = new_size; + map->hash_to_index = Mem_Realloc (map->hash_to_index, map->hash_size * sizeof (uint32_t)); + memset (map->hash_to_index, 0xFF, map->hash_size * sizeof (uint32_t)); + for (uint32_t i = 0; i < map->num_entries; ++i) + { + void *key = HashMap_GetKeyImpl (map, i); + const uint32_t hash = map->hasher (key); + const uint32_t hash_index = hash & (map->hash_size - 1); + map->index_chain[i] = map->hash_to_index[hash_index]; + map->hash_to_index[hash_index] = i; + } +} + +/* +================= +HashMap_ExpandKeyValueStorage +================= +*/ +static void HashMap_ExpandKeyValueStorage (hash_map_t *map, const uint32_t new_size) +{ + map->keys = Mem_Realloc (map->keys, new_size * map->key_size); + map->values = Mem_Realloc (map->values, new_size * map->value_size); + map->index_chain = Mem_Realloc (map->index_chain, new_size * sizeof (uint32_t)); + map->key_value_storage_size = new_size; +} + +/* +================= +HashMap_CreateImpl +================= +*/ +hash_map_t *HashMap_CreateImpl (const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher) (const void *const)) +{ + hash_map_t *map = Mem_Alloc (sizeof (hash_map_t)); + map->key_size = key_size; + map->value_size = value_size; + map->hasher = hasher; + return map; +} + +/* +================= +HashMap_Destroy +================= +*/ +void HashMap_Destroy (hash_map_t *map) +{ + Mem_Free (map->hash_to_index); + Mem_Free (map->index_chain); + Mem_Free (map->keys); + Mem_Free (map->values); + Mem_Free (map); +} + +/* +================= +HashMap_Reserve +================= +*/ +void HashMap_Reserve (hash_map_t *map, int capacity) +{ + const uint32_t new_key_value_storage_size = Q_nextPow2 (capacity); + if (map->key_value_storage_size < new_key_value_storage_size) + HashMap_ExpandKeyValueStorage (map, Q_nextPow2 (capacity)); + const uint32_t new_hash_size = Q_nextPow2 (ceilf (capacity / LOAD_FACTOR)); + if (map->hash_size < new_hash_size) + HashMap_Rehash (map, new_hash_size); +} + +/* +================= +HashMap_Insert +================= +*/ +qboolean HashMap_Insert (hash_map_t *map, const void *const key, const void *const value) +{ + if (map->num_entries >= map->key_value_storage_size) + HashMap_ExpandKeyValueStorage (map, q_max (map->key_value_storage_size * 2, MIN_KEY_VALUE_STORAGE_SIZE)); + if (map->num_entries >= (LOAD_FACTOR * map->hash_size)) + HashMap_Rehash (map, q_max (map->hash_size * 2, MIN_HASH_SIZE)); + + const uint32_t hash = map->hasher (key); + const uint32_t hash_index = hash & (map->hash_size - 1); + { + uint32_t storage_index = map->hash_to_index[hash_index]; + while (storage_index != UINT32_MAX) + { + if (memcmp (key, HashMap_GetKeyImpl (map, storage_index), map->key_size) == 0) + { + memcpy (HashMap_GetValueImpl (map, storage_index), value, map->value_size); + return true; + } + storage_index = map->index_chain[storage_index]; + } + } + + map->index_chain[map->num_entries] = map->hash_to_index[hash_index]; + map->hash_to_index[hash_index] = map->num_entries; + memcpy (HashMap_GetKeyImpl (map, map->num_entries), key, map->key_size); + memcpy (HashMap_GetValueImpl (map, map->num_entries), value, map->value_size); + ++map->num_entries; + + return false; +} + +/* +================= +HashMap_Erase +================= +*/ +qboolean HashMap_Erase (hash_map_t *map, const void *const key) +{ + if (map->num_entries == 0) + return false; + + const uint32_t hash = map->hasher (key); + const uint32_t hash_index = hash & (map->hash_size - 1); + uint32_t storage_index = map->hash_to_index[hash_index]; + uint32_t *prev_storage_index_ptr = NULL; + while (storage_index != UINT32_MAX) + { + if (memcmp (key, HashMap_GetKeyImpl (map, storage_index), map->key_size) == 0) + { + { + // Remove found key from index + if (prev_storage_index_ptr == NULL) + map->hash_to_index[hash_index] = map->index_chain[storage_index]; + else + *prev_storage_index_ptr = map->index_chain[storage_index]; + } + + const uint32_t last_index = map->num_entries - 1; + const uint32_t last_hash = map->hasher (HashMap_GetKeyImpl (map, last_index)); + const uint32_t last_hash_index = last_hash & (map->hash_size - 1); + + if (storage_index == last_index) + { + --map->num_entries; + return true; + } + + { + // Remove last key from index + if (map->hash_to_index[last_hash_index] == last_index) + map->hash_to_index[last_hash_index] = map->index_chain[last_index]; + else + { + qboolean found = false; + for (uint32_t last_storage_index = map->hash_to_index[last_hash_index]; last_storage_index != UINT32_MAX; + last_storage_index = map->index_chain[last_storage_index]) + { + if (map->index_chain[last_storage_index] == last_index) + { + map->index_chain[last_storage_index] = map->index_chain[last_index]; + found = true; + break; + } + } + assert (found); + } + } + + { + // Copy last key to current key position and add back to index + memcpy (HashMap_GetKeyImpl (map, storage_index), HashMap_GetKeyImpl (map, last_index), map->key_size); + memcpy (HashMap_GetValueImpl (map, storage_index), HashMap_GetValueImpl (map, last_index), map->value_size); + map->index_chain[storage_index] = map->hash_to_index[last_hash_index]; + map->hash_to_index[last_hash_index] = storage_index; + } + + --map->num_entries; + return true; + } + prev_storage_index_ptr = &map->index_chain[storage_index]; + storage_index = map->index_chain[storage_index]; + } + return false; +} + +/* +================= +HashMap_LookupImpl +================= +*/ +void *HashMap_LookupImpl (hash_map_t *map, const void *const key) +{ + if (map->num_entries == 0) + return NULL; + + const uint32_t hash = map->hasher (key); + const uint32_t hash_index = hash & (map->hash_size - 1); + uint32_t storage_index = map->hash_to_index[hash_index]; + while (storage_index != UINT32_MAX) + { + if (memcmp (key, (byte *)map->keys + (storage_index * map->key_size), map->key_size) == 0) + return (byte *)map->values + (storage_index * map->value_size); + storage_index = map->index_chain[storage_index]; + } + + return NULL; +} + +/* +================= +HashMap_Size +================= +*/ +uint32_t HashMap_Size (hash_map_t *map) +{ + return map->num_entries; +} + +#ifdef _DEBUG +/* +================= +HashMap_TestAssert +================= +*/ +#define HashMap_TestAssert(cond, what) \ + if (!(cond)) \ + { \ + Con_Printf (what); \ + abort (); \ + } + +/* +================= +HashMap_BasicTest +================= +*/ +static void HashMap_BasicTest (const qboolean reserve) +{ + const int TEST_SIZE = 1000; + hash_map_t *map = HashMap_Create (int32_t, int64_t, &HashInt32); + if (reserve) + HashMap_Reserve (map, TEST_SIZE); + for (int i = 0; i < TEST_SIZE; ++i) + { + int64_t value = i; + HashMap_TestAssert (!HashMap_Insert (map, &i, &value), va ("%d should not be overwritten\n", i)); + } + for (int i = 0; i < TEST_SIZE; ++i) + HashMap_TestAssert (*HashMap_Lookup (int64_t, map, &i) == i, va ("Wrong lookup for %d\n", i)); + for (int i = 0; i < TEST_SIZE; i += 2) + HashMap_Erase (map, &i); + for (int i = 1; i < TEST_SIZE; i += 2) + HashMap_TestAssert (*HashMap_Lookup (int64_t, map, &i) == i, va ("Wrong lookup for %d\n", i)); + for (int i = 0; i < TEST_SIZE; i += 2) + HashMap_TestAssert (HashMap_Lookup (int64_t, map, &i) == NULL, va ("Wrong lookup for %d\n", i)); + for (int i = 0; i < TEST_SIZE; ++i) + HashMap_Erase (map, &i); + HashMap_TestAssert (HashMap_Size (map) == 0, "Map is not empty"); + for (int i = 0; i < TEST_SIZE; ++i) + HashMap_TestAssert (HashMap_Lookup (int64_t, map, &i) == NULL, va ("Wrong lookup for %d\n", i)); + HashMap_Destroy (map); +} + +/* +================= +HashMap_BasicTest +================= +*/ +static void HashMap_StressTest (void) +{ + srand (0); + const int TEST_SIZE = 10000; + TEMP_ALLOC (int64_t, keys, TEST_SIZE); + hash_map_t *map = HashMap_Create (int64_t, int32_t, &HashInt64); + for (int j = 0; j < 10; ++j) + { + for (int i = 0; i < TEST_SIZE; ++i) + { + keys[i] = i; + } + for (int i = TEST_SIZE - 1; i > 0; --i) + { + const int swap_index = rand () % (i + 1); + const int temp = keys[swap_index]; + keys[swap_index] = keys[i]; + keys[i] = temp; + } + for (int i = 0; i < TEST_SIZE; ++i) + HashMap_Insert (map, &keys[i], &i); + for (int i = 0; i < TEST_SIZE; ++i) + HashMap_TestAssert (*HashMap_Lookup (int32_t, map, &keys[i]) == i, va ("Wrong lookup for %d\n", i)); + for (int i = TEST_SIZE - 1; i >= 0; --i) + HashMap_Erase (map, &keys[i]); + HashMap_TestAssert (HashMap_Size (map) == 0, "Map is not empty"); + } + HashMap_Destroy (map); + TEMP_FREE (keys); +} + +/* +================= +TestHashMap_f +================= +*/ +void TestHashMap_f (void) +{ + HashMap_BasicTest (false); + HashMap_BasicTest (true); + HashMap_StressTest (); +} +#endif \ No newline at end of file diff --git a/Quake/hash_map.h b/Quake/hash_map.h new file mode 100644 index 000000000..0cb66a738 --- /dev/null +++ b/Quake/hash_map.h @@ -0,0 +1,65 @@ +/* +Copyright (C) 2023 Axel Gneiting +*/ + +typedef struct hash_map_s hash_map_t; + +hash_map_t *HashMap_CreateImpl (const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher) (const void *const)); +void HashMap_Destroy (hash_map_t *map); +void HashMap_Reserve (hash_map_t *map, int capacity); +qboolean HashMap_Insert (hash_map_t *map, const void *const key, const void *const value); +qboolean HashMap_Erase (hash_map_t *map, const void *const key); +void *HashMap_LookupImpl (hash_map_t *map, const void *const key); +uint32_t HashMap_Size (hash_map_t *map); +void *HashMap_GetKeyImpl (hash_map_t *map, uint32_t index); +void *HashMap_GetValueImpl (hash_map_t *map, uint32_t index); + +#define HashMap_Create(key_type, value_type, hasher) HashMap_CreateImpl (sizeof (key_type), sizeof (value_type), hasher) +#define HashMap_Lookup(type, map, key) ((type *)HashMap_LookupImpl (map, key)) +#define HashMap_GetKey(type, map, index) ((type *)HashMap_GetKeyImpl (map, index)) +#define HashMap_GetValue(type, map, index) ((type *)HashMap_GetValueImpl (map, index)) + +// Murmur3 fmix32 +static inline uint32_t HashInt32 (const uint32_t *const val) +{ + uint32_t h = *val; + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + return h; +} + +// Murmur3 fmix64 +static inline uint32_t HashInt64 (const uint64_t *const val) +{ + uint64_t k = *val; + k ^= k >> 33; + k *= 0xff51afd7ed558ccdull; + k ^= k >> 33; + k *= 0xc4ceb9fe1a85ec53ull; + k ^= k >> 33; + // Truncates, but all bits should be equally good + return k; +} + +static inline uint32_t HashFloat (const float *const val) +{ + uint32_t float_bits; + memcpy (&float_bits, val, sizeof (uint32_t)); + if (float_bits == 0x80000000) + float_bits = 0; + return HashInt32 (&float_bits); +} + +static inline uint32_t HashPtr (const void *const val) +{ + if (sizeof (void *) == sizeof (uint64_t)) + return HashInt64 (val); + return HashInt32 (val); +} + +#ifdef _DEBUG +void TestHashMap_f (void); +#endif \ No newline at end of file diff --git a/Quake/host.c b/Quake/host.c index 3cb775dd3..568454969 100644 --- a/Quake/host.c +++ b/Quake/host.c @@ -986,6 +986,18 @@ void Host_Frame (double time) Con_Printf ("serverprofile: %2i clients %2i msec\n", c, m); } +/* +==================== +Tests_Init +==================== +*/ +static void Tests_Init () +{ +#ifdef _DEBUG + Cmd_AddCommand ("test_hash_map", TestHashMap_f); +#endif +} + /* ==================== Host_Init @@ -1042,6 +1054,7 @@ void Host_Init (void) BGM_Init (); Sbar_Init (); CL_Init (); + Tests_Init (); } #ifdef PSET_SCRIPT diff --git a/Quake/palette.c b/Quake/palette.c index e0c297f8d..80b85db3b 100644 --- a/Quake/palette.c +++ b/Quake/palette.c @@ -954,6 +954,7 @@ uint32_t palette_octree_colors[NUM_PALETTE_OCTREE_COLORS] = extern unsigned int d_8to24table[256]; +#ifdef _DEBUG /* ================= CreatePaletteOctreeRec @@ -1096,3 +1097,4 @@ void CreatePaletteOctree_f (void) Mem_Free (colors_lut); } +#endif \ No newline at end of file diff --git a/Quake/palette.h b/Quake/palette.h index 404a9ff02..73d1e357b 100644 --- a/Quake/palette.h +++ b/Quake/palette.h @@ -35,6 +35,8 @@ COMPILE_TIME_ASSERT ("palette_octree_node_t", sizeof (palette_octree_node_t) == extern palette_octree_node_t palette_octree_nodes[NUM_PALETTE_OCTREE_NODES]; extern uint32_t palette_octree_colors[NUM_PALETTE_OCTREE_COLORS]; +#ifdef _DEBUG void CreatePaletteOctree_f (void); +#endif #endif /* _PALETTE_H */ diff --git a/Windows/VisualStudio/vkquake.vcxproj b/Windows/VisualStudio/vkquake.vcxproj index 1f7cb5e1d..d8a8ba97d 100644 --- a/Windows/VisualStudio/vkquake.vcxproj +++ b/Windows/VisualStudio/vkquake.vcxproj @@ -275,6 +275,7 @@ copy "$(SolutionDir)\..\SDL2\lib64\*.dll" "$(TargetDir)" + @@ -866,6 +867,7 @@ copy "$(SolutionDir)\..\SDL2\lib64\*.dll" "$(TargetDir)" + diff --git a/Windows/VisualStudio/vkquake.vcxproj.filters b/Windows/VisualStudio/vkquake.vcxproj.filters index 102d94174..6c17a5e52 100644 --- a/Windows/VisualStudio/vkquake.vcxproj.filters +++ b/Windows/VisualStudio/vkquake.vcxproj.filters @@ -478,6 +478,9 @@ Shaders\Debug + + Main + @@ -693,6 +696,9 @@ Main + + Main + diff --git a/meson.build b/meson.build index d488b5edd..5eeeb6ed0 100644 --- a/meson.build +++ b/meson.build @@ -137,6 +137,7 @@ srcs = [ 'Quake/view.c', 'Quake/wad.c', 'Quake/world.c', + 'Quake/hash_map.c', 'Quake/embedded_pak.c', ]