use std::mem::{align_of, size_of};
use std::sync::{Arc, RwLock};
use syren::{
advanced::{cast_slice, Archetype, Attribute, EntityShards, TypeErasedAttribute},
ArchetypeID, Bundle, ChunkID, Command, ComponentRegistry, ECSManager, Signature, CHUNK_CAP,
};
#[derive(Clone, Copy, Debug, PartialEq)]
struct Position {
x: f32,
y: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Velocity {
dx: f32,
dy: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct A(u64);
#[derive(Clone, Copy, Debug, PartialEq)]
struct B(u32);
#[derive(Clone, Copy, Debug, PartialEq)]
struct ReduceValue(u32);
#[derive(Clone, Copy, Debug, PartialEq)]
struct ReduceTag {
_tag: u8,
}
fn make_registry() -> Arc<RwLock<ComponentRegistry>> {
let registry = Arc::new(RwLock::new(ComponentRegistry::new()));
{
let mut reg = registry.write().unwrap();
reg.register::<Position>().unwrap();
reg.register::<Velocity>().unwrap();
reg.register::<A>().unwrap();
reg.register::<B>().unwrap();
reg.freeze();
}
registry
}
fn attr_push<T: Send + Sync + 'static>(attr: &mut Attribute<T>, value: T) -> (ChunkID, u32) {
{
let (c, r) = attr.push(value).unwrap();
(c, r)
}
}
#[test]
fn attribute_chunk_is_contiguous_and_aligned() {
let mut attr: Attribute<Position> = Attribute::default();
for i in 0..CHUNK_CAP {
let (c, r) = attr_push(
&mut attr,
Position {
x: i as f32,
y: 0.0,
},
);
assert_eq!(c, 0);
assert_eq!(r as usize, i);
}
let (ptr, bytes) = attr
.chunk_bytes(0, CHUNK_CAP)
.expect("chunk 0 should exist");
assert_eq!(bytes, CHUNK_CAP * size_of::<Position>());
assert_eq!(
(ptr as usize) % align_of::<Position>(),
0,
"chunk base pointer must be aligned for Position"
);
let slice: &[Position] = unsafe { cast_slice(ptr, bytes) };
assert_eq!(slice.len(), CHUNK_CAP);
let base = slice.as_ptr() as usize;
let stride = size_of::<Position>();
for i in 0..CHUNK_CAP {
let pi = unsafe { slice.as_ptr().add(i) as usize };
assert_eq!(
pi,
base + i * stride,
"row {i} not at expected byte offset within chunk"
);
}
}
#[test]
fn attribute_crosses_chunk_boundary_as_expected() {
let mut attr: Attribute<u64> = Attribute::default();
for i in 0..(CHUNK_CAP + 1) {
let (c, r) = attr_push(&mut attr, i as u64);
if i < CHUNK_CAP {
assert_eq!(c, 0);
assert_eq!(r as usize, i);
} else {
assert_eq!(c, 1);
assert_eq!(r as usize, 0);
}
}
let (_p0, b0) = attr.chunk_bytes(0, CHUNK_CAP).unwrap();
assert_eq!(b0, CHUNK_CAP * size_of::<u64>());
let (_p1, b1) = attr.chunk_bytes(1, 1).unwrap();
assert_eq!(b1, size_of::<u64>());
}
#[test]
fn archetype_borrow_exposes_soa_columns_with_independent_addresses() {
let registry = make_registry();
let reg = registry.read().unwrap();
let pos_id = reg.id_of::<Position>().unwrap();
let vel_id = reg.id_of::<Velocity>().unwrap();
let mut sig = Signature::default();
sig.set(pos_id);
sig.set(vel_id);
let mut arch = Archetype::new(0 as ArchetypeID, sig, ®).unwrap();
let shards = EntityShards::new(1).unwrap();
for i in 0..1024usize {
let mut b = Bundle::new();
b.insert(
pos_id,
Position {
x: i as f32,
y: 1.0,
},
);
b.insert(
vel_id,
Velocity {
dx: 0.5,
dy: i as f32,
},
);
let _ = arch.spawn_on(&shards, 0, b).unwrap();
}
let borrow = arch.borrow_chunk_for(0, &[pos_id, vel_id], &[]).unwrap();
assert!(borrow.length > 0);
let (pos_ptr, pos_bytes) = borrow.reads[0];
let (vel_ptr, vel_bytes) = borrow.reads[1];
assert_ne!(
pos_ptr as usize, vel_ptr as usize,
"Position and Velocity columns should not start at same address"
);
assert_eq!(pos_bytes, borrow.length * size_of::<Position>());
assert_eq!(vel_bytes, borrow.length * size_of::<Velocity>());
let pos_slice: &[Position] = unsafe { cast_slice(pos_ptr, pos_bytes) };
let vel_slice: &[Velocity] = unsafe { cast_slice(vel_ptr, vel_bytes) };
assert_eq!(pos_slice.len(), borrow.length);
assert_eq!(vel_slice.len(), borrow.length);
let pos_base = pos_slice.as_ptr() as usize;
let vel_base = vel_slice.as_ptr() as usize;
for i in 0..borrow.length {
let pi = unsafe { pos_slice.as_ptr().add(i) as usize };
let vi = unsafe { vel_slice.as_ptr().add(i) as usize };
assert_eq!(pi, pos_base + i * size_of::<Position>());
assert_eq!(vi, vel_base + i * size_of::<Velocity>());
}
assert_eq!((pos_ptr as usize) % align_of::<Position>(), 0);
assert_eq!((vel_ptr as usize) % align_of::<Velocity>(), 0);
}
#[test]
fn archetype_bytes_per_row_matches_component_sizes() {
let registry = make_registry();
let reg = registry.read().unwrap();
let a = reg.id_of::<A>().unwrap();
let b = reg.id_of::<B>().unwrap();
let mut sig = Signature::default();
sig.set(a);
sig.set(b);
let mut arch = Archetype::new(0 as ArchetypeID, sig, ®).unwrap();
let shards = EntityShards::new(1).unwrap();
for i in 0..256u32 {
let mut bundle = Bundle::new();
bundle.insert(a, A(i as u64));
bundle.insert(b, B(i));
let _ = arch.spawn_on(&shards, 0, bundle).unwrap();
}
let borrow = arch.borrow_chunk_for(0, &[a, b], &[]).unwrap();
let len = borrow.length;
let bytes_a = borrow.reads[0].1;
let bytes_b = borrow.reads[1].1;
assert_eq!(bytes_a / len, size_of::<A>());
assert_eq!(bytes_b / len, size_of::<B>());
}
#[test]
fn archetype_chunk_pointer_is_stable_across_borrows() {
let registry = make_registry();
let reg = registry.read().unwrap();
let pos_id = reg.id_of::<Position>().unwrap();
let mut sig = Signature::default();
sig.set(pos_id);
let mut arch = Archetype::new(1 as ArchetypeID, sig, ®).unwrap();
let shards = EntityShards::new(1).unwrap();
for i in 0..(CHUNK_CAP / 2) {
let mut b = Bundle::new();
b.insert(
pos_id,
Position {
x: i as f32,
y: 0.0,
},
);
let _ = arch.spawn_on(&shards, 0, b).unwrap();
}
let b1 = arch.borrow_chunk_for(0, &[pos_id], &[]).unwrap();
let p1 = b1.reads[0].0 as usize;
drop(b1);
let b2 = arch.borrow_chunk_for(0, &[pos_id], &[]).unwrap();
let p2 = b2.reads[0].0 as usize;
assert_eq!(p1, p2, "chunk pointer moved between borrows");
}
#[test]
fn reduction_combines_multi_archetype_chunks_deterministically() {
let registry = Arc::new(RwLock::new(ComponentRegistry::new()));
let (value_id, tag_id) = {
let mut reg = registry.write().unwrap();
let value_id = reg.register::<ReduceValue>().unwrap();
let tag_id = reg.register::<ReduceTag>().unwrap();
reg.freeze();
(value_id, tag_id)
};
let world = ECSManager::with_registry(EntityShards::new(4).unwrap(), Arc::clone(®istry));
let ecs = world.world_ref();
let per_archetype = CHUNK_CAP + 16;
for i in 0..per_archetype {
let mut bundle = Bundle::new();
bundle.insert(value_id, ReduceValue((i * 2) as u32));
ecs.defer(Command::Spawn { bundle }).unwrap();
}
for i in 0..per_archetype {
let mut bundle = Bundle::new();
bundle.insert(value_id, ReduceValue((i * 2 + 1) as u32));
bundle.insert(tag_id, ReduceTag { _tag: 1 });
ecs.defer(Command::Spawn { bundle }).unwrap();
}
world.apply_deferred_commands().unwrap();
let query = ecs
.query()
.unwrap()
.read::<ReduceValue>()
.unwrap()
.build()
.unwrap();
let mut expected = Vec::with_capacity(per_archetype * 2);
expected.extend((0..per_archetype).map(|i| (i * 2) as u32));
expected.extend((0..per_archetype).map(|i| (i * 2 + 1) as u32));
for _ in 0..8 {
let observed = ecs
.reduce_read::<ReduceValue, Vec<u32>>(
query.clone(),
Vec::new,
|acc, value| acc.push(value.0),
|acc, mut other| acc.append(&mut other),
)
.unwrap();
assert_eq!(observed, expected);
}
}