From ee477272c44695abe6da7abe22787862336c0b1d Mon Sep 17 00:00:00 2001 From: doyle Date: Wed, 12 Apr 2023 00:27:32 +1000 Subject: [PATCH] dqn: Document VArray, TicketMutex, fix test ASAN crash --- build.bat | 4 +- dqn.h | 300 +++++++++++++++++++++-------------- Project/dqn.rdbg => dqn.rdbg | Bin 667 -> 671 bytes dqn_unit_tests.cpp | 183 ++++++++++++++------- readme.md | 3 +- 5 files changed, 309 insertions(+), 181 deletions(-) rename Project/dqn.rdbg => dqn.rdbg (85%) diff --git a/build.bat b/build.bat index c8be354..c4ff80e 100644 --- a/build.bat +++ b/build.bat @@ -16,7 +16,7 @@ pushd Build REM Tp Treat header file as CPP source file set compile_flags=-MT -EHa -GR- -Od -Oi -Z7 -wd4201 -D DQN_TEST_WITH_MAIN -nologo set linker_flags=-link -nologo - set msvc_flags= + set msvc_flags=-fsanitize=address set clang_flags=-fsanitize=address -fsanitize=undefined REM Compiler: MSVC cl @@ -31,7 +31,7 @@ pushd Build REM ------------------------------------------------------------------------ where /q clang-cl || ( echo [WARN] Optional clang compile via clang-cl if it's in the path, please put clang-cl on the path for this feature - exit /b 0 + exit /b 1 ) clang-cl %compile_flags% %clang_flags% %code_dir%dqn_unit_tests.cpp /Fe:dqn_unit_tests_clang %link_flags% popd diff --git a/dqn.h b/dqn.h index df56c10..c23b594 100644 --- a/dqn.h +++ b/dqn.h @@ -12,6 +12,7 @@ // [$GSTR] Global Structs | | Forward declare useful structs // [$W32H] Win32 minimal header | DQN_NO_WIN32_MINIMAL_HEADER | Minimal windows.h subset // [$INTR] Intrinsics | | Atomics, cpuid, ticket mutex +// [$TMUT] Dqn_TicketMutex | | Userland mutex via spinlocking atomics // [$STBS] stb_sprintf | | Portable sprintf // [$CALL] Dqn_CallSite | | Source code location/tracing // [$ALLO] Dqn_Allocator | | Generic allocator interface @@ -135,8 +136,7 @@ // ================================================================================================= #define Dqn_PowerOfTwoRoundUp(value, power_of_two) (((value) + ((power_of_two) - 1)) & ~((power_of_two) - 1)) -// NOTE: Memory allocation dependencies -// ----------------------------------------------------------------------------- +// NOTE: Alloc Macros ============================================================================== #if !defined(DQN_ALLOC) #define DQN_ALLOC(size) Dqn_VMem_Reserve(size, Dqn_VMemCommit_Yes) #endif @@ -145,8 +145,7 @@ #define DQN_DEALLOC(ptr, size) Dqn_VMem_Release(ptr, size) #endif -// NOTE: string.h dependencies -// ----------------------------------------------------------------------------- +// NOTE: String.h Dependnecies ===================================================================== #if !defined(DQN_MEMCPY) || !defined(DQN_MEMSET) || !defined(DQN_MEMCMP) || !defined(DQN_MEMMOVE) #include #if !defined(DQN_MEMCPY) @@ -163,8 +162,7 @@ #endif #endif -// NOTE: math.h dependencies -// ----------------------------------------------------------------------------- +// NOTE: Math.h Dependnecies ======================================================================= #if !defined(DQN_SQRTF) || !defined(DQN_SINF) || !defined(DQN_COSF) || !defined(DQN_TANF) #include #define DQN_SQRTF(val) sqrtf(val) @@ -179,8 +177,7 @@ #endif #endif -// NOTE: Math macros -// ----------------------------------------------------------------------------- +// NOTE: Math Macros =============================================================================== #define DQN_PI 3.14159265359f #define DQN_DEGREE_TO_RADIAN(degrees) ((degrees) * (DQN_PI / 180.0f)) @@ -192,8 +189,7 @@ #define DQN_CLAMP(val, lo, hi) DQN_MAX(DQN_MIN(val, hi), lo) #define DQN_SQUARED(val) ((val) * (val)) -// NOTE: Function/variable annotations -// ----------------------------------------------------------------------------- +// NOTE: Function/Variable Annotations ============================================================= #if defined(DQN_STATIC_API) #define DQN_API static #else @@ -210,8 +206,7 @@ #define DQN_FORCE_INLINE inline __attribute__((always_inline)) #endif -// NOTE: Preprocessor token tricks -// ----------------------------------------------------------------------------- +// NOTE: Preprocessor Token Tricks ================================================================= #define DQN_TOKEN_COMBINE2(x, y) x ## y #define DQN_TOKEN_COMBINE(x, y) DQN_TOKEN_COMBINE2(x, y) #define DQN_UNIQUE_NAME(prefix) DQN_TOKEN_COMBINE(prefix, __LINE__) @@ -224,30 +219,26 @@ b = temp; \ } while (0) -// NOTE: Compile time evaluation macros -// ----------------------------------------------------------------------------- +// NOTE: Size Macros =============================================================================== #define DQN_ISIZEOF(val) DQN_CAST(ptrdiff_t)sizeof(val) #define DQN_ARRAY_UCOUNT(array) (sizeof(array)/(sizeof((array)[0]))) #define DQN_ARRAY_ICOUNT(array) (Dqn_isize)DQN_ARRAY_UCOUNT(array) #define DQN_CHAR_COUNT(string) (sizeof(string) - 1) -// NOTE: SI byte units -// ----------------------------------------------------------------------------- +// NOTE: SI Byte Macros ============================================================================ #define DQN_BYTES(val) (val) #define DQN_KILOBYTES(val) (1024ULL * DQN_BYTES(val)) #define DQN_MEGABYTES(val) (1024ULL * DQN_KILOBYTES(val)) #define DQN_GIGABYTES(val) (1024ULL * DQN_MEGABYTES(val)) -// NOTE: Duration to sections -// ----------------------------------------------------------------------------- +// NOTE: Time Macros =============================================================================== #define DQN_SECONDS_TO_MS(val) ((val) * 1000.0f) #define DQN_MINS_TO_S(val) ((val) * 60ULL) #define DQN_HOURS_TO_S(val) (DQN_MINS_TO_S(val) * 60ULL) #define DQN_DAYS_TO_S(val) (DQN_HOURS_TO_S(val) * 24ULL) #define DQN_YEARS_TO_S(val) (DQN_DAYS_TO_S(val) * 365ULL) -// NOTE: Debug macros -// ----------------------------------------------------------------------------- +// NOTE: Debug Macros ============================================================================== #if !defined(DQN_DEBUG_BREAK) #if defined(NDEBUG) #define DQN_DEBUG_BREAK @@ -267,37 +258,36 @@ #define DQN_MEMSET_BYTE 0 #endif -// NOTE: Assert macros -// ------------------------------------------------------------------------------------------------ -#define DQN_HARD_ASSERT(expr) DQN_HARD_ASSERT_MSG(expr, "") -#define DQN_HARD_ASSERT_MSG(expr, fmt, ...) \ - if (!(expr)) { \ +// NOTE: Assert Macros ============================================================================= +#define DQN_HARD_ASSERT(expr) DQN_HARD_ASSERTF(expr, "") +#define DQN_HARD_ASSERTF(expr, fmt, ...) \ + if (!(expr)) { \ Dqn_Log_ErrorF("Hard assert triggered " #expr ". " fmt, ##__VA_ARGS__); \ - DQN_DEBUG_BREAK; \ + DQN_DEBUG_BREAK; \ } +#if defined(DQN_NO_ASSERT) + #define DQN_ASSERTF(...) + #define DQN_ASSERT(...) +#else + #define DQN_ASSERT(expr) DQN_ASSERTF(expr, "") + #define DQN_ASSERTF(expr, fmt, ...) \ + if (!(expr)) { \ + Dqn_Log_ErrorF("Assert triggered " #expr ". " fmt, ##__VA_ARGS__); \ + DQN_DEBUG_BREAK; \ + } +#endif + #if defined(__cplusplus__) #define DQN_ZERO_INIT {} #else #define DQN_ZERO_INIT {0} #endif -#if defined(DQN_NO_ASSERT) - #define DQN_ASSERT(expr) - #define DQN_ASSERT_MSG(expr, fmt, ...) -#else - #define DQN_ASSERT(expr) DQN_HARD_ASSERT_MSG(expr, "") - #define DQN_ASSERT_MSG(expr, fmt, ...) DQN_HARD_ASSERT_MSG(expr, fmt, ##__VA_ARGS__) -#endif +#define DQN_INVALID_CODE_PATHF(fmt, ...) DQN_ASSERTF(0, fmt, ##__VA_ARGS__) +#define DQN_INVALID_CODE_PATH DQN_INVALID_CODE_PATHF("Invalid code path triggered") -#define DQN_INVALID_CODE_PATH_MSG(fmt, ...) DQN_ASSERT_MSG(0, fmt, ##__VA_ARGS__) -#define DQN_INVALID_CODE_PATH DQN_INVALID_CODE_PATH_MSG("Invalid code path triggered") - -#define DQN_HARD_INVALID_CODE_PATH_MSG(fmt, ...) DQN_HARD_ASSERT_MSG(0, fmt, ##__VA_ARGS__) -#define DQN_HARD_INVALID_CODE_PATH DQN_HARD_INVALID_CODE_PATH_MSG("Invalid code path triggered") - -// NOTE: Defer macros -// ------------------------------------------------------------------------------------------------ +// NOTE: Defer Macro =============================================================================== #if 0 #include int main() @@ -479,50 +469,57 @@ struct Dqn_CPUIDRegisters /// Execute 'CPUID' instruction to query the capabilities of the current CPU. Dqn_CPUIDRegisters Dqn_CPUID(int function_id); -/// A mutex implemented using an atomic compare and swap on tickets handed out -/// for each critical section. -/// -/// This mutex serves ticket in order and will block all other threads until the -/// tickets are returned in order. The thread with the oldest ticket that has -/// not been returned has right of way to execute, all other threads will be -/// blocked in an atomic compare and swap loop. block execution by going into an -/// atomic -/// -/// When a thread is blocked by this mutex, a spinlock intrinsic `_mm_pause` is -/// used to yield the CPU and reduce spinlock on the thread. This mutex is not -/// ideal for long blocking operations. This mutex does not issue any syscalls -/// and relies entirely on atomic instructions. +// ================================================================================================= +// [$TMUT] Dqn_TicketMutex | | Userland mutex via spinlocking atomics +// ================================================================================================= +// A mutex implemented using an atomic compare and swap on tickets handed out +// for each critical section. +// +// This mutex serves ticket in order and will block all other threads until the +// tickets are returned in order. The thread with the oldest ticket that has +// not been returned has right of way to execute, all other threads will be +// blocked in an atomic compare and swap loop. block execution by going into an +// atomic +// +// When a thread is blocked by this mutex, a spinlock intrinsic `_mm_pause` is +// used to yield the CPU and reduce spinlock on the thread. This mutex is not +// ideal for long blocking operations. This mutex does not issue any syscalls +// and relies entirely on atomic instructions. +// +// NOTE: API +// +// @proc Dqn_TicketMutex_Begin, End +// @desc Lock and unlock the mutex respectively +// +// @proc Dqn_TicketMutex_MakeTicket +// @desc Allocate the next available ticket from the mutex for locking using +// Dqn_TicketMutex_BeginTicket(). +// @param[in] mutex The mutex +// @code +// Dqn_TicketMutex mutex = {}; +// unsigned int ticket = Dqn_TicketMutex_MakeTicket(&mutex); +// Dqn_TicketMutex_BeginTicket(&mutex, ticket); // Blocking call until we attain the lock +// Dqn_TicketMutex_End(&mutex); +// @endcode +// +// @proc Dqn_TicketMutex_BeginTicket +// @desc Lock the mutex using the given ticket if possible, otherwise block +// waiting until the mutex can be locked. +// +// @proc Dqn_TicketMutex_CanLock +// @desc Determine if the mutex can be locked using the given ticket number + struct Dqn_TicketMutex { unsigned int volatile ticket; ///< The next ticket to give out to the thread taking the mutex unsigned int volatile serving; ///< The ticket ID to block the mutex on until it is returned }; -/// Lock the mutex -void Dqn_TicketMutex_Begin(Dqn_TicketMutex *mutex); - -/// Unlock the mutex -void Dqn_TicketMutex_End(Dqn_TicketMutex *mutex); - -/// Allocate the next available ticket from the mutex for locking using -/// Dqn_TicketMutex_BeginTicket(). -/// -/// @param[in] mutex The mutex -/// -/// @code -/// Dqn_TicketMutex mutex = {}; -/// unsigned int ticket = Dqn_TicketMutex_MakeTicket(&mutex); -/// Dqn_TicketMutex_BeginTicket(&mutex, ticket); // Blocking call until we attain the lock -/// Dqn_TicketMutex_End(&mutex); -/// @endcode -Dqn_uint Dqn_TicketMutex_MakeTicket(Dqn_TicketMutex *mutex); - -/// Lock the mutex using the given ticket if possible, otherwise block -/// waiting until the mutex can be locked. -void Dqn_TicketMutex_BeginTicket(Dqn_TicketMutex const *mutex, Dqn_uint ticket); - -/// Determine if the mutex can be locked using the given ticket number. -bool Dqn_TicketMutex_CanLock(Dqn_TicketMutex const *mutex, Dqn_uint ticket); +void Dqn_TicketMutex_Begin (Dqn_TicketMutex *mutex); +void Dqn_TicketMutex_End (Dqn_TicketMutex *mutex); +Dqn_uint Dqn_TicketMutex_MakeTicket (Dqn_TicketMutex *mutex); +void Dqn_TicketMutex_BeginTicket(Dqn_TicketMutex const *mutex, Dqn_uint ticket); +bool Dqn_TicketMutex_CanLock (Dqn_TicketMutex const *mutex, Dqn_uint ticket); // ================================================================================================= // [$STBS] stb_sprintf | | Portable sprintf @@ -1526,6 +1523,67 @@ DQN_API Dqn_Arena *Dqn_ArenaCatalog_AllocF (Dqn_ArenaCatalog *catalog, Dqn_usize // ================================================================================================= // [$VARR] Dqn_VArray | DQN_NO_VARRAY | Array backed by virtual memory arena // ================================================================================================= +// +// An array that is backed by virtual memory by reserving addressing space and +// comitting pages as items are allocated in the array. This array never +// reallocs, instead you should reserve the upper bound of the memory you will +// possibly ever need (e.g. 16GB) and let the array commit physical pages on +// demand. On 64 bit operating systems you are given 48 bits of addressable +// space giving you 256 TB of reservable memory. This gives you practically +// an unlimited array capacity that avoids reallocs and only consumes memory +// that is actually occupied by the array. +// +// Each page that is committed into the array will be at page/allocation +// granularity which are always cache aligned. This array essentially retains +// all the benefits of normal arrays, +// +// - contiguous memory +// - O(1) random access +// - O(N) iterate +// +// In addition to no realloc on expansion or shrinking. +// +// NOTE: API +// +// @proc Dqn_VArray_InitByteSize, Dqn_VArray_Init +// @desc Initialise an array with the requested byte size or item capacity +// respectively. The returned array may have a higher capacity than the +// requested amount since requested memory from the OS may have a certain +// alignment requirement (e.g. on Windows reserve/commit are 64k/4k aligned). +// +// @proc Dqn_VArray_IsValid +// @desc Verify if the array has been initialised +// +// @proc Dqn_VArray_Make, Dqn_VArray_Add +// @desc Allocate items into the array +// 'Make' creates the `count` number of requested items +// 'Add' adds the array of items into the array +// @return The array of items allocated. Null pointer if the array is invalid +// or the array has insufficient space for the requested items. +// +// @proc Dqn_VArray_EraseRange +// @desc Erase the next `count` items at `begin_index` in the array. `count` +// can be positive or negative which dictates the if we erase forward from the +// `begin_index` or in reverse. +// +// This operation will invalidate all pointers to the array! +// +// @param erase The erase method, stable erase will shift all elements after +// the erase ranged into the range. Unstable erase will copy the tail elements +// into the range to delete. +// +// @proc Dqn_VArray_Reserve +// @desc Ensure that the requested number of items are backed by physical +// pages from the OS. Calling this pre-emptively will minimise syscalls into +// the kernel to request memory. The requested items will be rounded up to the +// in bytes to the allocation granularity of OS allocation APIs hence the +// reserved space may be greater than the requested amount (e.g. this is 4k +// on Windows). +// +// TODO(doyle) +// +// Add an API for shrinking the array by decomitting pages back to the OS. + template struct Dqn_VArray { Dqn_ArenaBlock *block; ///< Block of memory from the allocator for this array @@ -3070,7 +3128,7 @@ struct Dqn_OSTimedBlock // Dump the timing block via Dqn_Log #define DQN_OS_TIMED_BLOCK_DUMP \ - DQN_ASSERT_MSG(timings_size_ < sizeof(timings_) / sizeof(timings_[0]), \ + DQN_ASSERTF(timings_size_ < sizeof(timings_) / sizeof(timings_[0]), \ "Timings array indexed out-of-bounds, use a bigger size"); \ for (int timings_index_ = 0; timings_index_ < (timings_size_ - 1); timings_index_++) { \ Dqn_OSTimedBlock t1 = timings_[timings_index_ + 0]; \ @@ -4006,10 +4064,9 @@ DQN_FSTRING8_API Dqn_String8 Dqn_FString8_ToString8(DQN_FSTRING8 const *string) { Dqn_String8 result = {}; - if (!string) + if (!string || string->size <= 0) return result; - DQN_HARD_ASSERT(string->size >= 0); result.data = DQN_CAST(char *)string->data; result.size = string->size; return result; @@ -4831,11 +4888,11 @@ DQN_API Dqn_uint Dqn_TicketMutex_MakeTicket(Dqn_TicketMutex *mutex) DQN_API void Dqn_TicketMutex_BeginTicket(Dqn_TicketMutex const *mutex, Dqn_uint ticket) { - DQN_ASSERT_MSG(mutex->serving <= ticket, - "Mutex skipped ticket? Was ticket generated by the correct mutex via MakeTicket? ticket = %u, " - "mutex->serving = %u", - ticket, - mutex->serving); + DQN_ASSERTF(mutex->serving <= ticket, + "Mutex skipped ticket? Was ticket generated by the correct mutex via MakeTicket? ticket = %u, " + "mutex->serving = %u", + ticket, + mutex->serving); while (ticket != mutex->serving) { // NOTE: Use spinlock intrinsic _mm_pause(); @@ -6177,7 +6234,7 @@ DQN_API void Dqn_Library_LeakTraceAdd(Dqn_CallSite call_site, void *ptr, Dqn_usi // TODO: Add API for always making the item but exposing a var to indicate if the item was newly created or it already existed. Dqn_LeakTrace *trace = Dqn_DSMap_Find(&dqn_library.alloc_table, Dqn_DSMap_KeyU64(DQN_CAST(uintptr_t)ptr)); if (trace) { - DQN_HARD_ASSERT_MSG(trace->freed, "This pointer is already in the leak tracker, however it" + DQN_HARD_ASSERTF(trace->freed, "This pointer is already in the leak tracker, however it" " has not been freed yet. Somehow this pointer has been" " given to the allocation table and has not being marked" " freed with an equivalent call to LeakTraceMarkFree()" @@ -6206,12 +6263,12 @@ DQN_API void Dqn_Library_LeakTraceMarkFree(Dqn_CallSite call_site, void *ptr) Dqn_TicketMutex_Begin(&dqn_library.alloc_table_mutex); Dqn_LeakTrace *trace = Dqn_DSMap_Find(&dqn_library.alloc_table, Dqn_DSMap_KeyU64(DQN_CAST(uintptr_t)ptr)); - DQN_HARD_ASSERT_MSG(trace, "Allocated pointer can not be removed as it does not exist in the" + DQN_HARD_ASSERTF(trace, "Allocated pointer can not be removed as it does not exist in the" " allocation table. When this memory was allocated, the pointer was" " not added to the allocation table [ptr=%p]", ptr); - DQN_HARD_ASSERT_MSG(!trace->freed, + DQN_HARD_ASSERTF(!trace->freed, "Double free detected, pointer was previously allocated at [ptr=%p, %_$$d, file=\"%.*s:%u\", function=\"%.*s\"]", ptr, trace->size, @@ -6328,22 +6385,28 @@ DQN_API void Dqn_Arena_CommitFromBlock(Dqn_ArenaBlock *block, Dqn_isize size, Dq DQN_API void *Dqn_Arena_AllocateFromBlock(Dqn_ArenaBlock *block, Dqn_isize size, uint8_t align, Dqn_ZeroMem zero_mem) { - Dqn_isize allocation_size = size + (align - 1); - if ((block->used + allocation_size) > block->size) - return nullptr; - DQN_ASSERT(block->hwm_used <= block->commit); - DQN_ASSERT_MSG(block->commit >= block->used, + DQN_ASSERTF(block->commit >= block->used, "Internal error: Committed size must always be greater than the used size [commit=%_$$zd, used=%_$$zd]", block->commit, block->used); - void *unaligned_result = DQN_CAST(char *)block->memory + block->used; - void *result = DQN_CAST(void *)Dqn_PowerOfTwoRoundUp(DQN_CAST(uintptr_t)unaligned_result, align); + // NOTE: Calculate how much we need to pad the next pointer to divvy out + // (only if it is unaligned) + uintptr_t next_ptr = DQN_CAST(uintptr_t)block->memory + block->used; + Dqn_isize align_offset = 0; + if (next_ptr & (align - 1)) + align_offset = (align - (next_ptr & (align - 1))); + + Dqn_isize allocation_size = size + align_offset; + if ((block->used + allocation_size) > block->size) + return nullptr; + + void *result = DQN_CAST(char *)next_ptr + align_offset; if (zero_mem == Dqn_ZeroMem_Yes) { // NOTE: Newly commit pages are always 0-ed out, we only need to - // memset the memory that was used before from the arena. + // memset the memory that are being reused. Dqn_isize const reused_bytes = DQN_MIN(block->hwm_used - block->used, allocation_size); - DQN_MEMSET(unaligned_result, DQN_MEMSET_BYTE, reused_bytes); + DQN_MEMSET(DQN_CAST(void *)next_ptr, DQN_MEMSET_BYTE, reused_bytes); } // NOTE: Ensure requested bytes are backed by physical pages from the OS @@ -6356,9 +6419,9 @@ DQN_API void *Dqn_Arena_AllocateFromBlock(Dqn_ArenaBlock *block, Dqn_isize size, block->used += allocation_size; block->hwm_used = DQN_MAX(block->hwm_used, block->used); - DQN_ASSERT_MSG(block->used <= block->commit, "Internal error: Committed size must be greater than used size [used=%_$$zd, commit=%_$$zd]", block->used, block->commit); - DQN_ASSERT_MSG(block->commit <= block->size, "Internal error: Allocation exceeded block capacity [commit=%_$$zd, size=%_$$zd]", block->commit, block->size); - DQN_ASSERT_MSG(((DQN_CAST(uintptr_t)result) & (align - 1)) == 0, "Internal error: Pointer alignment failed [address=%p, align=%x]", result, align); + DQN_ASSERTF(block->used <= block->commit, "Internal error: Committed size must be greater than used size [used=%_$$zd, commit=%_$$zd]", block->used, block->commit); + DQN_ASSERTF(block->commit <= block->size, "Internal error: Allocation exceeded block capacity [commit=%_$$zd, size=%_$$zd]", block->commit, block->size); + DQN_ASSERTF(((DQN_CAST(uintptr_t)result) & (align - 1)) == 0, "Internal error: Pointer alignment failed [address=%p, align=%x]", result, align); return result; } @@ -6588,7 +6651,7 @@ DQN_API Dqn_ArenaBlock *Dqn_Arena_Grow_(DQN_LEAK_TRACE_FUNCTION Dqn_Arena *arena DQN_API void *Dqn_Arena_Allocate_(DQN_LEAK_TRACE_FUNCTION Dqn_Arena *arena, Dqn_isize size, uint8_t align, Dqn_ZeroMem zero_mem) { - DQN_ASSERT_MSG((align & (align - 1)) == 0, "Power of two alignment required"); + DQN_ASSERTF((align & (align - 1)) == 0, "Power of two alignment required"); Dqn_isize allocation_size = size + (align - 1); while (!arena->curr || (arena->curr->flags & Dqn_ArenaBlockFlags_Private) || @@ -6869,9 +6932,9 @@ DQN_API Dqn_M4 Dqn_M4_Transpose(Dqn_M4 mat) DQN_API Dqn_M4 Dqn_M4_Rotate(Dqn_V3 axis01, Dqn_f32 radians) { - DQN_ASSERT_MSG(DQN_ABS(Dqn_V3Length(axis01) - 1.f) <= 0.01f, - "Rotation axis must be normalised, length = %f", - Dqn_V3Length(axis01)); + DQN_ASSERTF(DQN_ABS(Dqn_V3Length(axis01) - 1.f) <= 0.01f, + "Rotation axis must be normalised, length = %f", + Dqn_V3Length(axis01)); Dqn_f32 sin = DQN_SINF(radians); Dqn_f32 cos = DQN_COSF(radians); @@ -7554,7 +7617,7 @@ DQN_API bool Dqn_Char_IsHex(char ch) DQN_API uint8_t Dqn_Char_HexToU8(char ch) { - DQN_ASSERT_MSG(Dqn_Char_IsHex(ch), "Hex character not valid '%c'", ch); + DQN_ASSERTF(Dqn_Char_IsHex(ch), "Hex character not valid '%c'", ch); uint8_t result = 0; if (ch >= 'a' && ch <= 'f') @@ -7824,7 +7887,7 @@ DQN_API Dqn_isize Dqn_Hex_CString8ToByteBuffer(char const *hex, Dqn_isize hex_si Dqn_isize trim_size_rounded_up = trim_size + (trim_size % 2); Dqn_isize min_buffer_size = trim_size_rounded_up / 2; if (dest_size < min_buffer_size || trim_size <= 0) { - DQN_ASSERT_MSG(dest_size >= min_buffer_size, "Insufficient buffer size for converting hex to binary"); + DQN_ASSERTF(dest_size >= min_buffer_size, "Insufficient buffer size for converting hex to binary"); return result; } @@ -8173,7 +8236,7 @@ DQN_API Dqn_isize Dqn_Win_EXEDirW(wchar_t *buffer, Dqn_isize size) { wchar_t module_path[DQN_OS_WIN32_MAX_PATH]; int module_size = GetModuleFileNameW(nullptr /*module*/, module_path, DQN_ARRAY_UCOUNT(module_path)); - DQN_HARD_ASSERT_MSG(GetLastError() != ERROR_INSUFFICIENT_BUFFER, "How the hell?"); + DQN_HARD_ASSERTF(GetLastError() != ERROR_INSUFFICIENT_BUFFER, "How the hell?"); Dqn_isize result = 0; for (int index = module_size - 1; !result && index >= 0; index--) @@ -8191,7 +8254,7 @@ DQN_API Dqn_String16 Dqn_Win_EXEDirWArena(Dqn_Arena *arena) { wchar_t dir[DQN_OS_WIN32_MAX_PATH]; Dqn_isize dir_size = Dqn_Win_EXEDirW(dir, DQN_ARRAY_ICOUNT(dir)); - DQN_HARD_ASSERT_MSG(dir_size <= DQN_ARRAY_ICOUNT(dir), "How the hell?"); + DQN_HARD_ASSERTF(dir_size <= DQN_ARRAY_ICOUNT(dir), "How the hell?"); Dqn_String16 result = {}; if (dir_size > 0) { @@ -8767,9 +8830,9 @@ DQN_API bool Dqn_OS_SecureRNGBytes(void *buffer, uint32_t size) } #else - DQN_ASSERT_MSG(size <= 32, - "We can increase this by chunking the buffer and filling 32 bytes at a time. *Nix guarantees 32 " - "bytes can always be fulfilled by this system at a time"); + DQN_ASSERTF(size <= 32, + "We can increase this by chunking the buffer and filling 32 bytes at a time. *Nix guarantees 32 " + "bytes can always be fulfilled by this system at a time"); // TODO(doyle): https://github.com/jedisct1/libsodium/blob/master/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c // TODO(doyle): https://man7.org/linux/man-pages/man2/getrandom.2.html int read_bytes = 0; @@ -8825,7 +8888,7 @@ DQN_API Dqn_String8 Dqn_OS_EXEDir(Dqn_Allocator allocator) // try_buf around, memcopy the byte and trash the try_buf from the // arena. Instead we just get the size and redo the call one last // time after this "calculate" step. - DQN_ASSERT_MSG(bytes_written < try_size, "bytes_written can never be greater than the try size, function writes at most try_size"); + DQN_ASSERTF(bytes_written < try_size, "bytes_written can never be greater than the try size, function writes at most try_size"); required_size_wo_null_terminator = bytes_written; for (int index_of_last_slash = bytes_written; @@ -8948,7 +9011,7 @@ DQN_API uint64_t Dqn_OS_PerfCounterNow() LARGE_INTEGER integer = {}; int qpc_result = QueryPerformanceCounter(&integer); (void)qpc_result; - DQN_ASSERT_MSG(qpc_result, "MSDN says this can only fail when running on a version older than Windows XP"); + DQN_ASSERTF(qpc_result, "MSDN says this can only fail when running on a version older than Windows XP"); result = integer.QuadPart; #else struct timespec ts; @@ -9540,9 +9603,8 @@ DQN_API Dqn_FsFile Dqn_Fs_OpenFile(Dqn_String8 path, Dqn_FsFileOpen open_mode, u unsigned long access_mode = 0; if (access & Dqn_FsFileAccess_AppendOnly) { - DQN_ASSERT_MSG( - (access & ~Dqn_FsFileAccess_AppendOnly) == 0, - "Append can only be applied exclusively to the file, other access modes not permitted"); + DQN_ASSERTF((access & ~Dqn_FsFileAccess_AppendOnly) == 0, + "Append can only be applied exclusively to the file, other access modes not permitted"); access_mode = FILE_APPEND_DATA; } else { if (access & Dqn_FsFileAccess_Read) @@ -9762,9 +9824,11 @@ DQN_API uint32_t Dqn_Thread_GetID() DQN_API Dqn_ThreadContext *Dqn_Thread_GetContext_(DQN_LEAK_TRACE_FUNCTION_NO_COMMA) { + thread_local Dqn_ThreadContext result = {}; if (!result.init) { result.init = true; + DQN_ASSERTF(dqn_library.lib_init, "Library must be initialised by calling Dqn_Library_Init(nullptr)"); // NOTE: Setup permanent arena Dqn_ArenaCatalog *catalog = &dqn_library.arena_catalog; diff --git a/Project/dqn.rdbg b/dqn.rdbg similarity index 85% rename from Project/dqn.rdbg rename to dqn.rdbg index 4ed6eac7b9c9e2e267be1747d02c6de1cacebbaa..1a40fae9d606993d8f2bedd37ed20a718fe83138 100644 GIT binary patch delta 43 xcmbQuI-hldyl_fkUVLd@W=VWWYH>+%d~R`B@&rNosLP@kXOWCJqKD000t=3tj*K diff --git a/dqn_unit_tests.cpp b/dqn_unit_tests.cpp index ee80808..9c3b7ff 100644 --- a/dqn_unit_tests.cpp +++ b/dqn_unit_tests.cpp @@ -9,6 +9,15 @@ */ #if defined(DQN_TEST_WITH_MAIN) + #if defined(_MSC_VER) && !defined(__clang__) + // NOTE: C-strings declared in a ternary cause global-buffer-overflow in + // MSVC2022. + // stb_sprintf assumes c-string literals are 4 byte aligned which is always + // true, however, reading past the end of a string whose size is not a multiple + // of 4 is UB causing ASAN to complain. + #define STBSP__ASAN __declspec(no_sanitize_address) + #endif + #define DQN_IMPLEMENTATION #include "dqn.h" #endif @@ -1362,81 +1371,134 @@ Dqn_Tester TestTicketMutex() Dqn_Tester TestVArray() { Dqn_Tester test = {}; - Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); - Dqn_VArray array = Dqn_VArray_InitByteSize(scratch.arena, DQN_KILOBYTES(64)); - DQN_TESTER_GROUP(test, "Dqn_VArray") { - DQN_TESTER_TEST("Test adding an array of items to the array") { - uint32_t array_literal[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; - Dqn_VArray_Add(&array, array_literal, DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + { + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + Dqn_VArray array = Dqn_VArray_InitByteSize(scratch.arena, DQN_KILOBYTES(64)); - DQN_TESTER_TEST("Test stable erase, 1 item, the '2' value from the array") { - Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, 1 /*count*/, Dqn_VArrayErase_Stable); - uint32_t array_literal[] = {0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + DQN_TESTER_TEST("Test adding an array of items to the array") { + uint32_t array_literal[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + Dqn_VArray_Add(&array, array_literal, DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } - DQN_TESTER_TEST("Test unstable erase, 1 item, the '1' value from the array") { - Dqn_VArray_EraseRange(&array, 1 /*begin_index*/, 1 /*count*/, Dqn_VArrayErase_Unstable); - uint32_t array_literal[] = {0, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + DQN_TESTER_TEST("Test stable erase, 1 item, the '2' value from the array") { + Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, 1 /*count*/, Dqn_VArrayErase_Stable); + uint32_t array_literal[] = {0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } - Dqn_VArrayErase erase_enums[] = {Dqn_VArrayErase_Stable, Dqn_VArrayErase_Unstable}; - DQN_TESTER_TEST("Test un/stable erase, OOB") { - for (Dqn_VArrayErase erase : erase_enums) { + DQN_TESTER_TEST("Test unstable erase, 1 item, the '1' value from the array") { + Dqn_VArray_EraseRange(&array, 1 /*begin_index*/, 1 /*count*/, Dqn_VArrayErase_Unstable); uint32_t array_literal[] = {0, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; - Dqn_VArray_EraseRange(&array, DQN_ARRAY_UCOUNT(array_literal) /*begin_index*/, DQN_ARRAY_UCOUNT(array_literal) + 100 /*count*/, erase); + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + + Dqn_VArrayErase erase_enums[] = {Dqn_VArrayErase_Stable, Dqn_VArrayErase_Unstable}; + DQN_TESTER_TEST("Test un/stable erase, OOB") { + for (Dqn_VArrayErase erase : erase_enums) { + uint32_t array_literal[] = {0, 15, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; + Dqn_VArray_EraseRange(&array, DQN_ARRAY_UCOUNT(array_literal) /*begin_index*/, DQN_ARRAY_UCOUNT(array_literal) + 100 /*count*/, erase); + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + } + + DQN_TESTER_TEST("Test flipped begin/end index stable erase, 2 items, the '15, 3' value from the array") { + Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, -2 /*count*/, Dqn_VArrayErase_Stable); + uint32_t array_literal[] = {0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + + DQN_TESTER_TEST("Test flipped begin/end index unstable erase, 2 items, the '4, 5' value from the array") { + Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, -2 /*count*/, Dqn_VArrayErase_Unstable); + uint32_t array_literal[] = {0, 13, 14, 6, 7, 8, 9, 10, 11, 12}; + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + + DQN_TESTER_TEST("Test stable erase range, 2+1 (oob) item, the '13, 14, +1 OOB' value from the array") { + Dqn_VArray_EraseRange(&array, 8 /*begin_index*/, 3 /*count*/, Dqn_VArrayErase_Stable); + uint32_t array_literal[] = {0, 13, 14, 6, 7, 8, 9, 10}; + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + + DQN_TESTER_TEST("Test unstable erase range, 3+1 (oob) item, the '11, 12, +1 OOB' value from the array") { + Dqn_VArray_EraseRange(&array, 6 /*begin_index*/, 3 /*count*/, Dqn_VArrayErase_Unstable); + uint32_t array_literal[] = {0, 13, 14, 6, 7, 8}; + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + + DQN_TESTER_TEST("Test stable erase -overflow OOB, erasing the '0, 13' value from the array") { + Dqn_VArray_EraseRange(&array, 1 /*begin_index*/, -DQN_ISIZE_MAX /*count*/, Dqn_VArrayErase_Stable); + uint32_t array_literal[] = {14, 6, 7, 8}; + DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); + DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + } + + DQN_TESTER_TEST("Test unstable erase +overflow OOB, erasing the '7, 8' value from the array") { + Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, DQN_ISIZE_MAX /*count*/, Dqn_VArrayErase_Unstable); + uint32_t array_literal[] = {14, 6}; DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); } } - DQN_TESTER_TEST("Test flipped begin/end index stable erase, 2 items, the '15, 3' value from the array") { - Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, -2 /*count*/, Dqn_VArrayErase_Stable); - uint32_t array_literal[] = {0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + DQN_TESTER_TEST("Array of unaligned objects are contiguously laid out in memory") { + // NOTE: Since we allocate from a virtual memory block, each time + // we request memory from the block we can demand some alignment + // on the returned pointer from the memory block. If there's + // additional alignment done in that function then we can no + // longer access the items in the array contiguously leading to + // confusing memory "corruption" errors. + // + // This test makes sure that the unaligned objects are allocated + // from the memory block (and hence the array) contiguously + // when the size of the object is not aligned with the required + // alignment of the object. - DQN_TESTER_TEST("Test flipped begin/end index unstable erase, 2 items, the '4, 5' value from the array") { - Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, -2 /*count*/, Dqn_VArrayErase_Unstable); - uint32_t array_literal[] = {0, 13, 14, 6, 7, 8, 9, 10, 11, 12}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + #if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable: 4324) // warning C4324: 'TestVArray::UnalignedObject': structure was padded due to alignment specifier + struct alignas(8) UnalignedObject { + char data[511]; + }; + #pragma warning(pop) + #endif // _MSC_VER - DQN_TESTER_TEST("Test stable erase range, 2+1 (oob) item, the '13, 14, +1 OOB' value from the array") { - Dqn_VArray_EraseRange(&array, 8 /*begin_index*/, 3 /*count*/, Dqn_VArrayErase_Stable); - uint32_t array_literal[] = {0, 13, 14, 6, 7, 8, 9, 10}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); + Dqn_VArray array = Dqn_VArray_InitByteSize(scratch.arena, DQN_KILOBYTES(64)); - DQN_TESTER_TEST("Test unstable erase range, 3+1 (oob) item, the '11, 12, +1 OOB' value from the array") { - Dqn_VArray_EraseRange(&array, 6 /*begin_index*/, 3 /*count*/, Dqn_VArrayErase_Unstable); - uint32_t array_literal[] = {0, 13, 14, 6, 7, 8}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + // NOTE: Verify that the items returned from the data array are + // contiguous in memory. + UnalignedObject *make_item_a = Dqn_VArray_Make(&array, 1, Dqn_ZeroMem_Yes); + UnalignedObject *make_item_b = Dqn_VArray_Make(&array, 1, Dqn_ZeroMem_Yes); + DQN_MEMSET(make_item_a->data, 'a', sizeof(make_item_a->data)); + DQN_MEMSET(make_item_b->data, 'b', sizeof(make_item_b->data)); + DQN_TESTER_ASSERT(&test, (uintptr_t)make_item_b == (uintptr_t)(make_item_a + 1)); - DQN_TESTER_TEST("Test stable erase, negative overflow OOB items, erasing the '0, 13' value from the array") { - Dqn_VArray_EraseRange(&array, 1 /*begin_index*/, -DQN_ISIZE_MAX /*count*/, Dqn_VArrayErase_Stable); - uint32_t array_literal[] = {14, 6, 7, 8}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); - } + // NOTE: Verify that accessing the items from the data array yield + // the same object. + DQN_TESTER_ASSERT(&test, array.size == 2); + UnalignedObject *data_item_a = array.data + 0; + UnalignedObject *data_item_b = array.data + 1; + DQN_TESTER_ASSERT(&test, (uintptr_t)data_item_b == (uintptr_t)(data_item_a + 1)); + DQN_TESTER_ASSERT(&test, (uintptr_t)data_item_b == (uintptr_t)(make_item_a + 1)); + DQN_TESTER_ASSERT(&test, (uintptr_t)data_item_b == (uintptr_t)make_item_b); - DQN_TESTER_TEST("Test unstable erase, positive overflow OOB items, erasing the '0, 13' value from the array") { - Dqn_VArray_EraseRange(&array, 2 /*begin_index*/, DQN_ISIZE_MAX /*count*/, Dqn_VArrayErase_Unstable); - uint32_t array_literal[] = {14, 6}; - DQN_TESTER_ASSERT(&test, array.size == DQN_ARRAY_UCOUNT(array_literal)); - DQN_TESTER_ASSERT(&test, DQN_MEMCMP(array.data, array_literal, DQN_ARRAY_UCOUNT(array_literal) * sizeof(array_literal[0])) == 0); + for (int i = 0; i < sizeof(data_item_a->data); i++) { + DQN_TESTER_ASSERT(&test, data_item_a->data[i] == 'a'); + } + + for (int i = 0; i < sizeof(data_item_b->data); i++) { + DQN_TESTER_ASSERT(&test, data_item_b->data[i] == 'b'); + } } } return test; @@ -1505,6 +1567,7 @@ Dqn_Tester TestWin() void TestRunSuite() { + Dqn_Library_Init(nullptr); Dqn_Tester tests[] { TestArena(), diff --git a/readme.md b/readme.md index bfd4bfc..3f90b6b 100644 --- a/readme.md +++ b/readme.md @@ -1,2 +1,3 @@ # Dqn -Personal utility library that provides allocator aware data structures, custom memory allocators and various miscellaneous helpers. +Personal utility library that provides allocator aware data structures, custom +memory allocators and various miscellaneous helpers.