use std::fmt;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Mutex, MutexGuard};
use crate::engine::error::{ShardBoundsError, SpawnError};
use crate::engine::types::{ShardID, SHARD_BITS};
use super::entities::Entities;
use super::entity::Entity;
use super::location::EntityLocation;
pub struct Shard {
pub(crate) entities: Mutex<Entities>,
pub(crate) live_entity_count: AtomicU32,
pub(crate) approximate_free_store_length: AtomicU32,
}
impl Shard {
fn new() -> Self {
Self {
entities: Mutex::new(Entities::default()),
live_entity_count: AtomicU32::new(0),
approximate_free_store_length: AtomicU32::new(0),
}
}
#[allow(dead_code)]
#[inline]
pub fn live_count(&self) -> u32 {
self.live_entity_count.load(Ordering::Relaxed)
}
#[allow(dead_code)]
#[inline]
pub fn approximate_free_count(&self) -> u32 {
self.approximate_free_store_length.load(Ordering::Relaxed)
}
}
pub struct EntityShards {
shards: Vec<Shard>,
}
impl fmt::Debug for EntityShards {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EntityShards")
.field("shard_count", &self.shard_count())
.finish()
}
}
impl EntityShards {
#[inline]
pub fn shard_count(&self) -> usize {
self.shards.len()
}
#[inline]
fn lock_entities(&self, shard_id: ShardID) -> Result<MutexGuard<'_, Entities>, SpawnError> {
self.shards[shard_id as usize]
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)
}
pub fn new(n_shards: usize) -> Result<Self, SpawnError> {
let max_shards = 1usize << SHARD_BITS;
if n_shards == 0 || n_shards > max_shards {
return Err(SpawnError::InvalidShardCount {
requested: n_shards as u16,
max: max_shards as u16,
});
}
let mut shards = Vec::with_capacity(n_shards);
for _ in 0..n_shards {
shards.push(Shard::new());
}
Ok(Self { shards })
}
pub fn spawn_on(
&self,
shard_id: ShardID,
location: EntityLocation,
) -> Result<Entity, SpawnError> {
let shard_count = self.shard_count();
if (shard_id as usize) >= shard_count {
return Err(SpawnError::ShardBounds(ShardBoundsError {
index: shard_id,
max_index: shard_count.saturating_sub(1) as u32,
}));
}
let mut entities = self.lock_entities(shard_id)?;
let before_free = entities.free_store.len();
let entity = entities.spawn(shard_id, location)?;
let shard = &self.shards[shard_id as usize];
shard.live_entity_count.fetch_add(1, Ordering::Relaxed);
if before_free > 0 {
shard
.approximate_free_store_length
.fetch_sub(1, Ordering::Relaxed);
} else {
let after_free = entities.free_store.len();
shard
.approximate_free_store_length
.fetch_add(after_free as u32, Ordering::Relaxed);
}
Ok(entity)
}
pub fn is_alive(&self, entity: Entity) -> Result<bool, SpawnError> {
let shard_id = entity.shard() as usize;
if shard_id >= self.shard_count() {
return Ok(false);
}
let entities = self.shards[shard_id]
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)?;
Ok(entities.is_alive(entity))
}
pub fn get_location(&self, entity: Entity) -> Result<Option<EntityLocation>, SpawnError> {
let shard_id = entity.shard() as usize;
if shard_id >= self.shard_count() {
return Ok(None);
}
let entities = self.shards[shard_id]
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)?;
Ok(entities.get_location(entity))
}
pub(crate) fn set_location(
&self,
entity: Entity,
location: EntityLocation,
) -> Result<(), SpawnError> {
let shard_id = entity.shard() as usize;
if shard_id >= self.shard_count() {
return Err(SpawnError::ShardBounds(ShardBoundsError {
index: entity.shard(),
max_index: self.shard_count().saturating_sub(1) as u32,
}));
}
let mut entities = self.shards[shard_id]
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)?;
entities.set_location(entity, location);
Ok(())
}
pub(crate) fn spawn_batch(
&self,
count: usize,
mut location_for: impl FnMut(usize) -> EntityLocation,
) -> Result<Vec<Entity>, SpawnError> {
let mut out = Vec::with_capacity(count);
if count == 0 {
return Ok(out);
}
let shard_count = self.shard_count();
let per_shard = count.div_ceil(shard_count);
let mut offset = 0usize;
let mut shard_id: ShardID = 0;
while offset < count {
let run = per_shard.min(count - offset);
let result = {
let mut entities = self.lock_entities(shard_id)?;
let base = offset;
entities.spawn_many(shard_id, run, |k| location_for(base + k), &mut out)
};
match result {
Ok(()) => {
let shard = &self.shards[shard_id as usize];
shard
.live_entity_count
.fetch_add(run as u32, Ordering::Relaxed);
let free_now = self
.lock_entities(shard_id)
.map(|entities| entities.free_store.len())
.unwrap_or(0);
shard
.approximate_free_store_length
.store(free_now as u32, Ordering::Relaxed);
}
Err(error) => {
for entity in out.drain(..) {
let _ = self.despawn(entity);
}
return Err(SpawnError::Capacity(error));
}
}
offset += run;
shard_id += 1;
}
Ok(out)
}
pub(crate) fn despawn_grouped(&self, entities: &[Entity]) -> Result<(), SpawnError> {
let shard_count = self.shard_count();
let mut buckets: Vec<Vec<Entity>> = vec![Vec::new(); shard_count];
for &entity in entities {
let shard_id = entity.shard() as usize;
if shard_id >= shard_count {
return Err(SpawnError::StaleEntity(
crate::engine::error::StaleEntityError,
));
}
buckets[shard_id].push(entity);
}
for (shard_id, bucket) in buckets.into_iter().enumerate() {
if bucket.is_empty() {
continue;
}
let shard = &self.shards[shard_id];
let mut pool = shard
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)?;
for entity in bucket {
if !pool.despawn(entity) {
return Err(SpawnError::StaleEntity(
crate::engine::error::StaleEntityError,
));
}
shard
.approximate_free_store_length
.fetch_add(1, Ordering::Relaxed);
shard.live_entity_count.fetch_sub(1, Ordering::Relaxed);
}
}
Ok(())
}
pub(crate) fn set_locations_grouped(
&self,
moves: &[(Entity, EntityLocation)],
) -> Result<(), SpawnError> {
let shard_count = self.shard_count();
let mut buckets: Vec<Vec<(Entity, EntityLocation)>> = vec![Vec::new(); shard_count];
for &(entity, location) in moves {
let shard_id = entity.shard() as usize;
if shard_id >= shard_count {
return Err(SpawnError::ShardBounds(ShardBoundsError {
index: entity.shard(),
max_index: shard_count.saturating_sub(1) as u32,
}));
}
buckets[shard_id].push((entity, location));
}
for (shard_id, bucket) in buckets.into_iter().enumerate() {
if bucket.is_empty() {
continue;
}
let mut pool = self.shards[shard_id]
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)?;
for (entity, location) in bucket {
pool.set_location(entity, location);
}
}
Ok(())
}
pub fn despawn(&self, entity: Entity) -> Result<bool, SpawnError> {
let shard_id = entity.shard() as usize;
if shard_id >= self.shard_count() {
return Ok(false);
}
let shard = &self.shards[shard_id];
let mut entities = shard
.entities
.lock()
.map_err(|_| SpawnError::ShardLockPoisoned)?;
if entities.despawn(entity) {
shard
.approximate_free_store_length
.fetch_add(1, Ordering::Relaxed);
shard.live_entity_count.fetch_sub(1, Ordering::Relaxed);
Ok(true)
} else {
Ok(false)
}
}
}