package runtime import "core:intrinsics" DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE :: uint(DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE) Memory_Block :: struct { prev: ^Memory_Block, allocator: Allocator, base: [^]byte, used: uint, capacity: uint, } Arena :: struct { backing_allocator: Allocator, curr_block: ^Memory_Block, total_used: uint, total_capacity: uint, minimum_block_size: uint, temp_count: uint, } @(private, require_results) safe_add :: #force_inline proc "contextless" (x, y: uint) -> (uint, bool) { z, did_overflow := intrinsics.overflow_add(x, y) return z, !did_overflow } @(require_results) memory_block_alloc :: proc(allocator: Allocator, capacity: uint, loc := #caller_location) -> (block: ^Memory_Block, err: Allocator_Error) { total_size := uint(capacity + size_of(Memory_Block)) base_offset := uintptr(size_of(Memory_Block)) min_alignment: int = max(16, align_of(Memory_Block)) data := mem_alloc(int(total_size), min_alignment, allocator, loc) or_return block = (^Memory_Block)(raw_data(data)) end := uintptr(raw_data(data)[len(data):]) block.allocator = allocator block.base = ([^]byte)(uintptr(block) + base_offset) block.capacity = uint(end - uintptr(block.base)) // Should be zeroed assert(block.used == 0) assert(block.prev == nil) return } memory_block_dealloc :: proc(block_to_free: ^Memory_Block, loc := #caller_location) { if block_to_free != nil { allocator := block_to_free.allocator mem_free(block_to_free, allocator, loc) } } @(require_results) alloc_from_memory_block :: proc(block: ^Memory_Block, min_size, alignment: uint) -> (data: []byte, err: Allocator_Error) { calc_alignment_offset :: proc "contextless" (block: ^Memory_Block, alignment: uintptr) -> uint { alignment_offset := uint(0) ptr := uintptr(block.base[block.used:]) mask := alignment-1 if ptr & mask != 0 { alignment_offset = uint(alignment - (ptr & mask)) } return alignment_offset } if block == nil { return nil, .Out_Of_Memory } alignment_offset := calc_alignment_offset(block, uintptr(alignment)) size, size_ok := safe_add(min_size, alignment_offset) if !size_ok { err = .Out_Of_Memory return } if to_be_used, ok := safe_add(block.used, size); !ok || to_be_used > block.capacity { err = .Out_Of_Memory return } data = block.base[block.used+alignment_offset:][:min_size] block.used += size return } @(require_results) arena_alloc :: proc(arena: ^Arena, size, alignment: uint, loc := #caller_location) -> (data: []byte, err: Allocator_Error) { align_forward_uint :: proc "contextless" (ptr, align: uint) -> uint { p := ptr modulo := p & (align-1) if modulo != 0 { p += align - modulo } return p } assert(alignment & (alignment-1) == 0, "non-power of two alignment", loc) size := size if size == 0 { return } if arena.curr_block == nil || (safe_add(arena.curr_block.used, size) or_else 0) > arena.curr_block.capacity { size = align_forward_uint(size, alignment) if arena.minimum_block_size == 0 { arena.minimum_block_size = DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE } block_size := max(size, arena.minimum_block_size) if arena.backing_allocator.procedure == nil { arena.backing_allocator = default_allocator() } new_block := memory_block_alloc(arena.backing_allocator, block_size, loc) or_return new_block.prev = arena.curr_block arena.curr_block = new_block arena.total_capacity += new_block.capacity } prev_used := arena.curr_block.used data, err = alloc_from_memory_block(arena.curr_block, size, alignment) arena.total_used += arena.curr_block.used - prev_used return } // `arena_init` will initialize the arena with a usuable block. // This procedure is not necessary to use the Arena as the default zero as `arena_alloc` will set things up if necessary @(require_results) arena_init :: proc(arena: ^Arena, size: uint, backing_allocator: Allocator, loc := #caller_location) -> Allocator_Error { arena^ = {} arena.backing_allocator = backing_allocator arena.minimum_block_size = max(size, 1<<12) // minimum block size of 4 KiB new_block := memory_block_alloc(arena.backing_allocator, arena.minimum_block_size, loc) or_return arena.curr_block = new_block arena.total_capacity += new_block.capacity return nil } arena_free_last_memory_block :: proc(arena: ^Arena, loc := #caller_location) { if free_block := arena.curr_block; free_block != nil { arena.curr_block = free_block.prev arena.total_capacity -= free_block.capacity memory_block_dealloc(free_block, loc) } } // `arena_free_all` will free all but the first memory block, and then reset the memory block arena_free_all :: proc(arena: ^Arena, loc := #caller_location) { for arena.curr_block != nil && arena.curr_block.prev != nil { arena_free_last_memory_block(arena, loc) } if arena.curr_block != nil { intrinsics.mem_zero(arena.curr_block.base, arena.curr_block.used) arena.curr_block.used = 0 } arena.total_used = 0 } arena_destroy :: proc(arena: ^Arena, loc := #caller_location) { for arena.curr_block != nil { free_block := arena.curr_block arena.curr_block = free_block.prev arena.total_capacity -= free_block.capacity memory_block_dealloc(free_block, loc) } arena.total_used = 0 arena.total_capacity = 0 } arena_allocator :: proc(arena: ^Arena) -> Allocator { return Allocator{arena_allocator_proc, arena} } arena_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode, size, alignment: int, old_memory: rawptr, old_size: int, location := #caller_location) -> (data: []byte, err: Allocator_Error) { arena := (^Arena)(allocator_data) size, alignment := uint(size), uint(alignment) old_size := uint(old_size) switch mode { case .Alloc, .Alloc_Non_Zeroed: return arena_alloc(arena, size, alignment, location) case .Free: err = .Mode_Not_Implemented case .Free_All: arena_free_all(arena, location) case .Resize: old_data := ([^]byte)(old_memory) switch { case old_data == nil: return arena_alloc(arena, size, alignment, location) case size == old_size: // return old memory data = old_data[:size] return case size == 0: err = .Mode_Not_Implemented return case (uintptr(old_data) & uintptr(alignment-1) == 0) && size < old_size: // shrink data in-place data = old_data[:size] return } new_memory := arena_alloc(arena, size, alignment, location) or_return if new_memory == nil { return } copy(new_memory, old_data[:old_size]) return new_memory, nil case .Query_Features: set := (^Allocator_Mode_Set)(old_memory) if set != nil { set^ = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features} } case .Query_Info: err = .Mode_Not_Implemented } return } Arena_Temp :: struct { arena: ^Arena, block: ^Memory_Block, used: uint, } @(require_results) arena_temp_begin :: proc(arena: ^Arena, loc := #caller_location) -> (temp: Arena_Temp) { assert(arena != nil, "nil arena", loc) temp.arena = arena temp.block = arena.curr_block if arena.curr_block != nil { temp.used = arena.curr_block.used } arena.temp_count += 1 return } arena_temp_end :: proc(temp: Arena_Temp, loc := #caller_location) { if temp.arena == nil { assert(temp.block == nil) assert(temp.used == 0) return } arena := temp.arena if temp.block != nil { memory_block_found := false for block := arena.curr_block; block != nil; block = block.prev { if block == temp.block { memory_block_found = true break } } if !memory_block_found { assert(arena.curr_block == temp.block, "memory block stored within Arena_Temp not owned by Arena", loc) } for arena.curr_block != temp.block { arena_free_last_memory_block(arena) } if block := arena.curr_block; block != nil { assert(block.used >= temp.used, "out of order use of arena_temp_end", loc) amount_to_zero := min(block.used-temp.used, block.capacity-block.used) intrinsics.mem_zero(block.base[temp.used:], amount_to_zero) block.used = temp.used } } assert(arena.temp_count > 0, "double-use of arena_temp_end", loc) arena.temp_count -= 1 } // Ignore the use of a `arena_temp_begin` entirely arena_temp_ignore :: proc(temp: Arena_Temp, loc := #caller_location) { assert(temp.arena != nil, "nil arena", loc) arena := temp.arena assert(arena.temp_count > 0, "double-use of arena_temp_end", loc) arena.temp_count -= 1 } arena_check_temp :: proc(arena: ^Arena, loc := #caller_location) { assert(arena.temp_count == 0, "Arena_Temp not been ended", loc) }