package ring import "base:runtime" import "core:fmt" @(private) ODIN_BOUNDS_CHECK :: !ODIN_NO_BOUNDS_CHECK Ring :: struct($E: typeid) { data: []E, next_write_index, len: int, } Ring_Soa :: struct($E: typeid) { data: #soa[]E, next_write_index, len: int, } destroy_aos :: #force_inline proc( ring: ^Ring($E), allocator := context.allocator, ) -> runtime.Allocator_Error { return delete(ring.data) } destroy_soa :: #force_inline proc( ring: ^Ring_Soa($E), allocator := context.allocator, ) -> runtime.Allocator_Error { return delete(ring.data) } destroy :: proc { destroy_aos, destroy_soa, } create_aos :: #force_inline proc( $E: typeid, capacity: int, allocator := context.allocator, ) -> ( ring: Ring(E), err: runtime.Allocator_Error, ) #optional_allocator_error { ring.data, err = make([]E, capacity, allocator) return ring, err } create_soa :: #force_inline proc( $E: typeid, capacity: int, allocator := context.allocator, ) -> ( ring: Ring_Soa(E), err: runtime.Allocator_Error, ) #optional_allocator_error { ring.data, err = make(#soa[]E, capacity, allocator) return ring, err } // All contents of `data` will be completely ignored, `data` is treated as an empty slice. init_from_slice_aos :: #force_inline proc(ring: ^Ring($E), data: $T/[]E) { ring.data = data ring.len = 0 ring.next_write_index = 0 return } // All contents of `data` will be completely ignored, `data` is treated as an empty slice. init_from_slice_soa :: #force_inline proc(ring: ^Ring_Soa($E), data: $T/#soa[]E) { ring.data = data ring.len = 0 ring.next_write_index = 0 return } init_from_slice :: proc { init_from_slice_aos, init_from_slice_soa, } // Internal // Index in the backing array where the ring starts start_index_aos :: #force_inline proc(ring: Ring($E)) -> int { return ring.len < len(ring.data) ? 0 : ring.next_write_index } // Internal // Index in the backing array where the ring starts start_index_soa :: #force_inline proc(ring: Ring_Soa($E)) -> int { return ring.len < len(ring.data) ? 0 : ring.next_write_index } advance_aos :: #force_inline proc(ring: ^Ring($E)) { // Length if ring.len != len(ring.data) do ring.len += 1 // Write index ring.next_write_index += 1 if ring.next_write_index == len(ring.data) do ring.next_write_index = 0 } advance_soa :: #force_inline proc(ring: ^Ring_Soa($E)) { // Length if ring.len != len(ring.data) do ring.len += 1 // Write index ring.next_write_index += 1 if ring.next_write_index == len(ring.data) do ring.next_write_index = 0 } advance :: proc { advance_aos, advance_soa, } append_aos :: #force_inline proc(ring: ^Ring($E), element: E) { ring.data[ring.next_write_index] = element advance(ring) } append_soa :: #force_inline proc(ring: ^Ring_Soa($E), element: E) { ring.data[ring.next_write_index] = element advance(ring) } append :: proc { append_aos, append_soa, } get_aos :: #force_inline proc(ring: Ring($E), index: int) -> ^E { when ODIN_BOUNDS_CHECK { fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len) } array_index := start_index_aos(ring) + index if array_index < len(ring.data) { return &ring.data[array_index] } else { array_index = array_index - len(ring.data) return &ring.data[array_index] } } // SOA can't return soa pointer to parapoly T. get_soa :: #force_inline proc(ring: Ring_Soa($E), index: int) -> E { when ODIN_BOUNDS_CHECK { fmt.assertf(index < ring.len, "Ring index %i out of bounds for length %i", index, ring.len) } array_index := start_index_soa(ring) + index if array_index < len(ring.data) { return ring.data[array_index] } else { array_index = array_index - len(ring.data) return ring.data[array_index] } } get :: proc { get_aos, get_soa, } get_last_aos :: #force_inline proc(ring: Ring($E)) -> ^E { return get(ring, ring.len - 1) } get_last_soa :: #force_inline proc(ring: Ring_Soa($E)) -> E { return get(ring, ring.len - 1) } get_last :: proc { get_last_aos, get_last_soa, } clear_aos :: #force_inline proc "contextless" (ring: ^Ring($E)) { ring.len = 0 ring.next_write_index = 0 } clear_soa :: #force_inline proc "contextless" (ring: ^Ring_Soa($E)) { ring.len = 0 ring.next_write_index = 0 } clear :: proc { clear_aos, clear_soa, } // --------------------------------------------------------------------------------------------------------------------- // ----- Tests ------------------------ // --------------------------------------------------------------------------------------------------------------------- import "core:log" import "core:testing" @(test) test_ring_aos :: proc(t: ^testing.T) { ring := create_aos(int, 10) defer destroy(&ring) for i in 1 ..= 5 { append(&ring, i) log.debug("Length:", ring.len) log.debug("Start index:", start_index_aos(ring)) log.debug("Next write index:", ring.next_write_index) log.debug(ring.data) } testing.expect_value(t, get(ring, 0)^, 1) testing.expect_value(t, get(ring, 4)^, 5) testing.expect_value(t, ring.len, 5) testing.expect_value(t, ring.next_write_index, 5) testing.expect_value(t, start_index_aos(ring), 0) for i in 6 ..= 15 { append(&ring, i) log.debug("Length:", ring.len) log.debug("Start index:", start_index_aos(ring)) log.debug("Next write index:", ring.next_write_index) log.debug(ring.data) } testing.expect_value(t, get(ring, 0)^, 6) testing.expect_value(t, get(ring, 4)^, 10) testing.expect_value(t, get(ring, 9)^, 15) testing.expect_value(t, get_last(ring)^, 15) testing.expect_value(t, ring.len, 10) testing.expect_value(t, ring.next_write_index, 5) testing.expect_value(t, start_index_aos(ring), 5) for i in 15 ..= 25 { append(&ring, i) log.debug("Length:", ring.len) log.debug("Start index:", start_index_aos(ring)) log.debug("Next write index:", ring.next_write_index) log.debug(ring.data) } testing.expect_value(t, get(ring, 0)^, 16) testing.expect_value(t, ring.next_write_index, 6) testing.expect_value(t, get_last(ring)^, 25) clear(&ring) append(&ring, 1) testing.expect_value(t, ring.len, 1) testing.expect_value(t, get(ring, 0)^, 1) } @(test) test_ring_soa :: proc(t: ^testing.T) { Ints :: struct { x, y: int, } ring := create_soa(Ints, 10) defer destroy(&ring) for i in 1 ..= 5 { append(&ring, Ints{i, i}) log.debug("Length:", ring.len) log.debug("Start index:", start_index_soa(ring)) log.debug("Next write index:", ring.next_write_index) log.debug(ring.data) } testing.expect_value(t, get(ring, 0), Ints{1, 1}) testing.expect_value(t, get(ring, 4), Ints{5, 5}) testing.expect_value(t, ring.len, 5) testing.expect_value(t, ring.next_write_index, 5) testing.expect_value(t, start_index_soa(ring), 0) for i in 6 ..= 15 { append(&ring, Ints{i, i}) log.debug("Length:", ring.len) log.debug("Start index:", start_index_soa(ring)) log.debug("Next write index:", ring.next_write_index) log.debug(ring.data) } testing.expect_value(t, get(ring, 0), Ints{6, 6}) testing.expect_value(t, get(ring, 4), Ints{10, 10}) testing.expect_value(t, get(ring, 9), Ints{15, 15}) testing.expect_value(t, get_last(ring), Ints{15, 15}) testing.expect_value(t, ring.len, 10) testing.expect_value(t, ring.next_write_index, 5) testing.expect_value(t, start_index_soa(ring), 5) for i in 15 ..= 25 { append(&ring, Ints{i, i}) log.debug("Length:", ring.len) log.debug("Start index:", start_index_soa(ring)) log.debug("Next write index:", ring.next_write_index) log.debug(ring.data) } testing.expect_value(t, get(ring, 0), Ints{16, 16}) testing.expect_value(t, ring.next_write_index, 6) testing.expect_value(t, get_last(ring), Ints{25, 25}) clear(&ring) append(&ring, Ints{1, 1}) testing.expect_value(t, ring.len, 1) testing.expect_value(t, get(ring, 0), Ints{1, 1}) } @(test) test_ring_aos_init_from_slice :: proc(t: ^testing.T) { // Stack-allocated backing with pre-existing garbage and odd capacity. backing: [7]int = {99, 99, 99, 99, 99, 99, 99} ring: Ring(int) init_from_slice(&ring, backing[:]) // Empty ring invariants after init_from_slice. testing.expect_value(t, ring.len, 0) testing.expect_value(t, ring.next_write_index, 0) testing.expect_value(t, start_index_aos(ring), 0) // Partial fill (3 / 7). for i in 1 ..= 3 do append(&ring, i) testing.expect_value(t, ring.len, 3) testing.expect_value(t, ring.next_write_index, 3) testing.expect_value(t, start_index_aos(ring), 0) testing.expect_value(t, get(ring, 0)^, 1) testing.expect_value(t, get(ring, 2)^, 3) testing.expect_value(t, get_last(ring)^, 3) // Fill exactly to capacity. Pushing element 7 must make len == cap // AND wrap next_write_index from 6 back to 0 in the same step. for i in 4 ..= 7 do append(&ring, i) testing.expect_value(t, ring.len, 7) testing.expect_value(t, ring.next_write_index, 0) testing.expect_value(t, start_index_aos(ring), 0) testing.expect_value(t, get(ring, 0)^, 1) testing.expect_value(t, get(ring, 6)^, 7) testing.expect_value(t, get_last(ring)^, 7) // First overwrite — oldest element shifts by one. append(&ring, 8) testing.expect_value(t, ring.len, 7) testing.expect_value(t, ring.next_write_index, 1) testing.expect_value(t, start_index_aos(ring), 1) testing.expect_value(t, get(ring, 0)^, 2) testing.expect_value(t, get(ring, 6)^, 8) testing.expect_value(t, get_last(ring)^, 8) // Stress: 3 more complete wrap cycles (21 more pushes). // After 29 total pushes, ring contains the last 7 (23..=29), // and next_write_index = 29 mod 7 = 1. for i in 9 ..= 29 do append(&ring, i) testing.expect_value(t, ring.len, 7) testing.expect_value(t, ring.next_write_index, 1) testing.expect_value(t, start_index_aos(ring), 1) testing.expect_value(t, get(ring, 0)^, 23) testing.expect_value(t, get(ring, 3)^, 26) testing.expect_value(t, get(ring, 6)^, 29) testing.expect_value(t, get_last(ring)^, 29) // Clear returns ring to empty-equivalent state. clear(&ring) testing.expect_value(t, ring.len, 0) testing.expect_value(t, ring.next_write_index, 0) testing.expect_value(t, start_index_aos(ring), 0) // Single-element edge case: get_last(len==1) routes through get(ring, 0). append(&ring, 42) testing.expect_value(t, ring.len, 1) testing.expect_value(t, ring.next_write_index, 1) testing.expect_value(t, get(ring, 0)^, 42) testing.expect_value(t, get_last(ring)^, 42) } @(test) test_ring_soa_init_from_slice :: proc(t: ^testing.T) { Ints :: struct { x, y: int, } // Stack-allocated backing with pre-existing garbage and odd capacity. backing: #soa[7]Ints = {{99, 99}, {99, 99}, {99, 99}, {99, 99}, {99, 99}, {99, 99}, {99, 99}} ring: Ring_Soa(Ints) init_from_slice(&ring, backing[:]) // Empty ring invariants after init_from_slice. testing.expect_value(t, ring.len, 0) testing.expect_value(t, ring.next_write_index, 0) testing.expect_value(t, start_index_soa(ring), 0) // Partial fill (3 / 7). for i in 1 ..= 3 do append(&ring, Ints{i, i}) testing.expect_value(t, ring.len, 3) testing.expect_value(t, ring.next_write_index, 3) testing.expect_value(t, start_index_soa(ring), 0) testing.expect_value(t, get(ring, 0), Ints{1, 1}) testing.expect_value(t, get(ring, 2), Ints{3, 3}) testing.expect_value(t, get_last(ring), Ints{3, 3}) // Fill exactly to capacity. Pushing element 7 must make len == cap // AND wrap next_write_index from 6 back to 0 in the same step. for i in 4 ..= 7 do append(&ring, Ints{i, i}) testing.expect_value(t, ring.len, 7) testing.expect_value(t, ring.next_write_index, 0) testing.expect_value(t, start_index_soa(ring), 0) testing.expect_value(t, get(ring, 0), Ints{1, 1}) testing.expect_value(t, get(ring, 6), Ints{7, 7}) testing.expect_value(t, get_last(ring), Ints{7, 7}) // First overwrite — oldest element shifts by one. append(&ring, Ints{8, 8}) testing.expect_value(t, ring.len, 7) testing.expect_value(t, ring.next_write_index, 1) testing.expect_value(t, start_index_soa(ring), 1) testing.expect_value(t, get(ring, 0), Ints{2, 2}) testing.expect_value(t, get(ring, 6), Ints{8, 8}) testing.expect_value(t, get_last(ring), Ints{8, 8}) // Stress: 3 more complete wrap cycles (21 more pushes). // After 29 total pushes, ring contains the last 7 (23..=29), // and next_write_index = 29 mod 7 = 1. for i in 9 ..= 29 do append(&ring, Ints{i, i}) testing.expect_value(t, ring.len, 7) testing.expect_value(t, ring.next_write_index, 1) testing.expect_value(t, start_index_soa(ring), 1) testing.expect_value(t, get(ring, 0), Ints{23, 23}) testing.expect_value(t, get(ring, 3), Ints{26, 26}) testing.expect_value(t, get(ring, 6), Ints{29, 29}) testing.expect_value(t, get_last(ring), Ints{29, 29}) // Clear returns ring to empty-equivalent state. clear(&ring) testing.expect_value(t, ring.len, 0) testing.expect_value(t, ring.next_write_index, 0) testing.expect_value(t, start_index_soa(ring), 0) // Single-element edge case: get_last(len==1) routes through get(ring, 0). append(&ring, Ints{42, 42}) testing.expect_value(t, ring.len, 1) testing.expect_value(t, ring.next_write_index, 1) testing.expect_value(t, get(ring, 0), Ints{42, 42}) testing.expect_value(t, get_last(ring), Ints{42, 42}) }