terminals_core/substrate/
atom.rs1use super::layout::ProjectionLayout;
8use super::projection::{Projection, ProjectionId};
9use super::splat::SplatProjection;
10use super::kuramoto::KuramotoProjection;
11use super::expert::ExpertProjection;
12use super::graph::GraphProjection;
13use super::thermal::ThermalProjection;
14
15fn fnv1a(data: &[u8]) -> u32 {
17 let mut hash = 0x811c_9dc5u32;
18 for &byte in data {
19 hash ^= byte as u32;
20 hash = hash.wrapping_mul(0x0100_0193);
21 }
22 hash
23}
24
25#[derive(Debug, Clone)]
27pub struct ComputeAtom {
28 pub buffer: Vec<u8>,
30 pub layout: ProjectionLayout,
32 pub shape_hash: u32,
34}
35
36impl ComputeAtom {
37 pub fn new(layout: ProjectionLayout) -> Self {
40 let buffer = vec![0u8; layout.stride];
41 let mut atom = Self {
42 buffer,
43 layout,
44 shape_hash: 0,
45 };
46 atom.recompute_hash();
47 atom
48 }
49
50 pub fn create_n(layout: &ProjectionLayout, n: usize) -> Vec<Self> {
52 (0..n).map(|_| Self::new(layout.clone())).collect()
53 }
54
55 pub fn read_projection<P: Projection>(&self) -> Option<P> {
57 let offset = self.layout.offset_of(P::id())?;
58 Some(P::read(&self.buffer[offset..]))
59 }
60
61 pub fn write_projection<P: Projection>(&mut self, proj: &P) -> bool {
63 if let Some(offset) = self.layout.offset_of(P::id()) {
64 proj.write(&mut self.buffer[offset..]);
65 self.recompute_hash();
66 true
67 } else {
68 false
69 }
70 }
71
72 pub fn projection_bytes(&self, id: ProjectionId) -> Option<&[u8]> {
74 let offset = self.layout.offset_of(id)?;
75 let size = match id {
76 ProjectionId::Splat => SplatProjection::byte_size(),
77 ProjectionId::Kuramoto => KuramotoProjection::byte_size(),
78 ProjectionId::Expert => ExpertProjection::byte_size(),
79 ProjectionId::Graph => GraphProjection::byte_size(),
80 ProjectionId::Thermal => ThermalProjection::byte_size(),
81 };
82 Some(&self.buffer[offset..offset + size])
83 }
84
85 fn recompute_hash(&mut self) {
87 self.shape_hash = fnv1a(&self.buffer);
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_atom_full_layout() {
98 let atom = ComputeAtom::new(ProjectionLayout::full());
99 assert_eq!(atom.buffer.len(), 1612);
100 assert!(atom.read_projection::<SplatProjection>().is_some());
101 assert!(atom.read_projection::<KuramotoProjection>().is_some());
102 assert!(atom.read_projection::<ExpertProjection>().is_some());
103 assert!(atom.read_projection::<GraphProjection>().is_some());
104 assert!(atom.read_projection::<ThermalProjection>().is_some());
105 }
106
107 #[test]
108 fn test_atom_minimal_layout() {
109 let atom = ComputeAtom::new(ProjectionLayout::minimal());
110 assert!(atom.read_projection::<SplatProjection>().is_some());
111 assert!(atom.read_projection::<KuramotoProjection>().is_some());
112 assert!(atom.read_projection::<ExpertProjection>().is_none());
113 assert!(atom.read_projection::<GraphProjection>().is_none());
114 }
115
116 #[test]
117 fn test_atom_write_read_roundtrip() {
118 let mut atom = ComputeAtom::new(ProjectionLayout::full());
119
120 let k = KuramotoProjection {
121 theta: 1.5,
122 omega: 0.3,
123 coupling: 2.0,
124 };
125 assert!(atom.write_projection(&k));
126
127 let restored = atom.read_projection::<KuramotoProjection>().unwrap();
128 assert!((restored.theta - 1.5).abs() < 1e-6);
129 assert!((restored.omega - 0.3).abs() < 1e-6);
130 }
131
132 #[test]
133 fn test_atom_shape_hash_changes_on_write() {
134 let mut atom = ComputeAtom::new(ProjectionLayout::full());
135 let hash_before = atom.shape_hash;
136
137 let k = KuramotoProjection {
138 theta: 3.14,
139 omega: 1.0,
140 coupling: 1.0,
141 };
142 atom.write_projection(&k);
143 assert_ne!(atom.shape_hash, hash_before);
144 }
145
146 #[test]
147 fn test_create_n() {
148 let layout = ProjectionLayout::minimal();
149 let atoms = ComputeAtom::create_n(&layout, 100);
150 assert_eq!(atoms.len(), 100);
151 assert_eq!(atoms[0].buffer.len(), layout.stride);
152 }
153}