1use std::sync::{Arc, Mutex};
7
8use clawft_bvh::{
9 Aabb, BranchId, BranchMeta, BvhChainKind, BvhError, BvhStore, BvhStoreConfig, ChainSink,
10 DiffEntry, Frustum, Leaf, LeafId, Ray, RayHit, Vec3,
11};
12
13use crate::spatial_backend::{SpatialBackend, SpatialError, SpatialResult};
14
15#[cfg(feature = "exochain")]
16use crate::chain::ChainManager;
17
18pub struct KernelBvhChainSink {
26 #[cfg(feature = "exochain")]
27 chain: Arc<ChainManager>,
28}
29
30impl KernelBvhChainSink {
31 #[cfg(feature = "exochain")]
33 pub fn new(chain: Arc<ChainManager>) -> Self {
34 Self { chain }
35 }
36
37 #[cfg(not(feature = "exochain"))]
38 pub fn new_noop() -> Self {
39 Self {}
40 }
41}
42
43impl ChainSink for KernelBvhChainSink {
44 fn on_event(&self, event: &BvhChainKind) {
45 #[cfg(feature = "exochain")]
46 {
47 let (kind, payload) = match event {
48 BvhChainKind::Insert {
49 leaf_id,
50 leaf,
51 branch,
52 } => (
53 crate::chain::EVENT_KIND_BVH_INSERT,
54 serde_json::json!({
55 "leaf_id": leaf_id.0,
56 "branch": branch.0,
57 "tag": leaf.tag,
58 "identity": match leaf.identity_kind {
59 clawft_bvh::IdentityKind::Object => "object",
60 clawft_bvh::IdentityKind::Event => "event",
61 },
62 "bound": {
63 "min": [leaf.bound.min.x, leaf.bound.min.y, leaf.bound.min.z],
64 "max": [leaf.bound.max.x, leaf.bound.max.y, leaf.bound.max.z],
65 },
66 "payload_hex": hex_encode(&leaf.payload),
68 }),
69 ),
70 BvhChainKind::Remove { leaf_id, branch } => (
71 crate::chain::EVENT_KIND_BVH_REMOVE,
72 serde_json::json!({
73 "leaf_id": leaf_id.0,
74 "branch": branch.0,
75 }),
76 ),
77 BvhChainKind::Derive {
78 parent,
79 child,
80 meta,
81 } => (
82 crate::chain::EVENT_KIND_BVH_DERIVE,
83 serde_json::json!({
84 "parent": parent.0,
85 "child": child.0,
86 "name": meta.name,
87 "priority_tier": meta.priority_tier,
88 }),
89 ),
90 BvhChainKind::RebalanceSeal {
91 branch,
92 leaf_count,
93 epoch,
94 } => (
95 crate::chain::EVENT_KIND_BVH_REBALANCE_SEAL,
96 serde_json::json!({
97 "branch": branch.0,
98 "leaf_count": leaf_count,
99 "epoch": epoch,
100 }),
101 ),
102 };
103 self.chain.append("spatial_bvh", kind, Some(payload));
104 }
105 #[cfg(not(feature = "exochain"))]
106 {
107 let _ = event;
108 }
109 }
110}
111
112fn hex_encode(bytes: &[u8]) -> String {
113 const HEX: &[u8; 16] = b"0123456789abcdef";
114 let mut out = String::with_capacity(bytes.len() * 2);
115 for &b in bytes {
116 out.push(HEX[(b >> 4) as usize] as char);
117 out.push(HEX[(b & 0xf) as usize] as char);
118 }
119 out
120}
121
122pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
124 if !s.len().is_multiple_of(2) {
125 return None;
126 }
127 let mut out = Vec::with_capacity(s.len() / 2);
128 let bytes = s.as_bytes();
129 let mut i = 0;
130 while i < bytes.len() {
131 let hi = hex_nibble(bytes[i])?;
132 let lo = hex_nibble(bytes[i + 1])?;
133 out.push((hi << 4) | lo);
134 i += 2;
135 }
136 Some(out)
137}
138
139fn hex_nibble(c: u8) -> Option<u8> {
140 match c {
141 b'0'..=b'9' => Some(c - b'0'),
142 b'a'..=b'f' => Some(c - b'a' + 10),
143 b'A'..=b'F' => Some(c - b'A' + 10),
144 _ => None,
145 }
146}
147
148pub struct BvhBackend {
152 store: Mutex<BvhStore>,
153 max_leaves: Option<usize>,
154 log: Arc<EventLog>,
157}
158
159pub const EVENT_LOG_CAP: usize = 100_000;
162
163#[derive(Default)]
165pub struct EventLog {
166 events: Mutex<Vec<clawft_bvh::BvhChainKind>>,
167 truncated: std::sync::atomic::AtomicBool,
168}
169
170impl ChainSink for EventLog {
171 fn on_event(&self, event: &clawft_bvh::BvhChainKind) {
172 let mut ev = self.events.lock().expect("event log poisoned");
173 if ev.len() >= EVENT_LOG_CAP {
174 ev.remove(0);
175 self.truncated.store(true, std::sync::atomic::Ordering::Relaxed);
176 }
177 ev.push(event.clone());
178 }
179}
180
181struct TeeSink {
183 log: Arc<EventLog>,
184 inner: Option<Arc<dyn ChainSink>>,
185}
186
187impl ChainSink for TeeSink {
188 fn on_event(&self, event: &clawft_bvh::BvhChainKind) {
189 self.log.on_event(event);
190 if let Some(ref s) = self.inner {
191 s.on_event(event);
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ReplayReport {
199 pub events: usize,
201 pub ok: bool,
203 pub truncated: bool,
205 pub branches: usize,
207}
208
209impl BvhBackend {
210 pub fn new(config: BvhStoreConfig) -> Self {
212 let max = if config.max_leaves == 0 {
213 None
214 } else {
215 Some(config.max_leaves)
216 };
217 let log = Arc::new(EventLog::default());
218 let tee: Arc<dyn ChainSink> = Arc::new(TeeSink { log: log.clone(), inner: None });
219 Self {
220 store: Mutex::new(BvhStore::with_sink(config, tee)),
221 max_leaves: max,
222 log,
223 }
224 }
225
226 pub fn with_defaults() -> Self {
228 Self::new(BvhStoreConfig::default())
229 }
230
231 pub fn with_sink(config: BvhStoreConfig, sink: Arc<dyn ChainSink>) -> Self {
233 let max = if config.max_leaves == 0 {
234 None
235 } else {
236 Some(config.max_leaves)
237 };
238 let log = Arc::new(EventLog::default());
239 let tee: Arc<dyn ChainSink> = Arc::new(TeeSink { log: log.clone(), inner: Some(sink) });
240 Self {
241 store: Mutex::new(BvhStore::with_sink(config, tee)),
242 max_leaves: max,
243 log,
244 }
245 }
246
247 pub fn set_chain_sink(&self, sink: Arc<dyn ChainSink>) {
249 let mut store = self.store.lock().expect("BvhStore lock poisoned");
250 store.set_sink(Arc::new(TeeSink { log: self.log.clone(), inner: Some(sink) }));
251 }
252
253 pub fn with_store<R>(&self, f: impl FnOnce(&mut BvhStore) -> R) -> R {
256 let mut store = self.store.lock().expect("BvhStore lock poisoned");
257 f(&mut store)
258 }
259
260 pub fn events(&self) -> (Vec<clawft_bvh::BvhChainKind>, bool) {
262 let ev = self.log.events.lock().expect("event log poisoned").clone();
263 (ev, self.log.truncated.load(std::sync::atomic::Ordering::Relaxed))
264 }
265
266 pub fn verify_replay(&self) -> ReplayReport {
269 let (events, truncated) = self.events();
270 let store = self.store.lock().expect("BvhStore lock poisoned");
271 let mut replica = BvhStore::new(store.config().clone());
272 let applied = events.iter().all(|e| replica.apply_chain_event(e).is_ok());
273 let branches = store.branch_count();
274 let ids = |s: &BvhStore, b: u64| -> Option<Vec<u64>> {
275 let mut v: Vec<u64> = s.snapshot_leaves(BranchId(b)).ok()?.into_iter().map(|(id, _)| id.0).collect();
276 v.sort_unstable();
277 Some(v)
278 };
279 let same = replica.branch_count() == branches && (0..branches as u64).all(|b| ids(&store, b) == ids(&replica, b));
280 ReplayReport { events: events.len(), ok: applied && same && !truncated, truncated, branches }
281 }
282
283 pub fn snapshot_main(&self) -> Vec<(LeafId, Leaf)> {
285 let store = self.store.lock().expect("BvhStore lock poisoned");
286 store
287 .snapshot_leaves(BranchId::MAIN)
288 .unwrap_or_default()
289 }
290
291 pub fn restore_main(&self, leaves: &[(LeafId, Leaf)]) -> SpatialResult<()> {
293 let mut store = self.store.lock().expect("BvhStore lock poisoned");
294 store
295 .restore_leaves(BranchId::MAIN, leaves)
296 .map_err(map_bvh_err)
297 }
298
299 pub fn branch_count(&self) -> usize {
301 self.store
302 .lock()
303 .expect("BvhStore lock poisoned")
304 .branch_count()
305 }
306
307 pub fn rebalance_seal(&self) -> SpatialResult<()> {
309 let mut store = self.store.lock().expect("BvhStore lock poisoned");
310 let branch = store.active_branch();
311 store.rebalance_seal(branch).map_err(map_bvh_err)
312 }
313}
314
315fn map_bvh_err(e: BvhError) -> SpatialError {
316 match e {
317 BvhError::CapacityExceeded { max, current } => {
318 SpatialError::CapacityExceeded { max, current }
319 }
320 BvhError::UnknownBranch(id) => SpatialError::UnknownBranch(id.0),
321 BvhError::Other(msg) => SpatialError::Other(msg.to_string()),
322 }
323}
324
325impl SpatialBackend for BvhBackend {
326 fn insert(&self, leaf: Leaf) -> SpatialResult<LeafId> {
327 let mut store = self.store.lock().expect("BvhStore lock poisoned");
328 store.insert(leaf).map_err(map_bvh_err)
329 }
330
331 fn remove(&self, id: LeafId) -> bool {
332 let mut store = self.store.lock().expect("BvhStore lock poisoned");
333 store.remove(id)
334 }
335
336 fn get(&self, id: LeafId) -> Option<Leaf> {
337 let store = self.store.lock().expect("BvhStore lock poisoned");
338 store.get(id).cloned()
339 }
340
341 fn query_point(&self, p: Vec3) -> Vec<LeafId> {
342 let store = self.store.lock().expect("BvhStore lock poisoned");
343 store.query_point(p)
344 }
345
346 fn query_aabb(&self, bb: Aabb) -> Vec<LeafId> {
347 let store = self.store.lock().expect("BvhStore lock poisoned");
348 store.query_aabb(bb)
349 }
350
351 fn query_sphere(&self, center: Vec3, radius: f32) -> Vec<LeafId> {
352 let store = self.store.lock().expect("BvhStore lock poisoned");
353 store.query_sphere(center, radius)
354 }
355
356 fn query_ray(&self, ray: Ray, max_t: f32) -> Vec<RayHit> {
357 let store = self.store.lock().expect("BvhStore lock poisoned");
358 store.query_ray(ray, max_t)
359 }
360
361 fn query_frustum(&self, frustum: &Frustum) -> Vec<LeafId> {
362 let store = self.store.lock().expect("BvhStore lock poisoned");
363 store.query_frustum(frustum)
364 }
365
366 fn query_knn(&self, p: Vec3, k: usize) -> Vec<LeafId> {
367 let store = self.store.lock().expect("BvhStore lock poisoned");
368 store.query_knn(p, k)
369 }
370
371 fn derive_branch(&self, meta: BranchMeta) -> SpatialResult<BranchId> {
372 let mut store = self.store.lock().expect("BvhStore lock poisoned");
373 store.derive(meta).map_err(map_bvh_err)
374 }
375
376 fn branch_diff(&self, a: BranchId, b: BranchId, region: Aabb) -> Vec<DiffEntry> {
377 let store = self.store.lock().expect("BvhStore lock poisoned");
378 store.branch_diff(a, b, region).unwrap_or_default()
379 }
380
381 fn epoch(&self) -> u64 {
382 self.store.lock().expect("BvhStore lock poisoned").epoch()
383 }
384
385 fn len(&self) -> usize {
386 self.store.lock().expect("BvhStore lock poisoned").len()
387 }
388
389 fn backend_name(&self) -> &str {
390 "bvh"
391 }
392
393 fn max_leaves(&self) -> Option<usize> {
394 self.max_leaves
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use clawft_bvh::{IdentityKind, RecordingChainSink};
402
403 fn leaf_at(x: f32, tag: u32) -> Leaf {
404 Leaf::empty_payload(
405 Aabb::from_min_max(
406 Vec3::new(x, 0.0, 0.0),
407 Vec3::new(x + 0.5, 0.5, 0.5),
408 ),
409 IdentityKind::Object,
410 tag,
411 )
412 }
413
414 #[test]
415 fn insert_query_remove() {
416 let b = BvhBackend::with_defaults();
417 let id = b.insert(leaf_at(0.0, 1)).unwrap();
418 assert_eq!(b.len(), 1);
419 assert!(b.query_point(Vec3::new(0.25, 0.25, 0.25)).contains(&id));
420 assert!(b.remove(id));
421 assert!(b.is_empty());
422 }
423
424 #[test]
425 fn chain_sink_receives_insert() {
426 let sink = Arc::new(RecordingChainSink::new());
427 let b = BvhBackend::with_sink(BvhStoreConfig::default(), sink.clone());
428 let _ = b.insert(leaf_at(1.0, 9)).unwrap();
429 assert_eq!(sink.len(), 1);
430 }
431
432 #[test]
433 fn snapshot_restore_byte_identical() {
434 let b = BvhBackend::with_defaults();
435 for i in 0..20 {
436 b.insert(leaf_at(i as f32, i)).unwrap();
437 }
438 let snap = b.snapshot_main();
439 let b2 = BvhBackend::with_defaults();
440 b2.restore_main(&snap).unwrap();
441 assert_eq!(b.snapshot_main(), b2.snapshot_main());
442 assert_eq!(b.len(), b2.len());
443 }
444
445 #[test]
446 fn hex_roundtrip() {
447 let raw = vec![0u8, 1, 0xab, 0xff];
448 let h = hex_encode(&raw);
449 assert_eq!(hex_decode(&h).unwrap(), raw);
450 }
451}