use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use async_trait::async_trait;
use clawft_bvh::{
Aabb, BranchId, BranchMeta, DiffEntry, Frustum, Leaf, LeafId, Ray, RayHit, Vec3,
};
use clawft_types::config::SpatialConfig;
use serde::{Deserialize, Serialize};
use crate::health::HealthStatus;
use crate::service::{ServiceType, SystemService};
use crate::spatial_backend::{SpatialBackend, SpatialResult};
use crate::spatial_bvh::{BvhBackend, KernelBvhChainSink};
#[cfg(feature = "exochain")]
use crate::chain::ChainManager;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpatialServiceConfig {
pub enabled: bool,
pub max_leaves: usize,
pub phase_commit_threshold: usize,
}
impl Default for SpatialServiceConfig {
fn default() -> Self {
Self::from_spatial_config(&SpatialConfig::default())
}
}
impl SpatialServiceConfig {
pub fn from_spatial_config(cfg: &SpatialConfig) -> Self {
Self {
enabled: cfg.enabled,
max_leaves: cfg.max_leaves,
phase_commit_threshold: cfg.phase_commit_threshold,
}
}
pub fn to_store_config(&self) -> clawft_bvh::BvhStoreConfig {
clawft_bvh::BvhStoreConfig {
max_leaves: self.max_leaves,
phase_commit_threshold: self.phase_commit_threshold,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpatialServiceStatus {
pub backend: String,
pub leaf_count: usize,
pub epoch: u64,
pub branch_count: usize,
pub insert_count: u64,
pub query_count: u64,
pub max_leaves: usize,
pub enabled: bool,
}
pub struct SpatialService {
backend: Arc<dyn SpatialBackend>,
bvh: Option<Arc<BvhBackend>>,
config: SpatialServiceConfig,
insert_count: AtomicU64,
query_count: AtomicU64,
started: std::sync::atomic::AtomicBool,
}
impl SpatialService {
pub fn new(config: SpatialServiceConfig, backend: Arc<dyn SpatialBackend>) -> Self {
Self {
backend,
bvh: None,
config,
insert_count: AtomicU64::new(0),
query_count: AtomicU64::new(0),
started: std::sync::atomic::AtomicBool::new(false),
}
}
pub fn from_config(config: SpatialServiceConfig) -> Self {
let bvh = Arc::new(BvhBackend::new(config.to_store_config()));
let backend: Arc<dyn SpatialBackend> = bvh.clone();
Self {
backend,
bvh: Some(bvh),
config,
insert_count: AtomicU64::new(0),
query_count: AtomicU64::new(0),
started: std::sync::atomic::AtomicBool::new(false),
}
}
pub fn from_spatial_config(cfg: &SpatialConfig) -> Self {
Self::from_config(SpatialServiceConfig::from_spatial_config(cfg))
}
#[cfg(feature = "exochain")]
pub fn attach_chain(&self, cm: Arc<ChainManager>) {
if let Some(ref bvh) = self.bvh {
bvh.set_chain_sink(Arc::new(KernelBvhChainSink::new(cm)));
}
}
pub fn backend(&self) -> &Arc<dyn SpatialBackend> {
&self.backend
}
pub fn bvh(&self) -> Option<&Arc<BvhBackend>> {
self.bvh.as_ref()
}
pub fn insert(&self, leaf: Leaf) -> SpatialResult<LeafId> {
let id = self.backend.insert(leaf)?;
self.insert_count.fetch_add(1, Ordering::Relaxed);
Ok(id)
}
pub fn remove(&self, id: LeafId) -> bool {
self.backend.remove(id)
}
pub fn get(&self, id: LeafId) -> Option<Leaf> {
self.backend.get(id)
}
pub fn query_point(&self, p: Vec3) -> Vec<LeafId> {
self.query_count.fetch_add(1, Ordering::Relaxed);
self.backend.query_point(p)
}
pub fn query_aabb(&self, bb: Aabb) -> Vec<LeafId> {
self.query_count.fetch_add(1, Ordering::Relaxed);
self.backend.query_aabb(bb)
}
pub fn query_sphere(&self, center: Vec3, radius: f32) -> Vec<LeafId> {
self.query_count.fetch_add(1, Ordering::Relaxed);
self.backend.query_sphere(center, radius)
}
pub fn query_ray(&self, ray: Ray, max_t: f32) -> Vec<RayHit> {
self.query_count.fetch_add(1, Ordering::Relaxed);
self.backend.query_ray(ray, max_t)
}
pub fn query_frustum(&self, frustum: &Frustum) -> Vec<LeafId> {
self.query_count.fetch_add(1, Ordering::Relaxed);
self.backend.query_frustum(frustum)
}
pub fn query_knn(&self, p: Vec3, k: usize) -> Vec<LeafId> {
self.query_count.fetch_add(1, Ordering::Relaxed);
self.backend.query_knn(p, k)
}
pub fn derive_branch(&self, meta: BranchMeta) -> SpatialResult<BranchId> {
self.backend.derive_branch(meta)
}
pub fn branch_diff(&self, a: BranchId, b: BranchId, region: Aabb) -> Vec<DiffEntry> {
self.backend.branch_diff(a, b, region)
}
pub fn status(&self) -> SpatialServiceStatus {
let branch_count = self.bvh.as_ref().map(|b| b.branch_count()).unwrap_or(1);
SpatialServiceStatus {
backend: self.backend.backend_name().to_string(),
leaf_count: self.backend.len(),
epoch: self.backend.epoch(),
branch_count,
insert_count: self.insert_count.load(Ordering::Relaxed),
query_count: self.query_count.load(Ordering::Relaxed),
max_leaves: self.config.max_leaves,
enabled: self.config.enabled,
}
}
}
#[async_trait]
impl SystemService for SpatialService {
fn name(&self) -> &str {
"ecc.spatial"
}
fn service_type(&self) -> ServiceType {
ServiceType::Core
}
async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.started.store(true, Ordering::SeqCst);
Ok(())
}
async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.started.store(false, Ordering::SeqCst);
Ok(())
}
async fn health_check(&self) -> HealthStatus {
if !self.config.enabled {
return HealthStatus::Healthy; }
if self.started.load(Ordering::SeqCst) {
HealthStatus::Healthy
} else {
HealthStatus::Degraded("spatial service not started".into())
}
}
fn health_detail(&self) -> Option<String> {
let s = self.status();
Some(format!(
"backend={} leaves={} epoch={} branches={} inserts={} queries={}",
s.backend, s.leaf_count, s.epoch, s.branch_count, s.insert_count, s.query_count
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use clawft_bvh::IdentityKind;
#[tokio::test]
async fn service_lifecycle_and_status() {
let svc = SpatialService::from_config(SpatialServiceConfig {
enabled: true,
max_leaves: 100,
phase_commit_threshold: 16,
});
assert_eq!(svc.name(), "ecc.spatial");
svc.start().await.unwrap();
let id = svc
.insert(Leaf::empty_payload(
Aabb::unit(),
IdentityKind::Object,
1,
))
.unwrap();
assert!(svc.query_point(Vec3::ZERO).contains(&id));
let st = svc.status();
assert_eq!(st.leaf_count, 1);
assert_eq!(st.insert_count, 1);
assert!(matches!(svc.health_check().await, HealthStatus::Healthy));
assert!(svc.health_detail().unwrap().contains("leaves=1"));
svc.stop().await.unwrap();
}
}