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',
]