From 3bb224b5e457d7994dd0bb24ff8874f94d0871c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Fri, 4 Sep 2026 22:47:57 +0200 Subject: [PATCH 01/60] Simplify memory arena page layout --- src/Foundations/SystemMemory.cpp | 96 ++++++++++++++++---------- src/Foundations/SystemMemory.h | 8 +-- tests/FoundationsTests/MemoryTests.cpp | 55 +++------------ 3 files changed, 67 insertions(+), 92 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index 61dc36de..f53fa235 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -46,13 +46,17 @@ struct PageSizeIndexes thread_local MemoryArenaStorage* stackMemoryArenaStorage = nullptr; size_t systemPageSizeInBytes = 0; +void PopStackMemory(MemoryArena memoryArena, size_t sizeInBytes); +size_t ResizeToPageSizeMultiple(size_t sizeInBytes, size_t pageSizeInBytes); + PageSizeIndexes ComputePageSizeInfoIndexes(MemoryArenaStorage* storage, void* pointer, size_t sizeInBytes) { - auto offset = (uint8_t*)pointer - (uint8_t*)storage; + auto dataStart = (uint8_t*)storage + storage->HeaderSizeInBytes; + auto offset = (uint8_t*)pointer - dataStart; PageSizeIndexes result = {}; result.StartIndex = offset / systemPageSizeInBytes; - result.EndIndex = (size_t)SystemRoundUp((float)(offset + sizeInBytes) / systemPageSizeInBytes); + result.EndIndex = ResizeToPageSizeMultiple(offset + sizeInBytes, systemPageSizeInBytes) / systemPageSizeInBytes; return result; } @@ -61,7 +65,7 @@ PageSizeIndexes ComputePageSizeLocalOffsets(MemoryArenaStorage* storage, size_t auto absoluteStart = (uint8_t*)pointer; auto absoluteEnd = (uint8_t*)pointer + sizeInBytes; - auto pageStart = (uint8_t*)storage + index * systemPageSizeInBytes; + auto pageStart = (uint8_t*)storage + storage->HeaderSizeInBytes + index * systemPageSizeInBytes; auto pageEnd = pageStart + systemPageSizeInBytes; PageSizeIndexes result = {}; @@ -104,45 +108,32 @@ MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) systemPageSizeInBytes = SystemPlatformGetPageSize(); } - auto pageInfosCount = SystemRoundUp((float)sizeInBytes / (float)systemPageSizeInBytes); - auto headerSizeInBytes = sizeof(MemoryArenaStorage) + pageInfosCount * sizeof(MemoryArenaPageInfo) + SystemRoundUp((float)pageInfosCount / 32) * sizeof(MemoryArenaPageCommitInfo); - - auto sizeResized = ResizeToPageSizeMultiple(headerSizeInBytes + sizeInBytes, systemPageSizeInBytes); - auto storage = (MemoryArenaStorage*)SystemPlatformReserveMemory(sizeResized); + auto dataSizeInBytes = ResizeToPageSizeMultiple(sizeInBytes, systemPageSizeInBytes); + auto pageInfosCount = dataSizeInBytes / systemPageSizeInBytes; + auto pageCommitInfosCount = (pageInfosCount + 31) / 32; + auto headerMetadataSizeInBytes = sizeof(MemoryArenaStorage) + pageInfosCount * sizeof(MemoryArenaPageInfo) + pageCommitInfosCount * sizeof(MemoryArenaPageCommitInfo); + auto headerSizeInBytes = ResizeToPageSizeMultiple(headerMetadataSizeInBytes, systemPageSizeInBytes); + auto reservedSizeInBytes = headerSizeInBytes + dataSizeInBytes; - auto headerResized = ResizeToPageSizeMultiple(headerSizeInBytes, systemPageSizeInBytes); - SystemPlatformCommitMemory(storage, headerResized); + auto storage = (MemoryArenaStorage*)SystemPlatformReserveMemory(reservedSizeInBytes); + SystemPlatformCommitMemory(storage, headerSizeInBytes); storage->CurrentPointer = (uint8_t*)storage + headerSizeInBytes; storage->SizeInBytes = sizeInBytes; storage->HeaderSizeInBytes = headerSizeInBytes; storage->IsCommitOperationInProgres = false; - storage->CommittedPagesCount = 0; + storage->CommittedPagesCount = headerSizeInBytes / systemPageSizeInBytes; storage->PagesInfos = (MemoryArenaPageInfo*)((uint8_t*)storage + sizeof(MemoryArenaStorage)); storage->PagesCommitInfos = (MemoryArenaPageCommitInfo*)((uint8_t*)storage + sizeof(MemoryArenaStorage) + pageInfosCount * sizeof(MemoryArenaPageInfo)); storage->StackExtraStorage = {}; storage->StackLevel = 0; storage->StackMinAllocatedLevel = 255; - auto headerPageCount = (size_t)SystemRoundUp((float)headerResized / systemPageSizeInBytes); - - for (size_t i = 0; i < (size_t)pageInfosCount; i++) + for (size_t i = 0; i < pageInfosCount; i++) { - if (headerPageCount > i) - { - auto offsets = ComputePageSizeLocalOffsets(storage, i, storage, headerSizeInBytes); - - SetPageCommitted(storage, (uint32_t)i); - storage->CommittedPagesCount++; - storage->PagesInfos[i].MinCommittedOffset = offsets.StartIndex; - storage->PagesInfos[i].MaxCommittedOffset = offsets.EndIndex; - } - else - { - ClearPageCommitted(storage, (uint32_t)i); - storage->PagesInfos[i].MinCommittedOffset = systemPageSizeInBytes - 1; - storage->PagesInfos[i].MaxCommittedOffset = 0; - } + ClearPageCommitted(storage, (uint32_t)i); + storage->PagesInfos[i].MinCommittedOffset = systemPageSizeInBytes - 1; + storage->PagesInfos[i].MaxCommittedOffset = 0; } return storage; @@ -167,6 +158,17 @@ MemoryArena GetStackWorkingMemoryArena(MemoryArena memoryArena) return workingMemoryArena; } +bool IsStackMemoryArena(MemoryArena memoryArena) +{ + if (stackMemoryArenaStorage == nullptr) + { + return false; + } + + return memoryArena.Storage == stackMemoryArenaStorage || + (stackMemoryArenaStorage->StackExtraStorage.Storage != nullptr && memoryArena.Storage == stackMemoryArenaStorage->StackExtraStorage.Storage); +} + size_t GetMemoryArenaAllocatedBytes(MemoryArena memoryArena) { return memoryArena.Storage->CurrentPointer - (uint8_t*)memoryArena.Storage - memoryArena.Storage->HeaderSizeInBytes; @@ -198,12 +200,27 @@ MemoryArena SystemAllocateMemoryArena(size_t sizeInBytes) void SystemFreeMemoryArena(MemoryArena memoryArena) { - SystemPlatformFreeMemory(memoryArena.Storage, memoryArena.Storage->HeaderSizeInBytes + memoryArena.Storage->SizeInBytes); + auto dataSizeInBytes = ResizeToPageSizeMultiple(memoryArena.Storage->SizeInBytes, systemPageSizeInBytes); + SystemPlatformFreeMemory(memoryArena.Storage, memoryArena.Storage->HeaderSizeInBytes + dataSizeInBytes); } void SystemClearMemoryArena(MemoryArena memoryArena) { - SystemPopMemory(memoryArena, GetMemoryArenaAllocatedBytes(memoryArena)); + auto storage = memoryArena.Storage; + auto allocatedSize = GetMemoryArenaAllocatedBytes(memoryArena); + + if (allocatedSize == 0) + { + return; + } + + auto pointer = storage->CurrentPointer; + storage->CurrentPointer -= allocatedSize; + + if (memoryArena.Storage != stackMemoryArenaStorage) + { + SystemDecommitMemory(memoryArena, pointer - allocatedSize, allocatedSize); + } } MemoryArenaAllocationInfos SystemGetMemoryArenaAllocationInfos(MemoryArena memoryArena) @@ -253,7 +270,7 @@ StackMemoryArena::~StackMemoryArena() if (extraBytesToPop && storage->StackMinAllocatedLevel >= Arena.Level) { - SystemPopMemory(storage->StackExtraStorage, extraBytesToPop); + PopStackMemory(storage->StackExtraStorage, extraBytesToPop); storage->StackMinAllocatedLevel = 255; } } @@ -264,7 +281,7 @@ StackMemoryArena::~StackMemoryArena() if (bytesToPop > 0) { - SystemPopMemory(Arena, bytesToPop); + PopStackMemory(Arena, bytesToPop); } } @@ -330,11 +347,12 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt { if (!IsPageCommitted(storage, (uint32_t)i)) { - SystemPlatformCommitMemory((uint8_t*)storage + i * systemPageSizeInBytes, systemPageSizeInBytes); + auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * systemPageSizeInBytes; + SystemPlatformCommitMemory(pagePointer, systemPageSizeInBytes); if (clearMemory) { - SystemPlatformClearMemory((uint8_t*)storage + i * systemPageSizeInBytes, systemPageSizeInBytes); + SystemPlatformClearMemory(pagePointer, systemPageSizeInBytes); } SetPageCommitted(storage, (uint32_t)i); @@ -404,7 +422,7 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB if (IsPageCommitted(storage, (uint32_t)i)) { - auto pagePointer = (uint8_t*)storage + i * systemPageSizeInBytes; + auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * systemPageSizeInBytes; if ((int32_t)(pageInfos->MaxCommittedOffset - pageInfos->MinCommittedOffset) <= 0) { @@ -455,14 +473,16 @@ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationSt return pointer; } -void SystemPopMemory(MemoryArena memoryArena, size_t sizeInBytes) +void PopStackMemory(MemoryArena memoryArena, size_t sizeInBytes) { + SystemAssert(IsStackMemoryArena(memoryArena)); + auto storage = memoryArena.Storage; auto allocatedSize = GetMemoryArenaAllocatedBytes(memoryArena); if (sizeInBytes > allocatedSize) { - SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot pop memory arena with: %u (Allocated size is: %u)", (uint32_t)sizeInBytes, (uint32_t)allocatedSize); + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot pop stack memory arena with: %u (Allocated size is: %u)", (uint32_t)sizeInBytes, (uint32_t)allocatedSize); return; } diff --git a/src/Foundations/SystemMemory.h b/src/Foundations/SystemMemory.h index d0c5e607..658d94a6 100644 --- a/src/Foundations/SystemMemory.h +++ b/src/Foundations/SystemMemory.h @@ -107,6 +107,7 @@ void SystemFreeMemoryArena(MemoryArena memoryArena); /** * Clears the contents of a MemoryArena. + * This operation is not thread-safe and requires exclusive access to the arena. * @param memoryArena Pointer to the MemoryArena to be cleared. */ void SystemClearMemoryArena(MemoryArena memoryArena); @@ -133,13 +134,6 @@ StackMemoryArena SystemGetStackMemoryArena(); */ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationState state = AllocationState_Committed); -/** - * Frees a block of memory in a MemoryArena. - * @param memoryArena MemoryArena containing the block. - * @param sizeInBytes Size of the memory block to free. - */ -void SystemPopMemory(MemoryArena memoryArena, size_t sizeInBytes); - /** * Commits a block of memory in a MemoryArena. * diff --git a/tests/FoundationsTests/MemoryTests.cpp b/tests/FoundationsTests/MemoryTests.cpp index af0b05d2..aad57ad3 100644 --- a/tests/FoundationsTests/MemoryTests.cpp +++ b/tests/FoundationsTests/MemoryTests.cpp @@ -19,16 +19,6 @@ void MemoryConcurrentAddFunction(void* parameter) } } -void MemoryConcurrentPopFunction(void* parameter) -{ - auto threadParameter = (MemoryThreadParameter*)parameter; - - for (int32_t i = 0; i < threadParameter->ItemCount; i++) - { - SystemPopMemory(threadParameter->MemoryArena, 64); - } -} - UTEST(Memory, Allocate) { // Arrange @@ -53,23 +43,24 @@ UTEST(Memory, AllocateMultiple) // Act SystemPushArrayZero(memoryArena, dataSizeInBytes); SystemPushArrayZero(memoryArena, 1024); - SystemPopMemory(memoryArena, 20000); // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(dataSizeInBytes + 1024 - 20000, allocationInfos.AllocatedBytes); + ASSERT_EQ(dataSizeInBytes + 1024, allocationInfos.AllocatedBytes); ASSERT_GT(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes); } -UTEST(Memory, AllocatePop) +UTEST(Memory, ClearMemoryArena) { // Arrange auto memoryArena = SystemAllocateMemoryArena(); - auto dataSizeInBytes = 64llu; - + auto dataSizeInBytes = 70024llu; + + SystemPushArrayZero(memoryArena, dataSizeInBytes); + SystemPushArrayZero(memoryArena, 1024); + // Act - SystemPushArrayZero(memoryArena, dataSizeInBytes); - SystemPopMemory(memoryArena, dataSizeInBytes); + SystemClearMemoryArena(memoryArena); // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); @@ -237,36 +228,6 @@ UTEST(Memory, ConcurrentPush) ASSERT_EQ(maxSize, allocationInfos.AllocatedBytes); } -UTEST(Memory, ConcurrentPop) -{ - // Arrange - const int32_t itemCount = 80000; - const int32_t threadCount = 32; - auto maxSize = (size_t)itemCount * 64; - auto memoryArena = SystemAllocateMemoryArena(maxSize); - SystemPushMemory(memoryArena, maxSize); - - // Act - SystemThread threads[threadCount]; - MemoryThreadParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { memoryArena, i, itemCount / threadCount }; - threads[i] = SystemCreateThread(MemoryConcurrentPopFunction, &threadParameters[i]); - } - - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(0llu, allocationInfos.AllocatedBytes); -} - UTEST(Memory, AllocateReserved) { // Arrange From 4a59f749ebce555f8015466b122ae2f0c429a586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:00:42 +0200 Subject: [PATCH 02/60] Document memory arena contracts --- src/Foundations/SystemMemory.h | 352 ++++++++++++++++++++++----------- 1 file changed, 232 insertions(+), 120 deletions(-) diff --git a/src/Foundations/SystemMemory.h b/src/Foundations/SystemMemory.h index 658d94a6..df71ce8a 100644 --- a/src/Foundations/SystemMemory.h +++ b/src/Foundations/SystemMemory.h @@ -15,62 +15,85 @@ /** - * Enumeration representing the allocation state of a memory block. + * Defines whether an arena allocation is immediately backed by committed memory. */ enum AllocationState { - AllocationState_Committed, ///< Indicates the memory block is allocated and committed. - AllocationState_Reserved ///< Indicates the memory block is reserved but not committed. + AllocationState_Committed, ///< The allocated range is committed and can be accessed immediately. + AllocationState_Reserved ///< The allocated range is reserved only and must be committed before it is accessed. }; /** - * Struct providing information about overall memory allocation. + * Provides process-wide virtual memory allocation information reported by the platform layer. */ struct AllocationInfos { - size_t CommittedBytes; ///< Total bytes of committed memory. - size_t ReservedBytes; ///< Total bytes of reserved memory. + size_t CommittedBytes; ///< Total number of committed bytes. + size_t ReservedBytes; ///< Total number of reserved virtual-address bytes. }; struct MemoryArenaStorage; /** - * Represents a memory arena for managing memory allocations efficiently. + * Lightweight handle to a MemoryArena storage. + * + * MemoryArena is intentionally passed and copied by value. Copying a MemoryArena does not copy + * its allocations or storage; every copy references the same MemoryArenaStorage. No ownership, + * reference counting, or lifetime tracking is added by the handle. + * + * For regular arenas, Level is 0. For handles produced by StackMemoryArena, Level identifies the + * stack lifetime associated with that handle and allows an ancestor arena to be passed down the + * call tree while preserving the ancestor allocation lifetime. + * + * The caller is responsible for respecting the lifetime of the referenced storage. Freeing an + * arena invalidates every MemoryArena value and every allocation that references that storage. */ struct MemoryArena { - MemoryArenaStorage* Storage; ///< Internal storage structure of the memory arena. - uint8_t Level; ///< Nesting level of the memory arena. + MemoryArenaStorage* Storage; ///< Shared internal storage referenced by this handle. + uint8_t Level; ///< Stack lifetime level, or 0 for a regular arena. }; /** - * Struct providing detailed information about allocations within a MemoryArena. + * Provides allocation information for a MemoryArena. */ struct MemoryArenaAllocationInfos { - size_t AllocatedBytes; ///< Total bytes currently allocated in the MemoryArena. - size_t CommittedBytes; ///< Total bytes committed in the MemoryArena. - size_t MaximumSizeInBytes; ///< Maximum allocatable size of the MemoryArena in bytes. + size_t AllocatedBytes; ///< Bytes currently allocated from the arena data region. + size_t CommittedBytes; ///< Bytes currently committed by the arena, including its internal header pages. + size_t MaximumSizeInBytes; ///< Maximum number of data bytes that can be allocated from the arena. }; /** - * Specialized MemoryArena for stack-based memory management. + * Scoped thread-local MemoryArena. + * + * Destroying the StackMemoryArena releases allocations associated with its stack lifetime. The + * contained MemoryArena can be passed by value to deeper functions while this scope is alive. + * Passing a MemoryArena from an ancestor scope allows a deeper function to allocate data that + * survives its local stack scopes and is released with that ancestor. + * + * StackMemoryArena is thread-local and must not be shared across threads. A MemoryArena obtained + * from it must not be retained after the corresponding StackMemoryArena scope has ended. + * + * StackMemoryArena itself represents a scope and must not be copied by user code. Copying the + * contained MemoryArena handle is the intended way to pass an allocation lifetime around. */ struct StackMemoryArena { - MemoryArena Arena; ///< Associated MemoryArena object. + MemoryArena Arena; ///< MemoryArena handle associated with this stack scope. - size_t StartOffsetInBytes; ///< Starting offset for memory allocations within the arena. - size_t StartExtraOffsetInBytes; ///< Additional internal offset. + size_t StartOffsetInBytes; ///< Internal data offset captured when the scope begins. + size_t StartExtraOffsetInBytes; ///< Internal ancestor-lifetime storage offset captured when the scope begins. /** - * Destructor for StackMemoryArena. + * Releases allocations owned by this stack scope and restores the previous stack lifetime. */ ~StackMemoryArena(); /** - * Conversion operator to a MemoryArena pointer. - * @return Pointer to the associated MemoryArena. + * Returns the lightweight MemoryArena handle associated with this stack scope. + * + * @return MemoryArena value that can be passed to allocation functions while this scope is alive. */ operator MemoryArena() const { @@ -79,212 +102,301 @@ struct StackMemoryArena }; /** - * Retrieves information about system-wide memory allocations. - * @return AllocationInfos structure with memory allocation details. + * Retrieves process-wide virtual memory allocation information from the platform layer. + * + * @return Allocation information containing committed and reserved byte counts. */ AllocationInfos SystemGetAllocationInfos(); /** - * Creates a new MemoryArena with a default size. - * Default size is 64GB. Note that the memory is reserved but committed only when needed. - * @return Pointer to the newly created MemoryArena. + * Allocates a MemoryArena using the default capacity. + * + * The arena reserves its virtual address range up front while data pages are committed on demand. + * The returned MemoryArena is a lightweight value handle to the allocated storage. The caller is + * responsible for releasing that storage exactly once with SystemFreeMemoryArena(). + * + * @return MemoryArena handle referencing the newly allocated storage. */ MemoryArena SystemAllocateMemoryArena(); /** - * Creates a new MemoryArena with a specified size. - * Note that the memory is reserved but committed only when needed. - * @param sizeInBytes Size of the MemoryArena in bytes. - * @return Pointer to the newly created MemoryArena. + * Allocates a MemoryArena with the specified data capacity. + * + * The arena reserves enough virtual address space for its internal metadata and requested data + * capacity. Internal header pages are committed immediately; data pages are committed on demand. + * The returned MemoryArena can be copied freely, but all copies reference the same storage. + * + * @param sizeInBytes Maximum number of data bytes that can be allocated from the arena. + * @return MemoryArena handle referencing the newly allocated storage. */ MemoryArena SystemAllocateMemoryArena(size_t sizeInBytes); /** - * Frees the memory associated with a MemoryArena. - * @param memoryArena Pointer to the MemoryArena to be freed. + * Releases the storage referenced by a MemoryArena. + * + * This is an exclusive lifetime operation and is not safe to call while another thread is using + * the arena. All MemoryArena copies and all pointers/spans allocated from the arena become invalid + * immediately after this call. The function does not perform reference counting or alias tracking. + * + * StackMemoryArena storage is managed by the stack arena system and must not be released through + * this function. + * + * @param memoryArena MemoryArena whose storage will be released. */ void SystemFreeMemoryArena(MemoryArena memoryArena); /** - * Clears the contents of a MemoryArena. - * This operation is not thread-safe and requires exclusive access to the arena. - * @param memoryArena Pointer to the MemoryArena to be cleared. + * Resets a MemoryArena to its initial empty state. + * + * All allocations made from the arena become invalid. The MemoryArena storage and copied handles + * remain valid and can be used for new allocations after the reset. + * + * This is an exclusive operation and is intentionally not thread-safe. The caller must guarantee + * that no other thread is reading from, allocating from, committing, or decommitting the arena. + * + * @param memoryArena MemoryArena to reset. */ void SystemClearMemoryArena(MemoryArena memoryArena); /** - * Retrieves allocation information for a specific MemoryArena. + * Retrieves allocation information for a MemoryArena. + * * @param memoryArena MemoryArena to query. - * @return MemoryArenaAllocationInfos structure with detailed allocation information. + * @return Current allocated, committed, and maximum data-capacity information. */ MemoryArenaAllocationInfos SystemGetMemoryArenaAllocationInfos(MemoryArena memoryArena); /** - * Gets a StackMemoryArena, a specialized MemoryArena with stack-based allocation. - * @return A StackMemoryArena. + * Begins a new scoped MemoryArena lifetime on the current thread. + * + * Stack arenas are nested per thread. The returned object owns the scope rollback, while its + * contained MemoryArena is the lightweight value intended to be passed down the call tree. + * Allocating through an ancestor MemoryArena from a deeper scope preserves the ancestor lifetime. + * + * @return StackMemoryArena representing the newly entered stack scope. */ StackMemoryArena SystemGetStackMemoryArena(); /** - * Allocates a block of memory in a MemoryArena. - * @param memoryArena MemoryArena for the allocation. - * @param sizeInBytes Size of the memory block to allocate. - * @param state Allocation state (committed or reserved). - * @return Pointer to the allocated memory block. + * Allocates a contiguous range of bytes from a MemoryArena. + * + * The allocation advances the arena and is not individually freed. Regular shared MemoryArena + * allocation is intended to be thread-safe; StackMemoryArena allocation is thread-local. + * + * A committed allocation can be accessed immediately. A reserved allocation only reserves its + * range in the arena and must be committed with SystemCommitMemory() before access. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param sizeInBytes Number of bytes to allocate. + * @param state Initial allocation state. + * @return Pointer to the allocated range, or nullptr if the arena cannot satisfy the allocation. */ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationState state = AllocationState_Committed); /** - * Commits a block of memory in a MemoryArena. - * - * @param memoryArena The MemoryArena to commit memory in. - * @param pointer Start pointer for memory commitment. - * @param sizeInBytes Size of memory block in bytes. - * @param clearMemory If true, initializes memory to 0. Defaults to false. + * Commits the pages covering a previously allocated range in a MemoryArena. + * + * The range must belong to the specified arena. Commitment is tracked at platform page granularity, + * so pages shared by multiple logical ranges remain committed while any tracked range still needs + * them. The operation is intended to be thread-safe for regular shared MemoryArena instances. + * + * If clearMemory is true, pages that are newly committed by this operation are cleared before use. + * Use SystemPushMemoryZero() when the exact returned allocation range must be initialized to zero. + * + * @param memoryArena MemoryArena containing the range. + * @param pointer Start of the range to commit. + * @param sizeInBytes Number of bytes in the range. + * @param clearMemory Whether newly committed pages should be cleared. */ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory = false); /** - * Commits memory for an array of elements in a MemoryArena. - * - * @tparam T Element type in the buffer. - * @param memoryArena The MemoryArena to commit memory in. - * @param buffer ReadOnlySpan representing an array of elements. - * @param clearMemory If true, initializes memory to 0. Defaults to false. + * Commits the pages covering a previously allocated buffer in a MemoryArena. + * + * The buffer must reference memory allocated from the specified arena. Commitment is tracked at + * platform page granularity. + * + * @tparam T Element type stored in the buffer. + * @param memoryArena MemoryArena containing the buffer. + * @param buffer Buffer whose memory range will be committed. + * @param clearMemory Whether newly committed pages should be cleared. */ template void SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory = false); /** - * Decomits memory in the specified MemoryArena. - * The page will be decommitted if all the allocations (including spaces between them) have been decommitted. - * @param memoryArena MemoryArena in which to decommit memory. - * @param pointer Pointer to start decommitting memory. - * @param sizeInBytes Size of the memory block to decommit. + * Decommits pages that are no longer needed by a range in a MemoryArena. + * + * Decommitting memory does not release the logical arena allocation or move the arena pointer. The + * same reserved range can be committed again later. Physical pages are only decommitted when the + * arena bookkeeping determines that no remaining committed range still needs that page. + * + * The caller is responsible for passing a valid range belonging to the arena and for not accessing + * the range while it is decommitted. The operation is intended to be thread-safe for regular shared + * MemoryArena instances. + * + * @param memoryArena MemoryArena containing the range. + * @param pointer Start of the range to decommit. + * @param sizeInBytes Number of bytes in the range. */ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes); /** - * Allocates and initializes a block of memory with zero values in the specified MemoryArena. - * @param memoryArena A pointer to the MemoryArena. - * @param sizeInBytes The size, in bytes, to allocate and initialize. - * @return A pointer to the allocated and initialized memory block. + * Allocates a committed range of bytes and initializes the requested range to zero. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param sizeInBytes Number of bytes to allocate and clear. + * @return Pointer to the allocated range, or nullptr if the arena cannot satisfy the allocation. */ void* SystemPushMemoryZero(MemoryArena memoryArena, size_t sizeInBytes); /** - * Allocates an array of elements in the specified MemoryArena. - * @tparam T The type of elements in the array. - * @param memoryArena A pointer to the MemoryArena. - * @param count The number of elements to allocate. - * @return A Span representing the newly allocated array. + * Allocates a contiguous array from a MemoryArena. + * + * The returned Span references arena-owned memory and remains valid only for the lifetime of the + * corresponding arena allocation context. + * + * @tparam T Element type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Number of elements to allocate. + * @param state Initial allocation state. + * @return Span referencing the allocated array. */ template Span SystemPushArray(MemoryArena memoryArena, size_t count, AllocationState state = AllocationState_Committed); /** - * Allocates and initializes an array of elements with zero values in the specified MemoryArena. - * @tparam T The type of elements in the array. - * @param memoryArena A pointer to the MemoryArena. - * @param count The number of elements to allocate and initialize. - * @return A Span representing the newly allocated and initialized array. + * Allocates a contiguous array and initializes it to zero. + * + * @tparam T Element type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Number of elements to allocate and clear. + * @return Span referencing the zero-initialized array. */ template Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** - * Allocates and initializes an array of elements with zero values in the specified MemoryArena. - * @param memoryArena A pointer to the MemoryArena. - * @param count The number of elements to allocate and initialize. - * @return A Span representing the newly allocated and initialized array. + * Allocates a zero-initialized char array with an additional zero terminator after the returned Span. + * + * The terminator is allocated immediately after the requested elements and is not included in the + * returned Span length. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Number of char elements in the returned Span. + * @return Span referencing the requested zero-initialized char elements. */ template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** - * Allocates and initializes an array of elements with zero values in the specified MemoryArena. - * @param memoryArena A pointer to the MemoryArena. - * @param count The number of elements to allocate and initialize. - * @return A Span representing the newly allocated and initialized array. + * Allocates a zero-initialized wchar_t array with an additional zero terminator after the returned Span. + * + * The terminator is allocated immediately after the requested elements and is not included in the + * returned Span length. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Number of wchar_t elements in the returned Span. + * @return Span referencing the requested zero-initialized wchar_t elements. */ template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** - * Allocates a single instance of a structure in the specified MemoryArena. - * @tparam T The type of structure to allocate. - * @param memoryArena A pointer to the MemoryArena. - * @return A pointer to the newly allocated structure. + * Allocates storage for one structure from a MemoryArena. + * + * No constructor is invoked; this is raw arena allocation for T. + * + * @tparam T Structure type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @return Pointer to the allocated storage, or nullptr if the arena cannot satisfy the allocation. */ template T* SystemPushStruct(MemoryArena memoryArena); /** - * Allocates and initializes a single instance of a structure with zero values in the specified MemoryArena. - * @tparam T The type of structure to allocate. - * @param memoryArena A pointer to the MemoryArena. - * @return A pointer to the newly allocated and initialized structure. + * Allocates zero-initialized storage for one structure from a MemoryArena. + * + * No constructor is invoked; this is raw zeroed arena allocation for T. + * + * @tparam T Structure type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @return Pointer to the zero-initialized storage, or nullptr if the arena cannot satisfy the allocation. */ template T* SystemPushStructZero(MemoryArena memoryArena); /** - * Copies elements from a source buffer to a destination buffer. - * @tparam T The type of elements in the buffers. - * @param destination A Span representing the destination buffer. - * @param source A ReadOnlySpan representing the source buffer. + * Copies all source elements into an existing destination buffer. + * + * The destination must contain at least source.Length elements. No allocation is performed. + * + * @tparam T Element type stored in the buffers. + * @param destination Destination buffer. + * @param source Source buffer to copy. */ template void SystemCopyBuffer(Span destination, ReadOnlySpan source); /** - * Dupliquate elements from a source buffer to a destination buffer. - * @tparam T The type of elements in the buffers. - * @param memoryArena A pointer to the MemoryArena. - * @param source A ReadOnlySpan representing the source buffer. - * @return A pointer to the newly allocated structure that contains a copy of source. + * Allocates a new buffer in a MemoryArena and copies the source elements into it. + * + * @tparam T Element type stored in the buffer. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param source Source buffer to duplicate. + * @return Span referencing the newly allocated copy. */ template Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source); /** - * Dupliquate elements from a source buffer to a destination buffer. - * @tparam T The type of elements in the buffers. - * @param memoryArena A pointer to the MemoryArena. - * @param source A ReadOnlySpan representing the source buffer. - * @return A pointer to the newly allocated structure that contains a copy of source. + * Allocates a new char buffer in a MemoryArena and copies the source into it. + * + * The char specialization preserves zero-initialized storage after the copied data so the result can + * be used by code that expects a zero-terminated character sequence. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param source Source character buffer to duplicate. + * @return Span referencing the newly allocated copy. */ template<> Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source); /** - * Concatenates two buffers into a new buffer allocated in the specified memory arena. - * @tparam T The type of elements in the buffers. - * @param memoryArena The memory arena to allocate space for the concatenated buffer. - * @param buffer1 A ReadOnlySpan representing the first buffer to concatenate. - * @param buffer2 A ReadOnlySpan representing the second buffer to concatenate. - * @return A Span representing the newly allocated concatenated buffer. + * Allocates a buffer containing the concatenation of two source buffers. + * + * @tparam T Element type stored in the buffers. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param buffer1 First source buffer. + * @param buffer2 Second source buffer. + * @return Span referencing the concatenated buffer. */ template Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); /** - * Concatenates two buffers into a new buffer allocated in the specified memory arena. - * @param memoryArena The memory arena to allocate space for the concatenated buffer. - * @param buffer1 A ReadOnlySpan representing the first buffer to concatenate. - * @param buffer2 A ReadOnlySpan representing the second buffer to concatenate. - * @return A Span representing the newly allocated concatenated buffer. + * Allocates a char buffer containing the concatenation of two source buffers. + * + * The specialization allocates an additional zero terminator after the returned Span. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param buffer1 First source character buffer. + * @param buffer2 Second source character buffer. + * @return Span referencing the concatenated characters, excluding the trailing terminator. */ template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); /** - * Concatenates two buffers into a new buffer allocated in the specified memory arena. - * @param memoryArena The memory arena to allocate space for the concatenated buffer. - * @param buffer1 A ReadOnlySpan representing the first buffer to concatenate. - * @param buffer2 A ReadOnlySpan representing the second buffer to concatenate. - * @return A Span representing the newly allocated concatenated buffer. + * Allocates a wchar_t buffer containing the concatenation of two source buffers. + * + * The specialization allocates an additional zero terminator after the returned Span. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param buffer1 First source wide-character buffer. + * @param buffer2 Second source wide-character buffer. + * @return Span referencing the concatenated characters, excluding the trailing terminator. */ template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); From 49635005666652eddbe034c0948f767a8a817dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:17:01 +0200 Subject: [PATCH 03/60] Harden span contracts and const usage --- src/Foundations/SystemSpan.h | 205 ++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 102 deletions(-) diff --git a/src/Foundations/SystemSpan.h b/src/Foundations/SystemSpan.h index 2e961fc8..ac6c735a 100644 --- a/src/Foundations/SystemSpan.h +++ b/src/Foundations/SystemSpan.h @@ -3,18 +3,27 @@ #include /** - * Represents a span of elements in memory. + * Lightweight non-owning mutable view over a contiguous sequence of elements. * - * The Span class provides a non-owning view over a contiguous sequence of elements. - * It is designed for efficient and safe manipulation of arrays and memory buffers. + * Span is intentionally passed and copied by value. Copying a Span only copies its pointer and + * length; it does not copy, allocate, own, or track the lifetime of the referenced elements. * - * @tparam T The type of elements in the span. + * Element access and slicing are intentionally unchecked. The caller is responsible for keeping + * indexes and slices inside the referenced range and for ensuring that the underlying storage + * remains valid for the lifetime of the Span. + * + * A Span or Span is still only a pointer-and-length view. Some Foundations string + * helpers allocate an additional null character immediately after Length, but null termination is + * a property of those produced buffers, not of Span itself. In particular, a slice is not + * necessarily null-terminated. + * + * @tparam T Element type referenced by the span. */ template struct Span { /** - * Default constructor for an empty span. + * Constructs an empty Span. */ Span() { @@ -23,10 +32,12 @@ struct Span } /** - * Constructs a span with the given pointer and length. + * Constructs a Span over an existing contiguous range. + * + * No ownership or lifetime tracking is added. * - * @param pointer The pointer to the first element of the span. - * @param length The length of the span. + * @param pointer Pointer to the first element. + * @param length Number of elements in the range. */ Span(T* pointer, size_t length) { @@ -34,67 +45,72 @@ struct Span Length = length; } - /** - * Pointer to the first element of the span. - */ - T* Pointer; + T* Pointer; ///< Pointer to the first element, or nullptr for an empty span. + size_t Length; ///< Number of elements in the view. /** - * Length of the span. - */ - size_t Length; - - /** - * Accesses the element at the specified index. + * Returns a mutable reference to an element. + * + * No bounds check is performed. * - * @param index The index of the element to access. - * @return A reference to the element at the specified index. + * @param index Element index inside the span. + * @return Mutable reference to the selected element. */ - T& operator[](int index) + T& operator[](size_t index) const { - // TODO: Check bounds return Pointer[index]; } /** - * Creates a new span that represents a slice of the current span. + * Returns the suffix beginning at start. * - * @param start The starting index of the slice. - * @return A new span representing the sliced portion. + * No bounds check is performed. For character spans, slicing does not guarantee that the + * returned view is null-terminated at its new Length. + * + * @param start Index of the first element in the returned span. + * @return Span covering [start, Length). */ - Span Slice(size_t start) + Span Slice(size_t start) const { - // TODO: Add checks return Span(Pointer + start, Length - start); } /** - * Creates a new span that represents a sub-span of the current span. + * Returns a sub-range of this span. + * + * No bounds check is performed. For character spans, slicing does not guarantee that the + * returned view is null-terminated at its new Length. * - * @param start The starting index of the sub-span. - * @param length The length of the sub-span. - * @return A new span representing the sub-span. + * @param start Index of the first element in the returned span. + * @param length Number of elements in the returned span. + * @return Span covering [start, start + length). */ - Span Slice(size_t start, size_t length) + Span Slice(size_t start, size_t length) const { - // TODO: Add checks return Span(Pointer + start, length); } }; - /** - * Represents a read-only span of elements in memory. + * Lightweight non-owning read-only view over a contiguous sequence of elements. + * + * ReadOnlySpan is intentionally passed and copied by value. Copying it only copies its pointer and + * length; it does not copy, allocate, own, or track the lifetime of the referenced elements. + * + * Element access and slicing are intentionally unchecked. The caller is responsible for the + * lifetime of the referenced storage and for keeping indexes and slices inside the referenced range. * - * The ReadOnlySpan class provides a non-owning, read-only view over a contiguous sequence of elements. + * For ReadOnlySpan and ReadOnlySpan, construction from a null-terminated string scans + * up to the terminator and stores the logical character count in Length. The terminator is therefore + * not part of the span. Other constructors and Slice() do not imply null termination. * - * @tparam T The type of elements in the read-only span. + * @tparam T Element type referenced by the span. */ template struct ReadOnlySpan { /** - * Default constructor for an empty read-only span. + * Constructs an empty ReadOnlySpan. */ ReadOnlySpan() { @@ -103,22 +119,28 @@ struct ReadOnlySpan } /** - * Constructs a read-only span with the given pointer and length. + * Constructs a ReadOnlySpan over an existing contiguous range. * - * @param pointer The pointer to the first element of the read-only span. - * @param length The length of the read-only span. + * The source may be const. No ownership or lifetime tracking is added. + * + * @param pointer Pointer to the first element. + * @param length Number of elements in the range. */ - ReadOnlySpan(T* pointer, size_t length) + ReadOnlySpan(const T* pointer, size_t length) { Pointer = pointer; Length = length; } /** - * Constructs a read-only span from an std::initializer_list. - * This allows for the {{ ... }} initialization syntax. + * Constructs a ReadOnlySpan from an std::initializer_list. + * + * This is the deliberate STL convenience exception used to keep small call-site lists concise. + * The elements are not copied. The caller must not retain the resulting ReadOnlySpan beyond the + * lifetime of the initializer-list backing storage; this constructor is primarily intended for + * immediate function-call arguments. * - * @param initList An initializer list containing elements of type T. + * @param initList Initializer list whose elements are referenced by the span. */ ReadOnlySpan(std::initializer_list initList) { @@ -127,9 +149,12 @@ struct ReadOnlySpan } /** - * Constructs a read-only span from a null-terminated string. + * Constructs a ReadOnlySpan from a null-terminated character string. * - * @param stringValue A pointer to the null-terminated string. + * Length contains the number of characters before the null terminator. The terminator is not + * included in Length. + * + * @param stringValue Null-terminated character string. */ ReadOnlySpan(const char* stringValue) { @@ -143,49 +168,28 @@ struct ReadOnlySpan } /** - * Constructs a read-only span from a substring of a null-terminated string. + * Constructs a ReadOnlySpan from a null-terminated wide-character string. * - * @param stringValue A pointer to the null-terminated string. - * @param length The length of the substring. - */ - ReadOnlySpan(const char* stringValue, size_t length) - { - Pointer = stringValue; - Length = length; - } - - /** - * Constructs a read-only span from a null-terminated wide string. + * Length contains the number of characters before the null terminator. The terminator is not + * included in Length. * - * @param stringValue A pointer to the null-terminated wide string. + * @param stringValue Null-terminated wide-character string. */ ReadOnlySpan(const wchar_t* stringValue) { Pointer = stringValue; Length = 0; - while (stringValue[Length] != '\0') + while (stringValue[Length] != L'\0') { Length++; } } /** - * Constructs a read-only span from a substring of a null-terminated wide string. - * - * @param stringValue A pointer to the null-terminated wide string. - * @param length The length of the substring. - */ - ReadOnlySpan(const wchar_t* stringValue, size_t length) - { - Pointer = stringValue; - Length = length; - } - - /** - * Constructs a read-only span from a mutable span. + * Constructs a read-only view over a mutable Span. * - * @param spanValue A mutable span. + * @param spanValue Mutable span whose range will be referenced. */ ReadOnlySpan(Span spanValue) { @@ -193,51 +197,48 @@ struct ReadOnlySpan Length = spanValue.Length; } - /** - * Pointer to the first element of the read-only span. - */ - const T* Pointer; - - /** - * Length of the read-only span. - */ - size_t Length; + const T* Pointer; ///< Pointer to the first element, or nullptr for an empty span. + size_t Length; ///< Number of elements in the view. /** - * Accesses the element at the specified index in a read-only manner. + * Returns a read-only reference to an element. + * + * No bounds check is performed. * - * @param index The index of the element to access. - * @return A const reference to the element at the specified index. + * @param index Element index inside the span. + * @return Read-only reference to the selected element. */ - const T& operator[](int index) const + const T& operator[](size_t index) const { - // TODO: Check bounds return Pointer[index]; } /** - * Creates a new read-only span that represents a slice of the current span. + * Returns the suffix beginning at start. + * + * No bounds check is performed. For character spans, slicing does not guarantee that the + * returned view is null-terminated at its new Length. * - * @param start The starting index of the slice. - * @return A new read-only span representing the sliced portion. + * @param start Index of the first element in the returned span. + * @return ReadOnlySpan covering [start, Length). */ - ReadOnlySpan Slice(size_t start) + ReadOnlySpan Slice(size_t start) const { - // TODO: Add checks return ReadOnlySpan(Pointer + start, Length - start); } /** - * Creates a new read-only span that represents a sub-span of the current span. + * Returns a sub-range of this span. * - * @param start The starting index of the sub-span. - * @param length The length of the sub-span. - * @return A new read-only span representing the sub-span. + * No bounds check is performed. For character spans, slicing does not guarantee that the + * returned view is null-terminated at its new Length. + * + * @param start Index of the first element in the returned span. + * @param length Number of elements in the returned span. + * @return ReadOnlySpan covering [start, start + length). */ - ReadOnlySpan Slice(size_t start, size_t length) + ReadOnlySpan Slice(size_t start, size_t length) const { - // TODO: Add checks - return ReadOnlySpan((T*)Pointer + start, length); + return ReadOnlySpan(Pointer + start, length); } }; - From e33cb7e9036fbcb3f5f5465813b9c5bb9c91ed72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:17:53 +0200 Subject: [PATCH 04/60] Fix duplicated string span length --- src/Foundations/SystemMemory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index f53fa235..eddeddf0 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -570,7 +570,7 @@ Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source) template<> Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source) { - auto result = SystemPushArrayZero(memoryArena, source.Length + 1); + auto result = SystemPushArrayZero(memoryArena, source.Length); SystemCopyBuffer(result, source); return result; } @@ -606,4 +606,4 @@ Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan SystemCopyBuffer(result.Slice(buffer1.Length), buffer2); return result; -} +} \ No newline at end of file From dc95ea7c257c503dd92fc3178528aa1aee2da3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:18:10 +0200 Subject: [PATCH 05/60] Add span and string buffer tests --- tests/FoundationsTests/SpanTests.cpp | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/FoundationsTests/SpanTests.cpp diff --git a/tests/FoundationsTests/SpanTests.cpp b/tests/FoundationsTests/SpanTests.cpp new file mode 100644 index 00000000..958e31fa --- /dev/null +++ b/tests/FoundationsTests/SpanTests.cpp @@ -0,0 +1,66 @@ +#include "SystemMemory.h" +#include "SystemSpan.h" +#include "utest.h" + +int32_t SumSpanValues(ReadOnlySpan values) +{ + auto result = 0; + + for (size_t i = 0; i < values.Length; i++) + { + result += values[i]; + } + + return result; +} + +UTEST(Span, ReadOnlyConstBuffer) +{ + // Arrange + const int32_t values[] = { 10, 20, 30, 40 }; + const ReadOnlySpan span(values, 4); + + // Act + auto slice = span.Slice(1, 2); + + // Assert + ASSERT_EQ(2llu, slice.Length); + ASSERT_EQ(20, slice[0]); + ASSERT_EQ(30, slice[1]); +} + +UTEST(Span, InitializerList) +{ + // Act + auto result = SumSpanValues({ 10, 20, 30 }); + + // Assert + ASSERT_EQ(60, result); +} + +UTEST(Span, StringLengthExcludesNullTerminator) +{ + // Arrange + ReadOnlySpan value = "Elemental"; + + // Assert + ASSERT_EQ(9llu, value.Length); + ASSERT_EQ('\0', value.Pointer[value.Length]); +} + +UTEST(Span, DuplicateStringPreservesLogicalLengthAndNullTerminator) +{ + // Arrange + auto memoryArena = SystemAllocateMemoryArena(1024); + ReadOnlySpan source = "Elemental"; + + // Act + auto result = SystemDuplicateBuffer(memoryArena, source); + + // Assert + ASSERT_EQ(source.Length, result.Length); + ASSERT_EQ('\0', result.Pointer[result.Length]); + ASSERT_STREQ("Elemental", result.Pointer); + + SystemFreeMemoryArena(memoryArena); +} From 7ad19b2ee576de4bd80dda4201001ca86cf6ee73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:18:17 +0200 Subject: [PATCH 06/60] Include span tests in FoundationsTests --- tests/FoundationsTests/UnityBuild.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/FoundationsTests/UnityBuild.cpp b/tests/FoundationsTests/UnityBuild.cpp index f8589afd..0524b29d 100644 --- a/tests/FoundationsTests/UnityBuild.cpp +++ b/tests/FoundationsTests/UnityBuild.cpp @@ -1,6 +1,7 @@ #include "utest.h" #include "MemoryTests.cpp" +#include "SpanTests.cpp" #include "MathTests.cpp" #include "StringTests.cpp" #include "IOTests.cpp" From 884051db2da3f4671295172f856c87027a9738c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:30:50 +0200 Subject: [PATCH 07/60] Fix bounded concurrent memory arena pushes --- src/Foundations/SystemMemory.cpp | 76 +++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index eddeddf0..5b75ad84 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -44,7 +44,7 @@ struct PageSizeIndexes }; thread_local MemoryArenaStorage* stackMemoryArenaStorage = nullptr; -size_t systemPageSizeInBytes = 0; +const size_t systemPageSizeInBytes = SystemPlatformGetPageSize(); void PopStackMemory(MemoryArena memoryArena, size_t sizeInBytes); size_t ResizeToPageSizeMultiple(size_t sizeInBytes, size_t pageSizeInBytes); @@ -103,11 +103,6 @@ bool IsPageCommitted(MemoryArenaStorage* storage, uint32_t pageIndex) MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) { - if (systemPageSizeInBytes == 0) - { - systemPageSizeInBytes = SystemPlatformGetPageSize(); - } - auto dataSizeInBytes = ResizeToPageSizeMultiple(sizeInBytes, systemPageSizeInBytes); auto pageInfosCount = dataSizeInBytes / systemPageSizeInBytes; auto pageCommitInfosCount = (pageInfosCount + 31) / 32; @@ -171,7 +166,18 @@ bool IsStackMemoryArena(MemoryArena memoryArena) size_t GetMemoryArenaAllocatedBytes(MemoryArena memoryArena) { - return memoryArena.Storage->CurrentPointer - (uint8_t*)memoryArena.Storage - memoryArena.Storage->HeaderSizeInBytes; + uint8_t* currentPointer; + + if (IsStackMemoryArena(memoryArena)) + { + currentPointer = memoryArena.Storage->CurrentPointer; + } + else + { + SystemAtomicLoad(memoryArena.Storage->CurrentPointer, currentPointer); + } + + return currentPointer - (uint8_t*)memoryArena.Storage - memoryArena.Storage->HeaderSizeInBytes; } AllocationInfos SystemGetAllocationInfos() @@ -288,7 +294,7 @@ StackMemoryArena::~StackMemoryArena() template void SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory) { - SystemCommitMemory(memoryArena, (uint8_t*)buffer.Pointer, sizeof(T) * buffer.Length, true); + SystemCommitMemory(memoryArena, (uint8_t*)buffer.Pointer, sizeof(T) * buffer.Length, clearMemory); } void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory) @@ -445,24 +451,45 @@ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationSt auto workingMemoryArena = GetStackWorkingMemoryArena(memoryArena); auto storage = workingMemoryArena.Storage; - auto allocatedSize = GetMemoryArenaAllocatedBytes(memoryArena); - - if (allocatedSize + sizeInBytes > storage->SizeInBytes) - { - SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot push to memory arena with: %d (Allocated size is: %d, Max size is: %d)", (uint32_t)sizeInBytes, (uint32_t)allocatedSize, (uint32_t)storage->SizeInBytes); - return nullptr; - } - uint8_t* pointer; if (memoryArena.Storage == stackMemoryArenaStorage) { + auto allocatedSize = GetMemoryArenaAllocatedBytes(workingMemoryArena); + + if (allocatedSize > storage->SizeInBytes || sizeInBytes > storage->SizeInBytes - allocatedSize) + { + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot push to memory arena with: %d (Allocated size is: %d, Max size is: %d)", (uint32_t)sizeInBytes, (uint32_t)allocatedSize, (uint32_t)storage->SizeInBytes); + return nullptr; + } + pointer = storage->CurrentPointer; storage->CurrentPointer += sizeInBytes; } else { - pointer = SystemAtomicAdd(storage->CurrentPointer, sizeInBytes); + auto dataStart = (uint8_t*)storage + storage->HeaderSizeInBytes; + SystemAtomicLoad(storage->CurrentPointer, pointer); + + while (true) + { + auto allocatedSize = (size_t)(pointer - dataStart); + + if (allocatedSize > storage->SizeInBytes || sizeInBytes > storage->SizeInBytes - allocatedSize) + { + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot push to memory arena with: %d (Allocated size is: %d, Max size is: %d)", (uint32_t)sizeInBytes, (uint32_t)allocatedSize, (uint32_t)storage->SizeInBytes); + return nullptr; + } + + auto nextPointer = pointer + sizeInBytes; + + if (SystemAtomicCompareExchange(storage->CurrentPointer, pointer, nextPointer)) + { + break; + } + + SystemYieldThread(); + } } if (state == AllocationState_Committed) @@ -502,8 +529,13 @@ void PopStackMemory(MemoryArena memoryArena, size_t sizeInBytes) void* SystemPushMemoryZero(MemoryArena memoryArena, size_t sizeInBytes) { auto result = SystemPushMemory(memoryArena, sizeInBytes); - SystemPlatformClearMemory(result, sizeInBytes); + if (result == nullptr) + { + return nullptr; + } + + SystemPlatformClearMemory(result, sizeInBytes); return result; } @@ -511,28 +543,28 @@ template Span SystemPushArray(MemoryArena memoryArena, size_t count, AllocationState state) { auto memory = SystemPushMemory(memoryArena, sizeof(T) * count, state); - return Span((T*)memory, count); + return memory ? Span((T*)memory, count) : Span(); } template Span SystemPushArrayZero(MemoryArena memoryArena, size_t count) { auto memory = SystemPushMemoryZero(memoryArena, sizeof(T) * count); - return Span((T*)memory, count); + return memory ? Span((T*)memory, count) : Span(); } template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count) { auto memory = SystemPushMemoryZero(memoryArena, sizeof(char) * (count + 1)); - return Span((char*)memory, count); + return memory ? Span((char*)memory, count) : Span(); } template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count) { auto memory = SystemPushMemoryZero(memoryArena, sizeof(wchar_t) * (count + 1)); - return Span((wchar_t*)memory, count); + return memory ? Span((wchar_t*)memory, count) : Span(); } template From f9087525572b37a23259f0f59a02812f55d4a9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:31:42 +0200 Subject: [PATCH 08/60] Add memory arena overflow regression tests --- tests/FoundationsTests/MemoryTests.cpp | 118 +++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/FoundationsTests/MemoryTests.cpp b/tests/FoundationsTests/MemoryTests.cpp index aad57ad3..e8ee0733 100644 --- a/tests/FoundationsTests/MemoryTests.cpp +++ b/tests/FoundationsTests/MemoryTests.cpp @@ -9,6 +9,14 @@ struct MemoryThreadParameter int32_t ItemCount; }; +struct MemoryConcurrentOverflowThreadParameter +{ + MemoryArena MemoryArena; + bool* Start; + void** Results; + int32_t ThreadId; +}; + void MemoryConcurrentAddFunction(void* parameter) { auto threadParameter = (MemoryThreadParameter*)parameter; @@ -19,6 +27,24 @@ void MemoryConcurrentAddFunction(void* parameter) } } +void MemoryConcurrentOverflowFunction(void* parameter) +{ + auto threadParameter = (MemoryConcurrentOverflowThreadParameter*)parameter; + bool start = false; + + while (!start) + { + SystemAtomicLoad(*threadParameter->Start, start); + + if (!start) + { + SystemYieldThread(); + } + } + + threadParameter->Results[threadParameter->ThreadId] = SystemPushMemory(threadParameter->MemoryArena, 64, AllocationState_Reserved); +} + UTEST(Memory, Allocate) { // Arrange @@ -82,6 +108,28 @@ UTEST(Memory, AllocateCheckAlignement) ASSERT_TRUE(((size_t)data.Pointer & (alignment - 1)) == 0); } +UTEST(Memory, PushOverflowReturnsNull) +{ + // Arrange + auto memoryArena = SystemAllocateMemoryArena(64); + auto allocation = SystemPushMemory(memoryArena, 64, AllocationState_Reserved); + + // Act + auto overflowAllocation = SystemPushMemory(memoryArena, 8, AllocationState_Reserved); + auto zeroOverflowAllocation = SystemPushMemoryZero(memoryArena, 8); + auto overflowArray = SystemPushArray(memoryArena, 2, AllocationState_Reserved); + + // Assert + ASSERT_TRUE(allocation != nullptr); + ASSERT_TRUE(overflowAllocation == nullptr); + ASSERT_TRUE(zeroOverflowAllocation == nullptr); + ASSERT_TRUE(overflowArray.Pointer == nullptr); + ASSERT_EQ(0llu, overflowArray.Length); + + auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); + ASSERT_EQ(64llu, allocationInfos.AllocatedBytes); +} + UTEST(Memory, ConcatBuffers) { // Arrange @@ -199,6 +247,24 @@ UTEST(Memory, StackMemoryArenaRelease) ASSERT_STREQ("Test5Stack1", string5.Pointer); } +UTEST(Memory, StackAncestorAllocationUsesExtraStorageCapacity) +{ + // Arrange + auto stackMemoryArena1 = SystemGetStackMemoryArena(); + auto mainAllocation = SystemPushMemory(stackMemoryArena1, 120llu * 1024 * 1024, AllocationState_Reserved); + void* ancestorAllocation = nullptr; + + // Act + { + auto stackMemoryArena2 = SystemGetStackMemoryArena(); + ancestorAllocation = SystemPushMemory(stackMemoryArena1, 16llu * 1024 * 1024, AllocationState_Reserved); + } + + // Assert + ASSERT_TRUE(mainAllocation != nullptr); + ASSERT_TRUE(ancestorAllocation != nullptr); +} + UTEST(Memory, ConcurrentPush) { // Arrange @@ -228,6 +294,58 @@ UTEST(Memory, ConcurrentPush) ASSERT_EQ(maxSize, allocationInfos.AllocatedBytes); } +UTEST(Memory, ConcurrentPushDoesNotOverflow) +{ + // Arrange + const int32_t threadCount = 32; + const int32_t capacityCount = 8; + const size_t allocationSizeInBytes = 64; + auto memoryArena = SystemAllocateMemoryArena(capacityCount * allocationSizeInBytes); + bool start = false; + void* results[threadCount] = {}; + SystemThread threads[threadCount]; + MemoryConcurrentOverflowThreadParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { memoryArena, &start, results, i }; + threads[i] = SystemCreateThread(MemoryConcurrentOverflowFunction, &threadParameters[i]); + } + + // Act + SystemAtomicStore(start, true); + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto successCount = 0; + + for (int32_t i = 0; i < threadCount; i++) + { + if (results[i] != nullptr) + { + successCount++; + + for (int32_t j = i + 1; j < threadCount; j++) + { + if (results[j] != nullptr) + { + ASSERT_TRUE(results[i] != results[j]); + } + } + } + } + + ASSERT_EQ(capacityCount, successCount); + + auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); + ASSERT_EQ(capacityCount * allocationSizeInBytes, allocationInfos.AllocatedBytes); +} + UTEST(Memory, AllocateReserved) { // Arrange From 9f514444e19e114e740339c2e620ede63829e42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:33:41 +0200 Subject: [PATCH 09/60] Serialize memory arena page bookkeeping --- src/Foundations/SystemMemory.cpp | 111 ++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index 5b75ad84..d94130be 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -101,6 +101,16 @@ bool IsPageCommitted(MemoryArenaStorage* storage, uint32_t pageIndex) return (storage->PagesCommitInfos[arrayIndex].CommittedStates & (1U << bitIndex)) != 0; } +void LockMemoryArenaCommitOperations(MemoryArenaStorage* storage) +{ + SystemAtomicReplace(storage->IsCommitOperationInProgres, false, true); +} + +void UnlockMemoryArenaCommitOperations(MemoryArenaStorage* storage) +{ + SystemAtomicStore(storage->IsCommitOperationInProgres, false); +} + MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) { auto dataSizeInBytes = ResizeToPageSizeMultiple(sizeInBytes, systemPageSizeInBytes); @@ -231,9 +241,20 @@ void SystemClearMemoryArena(MemoryArena memoryArena) MemoryArenaAllocationInfos SystemGetMemoryArenaAllocationInfos(MemoryArena memoryArena) { + size_t committedPagesCount; + + if (IsStackMemoryArena(memoryArena)) + { + committedPagesCount = memoryArena.Storage->CommittedPagesCount; + } + else + { + SystemAtomicLoad(memoryArena.Storage->CommittedPagesCount, committedPagesCount); + } + MemoryArenaAllocationInfos result = {}; result.AllocatedBytes = GetMemoryArenaAllocatedBytes(memoryArena); - result.CommittedBytes = memoryArena.Storage->CommittedPagesCount * systemPageSizeInBytes; + result.CommittedBytes = committedPagesCount * systemPageSizeInBytes; result.MaximumSizeInBytes = memoryArena.Storage->SizeInBytes; return result; @@ -307,6 +328,13 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt return; } + auto needsSynchronization = !IsStackMemoryArena(memoryArena); + + if (needsSynchronization) + { + LockMemoryArenaCommitOperations(storage); + } + auto pageSizeIndexes = ComputePageSizeInfoIndexes(storage, pointer, sizeInBytes); auto needToCommit = false; @@ -315,16 +343,8 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt auto pageSizeOffsets = ComputePageSizeLocalOffsets(storage, i, pointer, sizeInBytes); auto pageInfos = &storage->PagesInfos[i]; - if (memoryArena.Storage == stackMemoryArenaStorage) - { - pageInfos->MinCommittedOffset = pageSizeOffsets.StartIndex < pageInfos->MinCommittedOffset ? pageSizeOffsets.StartIndex : pageInfos->MinCommittedOffset; - pageInfos->MaxCommittedOffset = pageSizeOffsets.EndIndex > pageInfos->MaxCommittedOffset ? pageSizeOffsets.EndIndex : pageInfos->MaxCommittedOffset; - } - else - { - SystemAtomicReplace(pageInfos->MinCommittedOffset, pageInfos->MinCommittedOffset, pageSizeOffsets.StartIndex < pageInfos->MinCommittedOffset ? pageSizeOffsets.StartIndex : pageInfos->MinCommittedOffset); - SystemAtomicReplace(pageInfos->MaxCommittedOffset, pageInfos->MaxCommittedOffset, pageSizeOffsets.EndIndex > pageInfos->MaxCommittedOffset ? pageSizeOffsets.EndIndex : pageInfos->MaxCommittedOffset); - } + pageInfos->MinCommittedOffset = pageSizeOffsets.StartIndex < pageInfos->MinCommittedOffset ? pageSizeOffsets.StartIndex : pageInfos->MinCommittedOffset; + pageInfos->MaxCommittedOffset = pageSizeOffsets.EndIndex > pageInfos->MaxCommittedOffset ? pageSizeOffsets.EndIndex : pageInfos->MaxCommittedOffset; if (!IsPageCommitted(storage, (uint32_t)i)) { @@ -334,6 +354,11 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt if (!needToCommit) { + if (needsSynchronization) + { + UnlockMemoryArenaCommitOperations(storage); + } + if (memoryArena.Storage == stackMemoryArenaStorage) { SystemPlatformClearMemory(pointer, sizeInBytes); @@ -342,13 +367,6 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt return; } - if (memoryArena.Storage != stackMemoryArenaStorage) - { - SystemAtomicReplace(storage->IsCommitOperationInProgres, false, true); - } - - pageSizeIndexes = ComputePageSizeInfoIndexes(storage, pointer, sizeInBytes); - for (size_t i = pageSizeIndexes.StartIndex; i < pageSizeIndexes.EndIndex; i++) { if (!IsPageCommitted(storage, (uint32_t)i)) @@ -362,13 +380,21 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt } SetPageCommitted(storage, (uint32_t)i); - storage->CommittedPagesCount++; + + if (needsSynchronization) + { + SystemAtomicAdd(storage->CommittedPagesCount, 1); + } + else + { + storage->CommittedPagesCount++; + } } } - if (memoryArena.Storage != stackMemoryArenaStorage) + if (needsSynchronization) { - SystemAtomicStore(storage->IsCommitOperationInProgres, false); + UnlockMemoryArenaCommitOperations(storage); } if (memoryArena.Storage == stackMemoryArenaStorage) @@ -387,6 +413,13 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB return; } + auto needsSynchronization = !IsStackMemoryArena(memoryArena); + + if (needsSynchronization) + { + LockMemoryArenaCommitOperations(storage); + } + auto pageSizeIndexes = ComputePageSizeInfoIndexes(storage, pointer, sizeInBytes); auto needToDecommit = false; @@ -395,16 +428,8 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB auto pageSizeOffsets = ComputePageSizeLocalOffsets(storage, i, pointer, sizeInBytes); auto pageInfos = &storage->PagesInfos[i]; - if (memoryArena.Storage == stackMemoryArenaStorage) - { - pageInfos->MinCommittedOffset = pageSizeOffsets.StartIndex == pageInfos->MinCommittedOffset ? pageSizeOffsets.EndIndex : pageInfos->MinCommittedOffset; - pageInfos->MaxCommittedOffset = pageSizeOffsets.EndIndex == pageInfos->MaxCommittedOffset ? pageSizeOffsets.StartIndex : pageInfos->MaxCommittedOffset; - } - else - { - SystemAtomicReplace(pageInfos->MinCommittedOffset, pageInfos->MinCommittedOffset, pageSizeOffsets.StartIndex == pageInfos->MinCommittedOffset ? pageSizeOffsets.EndIndex : pageInfos->MinCommittedOffset); - SystemAtomicReplace(pageInfos->MaxCommittedOffset, pageInfos->MaxCommittedOffset, pageSizeOffsets.EndIndex == pageInfos->MaxCommittedOffset ? pageSizeOffsets.StartIndex : pageInfos->MaxCommittedOffset); - } + pageInfos->MinCommittedOffset = pageSizeOffsets.StartIndex == pageInfos->MinCommittedOffset ? pageSizeOffsets.EndIndex : pageInfos->MinCommittedOffset; + pageInfos->MaxCommittedOffset = pageSizeOffsets.EndIndex == pageInfos->MaxCommittedOffset ? pageSizeOffsets.StartIndex : pageInfos->MaxCommittedOffset; if (IsPageCommitted(storage, (uint32_t)i) && (int32_t)(pageInfos->MaxCommittedOffset - pageInfos->MinCommittedOffset) <= 0) { @@ -414,14 +439,14 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB if (!needToDecommit) { + if (needsSynchronization) + { + UnlockMemoryArenaCommitOperations(storage); + } + return; } - if (memoryArena.Storage != stackMemoryArenaStorage) - { - SystemAtomicReplace(storage->IsCommitOperationInProgres, false, true); - } - for (size_t i = pageSizeIndexes.StartIndex; i < pageSizeIndexes.EndIndex; i++) { auto pageInfos = &storage->PagesInfos[i]; @@ -434,14 +459,22 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB { SystemPlatformDecommitMemory(pagePointer, systemPageSizeInBytes); ClearPageCommitted(storage, (uint32_t)i); - storage->CommittedPagesCount--; + + if (needsSynchronization) + { + SystemAtomicSubstract(storage->CommittedPagesCount, 1); + } + else + { + storage->CommittedPagesCount--; + } } } } - if (memoryArena.Storage != stackMemoryArenaStorage) + if (needsSynchronization) { - SystemAtomicStore(storage->IsCommitOperationInProgres, false); + UnlockMemoryArenaCommitOperations(storage); } } From 356993538982e150cbfc2863a4c47fab2a50a001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:34:51 +0200 Subject: [PATCH 10/60] Add concurrent commit regression coverage --- tests/FoundationsTests/MemoryTests.cpp | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/FoundationsTests/MemoryTests.cpp b/tests/FoundationsTests/MemoryTests.cpp index e8ee0733..9995d96c 100644 --- a/tests/FoundationsTests/MemoryTests.cpp +++ b/tests/FoundationsTests/MemoryTests.cpp @@ -1,5 +1,6 @@ #include "SystemFunctions.h" #include "SystemMemory.h" +#include "SystemPlatformFunctions.h" #include "utest.h" struct MemoryThreadParameter @@ -17,6 +18,14 @@ struct MemoryConcurrentOverflowThreadParameter int32_t ThreadId; }; +struct MemoryConcurrentCommitThreadParameter +{ + MemoryArena MemoryArena; + uint8_t* Pointer; + size_t SizeInBytes; + uint8_t Value; +}; + void MemoryConcurrentAddFunction(void* parameter) { auto threadParameter = (MemoryThreadParameter*)parameter; @@ -45,6 +54,17 @@ void MemoryConcurrentOverflowFunction(void* parameter) threadParameter->Results[threadParameter->ThreadId] = SystemPushMemory(threadParameter->MemoryArena, 64, AllocationState_Reserved); } +void MemoryConcurrentCommitFunction(void* parameter) +{ + auto threadParameter = (MemoryConcurrentCommitThreadParameter*)parameter; + SystemCommitMemory(threadParameter->MemoryArena, threadParameter->Pointer, threadParameter->SizeInBytes); + + for (size_t i = 0; i < threadParameter->SizeInBytes; i++) + { + threadParameter->Pointer[i] = threadParameter->Value; + } +} + UTEST(Memory, Allocate) { // Arrange @@ -346,6 +366,44 @@ UTEST(Memory, ConcurrentPushDoesNotOverflow) ASSERT_EQ(capacityCount * allocationSizeInBytes, allocationInfos.AllocatedBytes); } +UTEST(Memory, ConcurrentCommitSharedPage) +{ + // Arrange + const int32_t threadCount = 32; + const size_t rangeSizeInBytes = 64; + auto pageSizeInBytes = SystemPlatformGetPageSize(); + auto memoryArena = SystemAllocateMemoryArena(pageSizeInBytes); + auto buffer = SystemPushArray(memoryArena, pageSizeInBytes, AllocationState_Reserved); + auto committedBytesBefore = SystemGetMemoryArenaAllocationInfos(memoryArena).CommittedBytes; + SystemThread threads[threadCount]; + MemoryConcurrentCommitThreadParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { memoryArena, buffer.Pointer + i * rangeSizeInBytes, rangeSizeInBytes, (uint8_t)(i + 1) }; + threads[i] = SystemCreateThread(MemoryConcurrentCommitFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); + ASSERT_EQ(committedBytesBefore + pageSizeInBytes, allocationInfos.CommittedBytes); + + for (int32_t i = 0; i < threadCount; i++) + { + for (size_t j = 0; j < rangeSizeInBytes; j++) + { + ASSERT_EQ((uint8_t)(i + 1), buffer[i * rangeSizeInBytes + j]); + } + } +} + UTEST(Memory, AllocateReserved) { // Arrange From 029fba821454a4941b34c47e6bd11ebe9ed303c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:37:07 +0200 Subject: [PATCH 11/60] Finalize memory arena synchronization invariants --- src/Foundations/SystemMemory.cpp | 83 ++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index d94130be..7bc1b586 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -44,33 +44,40 @@ struct PageSizeIndexes }; thread_local MemoryArenaStorage* stackMemoryArenaStorage = nullptr; -const size_t systemPageSizeInBytes = SystemPlatformGetPageSize(); void PopStackMemory(MemoryArena memoryArena, size_t sizeInBytes); size_t ResizeToPageSizeMultiple(size_t sizeInBytes, size_t pageSizeInBytes); +size_t GetSystemPageSizeInBytes() +{ + static const size_t pageSizeInBytes = SystemPlatformGetPageSize(); + return pageSizeInBytes; +} + PageSizeIndexes ComputePageSizeInfoIndexes(MemoryArenaStorage* storage, void* pointer, size_t sizeInBytes) { + auto pageSizeInBytes = GetSystemPageSizeInBytes(); auto dataStart = (uint8_t*)storage + storage->HeaderSizeInBytes; auto offset = (uint8_t*)pointer - dataStart; PageSizeIndexes result = {}; - result.StartIndex = offset / systemPageSizeInBytes; - result.EndIndex = ResizeToPageSizeMultiple(offset + sizeInBytes, systemPageSizeInBytes) / systemPageSizeInBytes; + result.StartIndex = offset / pageSizeInBytes; + result.EndIndex = ResizeToPageSizeMultiple(offset + sizeInBytes, pageSizeInBytes) / pageSizeInBytes; return result; } PageSizeIndexes ComputePageSizeLocalOffsets(MemoryArenaStorage* storage, size_t index, void* pointer, size_t sizeInBytes) { + auto pageSizeInBytes = GetSystemPageSizeInBytes(); auto absoluteStart = (uint8_t*)pointer; auto absoluteEnd = (uint8_t*)pointer + sizeInBytes; - auto pageStart = (uint8_t*)storage + storage->HeaderSizeInBytes + index * systemPageSizeInBytes; - auto pageEnd = pageStart + systemPageSizeInBytes; + auto pageStart = (uint8_t*)storage + storage->HeaderSizeInBytes + index * pageSizeInBytes; + auto pageEnd = pageStart + pageSizeInBytes; PageSizeIndexes result = {}; result.StartIndex = absoluteStart > pageStart ? absoluteStart - pageStart : 0; - result.EndIndex = absoluteEnd < pageEnd ? absoluteEnd - pageStart : systemPageSizeInBytes - 1; + result.EndIndex = absoluteEnd < pageEnd ? absoluteEnd - pageStart : pageSizeInBytes - 1; return result; } @@ -113,11 +120,12 @@ void UnlockMemoryArenaCommitOperations(MemoryArenaStorage* storage) MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) { - auto dataSizeInBytes = ResizeToPageSizeMultiple(sizeInBytes, systemPageSizeInBytes); - auto pageInfosCount = dataSizeInBytes / systemPageSizeInBytes; + auto pageSizeInBytes = GetSystemPageSizeInBytes(); + auto dataSizeInBytes = ResizeToPageSizeMultiple(sizeInBytes, pageSizeInBytes); + auto pageInfosCount = dataSizeInBytes / pageSizeInBytes; auto pageCommitInfosCount = (pageInfosCount + 31) / 32; auto headerMetadataSizeInBytes = sizeof(MemoryArenaStorage) + pageInfosCount * sizeof(MemoryArenaPageInfo) + pageCommitInfosCount * sizeof(MemoryArenaPageCommitInfo); - auto headerSizeInBytes = ResizeToPageSizeMultiple(headerMetadataSizeInBytes, systemPageSizeInBytes); + auto headerSizeInBytes = ResizeToPageSizeMultiple(headerMetadataSizeInBytes, pageSizeInBytes); auto reservedSizeInBytes = headerSizeInBytes + dataSizeInBytes; auto storage = (MemoryArenaStorage*)SystemPlatformReserveMemory(reservedSizeInBytes); @@ -127,7 +135,7 @@ MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) storage->SizeInBytes = sizeInBytes; storage->HeaderSizeInBytes = headerSizeInBytes; storage->IsCommitOperationInProgres = false; - storage->CommittedPagesCount = headerSizeInBytes / systemPageSizeInBytes; + storage->CommittedPagesCount = headerSizeInBytes / pageSizeInBytes; storage->PagesInfos = (MemoryArenaPageInfo*)((uint8_t*)storage + sizeof(MemoryArenaStorage)); storage->PagesCommitInfos = (MemoryArenaPageCommitInfo*)((uint8_t*)storage + sizeof(MemoryArenaStorage) + pageInfosCount * sizeof(MemoryArenaPageInfo)); storage->StackExtraStorage = {}; @@ -137,7 +145,7 @@ MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) for (size_t i = 0; i < pageInfosCount; i++) { ClearPageCommitted(storage, (uint32_t)i); - storage->PagesInfos[i].MinCommittedOffset = systemPageSizeInBytes - 1; + storage->PagesInfos[i].MinCommittedOffset = pageSizeInBytes - 1; storage->PagesInfos[i].MaxCommittedOffset = 0; } @@ -216,7 +224,8 @@ MemoryArena SystemAllocateMemoryArena(size_t sizeInBytes) void SystemFreeMemoryArena(MemoryArena memoryArena) { - auto dataSizeInBytes = ResizeToPageSizeMultiple(memoryArena.Storage->SizeInBytes, systemPageSizeInBytes); + auto pageSizeInBytes = GetSystemPageSizeInBytes(); + auto dataSizeInBytes = ResizeToPageSizeMultiple(memoryArena.Storage->SizeInBytes, pageSizeInBytes); SystemPlatformFreeMemory(memoryArena.Storage, memoryArena.Storage->HeaderSizeInBytes + dataSizeInBytes); } @@ -254,7 +263,7 @@ MemoryArenaAllocationInfos SystemGetMemoryArenaAllocationInfos(MemoryArena memor MemoryArenaAllocationInfos result = {}; result.AllocatedBytes = GetMemoryArenaAllocatedBytes(memoryArena); - result.CommittedBytes = committedPagesCount * systemPageSizeInBytes; + result.CommittedBytes = committedPagesCount * GetSystemPageSizeInBytes(); result.MaximumSizeInBytes = memoryArena.Storage->SizeInBytes; return result; @@ -367,16 +376,18 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt return; } + auto pageSizeInBytes = GetSystemPageSizeInBytes(); + for (size_t i = pageSizeIndexes.StartIndex; i < pageSizeIndexes.EndIndex; i++) { if (!IsPageCommitted(storage, (uint32_t)i)) { - auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * systemPageSizeInBytes; - SystemPlatformCommitMemory(pagePointer, systemPageSizeInBytes); + auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * pageSizeInBytes; + SystemPlatformCommitMemory(pagePointer, pageSizeInBytes); if (clearMemory) { - SystemPlatformClearMemory(pagePointer, systemPageSizeInBytes); + SystemPlatformClearMemory(pagePointer, pageSizeInBytes); } SetPageCommitted(storage, (uint32_t)i); @@ -447,17 +458,19 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB return; } + auto pageSizeInBytes = GetSystemPageSizeInBytes(); + for (size_t i = pageSizeIndexes.StartIndex; i < pageSizeIndexes.EndIndex; i++) { auto pageInfos = &storage->PagesInfos[i]; if (IsPageCommitted(storage, (uint32_t)i)) { - auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * systemPageSizeInBytes; + auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * pageSizeInBytes; if ((int32_t)(pageInfos->MaxCommittedOffset - pageInfos->MinCommittedOffset) <= 0) { - SystemPlatformDecommitMemory(pagePointer, systemPageSizeInBytes); + SystemPlatformDecommitMemory(pagePointer, pageSizeInBytes); ClearPageCommitted(storage, (uint32_t)i); if (needsSynchronization) @@ -621,6 +634,11 @@ void SystemCopyBuffer(Span destination, ReadOnlySpan source) return; } + if (source.Length == 0) + { + return; + } + SystemPlatformCopyMemory(destination.Pointer, source.Pointer, source.Length * sizeof(T)); } @@ -628,6 +646,12 @@ template Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source) { auto result = SystemPushArray(memoryArena, source.Length); + + if (result.Pointer == nullptr) + { + return {}; + } + SystemCopyBuffer(result, source); return result; } @@ -636,6 +660,12 @@ template<> Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source) { auto result = SystemPushArrayZero(memoryArena, source.Length); + + if (result.Pointer == nullptr) + { + return {}; + } + SystemCopyBuffer(result, source); return result; } @@ -645,6 +675,11 @@ Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, Re { auto result = SystemPushArray(memoryArena, buffer1.Length + buffer2.Length); + if (result.Pointer == nullptr) + { + return {}; + } + SystemCopyBuffer(result, buffer1); SystemCopyBuffer(result.Slice(buffer1.Length), buffer2); @@ -656,6 +691,11 @@ Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffe { auto result = SystemPushArrayZero(memoryArena, buffer1.Length + buffer2.Length); + if (result.Pointer == nullptr) + { + return {}; + } + SystemCopyBuffer(result, buffer1); SystemCopyBuffer(result.Slice(buffer1.Length), buffer2); @@ -667,8 +707,13 @@ Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan { auto result = SystemPushArrayZero(memoryArena, buffer1.Length + buffer2.Length); + if (result.Pointer == nullptr) + { + return {}; + } + SystemCopyBuffer(result, buffer1); SystemCopyBuffer(result.Slice(buffer1.Length), buffer2); return result; -} \ No newline at end of file +} From e5b225003942643cd6abfff606d8fb773b9143cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:42:59 +0200 Subject: [PATCH 12/60] Write shader string terminators explicitly --- src/ElementalTools/Shaders/ShaderCompilerUtils.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ElementalTools/Shaders/ShaderCompilerUtils.cpp b/src/ElementalTools/Shaders/ShaderCompilerUtils.cpp index dc669b57..1d0fb188 100644 --- a/src/ElementalTools/Shaders/ShaderCompilerUtils.cpp +++ b/src/ElementalTools/Shaders/ShaderCompilerUtils.cpp @@ -19,7 +19,9 @@ void WriteShaderData(Span data, uint32_t* currentOffset, ReadOnlySpan((uint8_t*)value.Pointer, value.Length); SystemCopyBuffer(data.Slice(*currentOffset), dataSpan); - *currentOffset += value.Length + 1; + *currentOffset += value.Length; + data[*currentOffset] = 0; + *currentOffset += 1; } template @@ -160,4 +162,4 @@ ReadOnlySpan ReadShaderParts(MemoryArena memoryArena, ReadOnlySpan Date: Sat, 5 Sep 2026 11:44:23 +0200 Subject: [PATCH 13/60] Report platform memory operation failures --- src/Foundations/SystemPlatformFunctions.h | 70 +++++++++-------------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/src/Foundations/SystemPlatformFunctions.h b/src/Foundations/SystemPlatformFunctions.h index 6cf1977b..8500cf95 100644 --- a/src/Foundations/SystemPlatformFunctions.h +++ b/src/Foundations/SystemPlatformFunctions.h @@ -54,74 +54,60 @@ uint64_t SystemPlatformGetHighPerformanceCounter(); uint64_t SystemPlatformGetHighPerformanceCounterFrequencyInSeconds(); /** - * Retrieves the size of the system's memory page. - * - * This function is used to obtain the size of a single page of memory as used by the system's memory management. - * The page size is a fundamental property in memory management, as it determines the granularity of memory allocation - * and management operations. + * Retrieves the virtual-memory granularity used by Foundations memory operations. * - * @return The size of a memory page in bytes. + * @return The platform memory granularity in bytes. */ size_t SystemPlatformGetPageSize(); /** - * Retrieves allocation information of the system platform. + * Retrieves process-wide virtual-memory counters maintained by the platform layer. * - * This function provides detailed information about the memory allocation status of the system platform. It returns - * a `SystemPlatformAllocationInfos` structure containing two size_t values: `CommittedBytes` and `ReservedBytes`. - * `CommittedBytes` indicates the total amount of memory that is currently allocated and in use, whereas `ReservedBytes` - * represents the total amount of memory that has been reserved but not necessarily allocated. This information is - * crucial for understanding the memory usage and availability on the system platform, helping in optimizing memory - * management for applications. + * The returned values only include successful reserve/commit operations tracked by Foundations. * - * @return A `SystemPlatformAllocationInfos` structure containing the committed and reserved memory information. + * @return Current committed and reserved byte counts. */ SystemPlatformAllocationInfos SystemPlatformGetAllocationInfos(); /** - * Reserves a block of memory. - * - * This function reserves a region of memory of the specified size. The reserved memory is not committed (i.e., physical - * storage has not been allocated). This is typically used in systems to reserve a large block of address space and commit - * portions of it as needed. + * Reserves a virtual-address range without committing its data pages. * - * @param sizeInBytes The size of the memory to reserve in bytes. - * @return A pointer to the beginning of the reserved memory block. + * @param sizeInBytes Number of bytes to reserve. + * @return Pointer to the reserved range, or nullptr if the platform reservation fails. */ void* SystemPlatformReserveMemory(size_t sizeInBytes); /** - * Frees a previously reserved block of memory. - * - * This function releases a previously reserved block of memory, making it available for other uses. The specified memory - * block should have been reserved using SystemPlatformReserveMemory. + * Releases a previously reserved virtual-address range. * - * @param pointer A pointer to the start of the memory block to be freed. - * @param sizeInBytes The size of the memory block in bytes. + * Allocation counters are updated only when the platform release succeeds. + * + * @param pointer Start of the reserved range. + * @param sizeInBytes Size of the reserved range in bytes. */ void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes); /** - * Commits a block of reserved memory. - * - * After reserving memory using SystemPlatformReserveMemory, this function is used to commit a portion (or all) of that - * memory. Committing memory allocates physical storage (RAM or disk) for that memory region. + * Commits a range inside a previously reserved virtual-address region. + * + * Allocation counters are updated only when the platform operation succeeds. * - * @param pointer A pointer to the start of the memory block to be committed. - * @param sizeInBytes The size of the memory block to commit in bytes. + * @param pointer Start of the range to commit. + * @param sizeInBytes Number of bytes to commit. + * @return true when the platform commit succeeds; otherwise false. */ -void SystemPlatformCommitMemory(void* pointer, size_t sizeInBytes); +bool SystemPlatformCommitMemory(void* pointer, size_t sizeInBytes); /** - * Decomits a previously committed block of memory. - * - * This function is used to decommit a portion of memory that was previously committed using SystemPlatformCommitMemory. - * This effectively frees up the physical storage associated with the memory, while keeping the memory region reserved. + * Decommits a range while keeping its virtual-address region reserved. + * + * Allocation counters are updated only when the platform operation succeeds. * - * @param pointer A pointer to the start of the memory block to decommit. - * @param sizeInBytes The size of the memory block to decommit in bytes. + * @param pointer Start of the range to decommit. + * @param sizeInBytes Number of bytes to decommit. + * @return true when the platform decommit succeeds; otherwise false. */ -void SystemPlatformDecommitMemory(void* pointer, size_t sizeInBytes); +bool SystemPlatformDecommitMemory(void* pointer, size_t sizeInBytes); /** * Clears the contents of a memory block. @@ -168,7 +154,7 @@ bool SystemPlatformFileExists(ReadOnlySpan path); * Writes bytes to a file. * * @param path A ReadOnlySpan representing the path of the file where bytes are to be written. - * @param data A ReadOnlySpan containing the data to be written to the file. + * @param data A ReadOnlySpan containing the data to be written. */ void SystemPlatformFileWriteBytes(ReadOnlySpan path, ReadOnlySpan data); From 4e5e5d833a0c5530a8c314f705f57850e475eb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:45:07 +0200 Subject: [PATCH 14/60] Make Windows VM accounting thread-safe --- .../Microsoft/SystemPlatformFunctions.cpp | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/Foundations/Microsoft/SystemPlatformFunctions.cpp b/src/Foundations/Microsoft/SystemPlatformFunctions.cpp index 00d85574..4eb1c144 100644 --- a/src/Foundations/Microsoft/SystemPlatformFunctions.cpp +++ b/src/Foundations/Microsoft/SystemPlatformFunctions.cpp @@ -63,31 +63,54 @@ size_t SystemPlatformGetPageSize() SystemPlatformAllocationInfos SystemPlatformGetAllocationInfos() { - return systemPlatformAllocationInfos; + SystemPlatformAllocationInfos result = {}; + SystemAtomicLoad(systemPlatformAllocationInfos.CommittedBytes, result.CommittedBytes); + SystemAtomicLoad(systemPlatformAllocationInfos.ReservedBytes, result.ReservedBytes); + return result; } void* SystemPlatformReserveMemory(size_t sizeInBytes) { - systemPlatformAllocationInfos.ReservedBytes += sizeInBytes; - return VirtualAlloc2(nullptr, nullptr, sizeInBytes, MEM_RESERVE, PAGE_NOACCESS, nullptr, 0); + auto result = VirtualAlloc2(nullptr, nullptr, sizeInBytes, MEM_RESERVE, PAGE_NOACCESS, nullptr, 0); + + if (result != nullptr) + { + SystemAtomicAdd(systemPlatformAllocationInfos.ReservedBytes, sizeInBytes); + } + + return result; } void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes) { - systemPlatformAllocationInfos.ReservedBytes -= sizeInBytes; - VirtualFree(pointer, 0, MEM_RELEASE); + if (VirtualFree(pointer, 0, MEM_RELEASE)) + { + SystemAtomicSubstract(systemPlatformAllocationInfos.ReservedBytes, sizeInBytes); + } } -void SystemPlatformCommitMemory(void* pointer, size_t sizeInBytes) +bool SystemPlatformCommitMemory(void* pointer, size_t sizeInBytes) { - systemPlatformAllocationInfos.CommittedBytes += sizeInBytes; - VirtualAlloc2(nullptr, pointer, sizeInBytes, MEM_COMMIT, PAGE_READWRITE, nullptr, 0); + auto result = VirtualAlloc2(nullptr, pointer, sizeInBytes, MEM_COMMIT, PAGE_READWRITE, nullptr, 0); + + if (result == nullptr) + { + return false; + } + + SystemAtomicAdd(systemPlatformAllocationInfos.CommittedBytes, sizeInBytes); + return true; } -void SystemPlatformDecommitMemory(void* pointer, size_t sizeInBytes) +bool SystemPlatformDecommitMemory(void* pointer, size_t sizeInBytes) { - systemPlatformAllocationInfos.CommittedBytes -= sizeInBytes; - VirtualFree(pointer, sizeInBytes, MEM_DECOMMIT); + if (!VirtualFree(pointer, sizeInBytes, MEM_DECOMMIT)) + { + return false; + } + + SystemAtomicSubstract(systemPlatformAllocationInfos.CommittedBytes, sizeInBytes); + return true; } void SystemPlatformClearMemory(void* pointer, size_t sizeInBytes) From 12d5da18bb90d4dcfadbfd418d0b2c0148561b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:45:38 +0200 Subject: [PATCH 15/60] Make POSIX VM accounting thread-safe --- src/Foundations/PosixPlatformFunctions.cpp | 47 ++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/Foundations/PosixPlatformFunctions.cpp b/src/Foundations/PosixPlatformFunctions.cpp index 9b0a1b6a..23886d10 100644 --- a/src/Foundations/PosixPlatformFunctions.cpp +++ b/src/Foundations/PosixPlatformFunctions.cpp @@ -1,4 +1,5 @@ #include "SystemPlatformFunctions.h" +#include "SystemFunctions.h" #ifdef ElemAPI #include "SystemLogging.h" @@ -76,31 +77,53 @@ size_t SystemPlatformGetPageSize() SystemPlatformAllocationInfos SystemPlatformGetAllocationInfos() { - return systemPlatformAllocationInfos; + SystemPlatformAllocationInfos result = {}; + SystemAtomicLoad(systemPlatformAllocationInfos.CommittedBytes, result.CommittedBytes); + SystemAtomicLoad(systemPlatformAllocationInfos.ReservedBytes, result.ReservedBytes); + return result; } void* SystemPlatformReserveMemory(size_t sizeInBytes) { - systemPlatformAllocationInfos.ReservedBytes += sizeInBytes; - return mmap(nullptr, sizeInBytes, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); + auto result = mmap(nullptr, sizeInBytes, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); + + if (result == MAP_FAILED) + { + return nullptr; + } + + SystemAtomicAdd(systemPlatformAllocationInfos.ReservedBytes, sizeInBytes); + return result; } void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes) { - systemPlatformAllocationInfos.ReservedBytes -= sizeInBytes; - munmap(pointer, sizeInBytes); + if (munmap(pointer, sizeInBytes) == 0) + { + SystemAtomicSubstract(systemPlatformAllocationInfos.ReservedBytes, sizeInBytes); + } } -void SystemPlatformCommitMemory(void* pointer, size_t sizeInBytes) +bool SystemPlatformCommitMemory(void* pointer, size_t sizeInBytes) { - systemPlatformAllocationInfos.CommittedBytes += sizeInBytes; - mprotect(pointer, sizeInBytes, PROT_READ | PROT_WRITE); + if (mprotect(pointer, sizeInBytes, PROT_READ | PROT_WRITE) != 0) + { + return false; + } + + SystemAtomicAdd(systemPlatformAllocationInfos.CommittedBytes, sizeInBytes); + return true; } -void SystemPlatformDecommitMemory(void* pointer, size_t sizeInBytes) +bool SystemPlatformDecommitMemory(void* pointer, size_t sizeInBytes) { - systemPlatformAllocationInfos.CommittedBytes -= sizeInBytes; - mprotect(pointer, sizeInBytes, PROT_NONE); + if (mprotect(pointer, sizeInBytes, PROT_NONE) != 0) + { + return false; + } + + SystemAtomicSubstract(systemPlatformAllocationInfos.CommittedBytes, sizeInBytes); + return true; } void SystemPlatformClearMemory(void* pointer, size_t sizeInBytes) @@ -144,7 +167,7 @@ void SystemPlatformFileWriteBytes(ReadOnlySpan path, ReadOnlySpan if (write(fileHandle, data.Pointer, data.Length) < 0) { - SystemLogErrorMessage(ElemLogMessageCategory_Application, "Error writing to file %s.", path.Pointer); + SystemLogErrorMessage(ElemLogMessageCategory_Application, "Error writing file %s.", path.Pointer); } close(fileHandle); From 16f65803e5b830428bd8861d64f27daa4a2c1a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:47:41 +0200 Subject: [PATCH 16/60] Harden memory arena size and VM failure handling --- src/Foundations/SystemMemory.cpp | 288 ++++++++++++++++++++++++++----- 1 file changed, 247 insertions(+), 41 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index 7bc1b586..d3350ba5 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -46,7 +46,6 @@ struct PageSizeIndexes thread_local MemoryArenaStorage* stackMemoryArenaStorage = nullptr; void PopStackMemory(MemoryArena memoryArena, size_t sizeInBytes); -size_t ResizeToPageSizeMultiple(size_t sizeInBytes, size_t pageSizeInBytes); size_t GetSystemPageSizeInBytes() { @@ -54,15 +53,43 @@ size_t GetSystemPageSizeInBytes() return pageSizeInBytes; } +bool TryAlignSize(size_t sizeInBytes, size_t alignment, size_t* result) +{ + auto alignmentMask = alignment - 1; + + if (sizeInBytes > SIZE_MAX - alignmentMask) + { + return false; + } + + *result = (sizeInBytes + alignmentMask) & ~alignmentMask; + return true; +} + +bool TryMultiplySize(size_t value1, size_t value2, size_t* result) +{ + if (value1 != 0 && value2 > SIZE_MAX / value1) + { + return false; + } + + *result = value1 * value2; + return true; +} + PageSizeIndexes ComputePageSizeInfoIndexes(MemoryArenaStorage* storage, void* pointer, size_t sizeInBytes) { auto pageSizeInBytes = GetSystemPageSizeInBytes(); auto dataStart = (uint8_t*)storage + storage->HeaderSizeInBytes; auto offset = (uint8_t*)pointer - dataStart; + auto endOffset = offset + sizeInBytes; + size_t alignedEndOffset; + auto alignmentSucceeded = TryAlignSize(endOffset, pageSizeInBytes, &alignedEndOffset); + SystemAssert(alignmentSucceeded); PageSizeIndexes result = {}; result.StartIndex = offset / pageSizeInBytes; - result.EndIndex = ResizeToPageSizeMultiple(offset + sizeInBytes, pageSizeInBytes) / pageSizeInBytes; + result.EndIndex = alignedEndOffset / pageSizeInBytes; return result; } @@ -82,11 +109,6 @@ PageSizeIndexes ComputePageSizeLocalOffsets(MemoryArenaStorage* storage, size_t return result; } -size_t ResizeToPageSizeMultiple(size_t sizeInBytes, size_t pageSizeInBytes) -{ - return (sizeInBytes + pageSizeInBytes - 1) & ~(pageSizeInBytes - 1); -} - void SetPageCommitted(MemoryArenaStorage* storage, uint32_t pageIndex) { auto arrayIndex = pageIndex / 32; @@ -121,15 +143,52 @@ void UnlockMemoryArenaCommitOperations(MemoryArenaStorage* storage) MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) { auto pageSizeInBytes = GetSystemPageSizeInBytes(); - auto dataSizeInBytes = ResizeToPageSizeMultiple(sizeInBytes, pageSizeInBytes); + size_t dataSizeInBytes; + + if (!TryAlignSize(sizeInBytes, pageSizeInBytes, &dataSizeInBytes)) + { + return nullptr; + } + auto pageInfosCount = dataSizeInBytes / pageSizeInBytes; auto pageCommitInfosCount = (pageInfosCount + 31) / 32; - auto headerMetadataSizeInBytes = sizeof(MemoryArenaStorage) + pageInfosCount * sizeof(MemoryArenaPageInfo) + pageCommitInfosCount * sizeof(MemoryArenaPageCommitInfo); - auto headerSizeInBytes = ResizeToPageSizeMultiple(headerMetadataSizeInBytes, pageSizeInBytes); - auto reservedSizeInBytes = headerSizeInBytes + dataSizeInBytes; + auto headerMetadataSizeInBytes = sizeof(MemoryArenaStorage); + + if (pageInfosCount > (SIZE_MAX - headerMetadataSizeInBytes) / sizeof(MemoryArenaPageInfo)) + { + return nullptr; + } + + headerMetadataSizeInBytes += pageInfosCount * sizeof(MemoryArenaPageInfo); + + if (pageCommitInfosCount > (SIZE_MAX - headerMetadataSizeInBytes) / sizeof(MemoryArenaPageCommitInfo)) + { + return nullptr; + } + headerMetadataSizeInBytes += pageCommitInfosCount * sizeof(MemoryArenaPageCommitInfo); + + size_t headerSizeInBytes; + + if (!TryAlignSize(headerMetadataSizeInBytes, pageSizeInBytes, &headerSizeInBytes) || + dataSizeInBytes > SIZE_MAX - headerSizeInBytes) + { + return nullptr; + } + + auto reservedSizeInBytes = headerSizeInBytes + dataSizeInBytes; auto storage = (MemoryArenaStorage*)SystemPlatformReserveMemory(reservedSizeInBytes); - SystemPlatformCommitMemory(storage, headerSizeInBytes); + + if (storage == nullptr) + { + return nullptr; + } + + if (!SystemPlatformCommitMemory(storage, headerSizeInBytes)) + { + SystemPlatformFreeMemory(storage, reservedSizeInBytes); + return nullptr; + } storage->CurrentPointer = (uint8_t*)storage + headerSizeInBytes; storage->SizeInBytes = sizeInBytes; @@ -154,14 +213,25 @@ MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) MemoryArena GetStackWorkingMemoryArena(MemoryArena memoryArena) { + if (memoryArena.Storage == nullptr) + { + return {}; + } + MemoryArena workingMemoryArena = memoryArena; if (memoryArena.Level != memoryArena.Storage->StackLevel) { if (memoryArena.Storage->StackExtraStorage.Storage == nullptr) { - auto extraHandle = AllocateMemoryArenaStorage(MEMORYARENA_DEFAULT_SIZE); - memoryArena.Storage->StackExtraStorage = { extraHandle, 0 }; + auto extraStorage = AllocateMemoryArenaStorage(MEMORYARENA_DEFAULT_SIZE); + + if (extraStorage == nullptr) + { + return {}; + } + + memoryArena.Storage->StackExtraStorage = { extraStorage, 0 }; } workingMemoryArena = memoryArena.Storage->StackExtraStorage; @@ -173,7 +243,7 @@ MemoryArena GetStackWorkingMemoryArena(MemoryArena memoryArena) bool IsStackMemoryArena(MemoryArena memoryArena) { - if (stackMemoryArenaStorage == nullptr) + if (memoryArena.Storage == nullptr || stackMemoryArenaStorage == nullptr) { return false; } @@ -184,6 +254,11 @@ bool IsStackMemoryArena(MemoryArena memoryArena) size_t GetMemoryArenaAllocatedBytes(MemoryArena memoryArena) { + if (memoryArena.Storage == nullptr) + { + return 0; + } + uint8_t* currentPointer; if (IsStackMemoryArena(memoryArena)) @@ -224,13 +299,25 @@ MemoryArena SystemAllocateMemoryArena(size_t sizeInBytes) void SystemFreeMemoryArena(MemoryArena memoryArena) { + if (memoryArena.Storage == nullptr) + { + return; + } + auto pageSizeInBytes = GetSystemPageSizeInBytes(); - auto dataSizeInBytes = ResizeToPageSizeMultiple(memoryArena.Storage->SizeInBytes, pageSizeInBytes); + size_t dataSizeInBytes; + auto alignmentSucceeded = TryAlignSize(memoryArena.Storage->SizeInBytes, pageSizeInBytes, &dataSizeInBytes); + SystemAssert(alignmentSucceeded); SystemPlatformFreeMemory(memoryArena.Storage, memoryArena.Storage->HeaderSizeInBytes + dataSizeInBytes); } void SystemClearMemoryArena(MemoryArena memoryArena) { + if (memoryArena.Storage == nullptr) + { + return; + } + auto storage = memoryArena.Storage; auto allocatedSize = GetMemoryArenaAllocatedBytes(memoryArena); @@ -250,6 +337,11 @@ void SystemClearMemoryArena(MemoryArena memoryArena) MemoryArenaAllocationInfos SystemGetMemoryArenaAllocationInfos(MemoryArena memoryArena) { + if (memoryArena.Storage == nullptr) + { + return {}; + } + size_t committedPagesCount; if (IsStackMemoryArena(memoryArena)) @@ -274,6 +366,11 @@ StackMemoryArena SystemGetStackMemoryArena() if (stackMemoryArenaStorage == nullptr) { stackMemoryArenaStorage = AllocateMemoryArenaStorage(MEMORYARENA_DEFAULT_SIZE); + + if (stackMemoryArenaStorage == nullptr) + { + return {}; + } } stackMemoryArenaStorage->StackLevel++; @@ -298,6 +395,11 @@ StackMemoryArena SystemGetStackMemoryArena() StackMemoryArena::~StackMemoryArena() { + if (Arena.Storage == nullptr) + { + return; + } + auto storage = Arena.Storage; if (storage->StackExtraStorage.Storage != nullptr) @@ -324,15 +426,35 @@ StackMemoryArena::~StackMemoryArena() template void SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory) { - SystemCommitMemory(memoryArena, (uint8_t*)buffer.Pointer, sizeof(T) * buffer.Length, clearMemory); + size_t sizeInBytes; + + if (!TryMultiplySize(sizeof(T), buffer.Length, &sizeInBytes)) + { + return; + } + + SystemCommitMemory(memoryArena, (void*)buffer.Pointer, sizeInBytes, clearMemory); } void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory) { + if (memoryArena.Storage == nullptr || pointer == nullptr || sizeInBytes == 0) + { + return; + } + auto storage = memoryArena.Storage; - auto offset = (uint8_t*)pointer - ((uint8_t*)storage + storage->HeaderSizeInBytes); + auto dataStart = (uintptr_t)storage + storage->HeaderSizeInBytes; + auto pointerAddress = (uintptr_t)pointer; - if (offset < 0 || offset + sizeInBytes > storage->SizeInBytes) + if (pointerAddress < dataStart) + { + return; + } + + auto offset = (size_t)(pointerAddress - dataStart); + + if (offset > storage->SizeInBytes || sizeInBytes > storage->SizeInBytes - offset) { return; } @@ -368,11 +490,6 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt UnlockMemoryArenaCommitOperations(storage); } - if (memoryArena.Storage == stackMemoryArenaStorage) - { - SystemPlatformClearMemory(pointer, sizeInBytes); - } - return; } @@ -383,7 +500,18 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt if (!IsPageCommitted(storage, (uint32_t)i)) { auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * pageSizeInBytes; - SystemPlatformCommitMemory(pagePointer, pageSizeInBytes); + + if (!SystemPlatformCommitMemory(pagePointer, pageSizeInBytes)) + { + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot commit memory arena page."); + + if (needsSynchronization) + { + UnlockMemoryArenaCommitOperations(storage); + } + + return; + } if (clearMemory) { @@ -407,19 +535,27 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt { UnlockMemoryArenaCommitOperations(storage); } - - if (memoryArena.Storage == stackMemoryArenaStorage) - { - SystemPlatformClearMemory(pointer, sizeInBytes); - } } void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes) { + if (memoryArena.Storage == nullptr || pointer == nullptr || sizeInBytes == 0) + { + return; + } + auto storage = memoryArena.Storage; - auto offset = (uint8_t*)pointer - ((uint8_t*)storage + storage->HeaderSizeInBytes); + auto dataStart = (uintptr_t)storage + storage->HeaderSizeInBytes; + auto pointerAddress = (uintptr_t)pointer; - if (offset < 0 || offset + sizeInBytes > storage->SizeInBytes) + if (pointerAddress < dataStart) + { + return; + } + + auto offset = (size_t)(pointerAddress - dataStart); + + if (offset > storage->SizeInBytes || sizeInBytes > storage->SizeInBytes - offset) { return; } @@ -464,13 +600,12 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB { auto pageInfos = &storage->PagesInfos[i]; - if (IsPageCommitted(storage, (uint32_t)i)) + if (IsPageCommitted(storage, (uint32_t)i) && (int32_t)(pageInfos->MaxCommittedOffset - pageInfos->MinCommittedOffset) <= 0) { auto pagePointer = (uint8_t*)storage + storage->HeaderSizeInBytes + i * pageSizeInBytes; - if ((int32_t)(pageInfos->MaxCommittedOffset - pageInfos->MinCommittedOffset) <= 0) + if (SystemPlatformDecommitMemory(pagePointer, pageSizeInBytes)) { - SystemPlatformDecommitMemory(pagePointer, pageSizeInBytes); ClearPageCommitted(storage, (uint32_t)i); if (needsSynchronization) @@ -493,9 +628,27 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationState state) { - sizeInBytes = SystemAlign(sizeInBytes, MEMORYARENA_DEFAULT_ALIGNMENT); + if (memoryArena.Storage == nullptr) + { + return nullptr; + } + + size_t alignedSizeInBytes; + + if (!TryAlignSize(sizeInBytes, MEMORYARENA_DEFAULT_ALIGNMENT, &alignedSizeInBytes)) + { + return nullptr; + } + + sizeInBytes = alignedSizeInBytes; auto workingMemoryArena = GetStackWorkingMemoryArena(memoryArena); + + if (workingMemoryArena.Storage == nullptr) + { + return nullptr; + } + auto storage = workingMemoryArena.Storage; uint8_t* pointer; @@ -588,28 +741,59 @@ void* SystemPushMemoryZero(MemoryArena memoryArena, size_t sizeInBytes) template Span SystemPushArray(MemoryArena memoryArena, size_t count, AllocationState state) { - auto memory = SystemPushMemory(memoryArena, sizeof(T) * count, state); + size_t sizeInBytes; + + if (!TryMultiplySize(sizeof(T), count, &sizeInBytes)) + { + return {}; + } + + auto memory = SystemPushMemory(memoryArena, sizeInBytes, state); return memory ? Span((T*)memory, count) : Span(); } template Span SystemPushArrayZero(MemoryArena memoryArena, size_t count) { - auto memory = SystemPushMemoryZero(memoryArena, sizeof(T) * count); + size_t sizeInBytes; + + if (!TryMultiplySize(sizeof(T), count, &sizeInBytes)) + { + return {}; + } + + auto memory = SystemPushMemoryZero(memoryArena, sizeInBytes); return memory ? Span((T*)memory, count) : Span(); } template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count) { - auto memory = SystemPushMemoryZero(memoryArena, sizeof(char) * (count + 1)); + if (count == SIZE_MAX) + { + return {}; + } + + auto memory = SystemPushMemoryZero(memoryArena, count + 1); return memory ? Span((char*)memory, count) : Span(); } template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count) { - auto memory = SystemPushMemoryZero(memoryArena, sizeof(wchar_t) * (count + 1)); + if (count == SIZE_MAX) + { + return {}; + } + + size_t sizeInBytes; + + if (!TryMultiplySize(sizeof(wchar_t), count + 1, &sizeInBytes)) + { + return {}; + } + + auto memory = SystemPushMemoryZero(memoryArena, sizeInBytes); return memory ? Span((wchar_t*)memory, count) : Span(); } @@ -639,7 +823,14 @@ void SystemCopyBuffer(Span destination, ReadOnlySpan source) return; } - SystemPlatformCopyMemory(destination.Pointer, source.Pointer, source.Length * sizeof(T)); + size_t sizeInBytes; + + if (!TryMultiplySize(sizeof(T), source.Length, &sizeInBytes)) + { + return; + } + + SystemPlatformCopyMemory(destination.Pointer, source.Pointer, sizeInBytes); } template @@ -673,6 +864,11 @@ Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan sou template Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2) { + if (buffer1.Length > SIZE_MAX - buffer2.Length) + { + return {}; + } + auto result = SystemPushArray(memoryArena, buffer1.Length + buffer2.Length); if (result.Pointer == nullptr) @@ -689,6 +885,11 @@ Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, Re template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2) { + if (buffer1.Length > SIZE_MAX - buffer2.Length) + { + return {}; + } + auto result = SystemPushArrayZero(memoryArena, buffer1.Length + buffer2.Length); if (result.Pointer == nullptr) @@ -705,6 +906,11 @@ Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffe template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2) { + if (buffer1.Length > SIZE_MAX - buffer2.Length) + { + return {}; + } + auto result = SystemPushArrayZero(memoryArena, buffer1.Length + buffer2.Length); if (result.Pointer == nullptr) From b80f7e39c2e92fcd5f449d653cc45111b317eefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:48:14 +0200 Subject: [PATCH 17/60] Add VM accounting and overflow regression tests --- .../MemoryRobustnessTests.cpp | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/FoundationsTests/MemoryRobustnessTests.cpp diff --git a/tests/FoundationsTests/MemoryRobustnessTests.cpp b/tests/FoundationsTests/MemoryRobustnessTests.cpp new file mode 100644 index 00000000..d8a8ee35 --- /dev/null +++ b/tests/FoundationsTests/MemoryRobustnessTests.cpp @@ -0,0 +1,84 @@ +#include "SystemFunctions.h" +#include "SystemMemory.h" +#include "SystemPlatformFunctions.h" +#include "utest.h" + +struct ConcurrentArenaAllocationParameter +{ + MemoryArena* Result; + size_t SizeInBytes; +}; + +void ConcurrentArenaAllocationFunction(void* parameter) +{ + auto threadParameter = (ConcurrentArenaAllocationParameter*)parameter; + *threadParameter->Result = SystemAllocateMemoryArena(threadParameter->SizeInBytes); +} + +UTEST(MemoryRobustness, ArenaSizeOverflowReturnsEmptyHandle) +{ + // Act + auto memoryArena = SystemAllocateMemoryArena(SIZE_MAX); + + // Assert + ASSERT_TRUE(memoryArena.Storage == nullptr); +} + +UTEST(MemoryRobustness, PushSizeOverflowDoesNotAdvanceArena) +{ + // Arrange + auto memoryArena = SystemAllocateMemoryArena(64); + + // Act + auto allocation = SystemPushMemory(memoryArena, SIZE_MAX, AllocationState_Reserved); + auto array = SystemPushArray(memoryArena, SIZE_MAX / sizeof(uint64_t) + 1, AllocationState_Reserved); + + // Assert + ASSERT_TRUE(allocation == nullptr); + ASSERT_TRUE(array.Pointer == nullptr); + ASSERT_EQ(0llu, array.Length); + ASSERT_EQ(0llu, SystemGetMemoryArenaAllocationInfos(memoryArena).AllocatedBytes); +} + +UTEST(MemoryRobustness, ConcurrentArenaAllocationAccounting) +{ + // Arrange + const int32_t threadCount = 16; + auto pageSizeInBytes = SystemPlatformGetPageSize(); + auto allocationInfosBefore = SystemGetAllocationInfos(); + MemoryArena memoryArenas[threadCount] = {}; + SystemThread threads[threadCount]; + ConcurrentArenaAllocationParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { &memoryArenas[i], pageSizeInBytes }; + threads[i] = SystemCreateThread(ConcurrentArenaAllocationFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + for (int32_t i = 0; i < threadCount; i++) + { + ASSERT_TRUE(memoryArenas[i].Storage != nullptr); + } + + auto allocationInfosAfterAllocate = SystemGetAllocationInfos(); + ASSERT_EQ(allocationInfosBefore.ReservedBytes + threadCount * pageSizeInBytes * 2, allocationInfosAfterAllocate.ReservedBytes); + ASSERT_EQ(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterAllocate.CommittedBytes); + + for (int32_t i = 0; i < threadCount; i++) + { + SystemFreeMemoryArena(memoryArenas[i]); + } + + auto allocationInfosAfterFree = SystemGetAllocationInfos(); + ASSERT_EQ(allocationInfosBefore.ReservedBytes, allocationInfosAfterFree.ReservedBytes); + ASSERT_EQ(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterFree.CommittedBytes); +} From 7e715498452aa490d594b7163a6d3cfbad67afb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:48:57 +0200 Subject: [PATCH 18/60] Track committed bytes when freeing VM ranges --- src/Foundations/SystemPlatformFunctions.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Foundations/SystemPlatformFunctions.h b/src/Foundations/SystemPlatformFunctions.h index 8500cf95..399e3d05 100644 --- a/src/Foundations/SystemPlatformFunctions.h +++ b/src/Foundations/SystemPlatformFunctions.h @@ -84,8 +84,9 @@ void* SystemPlatformReserveMemory(size_t sizeInBytes); * * @param pointer Start of the reserved range. * @param sizeInBytes Size of the reserved range in bytes. + * @param committedSizeInBytes Number of committed bytes still contained in the range. */ -void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes); +void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes, size_t committedSizeInBytes); /** * Commits a range inside a previously reserved virtual-address region. From 405715e6b0aacef12bf85cb83a9b2de1c45db633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:49:27 +0200 Subject: [PATCH 19/60] Account committed bytes on Windows VM release --- src/Foundations/Microsoft/SystemPlatformFunctions.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Foundations/Microsoft/SystemPlatformFunctions.cpp b/src/Foundations/Microsoft/SystemPlatformFunctions.cpp index 4eb1c144..a2ecd510 100644 --- a/src/Foundations/Microsoft/SystemPlatformFunctions.cpp +++ b/src/Foundations/Microsoft/SystemPlatformFunctions.cpp @@ -81,11 +81,12 @@ void* SystemPlatformReserveMemory(size_t sizeInBytes) return result; } -void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes) +void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes, size_t committedSizeInBytes) { if (VirtualFree(pointer, 0, MEM_RELEASE)) { SystemAtomicSubstract(systemPlatformAllocationInfos.ReservedBytes, sizeInBytes); + SystemAtomicSubstract(systemPlatformAllocationInfos.CommittedBytes, committedSizeInBytes); } } From 5dee42f6f74020181f533f96951ce9584f84e4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 11:49:55 +0200 Subject: [PATCH 20/60] Account committed bytes on POSIX VM release --- src/Foundations/PosixPlatformFunctions.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Foundations/PosixPlatformFunctions.cpp b/src/Foundations/PosixPlatformFunctions.cpp index 23886d10..c81f49a7 100644 --- a/src/Foundations/PosixPlatformFunctions.cpp +++ b/src/Foundations/PosixPlatformFunctions.cpp @@ -96,11 +96,12 @@ void* SystemPlatformReserveMemory(size_t sizeInBytes) return result; } -void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes) +void SystemPlatformFreeMemory(void* pointer, size_t sizeInBytes, size_t committedSizeInBytes) { if (munmap(pointer, sizeInBytes) == 0) { SystemAtomicSubstract(systemPlatformAllocationInfos.ReservedBytes, sizeInBytes); + SystemAtomicSubstract(systemPlatformAllocationInfos.CommittedBytes, committedSizeInBytes); } } From b6f5139e1a1ffc47b097e042e4d45954ca9f35d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:03:54 +0200 Subject: [PATCH 21/60] Validate memory accounting after arena release --- tests/FoundationsTests/MemoryRobustnessTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FoundationsTests/MemoryRobustnessTests.cpp b/tests/FoundationsTests/MemoryRobustnessTests.cpp index d8a8ee35..0234aa1f 100644 --- a/tests/FoundationsTests/MemoryRobustnessTests.cpp +++ b/tests/FoundationsTests/MemoryRobustnessTests.cpp @@ -80,5 +80,5 @@ UTEST(MemoryRobustness, ConcurrentArenaAllocationAccounting) auto allocationInfosAfterFree = SystemGetAllocationInfos(); ASSERT_EQ(allocationInfosBefore.ReservedBytes, allocationInfosAfterFree.ReservedBytes); - ASSERT_EQ(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterFree.CommittedBytes); + ASSERT_EQ(allocationInfosBefore.CommittedBytes, allocationInfosAfterFree.CommittedBytes); } From ffc7ad1b710adf233a3a4bab5847c17cb22dcacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:04:03 +0200 Subject: [PATCH 22/60] Run memory robustness regressions --- tests/FoundationsTests/UnityBuild.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/FoundationsTests/UnityBuild.cpp b/tests/FoundationsTests/UnityBuild.cpp index 0524b29d..b1ef6aba 100644 --- a/tests/FoundationsTests/UnityBuild.cpp +++ b/tests/FoundationsTests/UnityBuild.cpp @@ -1,6 +1,7 @@ #include "utest.h" #include "MemoryTests.cpp" +#include "MemoryRobustnessTests.cpp" #include "SpanTests.cpp" #include "MathTests.cpp" #include "StringTests.cpp" From c2c90bb9aa22a0d6bc07f4e93e1322fa8a827807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:05:11 +0200 Subject: [PATCH 23/60] Track committed bytes when freeing arenas --- src/Foundations/SystemMemory.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index d3350ba5..5c8f4320 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -186,7 +186,7 @@ MemoryArenaStorage* AllocateMemoryArenaStorage(size_t sizeInBytes) if (!SystemPlatformCommitMemory(storage, headerSizeInBytes)) { - SystemPlatformFreeMemory(storage, reservedSizeInBytes); + SystemPlatformFreeMemory(storage, reservedSizeInBytes, 0); return nullptr; } @@ -308,7 +308,10 @@ void SystemFreeMemoryArena(MemoryArena memoryArena) size_t dataSizeInBytes; auto alignmentSucceeded = TryAlignSize(memoryArena.Storage->SizeInBytes, pageSizeInBytes, &dataSizeInBytes); SystemAssert(alignmentSucceeded); - SystemPlatformFreeMemory(memoryArena.Storage, memoryArena.Storage->HeaderSizeInBytes + dataSizeInBytes); + + auto reservedSizeInBytes = memoryArena.Storage->HeaderSizeInBytes + dataSizeInBytes; + auto committedSizeInBytes = memoryArena.Storage->CommittedPagesCount * pageSizeInBytes; + SystemPlatformFreeMemory(memoryArena.Storage, reservedSizeInBytes, committedSizeInBytes); } void SystemClearMemoryArena(MemoryArena memoryArena) From 550613f16b290d985cc69d0dc0f5f8cd3126571c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:08:57 +0200 Subject: [PATCH 24/60] Harden concurrent data pool allocation --- src/Foundations/SystemDataPool.cpp | 205 +++++++++++++++++++++++------ 1 file changed, 164 insertions(+), 41 deletions(-) diff --git a/src/Foundations/SystemDataPool.cpp b/src/Foundations/SystemDataPool.cpp index ae045a68..96494eb0 100644 --- a/src/Foundations/SystemDataPool.cpp +++ b/src/Foundations/SystemDataPool.cpp @@ -18,6 +18,7 @@ struct SystemDataPoolStorage MemoryArena MemoryArena; Span> Data; Span DataFull; + bool IsItemAllocationInProgress; uint32_t CurrentIndex; uint32_t FreeListIndex; uint32_t ItemCount; @@ -37,23 +38,98 @@ SystemDataPoolHandle UnpackSystemDataPoolHandle(uint64_t packedValue) return result; } +uint32_t GetNextSystemDataPoolVersion(uint32_t version) +{ + auto result = version + 1; + + if (result == SYSTEM_DATAPOOL_INDEX_EMPTY) + { + result = 0; + } + + return result; +} + template bool IsTypeEmpty() { return sizeof(T) == 1; } +template +void LockSystemDataPoolItemAllocation(SystemDataPoolStorage* storage) +{ + SystemAtomicReplace(storage->IsItemAllocationInProgress, false, true); +} + +template +void UnlockSystemDataPoolItemAllocation(SystemDataPoolStorage* storage) +{ + SystemAtomicStore(storage->IsItemAllocationInProgress, false); +} + +template +uint32_t AcquireSystemDataPoolItemIndex(SystemDataPoolStorage* storage, bool* isNewIndex) +{ + LockSystemDataPoolItemAllocation(storage); + + if (storage->FreeListIndex != SYSTEM_DATAPOOL_INDEX_EMPTY) + { + auto index = storage->FreeListIndex; + storage->FreeListIndex = storage->Data[index].Next; + storage->Data[index].Next = SYSTEM_DATAPOOL_INDEX_EMPTY; + *isNewIndex = false; + UnlockSystemDataPoolItemAllocation(storage); + return index; + } + + uint32_t currentIndex; + SystemAtomicLoad(storage->CurrentIndex, currentIndex); + + if (currentIndex >= storage->Data.Length) + { + UnlockSystemDataPoolItemAllocation(storage); + return SYSTEM_DATAPOOL_INDEX_EMPTY; + } + + SystemAtomicStore(storage->CurrentIndex, currentIndex + 1); + *isNewIndex = true; + UnlockSystemDataPoolItemAllocation(storage); + return currentIndex; +} + template SystemDataPool SystemCreateDataPool(MemoryArena memoryArena, size_t maxItems) { + if (maxItems > UINT32_MAX) + { + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Data Pool maximum item count is too large."); + return {}; + } + auto storage = SystemPushStructZero>(memoryArena); + + if (storage == nullptr) + { + return {}; + } + storage->MemoryArena = memoryArena; - storage->Data = SystemPushArray>(memoryArena, maxItems, AllocationState_Reserved); + + if (maxItems > 0 && storage->Data.Pointer == nullptr) + { + return {}; + } if (!IsTypeEmpty()) { storage->DataFull = SystemPushArray(memoryArena, maxItems, AllocationState_Reserved); + + if (maxItems > 0 && storage->DataFull.Pointer == nullptr) + { + return {}; + } } storage->FreeListIndex = SYSTEM_DATAPOOL_INDEX_EMPTY; @@ -70,29 +146,17 @@ ElemHandle SystemAddDataPoolItem(SystemDataPool dataPool, T data) auto storage = dataPool.Storage; SystemAssert(storage); - auto index = SYSTEM_DATAPOOL_INDEX_EMPTY; - - do - { - if (storage->FreeListIndex == SYSTEM_DATAPOOL_INDEX_EMPTY) - { - index = SYSTEM_DATAPOOL_INDEX_EMPTY; - break; - } - - index = storage->FreeListIndex; - } while (!SystemAtomicCompareExchange(storage->FreeListIndex, index, storage->Data[storage->FreeListIndex].Next)); + auto isNewIndex = false; + auto index = AcquireSystemDataPoolItemIndex(storage, &isNewIndex); if (index == SYSTEM_DATAPOOL_INDEX_EMPTY) { - if (storage->CurrentIndex >= storage->Data.Length) - { - SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Data Pool is full."); - return ELEM_HANDLE_NULL; - } - - index = SystemAtomicAdd(storage->CurrentIndex, 1); + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Data Pool is full."); + return ELEM_HANDLE_NULL; + } + if (isNewIndex) + { auto remainingItemCount = storage->Data.Length - index; auto itemCountToCommit = remainingItemCount > 1000 ? 1000 : remainingItemCount; SystemCommitMemory>(storage->MemoryArena, storage->Data.Slice(index, itemCountToCommit), true); @@ -104,12 +168,14 @@ ElemHandle SystemAddDataPoolItem(SystemDataPool dataPool, T data) } storage->Data[index].Data = data; - storage->Data[index].Next = SYSTEM_DATAPOOL_INDEX_EMPTY; - + SystemAtomicStore(storage->Data[index].Next, SYSTEM_DATAPOOL_INDEX_EMPTY); SystemAtomicAdd(storage->ItemCount, 1); + uint32_t version; + SystemAtomicLoad(storage->Data[index].Version, version); + result.Index = index; - result.Version = storage->Data[index].Version; + result.Version = version; return PackSystemDataPoolHandle(result); } @@ -128,6 +194,19 @@ void SystemAddDataPoolItemFull(SystemDataPool dataPool, ElemHandle han auto dataPoolHandle = UnpackSystemDataPoolHandle(handle); + if (dataPoolHandle.Index >= storage->Data.Length) + { + return; + } + + uint32_t version; + SystemAtomicLoad(storage->Data[dataPoolHandle.Index].Version, version); + + if (version != dataPoolHandle.Version) + { + return; + } + storage->DataFull[dataPoolHandle.Index] = data; } @@ -140,19 +219,31 @@ void SystemRemoveDataPoolItem(SystemDataPool dataPool, ElemHandle hand auto dataPoolHandle = UnpackSystemDataPoolHandle(handle); - if (dataPoolHandle.Version != storage->Data[dataPoolHandle.Index].Version) + if (dataPoolHandle.Index >= storage->Data.Length) + { + return; + } + + LockSystemDataPoolItemAllocation(storage); + + uint32_t version; + SystemAtomicLoad(storage->Data[dataPoolHandle.Index].Version, version); + + if (dataPoolHandle.Version != version) { + UnlockSystemDataPoolItemAllocation(storage); SystemLogWarningMessage(ElemLogMessageCategory_Memory, "Trying to remove an already deleted handle."); return; } - storage->Data[dataPoolHandle.Index].Version = SystemAtomicAdd(storage->Data[dataPoolHandle.Index].Version, 1) + 1; + auto nextVersion = GetNextSystemDataPoolVersion(version); + SystemAtomicStore(storage->Data[dataPoolHandle.Index].Version, nextVersion); SystemAtomicSubstract(storage->ItemCount, 1); - do - { - storage->Data[dataPoolHandle.Index].Next = storage->FreeListIndex; - } while (!SystemAtomicCompareExchange(storage->FreeListIndex, storage->FreeListIndex, dataPoolHandle.Index)); + storage->Data[dataPoolHandle.Index].Next = storage->FreeListIndex; + storage->FreeListIndex = dataPoolHandle.Index; + + UnlockSystemDataPoolItemAllocation(storage); } template @@ -163,38 +254,70 @@ T* SystemGetDataPoolItem(SystemDataPool dataPool, ElemHandle handle) SystemAssert(handle != ELEM_HANDLE_NULL); auto dataPoolHandle = UnpackSystemDataPoolHandle(handle); - - T* result = nullptr; - if (dataPoolHandle.Version != SYSTEM_DATAPOOL_INDEX_EMPTY && storage->CurrentIndex > dataPoolHandle.Index && storage->Data[dataPoolHandle.Index].Version == dataPoolHandle.Version) + if (dataPoolHandle.Version == SYSTEM_DATAPOOL_INDEX_EMPTY || dataPoolHandle.Index >= storage->Data.Length) { - result = &storage->Data[dataPoolHandle.Index].Data; + return nullptr; } - return result; + uint32_t currentIndex; + SystemAtomicLoad(storage->CurrentIndex, currentIndex); + + if (dataPoolHandle.Index >= currentIndex) + { + return nullptr; + } + + uint32_t version; + SystemAtomicLoad(storage->Data[dataPoolHandle.Index].Version, version); + + if (version != dataPoolHandle.Version) + { + return nullptr; + } + + return &storage->Data[dataPoolHandle.Index].Data; } template TFull* SystemGetDataPoolItemFull(SystemDataPool dataPool, ElemHandle handle) { auto storage = dataPool.Storage; - auto dataPoolHandle = UnpackSystemDataPoolHandle(handle); SystemAssert(storage); SystemAssert(handle != ELEM_HANDLE_NULL); - - TFull* result = nullptr; - if (dataPoolHandle.Version != SYSTEM_DATAPOOL_INDEX_EMPTY && storage->CurrentIndex > dataPoolHandle.Index && storage->Data[dataPoolHandle.Index].Version == dataPoolHandle.Version) + auto dataPoolHandle = UnpackSystemDataPoolHandle(handle); + + if (dataPoolHandle.Version == SYSTEM_DATAPOOL_INDEX_EMPTY || dataPoolHandle.Index >= storage->Data.Length) { - result = &storage->DataFull[dataPoolHandle.Index]; + return nullptr; } - return result; + uint32_t currentIndex; + SystemAtomicLoad(storage->CurrentIndex, currentIndex); + + if (dataPoolHandle.Index >= currentIndex) + { + return nullptr; + } + + uint32_t version; + SystemAtomicLoad(storage->Data[dataPoolHandle.Index].Version, version); + + if (version != dataPoolHandle.Version) + { + return nullptr; + } + + return &storage->DataFull[dataPoolHandle.Index]; } template size_t SystemGetDataPoolItemCount(SystemDataPool dataPool) { SystemAssert(dataPool.Storage); - return dataPool.Storage->ItemCount; + + uint32_t itemCount; + SystemAtomicLoad(dataPool.Storage->ItemCount, itemCount); + return itemCount; } From 82dcb04896d498e19b02b6d937a77b0c94faca61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:09:37 +0200 Subject: [PATCH 25/60] Add concurrent data pool regressions --- .../DataPoolRobustnessTests.cpp | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 tests/FoundationsTests/DataPoolRobustnessTests.cpp diff --git a/tests/FoundationsTests/DataPoolRobustnessTests.cpp b/tests/FoundationsTests/DataPoolRobustnessTests.cpp new file mode 100644 index 00000000..2be89fc5 --- /dev/null +++ b/tests/FoundationsTests/DataPoolRobustnessTests.cpp @@ -0,0 +1,199 @@ +#include "SystemDataPool.h" +#include "SystemFunctions.h" +#include "utest.h" + +struct DataPoolRobustnessData +{ + uint64_t Value; +}; + +struct DataPoolConcurrentAddOneParameter +{ + SystemDataPool DataPool; + ElemHandle* Result; + uint64_t Value; +}; + +struct DataPoolConcurrentRemoveOneParameter +{ + SystemDataPool DataPool; + ElemHandle Handle; +}; + +struct DataPoolConcurrentReuseParameter +{ + SystemDataPool DataPool; + ElemHandle Handle; + uint32_t ThreadId; + uint32_t IterationCount; + bool Failed; +}; + +void DataPoolConcurrentAddOneFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentAddOneParameter*)parameter; + DataPoolRobustnessData data = { threadParameter->Value }; + *threadParameter->Result = SystemAddDataPoolItem(threadParameter->DataPool, data); +} + +void DataPoolConcurrentRemoveOneFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentRemoveOneParameter*)parameter; + SystemRemoveDataPoolItem(threadParameter->DataPool, threadParameter->Handle); +} + +void DataPoolConcurrentReuseFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentReuseParameter*)parameter; + auto handle = threadParameter->Handle; + + for (uint32_t i = 0; i < threadParameter->IterationCount; i++) + { + SystemRemoveDataPoolItem(threadParameter->DataPool, handle); + + DataPoolRobustnessData data = {}; + data.Value = ((uint64_t)threadParameter->ThreadId << 32) | i; + handle = SystemAddDataPoolItem(threadParameter->DataPool, data); + + if (handle == ELEM_HANDLE_NULL) + { + threadParameter->Failed = true; + return; + } + } + + threadParameter->Handle = handle; +} + +UTEST(DataPoolRobustness, ConcurrentAddStopsAtCapacity) +{ + // Arrange + const int32_t threadCount = 32; + const int32_t capacity = 8; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, capacity); + ElemHandle handles[threadCount] = {}; + SystemThread threads[threadCount]; + DataPoolConcurrentAddOneParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dataPool, &handles[i], (uint64_t)i }; + threads[i] = SystemCreateThread(DataPoolConcurrentAddOneFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto successCount = 0; + + for (int32_t i = 0; i < threadCount; i++) + { + if (handles[i] == ELEM_HANDLE_NULL) + { + continue; + } + + successCount++; + auto handleInfo = UnpackSystemDataPoolHandle(handles[i]); + + for (int32_t j = i + 1; j < threadCount; j++) + { + if (handles[j] != ELEM_HANDLE_NULL) + { + ASSERT_TRUE(handleInfo.Index != UnpackSystemDataPoolHandle(handles[j]).Index); + } + } + } + + ASSERT_EQ(capacity, successCount); + ASSERT_EQ((size_t)capacity, SystemGetDataPoolItemCount(dataPool)); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DataPoolRobustness, ConcurrentRemoveSameHandleOnlyFreesOnce) +{ + // Arrange + const int32_t threadCount = 16; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, 1); + auto handle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { 42 }); + SystemThread threads[threadCount]; + DataPoolConcurrentRemoveOneParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dataPool, handle }; + threads[i] = SystemCreateThread(DataPoolConcurrentRemoveOneFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + ASSERT_EQ(0llu, SystemGetDataPoolItemCount(dataPool)); + + auto reusedHandle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { 100 }); + auto overflowHandle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { 200 }); + ASSERT_TRUE(reusedHandle != ELEM_HANDLE_NULL); + ASSERT_TRUE(overflowHandle == ELEM_HANDLE_NULL); + ASSERT_EQ(1llu, SystemGetDataPoolItemCount(dataPool)); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DataPoolRobustness, ConcurrentReuseKeepsSlotsUnique) +{ + // Arrange + const int32_t threadCount = 16; + const uint32_t iterationCount = 5000; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, threadCount); + SystemThread threads[threadCount]; + DataPoolConcurrentReuseParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + auto handle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { (uint64_t)i }); + threadParameters[i] = { dataPool, handle, (uint32_t)i, iterationCount, false }; + threads[i] = SystemCreateThread(DataPoolConcurrentReuseFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + ASSERT_EQ((size_t)threadCount, SystemGetDataPoolItemCount(dataPool)); + + for (int32_t i = 0; i < threadCount; i++) + { + ASSERT_FALSE(threadParameters[i].Failed); + ASSERT_TRUE(threadParameters[i].Handle != ELEM_HANDLE_NULL); + + auto data = SystemGetDataPoolItem(dataPool, threadParameters[i].Handle); + ASSERT_TRUE(data != nullptr); + ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), data->Value); + + auto handleInfo = UnpackSystemDataPoolHandle(threadParameters[i].Handle); + + for (int32_t j = i + 1; j < threadCount; j++) + { + auto otherHandleInfo = UnpackSystemDataPoolHandle(threadParameters[j].Handle); + ASSERT_TRUE(handleInfo.Index != otherHandleInfo.Index); + } + } + + SystemFreeMemoryArena(memoryArena); +} From 6753494a88e1e345f53f735b65a57e1ef3e380d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:09:49 +0200 Subject: [PATCH 26/60] Run data pool robustness regressions --- tests/FoundationsTests/UnityBuild.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/FoundationsTests/UnityBuild.cpp b/tests/FoundationsTests/UnityBuild.cpp index b1ef6aba..721189dc 100644 --- a/tests/FoundationsTests/UnityBuild.cpp +++ b/tests/FoundationsTests/UnityBuild.cpp @@ -9,6 +9,7 @@ #include "LibraryProcessTests.cpp" #include "DictionaryTests.cpp" #include "DataPoolTests.cpp" +#include "DataPoolRobustnessTests.cpp" #ifndef _WIN32 #include "PosixPlatformFunctions.cpp" From 1169584c07fcf542d020d6ec15ea22d6508c3f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:12:42 +0200 Subject: [PATCH 27/60] Harden dictionary concurrency --- src/Foundations/SystemDictionary.cpp | 358 ++++++++++++--------------- 1 file changed, 162 insertions(+), 196 deletions(-) diff --git a/src/Foundations/SystemDictionary.cpp b/src/Foundations/SystemDictionary.cpp index aa40b768..47273baf 100644 --- a/src/Foundations/SystemDictionary.cpp +++ b/src/Foundations/SystemDictionary.cpp @@ -6,8 +6,6 @@ #endif #define SYSTEM_DICTIONARY_HASH_SEED 123456789 - -// TODO: Convert the indexes to uint32_t and use maxsize for empty #define SYSTEM_DICTIONARY_INDEX_EMPTY -1 template @@ -24,6 +22,7 @@ struct SystemDictionaryStorage MemoryArena MemoryArena; Span Buckets; Span> Entries; + bool IsOperationInProgress; size_t CurrentEntryIndex; int32_t FreeListIndex; }; @@ -43,46 +42,21 @@ struct SystemDictionaryHashInfo }; template -SystemDictionaryIndexInfo GetDictionaryEntryIndexInfo(SystemDictionaryStorage* storage, SystemDictionaryHashInfo hashInfo) +void LockSystemDictionary(SystemDictionaryStorage* storage) { - int32_t currentIndex; - SystemAtomicLoad(storage->Buckets[hashInfo.BucketIndex], currentIndex); - - auto rootIndex = currentIndex; - auto parentIndex = SYSTEM_DICTIONARY_INDEX_EMPTY; - - while (currentIndex != SYSTEM_DICTIONARY_INDEX_EMPTY) - { - auto currentEntry = GetDictionaryEntryByIndex(storage, currentIndex); - - if (currentEntry->Hash == hashInfo.Hash) - { - SystemDictionaryIndexInfo result = {}; - result.BucketIndex = hashInfo.BucketIndex; - result.RootIndex = rootIndex; - result.ParentIndex = parentIndex; - result.Index = currentIndex; - - return result; - } - - parentIndex = currentIndex; - SystemAtomicLoad(currentEntry->Next, currentIndex); - } - - SystemDictionaryIndexInfo result = {}; - result.BucketIndex = SYSTEM_DICTIONARY_INDEX_EMPTY; - result.RootIndex = SYSTEM_DICTIONARY_INDEX_EMPTY; - result.ParentIndex = SYSTEM_DICTIONARY_INDEX_EMPTY; - result.Index = SYSTEM_DICTIONARY_INDEX_EMPTY; + SystemAtomicReplace(storage->IsOperationInProgress, false, true); +} - return result; +template +void UnlockSystemDictionary(SystemDictionaryStorage* storage) +{ + SystemAtomicStore(storage->IsOperationInProgress, false); } template SystemDictionaryEntry* GetDictionaryEntryByIndex(SystemDictionaryStorage* storage, int32_t index) { - if (index == SYSTEM_DICTIONARY_INDEX_EMPTY) + if (index == SYSTEM_DICTIONARY_INDEX_EMPTY || index < 0 || (size_t)index >= storage->Entries.Length) { return nullptr; } @@ -91,209 +65,240 @@ SystemDictionaryEntry* GetDictionaryEntryByIndex(SystemDictionaryStorage } template -int32_t GetFreeListEntry(SystemDictionaryStorage* storage) +SystemDictionaryIndexInfo GetDictionaryEntryIndexInfo(SystemDictionaryStorage* storage, SystemDictionaryHashInfo hashInfo) { - int32_t entryIndex; - SystemAtomicLoad(storage->FreeListIndex, entryIndex); + if (hashInfo.BucketIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) + { + return { SYSTEM_DICTIONARY_INDEX_EMPTY, SYSTEM_DICTIONARY_INDEX_EMPTY, SYSTEM_DICTIONARY_INDEX_EMPTY, SYSTEM_DICTIONARY_INDEX_EMPTY }; + } - SystemDictionaryEntry* freeListEntry = nullptr; + auto currentIndex = storage->Buckets[hashInfo.BucketIndex]; + auto rootIndex = currentIndex; + auto parentIndex = SYSTEM_DICTIONARY_INDEX_EMPTY; - do + while (currentIndex != SYSTEM_DICTIONARY_INDEX_EMPTY) { - if (entryIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) + auto currentEntry = GetDictionaryEntryByIndex(storage, currentIndex); + + if (currentEntry == nullptr) { break; } - if (freeListEntry != nullptr) + if (currentEntry->Hash == hashInfo.Hash) { - SystemYieldThread(); + return { hashInfo.BucketIndex, rootIndex, parentIndex, currentIndex }; } - freeListEntry = GetDictionaryEntryByIndex(storage, entryIndex); + parentIndex = currentIndex; + currentIndex = currentEntry->Next; } - while (!SystemAtomicCompareExchange(storage->FreeListIndex, entryIndex, freeListEntry->Next)); - return entryIndex; + return { SYSTEM_DICTIONARY_INDEX_EMPTY, SYSTEM_DICTIONARY_INDEX_EMPTY, SYSTEM_DICTIONARY_INDEX_EMPTY, SYSTEM_DICTIONARY_INDEX_EMPTY }; } template -void InsertFreeListEntry(SystemDictionaryStorage* storage, int32_t index, SystemDictionaryEntry* entry) +int32_t GetFreeListEntry(SystemDictionaryStorage* storage) { - int32_t entryIndex; - SystemAtomicLoad(storage->FreeListIndex, entryIndex); - - entry->Next = entryIndex; - - while (!SystemAtomicCompareExchange(storage->FreeListIndex, entryIndex, index)) + if (storage->FreeListIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) { - entry->Next = entryIndex; - SystemYieldThread(); + return SYSTEM_DICTIONARY_INDEX_EMPTY; } + + auto entryIndex = storage->FreeListIndex; + auto entry = GetDictionaryEntryByIndex(storage, entryIndex); + SystemAssert(entry); + + storage->FreeListIndex = entry->Next; + entry->Next = SYSTEM_DICTIONARY_INDEX_EMPTY; + return entryIndex; +} + +template +void InsertFreeListEntry(SystemDictionaryStorage* storage, int32_t index, SystemDictionaryEntry* entry) +{ + entry->Next = storage->FreeListIndex; + storage->FreeListIndex = index; } template void AddDictionaryEntry(SystemDictionaryStorage* storage, SystemDictionaryHashInfo hashInfo, TValue value) { - auto entryIndex = GetFreeListEntry(storage); + if (storage == nullptr || hashInfo.BucketIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) + { + return; + } + + LockSystemDictionary(storage); + + auto entryIndex = GetFreeListEntry(storage); if (entryIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) { - entryIndex = SystemAtomicAdd(storage->CurrentEntryIndex, 1); - - if (entryIndex == (int32_t)storage->Entries.Length) + if (storage->CurrentEntryIndex >= storage->Entries.Length) { + UnlockSystemDictionary(storage); + #ifdef ElemAPI SystemLogErrorMessage(ElemLogMessageCategory_Application, "Max items in dictionary reached, the item will not be added."); #endif return; } - + + entryIndex = (int32_t)storage->CurrentEntryIndex; + storage->CurrentEntryIndex++; SystemCommitMemory>(storage->MemoryArena, storage->Entries.Slice(entryIndex, 1), true); } - - auto entry = GetDictionaryEntryByIndex(storage, entryIndex); - - int32_t bucketHead; - SystemAtomicLoad(storage->Buckets[hashInfo.BucketIndex], bucketHead); - - auto firstTry = true; - - do - { - if (!firstTry) - { - SystemYieldThread(); - } - else - { - firstTry = false; - } - if (bucketHead != SYSTEM_DICTIONARY_INDEX_EMPTY) - { - auto bucketHeadEntry = GetDictionaryEntryByIndex(storage, bucketHead); - - if (bucketHeadEntry->Hash != 0) - { - entry->Next = bucketHead; - } - else - { - entry->Next = SYSTEM_DICTIONARY_INDEX_EMPTY; - } - } - else - { - entry->Next = SYSTEM_DICTIONARY_INDEX_EMPTY; - } - } - while (!SystemAtomicCompareExchange(storage->Buckets[hashInfo.BucketIndex], bucketHead, entryIndex)); + auto entry = GetDictionaryEntryByIndex(storage, entryIndex); + SystemAssert(entry); + auto bucketHead = storage->Buckets[hashInfo.BucketIndex]; entry->Hash = hashInfo.Hash; entry->Value = value; + entry->Next = bucketHead; + + storage->Buckets[hashInfo.BucketIndex] = entryIndex; + UnlockSystemDictionary(storage); } template void RemoveDictionaryEntry(SystemDictionaryStorage* storage, SystemDictionaryHashInfo hashInfo) { - SystemDictionaryIndexInfo entryIndex = {}; - SystemDictionaryEntry* entry = nullptr; - int32_t* parentNextEntryIndex = nullptr; - int32_t retryCount = 0; - - do + if (storage == nullptr || hashInfo.BucketIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) { - entryIndex = GetDictionaryEntryIndexInfo(storage, hashInfo); + return; + } - if (entryIndex.Index == SYSTEM_DICTIONARY_INDEX_EMPTY) - { - if (retryCount < 5) - { - #ifdef ElemAPI - SystemLogDebugMessage(ElemLogMessageCategory_Application, "Retrying to find the item to delete."); - #endif - SystemYieldThread(); - retryCount++; - entry = nullptr; - continue; - } + LockSystemDictionary(storage); + auto entryIndex = GetDictionaryEntryIndexInfo(storage, hashInfo); - #ifdef ElemAPI - SystemLogErrorMessage(ElemLogMessageCategory_Application, "No entry found to delete."); - #endif - return; - } + if (entryIndex.Index == SYSTEM_DICTIONARY_INDEX_EMPTY) + { + UnlockSystemDictionary(storage); - entry = GetDictionaryEntryByIndex(storage, entryIndex.Index); + #ifdef ElemAPI + SystemLogErrorMessage(ElemLogMessageCategory_Application, "No entry found to delete."); + #endif + return; + } - if (entryIndex.RootIndex != entryIndex.Index) - { - auto parentEntry = GetDictionaryEntryByIndex(storage, entryIndex.ParentIndex); - parentNextEntryIndex = &parentEntry->Next; - } - else - { - parentNextEntryIndex = &storage->Buckets[hashInfo.BucketIndex]; - } + auto entry = GetDictionaryEntryByIndex(storage, entryIndex.Index); + SystemAssert(entry); - SystemYieldThread(); + if (entryIndex.ParentIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) + { + storage->Buckets[hashInfo.BucketIndex] = entry->Next; + } + else + { + auto parentEntry = GetDictionaryEntryByIndex(storage, entryIndex.ParentIndex); + SystemAssert(parentEntry); + parentEntry->Next = entry->Next; } - while (entry == nullptr || !SystemAtomicCompareExchange(*parentNextEntryIndex, entryIndex.Index, entry->Next)); entry->Hash = 0; entry->Value = {}; InsertFreeListEntry(storage, entryIndex.Index, entry); + UnlockSystemDictionary(storage); } template TValue* GetDictionaryValue(SystemDictionaryStorage* storage, SystemDictionaryHashInfo hashInfo) { + if (storage == nullptr || hashInfo.BucketIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) + { + return nullptr; + } + + LockSystemDictionary(storage); auto entryIndex = GetDictionaryEntryIndexInfo(storage, hashInfo); auto entry = GetDictionaryEntryByIndex(storage, entryIndex.Index); + auto result = entry != nullptr ? &entry->Value : nullptr; + UnlockSystemDictionary(storage); + return result; +} - if (entry != nullptr) +template +bool ContainsDictionaryValue(SystemDictionaryStorage* storage, SystemDictionaryHashInfo hashInfo) +{ + if (storage == nullptr || hashInfo.BucketIndex == SYSTEM_DICTIONARY_INDEX_EMPTY) { - return &entry->Value; + return false; } - static TValue defaultValue; - return &defaultValue; + LockSystemDictionary(storage); + auto entryIndex = GetDictionaryEntryIndexInfo(storage, hashInfo); + auto result = entryIndex.Index != SYSTEM_DICTIONARY_INDEX_EMPTY; + UnlockSystemDictionary(storage); + return result; } template SystemDictionaryHashInfo DictionaryComputeHashInfo(SystemDictionaryStorage* storage, ReadOnlySpan data) { - auto hash = XXH64(data.Pointer, data.Length, SYSTEM_DICTIONARY_HASH_SEED); - auto bucketIndex = (int32_t)(hash % storage->Buckets.Length); + if (storage == nullptr || storage->Buckets.Length == 0 || data.Length > SIZE_MAX / sizeof(T)) + { + return { 0, SYSTEM_DICTIONARY_INDEX_EMPTY }; + } - SystemDictionaryHashInfo result = {}; - result.Hash = hash; - result.BucketIndex = bucketIndex; + auto dataSizeInBytes = data.Length * sizeof(T); + auto hash = XXH64(data.Pointer, dataSizeInBytes, SYSTEM_DICTIONARY_HASH_SEED); + auto bucketIndex = (int32_t)(hash % storage->Buckets.Length); - return result; + return { hash, bucketIndex }; } template TValue& SystemDictionary::operator[](TKey key) { - return *SystemGetDictionaryValue(*this, key); + auto value = SystemGetDictionaryValue(*this, key); + + if (value != nullptr) + { + return *value; + } + + static thread_local TValue defaultValue = {}; + defaultValue = {}; + return defaultValue; } template SystemDictionary SystemCreateDictionary(MemoryArena memoryArena, size_t maxItemsCount) { - auto storage = SystemPushStruct>(memoryArena); + if (maxItemsCount > INT32_MAX) + { + return {}; + } + + auto storage = SystemPushStructZero>(memoryArena); + + if (storage == nullptr) + { + return {}; + } + storage->MemoryArena = memoryArena; storage->Buckets = SystemPushArray(memoryArena, maxItemsCount); + if (maxItemsCount > 0 && storage->Buckets.Pointer == nullptr) + { + return {}; + } + for (size_t i = 0; i < storage->Buckets.Length; i++) { storage->Buckets[i] = SYSTEM_DICTIONARY_INDEX_EMPTY; } storage->Entries = SystemPushArray>(memoryArena, maxItemsCount, AllocationState_Reserved); - storage->CurrentEntryIndex = 0; + + if (maxItemsCount > 0 && storage->Entries.Pointer == nullptr) + { + return {}; + } + storage->FreeListIndex = SYSTEM_DICTIONARY_INDEX_EMPTY; SystemDictionary result = {}; @@ -368,81 +373,42 @@ template bool SystemDictionaryContainsKey(SystemDictionary dictionary, TKey key) { auto hashInfo = DictionaryComputeHashInfo(dictionary.Storage, ReadOnlySpan((uint8_t*)&key, sizeof(key))); - auto entryIndex = GetDictionaryEntryIndexInfo(dictionary.Storage, hashInfo); - - return entryIndex.Index != SYSTEM_DICTIONARY_INDEX_EMPTY; + return ContainsDictionaryValue(dictionary.Storage, hashInfo); } template bool SystemDictionaryContainsKey(SystemDictionary, TValue> dictionary, ReadOnlySpan key) { auto hashInfo = DictionaryComputeHashInfo(dictionary.Storage, key); - auto entryIndex = GetDictionaryEntryIndexInfo(dictionary.Storage, hashInfo); - - return entryIndex.Index != SYSTEM_DICTIONARY_INDEX_EMPTY; + return ContainsDictionaryValue(dictionary.Storage, hashInfo); } template bool SystemDictionaryContainsKey(SystemDictionary, TValue> dictionary, ReadOnlySpan key) { auto hashInfo = DictionaryComputeHashInfo(dictionary.Storage, key); - auto entryIndex = GetDictionaryEntryIndexInfo(dictionary.Storage, hashInfo); - - return entryIndex.Index != SYSTEM_DICTIONARY_INDEX_EMPTY; + return ContainsDictionaryValue(dictionary.Storage, hashInfo); } template void SystemDebugDictionary(SystemDictionary dictionary) { - auto stackMemoryArena = SystemGetStackMemoryArena(); + #ifdef ElemAPI auto storage = dictionary.Storage; - for (size_t i = 0; i < storage->Buckets.Length; i++) + if (storage == nullptr) { - if (storage->Buckets[i] == SYSTEM_DICTIONARY_INDEX_EMPTY) - { - #ifdef ElemAPI - SystemLogDebugMessage(ElemLogMessageCategory_Application, "Bucket %u => (EMPTY)", i); - #endif - } - else - { - auto debugMessage = SystemFormatString(stackMemoryArena, "Bucket %u", i); - - auto entryIndex = storage->Buckets[i]; - - while (entryIndex != SYSTEM_DICTIONARY_INDEX_EMPTY) - { - auto entryIndexFull = GetDictionaryEntryIndexFull(entryIndex); - auto entry = storage->Partitions[entryIndexFull.PartitionIndex]->Entries[entryIndexFull.Index]; - - if (entry.Hash == 0) - { - debugMessage = SystemConcatBuffers(stackMemoryArena, debugMessage, " => (PREV_REMOVED)"); - break; - } - - debugMessage = SystemConcatBuffers(stackMemoryArena, debugMessage, SystemFormatString(stackMemoryArena, " => %u (Value: %d, Partition: %d, Index: %d)", entry.Hash, entry.Value, entryIndexFull.PartitionIndex, entryIndexFull.Index)); - entryIndex = entry.Next; - } - - #ifdef ElemAPI - SystemLogDebugMessage(ElemLogMessageCategory_Application, "%s", debugMessage.Pointer); - #endif - } + return; } - auto currentFreeListIndex = storage->FreeListIndex; - auto debugMessage = ReadOnlySpan("FreeList"); + LockSystemDictionary(storage); - while (currentFreeListIndex != SYSTEM_DICTIONARY_INDEX_EMPTY) + for (size_t i = 0; i < storage->Buckets.Length; i++) { - auto indexFull = GetDictionaryEntryIndexFull(currentFreeListIndex); - debugMessage = SystemConcatBuffers(stackMemoryArena, debugMessage, SystemFormatString(stackMemoryArena, " => Partition: %d, Index: %d", indexFull.PartitionIndex, indexFull.Index)); - currentFreeListIndex = storage->Partitions[indexFull.PartitionIndex]->Entries[indexFull.Index].Next; + auto entryIndex = storage->Buckets[i]; + SystemLogDebugMessage(ElemLogMessageCategory_Application, "Bucket %u => %d", (uint32_t)i, entryIndex); } - #ifdef ElemAPI - SystemLogDebugMessage(ElemLogMessageCategory_Application, "%s", debugMessage.Pointer); + UnlockSystemDictionary(storage); #endif } From d4dbec015d11adde5d49cb3bdcb87d5ff1164ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:13:20 +0200 Subject: [PATCH 28/60] Add dictionary robustness regressions --- .../DictionaryRobustnessTests.cpp | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/FoundationsTests/DictionaryRobustnessTests.cpp diff --git a/tests/FoundationsTests/DictionaryRobustnessTests.cpp b/tests/FoundationsTests/DictionaryRobustnessTests.cpp new file mode 100644 index 00000000..0333a44d --- /dev/null +++ b/tests/FoundationsTests/DictionaryRobustnessTests.cpp @@ -0,0 +1,150 @@ +#include "SystemDictionary.h" +#include "SystemFunctions.h" +#include "utest.h" + +struct DictionaryConcurrentAddOneParameter +{ + SystemDictionary Dictionary; + int32_t Key; +}; + +struct DictionaryConcurrentReuseParameter +{ + SystemDictionary Dictionary; + int64_t CurrentKey; + uint32_t ThreadId; + uint32_t IterationCount; +}; + +void DictionaryConcurrentAddOneFunction(void* parameter) +{ + auto threadParameter = (DictionaryConcurrentAddOneParameter*)parameter; + SystemAddDictionaryEntry(threadParameter->Dictionary, threadParameter->Key, threadParameter->Key); +} + +void DictionaryConcurrentReuseFunction(void* parameter) +{ + auto threadParameter = (DictionaryConcurrentReuseParameter*)parameter; + + for (uint32_t i = 0; i < threadParameter->IterationCount; i++) + { + SystemRemoveDictionaryEntry(threadParameter->Dictionary, threadParameter->CurrentKey); + + auto key = (int64_t)threadParameter->ThreadId * 1000000 + i + 1; + auto value = ((uint64_t)threadParameter->ThreadId << 32) | i; + SystemAddDictionaryEntry(threadParameter->Dictionary, key, value); + threadParameter->CurrentKey = key; + } +} + +UTEST(DictionaryRobustness, ReadOnlySpanHashUsesAllBytes) +{ + // Arrange + auto stackMemoryArena = SystemGetStackMemoryArena(); + auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 8); + uint32_t key1[] = { 0x00001234, 1 }; + uint32_t key2[] = { 0x00001234, 2 }; + + // Act + SystemAddDictionaryEntry(dictionary, ReadOnlySpan(key1, 2), 10); + SystemAddDictionaryEntry(dictionary, ReadOnlySpan(key2, 2), 20); + + // Assert + auto value1 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key1, 2)); + auto value2 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key2, 2)); + ASSERT_TRUE(value1 != nullptr); + ASSERT_TRUE(value2 != nullptr); + ASSERT_EQ(10, *value1); + ASSERT_EQ(20, *value2); +} + +UTEST(DictionaryRobustness, MissingValueReturnsNull) +{ + // Arrange + auto stackMemoryArena = SystemGetStackMemoryArena(); + auto dictionary = SystemCreateDictionary(stackMemoryArena, 8); + + // Act + auto value = SystemGetDictionaryValue(dictionary, 42); + + // Assert + ASSERT_TRUE(value == nullptr); + ASSERT_EQ(0, dictionary[42]); +} + +UTEST(DictionaryRobustness, ConcurrentAddStopsAtCapacity) +{ + // Arrange + const int32_t threadCount = 32; + const int32_t capacity = 8; + auto memoryArena = SystemAllocateMemoryArena(); + auto dictionary = SystemCreateDictionary(memoryArena, capacity); + SystemThread threads[threadCount]; + DictionaryConcurrentAddOneParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dictionary, i }; + threads[i] = SystemCreateThread(DictionaryConcurrentAddOneFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto foundCount = 0; + + for (int32_t i = 0; i < threadCount; i++) + { + if (SystemDictionaryContainsKey(dictionary, i)) + { + auto value = SystemGetDictionaryValue(dictionary, i); + ASSERT_TRUE(value != nullptr); + ASSERT_EQ(i, *value); + foundCount++; + } + } + + ASSERT_EQ(capacity, foundCount); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DictionaryRobustness, ConcurrentReusePreservesAllEntries) +{ + // Arrange + const int32_t threadCount = 16; + const uint32_t iterationCount = 5000; + auto memoryArena = SystemAllocateMemoryArena(); + auto dictionary = SystemCreateDictionary(memoryArena, threadCount); + SystemThread threads[threadCount]; + DictionaryConcurrentReuseParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + auto initialKey = -(int64_t)i - 1; + SystemAddDictionaryEntry(dictionary, initialKey, (uint64_t)i); + threadParameters[i] = { dictionary, initialKey, (uint32_t)i, iterationCount }; + threads[i] = SystemCreateThread(DictionaryConcurrentReuseFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + for (int32_t i = 0; i < threadCount; i++) + { + auto value = SystemGetDictionaryValue(dictionary, threadParameters[i].CurrentKey); + ASSERT_TRUE(value != nullptr); + ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), *value); + } + + SystemFreeMemoryArena(memoryArena); +} From b7d9aeb9f19976005e8db0da8ae911f2af226224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:13:29 +0200 Subject: [PATCH 29/60] Run dictionary robustness regressions --- tests/FoundationsTests/UnityBuild.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/FoundationsTests/UnityBuild.cpp b/tests/FoundationsTests/UnityBuild.cpp index 721189dc..82d081cf 100644 --- a/tests/FoundationsTests/UnityBuild.cpp +++ b/tests/FoundationsTests/UnityBuild.cpp @@ -8,6 +8,7 @@ #include "IOTests.cpp" #include "LibraryProcessTests.cpp" #include "DictionaryTests.cpp" +#include "DictionaryRobustnessTests.cpp" #include "DataPoolTests.cpp" #include "DataPoolRobustnessTests.cpp" From d96df7bae8645158630744a8ead0552df218d913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:14:14 +0200 Subject: [PATCH 30/60] Document data pool concurrency contract --- src/Foundations/SystemDataPool.h | 121 +++++++++++++++---------------- 1 file changed, 57 insertions(+), 64 deletions(-) diff --git a/src/Foundations/SystemDataPool.h b/src/Foundations/SystemDataPool.h index 1cf9c058..1db6e1cd 100644 --- a/src/Foundations/SystemDataPool.h +++ b/src/Foundations/SystemDataPool.h @@ -4,7 +4,7 @@ #include "SystemMemory.h" /** - * Represents an handle of a data pool decomposed into index and version. + * Represents a packed data-pool handle decomposed into index and version. */ struct SystemDataPoolHandle { @@ -13,118 +13,111 @@ struct SystemDataPoolHandle }; /** - * A default full structure used as a fallback for the SystemDataPool template when no full data type is specified. + * Default full-data type used when a data pool has no secondary data payload. */ struct SystemDataPoolDefaultFull { }; -/** - * Forward declaration of the SystemDataPoolStorage structure. - * This structure is meant to internally manage storage specifics of a data pool, but its implementation details are abstracted away from the user. - */ template struct SystemDataPoolStorage; /** - * Represents a data pool with specific item types and optionally, a fuller version of each item. - * This structure allows for the organization and efficient management of a collection of items of type T, - * with the option to extend each item with additional data of type TFull. - * - * @tparam T The primary type of items stored in the data pool. - * @tparam TFull The full data type associated with each item, providing additional information or properties. + * Lightweight handle to a fixed-capacity data pool. + * + * Data-pool add, remove, lookup, and count operations are thread-safe for a pool created from a + * shared MemoryArena. Index allocation and recycling are synchronized internally, while lookups + * validate the item generation without taking the allocation lock. + * + * A pointer returned by SystemGetDataPoolItem() or SystemGetDataPoolItemFull() does not pin the + * item. The caller must guarantee that the same item is not removed or reused while that pointer + * is being dereferenced. StackMemoryArena-backed pools remain subject to StackMemoryArena's + * thread-local contract. + * + * @tparam T Primary item type. + * @tparam TFull Optional secondary item type. */ template struct SystemDataPool { - SystemDataPoolStorage* Storage; ///< Pointer to the underlying storage mechanism of the data pool. + SystemDataPoolStorage* Storage; }; /** -* Unpack a data pool handle to a struct that contains the index and the version -* @param packedValue The packed value to unpack. -* @return The unpacked handle. -*/ + * Unpacks a data-pool handle into its index and generation. + * + * @param packedValue Packed handle value. + * @return Unpacked index and generation. + */ SystemDataPoolHandle UnpackSystemDataPoolHandle(uint64_t packedValue); /** - * Creates and initializes a data pool capable of storing items of type T, with an optional fuller version of each item of type TFull. - * - * @tparam T The primary type of items to be stored in the data pool. - * @tparam TFull The full data type associated with each item, defaulting to SystemDataPoolDefaultFull when not specified. - * @param memoryArena The memory arena to use for allocating data pool storage. - * @param maxItems The maximum number of items that the data pool can hold. - * @return An instance of SystemDataPool configured to store items of type T and TFull. + * Creates a fixed-capacity data pool. + * + * The storage is allocated from memoryArena and is not individually freed. maxItems must fit in the + * 32-bit handle index space. The returned pool is empty when its backing storage cannot be created. + * + * @tparam T Primary item type. + * @tparam TFull Optional secondary item type. + * @param memoryArena Arena that owns the pool storage. + * @param maxItems Maximum number of simultaneously allocated items. + * @return Data pool backed by memoryArena, or an empty pool on allocation failure. */ template SystemDataPool SystemCreateDataPool(MemoryArena memoryArena, size_t maxItems); /** - * Adds an item of type T to the specified data pool and returns a handle to the newly added item. - * - * @tparam T The type of the item to add to the data pool. - * @tparam TFull The full data type associated with each item in the pool. - * @param dataPool The data pool to which the item will be added. - * @param data The item to add to the data pool. - * @return A handle to the newly added item within the data pool. + * Adds an item to the pool. + * + * The operation is thread-safe. A recycled slot receives the generation established by its previous + * removal, so stale handles do not resolve to the new item. + * + * @return Handle to the added item, or ELEM_HANDLE_NULL when the pool is full. */ template ElemHandle SystemAddDataPoolItem(SystemDataPool dataPool, T data); /** - * Adds or updates the fuller version of an item in the data pool, identified by a given handle. - * - * @tparam T The primary type of items stored in the data pool. - * @tparam TFull The full data type associated with each item. - * @param dataPool The data pool containing the item. - * @param handle The handle identifying the item to be extended with fuller data. - * @param data The fuller version of the item to add or update in the data pool. + * Writes the optional secondary data associated with an existing item. + * + * The handle generation is validated before the write. This function does not pin the item after + * validation; the caller must not remove or reuse the same item concurrently with this write. */ template void SystemAddDataPoolItemFull(SystemDataPool dataPool, ElemHandle handle, TFull data); /** - * Removes an item from the data pool, identified by a given handle. - * - * @tparam T The primary type of items stored in the data pool. - * @tparam TFull The full data type associated with each item. - * @param dataPool The data pool from which the item will be removed. - * @param handle The handle identifying the item to remove. + * Removes an item and makes its slot available for reuse. + * + * The operation is thread-safe. Concurrent attempts to remove the same generation only free the + * slot once; later attempts observe the generation change and are ignored. */ template void SystemRemoveDataPoolItem(SystemDataPool dataPool, ElemHandle handle); /** - * Retrieves a pointer to an item in the data pool, identified by a given handle. - * - * @tparam T The primary type of items stored in the data pool. - * @tparam TFull The full data type associated with each item. - * @param dataPool The data pool containing the item. - * @param handle The handle identifying the item to retrieve. - * @return A pointer to the item associated with the given handle, or nullptr if the item does not exist. + * Resolves a handle to its primary item. + * + * The lookup itself is thread-safe and returns nullptr for a stale handle. The returned pointer is + * non-owning and is not lifetime-protected against a later concurrent removal/reuse of the same item. */ template T* SystemGetDataPoolItem(SystemDataPool dataPool, ElemHandle handle); /** - * Retrieves a pointer to the fuller version of an item in the data pool, identified by a given handle. - * - * @tparam T The primary type of items stored in the data pool. - * @tparam TFull The full data type associated with each item. - * @param dataPool The data pool containing the item. - * @param handle The handle identifying the item to retrieve its fuller version. - * @return A pointer to the fuller version of the item associated with the given handle, or nullptr if the fuller data does not exist. + * Resolves a handle to its secondary item data. + * + * The lookup itself is thread-safe and returns nullptr for a stale handle. The returned pointer is + * non-owning and is not lifetime-protected against a later concurrent removal/reuse of the same item. */ template TFull* SystemGetDataPoolItemFull(SystemDataPool dataPool, ElemHandle handle); /** - * Counts the number of items in the data pool. - * - * @tparam T Primary type of items in the data pool. - * @tparam TFull Full data type associated with each item. - * @param dataPool The data pool whose items are to be counted. - * @return The total count of items in the data pool. + * Returns the current number of live items. + * + * The count is read atomically and may change immediately after the function returns when the pool + * is being modified concurrently. */ template size_t SystemGetDataPoolItemCount(SystemDataPool dataPool); From 40c9fd7a9b193ca28e2e9733567dde945607f500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:14:29 +0200 Subject: [PATCH 31/60] Document dictionary concurrency contract --- src/Foundations/SystemDictionary.h | 151 +++++++++-------------------- 1 file changed, 44 insertions(+), 107 deletions(-) diff --git a/src/Foundations/SystemDictionary.h b/src/Foundations/SystemDictionary.h index 8f648972..e3a9f851 100644 --- a/src/Foundations/SystemDictionary.h +++ b/src/Foundations/SystemDictionary.h @@ -2,177 +2,114 @@ #include "SystemMemory.h" -/** - * Template structure for dictionary storage, specialized by value type. - */ template struct SystemDictionaryStorage; /** - * A dictionary data structure template, mapping keys of type TKey to values of type TValue. - * - * @tparam TKey The type of the keys. - * @tparam TValue The type of the values. + * Fixed-capacity hash dictionary backed by a MemoryArena. + * + * Dictionary add, remove, lookup, and contains operations are thread-safe for a dictionary created + * from a shared MemoryArena. Internal bucket traversal and entry recycling are synchronized so an + * entry is never published before its hash/value are initialized and a removed slot is not reused + * while another dictionary operation is traversing it. + * + * A pointer returned by SystemGetDictionaryValue(), or a reference returned by operator[], does not + * pin the entry after the lookup operation completes. The caller must guarantee that the same + * dictionary entry is not removed or reused while such a pointer/reference is being dereferenced. + * StackMemoryArena-backed dictionaries remain subject to StackMemoryArena's thread-local contract. + * + * @tparam TKey Key type. + * @tparam TValue Value type. */ template struct SystemDictionary { - SystemDictionaryStorage* Storage; ///< Pointer to the storage structure of the dictionary. + SystemDictionaryStorage* Storage; /** - * Overloads the [] operator to access values associated with a given key. - * @param key The key of the value to access. - * @return Reference to the value associated with the key. + * Returns the value associated with key. + * + * When the key is missing, a zero-initialized thread-local fallback value is returned. This + * operator is lookup-only; assigning through that fallback does not insert a dictionary entry. + * The returned reference follows the same lifetime rule as SystemGetDictionaryValue(). */ TValue& operator[](TKey key); }; /** - * Creates a dictionary with a specified maximum number of items. - * - * @tparam TKey The type of the keys. - * @tparam TValue The type of the values. - * @param memoryArena The memory arena where the dictionary is to be allocated. - * @param maxItemsCount The maximum number of items the dictionary can hold. - * @return A SystemDictionary instance. + * Creates a fixed-capacity dictionary in memoryArena. + * + * maxItemsCount must fit in the signed 32-bit internal index space. The returned dictionary is empty + * when its backing storage cannot be allocated. */ template SystemDictionary SystemCreateDictionary(MemoryArena memoryArena, size_t maxItemsCount); /** - * Adds a new entry to the specified dictionary. - * @tparam TKey The type of the keys. - * @tparam TValue The type of the values. - * @param dictionary The dictionary to which the entry is to be added. - * @param key The key for the new entry. - * @param value The value for the new entry. + * Adds a dictionary entry. + * + * The operation is thread-safe. The dictionary stores a 64-bit hash of the key rather than a copy of + * the key itself. */ template void SystemAddDictionaryEntry(SystemDictionary dictionary, TKey key, TValue value); -/** - * Adds a new entry to the specified dictionary. - * @tparam TValue The type of the values. - * @param dictionary The dictionary to which the entry is to be added. - * @param key The key for the new entry. - * @param value The value for the new entry. - */ template void SystemAddDictionaryEntry(SystemDictionary, TValue> dictionary, ReadOnlySpan key, TValue value); -/** - * Adds a new entry to the specified dictionary. - * @tparam TValue The type of the values. - * @tparam T The type of the key element. - * @param dictionary The dictionary to which the entry is to be added. - * @param key The key for the new entry. - * @param value The value for the new entry. - */ template void SystemAddDictionaryEntry(SystemDictionary, TValue> dictionary, ReadOnlySpan key, TValue value); /** - * Removes an entry from the specified dictionary. - * @tparam TKey The type of the keys. - * @tparam TValue The type of the values. - * @param dictionary The dictionary from which the entry is to be removed. - * @param key The key of the entry to remove. + * Removes the first entry matching the key hash and recycles its storage. + * + * The operation is thread-safe with other dictionary operations. Pointers/references previously + * returned for the removed entry must no longer be used. */ template void SystemRemoveDictionaryEntry(SystemDictionary dictionary, TKey key); -/** - * Removes an entry from the specified dictionary. - * @tparam TValue The type of the values. - * @param dictionary The dictionary from which the entry is to be removed. - * @param key The key of the entry to remove. - */ template void SystemRemoveDictionaryEntry(SystemDictionary, TValue> dictionary, ReadOnlySpan key); -/** - * Removes an entry from the specified dictionary. - * @tparam TValue The type of the values. - * @tparam T The type of the key element. - * @param dictionary The dictionary from which the entry is to be removed. - * @param key The key of the entry to remove. - */ template void SystemRemoveDictionaryEntry(SystemDictionary, TValue> dictionary, ReadOnlySpan key); /** - * Retrieves the value for a specified key from the dictionary, if it exists. - * @tparam TKey The type of the keys. - * @tparam TValue The type of the values. - * @param dictionary The dictionary from which to retrieve the value. - * @param key The key of the value to retrieve. - * @return Pointer to the value, or nullptr if the key does not exist. + * Resolves a key to its value. + * + * The lookup operation is thread-safe. The returned pointer is non-owning and does not protect the + * entry from a later concurrent removal/reuse; callers retaining the pointer must synchronize that + * entry's lifetime themselves. + * + * @return Pointer to the value, or nullptr when the key is not present. */ template TValue* SystemGetDictionaryValue(SystemDictionary dictionary, TKey key); -/** - * Retrieves the value for a specified key from the dictionary, if it exists. - * @tparam TValue The type of the values. - * @param dictionary The dictionary from which to retrieve the value. - * @param key The key of the value to retrieve. - * @return Pointer to the value, or nullptr if the key does not exist. - */ template TValue* SystemGetDictionaryValue(SystemDictionary, TValue> dictionary, ReadOnlySpan key); -/** - * Retrieves the value for a specified key from the dictionary, if it exists. - * @tparam TValue The type of the values. - * @tparam T The type of the key element. - * @param dictionary The dictionary from which to retrieve the value. - * @param key The key of the value to retrieve. - * @return Pointer to the value, or nullptr if the key does not exist. - */ template TValue* SystemGetDictionaryValue(SystemDictionary, TValue> dictionary, ReadOnlySpan key); /** - * Checks if the dictionary contains a given key. - * @tparam TKey The type of the keys. - * @tparam TValue The type of the values. - * @param dictionary The dictionary to check. - * @param key The key to look for. - * @return True if the key exists in the dictionary, false otherwise. + * Tests whether a key is present. + * + * The operation is thread-safe. The result is a snapshot and may become stale immediately after + * return when the dictionary is modified concurrently. */ template bool SystemDictionaryContainsKey(SystemDictionary dictionary, TKey key); -/** - * Checks if the dictionary contains a given key. - * @tparam TValue The type of the values. - * @param dictionary The dictionary to check. - * @param key The key to look for. - * @return True if the key exists in the dictionary, false otherwise. - */ template bool SystemDictionaryContainsKey(SystemDictionary, TValue> dictionary, ReadOnlySpan key); -/** - * Checks if the dictionary contains a given key. - * @tparam TValue The type of the values. - * @tparam T The type of the key element. - * @param dictionary The dictionary to check. - * @param key The key to look for. - * @return True if the key exists in the dictionary, false otherwise. - */ template bool SystemDictionaryContainsKey(SystemDictionary, TValue> dictionary, ReadOnlySpan key); /** - * Prints debug information for a given dictionary. This function is useful for - * development and debugging purposes to inspect the contents and state of the dictionary. - * It outputs key-value pairs, the structure of the storage, and other relevant information - * that aids in understanding the dictionary's current state. - * - * @tparam TKey The type of the keys in the dictionary. - * @tparam TValue The type of the values in the dictionary. - * @param dictionary The dictionary to debug. + * Logs the current bucket heads for debugging. */ template void SystemDebugDictionary(SystemDictionary dictionary); From a2c75565325dbc735248b23266f3b83ff4a469a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:17:50 +0200 Subject: [PATCH 32/60] Document memory commit failure contract --- src/Foundations/SystemMemory.h | 223 ++++++--------------------------- 1 file changed, 37 insertions(+), 186 deletions(-) diff --git a/src/Foundations/SystemMemory.h b/src/Foundations/SystemMemory.h index df71ce8a..1832e7b7 100644 --- a/src/Foundations/SystemMemory.h +++ b/src/Foundations/SystemMemory.h @@ -2,99 +2,41 @@ #include "SystemSpan.h" -// TODO: Get rid of all mallocs and new -// TODO: Maybe we should expose the memory arena to the outside world? - -//#define new NOT_IMPLEMENTED() - -//#define malloc(size) NOT_IMPLEMENTED(size) -//#define calloc(count, size) NOT_IMPLEMENTED(count, size) -//#define free(pointer) NOT_IMPLEMENTED(pointer) - -//#define new NOT_IMPLEMENTED - +struct MemoryArenaStorage; -/** - * Defines whether an arena allocation is immediately backed by committed memory. - */ enum AllocationState { - AllocationState_Committed, ///< The allocated range is committed and can be accessed immediately. - AllocationState_Reserved ///< The allocated range is reserved only and must be committed before it is accessed. + AllocationState_Committed, + AllocationState_Reserved }; -/** - * Provides process-wide virtual memory allocation information reported by the platform layer. - */ struct AllocationInfos { - size_t CommittedBytes; ///< Total number of committed bytes. - size_t ReservedBytes; ///< Total number of reserved virtual-address bytes. + size_t CommittedBytes; + size_t ReservedBytes; }; -struct MemoryArenaStorage; - -/** - * Lightweight handle to a MemoryArena storage. - * - * MemoryArena is intentionally passed and copied by value. Copying a MemoryArena does not copy - * its allocations or storage; every copy references the same MemoryArenaStorage. No ownership, - * reference counting, or lifetime tracking is added by the handle. - * - * For regular arenas, Level is 0. For handles produced by StackMemoryArena, Level identifies the - * stack lifetime associated with that handle and allows an ancestor arena to be passed down the - * call tree while preserving the ancestor allocation lifetime. - * - * The caller is responsible for respecting the lifetime of the referenced storage. Freeing an - * arena invalidates every MemoryArena value and every allocation that references that storage. - */ struct MemoryArena { - MemoryArenaStorage* Storage; ///< Shared internal storage referenced by this handle. - uint8_t Level; ///< Stack lifetime level, or 0 for a regular arena. + MemoryArenaStorage* Storage; + uint8_t Level; }; -/** - * Provides allocation information for a MemoryArena. - */ struct MemoryArenaAllocationInfos { - size_t AllocatedBytes; ///< Bytes currently allocated from the arena data region. - size_t CommittedBytes; ///< Bytes currently committed by the arena, including its internal header pages. - size_t MaximumSizeInBytes; ///< Maximum number of data bytes that can be allocated from the arena. + size_t AllocatedBytes; + size_t CommittedBytes; + size_t MaximumSizeInBytes; }; -/** - * Scoped thread-local MemoryArena. - * - * Destroying the StackMemoryArena releases allocations associated with its stack lifetime. The - * contained MemoryArena can be passed by value to deeper functions while this scope is alive. - * Passing a MemoryArena from an ancestor scope allows a deeper function to allocate data that - * survives its local stack scopes and is released with that ancestor. - * - * StackMemoryArena is thread-local and must not be shared across threads. A MemoryArena obtained - * from it must not be retained after the corresponding StackMemoryArena scope has ended. - * - * StackMemoryArena itself represents a scope and must not be copied by user code. Copying the - * contained MemoryArena handle is the intended way to pass an allocation lifetime around. - */ struct StackMemoryArena { - MemoryArena Arena; ///< MemoryArena handle associated with this stack scope. + MemoryArena Arena; + size_t StartOffsetInBytes; + size_t StartExtraOffsetInBytes; - size_t StartOffsetInBytes; ///< Internal data offset captured when the scope begins. - size_t StartExtraOffsetInBytes; ///< Internal ancestor-lifetime storage offset captured when the scope begins. - - /** - * Releases allocations owned by this stack scope and restores the previous stack lifetime. - */ ~StackMemoryArena(); - /** - * Returns the lightweight MemoryArena handle associated with this stack scope. - * - * @return MemoryArena value that can be passed to allocation functions while this scope is alive. - */ operator MemoryArena() const { return Arena; @@ -115,7 +57,8 @@ AllocationInfos SystemGetAllocationInfos(); * The returned MemoryArena is a lightweight value handle to the allocated storage. The caller is * responsible for releasing that storage exactly once with SystemFreeMemoryArena(). * - * @return MemoryArena handle referencing the newly allocated storage. + * @return MemoryArena handle referencing the newly allocated storage, or an empty handle when the + * platform reservation/header commitment cannot be created. */ MemoryArena SystemAllocateMemoryArena(); @@ -127,7 +70,8 @@ MemoryArena SystemAllocateMemoryArena(); * The returned MemoryArena can be copied freely, but all copies reference the same storage. * * @param sizeInBytes Maximum number of data bytes that can be allocated from the arena. - * @return MemoryArena handle referencing the newly allocated storage. + * @return MemoryArena handle referencing the newly allocated storage, or an empty handle when the + * requested size cannot be represented or the platform allocation fails. */ MemoryArena SystemAllocateMemoryArena(size_t sizeInBytes); @@ -181,15 +125,19 @@ StackMemoryArena SystemGetStackMemoryArena(); * Allocates a contiguous range of bytes from a MemoryArena. * * The allocation advances the arena and is not individually freed. Regular shared MemoryArena - * allocation is intended to be thread-safe; StackMemoryArena allocation is thread-local. + * allocation is thread-safe; StackMemoryArena allocation is thread-local. * * A committed allocation can be accessed immediately. A reserved allocation only reserves its * range in the arena and must be committed with SystemCommitMemory() before access. * + * When a committed allocation cannot be committed by the platform, nullptr is returned. For a + * regular shared arena the logical reservation remains consumed because rolling back a concurrent + * bump allocation would be unsafe after another thread may have reserved a later range. + * * @param memoryArena MemoryArena that provides the allocation lifetime. * @param sizeInBytes Number of bytes to allocate. * @param state Initial allocation state. - * @return Pointer to the allocated range, or nullptr if the arena cannot satisfy the allocation. + * @return Pointer to the allocated range, or nullptr if the arena cannot satisfy/commit it. */ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationState state = AllocationState_Committed); @@ -198,32 +146,34 @@ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationSt * * The range must belong to the specified arena. Commitment is tracked at platform page granularity, * so pages shared by multiple logical ranges remain committed while any tracked range still needs - * them. The operation is intended to be thread-safe for regular shared MemoryArena instances. + * them. The operation is thread-safe for regular shared MemoryArena instances. * - * If clearMemory is true, pages that are newly committed by this operation are cleared before use. - * Use SystemPushMemoryZero() when the exact returned allocation range must be initialized to zero. + * If clearMemory is true, pages newly committed by this operation are cleared before use. Use + * SystemPushMemoryZero() when the exact returned allocation range must be initialized to zero. + * + * A platform failure may occur after earlier pages in the requested range were successfully + * committed. In that case those successfully committed pages remain tracked and true is not + * returned; a later call may retry the remaining pages. * * @param memoryArena MemoryArena containing the range. * @param pointer Start of the range to commit. * @param sizeInBytes Number of bytes in the range. * @param clearMemory Whether newly committed pages should be cleared. + * @return true when every page covering the requested range is committed; otherwise false. */ -void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory = false); +bool SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory = false); /** * Commits the pages covering a previously allocated buffer in a MemoryArena. * - * The buffer must reference memory allocated from the specified arena. Commitment is tracked at - * platform page granularity. - * * @tparam T Element type stored in the buffer. * @param memoryArena MemoryArena containing the buffer. * @param buffer Buffer whose memory range will be committed. * @param clearMemory Whether newly committed pages should be cleared. + * @return true when every page covering the requested buffer is committed; otherwise false. */ template -void SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory = false); - +bool SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory = false); /** * Decommits pages that are no longer needed by a range in a MemoryArena. @@ -233,8 +183,8 @@ void SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool cl * arena bookkeeping determines that no remaining committed range still needs that page. * * The caller is responsible for passing a valid range belonging to the arena and for not accessing - * the range while it is decommitted. The operation is intended to be thread-safe for regular shared - * MemoryArena instances. + * the range while it is decommitted. The operation is thread-safe for regular shared MemoryArena + * instances. * * @param memoryArena MemoryArena containing the range. * @param pointer Start of the range to decommit. @@ -247,7 +197,7 @@ void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInB * * @param memoryArena MemoryArena that provides the allocation lifetime. * @param sizeInBytes Number of bytes to allocate and clear. - * @return Pointer to the allocated range, or nullptr if the arena cannot satisfy the allocation. + * @return Pointer to the allocated range, or nullptr if the arena cannot satisfy/commit it. */ void* SystemPushMemoryZero(MemoryArena memoryArena, size_t sizeInBytes); @@ -256,147 +206,48 @@ void* SystemPushMemoryZero(MemoryArena memoryArena, size_t sizeInBytes); * * The returned Span references arena-owned memory and remains valid only for the lifetime of the * corresponding arena allocation context. - * - * @tparam T Element type to allocate. - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param count Number of elements to allocate. - * @param state Initial allocation state. - * @return Span referencing the allocated array. */ template Span SystemPushArray(MemoryArena memoryArena, size_t count, AllocationState state = AllocationState_Committed); /** * Allocates a contiguous array and initializes it to zero. - * - * @tparam T Element type to allocate. - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param count Number of elements to allocate and clear. - * @return Span referencing the zero-initialized array. */ template Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** * Allocates a zero-initialized char array with an additional zero terminator after the returned Span. - * - * The terminator is allocated immediately after the requested elements and is not included in the - * returned Span length. - * - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param count Number of char elements in the returned Span. - * @return Span referencing the requested zero-initialized char elements. */ template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** * Allocates a zero-initialized wchar_t array with an additional zero terminator after the returned Span. - * - * The terminator is allocated immediately after the requested elements and is not included in the - * returned Span length. - * - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param count Number of wchar_t elements in the returned Span. - * @return Span referencing the requested zero-initialized wchar_t elements. */ template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); -/** - * Allocates storage for one structure from a MemoryArena. - * - * No constructor is invoked; this is raw arena allocation for T. - * - * @tparam T Structure type to allocate. - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @return Pointer to the allocated storage, or nullptr if the arena cannot satisfy the allocation. - */ template T* SystemPushStruct(MemoryArena memoryArena); -/** - * Allocates zero-initialized storage for one structure from a MemoryArena. - * - * No constructor is invoked; this is raw zeroed arena allocation for T. - * - * @tparam T Structure type to allocate. - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @return Pointer to the zero-initialized storage, or nullptr if the arena cannot satisfy the allocation. - */ template T* SystemPushStructZero(MemoryArena memoryArena); -/** - * Copies all source elements into an existing destination buffer. - * - * The destination must contain at least source.Length elements. No allocation is performed. - * - * @tparam T Element type stored in the buffers. - * @param destination Destination buffer. - * @param source Source buffer to copy. - */ template void SystemCopyBuffer(Span destination, ReadOnlySpan source); -/** - * Allocates a new buffer in a MemoryArena and copies the source elements into it. - * - * @tparam T Element type stored in the buffer. - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param source Source buffer to duplicate. - * @return Span referencing the newly allocated copy. - */ template Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source); -/** - * Allocates a new char buffer in a MemoryArena and copies the source into it. - * - * The char specialization preserves zero-initialized storage after the copied data so the result can - * be used by code that expects a zero-terminated character sequence. - * - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param source Source character buffer to duplicate. - * @return Span referencing the newly allocated copy. - */ template<> Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source); -/** - * Allocates a buffer containing the concatenation of two source buffers. - * - * @tparam T Element type stored in the buffers. - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param buffer1 First source buffer. - * @param buffer2 Second source buffer. - * @return Span referencing the concatenated buffer. - */ template Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); -/** - * Allocates a char buffer containing the concatenation of two source buffers. - * - * The specialization allocates an additional zero terminator after the returned Span. - * - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param buffer1 First source character buffer. - * @param buffer2 Second source character buffer. - * @return Span referencing the concatenated characters, excluding the trailing terminator. - */ template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); -/** - * Allocates a wchar_t buffer containing the concatenation of two source buffers. - * - * The specialization allocates an additional zero terminator after the returned Span. - * - * @param memoryArena MemoryArena that provides the allocation lifetime. - * @param buffer1 First source wide-character buffer. - * @param buffer2 Second source wide-character buffer. - * @return Span referencing the concatenated characters, excluding the trailing terminator. - */ template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); From 7dc6b3227878ff3d0ea0eb093961e0d6460a7c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:19:32 +0200 Subject: [PATCH 33/60] Propagate memory commit failures --- src/Foundations/SystemMemory.cpp | 36 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Foundations/SystemMemory.cpp b/src/Foundations/SystemMemory.cpp index 5c8f4320..af0a1f91 100644 --- a/src/Foundations/SystemMemory.cpp +++ b/src/Foundations/SystemMemory.cpp @@ -427,23 +427,28 @@ StackMemoryArena::~StackMemoryArena() } template -void SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory) +bool SystemCommitMemory(MemoryArena memoryArena, ReadOnlySpan buffer, bool clearMemory) { size_t sizeInBytes; if (!TryMultiplySize(sizeof(T), buffer.Length, &sizeInBytes)) { - return; + return false; } - SystemCommitMemory(memoryArena, (void*)buffer.Pointer, sizeInBytes, clearMemory); + return SystemCommitMemory(memoryArena, (void*)buffer.Pointer, sizeInBytes, clearMemory); } -void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory) +bool SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes, bool clearMemory) { - if (memoryArena.Storage == nullptr || pointer == nullptr || sizeInBytes == 0) + if (sizeInBytes == 0) { - return; + return true; + } + + if (memoryArena.Storage == nullptr || pointer == nullptr) + { + return false; } auto storage = memoryArena.Storage; @@ -452,14 +457,14 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt if (pointerAddress < dataStart) { - return; + return false; } auto offset = (size_t)(pointerAddress - dataStart); if (offset > storage->SizeInBytes || sizeInBytes > storage->SizeInBytes - offset) { - return; + return false; } auto needsSynchronization = !IsStackMemoryArena(memoryArena); @@ -493,7 +498,7 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt UnlockMemoryArenaCommitOperations(storage); } - return; + return true; } auto pageSizeInBytes = GetSystemPageSizeInBytes(); @@ -513,7 +518,7 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt UnlockMemoryArenaCommitOperations(storage); } - return; + return false; } if (clearMemory) @@ -538,6 +543,8 @@ void SystemCommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInByt { UnlockMemoryArenaCommitOperations(storage); } + + return true; } void SystemDecommitMemory(MemoryArena memoryArena, void* pointer, size_t sizeInBytes) @@ -694,9 +701,14 @@ void* SystemPushMemory(MemoryArena memoryArena, size_t sizeInBytes, AllocationSt } } - if (state == AllocationState_Committed) + if (state == AllocationState_Committed && !SystemCommitMemory(workingMemoryArena, pointer, sizeInBytes)) { - SystemCommitMemory(workingMemoryArena, pointer, sizeInBytes); + if (IsStackMemoryArena(workingMemoryArena)) + { + storage->CurrentPointer -= sizeInBytes; + } + + return nullptr; } return pointer; From 495afc134d55b0686025fa6a82dc51437cbaeec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:20:15 +0200 Subject: [PATCH 34/60] Handle data pool commit failures --- src/Foundations/SystemDataPool.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Foundations/SystemDataPool.cpp b/src/Foundations/SystemDataPool.cpp index 96494eb0..fcda7fc4 100644 --- a/src/Foundations/SystemDataPool.cpp +++ b/src/Foundations/SystemDataPool.cpp @@ -159,11 +159,17 @@ ElemHandle SystemAddDataPoolItem(SystemDataPool dataPool, T data) { auto remainingItemCount = storage->Data.Length - index; auto itemCountToCommit = remainingItemCount > 1000 ? 1000 : remainingItemCount; - SystemCommitMemory>(storage->MemoryArena, storage->Data.Slice(index, itemCountToCommit), true); + + if (!SystemCommitMemory>(storage->MemoryArena, storage->Data.Slice(index, itemCountToCommit), true)) + { + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot commit Data Pool item storage."); + return ELEM_HANDLE_NULL; + } - if (!IsTypeEmpty()) + if (!IsTypeEmpty() && !SystemCommitMemory(storage->MemoryArena, storage->DataFull.Slice(index, itemCountToCommit), true)) { - SystemCommitMemory(storage->MemoryArena, storage->DataFull.Slice(index, itemCountToCommit), true); + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot commit Data Pool full item storage."); + return ELEM_HANDLE_NULL; } } From 563620011b5108fcd854bcb302e91dd1ffd237de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:20:53 +0200 Subject: [PATCH 35/60] Handle dictionary commit failures --- src/Foundations/SystemDictionary.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Foundations/SystemDictionary.cpp b/src/Foundations/SystemDictionary.cpp index 47273baf..3272df95 100644 --- a/src/Foundations/SystemDictionary.cpp +++ b/src/Foundations/SystemDictionary.cpp @@ -147,7 +147,16 @@ void AddDictionaryEntry(SystemDictionaryStorage* storage, SystemDictiona entryIndex = (int32_t)storage->CurrentEntryIndex; storage->CurrentEntryIndex++; - SystemCommitMemory>(storage->MemoryArena, storage->Entries.Slice(entryIndex, 1), true); + + if (!SystemCommitMemory>(storage->MemoryArena, storage->Entries.Slice(entryIndex, 1), true)) + { + UnlockSystemDictionary(storage); + + #ifdef ElemAPI + SystemLogErrorMessage(ElemLogMessageCategory_Memory, "Cannot commit dictionary entry storage."); + #endif + return; + } } auto entry = GetDictionaryEntryByIndex(storage, entryIndex); From 873f7cf78679c840e2b04746442ef9ae2958782d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:21:27 +0200 Subject: [PATCH 36/60] Test memory commit result contract --- .../MemoryRobustnessTests.cpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/FoundationsTests/MemoryRobustnessTests.cpp b/tests/FoundationsTests/MemoryRobustnessTests.cpp index 0234aa1f..c3df79bf 100644 --- a/tests/FoundationsTests/MemoryRobustnessTests.cpp +++ b/tests/FoundationsTests/MemoryRobustnessTests.cpp @@ -40,6 +40,28 @@ UTEST(MemoryRobustness, PushSizeOverflowDoesNotAdvanceArena) ASSERT_EQ(0llu, SystemGetMemoryArenaAllocationInfos(memoryArena).AllocatedBytes); } +UTEST(MemoryRobustness, CommitReportsInvalidRange) +{ + // Arrange + auto memoryArena = SystemAllocateMemoryArena(64); + auto allocation = SystemPushArray(memoryArena, 64, AllocationState_Reserved); + + // Act + auto validCommit = SystemCommitMemory(memoryArena, allocation, true); + auto invalidCommit = SystemCommitMemory(memoryArena, allocation.Pointer + allocation.Length, 8); + + // Assert + ASSERT_TRUE(validCommit); + ASSERT_FALSE(invalidCommit); + + for (size_t i = 0; i < allocation.Length; i++) + { + ASSERT_EQ(0, allocation[i]); + } + + SystemFreeMemoryArena(memoryArena); +} + UTEST(MemoryRobustness, ConcurrentArenaAllocationAccounting) { // Arrange From bb5073b66ba66a95becaae45011ebd392a41fd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sat, 5 Sep 2026 12:23:36 +0200 Subject: [PATCH 37/60] Fix commit result regression call --- tests/FoundationsTests/MemoryRobustnessTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FoundationsTests/MemoryRobustnessTests.cpp b/tests/FoundationsTests/MemoryRobustnessTests.cpp index c3df79bf..4143c08d 100644 --- a/tests/FoundationsTests/MemoryRobustnessTests.cpp +++ b/tests/FoundationsTests/MemoryRobustnessTests.cpp @@ -47,7 +47,7 @@ UTEST(MemoryRobustness, CommitReportsInvalidRange) auto allocation = SystemPushArray(memoryArena, 64, AllocationState_Reserved); // Act - auto validCommit = SystemCommitMemory(memoryArena, allocation, true); + auto validCommit = SystemCommitMemory(memoryArena, allocation.Pointer, allocation.Length, true); auto invalidCommit = SystemCommitMemory(memoryArena, allocation.Pointer + allocation.Length, 8); // Assert From a585f8cec07f1b3dc55934789b5f170aa6ee5c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:02:51 +0200 Subject: [PATCH 38/60] Document remaining SystemMemory helpers --- src/Foundations/SystemMemory.h | 113 +++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/src/Foundations/SystemMemory.h b/src/Foundations/SystemMemory.h index 1832e7b7..a4bbc809 100644 --- a/src/Foundations/SystemMemory.h +++ b/src/Foundations/SystemMemory.h @@ -205,49 +205,154 @@ void* SystemPushMemoryZero(MemoryArena memoryArena, size_t sizeInBytes); * Allocates a contiguous array from a MemoryArena. * * The returned Span references arena-owned memory and remains valid only for the lifetime of the - * corresponding arena allocation context. + * corresponding arena allocation context. Element storage is not initialized by this helper. + * + * @tparam T Element type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Number of elements to allocate. + * @param state Initial allocation state for the underlying memory. + * @return Span referencing the allocated array, or an empty Span when the requested size overflows + * or the arena cannot satisfy/commit the allocation. */ template Span SystemPushArray(MemoryArena memoryArena, size_t count, AllocationState state = AllocationState_Committed); /** - * Allocates a contiguous array and initializes it to zero. + * Allocates a contiguous array from a MemoryArena and initializes its storage to zero. + * + * @tparam T Element type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Number of elements to allocate and clear. + * @return Span referencing the zero-initialized array, or an empty Span when the requested size + * overflows or the arena cannot satisfy/commit the allocation. */ template Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** - * Allocates a zero-initialized char array with an additional zero terminator after the returned Span. + * Allocates a zero-initialized char array with an additional null terminator after the returned Span. + * + * The returned Span Length is exactly count and excludes the terminator. The backing allocation + * contains count + 1 bytes so Pointer can be consumed by APIs expecting a null-terminated string. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Logical number of characters in the returned Span. + * @return Span referencing count zero-initialized characters, or an empty Span on failure. */ template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); /** - * Allocates a zero-initialized wchar_t array with an additional zero terminator after the returned Span. + * Allocates a zero-initialized wchar_t array with an additional null terminator after the returned Span. + * + * The returned Span Length is exactly count and excludes the terminator. The backing allocation + * contains count + 1 wchar_t elements. + * + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @param count Logical number of wide characters in the returned Span. + * @return Span referencing count zero-initialized wide characters, or an empty Span on failure. */ template<> Span SystemPushArrayZero(MemoryArena memoryArena, size_t count); +/** + * Allocates storage for one object from a MemoryArena without initializing it. + * + * @tparam T Object type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @return Pointer to arena-owned storage for one T, or nullptr when the allocation cannot be + * satisfied/committed. + */ template T* SystemPushStruct(MemoryArena memoryArena); +/** + * Allocates storage for one object from a MemoryArena and initializes its bytes to zero. + * + * This is raw zero-initialization of the allocated storage; constructors are not invoked. + * + * @tparam T Object type to allocate. + * @param memoryArena MemoryArena that provides the allocation lifetime. + * @return Pointer to zero-initialized arena-owned storage for one T, or nullptr on failure. + */ template T* SystemPushStructZero(MemoryArena memoryArena); +/** + * Copies all elements from a source buffer into an existing destination buffer. + * + * The destination must contain at least source.Length elements. When it is smaller, the function + * logs an error and does not perform a partial copy. + * + * @tparam T Element type of both buffers. + * @param destination Writable destination buffer. + * @param source Source buffer to copy. + */ template void SystemCopyBuffer(Span destination, ReadOnlySpan source); +/** + * Allocates a new buffer from a MemoryArena and copies the source elements into it. + * + * @tparam T Element type of the source and destination buffers. + * @param memoryArena MemoryArena that provides the duplicated buffer lifetime. + * @param source Buffer to duplicate. + * @return Span referencing the copied elements, or an empty Span when allocation fails. + */ template Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source); +/** + * Duplicates a character buffer and appends a null terminator in the backing allocation. + * + * The returned Span preserves source.Length exactly; the terminator is stored immediately after + * the logical Span and is not included in Length. + * + * @param memoryArena MemoryArena that provides the duplicated buffer lifetime. + * @param source Character buffer to duplicate. + * @return Span containing a copy of source with a trailing null terminator, or an empty Span on + * allocation failure. + */ template<> Span SystemDuplicateBuffer(MemoryArena memoryArena, ReadOnlySpan source); +/** + * Allocates a new buffer containing buffer1 immediately followed by buffer2. + * + * @tparam T Element type of both input buffers. + * @param memoryArena MemoryArena that provides the concatenated buffer lifetime. + * @param buffer1 First buffer in the result. + * @param buffer2 Second buffer in the result. + * @return Span whose Length is buffer1.Length + buffer2.Length, or an empty Span when the combined + * length overflows or allocation fails. + */ template Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); +/** + * Concatenates two character buffers and appends a null terminator in the backing allocation. + * + * The returned Span Length is the sum of the two logical input lengths and excludes the terminator. + * + * @param memoryArena MemoryArena that provides the concatenated buffer lifetime. + * @param buffer1 First character buffer in the result. + * @param buffer2 Second character buffer in the result. + * @return Null-terminated concatenated character Span, or an empty Span on overflow/allocation + * failure. + */ template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); +/** + * Concatenates two wide-character buffers and appends a null terminator in the backing allocation. + * + * The returned Span Length is the sum of the two logical input lengths and excludes the terminator. + * + * @param memoryArena MemoryArena that provides the concatenated buffer lifetime. + * @param buffer1 First wide-character buffer in the result. + * @param buffer2 Second wide-character buffer in the result. + * @return Null-terminated concatenated wide-character Span, or an empty Span on + * overflow/allocation failure. + */ template<> Span SystemConcatBuffers(MemoryArena memoryArena, ReadOnlySpan buffer1, ReadOnlySpan buffer2); From 748cde3edfbb37f7ba792ff24e3b1b8a87e22f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:03:15 +0200 Subject: [PATCH 39/60] Separate memory concurrency tests --- .../MemoryConcurrentTests.cpp | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 tests/FoundationsTests/MemoryConcurrentTests.cpp diff --git a/tests/FoundationsTests/MemoryConcurrentTests.cpp b/tests/FoundationsTests/MemoryConcurrentTests.cpp new file mode 100644 index 00000000..4851b2ff --- /dev/null +++ b/tests/FoundationsTests/MemoryConcurrentTests.cpp @@ -0,0 +1,238 @@ +#include "SystemFunctions.h" +#include "SystemMemory.h" +#include "SystemPlatformFunctions.h" +#include "utest.h" + +struct MemoryConcurrentPushParameter +{ + MemoryArena MemoryArena; + int32_t ItemCount; +}; + +struct MemoryConcurrentOverflowParameter +{ + MemoryArena MemoryArena; + bool* Start; + void** Results; + int32_t ThreadId; +}; + +struct MemoryConcurrentCommitParameter +{ + MemoryArena MemoryArena; + uint8_t* Pointer; + size_t SizeInBytes; + uint8_t Value; +}; + +struct MemoryConcurrentArenaAllocationParameter +{ + MemoryArena* Result; + size_t SizeInBytes; +}; + +void MemoryConcurrentPushFunction(void* parameter) +{ + auto threadParameter = (MemoryConcurrentPushParameter*)parameter; + + for (int32_t i = 0; i < threadParameter->ItemCount; i++) + { + SystemPushMemoryZero(threadParameter->MemoryArena, 64); + } +} + +void MemoryConcurrentOverflowFunction(void* parameter) +{ + auto threadParameter = (MemoryConcurrentOverflowParameter*)parameter; + bool start = false; + + while (!start) + { + SystemAtomicLoad(*threadParameter->Start, start); + + if (!start) + { + SystemYieldThread(); + } + } + + threadParameter->Results[threadParameter->ThreadId] = SystemPushMemory(threadParameter->MemoryArena, 64, AllocationState_Reserved); +} + +void MemoryConcurrentCommitFunction(void* parameter) +{ + auto threadParameter = (MemoryConcurrentCommitParameter*)parameter; + SystemCommitMemory(threadParameter->MemoryArena, threadParameter->Pointer, threadParameter->SizeInBytes); + + for (size_t i = 0; i < threadParameter->SizeInBytes; i++) + { + threadParameter->Pointer[i] = threadParameter->Value; + } +} + +void MemoryConcurrentArenaAllocationFunction(void* parameter) +{ + auto threadParameter = (MemoryConcurrentArenaAllocationParameter*)parameter; + *threadParameter->Result = SystemAllocateMemoryArena(threadParameter->SizeInBytes); +} + +UTEST(MemoryConcurrent, Push) +{ + // Arrange + const int32_t itemCount = 80000; + const int32_t threadCount = 32; + auto maxSize = (size_t)itemCount * 64; + auto memoryArena = SystemAllocateMemoryArena(maxSize); + SystemThread threads[threadCount]; + MemoryConcurrentPushParameter threadParameters[threadCount]; + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { memoryArena, itemCount / threadCount }; + threads[i] = SystemCreateThread(MemoryConcurrentPushFunction, &threadParameters[i]); + } + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); + ASSERT_EQ(maxSize, allocationInfos.AllocatedBytes); +} + +UTEST(MemoryConcurrent, PushDoesNotOverflow) +{ + // Arrange + const int32_t threadCount = 32; + const int32_t capacityCount = 8; + const size_t allocationSizeInBytes = 64; + auto memoryArena = SystemAllocateMemoryArena(capacityCount * allocationSizeInBytes); + bool start = false; + void* results[threadCount] = {}; + SystemThread threads[threadCount]; + MemoryConcurrentOverflowParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { memoryArena, &start, results, i }; + threads[i] = SystemCreateThread(MemoryConcurrentOverflowFunction, &threadParameters[i]); + } + + // Act + SystemAtomicStore(start, true); + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto successCount = 0; + + for (int32_t i = 0; i < threadCount; i++) + { + if (results[i] != nullptr) + { + successCount++; + + for (int32_t j = i + 1; j < threadCount; j++) + { + if (results[j] != nullptr) + { + ASSERT_TRUE(results[i] != results[j]); + } + } + } + } + + ASSERT_EQ(capacityCount, successCount); + + auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); + ASSERT_EQ(capacityCount * allocationSizeInBytes, allocationInfos.AllocatedBytes); +} + +UTEST(MemoryConcurrent, CommitSharedPage) +{ + // Arrange + const int32_t threadCount = 32; + const size_t rangeSizeInBytes = 64; + auto pageSizeInBytes = SystemPlatformGetPageSize(); + auto memoryArena = SystemAllocateMemoryArena(pageSizeInBytes); + auto buffer = SystemPushArray(memoryArena, pageSizeInBytes, AllocationState_Reserved); + auto committedBytesBefore = SystemGetMemoryArenaAllocationInfos(memoryArena).CommittedBytes; + SystemThread threads[threadCount]; + MemoryConcurrentCommitParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { memoryArena, buffer.Pointer + i * rangeSizeInBytes, rangeSizeInBytes, (uint8_t)(i + 1) }; + threads[i] = SystemCreateThread(MemoryConcurrentCommitFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); + ASSERT_EQ(committedBytesBefore + pageSizeInBytes, allocationInfos.CommittedBytes); + + for (int32_t i = 0; i < threadCount; i++) + { + for (size_t j = 0; j < rangeSizeInBytes; j++) + { + ASSERT_EQ((uint8_t)(i + 1), buffer[i * rangeSizeInBytes + j]); + } + } +} + +UTEST(MemoryConcurrent, ArenaAllocationAccounting) +{ + // Arrange + const int32_t threadCount = 16; + auto pageSizeInBytes = SystemPlatformGetPageSize(); + auto allocationInfosBefore = SystemGetAllocationInfos(); + MemoryArena memoryArenas[threadCount] = {}; + SystemThread threads[threadCount]; + MemoryConcurrentArenaAllocationParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { &memoryArenas[i], pageSizeInBytes }; + threads[i] = SystemCreateThread(MemoryConcurrentArenaAllocationFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + for (int32_t i = 0; i < threadCount; i++) + { + ASSERT_TRUE(memoryArenas[i].Storage != nullptr); + } + + auto allocationInfosAfterAllocate = SystemGetAllocationInfos(); + ASSERT_EQ(allocationInfosBefore.ReservedBytes + threadCount * pageSizeInBytes * 2, allocationInfosAfterAllocate.ReservedBytes); + ASSERT_EQ(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterAllocate.CommittedBytes); + + for (int32_t i = 0; i < threadCount; i++) + { + SystemFreeMemoryArena(memoryArenas[i]); + } + + auto allocationInfosAfterFree = SystemGetAllocationInfos(); + ASSERT_EQ(allocationInfosBefore.ReservedBytes, allocationInfosAfterFree.ReservedBytes); + ASSERT_EQ(allocationInfosBefore.CommittedBytes, allocationInfosAfterFree.CommittedBytes); +} From 9ea9f88b2bff00cc2c0ae14fc51847066f68fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:03:48 +0200 Subject: [PATCH 40/60] Separate data pool concurrency tests --- .../DataPoolConcurrentTests.cpp | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 tests/FoundationsTests/DataPoolConcurrentTests.cpp diff --git a/tests/FoundationsTests/DataPoolConcurrentTests.cpp b/tests/FoundationsTests/DataPoolConcurrentTests.cpp new file mode 100644 index 00000000..231ed772 --- /dev/null +++ b/tests/FoundationsTests/DataPoolConcurrentTests.cpp @@ -0,0 +1,342 @@ +#include "SystemDataPool.h" +#include "SystemFunctions.h" +#include "utest.h" + +struct DataPoolConcurrentTestData +{ + uint64_t Value; +}; + +struct DataPoolConcurrentTestDataFull +{ + uint64_t Value1; + uint64_t Value2; + uint64_t Value3; + uint64_t Value4; +}; + +struct DataPoolConcurrentBatchParameter +{ + SystemDataPool DataPool; + int32_t ThreadId; + int32_t ItemCount; + Span Handles; +}; + +struct DataPoolConcurrentAddOneParameter +{ + SystemDataPool DataPool; + ElemHandle* Result; + uint64_t Value; +}; + +struct DataPoolConcurrentRemoveOneParameter +{ + SystemDataPool DataPool; + ElemHandle Handle; +}; + +struct DataPoolConcurrentReuseParameter +{ + SystemDataPool DataPool; + ElemHandle Handle; + uint32_t ThreadId; + uint32_t IterationCount; + bool Failed; +}; + +void DataPoolConcurrentBatchAddFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentBatchParameter*)parameter; + auto dataPool = threadParameter->DataPool; + + for (int32_t i = 0; i < threadParameter->ItemCount; i++) + { + auto value = (uint64_t)threadParameter->ThreadId * threadParameter->ItemCount + i; + DataPoolConcurrentTestData data = { value }; + DataPoolConcurrentTestDataFull dataFull = { value, value + 1, value + 2, value + 3 }; + + auto handle = SystemAddDataPoolItem(dataPool, data); + SystemAddDataPoolItemFull(dataPool, handle, dataFull); + threadParameter->Handles[i] = handle; + } +} + +void DataPoolConcurrentBatchRemoveFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentBatchParameter*)parameter; + + for (int32_t i = 0; i < threadParameter->ItemCount; i++) + { + SystemRemoveDataPoolItem(threadParameter->DataPool, threadParameter->Handles[i]); + } +} + +void DataPoolConcurrentAddOneFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentAddOneParameter*)parameter; + *threadParameter->Result = SystemAddDataPoolItem(threadParameter->DataPool, DataPoolConcurrentTestData { threadParameter->Value }); +} + +void DataPoolConcurrentRemoveOneFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentRemoveOneParameter*)parameter; + SystemRemoveDataPoolItem(threadParameter->DataPool, threadParameter->Handle); +} + +void DataPoolConcurrentReuseFunction(void* parameter) +{ + auto threadParameter = (DataPoolConcurrentReuseParameter*)parameter; + auto handle = threadParameter->Handle; + + for (uint32_t i = 0; i < threadParameter->IterationCount; i++) + { + SystemRemoveDataPoolItem(threadParameter->DataPool, handle); + + auto value = ((uint64_t)threadParameter->ThreadId << 32) | i; + handle = SystemAddDataPoolItem(threadParameter->DataPool, DataPoolConcurrentTestData { value }); + + if (handle == ELEM_HANDLE_NULL) + { + threadParameter->Failed = true; + return; + } + } + + threadParameter->Handle = handle; +} + +UTEST(DataPoolConcurrent, Add) +{ + // Arrange + const int32_t itemCount = 80000; + const int32_t threadCount = 32; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, itemCount); + SystemThread threads[threadCount]; + DataPoolConcurrentBatchParameter threadParameters[threadCount]; + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i].DataPool = dataPool; + threadParameters[i].ItemCount = itemCount / threadCount; + threadParameters[i].ThreadId = i; + threadParameters[i].Handles = SystemPushArray(memoryArena, threadParameters[i].ItemCount); + threads[i] = SystemCreateThread(DataPoolConcurrentBatchAddFunction, &threadParameters[i]); + } + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + ASSERT_EQ(itemCount, (int32_t)SystemGetDataPoolItemCount(dataPool)); + + for (int32_t i = 0; i < threadCount; i++) + { + auto threadParameter = threadParameters[i]; + + for (int32_t j = 0; j < threadParameter.ItemCount; j++) + { + auto expectedValue = (uint64_t)threadParameter.ThreadId * threadParameter.ItemCount + j; + auto data = SystemGetDataPoolItem(dataPool, threadParameter.Handles[j]); + auto dataFull = SystemGetDataPoolItemFull(dataPool, threadParameter.Handles[j]); + + ASSERT_EQ(expectedValue, data->Value); + ASSERT_EQ(expectedValue, dataFull->Value1); + ASSERT_EQ(expectedValue + 1, dataFull->Value2); + ASSERT_EQ(expectedValue + 2, dataFull->Value3); + ASSERT_EQ(expectedValue + 3, dataFull->Value4); + } + } + + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DataPoolConcurrent, AddAndRemove) +{ + // Arrange + const int32_t itemCount = 80000; + const int32_t threadCount = 32; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, itemCount); + SystemThread addThreads[threadCount]; + SystemThread removeThreads[threadCount]; + DataPoolConcurrentBatchParameter firstParameters[threadCount]; + DataPoolConcurrentBatchParameter secondParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + firstParameters[i].DataPool = dataPool; + firstParameters[i].ItemCount = itemCount / 2 / threadCount; + firstParameters[i].ThreadId = i; + firstParameters[i].Handles = SystemPushArray(memoryArena, firstParameters[i].ItemCount); + addThreads[i] = SystemCreateThread(DataPoolConcurrentBatchAddFunction, &firstParameters[i]); + } + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(addThreads[i]); + SystemFreeThread(addThreads[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + secondParameters[i].DataPool = dataPool; + secondParameters[i].ItemCount = itemCount / 2 / threadCount; + secondParameters[i].ThreadId = i; + secondParameters[i].Handles = SystemPushArray(memoryArena, secondParameters[i].ItemCount); + + addThreads[i] = SystemCreateThread(DataPoolConcurrentBatchAddFunction, &secondParameters[i]); + removeThreads[i] = SystemCreateThread(DataPoolConcurrentBatchRemoveFunction, &firstParameters[i]); + } + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(addThreads[i]); + SystemWaitThread(removeThreads[i]); + SystemFreeThread(addThreads[i]); + SystemFreeThread(removeThreads[i]); + } + + // Assert + ASSERT_EQ((size_t)itemCount / 2, SystemGetDataPoolItemCount(dataPool)); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DataPoolConcurrent, AddStopsAtCapacity) +{ + // Arrange + const int32_t threadCount = 32; + const int32_t capacity = 8; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, capacity); + ElemHandle handles[threadCount] = {}; + SystemThread threads[threadCount]; + DataPoolConcurrentAddOneParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dataPool, &handles[i], (uint64_t)i }; + threads[i] = SystemCreateThread(DataPoolConcurrentAddOneFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto successCount = 0; + + for (int32_t i = 0; i < threadCount; i++) + { + if (handles[i] == ELEM_HANDLE_NULL) + { + continue; + } + + successCount++; + auto handleInfo = UnpackSystemDataPoolHandle(handles[i]); + + for (int32_t j = i + 1; j < threadCount; j++) + { + if (handles[j] != ELEM_HANDLE_NULL) + { + ASSERT_TRUE(handleInfo.Index != UnpackSystemDataPoolHandle(handles[j]).Index); + } + } + } + + ASSERT_EQ(capacity, successCount); + ASSERT_EQ((size_t)capacity, SystemGetDataPoolItemCount(dataPool)); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DataPoolConcurrent, RemoveSameHandleOnlyFreesOnce) +{ + // Arrange + const int32_t threadCount = 16; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, 1); + auto handle = SystemAddDataPoolItem(dataPool, DataPoolConcurrentTestData { 42 }); + SystemThread threads[threadCount]; + DataPoolConcurrentRemoveOneParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dataPool, handle }; + threads[i] = SystemCreateThread(DataPoolConcurrentRemoveOneFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + ASSERT_EQ(0llu, SystemGetDataPoolItemCount(dataPool)); + + auto reusedHandle = SystemAddDataPoolItem(dataPool, DataPoolConcurrentTestData { 100 }); + auto overflowHandle = SystemAddDataPoolItem(dataPool, DataPoolConcurrentTestData { 200 }); + ASSERT_TRUE(reusedHandle != ELEM_HANDLE_NULL); + ASSERT_TRUE(overflowHandle == ELEM_HANDLE_NULL); + ASSERT_EQ(1llu, SystemGetDataPoolItemCount(dataPool)); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DataPoolConcurrent, ReuseKeepsSlotsUnique) +{ + // Arrange + const int32_t threadCount = 16; + const uint32_t iterationCount = 5000; + auto memoryArena = SystemAllocateMemoryArena(); + auto dataPool = SystemCreateDataPool(memoryArena, threadCount); + SystemThread threads[threadCount]; + DataPoolConcurrentReuseParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + auto handle = SystemAddDataPoolItem(dataPool, DataPoolConcurrentTestData { (uint64_t)i }); + threadParameters[i] = { dataPool, handle, (uint32_t)i, iterationCount, false }; + threads[i] = SystemCreateThread(DataPoolConcurrentReuseFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + ASSERT_EQ((size_t)threadCount, SystemGetDataPoolItemCount(dataPool)); + + for (int32_t i = 0; i < threadCount; i++) + { + ASSERT_FALSE(threadParameters[i].Failed); + ASSERT_TRUE(threadParameters[i].Handle != ELEM_HANDLE_NULL); + + auto data = SystemGetDataPoolItem(dataPool, threadParameters[i].Handle); + ASSERT_TRUE(data != nullptr); + ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), data->Value); + + auto handleInfo = UnpackSystemDataPoolHandle(threadParameters[i].Handle); + + for (int32_t j = i + 1; j < threadCount; j++) + { + auto otherHandleInfo = UnpackSystemDataPoolHandle(threadParameters[j].Handle); + ASSERT_TRUE(handleInfo.Index != otherHandleInfo.Index); + } + } + + SystemFreeMemoryArena(memoryArena); +} From 0342fab8bce20ffb64861f1b661aa188d9d91414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:04:08 +0200 Subject: [PATCH 41/60] Separate dictionary concurrency tests --- .../DictionaryConcurrentTests.cpp | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/FoundationsTests/DictionaryConcurrentTests.cpp diff --git a/tests/FoundationsTests/DictionaryConcurrentTests.cpp b/tests/FoundationsTests/DictionaryConcurrentTests.cpp new file mode 100644 index 00000000..324f433c --- /dev/null +++ b/tests/FoundationsTests/DictionaryConcurrentTests.cpp @@ -0,0 +1,226 @@ +#include "SystemDictionary.h" +#include "SystemFunctions.h" +#include "utest.h" + +struct DictionaryConcurrentBatchParameter +{ + SystemDictionary Dictionary; + int32_t ThreadId; + int32_t ItemCount; +}; + +struct DictionaryConcurrentAddOneParameter +{ + SystemDictionary Dictionary; + int32_t Key; +}; + +struct DictionaryConcurrentReuseParameter +{ + SystemDictionary Dictionary; + int64_t CurrentKey; + uint32_t ThreadId; + uint32_t IterationCount; +}; + +void DictionaryConcurrentBatchAddFunction(void* parameter) +{ + auto threadParameter = (DictionaryConcurrentBatchParameter*)parameter; + + for (int32_t i = 0; i < threadParameter->ItemCount; i++) + { + auto key = threadParameter->ThreadId * threadParameter->ItemCount + i; + SystemAddDictionaryEntry(threadParameter->Dictionary, key, key); + } +} + +void DictionaryConcurrentBatchRemoveFunction(void* parameter) +{ + auto threadParameter = (DictionaryConcurrentBatchParameter*)parameter; + + for (int32_t i = 0; i < threadParameter->ItemCount; i++) + { + auto key = threadParameter->ThreadId * threadParameter->ItemCount + i; + SystemRemoveDictionaryEntry(threadParameter->Dictionary, key); + } +} + +void DictionaryConcurrentAddOneFunction(void* parameter) +{ + auto threadParameter = (DictionaryConcurrentAddOneParameter*)parameter; + SystemAddDictionaryEntry(threadParameter->Dictionary, threadParameter->Key, threadParameter->Key); +} + +void DictionaryConcurrentReuseFunction(void* parameter) +{ + auto threadParameter = (DictionaryConcurrentReuseParameter*)parameter; + + for (uint32_t i = 0; i < threadParameter->IterationCount; i++) + { + SystemRemoveDictionaryEntry(threadParameter->Dictionary, threadParameter->CurrentKey); + + auto key = (int64_t)threadParameter->ThreadId * 1000000 + i + 1; + auto value = ((uint64_t)threadParameter->ThreadId << 32) | i; + SystemAddDictionaryEntry(threadParameter->Dictionary, key, value); + threadParameter->CurrentKey = key; + } +} + +UTEST(DictionaryConcurrent, Add) +{ + // Arrange + const int32_t itemCount = 80000; + const int32_t threadCount = 32; + auto memoryArena = SystemAllocateMemoryArena(); + auto dictionary = SystemCreateDictionary(memoryArena, itemCount); + SystemThread threads[threadCount]; + DictionaryConcurrentBatchParameter threadParameters[threadCount]; + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dictionary, i, itemCount / threadCount }; + threads[i] = SystemCreateThread(DictionaryConcurrentBatchAddFunction, &threadParameters[i]); + } + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto count = 0; + + for (int32_t i = 0; i < itemCount; i++) + { + if (SystemDictionaryContainsKey(dictionary, i)) + { + count++; + } + } + + ASSERT_EQ(itemCount, count); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DictionaryConcurrent, Remove) +{ + // Arrange + const int32_t itemCount = 32000; + const int32_t threadCount = 32; + auto memoryArena = SystemAllocateMemoryArena(); + auto dictionary = SystemCreateDictionary(memoryArena, itemCount); + + for (int32_t i = 0; i < itemCount; i++) + { + SystemAddDictionaryEntry(dictionary, i, i); + } + + SystemThread threads[threadCount]; + DictionaryConcurrentBatchParameter threadParameters[threadCount]; + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dictionary, i, (itemCount / 2) / threadCount }; + threads[i] = SystemCreateThread(DictionaryConcurrentBatchRemoveFunction, &threadParameters[i]); + } + + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto count = 0; + + for (int32_t i = 0; i < itemCount; i++) + { + if (SystemDictionaryContainsKey(dictionary, i)) + { + count++; + } + } + + ASSERT_EQ(itemCount / 2, count); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DictionaryConcurrent, AddStopsAtCapacity) +{ + // Arrange + const int32_t threadCount = 32; + const int32_t capacity = 8; + auto memoryArena = SystemAllocateMemoryArena(); + auto dictionary = SystemCreateDictionary(memoryArena, capacity); + SystemThread threads[threadCount]; + DictionaryConcurrentAddOneParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + threadParameters[i] = { dictionary, i }; + threads[i] = SystemCreateThread(DictionaryConcurrentAddOneFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + auto foundCount = 0; + + for (int32_t i = 0; i < threadCount; i++) + { + if (SystemDictionaryContainsKey(dictionary, i)) + { + auto value = SystemGetDictionaryValue(dictionary, i); + ASSERT_TRUE(value != nullptr); + ASSERT_EQ(i, *value); + foundCount++; + } + } + + ASSERT_EQ(capacity, foundCount); + SystemFreeMemoryArena(memoryArena); +} + +UTEST(DictionaryConcurrent, ReusePreservesAllEntries) +{ + // Arrange + const int32_t threadCount = 16; + const uint32_t iterationCount = 5000; + auto memoryArena = SystemAllocateMemoryArena(); + auto dictionary = SystemCreateDictionary(memoryArena, threadCount); + SystemThread threads[threadCount]; + DictionaryConcurrentReuseParameter threadParameters[threadCount]; + + for (int32_t i = 0; i < threadCount; i++) + { + auto initialKey = -(int64_t)i - 1; + SystemAddDictionaryEntry(dictionary, initialKey, (uint64_t)i); + threadParameters[i] = { dictionary, initialKey, (uint32_t)i, iterationCount }; + threads[i] = SystemCreateThread(DictionaryConcurrentReuseFunction, &threadParameters[i]); + } + + // Act + for (int32_t i = 0; i < threadCount; i++) + { + SystemWaitThread(threads[i]); + SystemFreeThread(threads[i]); + } + + // Assert + for (int32_t i = 0; i < threadCount; i++) + { + auto value = SystemGetDictionaryValue(dictionary, threadParameters[i].CurrentKey); + ASSERT_TRUE(value != nullptr); + ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), *value); + } + + SystemFreeMemoryArena(memoryArena); +} From f98e132d190e4a966bb67674275998cce2deb671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:04:46 +0200 Subject: [PATCH 42/60] Keep memory tests focused on sequential behavior --- tests/FoundationsTests/MemoryTests.cpp | 271 ++++++------------------- 1 file changed, 67 insertions(+), 204 deletions(-) diff --git a/tests/FoundationsTests/MemoryTests.cpp b/tests/FoundationsTests/MemoryTests.cpp index 9995d96c..0b6b71b9 100644 --- a/tests/FoundationsTests/MemoryTests.cpp +++ b/tests/FoundationsTests/MemoryTests.cpp @@ -3,76 +3,14 @@ #include "SystemPlatformFunctions.h" #include "utest.h" -struct MemoryThreadParameter -{ - MemoryArena MemoryArena; - int32_t ThreadId; - int32_t ItemCount; -}; - -struct MemoryConcurrentOverflowThreadParameter -{ - MemoryArena MemoryArena; - bool* Start; - void** Results; - int32_t ThreadId; -}; - -struct MemoryConcurrentCommitThreadParameter -{ - MemoryArena MemoryArena; - uint8_t* Pointer; - size_t SizeInBytes; - uint8_t Value; -}; - -void MemoryConcurrentAddFunction(void* parameter) -{ - auto threadParameter = (MemoryThreadParameter*)parameter; - - for (int32_t i = 0; i < threadParameter->ItemCount; i++) - { - SystemPushMemoryZero(threadParameter->MemoryArena, 64); - } -} - -void MemoryConcurrentOverflowFunction(void* parameter) -{ - auto threadParameter = (MemoryConcurrentOverflowThreadParameter*)parameter; - bool start = false; - - while (!start) - { - SystemAtomicLoad(*threadParameter->Start, start); - - if (!start) - { - SystemYieldThread(); - } - } - - threadParameter->Results[threadParameter->ThreadId] = SystemPushMemory(threadParameter->MemoryArena, 64, AllocationState_Reserved); -} - -void MemoryConcurrentCommitFunction(void* parameter) -{ - auto threadParameter = (MemoryConcurrentCommitThreadParameter*)parameter; - SystemCommitMemory(threadParameter->MemoryArena, threadParameter->Pointer, threadParameter->SizeInBytes); - - for (size_t i = 0; i < threadParameter->SizeInBytes; i++) - { - threadParameter->Pointer[i] = threadParameter->Value; - } -} - -UTEST(Memory, Allocate) +UTEST(Memory, Allocate) { // Arrange auto memoryArena = SystemAllocateMemoryArena(); auto dataSizeInBytes = 70024llu; - + // Act - auto data = SystemPushArrayZero(memoryArena, dataSizeInBytes); + auto data = SystemPushArrayZero(memoryArena, dataSizeInBytes); // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); @@ -80,15 +18,15 @@ UTEST(Memory, Allocate) ASSERT_EQ(dataSizeInBytes, data.Length); } -UTEST(Memory, AllocateMultiple) +UTEST(Memory, AllocateMultiple) { // Arrange auto memoryArena = SystemAllocateMemoryArena(); auto dataSizeInBytes = 70024llu; - + // Act - SystemPushArrayZero(memoryArena, dataSizeInBytes); - SystemPushArrayZero(memoryArena, 1024); + SystemPushArrayZero(memoryArena, dataSizeInBytes); + SystemPushArrayZero(memoryArena, 1024); // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); @@ -113,16 +51,16 @@ UTEST(Memory, ClearMemoryArena) ASSERT_EQ(0llu, allocationInfos.AllocatedBytes); } -UTEST(Memory, AllocateCheckAlignement) +UTEST(Memory, AllocateCheckAlignment) { // Arrange auto memoryArena = SystemAllocateMemoryArena(); auto dataSizeInBytes = 70024llu; auto alignment = 8llu; - + // Act SystemPushArrayZero(memoryArena, 455); - auto data = SystemPushArrayZero(memoryArena, dataSizeInBytes); + auto data = SystemPushArrayZero(memoryArena, dataSizeInBytes); // Assert ASSERT_TRUE(((size_t)data.Pointer & (alignment - 1)) == 0); @@ -150,11 +88,58 @@ UTEST(Memory, PushOverflowReturnsNull) ASSERT_EQ(64llu, allocationInfos.AllocatedBytes); } +UTEST(Memory, ArenaSizeOverflowReturnsEmptyHandle) +{ + // Act + auto memoryArena = SystemAllocateMemoryArena(SIZE_MAX); + + // Assert + ASSERT_TRUE(memoryArena.Storage == nullptr); +} + +UTEST(Memory, PushSizeOverflowDoesNotAdvanceArena) +{ + // Arrange + auto memoryArena = SystemAllocateMemoryArena(64); + + // Act + auto allocation = SystemPushMemory(memoryArena, SIZE_MAX, AllocationState_Reserved); + auto array = SystemPushArray(memoryArena, SIZE_MAX / sizeof(uint64_t) + 1, AllocationState_Reserved); + + // Assert + ASSERT_TRUE(allocation == nullptr); + ASSERT_TRUE(array.Pointer == nullptr); + ASSERT_EQ(0llu, array.Length); + ASSERT_EQ(0llu, SystemGetMemoryArenaAllocationInfos(memoryArena).AllocatedBytes); +} + +UTEST(Memory, CommitReportsInvalidRange) +{ + // Arrange + auto memoryArena = SystemAllocateMemoryArena(64); + auto allocation = SystemPushArray(memoryArena, 64, AllocationState_Reserved); + + // Act + auto validCommit = SystemCommitMemory(memoryArena, allocation.Pointer, allocation.Length, true); + auto invalidCommit = SystemCommitMemory(memoryArena, allocation.Pointer + allocation.Length, 8); + + // Assert + ASSERT_TRUE(validCommit); + ASSERT_FALSE(invalidCommit); + + for (size_t i = 0; i < allocation.Length; i++) + { + ASSERT_EQ(0, allocation[i]); + } + + SystemFreeMemoryArena(memoryArena); +} + UTEST(Memory, ConcatBuffers) { // Arrange auto memoryArena = SystemAllocateMemoryArena(1024); - + // Act auto result = SystemConcatBuffers(memoryArena, "Test1", "Test2"); @@ -230,7 +215,7 @@ UTEST(Memory, StackMemoryArenaRelease) { auto stackMemoryArena2 = SystemGetStackMemoryArena(); string2 = SystemConcatBuffers(stackMemoryArena1, "Test2", "Stack1"); - + { auto stackMemoryArena3 = SystemGetStackMemoryArena(); SystemConcatBuffers(stackMemoryArena2, "Test", "Stack2"); @@ -245,11 +230,11 @@ UTEST(Memory, StackMemoryArenaRelease) SystemConcatBuffers(stackMemoryArena3, "Test", "Stack2"); SystemConcatBuffers(stackMemoryArena3, "Test", "Stack2"); } - + SystemConcatBuffers(stackMemoryArena4, "Test", "Stack2"); string5 = SystemConcatBuffers(memoryArenaPointer, "Test5", "Stack1"); } - + SystemConcatBuffers(stackMemoryArena2, "Test2", "Stack2"); string3 = SystemConcatBuffers(stackMemoryArena1, "Test3", "Stack1"); } @@ -285,133 +270,14 @@ UTEST(Memory, StackAncestorAllocationUsesExtraStorageCapacity) ASSERT_TRUE(ancestorAllocation != nullptr); } -UTEST(Memory, ConcurrentPush) -{ - // Arrange - const int32_t itemCount = 80000; - const int32_t threadCount = 32; - auto maxSize = (size_t)itemCount * 64; - auto memoryArena = SystemAllocateMemoryArena(maxSize); - - // Act - SystemThread threads[threadCount]; - MemoryThreadParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { memoryArena, i, itemCount / threadCount }; - threads[i] = SystemCreateThread(MemoryConcurrentAddFunction, &threadParameters[i]); - } - - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(maxSize, allocationInfos.AllocatedBytes); -} - -UTEST(Memory, ConcurrentPushDoesNotOverflow) -{ - // Arrange - const int32_t threadCount = 32; - const int32_t capacityCount = 8; - const size_t allocationSizeInBytes = 64; - auto memoryArena = SystemAllocateMemoryArena(capacityCount * allocationSizeInBytes); - bool start = false; - void* results[threadCount] = {}; - SystemThread threads[threadCount]; - MemoryConcurrentOverflowThreadParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { memoryArena, &start, results, i }; - threads[i] = SystemCreateThread(MemoryConcurrentOverflowFunction, &threadParameters[i]); - } - - // Act - SystemAtomicStore(start, true); - - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto successCount = 0; - - for (int32_t i = 0; i < threadCount; i++) - { - if (results[i] != nullptr) - { - successCount++; - - for (int32_t j = i + 1; j < threadCount; j++) - { - if (results[j] != nullptr) - { - ASSERT_TRUE(results[i] != results[j]); - } - } - } - } - - ASSERT_EQ(capacityCount, successCount); - - auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(capacityCount * allocationSizeInBytes, allocationInfos.AllocatedBytes); -} - -UTEST(Memory, ConcurrentCommitSharedPage) -{ - // Arrange - const int32_t threadCount = 32; - const size_t rangeSizeInBytes = 64; - auto pageSizeInBytes = SystemPlatformGetPageSize(); - auto memoryArena = SystemAllocateMemoryArena(pageSizeInBytes); - auto buffer = SystemPushArray(memoryArena, pageSizeInBytes, AllocationState_Reserved); - auto committedBytesBefore = SystemGetMemoryArenaAllocationInfos(memoryArena).CommittedBytes; - SystemThread threads[threadCount]; - MemoryConcurrentCommitThreadParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { memoryArena, buffer.Pointer + i * rangeSizeInBytes, rangeSizeInBytes, (uint8_t)(i + 1) }; - threads[i] = SystemCreateThread(MemoryConcurrentCommitFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(committedBytesBefore + pageSizeInBytes, allocationInfos.CommittedBytes); - - for (int32_t i = 0; i < threadCount; i++) - { - for (size_t j = 0; j < rangeSizeInBytes; j++) - { - ASSERT_EQ((uint8_t)(i + 1), buffer[i * rangeSizeInBytes + j]); - } - } -} - -UTEST(Memory, AllocateReserved) +UTEST(Memory, AllocateReserved) { // Arrange auto memoryArena = SystemAllocateMemoryArena(); auto dataSizeInBytes = 70024llu; - + // Act - SystemPushArray(memoryArena, dataSizeInBytes, AllocationState_Reserved); + SystemPushArray(memoryArena, dataSizeInBytes, AllocationState_Reserved); // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); @@ -419,7 +285,7 @@ UTEST(Memory, AllocateReserved) ASSERT_LT(allocationInfos.CommittedBytes, allocationInfos.MaximumSizeInBytes); } -UTEST(Memory, AllocateReservedCommit) +UTEST(Memory, AllocateReservedCommit) { // Arrange auto maxSizeInBytes = 4000000llu; @@ -427,9 +293,8 @@ UTEST(Memory, AllocateReservedCommit) auto offset = 150000llu; auto offset2 = 160000llu; auto bufferSize = 1024llu; - auto memoryArena = SystemAllocateMemoryArena(maxSizeInBytes); - + // Act auto array = SystemPushArray(memoryArena, dataSizeInBytes, AllocationState_Reserved); SystemCommitMemory(memoryArena, array.Pointer + offset, bufferSize); @@ -453,7 +318,7 @@ UTEST(Memory, AllocateReservedCommit) ASSERT_LT(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes); } -UTEST(Memory, AllocateReservedDecommit) +UTEST(Memory, AllocateReservedDecommit) { // Arrange auto maxSizeInBytes = 4000000llu; @@ -461,9 +326,7 @@ UTEST(Memory, AllocateReservedDecommit) auto offset = 150000llu; auto offset2 = 160000llu; auto bufferSize = 1024llu; - auto memoryArena = SystemAllocateMemoryArena(maxSizeInBytes); - auto array = SystemPushArray(memoryArena, dataSizeInBytes, AllocationState_Reserved); SystemCommitMemory(memoryArena, array.Pointer + offset, bufferSize); From 6c9fba8a7d81edd8028e22eb6e46c1d60cb9d1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:05:06 +0200 Subject: [PATCH 43/60] Keep data pool tests focused on sequential behavior --- tests/FoundationsTests/DataPoolTests.cpp | 158 +---------------------- 1 file changed, 6 insertions(+), 152 deletions(-) diff --git a/tests/FoundationsTests/DataPoolTests.cpp b/tests/FoundationsTests/DataPoolTests.cpp index 9ff2dc0b..40608f69 100644 --- a/tests/FoundationsTests/DataPoolTests.cpp +++ b/tests/FoundationsTests/DataPoolTests.cpp @@ -15,49 +15,7 @@ struct DataPoolTestDataFull uint64_t Data4; }; -struct DataPoolThreadParameter -{ - SystemDataPool DataPool; - int32_t ThreadId; - int32_t ItemCount; - Span Handles; -}; - -void DataPoolConcurrentAddFunction(void* parameter) -{ - auto threadParameter = (DataPoolThreadParameter*)parameter; - auto dataPool = threadParameter->DataPool; - - for (int32_t i = 0; i < threadParameter->ItemCount; i++) - { - DataPoolTestData testData = {}; - testData.Data = threadParameter->ThreadId * threadParameter->ItemCount + i; - - DataPoolTestDataFull testDataFull = {}; - testDataFull.Data1 = threadParameter->ThreadId * threadParameter->ItemCount + i; - testDataFull.Data2 = threadParameter->ThreadId * threadParameter->ItemCount + i + 1; - testDataFull.Data3 = threadParameter->ThreadId * threadParameter->ItemCount + i + 2; - testDataFull.Data4 = threadParameter->ThreadId * threadParameter->ItemCount + i + 3; - - auto handle = SystemAddDataPoolItem(dataPool, testData); - SystemAddDataPoolItemFull(dataPool, handle, testDataFull); - - threadParameter->Handles[i] = handle; - } -} - -void DataPoolConcurrentRemoveFunction(void* parameter) -{ - auto threadParameter = (DataPoolThreadParameter*)parameter; - auto dataPool = threadParameter->DataPool; - - for (int32_t i = 0; i < threadParameter->ItemCount; i++) - { - SystemRemoveDataPoolItem(dataPool, threadParameter->Handles[i]); - } -} - -UTEST(DataPool, AddItem) +UTEST(DataPool, AddItem) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -74,7 +32,7 @@ UTEST(DataPool, AddItem) ASSERT_EQ(testData.Data, result->Data); } -UTEST(DataPool, RemoveItem) +UTEST(DataPool, RemoveItem) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -91,7 +49,7 @@ UTEST(DataPool, RemoveItem) ASSERT_TRUE(result == nullptr); } -UTEST(DataPool, AddItemReuseDeletedItem) +UTEST(DataPool, AddItemReuseDeletedItem) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -112,7 +70,7 @@ UTEST(DataPool, AddItemReuseDeletedItem) ASSERT_EQ(testData.Data, result->Data); } -UTEST(DataPool, RemoveReusedItemWithOldVersion) +UTEST(DataPool, RemoveReusedItemWithOldVersion) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -135,7 +93,7 @@ UTEST(DataPool, RemoveReusedItemWithOldVersion) ASSERT_EQ(testData.Data, result->Data); } -UTEST(DataPool, AddItemWithFull) +UTEST(DataPool, AddItemWithFull) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -165,7 +123,7 @@ UTEST(DataPool, AddItemWithFull) ASSERT_EQ(testDataFull.Data4, resultFull->Data4); } -UTEST(DataPool, RemoveItemWithFull) +UTEST(DataPool, RemoveItemWithFull) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -190,107 +148,3 @@ UTEST(DataPool, RemoveItemWithFull) auto result = SystemGetDataPoolItemFull(dataPool, handle); ASSERT_TRUE(result == nullptr); } - -UTEST(DataPool, ConcurrentAdd) -{ - // Arrange - const int32_t itemCount = 80000; - const int32_t threadCount = 32; - auto memoryArena = SystemAllocateMemoryArena(); - auto dataPool = SystemCreateDataPool(memoryArena, itemCount); - - // Act - SystemThread threads[threadCount]; - DataPoolThreadParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i].DataPool = dataPool; - threadParameters[i].ItemCount = itemCount / threadCount; - threadParameters[i].ThreadId = i; - threadParameters[i].Handles = SystemPushArray(memoryArena, threadParameters[i].ItemCount); - - threads[i] = SystemCreateThread(DataPoolConcurrentAddFunction, &threadParameters[i]); - } - - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto dataPoolCount = SystemGetDataPoolItemCount(dataPool); - ASSERT_EQ(itemCount, (int32_t)dataPoolCount); - - for (int32_t i = 0; i < threadCount; i++) - { - auto threadParameter = threadParameters[i]; - - for (int32_t j = 0; j < itemCount / threadCount; j++) - { - auto testData = SystemGetDataPoolItem(dataPool, threadParameter.Handles[j]); - ASSERT_EQ((uint64_t)threadParameter.ThreadId * threadParameter.ItemCount + j, testData->Data); - - auto testDataFull = SystemGetDataPoolItemFull(dataPool, threadParameter.Handles[j]); - ASSERT_EQ((uint64_t)threadParameter.ThreadId * threadParameter.ItemCount + j, testDataFull->Data1); - ASSERT_EQ((uint64_t)threadParameter.ThreadId * threadParameter.ItemCount + j + 1, testDataFull->Data2); - ASSERT_EQ((uint64_t)threadParameter.ThreadId * threadParameter.ItemCount + j + 2, testDataFull->Data3); - ASSERT_EQ((uint64_t)threadParameter.ThreadId * threadParameter.ItemCount + j + 3, testDataFull->Data4); - } - } -} - -UTEST(DataPool, ConcurrentRemove) -{ - // Arrange - const int32_t itemCount = 80000; - const int32_t threadCount = 32; - auto memoryArena = SystemAllocateMemoryArena(); - auto dataPool = SystemCreateDataPool(memoryArena, itemCount); - - // Act - SystemThread threads[threadCount]; - SystemThread threads2[threadCount]; - DataPoolThreadParameter threadParameters[threadCount]; - DataPoolThreadParameter threadParameters2[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i].DataPool = dataPool; - threadParameters[i].ItemCount = itemCount / 2 / threadCount; - threadParameters[i].ThreadId = i; - threadParameters[i].Handles = SystemPushArray(memoryArena, threadParameters[i].ItemCount); - - threads[i] = SystemCreateThread(DataPoolConcurrentAddFunction, &threadParameters[i]); - } - - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters2[i].DataPool = dataPool; - threadParameters2[i].ItemCount = itemCount / 2 / threadCount; - threadParameters2[i].ThreadId = i; - threadParameters2[i].Handles = SystemPushArray(memoryArena, threadParameters2[i].ItemCount); - - threads2[i] = SystemCreateThread(DataPoolConcurrentAddFunction, &threadParameters2[i]); - threads[i] = SystemCreateThread(DataPoolConcurrentRemoveFunction, &threadParameters[i]); - } - - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemWaitThread(threads2[i]); - SystemFreeThread(threads[i]); - SystemFreeThread(threads2[i]); - } - - // Assert - auto dataPoolCount = SystemGetDataPoolItemCount(dataPool); - ASSERT_EQ((size_t)itemCount / 2, dataPoolCount); -} From 98ddbb29b6ec3eaec060814a02a33fdbf7d4bc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:05:30 +0200 Subject: [PATCH 44/60] Keep dictionary tests focused on sequential behavior --- tests/FoundationsTests/DictionaryTests.cpp | 211 ++++++--------------- 1 file changed, 60 insertions(+), 151 deletions(-) diff --git a/tests/FoundationsTests/DictionaryTests.cpp b/tests/FoundationsTests/DictionaryTests.cpp index 30ad4ee9..02026fbb 100644 --- a/tests/FoundationsTests/DictionaryTests.cpp +++ b/tests/FoundationsTests/DictionaryTests.cpp @@ -2,47 +2,18 @@ #include "SystemFunctions.h" #include "utest.h" -struct DictionaryThreadParameter -{ - SystemDictionary Dictionary; - int32_t ThreadId; - int32_t ItemCount; -}; - struct DictionaryTestStruct { int64_t Value1; int64_t Value2; }; -void DictionaryConcurrentAddFunction(void* parameter) -{ - auto threadParameter = (DictionaryThreadParameter*)parameter; - auto dictionary = threadParameter->Dictionary; - - for (int32_t i = 0; i < threadParameter->ItemCount; i++) - { - SystemAddDictionaryEntry(dictionary, threadParameter->ThreadId * threadParameter->ItemCount + i, threadParameter->ThreadId * threadParameter->ItemCount + i); - } -} - -void DictionaryConcurrentRemoveFunction(void* parameter) -{ - auto threadParameter = (DictionaryThreadParameter*)parameter; - auto dictionary = threadParameter->Dictionary; - - for (int32_t i = 0; i < threadParameter->ItemCount; i++) - { - SystemRemoveDictionaryEntry(dictionary, threadParameter->ThreadId * threadParameter->ItemCount + i); - } -} - -UTEST(Dictionary, AddValue) +UTEST(Dictionary, AddValue) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 24); - + // Act for (int32_t i = 0; i < 10; i++) { @@ -54,12 +25,12 @@ UTEST(Dictionary, AddValue) ASSERT_EQ(9, testValue); } -UTEST(Dictionary, AddValue_KeyStruct) +UTEST(Dictionary, AddValue_KeyStruct) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto dictionary = SystemCreateDictionary(stackMemoryArena, 24); - + // Act for (int32_t i = 0; i < 10; i++) { @@ -68,14 +39,14 @@ UTEST(Dictionary, AddValue_KeyStruct) testStruct.Value2 = i * i; SystemAddDictionaryEntry(dictionary, i, testStruct); } - + // Assert auto testValue = dictionary[9]; ASSERT_EQ(9, testValue.Value1); ASSERT_EQ(81, testValue.Value2); } -UTEST(Dictionary, RemoveValue) +UTEST(Dictionary, RemoveValue) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -84,8 +55,8 @@ UTEST(Dictionary, RemoveValue) for (int32_t i = 0; i < 10; i++) { SystemAddDictionaryEntry(dictionary, SystemFormatString(stackMemoryArena, "Test%d", i), i); - } - + } + // Act SystemRemoveDictionaryEntry(dictionary, "Test6"); @@ -98,14 +69,14 @@ UTEST(Dictionary, RemoveValue) { ASSERT_EQ(0, testValue); } - else + else { ASSERT_EQ(i, testValue); } - } + } } -UTEST(Dictionary, RemoveValueNoParent) +UTEST(Dictionary, RemoveValueNoParent) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -114,8 +85,8 @@ UTEST(Dictionary, RemoveValueNoParent) for (int32_t i = 0; i < 10; i++) { SystemAddDictionaryEntry(dictionary, SystemFormatString(stackMemoryArena, "Test%d", i), i); - } - + } + // Act SystemRemoveDictionaryEntry(dictionary, "Test8"); @@ -128,23 +99,23 @@ UTEST(Dictionary, RemoveValueNoParent) { ASSERT_EQ(0, testValue); } - else + else { ASSERT_EQ(i, testValue); } - } + } } -UTEST(Dictionary, RemoveValue_KeyStruct) +UTEST(Dictionary, RemoveValue_KeyStruct) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto dictionary = SystemCreateDictionary(stackMemoryArena, 24); - + for (int32_t i = 0; i < 10; i++) { SystemAddDictionaryEntry(dictionary, i, i); - } + } // Act SystemRemoveDictionaryEntry(dictionary, 9); @@ -154,12 +125,12 @@ UTEST(Dictionary, RemoveValue_KeyStruct) ASSERT_EQ(0, testValue); } -UTEST(Dictionary, GrowStorage) +UTEST(Dictionary, GrowStorage) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 24); - + for (int32_t i = 0; i < 10; i++) { SystemAddDictionaryEntry(dictionary, SystemFormatString(stackMemoryArena, "Test%d", i), i); @@ -185,12 +156,12 @@ UTEST(Dictionary, GrowStorage) ASSERT_EQ(32, testValue); } -UTEST(Dictionary, NotEnoughStorage) +UTEST(Dictionary, NotEnoughStorage) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 24); - + for (int32_t i = 0; i < 24; i++) { SystemAddDictionaryEntry(dictionary, SystemFormatString(stackMemoryArena, "Test%d", i), i); @@ -204,7 +175,7 @@ UTEST(Dictionary, NotEnoughStorage) ASSERT_EQ(0, testValue); } -UTEST(Dictionary, RemoveValuesAfterFull) +UTEST(Dictionary, RemoveValuesAfterFull) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); @@ -216,7 +187,7 @@ UTEST(Dictionary, RemoveValuesAfterFull) } SystemRemoveDictionaryEntry(dictionary, "Test0"); - + // Act SystemAddDictionaryEntry(dictionary, "TestNew", 28); @@ -225,14 +196,14 @@ UTEST(Dictionary, RemoveValuesAfterFull) ASSERT_EQ(28, testValue); } -UTEST(Dictionary, BigDictionary) +UTEST(Dictionary, BigDictionary) { // Arrange auto maxElements = 1000000; auto stackMemoryArena = SystemAllocateMemoryArena(); auto memoryArena = SystemAllocateMemoryArena(); auto dictionary = SystemCreateDictionary, int32_t>(memoryArena, maxElements); - + // Act for (int32_t i = 0; i < 10000; i++) { @@ -247,135 +218,73 @@ UTEST(Dictionary, BigDictionary) } } -UTEST(Dictionary, ConcurrentAdd) +UTEST(Dictionary, ContainsKey) { // Arrange - const int32_t itemCount = 80000; - const int32_t threadCount = 32; - auto memoryArena = SystemAllocateMemoryArena(); - auto dictionary = SystemCreateDictionary(memoryArena, itemCount); - - // Act - SystemThread threads[threadCount]; - DictionaryThreadParameter threadParameters[threadCount]; + auto stackMemoryArena = SystemGetStackMemoryArena(); + auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 24); - for (int32_t i = 0; i < threadCount; i++) + for (int32_t i = 0; i < 10; i++) { - threadParameters[i] = { dictionary, i, itemCount / threadCount }; - threads[i] = SystemCreateThread(DictionaryConcurrentAddFunction, &threadParameters[i]); + SystemAddDictionaryEntry(dictionary, SystemFormatString(stackMemoryArena, "Test%d", i), i); } - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } + // Act + auto testValue = SystemDictionaryContainsKey(dictionary, "Test9"); // Assert - auto count = 0; - - for (int32_t i = 0; i < threadCount; i++) - { - auto threadItemCount = (itemCount / threadCount); - - for (int32_t j = 0; j < threadItemCount; j++) - { - auto key = i * threadItemCount + j; - - if (SystemDictionaryContainsKey(dictionary, key)) - { - count++; - } - } - } - - ASSERT_EQ(itemCount, count); + ASSERT_TRUE(testValue); } -UTEST(Dictionary, ConcurrentRemove) +UTEST(Dictionary, ContainsKey_KeyStruct) { // Arrange - const int32_t itemCount = 32000; - const int32_t threadCount = 32; - auto memoryArena = SystemGetStackMemoryArena(); - auto dictionary = SystemCreateDictionary(memoryArena, itemCount); - - for (int32_t i = 0; i < itemCount; i++) - { - SystemAddDictionaryEntry(dictionary, i, i); - } - - // Act - SystemThread threads[threadCount]; - DictionaryThreadParameter threadParameters[threadCount]; + auto stackMemoryArena = SystemGetStackMemoryArena(); + auto dictionary = SystemCreateDictionary(stackMemoryArena, 24); - for (int32_t i = 0; i < threadCount; i++) + for (int32_t i = 0; i < 10; i++) { - threadParameters[i] = { dictionary, i, (itemCount / 2) / threadCount }; - threads[i] = SystemCreateThread(DictionaryConcurrentRemoveFunction, &threadParameters[i]); + SystemAddDictionaryEntry(dictionary, i, i); } - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } + // Act + auto testValue = SystemDictionaryContainsKey(dictionary, 9); // Assert - auto count = 0; - - for (int32_t i = 0; i < threadCount; i++) - { - auto threadItemCount = (itemCount / threadCount); - - for (int32_t j = 0; j < threadItemCount; j++) - { - auto key = i * threadItemCount + j; - - if (SystemDictionaryContainsKey(dictionary, key)) - { - count++; - } - } - } - - ASSERT_EQ(itemCount / 2, count); + ASSERT_TRUE(testValue); } -UTEST(Dictionary, ContainsKey) +UTEST(Dictionary, ReadOnlySpanHashUsesAllBytes) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); - auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 24); - - for (int32_t i = 0; i < 10; i++) - { - SystemAddDictionaryEntry(dictionary, SystemFormatString(stackMemoryArena, "Test%d", i), i); - } - + auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 8); + uint32_t key1[] = { 0x00001234, 1 }; + uint32_t key2[] = { 0x00001234, 2 }; + // Act - auto testValue = SystemDictionaryContainsKey(dictionary, "Test9"); + SystemAddDictionaryEntry(dictionary, ReadOnlySpan(key1, 2), 10); + SystemAddDictionaryEntry(dictionary, ReadOnlySpan(key2, 2), 20); // Assert - ASSERT_TRUE(testValue); + auto value1 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key1, 2)); + auto value2 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key2, 2)); + ASSERT_TRUE(value1 != nullptr); + ASSERT_TRUE(value2 != nullptr); + ASSERT_EQ(10, *value1); + ASSERT_EQ(20, *value2); } -UTEST(Dictionary, ContainsKey_KeyStruct) +UTEST(Dictionary, MissingValueReturnsNull) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); - auto dictionary = SystemCreateDictionary(stackMemoryArena, 24); - - // Act - for (int32_t i = 0; i < 10; i++) - { - SystemAddDictionaryEntry(dictionary, i, i); - } + auto dictionary = SystemCreateDictionary(stackMemoryArena, 8); // Act - auto testValue = SystemDictionaryContainsKey(dictionary, 9); + auto value = SystemGetDictionaryValue(dictionary, 42); // Assert - ASSERT_TRUE(testValue); + ASSERT_TRUE(value == nullptr); + ASSERT_EQ(0, dictionary[42]); } - From 79d81c6f7a439daf6cb7b407e991c721d87d4f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:05:44 +0200 Subject: [PATCH 45/60] Organize Foundations tests by concurrency contract --- tests/FoundationsTests/UnityBuild.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/FoundationsTests/UnityBuild.cpp b/tests/FoundationsTests/UnityBuild.cpp index 82d081cf..619ffddd 100644 --- a/tests/FoundationsTests/UnityBuild.cpp +++ b/tests/FoundationsTests/UnityBuild.cpp @@ -1,16 +1,16 @@ #include "utest.h" #include "MemoryTests.cpp" -#include "MemoryRobustnessTests.cpp" +#include "MemoryConcurrentTests.cpp" #include "SpanTests.cpp" #include "MathTests.cpp" #include "StringTests.cpp" #include "IOTests.cpp" #include "LibraryProcessTests.cpp" #include "DictionaryTests.cpp" -#include "DictionaryRobustnessTests.cpp" +#include "DictionaryConcurrentTests.cpp" #include "DataPoolTests.cpp" -#include "DataPoolRobustnessTests.cpp" +#include "DataPoolConcurrentTests.cpp" #ifndef _WIN32 #include "PosixPlatformFunctions.cpp" @@ -45,7 +45,7 @@ void LogMessageHandler(ElemLogMessageType messageType, ElemLogMessageCategory, c UTEST_STATE(); -int main(int argc, const char *const argv[]) +int main(int argc, const char *const argv[]) { #ifdef _DEBUG SystemRegisterLogHandler(LogMessageHandler); From 6efb753f7a163e0219e1d846f45e6bfb06a4fb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:05:49 +0200 Subject: [PATCH 46/60] Remove obsolete memory robustness tests --- .../MemoryRobustnessTests.cpp | 106 ------------------ 1 file changed, 106 deletions(-) delete mode 100644 tests/FoundationsTests/MemoryRobustnessTests.cpp diff --git a/tests/FoundationsTests/MemoryRobustnessTests.cpp b/tests/FoundationsTests/MemoryRobustnessTests.cpp deleted file mode 100644 index 4143c08d..00000000 --- a/tests/FoundationsTests/MemoryRobustnessTests.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include "SystemFunctions.h" -#include "SystemMemory.h" -#include "SystemPlatformFunctions.h" -#include "utest.h" - -struct ConcurrentArenaAllocationParameter -{ - MemoryArena* Result; - size_t SizeInBytes; -}; - -void ConcurrentArenaAllocationFunction(void* parameter) -{ - auto threadParameter = (ConcurrentArenaAllocationParameter*)parameter; - *threadParameter->Result = SystemAllocateMemoryArena(threadParameter->SizeInBytes); -} - -UTEST(MemoryRobustness, ArenaSizeOverflowReturnsEmptyHandle) -{ - // Act - auto memoryArena = SystemAllocateMemoryArena(SIZE_MAX); - - // Assert - ASSERT_TRUE(memoryArena.Storage == nullptr); -} - -UTEST(MemoryRobustness, PushSizeOverflowDoesNotAdvanceArena) -{ - // Arrange - auto memoryArena = SystemAllocateMemoryArena(64); - - // Act - auto allocation = SystemPushMemory(memoryArena, SIZE_MAX, AllocationState_Reserved); - auto array = SystemPushArray(memoryArena, SIZE_MAX / sizeof(uint64_t) + 1, AllocationState_Reserved); - - // Assert - ASSERT_TRUE(allocation == nullptr); - ASSERT_TRUE(array.Pointer == nullptr); - ASSERT_EQ(0llu, array.Length); - ASSERT_EQ(0llu, SystemGetMemoryArenaAllocationInfos(memoryArena).AllocatedBytes); -} - -UTEST(MemoryRobustness, CommitReportsInvalidRange) -{ - // Arrange - auto memoryArena = SystemAllocateMemoryArena(64); - auto allocation = SystemPushArray(memoryArena, 64, AllocationState_Reserved); - - // Act - auto validCommit = SystemCommitMemory(memoryArena, allocation.Pointer, allocation.Length, true); - auto invalidCommit = SystemCommitMemory(memoryArena, allocation.Pointer + allocation.Length, 8); - - // Assert - ASSERT_TRUE(validCommit); - ASSERT_FALSE(invalidCommit); - - for (size_t i = 0; i < allocation.Length; i++) - { - ASSERT_EQ(0, allocation[i]); - } - - SystemFreeMemoryArena(memoryArena); -} - -UTEST(MemoryRobustness, ConcurrentArenaAllocationAccounting) -{ - // Arrange - const int32_t threadCount = 16; - auto pageSizeInBytes = SystemPlatformGetPageSize(); - auto allocationInfosBefore = SystemGetAllocationInfos(); - MemoryArena memoryArenas[threadCount] = {}; - SystemThread threads[threadCount]; - ConcurrentArenaAllocationParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { &memoryArenas[i], pageSizeInBytes }; - threads[i] = SystemCreateThread(ConcurrentArenaAllocationFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - for (int32_t i = 0; i < threadCount; i++) - { - ASSERT_TRUE(memoryArenas[i].Storage != nullptr); - } - - auto allocationInfosAfterAllocate = SystemGetAllocationInfos(); - ASSERT_EQ(allocationInfosBefore.ReservedBytes + threadCount * pageSizeInBytes * 2, allocationInfosAfterAllocate.ReservedBytes); - ASSERT_EQ(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterAllocate.CommittedBytes); - - for (int32_t i = 0; i < threadCount; i++) - { - SystemFreeMemoryArena(memoryArenas[i]); - } - - auto allocationInfosAfterFree = SystemGetAllocationInfos(); - ASSERT_EQ(allocationInfosBefore.ReservedBytes, allocationInfosAfterFree.ReservedBytes); - ASSERT_EQ(allocationInfosBefore.CommittedBytes, allocationInfosAfterFree.CommittedBytes); -} From edd4f186c337f371e3eba49467421d7e0b1b2c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:05:55 +0200 Subject: [PATCH 47/60] Remove obsolete dictionary robustness tests --- .../DictionaryRobustnessTests.cpp | 150 ------------------ 1 file changed, 150 deletions(-) delete mode 100644 tests/FoundationsTests/DictionaryRobustnessTests.cpp diff --git a/tests/FoundationsTests/DictionaryRobustnessTests.cpp b/tests/FoundationsTests/DictionaryRobustnessTests.cpp deleted file mode 100644 index 0333a44d..00000000 --- a/tests/FoundationsTests/DictionaryRobustnessTests.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "SystemDictionary.h" -#include "SystemFunctions.h" -#include "utest.h" - -struct DictionaryConcurrentAddOneParameter -{ - SystemDictionary Dictionary; - int32_t Key; -}; - -struct DictionaryConcurrentReuseParameter -{ - SystemDictionary Dictionary; - int64_t CurrentKey; - uint32_t ThreadId; - uint32_t IterationCount; -}; - -void DictionaryConcurrentAddOneFunction(void* parameter) -{ - auto threadParameter = (DictionaryConcurrentAddOneParameter*)parameter; - SystemAddDictionaryEntry(threadParameter->Dictionary, threadParameter->Key, threadParameter->Key); -} - -void DictionaryConcurrentReuseFunction(void* parameter) -{ - auto threadParameter = (DictionaryConcurrentReuseParameter*)parameter; - - for (uint32_t i = 0; i < threadParameter->IterationCount; i++) - { - SystemRemoveDictionaryEntry(threadParameter->Dictionary, threadParameter->CurrentKey); - - auto key = (int64_t)threadParameter->ThreadId * 1000000 + i + 1; - auto value = ((uint64_t)threadParameter->ThreadId << 32) | i; - SystemAddDictionaryEntry(threadParameter->Dictionary, key, value); - threadParameter->CurrentKey = key; - } -} - -UTEST(DictionaryRobustness, ReadOnlySpanHashUsesAllBytes) -{ - // Arrange - auto stackMemoryArena = SystemGetStackMemoryArena(); - auto dictionary = SystemCreateDictionary, int32_t>(stackMemoryArena, 8); - uint32_t key1[] = { 0x00001234, 1 }; - uint32_t key2[] = { 0x00001234, 2 }; - - // Act - SystemAddDictionaryEntry(dictionary, ReadOnlySpan(key1, 2), 10); - SystemAddDictionaryEntry(dictionary, ReadOnlySpan(key2, 2), 20); - - // Assert - auto value1 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key1, 2)); - auto value2 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key2, 2)); - ASSERT_TRUE(value1 != nullptr); - ASSERT_TRUE(value2 != nullptr); - ASSERT_EQ(10, *value1); - ASSERT_EQ(20, *value2); -} - -UTEST(DictionaryRobustness, MissingValueReturnsNull) -{ - // Arrange - auto stackMemoryArena = SystemGetStackMemoryArena(); - auto dictionary = SystemCreateDictionary(stackMemoryArena, 8); - - // Act - auto value = SystemGetDictionaryValue(dictionary, 42); - - // Assert - ASSERT_TRUE(value == nullptr); - ASSERT_EQ(0, dictionary[42]); -} - -UTEST(DictionaryRobustness, ConcurrentAddStopsAtCapacity) -{ - // Arrange - const int32_t threadCount = 32; - const int32_t capacity = 8; - auto memoryArena = SystemAllocateMemoryArena(); - auto dictionary = SystemCreateDictionary(memoryArena, capacity); - SystemThread threads[threadCount]; - DictionaryConcurrentAddOneParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { dictionary, i }; - threads[i] = SystemCreateThread(DictionaryConcurrentAddOneFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto foundCount = 0; - - for (int32_t i = 0; i < threadCount; i++) - { - if (SystemDictionaryContainsKey(dictionary, i)) - { - auto value = SystemGetDictionaryValue(dictionary, i); - ASSERT_TRUE(value != nullptr); - ASSERT_EQ(i, *value); - foundCount++; - } - } - - ASSERT_EQ(capacity, foundCount); - SystemFreeMemoryArena(memoryArena); -} - -UTEST(DictionaryRobustness, ConcurrentReusePreservesAllEntries) -{ - // Arrange - const int32_t threadCount = 16; - const uint32_t iterationCount = 5000; - auto memoryArena = SystemAllocateMemoryArena(); - auto dictionary = SystemCreateDictionary(memoryArena, threadCount); - SystemThread threads[threadCount]; - DictionaryConcurrentReuseParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - auto initialKey = -(int64_t)i - 1; - SystemAddDictionaryEntry(dictionary, initialKey, (uint64_t)i); - threadParameters[i] = { dictionary, initialKey, (uint32_t)i, iterationCount }; - threads[i] = SystemCreateThread(DictionaryConcurrentReuseFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - for (int32_t i = 0; i < threadCount; i++) - { - auto value = SystemGetDictionaryValue(dictionary, threadParameters[i].CurrentKey); - ASSERT_TRUE(value != nullptr); - ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), *value); - } - - SystemFreeMemoryArena(memoryArena); -} From ead3758954f1d4035cc59adf00b9a1a17e8999ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:06:00 +0200 Subject: [PATCH 48/60] Remove obsolete data pool robustness tests --- .../DataPoolRobustnessTests.cpp | 199 ------------------ 1 file changed, 199 deletions(-) delete mode 100644 tests/FoundationsTests/DataPoolRobustnessTests.cpp diff --git a/tests/FoundationsTests/DataPoolRobustnessTests.cpp b/tests/FoundationsTests/DataPoolRobustnessTests.cpp deleted file mode 100644 index 2be89fc5..00000000 --- a/tests/FoundationsTests/DataPoolRobustnessTests.cpp +++ /dev/null @@ -1,199 +0,0 @@ -#include "SystemDataPool.h" -#include "SystemFunctions.h" -#include "utest.h" - -struct DataPoolRobustnessData -{ - uint64_t Value; -}; - -struct DataPoolConcurrentAddOneParameter -{ - SystemDataPool DataPool; - ElemHandle* Result; - uint64_t Value; -}; - -struct DataPoolConcurrentRemoveOneParameter -{ - SystemDataPool DataPool; - ElemHandle Handle; -}; - -struct DataPoolConcurrentReuseParameter -{ - SystemDataPool DataPool; - ElemHandle Handle; - uint32_t ThreadId; - uint32_t IterationCount; - bool Failed; -}; - -void DataPoolConcurrentAddOneFunction(void* parameter) -{ - auto threadParameter = (DataPoolConcurrentAddOneParameter*)parameter; - DataPoolRobustnessData data = { threadParameter->Value }; - *threadParameter->Result = SystemAddDataPoolItem(threadParameter->DataPool, data); -} - -void DataPoolConcurrentRemoveOneFunction(void* parameter) -{ - auto threadParameter = (DataPoolConcurrentRemoveOneParameter*)parameter; - SystemRemoveDataPoolItem(threadParameter->DataPool, threadParameter->Handle); -} - -void DataPoolConcurrentReuseFunction(void* parameter) -{ - auto threadParameter = (DataPoolConcurrentReuseParameter*)parameter; - auto handle = threadParameter->Handle; - - for (uint32_t i = 0; i < threadParameter->IterationCount; i++) - { - SystemRemoveDataPoolItem(threadParameter->DataPool, handle); - - DataPoolRobustnessData data = {}; - data.Value = ((uint64_t)threadParameter->ThreadId << 32) | i; - handle = SystemAddDataPoolItem(threadParameter->DataPool, data); - - if (handle == ELEM_HANDLE_NULL) - { - threadParameter->Failed = true; - return; - } - } - - threadParameter->Handle = handle; -} - -UTEST(DataPoolRobustness, ConcurrentAddStopsAtCapacity) -{ - // Arrange - const int32_t threadCount = 32; - const int32_t capacity = 8; - auto memoryArena = SystemAllocateMemoryArena(); - auto dataPool = SystemCreateDataPool(memoryArena, capacity); - ElemHandle handles[threadCount] = {}; - SystemThread threads[threadCount]; - DataPoolConcurrentAddOneParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { dataPool, &handles[i], (uint64_t)i }; - threads[i] = SystemCreateThread(DataPoolConcurrentAddOneFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - auto successCount = 0; - - for (int32_t i = 0; i < threadCount; i++) - { - if (handles[i] == ELEM_HANDLE_NULL) - { - continue; - } - - successCount++; - auto handleInfo = UnpackSystemDataPoolHandle(handles[i]); - - for (int32_t j = i + 1; j < threadCount; j++) - { - if (handles[j] != ELEM_HANDLE_NULL) - { - ASSERT_TRUE(handleInfo.Index != UnpackSystemDataPoolHandle(handles[j]).Index); - } - } - } - - ASSERT_EQ(capacity, successCount); - ASSERT_EQ((size_t)capacity, SystemGetDataPoolItemCount(dataPool)); - SystemFreeMemoryArena(memoryArena); -} - -UTEST(DataPoolRobustness, ConcurrentRemoveSameHandleOnlyFreesOnce) -{ - // Arrange - const int32_t threadCount = 16; - auto memoryArena = SystemAllocateMemoryArena(); - auto dataPool = SystemCreateDataPool(memoryArena, 1); - auto handle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { 42 }); - SystemThread threads[threadCount]; - DataPoolConcurrentRemoveOneParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - threadParameters[i] = { dataPool, handle }; - threads[i] = SystemCreateThread(DataPoolConcurrentRemoveOneFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - ASSERT_EQ(0llu, SystemGetDataPoolItemCount(dataPool)); - - auto reusedHandle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { 100 }); - auto overflowHandle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { 200 }); - ASSERT_TRUE(reusedHandle != ELEM_HANDLE_NULL); - ASSERT_TRUE(overflowHandle == ELEM_HANDLE_NULL); - ASSERT_EQ(1llu, SystemGetDataPoolItemCount(dataPool)); - SystemFreeMemoryArena(memoryArena); -} - -UTEST(DataPoolRobustness, ConcurrentReuseKeepsSlotsUnique) -{ - // Arrange - const int32_t threadCount = 16; - const uint32_t iterationCount = 5000; - auto memoryArena = SystemAllocateMemoryArena(); - auto dataPool = SystemCreateDataPool(memoryArena, threadCount); - SystemThread threads[threadCount]; - DataPoolConcurrentReuseParameter threadParameters[threadCount]; - - for (int32_t i = 0; i < threadCount; i++) - { - auto handle = SystemAddDataPoolItem(dataPool, DataPoolRobustnessData { (uint64_t)i }); - threadParameters[i] = { dataPool, handle, (uint32_t)i, iterationCount, false }; - threads[i] = SystemCreateThread(DataPoolConcurrentReuseFunction, &threadParameters[i]); - } - - // Act - for (int32_t i = 0; i < threadCount; i++) - { - SystemWaitThread(threads[i]); - SystemFreeThread(threads[i]); - } - - // Assert - ASSERT_EQ((size_t)threadCount, SystemGetDataPoolItemCount(dataPool)); - - for (int32_t i = 0; i < threadCount; i++) - { - ASSERT_FALSE(threadParameters[i].Failed); - ASSERT_TRUE(threadParameters[i].Handle != ELEM_HANDLE_NULL); - - auto data = SystemGetDataPoolItem(dataPool, threadParameters[i].Handle); - ASSERT_TRUE(data != nullptr); - ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), data->Value); - - auto handleInfo = UnpackSystemDataPoolHandle(threadParameters[i].Handle); - - for (int32_t j = i + 1; j < threadCount; j++) - { - auto otherHandleInfo = UnpackSystemDataPoolHandle(threadParameters[j].Handle); - ASSERT_TRUE(handleInfo.Index != otherHandleInfo.Index); - } - } - - SystemFreeMemoryArena(memoryArena); -} From 82e01306f7096da8245e5b17858afc2f9b52d936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:19:14 +0200 Subject: [PATCH 49/60] Document memory data structures --- src/Foundations/SystemMemory.h | 64 +++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/src/Foundations/SystemMemory.h b/src/Foundations/SystemMemory.h index a4bbc809..be2d6755 100644 --- a/src/Foundations/SystemMemory.h +++ b/src/Foundations/SystemMemory.h @@ -4,39 +4,79 @@ struct MemoryArenaStorage; +/** + * Defines the initial virtual-memory state of a MemoryArena allocation. + */ enum AllocationState { - AllocationState_Committed, - AllocationState_Reserved + AllocationState_Committed, ///< The allocation is committed and can be accessed immediately. + AllocationState_Reserved ///< The allocation reserves arena space but must be committed before access. }; +/** + * Process-wide virtual-memory allocation counters maintained by the platform layer. + * + * These values describe virtual memory managed through Foundations platform-memory functions. They + * are accounting information rather than ownership handles and can change concurrently as other + * threads reserve, commit, decommit, or release memory. + */ struct AllocationInfos { - size_t CommittedBytes; - size_t ReservedBytes; + size_t CommittedBytes; ///< Number of bytes currently committed through the platform layer. + size_t ReservedBytes; ///< Number of bytes currently reserved through the platform layer. }; +/** + * Lightweight value handle to MemoryArena storage. + * + * Copying a MemoryArena copies only the handle; all copies reference the same MemoryArenaStorage. + * MemoryArena performs no ownership tracking, reference counting, or automatic lifetime management. + * Releasing the storage through any copied handle invalidates every other handle and every allocation + * produced from that storage. + * + * Level is zero for regular arenas. Handles obtained from StackMemoryArena use Level to carry the + * stack lifetime that allocations made through that handle must follow. + */ struct MemoryArena { - MemoryArenaStorage* Storage; - uint8_t Level; + MemoryArenaStorage* Storage; ///< Shared allocator and virtual-memory state referenced by this handle. + uint8_t Level; ///< Stack lifetime level, or zero for a regular MemoryArena. }; +/** + * Allocation state of a single MemoryArena. + */ struct MemoryArenaAllocationInfos { - size_t AllocatedBytes; - size_t CommittedBytes; - size_t MaximumSizeInBytes; + size_t AllocatedBytes; ///< Logical data bytes currently allocated from the arena. + size_t CommittedBytes; ///< Physically committed bytes, including the arena's internal header pages. + size_t MaximumSizeInBytes; ///< Maximum logical data capacity requested for the arena. }; +/** + * Scoped thread-local MemoryArena lifetime. + * + * Creating a StackMemoryArena enters a nested stack lifetime. Destroying it rolls back allocations + * made with that lifetime while preserving allocations explicitly made through ancestor MemoryArena + * handles. The contained MemoryArena is the value intended to be copied and passed to callees. + * + * StackMemoryArena itself must not be copied. A MemoryArena obtained from it must not outlive the + * corresponding stack scope. StackMemoryArena is thread-local and must not be shared across threads. + */ struct StackMemoryArena { - MemoryArena Arena; - size_t StartOffsetInBytes; - size_t StartExtraOffsetInBytes; + MemoryArena Arena; ///< Value handle representing this stack lifetime. + size_t StartOffsetInBytes; ///< Main stack-storage offset restored when the scope ends. + size_t StartExtraOffsetInBytes; ///< Extra-storage offset restored when the scope ends. + /** + * Ends the stack lifetime and rolls back allocations owned by this scope. + */ ~StackMemoryArena(); + /** + * Returns the lightweight MemoryArena handle for this stack lifetime. + */ operator MemoryArena() const { return Arena; From 01d00ed8f3e047ab5dfb68a595b18236e4c2b762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:20:36 +0200 Subject: [PATCH 50/60] Add assertion messages to span tests --- tests/FoundationsTests/SpanTests.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/FoundationsTests/SpanTests.cpp b/tests/FoundationsTests/SpanTests.cpp index 958e31fa..3a0cdb01 100644 --- a/tests/FoundationsTests/SpanTests.cpp +++ b/tests/FoundationsTests/SpanTests.cpp @@ -24,9 +24,9 @@ UTEST(Span, ReadOnlyConstBuffer) auto slice = span.Slice(1, 2); // Assert - ASSERT_EQ(2llu, slice.Length); - ASSERT_EQ(20, slice[0]); - ASSERT_EQ(30, slice[1]); + ASSERT_EQ_MSG(2llu, slice.Length, "ReadOnlySpan slice length is invalid."); + ASSERT_EQ_MSG(20, slice[0], "ReadOnlySpan slice first value is invalid."); + ASSERT_EQ_MSG(30, slice[1], "ReadOnlySpan slice second value is invalid."); } UTEST(Span, InitializerList) @@ -35,7 +35,7 @@ UTEST(Span, InitializerList) auto result = SumSpanValues({ 10, 20, 30 }); // Assert - ASSERT_EQ(60, result); + ASSERT_EQ_MSG(60, result, "ReadOnlySpan initializer-list values were not preserved."); } UTEST(Span, StringLengthExcludesNullTerminator) @@ -44,8 +44,8 @@ UTEST(Span, StringLengthExcludesNullTerminator) ReadOnlySpan value = "Elemental"; // Assert - ASSERT_EQ(9llu, value.Length); - ASSERT_EQ('\0', value.Pointer[value.Length]); + ASSERT_EQ_MSG(9llu, value.Length, "Character span length should exclude the null terminator."); + ASSERT_EQ_MSG('\0', value.Pointer[value.Length], "Character span backing storage should remain null terminated."); } UTEST(Span, DuplicateStringPreservesLogicalLengthAndNullTerminator) @@ -58,9 +58,9 @@ UTEST(Span, DuplicateStringPreservesLogicalLengthAndNullTerminator) auto result = SystemDuplicateBuffer(memoryArena, source); // Assert - ASSERT_EQ(source.Length, result.Length); - ASSERT_EQ('\0', result.Pointer[result.Length]); - ASSERT_STREQ("Elemental", result.Pointer); + ASSERT_EQ_MSG(source.Length, result.Length, "Duplicated character span should preserve the logical source length."); + ASSERT_EQ_MSG('\0', result.Pointer[result.Length], "Duplicated character span should have a trailing null terminator."); + ASSERT_STREQ_MSG("Elemental", result.Pointer, "Duplicated character span data is invalid."); SystemFreeMemoryArena(memoryArena); } From d3375e5e22f1d6282008b1a6a0f89f1f3bc6b3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:20:49 +0200 Subject: [PATCH 51/60] Add assertion messages to math tests --- tests/FoundationsTests/MathTests.cpp | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/FoundationsTests/MathTests.cpp b/tests/FoundationsTests/MathTests.cpp index 983eb88b..19bc25fb 100644 --- a/tests/FoundationsTests/MathTests.cpp +++ b/tests/FoundationsTests/MathTests.cpp @@ -1,80 +1,80 @@ #include "SystemFunctions.h" #include "utest.h" -UTEST(MathFunctions, SystemRoundUpToPowerOf2) +UTEST(MathFunctions, SystemRoundUpToPowerOf2) { // Arrange auto testNumber = 45; - + // Act auto result = SystemRoundUpToPowerOf2(testNumber); // Assert - ASSERT_EQ((size_t)64, result); + ASSERT_EQ_MSG((size_t)64, result, "SystemRoundUpToPowerOf2 returned an invalid power-of-two value."); } -UTEST(MathFunctions, SystemRound) +UTEST(MathFunctions, SystemRound) { // Arrange auto testNumber = 45.67f; - + // Act auto result = SystemRound(testNumber); // Assert - ASSERT_EQ(46.0, result); + ASSERT_EQ_MSG(46.0, result, "SystemRound returned an invalid rounded value."); } -UTEST(MathFunctions, SystemRoundUpNearZero) +UTEST(MathFunctions, SystemRoundUpNearZero) { // Arrange auto testNumber = 0.1; - + // Act auto result = SystemRoundUp(testNumber); // Assert - ASSERT_EQ(1, result); + ASSERT_EQ_MSG(1, result, "SystemRoundUp should round a positive fractional value to the next integer."); } -UTEST(MathFunctions, SystemRoundUpExact) +UTEST(MathFunctions, SystemRoundUpExact) { // Arrange auto testNumber = 6.0; - + // Act auto result = SystemRoundUp(testNumber); // Assert - ASSERT_EQ(6, result); + ASSERT_EQ_MSG(6, result, "SystemRoundUp should preserve an exact integer value."); } -UTEST(MathFunctions, SystemAbs) +UTEST(MathFunctions, SystemAbs) { // Arrange auto testNumber = -65; - + // Act auto result = SystemAbs(testNumber); // Assert - ASSERT_EQ(65, result); + ASSERT_EQ_MSG(65, result, "SystemAbs returned an invalid absolute value."); } -UTEST(MathFunctions, SystemMax) +UTEST(MathFunctions, SystemMax) { // Arrange / Act auto result = SystemMax(67, 54); // Assert - ASSERT_EQ(67, result); + ASSERT_EQ_MSG(67, result, "SystemMax did not return the greater value."); } -UTEST(MathFunctions, SystemMin) +UTEST(MathFunctions, SystemMin) { // Arrange / Act auto result = SystemMin(67, 54); // Assert - ASSERT_EQ(54, result); + ASSERT_EQ_MSG(54, result, "SystemMin did not return the lesser value."); } From 7beb84df57342c3541bc6aa5e390e69034285981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:21:03 +0200 Subject: [PATCH 52/60] Add assertion messages to string tests --- tests/FoundationsTests/StringTests.cpp | 57 +++++++++++--------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/tests/FoundationsTests/StringTests.cpp b/tests/FoundationsTests/StringTests.cpp index a18cf074..711f0960 100644 --- a/tests/FoundationsTests/StringTests.cpp +++ b/tests/FoundationsTests/StringTests.cpp @@ -1,105 +1,96 @@ #include "utest.h" #include "SystemFunctions.h" -UTEST(StringFunctions, SystemConvertNumberToString) +UTEST(StringFunctions, SystemConvertNumberToString) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto testNumber = 45; - + // Act auto result = SystemConvertNumberToString(stackMemoryArena, testNumber); // Assert - ASSERT_STREQ("45", result.Pointer); + ASSERT_STREQ_MSG("45", result.Pointer, "Integer to string conversion returned invalid text."); } -UTEST(StringFunctions, SystemConvertFloatToString) +UTEST(StringFunctions, SystemConvertFloatToString) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto testNumber = 45.76; - + // Act auto result = SystemConvertFloatToString(stackMemoryArena, testNumber); // Assert - ASSERT_STREQ("45.76", result.Pointer); + ASSERT_STREQ_MSG("45.76", result.Pointer, "Floating-point to string conversion returned invalid text."); } -UTEST(StringFunctions, SystemFormatString) +UTEST(StringFunctions, SystemFormatString) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); - + // Act auto result = SystemFormatString(stackMemoryArena, "This is a test: %s, number: %d, float: %f end of the test", "TestString", -54, -23.89f); // Assert - ASSERT_STREQ("This is a test: TestString, number: -54, float: -23.89 end of the test", result.Pointer); + ASSERT_STREQ_MSG("This is a test: TestString, number: -54, float: -23.89 end of the test", result.Pointer, "Formatted string content is invalid."); } -UTEST(StringFunctions, SystemSplitString) +UTEST(StringFunctions, SystemSplitString) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); - + // Act auto result = SystemSplitString(stackMemoryArena, "Test/Split/String", '/'); // Assert - ASSERT_EQ(3, (int32_t)result.Length); - ASSERT_STREQ("Test", result[0].Pointer); - ASSERT_STREQ("Split", result[1].Pointer); - ASSERT_STREQ("String", result[2].Pointer); + ASSERT_EQ_MSG(3, (int32_t)result.Length, "String split returned an invalid number of parts."); + ASSERT_STREQ_MSG("Test", result[0].Pointer, "String split first part is invalid."); + ASSERT_STREQ_MSG("Split", result[1].Pointer, "String split second part is invalid."); + ASSERT_STREQ_MSG("String", result[2].Pointer, "String split third part is invalid."); } -UTEST(StringFunctions, SystemLastIndexOf) +UTEST(StringFunctions, SystemLastIndexOf) { - // Arrange - auto stackMemoryArena = SystemGetStackMemoryArena(); - // Act auto result = SystemLastIndexOf("Test/String/", '/'); // Assert - ASSERT_EQ(11, result); + ASSERT_EQ_MSG(11, result, "SystemLastIndexOf returned an invalid separator position."); } -UTEST(StringFunctions, SystemFindSubString_Found) +UTEST(StringFunctions, SystemFindSubString_Found) { - // Arrange - auto stackMemoryArena = SystemGetStackMemoryArena(); - // Act auto result = SystemFindSubString("Test/String/", "String"); // Assert - ASSERT_EQ(5, result); + ASSERT_EQ_MSG(5, result, "SystemFindSubString returned an invalid substring position."); } -UTEST(StringFunctions, SystemFindSubString_NotFound) +UTEST(StringFunctions, SystemFindSubString_NotFound) { - // Arrange - auto stackMemoryArena = SystemGetStackMemoryArena(); - // Act auto result = SystemFindSubString("Test/String/", "Sutoringu"); // Assert - ASSERT_EQ(-1, result); + ASSERT_EQ_MSG(-1, result, "SystemFindSubString should return -1 when the substring is absent."); } -UTEST(StringFunctions, SystemConvertUtf8ToWideChar) +UTEST(StringFunctions, SystemConvertUtf8ToWideChar) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto testString = "Test String éé"; - + // Act auto testStringWide = SystemConvertUtf8ToWideChar(stackMemoryArena, testString); auto result = SystemConvertWideCharToUtf8(stackMemoryArena, testStringWide); // Assert - ASSERT_STREQ(testString, result.Pointer); + ASSERT_STREQ_MSG(testString, result.Pointer, "UTF-8 to wide-character roundtrip did not preserve the source string."); } From a69d13e7fe622a0ede9ff953c37b6fb2312bf0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:21:16 +0200 Subject: [PATCH 53/60] Add assertion messages to IO tests --- tests/FoundationsTests/IOTests.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/FoundationsTests/IOTests.cpp b/tests/FoundationsTests/IOTests.cpp index 73a376e9..f40bfa34 100644 --- a/tests/FoundationsTests/IOTests.cpp +++ b/tests/FoundationsTests/IOTests.cpp @@ -1,12 +1,12 @@ #include "SystemFunctions.h" #include "utest.h" -UTEST(IOFunctions, GeneralIO) +UTEST(IOFunctions, GeneralIO) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); auto fileContent = ReadOnlySpan("Test File"); - + // Act auto fileName = SystemGenerateTempFilename(stackMemoryArena, "TestFile"); SystemFileWriteBytes(fileName, Span((uint8_t*)fileContent.Pointer, fileContent.Length + 1)); @@ -16,19 +16,19 @@ UTEST(IOFunctions, GeneralIO) auto fileExistsAfterDelete = SystemFileExists(fileName); // Assert - ASSERT_TRUE(fileExists); - ASSERT_STREQ(fileContent.Pointer, (char*)result.Pointer); - ASSERT_FALSE(fileExistsAfterDelete); + ASSERT_TRUE_MSG(fileExists, "Temporary file should exist after writing it."); + ASSERT_STREQ_MSG(fileContent.Pointer, (char*)result.Pointer, "File contents should match the bytes that were written."); + ASSERT_FALSE_MSG(fileExistsAfterDelete, "Temporary file should not exist after deletion."); } -UTEST(IOFunctions, SystemGetExecutableFolderPath) +UTEST(IOFunctions, SystemGetExecutableFolderPath) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); - + // Act auto result = SystemGetExecutableFolderPath(stackMemoryArena); // Assert - ASSERT_GT((int32_t)result.Length, 0); + ASSERT_GT_MSG((int32_t)result.Length, 0, "Executable folder path should not be empty."); } From 9527cfde151180fa1340320b4fe0f25316333c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:21:24 +0200 Subject: [PATCH 54/60] Add assertion messages to process tests --- tests/FoundationsTests/LibraryProcessTests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/FoundationsTests/LibraryProcessTests.cpp b/tests/FoundationsTests/LibraryProcessTests.cpp index 331c0ef8..04606f67 100644 --- a/tests/FoundationsTests/LibraryProcessTests.cpp +++ b/tests/FoundationsTests/LibraryProcessTests.cpp @@ -1,14 +1,14 @@ #include "SystemFunctions.h" #include "utest.h" -UTEST(LibraryProcess, SystemExecuteProcess) +UTEST(LibraryProcess, SystemExecuteProcess) { // Arrange auto stackMemoryArena = SystemGetStackMemoryArena(); - + // Act auto result = SystemExecuteProcess(stackMemoryArena, "ping"); // Assert - ASSERT_GT((int32_t)result.Length, 0); + ASSERT_GT_MSG((int32_t)result.Length, 0, "SystemExecuteProcess should return non-empty process output."); } From ff83b688885888ad22e6c0f34758691cd670e1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:21:47 +0200 Subject: [PATCH 55/60] Add assertion messages to data pool tests --- tests/FoundationsTests/DataPoolTests.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/FoundationsTests/DataPoolTests.cpp b/tests/FoundationsTests/DataPoolTests.cpp index 40608f69..6698a2e4 100644 --- a/tests/FoundationsTests/DataPoolTests.cpp +++ b/tests/FoundationsTests/DataPoolTests.cpp @@ -29,7 +29,7 @@ UTEST(DataPool, AddItem) // Assert auto result = SystemGetDataPoolItem(dataPool, handle); - ASSERT_EQ(testData.Data, result->Data); + ASSERT_EQ_MSG(testData.Data, result->Data, "DataPool item data should match the value used during insertion."); } UTEST(DataPool, RemoveItem) @@ -46,7 +46,7 @@ UTEST(DataPool, RemoveItem) // Assert auto result = SystemGetDataPoolItem(dataPool, handle); - ASSERT_TRUE(result == nullptr); + ASSERT_TRUE_MSG(result == nullptr, "Removed DataPool handle should no longer resolve to an item."); } UTEST(DataPool, AddItemReuseDeletedItem) @@ -66,8 +66,8 @@ UTEST(DataPool, AddItemReuseDeletedItem) // Assert auto result = SystemGetDataPoolItem(dataPool, handle); - ASSERT_FALSE(result == nullptr); - ASSERT_EQ(testData.Data, result->Data); + ASSERT_FALSE_MSG(result == nullptr, "DataPool should reuse a slot released by a removed item."); + ASSERT_EQ_MSG(testData.Data, result->Data, "Reused DataPool slot should contain the new item data."); } UTEST(DataPool, RemoveReusedItemWithOldVersion) @@ -89,8 +89,8 @@ UTEST(DataPool, RemoveReusedItemWithOldVersion) // Assert auto result = SystemGetDataPoolItem(dataPool, newHandle); - ASSERT_FALSE(result == nullptr); - ASSERT_EQ(testData.Data, result->Data); + ASSERT_FALSE_MSG(result == nullptr, "Removing a stale DataPool handle must not remove the reused slot."); + ASSERT_EQ_MSG(testData.Data, result->Data, "Reused DataPool item should remain unchanged after stale-handle removal."); } UTEST(DataPool, AddItemWithFull) @@ -114,13 +114,13 @@ UTEST(DataPool, AddItemWithFull) // Assert auto result = SystemGetDataPoolItem(dataPool, handle); - ASSERT_EQ(testData.Data, result->Data); + ASSERT_EQ_MSG(testData.Data, result->Data, "DataPool primary item data is invalid."); auto resultFull = SystemGetDataPoolItemFull(dataPool, handle); - ASSERT_EQ(testDataFull.Data1, resultFull->Data1); - ASSERT_EQ(testDataFull.Data2, resultFull->Data2); - ASSERT_EQ(testDataFull.Data3, resultFull->Data3); - ASSERT_EQ(testDataFull.Data4, resultFull->Data4); + ASSERT_EQ_MSG(testDataFull.Data1, resultFull->Data1, "DataPool full item field Data1 is invalid."); + ASSERT_EQ_MSG(testDataFull.Data2, resultFull->Data2, "DataPool full item field Data2 is invalid."); + ASSERT_EQ_MSG(testDataFull.Data3, resultFull->Data3, "DataPool full item field Data3 is invalid."); + ASSERT_EQ_MSG(testDataFull.Data4, resultFull->Data4, "DataPool full item field Data4 is invalid."); } UTEST(DataPool, RemoveItemWithFull) @@ -146,5 +146,5 @@ UTEST(DataPool, RemoveItemWithFull) // Assert auto result = SystemGetDataPoolItemFull(dataPool, handle); - ASSERT_TRUE(result == nullptr); + ASSERT_TRUE_MSG(result == nullptr, "Removed DataPool handle should not resolve to full item data."); } From 49460452a930629db3a859a984c5efae859dbc48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:22:24 +0200 Subject: [PATCH 56/60] Add assertion messages to concurrent data pool tests --- .../DataPoolConcurrentTests.cpp | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/FoundationsTests/DataPoolConcurrentTests.cpp b/tests/FoundationsTests/DataPoolConcurrentTests.cpp index 231ed772..e7375113 100644 --- a/tests/FoundationsTests/DataPoolConcurrentTests.cpp +++ b/tests/FoundationsTests/DataPoolConcurrentTests.cpp @@ -133,7 +133,7 @@ UTEST(DataPoolConcurrent, Add) } // Assert - ASSERT_EQ(itemCount, (int32_t)SystemGetDataPoolItemCount(dataPool)); + ASSERT_EQ_MSG(itemCount, (int32_t)SystemGetDataPoolItemCount(dataPool), "Concurrent DataPool insertion should publish every requested item."); for (int32_t i = 0; i < threadCount; i++) { @@ -145,11 +145,11 @@ UTEST(DataPoolConcurrent, Add) auto data = SystemGetDataPoolItem(dataPool, threadParameter.Handles[j]); auto dataFull = SystemGetDataPoolItemFull(dataPool, threadParameter.Handles[j]); - ASSERT_EQ(expectedValue, data->Value); - ASSERT_EQ(expectedValue, dataFull->Value1); - ASSERT_EQ(expectedValue + 1, dataFull->Value2); - ASSERT_EQ(expectedValue + 2, dataFull->Value3); - ASSERT_EQ(expectedValue + 3, dataFull->Value4); + ASSERT_EQ_MSG(expectedValue, data->Value, "Concurrent DataPool insertion corrupted primary item data."); + ASSERT_EQ_MSG(expectedValue, dataFull->Value1, "Concurrent DataPool insertion corrupted full item Value1."); + ASSERT_EQ_MSG(expectedValue + 1, dataFull->Value2, "Concurrent DataPool insertion corrupted full item Value2."); + ASSERT_EQ_MSG(expectedValue + 2, dataFull->Value3, "Concurrent DataPool insertion corrupted full item Value3."); + ASSERT_EQ_MSG(expectedValue + 3, dataFull->Value4, "Concurrent DataPool insertion corrupted full item Value4."); } } @@ -204,7 +204,7 @@ UTEST(DataPoolConcurrent, AddAndRemove) } // Assert - ASSERT_EQ((size_t)itemCount / 2, SystemGetDataPoolItemCount(dataPool)); + ASSERT_EQ_MSG((size_t)itemCount / 2, SystemGetDataPoolItemCount(dataPool), "Concurrent DataPool add/remove should preserve the expected final item count."); SystemFreeMemoryArena(memoryArena); } @@ -249,13 +249,13 @@ UTEST(DataPoolConcurrent, AddStopsAtCapacity) { if (handles[j] != ELEM_HANDLE_NULL) { - ASSERT_TRUE(handleInfo.Index != UnpackSystemDataPoolHandle(handles[j]).Index); + ASSERT_TRUE_MSG(handleInfo.Index != UnpackSystemDataPoolHandle(handles[j]).Index, "Concurrent DataPool allocations must never publish the same slot twice."); } } } - ASSERT_EQ(capacity, successCount); - ASSERT_EQ((size_t)capacity, SystemGetDataPoolItemCount(dataPool)); + ASSERT_EQ_MSG(capacity, successCount, "Concurrent DataPool insertion should stop exactly at pool capacity."); + ASSERT_EQ_MSG((size_t)capacity, SystemGetDataPoolItemCount(dataPool), "DataPool item count should never exceed capacity under contention."); SystemFreeMemoryArena(memoryArena); } @@ -283,13 +283,13 @@ UTEST(DataPoolConcurrent, RemoveSameHandleOnlyFreesOnce) } // Assert - ASSERT_EQ(0llu, SystemGetDataPoolItemCount(dataPool)); + ASSERT_EQ_MSG(0llu, SystemGetDataPoolItemCount(dataPool), "Concurrent removal of one handle should decrement the pool count exactly once."); auto reusedHandle = SystemAddDataPoolItem(dataPool, DataPoolConcurrentTestData { 100 }); auto overflowHandle = SystemAddDataPoolItem(dataPool, DataPoolConcurrentTestData { 200 }); - ASSERT_TRUE(reusedHandle != ELEM_HANDLE_NULL); - ASSERT_TRUE(overflowHandle == ELEM_HANDLE_NULL); - ASSERT_EQ(1llu, SystemGetDataPoolItemCount(dataPool)); + ASSERT_TRUE_MSG(reusedHandle != ELEM_HANDLE_NULL, "Slot removed concurrently should remain reusable exactly once."); + ASSERT_TRUE_MSG(overflowHandle == ELEM_HANDLE_NULL, "A concurrently removed slot must not be recycled more than once."); + ASSERT_EQ_MSG(1llu, SystemGetDataPoolItemCount(dataPool), "DataPool count should remain consistent after duplicate concurrent removal attempts."); SystemFreeMemoryArena(memoryArena); } @@ -318,23 +318,23 @@ UTEST(DataPoolConcurrent, ReuseKeepsSlotsUnique) } // Assert - ASSERT_EQ((size_t)threadCount, SystemGetDataPoolItemCount(dataPool)); + ASSERT_EQ_MSG((size_t)threadCount, SystemGetDataPoolItemCount(dataPool), "Concurrent remove/reuse cycles should preserve the total live item count."); for (int32_t i = 0; i < threadCount; i++) { - ASSERT_FALSE(threadParameters[i].Failed); - ASSERT_TRUE(threadParameters[i].Handle != ELEM_HANDLE_NULL); + ASSERT_FALSE_MSG(threadParameters[i].Failed, "Concurrent DataPool remove/reuse unexpectedly exhausted a reusable pool."); + ASSERT_TRUE_MSG(threadParameters[i].Handle != ELEM_HANDLE_NULL, "Concurrent DataPool remove/reuse should leave every thread with a valid handle."); auto data = SystemGetDataPoolItem(dataPool, threadParameters[i].Handle); - ASSERT_TRUE(data != nullptr); - ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), data->Value); + ASSERT_TRUE_MSG(data != nullptr, "Final DataPool handle should resolve after concurrent reuse cycles."); + ASSERT_EQ_MSG((((uint64_t)i << 32) | (iterationCount - 1)), data->Value, "Concurrent DataPool reuse corrupted final item data."); auto handleInfo = UnpackSystemDataPoolHandle(threadParameters[i].Handle); for (int32_t j = i + 1; j < threadCount; j++) { auto otherHandleInfo = UnpackSystemDataPoolHandle(threadParameters[j].Handle); - ASSERT_TRUE(handleInfo.Index != otherHandleInfo.Index); + ASSERT_TRUE_MSG(handleInfo.Index != otherHandleInfo.Index, "Concurrent DataPool reuse must not assign one slot to multiple live handles."); } } From 702aa8161c822159d52ec3a215820ed1874f9b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:22:53 +0200 Subject: [PATCH 57/60] Add assertion messages to dictionary tests --- tests/FoundationsTests/DictionaryTests.cpp | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/FoundationsTests/DictionaryTests.cpp b/tests/FoundationsTests/DictionaryTests.cpp index 02026fbb..86f9a32c 100644 --- a/tests/FoundationsTests/DictionaryTests.cpp +++ b/tests/FoundationsTests/DictionaryTests.cpp @@ -22,7 +22,7 @@ UTEST(Dictionary, AddValue) // Assert auto testValue = dictionary["Test9"]; - ASSERT_EQ(9, testValue); + ASSERT_EQ_MSG(9, testValue, "Dictionary lookup should return the value stored for a string key."); } UTEST(Dictionary, AddValue_KeyStruct) @@ -42,8 +42,8 @@ UTEST(Dictionary, AddValue_KeyStruct) // Assert auto testValue = dictionary[9]; - ASSERT_EQ(9, testValue.Value1); - ASSERT_EQ(81, testValue.Value2); + ASSERT_EQ_MSG(9, testValue.Value1, "Dictionary struct value field Value1 is invalid."); + ASSERT_EQ_MSG(81, testValue.Value2, "Dictionary struct value field Value2 is invalid."); } UTEST(Dictionary, RemoveValue) @@ -67,11 +67,11 @@ UTEST(Dictionary, RemoveValue) if (i == 6) { - ASSERT_EQ(0, testValue); + ASSERT_EQ_MSG(0, testValue, "Removed dictionary key should return the default value through operator[]."); } else { - ASSERT_EQ(i, testValue); + ASSERT_EQ_MSG(i, testValue, "Removing one dictionary key should not modify other entries."); } } } @@ -97,11 +97,11 @@ UTEST(Dictionary, RemoveValueNoParent) if (i == 8) { - ASSERT_EQ(0, testValue); + ASSERT_EQ_MSG(0, testValue, "Removed dictionary bucket-head entry should return the default value."); } else { - ASSERT_EQ(i, testValue); + ASSERT_EQ_MSG(i, testValue, "Removing a dictionary bucket-head entry should preserve sibling entries."); } } } @@ -122,7 +122,7 @@ UTEST(Dictionary, RemoveValue_KeyStruct) // Assert auto testValue = dictionary[9]; - ASSERT_EQ(0, testValue); + ASSERT_EQ_MSG(0, testValue, "Removed integer dictionary key should return the default value."); } UTEST(Dictionary, GrowStorage) @@ -153,7 +153,7 @@ UTEST(Dictionary, GrowStorage) // Assert auto testValue = dictionary["TestOneMore5"]; - ASSERT_EQ(32, testValue); + ASSERT_EQ_MSG(32, testValue, "Dictionary entry reuse should preserve surviving entries."); } UTEST(Dictionary, NotEnoughStorage) @@ -172,7 +172,7 @@ UTEST(Dictionary, NotEnoughStorage) // Assert auto testValue = dictionary["TestOneMore6"]; - ASSERT_EQ(0, testValue); + ASSERT_EQ_MSG(0, testValue, "Dictionary insertion beyond capacity should not publish an entry."); } UTEST(Dictionary, RemoveValuesAfterFull) @@ -193,7 +193,7 @@ UTEST(Dictionary, RemoveValuesAfterFull) // Assert auto testValue = dictionary["TestNew"]; - ASSERT_EQ(28, testValue); + ASSERT_EQ_MSG(28, testValue, "Dictionary should reuse a removed entry after reaching capacity."); } UTEST(Dictionary, BigDictionary) @@ -214,7 +214,7 @@ UTEST(Dictionary, BigDictionary) for (int32_t i = 0; i < 10000; i++) { auto testValue = dictionary[SystemFormatString(stackMemoryArena, "Test%d", i)]; - ASSERT_EQ(i, testValue); + ASSERT_EQ_MSG(i, testValue, "Large dictionary lookup returned an invalid value."); } } @@ -233,7 +233,7 @@ UTEST(Dictionary, ContainsKey) auto testValue = SystemDictionaryContainsKey(dictionary, "Test9"); // Assert - ASSERT_TRUE(testValue); + ASSERT_TRUE_MSG(testValue, "Dictionary should report an inserted string key as present."); } UTEST(Dictionary, ContainsKey_KeyStruct) @@ -251,7 +251,7 @@ UTEST(Dictionary, ContainsKey_KeyStruct) auto testValue = SystemDictionaryContainsKey(dictionary, 9); // Assert - ASSERT_TRUE(testValue); + ASSERT_TRUE_MSG(testValue, "Dictionary should report an inserted integer key as present."); } UTEST(Dictionary, ReadOnlySpanHashUsesAllBytes) @@ -269,10 +269,10 @@ UTEST(Dictionary, ReadOnlySpanHashUsesAllBytes) // Assert auto value1 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key1, 2)); auto value2 = SystemGetDictionaryValue(dictionary, ReadOnlySpan(key2, 2)); - ASSERT_TRUE(value1 != nullptr); - ASSERT_TRUE(value2 != nullptr); - ASSERT_EQ(10, *value1); - ASSERT_EQ(20, *value2); + ASSERT_TRUE_MSG(value1 != nullptr, "Dictionary should find the first non-char span key."); + ASSERT_TRUE_MSG(value2 != nullptr, "Dictionary should find the second non-char span key."); + ASSERT_EQ_MSG(10, *value1, "Hashing a non-char span should include every byte of the first key."); + ASSERT_EQ_MSG(20, *value2, "Hashing a non-char span should include every byte of the second key."); } UTEST(Dictionary, MissingValueReturnsNull) @@ -285,6 +285,6 @@ UTEST(Dictionary, MissingValueReturnsNull) auto value = SystemGetDictionaryValue(dictionary, 42); // Assert - ASSERT_TRUE(value == nullptr); - ASSERT_EQ(0, dictionary[42]); + ASSERT_TRUE_MSG(value == nullptr, "SystemGetDictionaryValue should return nullptr for a missing key."); + ASSERT_EQ_MSG(0, dictionary[42], "Dictionary operator[] should return a default value for a missing key."); } From a8981cac24fe1a896b1342bb30d16f6c315ad9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:23:15 +0200 Subject: [PATCH 58/60] Add assertion messages to concurrent dictionary tests --- .../FoundationsTests/DictionaryConcurrentTests.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/FoundationsTests/DictionaryConcurrentTests.cpp b/tests/FoundationsTests/DictionaryConcurrentTests.cpp index 324f433c..3806c152 100644 --- a/tests/FoundationsTests/DictionaryConcurrentTests.cpp +++ b/tests/FoundationsTests/DictionaryConcurrentTests.cpp @@ -100,7 +100,7 @@ UTEST(DictionaryConcurrent, Add) } } - ASSERT_EQ(itemCount, count); + ASSERT_EQ_MSG(itemCount, count, "Concurrent dictionary insertion should publish every requested entry."); SystemFreeMemoryArena(memoryArena); } @@ -144,7 +144,7 @@ UTEST(DictionaryConcurrent, Remove) } } - ASSERT_EQ(itemCount / 2, count); + ASSERT_EQ_MSG(itemCount / 2, count, "Concurrent dictionary removal should remove exactly the requested half of the entries."); SystemFreeMemoryArena(memoryArena); } @@ -179,13 +179,13 @@ UTEST(DictionaryConcurrent, AddStopsAtCapacity) if (SystemDictionaryContainsKey(dictionary, i)) { auto value = SystemGetDictionaryValue(dictionary, i); - ASSERT_TRUE(value != nullptr); - ASSERT_EQ(i, *value); + ASSERT_TRUE_MSG(value != nullptr, "A key reported as present after concurrent insertion should resolve to a value."); + ASSERT_EQ_MSG(i, *value, "Concurrent dictionary insertion should preserve each published key/value pair."); foundCount++; } } - ASSERT_EQ(capacity, foundCount); + ASSERT_EQ_MSG(capacity, foundCount, "Concurrent dictionary insertion should stop exactly at dictionary capacity."); SystemFreeMemoryArena(memoryArena); } @@ -218,8 +218,8 @@ UTEST(DictionaryConcurrent, ReusePreservesAllEntries) for (int32_t i = 0; i < threadCount; i++) { auto value = SystemGetDictionaryValue(dictionary, threadParameters[i].CurrentKey); - ASSERT_TRUE(value != nullptr); - ASSERT_EQ((((uint64_t)i << 32) | (iterationCount - 1)), *value); + ASSERT_TRUE_MSG(value != nullptr, "Final dictionary entry should remain reachable after concurrent remove/reuse cycles."); + ASSERT_EQ_MSG((((uint64_t)i << 32) | (iterationCount - 1)), *value, "Concurrent dictionary remove/reuse corrupted the final entry value."); } SystemFreeMemoryArena(memoryArena); From ac0bfbf58945a5e99800afa74783f7a5b714c187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:23:59 +0200 Subject: [PATCH 59/60] Add assertion messages to memory tests --- tests/FoundationsTests/MemoryTests.cpp | 80 +++++++++++++------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/FoundationsTests/MemoryTests.cpp b/tests/FoundationsTests/MemoryTests.cpp index 0b6b71b9..50208d90 100644 --- a/tests/FoundationsTests/MemoryTests.cpp +++ b/tests/FoundationsTests/MemoryTests.cpp @@ -14,8 +14,8 @@ UTEST(Memory, Allocate) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(dataSizeInBytes, allocationInfos.AllocatedBytes); - ASSERT_EQ(dataSizeInBytes, data.Length); + ASSERT_EQ_MSG(dataSizeInBytes, allocationInfos.AllocatedBytes, "MemoryArena allocated byte count should match the requested allocation size."); + ASSERT_EQ_MSG(dataSizeInBytes, data.Length, "Allocated array length should match the requested element count."); } UTEST(Memory, AllocateMultiple) @@ -30,8 +30,8 @@ UTEST(Memory, AllocateMultiple) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(dataSizeInBytes + 1024, allocationInfos.AllocatedBytes); - ASSERT_GT(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(dataSizeInBytes + 1024, allocationInfos.AllocatedBytes, "MemoryArena allocated byte count should include every pushed allocation."); + ASSERT_GT_MSG(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes, "Committed memory should include page granularity and arena metadata overhead."); } UTEST(Memory, ClearMemoryArena) @@ -48,7 +48,7 @@ UTEST(Memory, ClearMemoryArena) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(0llu, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(0llu, allocationInfos.AllocatedBytes, "Clearing a MemoryArena should reset its logical allocated byte count to zero."); } UTEST(Memory, AllocateCheckAlignment) @@ -63,7 +63,7 @@ UTEST(Memory, AllocateCheckAlignment) auto data = SystemPushArrayZero(memoryArena, dataSizeInBytes); // Assert - ASSERT_TRUE(((size_t)data.Pointer & (alignment - 1)) == 0); + ASSERT_TRUE_MSG(((size_t)data.Pointer & (alignment - 1)) == 0, "MemoryArena allocations should respect the default alignment."); } UTEST(Memory, PushOverflowReturnsNull) @@ -78,14 +78,14 @@ UTEST(Memory, PushOverflowReturnsNull) auto overflowArray = SystemPushArray(memoryArena, 2, AllocationState_Reserved); // Assert - ASSERT_TRUE(allocation != nullptr); - ASSERT_TRUE(overflowAllocation == nullptr); - ASSERT_TRUE(zeroOverflowAllocation == nullptr); - ASSERT_TRUE(overflowArray.Pointer == nullptr); - ASSERT_EQ(0llu, overflowArray.Length); + ASSERT_TRUE_MSG(allocation != nullptr, "An allocation that exactly fills the MemoryArena should succeed."); + ASSERT_TRUE_MSG(overflowAllocation == nullptr, "MemoryArena push beyond capacity should return nullptr."); + ASSERT_TRUE_MSG(zeroOverflowAllocation == nullptr, "Zero-initialized push beyond MemoryArena capacity should return nullptr."); + ASSERT_TRUE_MSG(overflowArray.Pointer == nullptr, "Array push beyond MemoryArena capacity should return an empty Span."); + ASSERT_EQ_MSG(0llu, overflowArray.Length, "Failed array allocation should return a zero-length Span."); auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(64llu, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(64llu, allocationInfos.AllocatedBytes, "Failed pushes should not advance the MemoryArena beyond capacity."); } UTEST(Memory, ArenaSizeOverflowReturnsEmptyHandle) @@ -94,7 +94,7 @@ UTEST(Memory, ArenaSizeOverflowReturnsEmptyHandle) auto memoryArena = SystemAllocateMemoryArena(SIZE_MAX); // Assert - ASSERT_TRUE(memoryArena.Storage == nullptr); + ASSERT_TRUE_MSG(memoryArena.Storage == nullptr, "MemoryArena allocation should fail when the requested capacity overflows internal size calculations."); } UTEST(Memory, PushSizeOverflowDoesNotAdvanceArena) @@ -107,10 +107,10 @@ UTEST(Memory, PushSizeOverflowDoesNotAdvanceArena) auto array = SystemPushArray(memoryArena, SIZE_MAX / sizeof(uint64_t) + 1, AllocationState_Reserved); // Assert - ASSERT_TRUE(allocation == nullptr); - ASSERT_TRUE(array.Pointer == nullptr); - ASSERT_EQ(0llu, array.Length); - ASSERT_EQ(0llu, SystemGetMemoryArenaAllocationInfos(memoryArena).AllocatedBytes); + ASSERT_TRUE_MSG(allocation == nullptr, "Memory push should reject a size that overflows alignment or range calculations."); + ASSERT_TRUE_MSG(array.Pointer == nullptr, "Array push should reject an element count whose byte size overflows."); + ASSERT_EQ_MSG(0llu, array.Length, "Overflowing array allocation should return a zero-length Span."); + ASSERT_EQ_MSG(0llu, SystemGetMemoryArenaAllocationInfos(memoryArena).AllocatedBytes, "Rejected overflow allocations should not advance the MemoryArena."); } UTEST(Memory, CommitReportsInvalidRange) @@ -124,12 +124,12 @@ UTEST(Memory, CommitReportsInvalidRange) auto invalidCommit = SystemCommitMemory(memoryArena, allocation.Pointer + allocation.Length, 8); // Assert - ASSERT_TRUE(validCommit); - ASSERT_FALSE(invalidCommit); + ASSERT_TRUE_MSG(validCommit, "Committing a valid reserved MemoryArena range should succeed."); + ASSERT_FALSE_MSG(invalidCommit, "Committing a range outside the MemoryArena allocation should fail."); for (size_t i = 0; i < allocation.Length; i++) { - ASSERT_EQ(0, allocation[i]); + ASSERT_EQ_MSG(0, allocation[i], "Commit with clearMemory should zero newly committed memory."); } SystemFreeMemoryArena(memoryArena); @@ -144,7 +144,7 @@ UTEST(Memory, ConcatBuffers) auto result = SystemConcatBuffers(memoryArena, "Test1", "Test2"); // Assert - ASSERT_STREQ("Test1Test2", result.Pointer); + ASSERT_STREQ_MSG("Test1Test2", result.Pointer, "Concatenated character buffer content is invalid."); } UTEST(Memory, StackMemoryArena) @@ -195,10 +195,10 @@ UTEST(Memory, StackMemoryArena) auto string4 = SystemConcatBuffers(stackMemoryArena1, "Test3", "Stack1"); // Assert - ASSERT_STREQ("TestStack1", string1.Pointer); - ASSERT_STREQ("Test2Stack1", string2.Pointer); - ASSERT_STREQ("Test3Stack1", string4.Pointer); - ASSERT_STREQ("Test4Stack1", string5.Pointer); + ASSERT_STREQ_MSG("TestStack1", string1.Pointer, "Root stack-lifetime allocation should survive nested stack scopes."); + ASSERT_STREQ_MSG("Test2Stack1", string2.Pointer, "Allocation made through an ancestor stack arena should survive younger scopes."); + ASSERT_STREQ_MSG("Test3Stack1", string4.Pointer, "Root stack arena should remain usable after nested scopes are released."); + ASSERT_STREQ_MSG("Test4Stack1", string5.Pointer, "Deep ancestor allocation should preserve the ancestor stack lifetime."); } UTEST(Memory, StackMemoryArenaRelease) @@ -245,11 +245,11 @@ UTEST(Memory, StackMemoryArenaRelease) } // Assert - ASSERT_STREQ("TestStack1", string1.Pointer); - ASSERT_STREQ("Test2Stack1", string2.Pointer); - ASSERT_STREQ("Test3Stack1", string3.Pointer); - ASSERT_STREQ("Test4Stack1", string4.Pointer); - ASSERT_STREQ("Test5Stack1", string5.Pointer); + ASSERT_STREQ_MSG("TestStack1", string1.Pointer, "Root stack allocation should survive every nested rollback."); + ASSERT_STREQ_MSG("Test2Stack1", string2.Pointer, "Ancestor allocation should survive the scope in which it was requested."); + ASSERT_STREQ_MSG("Test3Stack1", string3.Pointer, "Root lifetime allocation should remain valid after child scope release."); + ASSERT_STREQ_MSG("Test4Stack1", string4.Pointer, "Sibling stack scopes should restore offsets without corrupting ancestor allocations."); + ASSERT_STREQ_MSG("Test5Stack1", string5.Pointer, "Copied ancestor MemoryArena handle should preserve ancestor allocation lifetime."); } UTEST(Memory, StackAncestorAllocationUsesExtraStorageCapacity) @@ -266,8 +266,8 @@ UTEST(Memory, StackAncestorAllocationUsesExtraStorageCapacity) } // Assert - ASSERT_TRUE(mainAllocation != nullptr); - ASSERT_TRUE(ancestorAllocation != nullptr); + ASSERT_TRUE_MSG(mainAllocation != nullptr, "Large root stack allocation should fit in the main stack arena storage."); + ASSERT_TRUE_MSG(ancestorAllocation != nullptr, "Ancestor stack allocation should use extra storage when main stack storage is exhausted."); } UTEST(Memory, AllocateReserved) @@ -281,8 +281,8 @@ UTEST(Memory, AllocateReserved) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(dataSizeInBytes, allocationInfos.AllocatedBytes); - ASSERT_LT(allocationInfos.CommittedBytes, allocationInfos.MaximumSizeInBytes); + ASSERT_EQ_MSG(dataSizeInBytes, allocationInfos.AllocatedBytes, "Reserved MemoryArena allocation should advance the logical allocated byte count."); + ASSERT_LT_MSG(allocationInfos.CommittedBytes, allocationInfos.MaximumSizeInBytes, "Reserved allocation should not commit the entire MemoryArena capacity."); } UTEST(Memory, AllocateReservedCommit) @@ -313,9 +313,9 @@ UTEST(Memory, AllocateReservedCommit) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(dataSizeInBytes, allocationInfos.AllocatedBytes); - ASSERT_EQ(maxSizeInBytes, allocationInfos.MaximumSizeInBytes); - ASSERT_LT(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(dataSizeInBytes, allocationInfos.AllocatedBytes, "Committing reserved ranges should not change the logical allocation size."); + ASSERT_EQ_MSG(maxSizeInBytes, allocationInfos.MaximumSizeInBytes, "MemoryArena maximum data capacity should remain unchanged after commits."); + ASSERT_LT_MSG(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes, "Committing small subranges should not commit the entire reserved allocation."); } UTEST(Memory, AllocateReservedDecommit) @@ -347,7 +347,7 @@ UTEST(Memory, AllocateReservedDecommit) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(dataSizeInBytes, allocationInfos.AllocatedBytes); - ASSERT_EQ(maxSizeInBytes, allocationInfos.MaximumSizeInBytes); - ASSERT_LT(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(dataSizeInBytes, allocationInfos.AllocatedBytes, "Decommitting memory should not release the logical MemoryArena allocation."); + ASSERT_EQ_MSG(maxSizeInBytes, allocationInfos.MaximumSizeInBytes, "Decommitting memory should not change MemoryArena capacity."); + ASSERT_LT_MSG(allocationInfos.CommittedBytes, allocationInfos.AllocatedBytes, "Decommitting reserved ranges should leave only the required committed pages."); } From 9ec166f4c2bbe37cbeaef192cd5829a0a3512b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Decroy=C3=A8re?= Date: Sun, 6 Sep 2026 10:24:23 +0200 Subject: [PATCH 60/60] Add assertion messages to concurrent memory tests --- .../MemoryConcurrentTests.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/FoundationsTests/MemoryConcurrentTests.cpp b/tests/FoundationsTests/MemoryConcurrentTests.cpp index 4851b2ff..69717f4f 100644 --- a/tests/FoundationsTests/MemoryConcurrentTests.cpp +++ b/tests/FoundationsTests/MemoryConcurrentTests.cpp @@ -101,7 +101,7 @@ UTEST(MemoryConcurrent, Push) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(maxSize, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(maxSize, allocationInfos.AllocatedBytes, "Concurrent MemoryArena pushes should reserve every requested byte exactly once."); } UTEST(MemoryConcurrent, PushDoesNotOverflow) @@ -144,16 +144,16 @@ UTEST(MemoryConcurrent, PushDoesNotOverflow) { if (results[j] != nullptr) { - ASSERT_TRUE(results[i] != results[j]); + ASSERT_TRUE_MSG(results[i] != results[j], "Concurrent MemoryArena pushes must never return the same allocation address twice."); } } } } - ASSERT_EQ(capacityCount, successCount); + ASSERT_EQ_MSG(capacityCount, successCount, "Concurrent MemoryArena pushes should stop exactly at arena capacity."); auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(capacityCount * allocationSizeInBytes, allocationInfos.AllocatedBytes); + ASSERT_EQ_MSG(capacityCount * allocationSizeInBytes, allocationInfos.AllocatedBytes, "Concurrent overflow attempts must not advance the MemoryArena beyond capacity."); } UTEST(MemoryConcurrent, CommitSharedPage) @@ -183,13 +183,13 @@ UTEST(MemoryConcurrent, CommitSharedPage) // Assert auto allocationInfos = SystemGetMemoryArenaAllocationInfos(memoryArena); - ASSERT_EQ(committedBytesBefore + pageSizeInBytes, allocationInfos.CommittedBytes); + ASSERT_EQ_MSG(committedBytesBefore + pageSizeInBytes, allocationInfos.CommittedBytes, "Concurrent commits within one data page should commit that page exactly once."); for (int32_t i = 0; i < threadCount; i++) { for (size_t j = 0; j < rangeSizeInBytes; j++) { - ASSERT_EQ((uint8_t)(i + 1), buffer[i * rangeSizeInBytes + j]); + ASSERT_EQ_MSG((uint8_t)(i + 1), buffer[i * rangeSizeInBytes + j], "Concurrent shared-page commits should preserve each thread's written range."); } } } @@ -220,12 +220,12 @@ UTEST(MemoryConcurrent, ArenaAllocationAccounting) // Assert for (int32_t i = 0; i < threadCount; i++) { - ASSERT_TRUE(memoryArenas[i].Storage != nullptr); + ASSERT_TRUE_MSG(memoryArenas[i].Storage != nullptr, "Concurrent MemoryArena creation should return valid storage for every thread."); } auto allocationInfosAfterAllocate = SystemGetAllocationInfos(); - ASSERT_EQ(allocationInfosBefore.ReservedBytes + threadCount * pageSizeInBytes * 2, allocationInfosAfterAllocate.ReservedBytes); - ASSERT_EQ(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterAllocate.CommittedBytes); + ASSERT_EQ_MSG(allocationInfosBefore.ReservedBytes + threadCount * pageSizeInBytes * 2, allocationInfosAfterAllocate.ReservedBytes, "Concurrent MemoryArena creation should update reserved-byte accounting exactly once per arena."); + ASSERT_EQ_MSG(allocationInfosBefore.CommittedBytes + threadCount * pageSizeInBytes, allocationInfosAfterAllocate.CommittedBytes, "Concurrent MemoryArena creation should account for each committed header exactly once."); for (int32_t i = 0; i < threadCount; i++) { @@ -233,6 +233,6 @@ UTEST(MemoryConcurrent, ArenaAllocationAccounting) } auto allocationInfosAfterFree = SystemGetAllocationInfos(); - ASSERT_EQ(allocationInfosBefore.ReservedBytes, allocationInfosAfterFree.ReservedBytes); - ASSERT_EQ(allocationInfosBefore.CommittedBytes, allocationInfosAfterFree.CommittedBytes); + ASSERT_EQ_MSG(allocationInfosBefore.ReservedBytes, allocationInfosAfterFree.ReservedBytes, "Freeing concurrently created arenas should restore reserved-byte accounting to the baseline."); + ASSERT_EQ_MSG(allocationInfosBefore.CommittedBytes, allocationInfosAfterFree.CommittedBytes, "Freeing concurrently created arenas should restore committed-byte accounting to the baseline."); }