The public API of zig-offset-allocator. The doc comments in src/ give each
function's details. The README has a short example.
Allocatoruses 32-bit node indices;Allocator16uses 16-bit ones (the original'sUSE_16_BIT_NODE_INDICES), which halves the metadata and allows at most 65536 nodes. Both areAllocatorOf(NodeIndex)and can be used in one program.init(gpa, size, max_allocs)anddeinit(gpa).max_allocsis the node count, at least 2: each allocation and each free region uses one, and one is held back.default_max_allocs(128 * 1024) is the original's default.allocate(size) error{NoSpace}!Allocation. It fails when no free region's bin reaches the size rounded up to a bin, or when no node is left. It is aninlinefunction, so that the result comes back in registers (seeBENCHMARKS.md); it cannot be taken as a function pointer.free(allocation). Freeing twice is illegal (asserted, as in the original).reset()frees everything. AnAllocationis valid until it is freed or the allocator is reset; the core does not check, as the original does not (Rangesdoes).allocationSize(allocation); 0 forAllocation.none.storageReport(): the total free space and the largest free region, rounded down to its bin's size (a request of that size succeeds while a node is left); both 0 when out of nodes.storageReportFull(): the size and number of free regions of each of the 256 bins.Allocation { offset, metadata }:metadatais the node index, whichfreeandallocationSizeread.Allocation.noneis the original's default allocation.- The allocator owns its node arrays: a copy of it shares them, so after copying one (Zig's counterpart of the original's move), use only the copy.
small_float: the bin size conversionsuintToFloatRoundUp,uintToFloatRoundDownandfloatToUint.
The node arrays come from the std.mem.Allocator given to init, once:
nothing allocates after that, so allocate and free stay O(1) and cannot
fail for lack of memory. A caller-provided buffer was the other choice; an
allocator was kept because the two arrays have different types and sizes
for each node index width, which a single byte buffer would make the caller
compute, and because std.heap.FixedBufferAllocator gives the
caller-provided form anyway.
Ranges is a thin layer over the core for GPU sub-allocation, in
src/ranges.zig. It adds two things the original does not have:
- A unit. Sizes and offsets are in bytes (u64); the core manages
size_bytes / unitunits, whereunitis a power of two such as 256 (Vulkan's uniform buffer offset alignment). Every offset is a multiple of the unit, every request is rounded up to whole units, and since the core counts units rather than bytes, the space reaches past 4 GiB (2^32 units of 256 bytes is 1 TiB). - Generation-checked handles. Each node has a generation; a handle
carries its node's generation when it was allocated, and
freeandresetmove it on. A double free, or a free of a handle from before areset, panics in safe builds (Debug, ReleaseSafe) with a message naming the handle, instead of corrupting the core's lists. In ReleaseFast the check compiles out, as the core's asserts do; the generations stay, soisLiveandreset's invalidation work in every mode andHandlehas one layout whatever the mode.
It is a layer, and not a change to the core, so the core stays the
translation the differential tests check: the layer changes nothing in src/offset_allocator.zig, and
the differential tests keep checking the core against the original call
for call. The layer uses only the core's public API and reads its node
array.
var ranges = try offset_allocator.Ranges.init(gpa, 256 << 20, 4096, 256);
defer ranges.deinit(gpa);
const ubo = try ranges.allocate(200); // 256 bytes at a 256-byte-aligned offset
// ... write 200 bytes at ubo.offset ...
ranges.free(ubo);
std.debug.assert(!ranges.isLive(ubo)); // freeing it again would panicRanges(overAllocator) andRanges16(overAllocator16), bothRangesOf(Core).init(gpa, size_bytes: u64, max_allocs, unit: u32)anddeinit(gpa).size_bytes / unitmust fit in u32; a trailing partial unit is not used.unitBytes()returns the unit.allocate(size_bytes: u64) error{NoSpace}!Handle: at leastsize_bytes, rounded up to whole units. A zero-size request behaves as the core's: it takes a node and must be freed, holds no bytes, and may share its offset with the next range.free(handle): panics in safe builds if the handle is not live.checkHandle(handle) HandleError!void, the checkfreeruns:NodeOutOfRange,StaleGeneration(freed already, or reset since), orNodeNotInUse.isLive(handle) boolis the same as a bool.sizeOf(handle) u64: the bytes the handle holds, the request rounded up to the unit.reset()frees everything and makes every outstanding handle stale.storageReport()andstorageReportFull(): the core's reports in bytes.Handle { offset: u64, node, generation: u32 }.
A generation wraps after 2^32 frees of one node, so a handle that old would
read as live again; a handle from another Ranges whose node is in range is
not caught.
Ranges costs 0.0 to 1.6 ns per call over the core; see
BENCHMARKS.md. Its tests are described
in TESTING.md.