diff --git a/include/nd-wfc/wfc.hpp b/include/nd-wfc/wfc.hpp index bd525c7..2b3f3b5 100644 --- a/include/nd-wfc/wfc.hpp +++ b/include/nd-wfc/wfc.hpp @@ -11,7 +11,8 @@ #include #include #include -#include + +#include "wfc_allocator.hpp" namespace WFC { @@ -144,7 +145,7 @@ template class Wave { public: Wave() = default; - Wave(size_t size, size_t variableAmount) : m_data(size) + Wave(size_t size, size_t variableAmount, WFCStackAllocator& allocator) : m_data(size, WFCStackAllocatorAdapter(allocator)) { for (auto& wave : m_data) wave = (1 << variableAmount) - 1; } @@ -163,7 +164,7 @@ public: MaskType GetMask(size_t index) const { return m_data[index]; } private: - std::vector m_data; + WFCVector m_data; }; /** @@ -175,7 +176,7 @@ public: using MaskType = typename VariableIDMapT::MaskType; public: - Constrainer(Wave& wave, std::queue& propagationQueue) + Constrainer(Wave& wave, WFCQueue& propagationQueue) : m_wave(wave) , m_propagationQueue(propagationQueue) {} @@ -224,7 +225,7 @@ private: private: Wave& m_wave; - std::queue& m_propagationQueue; + WFCQueue& m_propagationQueue; }; /** @@ -255,14 +256,17 @@ public: public: struct SolverState { WorldT& world; - std::queue propagationQueue{}; - Wave wave{}; + WFCQueue propagationQueue; + Wave wave; std::mt19937& rng; + WFCStackAllocator& allocator; - SolverState(WorldT& world, size_t variableAmount, std::mt19937& rng) + SolverState(WorldT& world, size_t variableAmount, std::mt19937& rng, WFCStackAllocator& allocator) : world(world) - , wave(world.size(), variableAmount) + , propagationQueue{ WFCStackAllocatorAdapter(allocator) } + , wave{ world.size(), variableAmount, allocator } , rng(rng) + , allocator(allocator) {} SolverState(const SolverState& other) = default; @@ -276,10 +280,9 @@ public: public: bool Run(WorldT& world, bool propagateInitialValues = false) { - //auto seed = std::random_device{}(); - auto seed = 1844803044ul; - std::mt19937 random{ seed }; - SolverState state(world, m_variables.size(), random); + WFCStackAllocator allocator{}; + std::mt19937 random{ std::random_device{}() }; + SolverState state(world, m_variables.size(), random, allocator); return Run(state, propagateInitialValues); } @@ -388,6 +391,7 @@ private: { // copy the state and branch out + auto stackFrame = state.allocator.createFrame(); SolverState newState(state); newState.wave.Collapse(minEntropyCell, 1 << selectedValue); newState.propagationQueue.push(minEntropyCell); diff --git a/include/nd-wfc/wfc_allocator.hpp b/include/nd-wfc/wfc_allocator.hpp new file mode 100644 index 0000000..f65fbc1 --- /dev/null +++ b/include/nd-wfc/wfc_allocator.hpp @@ -0,0 +1,245 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace WFC { + +/** + * @brief Stack allocator specifically designed for WFC branching operations + * + * This allocator uses a stack-based approach where memory is allocated in chunks + * and automatically deallocated when branches go out of scope. It's optimized + * for the recursive branching pattern used in the WFC algorithm. + */ +class WFCStackAllocator { +private: + struct Block { + void* ptr; + size_t size; + size_t alignment; + + Block() : ptr(nullptr), size(0), alignment(0) {} + Block(void* p, size_t s, size_t a) : ptr(p), size(s), alignment(a) {} + }; + +public: + /** + * @brief Construct allocator with initial capacity + * @param initialCapacity Initial memory pool size in bytes + */ + explicit WFCStackAllocator(size_t initialCapacity = 1024 * 1024) // 1MB default + : m_capacity(initialCapacity) + , m_used(0) + , m_pool(nullptr) + { + m_pool = std::aligned_alloc(64, m_capacity); // 64-byte alignment for cache efficiency + if (!m_pool) { + throw std::bad_alloc(); + } + } + + ~WFCStackAllocator() { + if (m_pool) { + std::free(m_pool); + } + } + + // Non-copyable, non-movable for safety + WFCStackAllocator(const WFCStackAllocator&) = delete; + WFCStackAllocator& operator=(const WFCStackAllocator&) = delete; + WFCStackAllocator(WFCStackAllocator&&) = delete; + WFCStackAllocator& operator=(WFCStackAllocator&&) = delete; + + /** + * @brief Allocate memory from the stack + * @param size Number of bytes to allocate + * @param alignment Memory alignment requirement (default 8) + * @return Pointer to allocated memory + */ + void* allocate(size_t size, size_t alignment = 8) { + // Align the current position + size_t alignedUsed = alignUp(m_used, alignment); + + // Check if we have enough space + if (alignedUsed + size > m_capacity) { + // Grow the pool if needed + growPool(alignedUsed + size); + } + + void* ptr = static_cast(m_pool) + alignedUsed; + m_used = alignedUsed + size; + + // Track this allocation for proper cleanup + m_allocations.emplace_back(ptr, size, alignment); + + return ptr; + } + + /** + * @brief Deallocate memory (no-op as requested - memory freed when branch goes out of scope) + * @param ptr Pointer to deallocate + */ + void deallocate(void*) { + // No-op as requested - deallocation happens when branch goes out of scope + // The stack nature ensures automatic cleanup + } + + /** + * @brief Create a stack frame marker for RAII-based cleanup + * @return StackFrame object that will cleanup on destruction + */ + class StackFrame { + private: + WFCStackAllocator& m_allocator; + size_t m_savedUsed; + size_t m_savedAllocCount; + + public: + StackFrame(WFCStackAllocator& allocator) + : m_allocator(allocator) + , m_savedUsed(allocator.m_used) + , m_savedAllocCount(allocator.m_allocations.size()) + {} + + ~StackFrame() { + // Restore the stack to the previous state + m_allocator.m_used = m_savedUsed; + m_allocator.m_allocations.resize(m_savedAllocCount); + } + + // Non-copyable, movable + StackFrame(const StackFrame&) = delete; + StackFrame& operator=(const StackFrame&) = delete; + StackFrame(StackFrame&&) = default; + StackFrame& operator=(StackFrame&&) = default; + }; + + /** + * @brief Create a new stack frame for a branch + */ + StackFrame createFrame() { + return StackFrame(*this); + } + + /** + * @brief Get current memory usage + */ + size_t getUsed() const { return m_used; } + + /** + * @brief Get total capacity + */ + size_t getCapacity() const { return m_capacity; } + + /** + * @brief Get allocation count + */ + size_t getAllocationCount() const { return m_allocations.size(); } + + /** + * @brief Reset the allocator (useful for reusing between WFC runs) + */ + void reset() { + m_used = 0; + m_allocations.clear(); + } + +private: + size_t alignUp(size_t value, size_t alignment) const { + return (value + alignment - 1) & ~(alignment - 1); + } + + void growPool(size_t requiredSize) { + size_t newCapacity = std::max(m_capacity * 2, requiredSize); + + void* newPool = std::aligned_alloc(64, newCapacity); + if (!newPool) { + throw std::bad_alloc(); + } + + // Copy existing data + std::memcpy(newPool, m_pool, m_used); + + // Update all existing pointers in allocations + ptrdiff_t offset = static_cast(newPool) - static_cast(m_pool); + for (auto& block : m_allocations) { + block.ptr = static_cast(block.ptr) + offset; + } + + std::free(m_pool); + m_pool = newPool; + m_capacity = newCapacity; + } + +private: + size_t m_capacity; + size_t m_used; + void* m_pool; + std::vector m_allocations; +}; + +/** + * @brief Custom allocator adapter for STL containers using WFCStackAllocator + */ +template +class WFCStackAllocatorAdapter { +public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + template + struct rebind { + using other = WFCStackAllocatorAdapter; + }; + + WFCStackAllocatorAdapter(WFCStackAllocator& allocator) + : m_allocator(&allocator) {} + + template + WFCStackAllocatorAdapter(const WFCStackAllocatorAdapter& other) + : m_allocator(other.m_allocator) {} + + pointer allocate(size_type n) { + return static_cast(m_allocator->allocate(n * sizeof(T), alignof(T))); + } + + void deallocate(pointer ptr, size_type) { + m_allocator->deallocate(ptr); + } + + template + bool operator==(const WFCStackAllocatorAdapter& other) const { + return m_allocator == other.m_allocator; + } + + template + bool operator!=(const WFCStackAllocatorAdapter& other) const { + return !(*this == other); + } + + WFCStackAllocator* m_allocator; +}; + +/** + * @brief Stack-allocated vector using WFCStackAllocator + */ +template +using WFCVector = std::vector>; + +/** + * @brief Stack-allocated queue using WFCStackAllocator + */ +template +using WFCQueue = std::queue>>; + +} // namespace WFC diff --git a/prompts/5-allocator b/prompts/5-allocator new file mode 100644 index 0000000..cfce194 --- /dev/null +++ b/prompts/5-allocator @@ -0,0 +1 @@ +This is a repo for Wafe Function Collapse. The logic is already implemented. There is branching logic that creates a new state and allocates a lot of memory. I want you to create an "stack" allocator specifically for the wfc algorithm that the program can use to quickly allocate new branches. The deallocation should only occur when a branch goes out of scope, so no need to have logic in the deallocate() function. \ No newline at end of file