1use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use clawft_bvh::{
11 Aabb, BranchId, BranchMeta, DiffEntry, Frustum, Leaf, LeafId, Ray, RayHit, Vec3,
12};
13use clawft_types::config::SpatialConfig;
14use serde::{Deserialize, Serialize};
15
16use crate::health::HealthStatus;
17use crate::service::{ServiceType, SystemService};
18use crate::spatial_backend::{SpatialBackend, SpatialResult};
19use crate::spatial_bvh::{BvhBackend, KernelBvhChainSink};
20
21#[cfg(feature = "exochain")]
22use crate::chain::ChainManager;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct SpatialServiceConfig {
29 pub enabled: bool,
31 pub max_leaves: usize,
33 pub phase_commit_threshold: usize,
35}
36
37impl Default for SpatialServiceConfig {
38 fn default() -> Self {
39 Self::from_spatial_config(&SpatialConfig::default())
40 }
41}
42
43impl SpatialServiceConfig {
44 pub fn from_spatial_config(cfg: &SpatialConfig) -> Self {
46 Self {
47 enabled: cfg.enabled,
48 max_leaves: cfg.max_leaves,
49 phase_commit_threshold: cfg.phase_commit_threshold,
50 }
51 }
52
53 pub fn to_store_config(&self) -> clawft_bvh::BvhStoreConfig {
55 clawft_bvh::BvhStoreConfig {
56 max_leaves: self.max_leaves,
57 phase_commit_threshold: self.phase_commit_threshold,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SpatialServiceStatus {
67 pub backend: String,
69 pub leaf_count: usize,
71 pub epoch: u64,
73 pub branch_count: usize,
75 pub insert_count: u64,
77 pub query_count: u64,
79 pub max_leaves: usize,
81 pub enabled: bool,
83}
84
85pub struct SpatialService {
89 backend: Arc<dyn SpatialBackend>,
90 bvh: Option<Arc<BvhBackend>>,
93 config: SpatialServiceConfig,
94 insert_count: AtomicU64,
95 query_count: AtomicU64,
96 started: std::sync::atomic::AtomicBool,
97}
98
99impl SpatialService {
100 pub fn new(config: SpatialServiceConfig, backend: Arc<dyn SpatialBackend>) -> Self {
102 Self {
103 backend,
104 bvh: None,
105 config,
106 insert_count: AtomicU64::new(0),
107 query_count: AtomicU64::new(0),
108 started: std::sync::atomic::AtomicBool::new(false),
109 }
110 }
111
112 pub fn from_config(config: SpatialServiceConfig) -> Self {
114 let bvh = Arc::new(BvhBackend::new(config.to_store_config()));
115 let backend: Arc<dyn SpatialBackend> = bvh.clone();
116 Self {
117 backend,
118 bvh: Some(bvh),
119 config,
120 insert_count: AtomicU64::new(0),
121 query_count: AtomicU64::new(0),
122 started: std::sync::atomic::AtomicBool::new(false),
123 }
124 }
125
126 pub fn from_spatial_config(cfg: &SpatialConfig) -> Self {
128 Self::from_config(SpatialServiceConfig::from_spatial_config(cfg))
129 }
130
131 #[cfg(feature = "exochain")]
133 pub fn attach_chain(&self, cm: Arc<ChainManager>) {
134 if let Some(ref bvh) = self.bvh {
135 bvh.set_chain_sink(Arc::new(KernelBvhChainSink::new(cm)));
136 }
137 }
138
139 pub fn backend(&self) -> &Arc<dyn SpatialBackend> {
141 &self.backend
142 }
143
144 pub fn bvh(&self) -> Option<&Arc<BvhBackend>> {
146 self.bvh.as_ref()
147 }
148
149 pub fn insert(&self, leaf: Leaf) -> SpatialResult<LeafId> {
151 let id = self.backend.insert(leaf)?;
152 self.insert_count.fetch_add(1, Ordering::Relaxed);
153 Ok(id)
154 }
155
156 pub fn remove(&self, id: LeafId) -> bool {
158 self.backend.remove(id)
159 }
160
161 pub fn get(&self, id: LeafId) -> Option<Leaf> {
163 self.backend.get(id)
164 }
165
166 pub fn query_point(&self, p: Vec3) -> Vec<LeafId> {
168 self.query_count.fetch_add(1, Ordering::Relaxed);
169 self.backend.query_point(p)
170 }
171
172 pub fn query_aabb(&self, bb: Aabb) -> Vec<LeafId> {
174 self.query_count.fetch_add(1, Ordering::Relaxed);
175 self.backend.query_aabb(bb)
176 }
177
178 pub fn query_sphere(&self, center: Vec3, radius: f32) -> Vec<LeafId> {
180 self.query_count.fetch_add(1, Ordering::Relaxed);
181 self.backend.query_sphere(center, radius)
182 }
183
184 pub fn query_ray(&self, ray: Ray, max_t: f32) -> Vec<RayHit> {
186 self.query_count.fetch_add(1, Ordering::Relaxed);
187 self.backend.query_ray(ray, max_t)
188 }
189
190 pub fn query_frustum(&self, frustum: &Frustum) -> Vec<LeafId> {
192 self.query_count.fetch_add(1, Ordering::Relaxed);
193 self.backend.query_frustum(frustum)
194 }
195
196 pub fn query_knn(&self, p: Vec3, k: usize) -> Vec<LeafId> {
198 self.query_count.fetch_add(1, Ordering::Relaxed);
199 self.backend.query_knn(p, k)
200 }
201
202 pub fn derive_branch(&self, meta: BranchMeta) -> SpatialResult<BranchId> {
204 self.backend.derive_branch(meta)
205 }
206
207 pub fn branch_diff(&self, a: BranchId, b: BranchId, region: Aabb) -> Vec<DiffEntry> {
209 self.backend.branch_diff(a, b, region)
210 }
211
212 pub fn status(&self) -> SpatialServiceStatus {
214 let branch_count = self.bvh.as_ref().map(|b| b.branch_count()).unwrap_or(1);
215 SpatialServiceStatus {
216 backend: self.backend.backend_name().to_string(),
217 leaf_count: self.backend.len(),
218 epoch: self.backend.epoch(),
219 branch_count,
220 insert_count: self.insert_count.load(Ordering::Relaxed),
221 query_count: self.query_count.load(Ordering::Relaxed),
222 max_leaves: self.config.max_leaves,
223 enabled: self.config.enabled,
224 }
225 }
226}
227
228#[async_trait]
229impl SystemService for SpatialService {
230 fn name(&self) -> &str {
231 "ecc.spatial"
232 }
233
234 fn service_type(&self) -> ServiceType {
235 ServiceType::Core
236 }
237
238 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
239 self.started.store(true, Ordering::SeqCst);
240 Ok(())
241 }
242
243 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
244 self.started.store(false, Ordering::SeqCst);
245 Ok(())
246 }
247
248 async fn health_check(&self) -> HealthStatus {
249 if !self.config.enabled {
250 return HealthStatus::Healthy; }
252 if self.started.load(Ordering::SeqCst) {
253 HealthStatus::Healthy
254 } else {
255 HealthStatus::Degraded("spatial service not started".into())
256 }
257 }
258
259 fn health_detail(&self) -> Option<String> {
260 let s = self.status();
261 Some(format!(
262 "backend={} leaves={} epoch={} branches={} inserts={} queries={}",
263 s.backend, s.leaf_count, s.epoch, s.branch_count, s.insert_count, s.query_count
264 ))
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use clawft_bvh::IdentityKind;
272
273 #[tokio::test]
274 async fn service_lifecycle_and_status() {
275 let svc = SpatialService::from_config(SpatialServiceConfig {
276 enabled: true,
277 max_leaves: 100,
278 phase_commit_threshold: 16,
279 });
280 assert_eq!(svc.name(), "ecc.spatial");
281 svc.start().await.unwrap();
282 let id = svc
283 .insert(Leaf::empty_payload(
284 Aabb::unit(),
285 IdentityKind::Object,
286 1,
287 ))
288 .unwrap();
289 assert!(svc.query_point(Vec3::ZERO).contains(&id));
290 let st = svc.status();
291 assert_eq!(st.leaf_count, 1);
292 assert_eq!(st.insert_count, 1);
293 assert!(matches!(svc.health_check().await, HealthStatus::Healthy));
294 assert!(svc.health_detail().unwrap().contains("leaves=1"));
295 svc.stop().await.unwrap();
296 }
297}