1use std::collections::{BTreeMap, HashMap};
4
5use crate::ids::{ComponentId, EntityHandle};
6use crate::spatial::Vec3;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ComponentCodecError {
11 ExpectedBytes {
13 expected: usize,
15 actual: usize,
17 },
18}
19
20impl core::fmt::Display for ComponentCodecError {
21 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22 match self {
23 Self::ExpectedBytes { expected, actual } => {
24 write!(f, "expected {expected} bytes, got {actual}")
25 }
26 }
27 }
28}
29
30impl std::error::Error for ComponentCodecError {}
31
32pub trait ComponentCodec<T> {
35 fn encode(&self, value: &T, out: &mut Vec<u8>) -> Result<(), ComponentCodecError>;
37
38 fn decode(&self, input: &[u8]) -> Result<T, ComponentCodecError>;
40
41 fn fixed_size(&self) -> Option<usize> {
43 None
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default)]
49pub struct U32LeCodec;
50
51impl ComponentCodec<u32> for U32LeCodec {
52 fn encode(&self, value: &u32, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
53 out.extend_from_slice(&value.to_le_bytes());
54 Ok(())
55 }
56
57 fn decode(&self, input: &[u8]) -> Result<u32, ComponentCodecError> {
58 let bytes = exact_array::<4>(input)?;
59 Ok(u32::from_le_bytes(bytes))
60 }
61
62 fn fixed_size(&self) -> Option<usize> {
63 Some(4)
64 }
65}
66
67#[derive(Clone, Copy, Debug, Default)]
69pub struct F32LeCodec;
70
71impl ComponentCodec<f32> for F32LeCodec {
72 fn encode(&self, value: &f32, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
73 out.extend_from_slice(&value.to_le_bytes());
74 Ok(())
75 }
76
77 fn decode(&self, input: &[u8]) -> Result<f32, ComponentCodecError> {
78 let bytes = exact_array::<4>(input)?;
79 Ok(f32::from_le_bytes(bytes))
80 }
81
82 fn fixed_size(&self) -> Option<usize> {
83 Some(4)
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default)]
89pub struct Vec3LeCodec;
90
91impl ComponentCodec<Vec3> for Vec3LeCodec {
92 fn encode(&self, value: &Vec3, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
93 out.extend_from_slice(&value.x.to_le_bytes());
94 out.extend_from_slice(&value.y.to_le_bytes());
95 out.extend_from_slice(&value.z.to_le_bytes());
96 Ok(())
97 }
98
99 fn decode(&self, input: &[u8]) -> Result<Vec3, ComponentCodecError> {
100 if input.len() != 12 {
101 return Err(ComponentCodecError::ExpectedBytes {
102 expected: 12,
103 actual: input.len(),
104 });
105 }
106 let x = f32::from_le_bytes(input[0..4].try_into().expect("slice length checked"));
107 let y = f32::from_le_bytes(input[4..8].try_into().expect("slice length checked"));
108 let z = f32::from_le_bytes(input[8..12].try_into().expect("slice length checked"));
109 Ok(Vec3 { x, y, z })
110 }
111
112 fn fixed_size(&self) -> Option<usize> {
113 Some(12)
114 }
115}
116
117fn exact_array<const N: usize>(input: &[u8]) -> Result<[u8; N], ComponentCodecError> {
118 if input.len() != N {
119 return Err(ComponentCodecError::ExpectedBytes {
120 expected: N,
121 actual: input.len(),
122 });
123 }
124 let mut out = [0_u8; N];
125 out.copy_from_slice(input);
126 Ok(out)
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum ComponentStorageKind {
132 SparseBlob,
134 External,
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum ComponentSyncMode {
141 NotReplicated,
143 Delta,
145 Snapshot,
147 EventOnly,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum ComponentMigrationMode {
154 Copy,
156 Drop,
158 External,
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct ComponentDescriptor {
165 pub id: ComponentId,
167 pub name: &'static str,
169 pub storage: ComponentStorageKind,
171 pub sync: ComponentSyncMode,
173 pub migration: ComponentMigrationMode,
175 pub max_bytes: usize,
177 pub schema_hash: u64,
179}
180
181impl ComponentDescriptor {
182 pub const fn sparse_blob(
184 id: ComponentId,
185 name: &'static str,
186 sync: ComponentSyncMode,
187 migration: ComponentMigrationMode,
188 max_bytes: usize,
189 ) -> Self {
190 Self {
191 id,
192 name,
193 storage: ComponentStorageKind::SparseBlob,
194 sync,
195 migration,
196 max_bytes,
197 schema_hash: 0,
198 }
199 }
200
201 #[must_use]
203 pub const fn with_schema_hash(mut self, schema_hash: u64) -> Self {
204 self.schema_hash = schema_hash;
205 self
206 }
207}
208
209#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct ComponentSchema {
212 pub descriptor: ComponentDescriptor,
214 pub fixed_size: Option<usize>,
216}
217
218impl ComponentSchema {
219 pub fn new<T, C: ComponentCodec<T>>(descriptor: ComponentDescriptor, codec: &C) -> Self {
221 Self {
222 descriptor,
223 fixed_size: codec.fixed_size(),
224 }
225 }
226}
227
228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum ComponentFieldType {
231 U8,
233 U16,
235 U32,
237 U64,
239 I32,
241 F32,
243 Vec3,
245 Bytes {
247 max_len: usize,
249 },
250}
251
252impl ComponentFieldType {
253 pub const fn max_size(self) -> usize {
255 match self {
256 Self::U8 => 1,
257 Self::U16 => 2,
258 Self::U32 | Self::I32 | Self::F32 => 4,
259 Self::U64 => 8,
260 Self::Vec3 => 12,
261 Self::Bytes { max_len } => max_len,
262 }
263 }
264
265 const fn tag(self) -> u8 {
266 match self {
267 Self::U8 => 1,
268 Self::U16 => 2,
269 Self::U32 => 3,
270 Self::U64 => 4,
271 Self::I32 => 5,
272 Self::F32 => 6,
273 Self::Vec3 => 7,
274 Self::Bytes { .. } => 8,
275 }
276 }
277}
278
279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub struct ComponentFieldDescriptor {
282 pub name: &'static str,
284 pub ty: ComponentFieldType,
286 pub offset: usize,
288}
289
290impl ComponentFieldDescriptor {
291 pub const fn new(name: &'static str, ty: ComponentFieldType, offset: usize) -> Self {
293 Self { name, ty, offset }
294 }
295
296 pub const fn end_offset(self) -> usize {
298 self.offset.saturating_add(self.ty.max_size())
299 }
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq)]
304pub struct GeneratedComponentSchema {
305 pub id: ComponentId,
307 pub name: &'static str,
309 pub storage: ComponentStorageKind,
311 pub sync: ComponentSyncMode,
313 pub migration: ComponentMigrationMode,
315 pub max_bytes: usize,
317 pub fields: &'static [ComponentFieldDescriptor],
319}
320
321impl GeneratedComponentSchema {
322 pub const fn new(
324 id: ComponentId,
325 name: &'static str,
326 storage: ComponentStorageKind,
327 sync: ComponentSyncMode,
328 migration: ComponentMigrationMode,
329 max_bytes: usize,
330 fields: &'static [ComponentFieldDescriptor],
331 ) -> Self {
332 Self {
333 id,
334 name,
335 storage,
336 sync,
337 migration,
338 max_bytes,
339 fields,
340 }
341 }
342
343 pub fn validate(&self) -> Result<(), ComponentSchemaError> {
345 for (index, field) in self.fields.iter().enumerate() {
346 if field.end_offset() > self.max_bytes {
347 return Err(ComponentSchemaError::FieldOutOfBounds {
348 name: field.name,
349 offset: field.offset,
350 size: field.ty.max_size(),
351 max_bytes: self.max_bytes,
352 });
353 }
354
355 for earlier in &self.fields[..index] {
356 if earlier.name == field.name {
357 return Err(ComponentSchemaError::DuplicateFieldName(field.name));
358 }
359 if ranges_overlap(
360 earlier.offset,
361 earlier.end_offset(),
362 field.offset,
363 field.end_offset(),
364 ) {
365 return Err(ComponentSchemaError::FieldOverlap {
366 left: earlier.name,
367 right: field.name,
368 });
369 }
370 }
371 }
372 Ok(())
373 }
374
375 pub fn schema_hash(&self) -> u64 {
377 let mut hash = FNV_OFFSET;
378 hash = hash_u64(hash, self.id.get().into());
379 hash = hash_str(hash, self.name);
380 hash = hash_u8(hash, storage_tag(self.storage));
381 hash = hash_u8(hash, sync_tag(self.sync));
382 hash = hash_u8(hash, migration_tag(self.migration));
383 hash = hash_u64(hash, self.max_bytes as u64);
384 for field in self.fields {
385 hash = hash_str(hash, field.name);
386 hash = hash_u8(hash, field.ty.tag());
387 hash = hash_u64(hash, field.ty.max_size() as u64);
388 hash = hash_u64(hash, field.offset as u64);
389 }
390 hash
391 }
392
393 pub fn fixed_size(&self) -> Option<usize> {
395 self.fields
396 .iter()
397 .map(|field| field.end_offset())
398 .max()
399 .or(Some(0))
400 }
401
402 pub fn descriptor(&self) -> ComponentDescriptor {
404 ComponentDescriptor {
405 id: self.id,
406 name: self.name,
407 storage: self.storage,
408 sync: self.sync,
409 migration: self.migration,
410 max_bytes: self.max_bytes,
411 schema_hash: self.schema_hash(),
412 }
413 }
414
415 pub fn component_schema(&self) -> ComponentSchema {
417 ComponentSchema {
418 descriptor: self.descriptor(),
419 fixed_size: self.fixed_size(),
420 }
421 }
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426pub enum ComponentSchemaError {
427 DuplicateFieldName(&'static str),
429 FieldOutOfBounds {
431 name: &'static str,
433 offset: usize,
435 size: usize,
437 max_bytes: usize,
439 },
440 FieldOverlap {
442 left: &'static str,
444 right: &'static str,
446 },
447}
448
449impl core::fmt::Display for ComponentSchemaError {
450 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
451 match self {
452 Self::DuplicateFieldName(name) => write!(f, "duplicate component field {name}"),
453 Self::FieldOutOfBounds {
454 name,
455 offset,
456 size,
457 max_bytes,
458 } => write!(
459 f,
460 "component field {name} at {offset} with size {size} exceeds max bytes {max_bytes}"
461 ),
462 Self::FieldOverlap { left, right } => {
463 write!(f, "component fields {left} and {right} overlap")
464 }
465 }
466 }
467}
468
469impl std::error::Error for ComponentSchemaError {}
470
471#[derive(Clone, Debug, PartialEq, Eq)]
473pub enum ComponentRegistryError {
474 DuplicateId(ComponentId),
476 DuplicateName(&'static str),
478}
479
480impl core::fmt::Display for ComponentRegistryError {
481 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
482 match self {
483 Self::DuplicateId(id) => write!(f, "duplicate component id {}", id.get()),
484 Self::DuplicateName(name) => write!(f, "duplicate component name {name}"),
485 }
486 }
487}
488
489impl std::error::Error for ComponentRegistryError {}
490
491#[derive(Clone, Debug, PartialEq, Eq)]
493pub enum GeneratedSchemaRegistrationError {
494 Schema(ComponentSchemaError),
496 Registry(ComponentRegistryError),
498}
499
500impl core::fmt::Display for GeneratedSchemaRegistrationError {
501 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
502 match self {
503 Self::Schema(error) => write!(f, "{error}"),
504 Self::Registry(error) => write!(f, "{error}"),
505 }
506 }
507}
508
509impl std::error::Error for GeneratedSchemaRegistrationError {
510 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
511 match self {
512 Self::Schema(error) => Some(error),
513 Self::Registry(error) => Some(error),
514 }
515 }
516}
517
518#[derive(Clone, Debug, Default)]
520pub struct ComponentRegistry {
521 descriptors: Vec<Option<ComponentDescriptor>>,
522}
523
524impl ComponentRegistry {
525 pub fn register(
527 &mut self,
528 descriptor: ComponentDescriptor,
529 ) -> Result<(), ComponentRegistryError> {
530 if self.get(descriptor.id).is_some() {
531 return Err(ComponentRegistryError::DuplicateId(descriptor.id));
532 }
533 if self.iter().any(|existing| existing.name == descriptor.name) {
534 return Err(ComponentRegistryError::DuplicateName(descriptor.name));
535 }
536
537 let index = usize::from(descriptor.id.get());
538 if self.descriptors.len() <= index {
539 self.descriptors.resize(index + 1, None);
540 }
541 self.descriptors[index] = Some(descriptor);
542 Ok(())
543 }
544
545 pub fn register_generated_schema(
547 &mut self,
548 schema: &GeneratedComponentSchema,
549 ) -> Result<ComponentSchema, GeneratedSchemaRegistrationError> {
550 schema
551 .validate()
552 .map_err(GeneratedSchemaRegistrationError::Schema)?;
553 let component_schema = schema.component_schema();
554 self.register(component_schema.descriptor.clone())
555 .map_err(GeneratedSchemaRegistrationError::Registry)?;
556 Ok(component_schema)
557 }
558
559 pub fn get(&self, id: ComponentId) -> Option<&ComponentDescriptor> {
561 self.descriptors
562 .get(usize::from(id.get()))
563 .and_then(Option::as_ref)
564 }
565
566 pub fn iter(&self) -> impl Iterator<Item = &ComponentDescriptor> {
568 self.descriptors.iter().filter_map(Option::as_ref)
569 }
570
571 pub fn len(&self) -> usize {
573 self.iter().count()
574 }
575
576 pub fn is_empty(&self) -> bool {
578 self.len() == 0
579 }
580}
581
582const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
583const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
584
585fn hash_u8(hash: u64, value: u8) -> u64 {
586 (hash ^ u64::from(value)).wrapping_mul(FNV_PRIME)
587}
588
589fn hash_u64(mut hash: u64, value: u64) -> u64 {
590 for byte in value.to_le_bytes() {
591 hash = hash_u8(hash, byte);
592 }
593 hash
594}
595
596fn hash_str(mut hash: u64, value: &str) -> u64 {
597 for byte in value.bytes() {
598 hash = hash_u8(hash, byte);
599 }
600 hash_u8(hash, 0)
601}
602
603fn storage_tag(storage: ComponentStorageKind) -> u8 {
604 match storage {
605 ComponentStorageKind::SparseBlob => 1,
606 ComponentStorageKind::External => 2,
607 }
608}
609
610fn sync_tag(sync: ComponentSyncMode) -> u8 {
611 match sync {
612 ComponentSyncMode::NotReplicated => 0,
613 ComponentSyncMode::Delta => 1,
614 ComponentSyncMode::Snapshot => 2,
615 ComponentSyncMode::EventOnly => 3,
616 }
617}
618
619fn migration_tag(migration: ComponentMigrationMode) -> u8 {
620 match migration {
621 ComponentMigrationMode::Copy => 1,
622 ComponentMigrationMode::Drop => 2,
623 ComponentMigrationMode::External => 3,
624 }
625}
626
627fn ranges_overlap(
628 left_start: usize,
629 left_end: usize,
630 right_start: usize,
631 right_end: usize,
632) -> bool {
633 left_start < right_end && right_start < left_end
634}
635
636#[derive(Clone, Debug, Default, PartialEq, Eq)]
638pub struct ComponentBlob {
639 pub version: u64,
641 pub dirty: bool,
643 pub bytes: Vec<u8>,
645}
646
647#[derive(Clone, Debug, Default)]
649pub struct ComponentEncodeScratch {
650 bytes: Vec<u8>,
651}
652
653impl ComponentEncodeScratch {
654 pub const fn new() -> Self {
656 Self { bytes: Vec::new() }
657 }
658
659 pub fn with_capacity(capacity: usize) -> Self {
661 Self {
662 bytes: Vec::with_capacity(capacity),
663 }
664 }
665
666 pub fn retained_capacity(&self) -> usize {
668 self.bytes.capacity()
669 }
670}
671
672#[derive(Clone, Copy, Debug, PartialEq, Eq)]
674pub enum ComponentStoreError {
675 NotBlobStorage(ComponentId),
677 BlobTooLarge {
679 component_id: ComponentId,
681 actual: usize,
683 max: usize,
685 },
686 Codec(ComponentCodecError),
688 MissingBlob(ComponentId),
690}
691
692impl core::fmt::Display for ComponentStoreError {
693 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
694 match self {
695 Self::NotBlobStorage(id) => {
696 write!(f, "component {} is not SectorSync blob storage", id.get())
697 }
698 Self::BlobTooLarge {
699 component_id,
700 actual,
701 max,
702 } => write!(
703 f,
704 "component {} blob has {} bytes, max {}",
705 component_id.get(),
706 actual,
707 max
708 ),
709 Self::Codec(error) => write!(f, "{error}"),
710 Self::MissingBlob(id) => write!(f, "component {} blob is missing", id.get()),
711 }
712 }
713}
714
715impl std::error::Error for ComponentStoreError {}
716
717#[derive(Clone, Debug, Default)]
718struct ComponentColumn {
719 values: HashMap<EntityHandle, ComponentBlob>,
720}
721
722#[derive(Clone, Debug, Default)]
724pub struct ComponentStore {
725 dense_columns: Vec<Option<ComponentColumn>>,
726 sparse_columns: BTreeMap<ComponentId, ComponentColumn>,
727}
728
729const DENSE_COMPONENT_COLUMN_LIMIT: usize = 256;
730
731impl ComponentStore {
732 pub fn reserve_component(&mut self, component_id: ComponentId, additional_entities: usize) {
734 self.column_mut(component_id)
735 .values
736 .reserve(additional_entities);
737 }
738
739 pub fn column_slots_capacity(&self) -> usize {
741 self.dense_columns
742 .capacity()
743 .saturating_add(self.sparse_columns.len())
744 }
745
746 pub fn component_capacity(&self, component_id: ComponentId) -> usize {
748 self.column(component_id)
749 .map_or(0, |column| column.values.capacity())
750 }
751
752 pub fn set_blob(
754 &mut self,
755 descriptor: &ComponentDescriptor,
756 entity: EntityHandle,
757 version: u64,
758 bytes: Vec<u8>,
759 ) -> Result<(), ComponentStoreError> {
760 validate_blob_write(descriptor, bytes.len())?;
761
762 let column = self.column_mut(descriptor.id);
763 column.values.insert(
764 entity,
765 ComponentBlob {
766 version,
767 dirty: true,
768 bytes,
769 },
770 );
771 Ok(())
772 }
773
774 pub fn set_blob_from_slice(
779 &mut self,
780 descriptor: &ComponentDescriptor,
781 entity: EntityHandle,
782 version: u64,
783 bytes: &[u8],
784 ) -> Result<(), ComponentStoreError> {
785 validate_blob_write(descriptor, bytes.len())?;
786 let column = self.column_mut(descriptor.id);
787 if let Some(blob) = column.values.get_mut(&entity) {
788 blob.bytes.clear();
789 blob.bytes.extend_from_slice(bytes);
790 blob.version = version;
791 blob.dirty = true;
792 } else {
793 column.values.insert(
794 entity,
795 ComponentBlob {
796 version,
797 dirty: true,
798 bytes: bytes.to_vec(),
799 },
800 );
801 }
802 Ok(())
803 }
804
805 pub fn set_typed<T, C: ComponentCodec<T>>(
807 &mut self,
808 descriptor: &ComponentDescriptor,
809 entity: EntityHandle,
810 version: u64,
811 codec: &C,
812 value: &T,
813 ) -> Result<(), ComponentStoreError> {
814 let mut bytes = Vec::with_capacity(codec.fixed_size().unwrap_or(0));
815 codec
816 .encode(value, &mut bytes)
817 .map_err(ComponentStoreError::Codec)?;
818 self.set_blob(descriptor, entity, version, bytes)
819 }
820
821 pub fn set_typed_with_scratch<T, C: ComponentCodec<T>>(
824 &mut self,
825 descriptor: &ComponentDescriptor,
826 entity: EntityHandle,
827 version: u64,
828 codec: &C,
829 value: &T,
830 scratch: &mut ComponentEncodeScratch,
831 ) -> Result<(), ComponentStoreError> {
832 scratch.bytes.clear();
833 if let Some(size) = codec.fixed_size() {
834 scratch.bytes.reserve(size);
835 }
836 codec
837 .encode(value, &mut scratch.bytes)
838 .map_err(ComponentStoreError::Codec)?;
839 self.set_blob_from_slice(descriptor, entity, version, &scratch.bytes)
840 }
841
842 pub fn get_blob(
844 &self,
845 component_id: ComponentId,
846 entity: EntityHandle,
847 ) -> Option<&ComponentBlob> {
848 self.column(component_id)
849 .and_then(|column| column.values.get(&entity))
850 }
851
852 pub fn has_dirty_selected(&self, entity: EntityHandle, component_ids: &[ComponentId]) -> bool {
857 component_ids.iter().any(|component_id| {
858 self.get_blob(*component_id, entity)
859 .is_some_and(|blob| blob.dirty)
860 })
861 }
862
863 pub fn get_typed<T, C: ComponentCodec<T>>(
865 &self,
866 component_id: ComponentId,
867 entity: EntityHandle,
868 codec: &C,
869 ) -> Result<T, ComponentStoreError> {
870 let blob = self
871 .get_blob(component_id, entity)
872 .ok_or(ComponentStoreError::MissingBlob(component_id))?;
873 codec
874 .decode(&blob.bytes)
875 .map_err(ComponentStoreError::Codec)
876 }
877
878 pub fn get_blob_mut(
880 &mut self,
881 component_id: ComponentId,
882 entity: EntityHandle,
883 ) -> Option<&mut ComponentBlob> {
884 let index = usize::from(component_id.get());
885 if index < DENSE_COMPONENT_COLUMN_LIMIT {
886 self.dense_columns
887 .get_mut(index)
888 .and_then(Option::as_mut)
889 .and_then(|column| column.values.get_mut(&entity))
890 } else {
891 self.sparse_columns
892 .get_mut(&component_id)
893 .and_then(|column| column.values.get_mut(&entity))
894 }
895 }
896
897 pub fn clear_dirty_for_entity(&mut self, entity: EntityHandle) -> usize {
899 let mut cleared = 0;
900 for column in self
901 .dense_columns
902 .iter_mut()
903 .filter_map(Option::as_mut)
904 .chain(self.sparse_columns.values_mut())
905 {
906 if let Some(blob) = column.values.get_mut(&entity)
907 && blob.dirty
908 {
909 blob.dirty = false;
910 cleared += 1;
911 }
912 }
913 cleared
914 }
915
916 pub fn remove_entity(&mut self, entity: EntityHandle) -> Vec<(ComponentId, ComponentBlob)> {
918 let mut removed = Vec::new();
919 self.remove_entity_into(entity, &mut removed);
920 removed
921 }
922
923 pub fn remove_entity_into(
925 &mut self,
926 entity: EntityHandle,
927 removed: &mut Vec<(ComponentId, ComponentBlob)>,
928 ) -> usize {
929 removed.clear();
930 for (index, column) in self.dense_columns.iter_mut().enumerate() {
931 let Some(column) = column else {
932 continue;
933 };
934 if let Some(blob) = column.values.remove(&entity) {
935 removed.push((
936 ComponentId::new(u16::try_from(index).expect("dense component id fits u16")),
937 blob,
938 ));
939 }
940 }
941 for (component_id, column) in &mut self.sparse_columns {
942 if let Some(blob) = column.values.remove(&entity) {
943 removed.push((*component_id, blob));
944 }
945 }
946 removed.len()
947 }
948
949 pub fn clear_entity(&mut self, entity: EntityHandle) -> usize {
951 let mut removed = 0_usize;
952 for column in self
953 .dense_columns
954 .iter_mut()
955 .filter_map(Option::as_mut)
956 .chain(self.sparse_columns.values_mut())
957 {
958 removed = removed.saturating_add(usize::from(column.values.remove(&entity).is_some()));
959 }
960 removed
961 }
962
963 pub fn copy_for_migration(
965 &mut self,
966 registry: &ComponentRegistry,
967 source: EntityHandle,
968 target: EntityHandle,
969 ) -> usize {
970 let mut copied = 0;
971 for descriptor in registry.iter() {
972 if descriptor.migration != ComponentMigrationMode::Copy {
973 continue;
974 }
975 let Some(blob) = self.get_blob(descriptor.id, source).cloned() else {
976 continue;
977 };
978 self.column_mut(descriptor.id).values.insert(target, blob);
979 copied += 1;
980 }
981 copied
982 }
983
984 pub fn blob_count(&self) -> usize {
986 self.dense_columns
987 .iter()
988 .filter_map(Option::as_ref)
989 .chain(self.sparse_columns.values())
990 .map(|column| column.values.len())
991 .sum()
992 }
993
994 fn column_mut(&mut self, component_id: ComponentId) -> &mut ComponentColumn {
995 let index = usize::from(component_id.get());
996 if index < DENSE_COMPONENT_COLUMN_LIMIT {
997 if self.dense_columns.len() <= index {
998 self.dense_columns.resize_with(index + 1, || None);
999 }
1000 self.dense_columns[index].get_or_insert_with(ComponentColumn::default)
1001 } else {
1002 self.sparse_columns.entry(component_id).or_default()
1003 }
1004 }
1005
1006 fn column(&self, component_id: ComponentId) -> Option<&ComponentColumn> {
1007 let index = usize::from(component_id.get());
1008 if index < DENSE_COMPONENT_COLUMN_LIMIT {
1009 self.dense_columns.get(index).and_then(Option::as_ref)
1010 } else {
1011 self.sparse_columns.get(&component_id)
1012 }
1013 }
1014
1015 pub fn column_count(&self) -> usize {
1017 self.dense_columns
1018 .iter()
1019 .filter(|column| column.is_some())
1020 .count()
1021 + self.sparse_columns.len()
1022 }
1023}
1024
1025fn validate_blob_write(
1026 descriptor: &ComponentDescriptor,
1027 bytes: usize,
1028) -> Result<(), ComponentStoreError> {
1029 if descriptor.storage != ComponentStorageKind::SparseBlob {
1030 return Err(ComponentStoreError::NotBlobStorage(descriptor.id));
1031 }
1032 if bytes > descriptor.max_bytes {
1033 return Err(ComponentStoreError::BlobTooLarge {
1034 component_id: descriptor.id,
1035 actual: bytes,
1036 max: descriptor.max_bytes,
1037 });
1038 }
1039 Ok(())
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045
1046 #[test]
1047 fn component_column_capacity_is_explicit_and_observable() {
1048 let component_id = ComponentId::new(3);
1049 let mut store = ComponentStore::default();
1050 store.reserve_component(component_id, 16);
1051
1052 assert!(store.column_slots_capacity() >= 1);
1053 assert_eq!(store.column_count(), 1);
1054 assert!(store.component_capacity(component_id) >= 16);
1055
1056 let descriptor = ComponentDescriptor::sparse_blob(
1057 component_id,
1058 "reserved",
1059 ComponentSyncMode::Delta,
1060 ComponentMigrationMode::Copy,
1061 4,
1062 );
1063 let handle = EntityHandle::new(1, 0);
1064 store
1065 .set_blob(&descriptor, handle, 1, vec![1, 2, 3, 4])
1066 .expect("reserved component should write");
1067 assert_eq!(
1068 store
1069 .get_blob(component_id, handle)
1070 .map(|blob| blob.bytes.as_slice()),
1071 Some(&[1, 2, 3, 4][..])
1072 );
1073 }
1074
1075 #[test]
1076 fn sparse_component_ids_only_allocate_registered_columns() {
1077 let low = ComponentId::new(1);
1078 let high = ComponentId::new(u16::MAX);
1079 let mut store = ComponentStore::default();
1080 store.reserve_component(high, 4);
1081 let capacity_after_high = store.column_slots_capacity();
1082 store.reserve_component(low, 4);
1083
1084 assert_eq!(store.column_count(), 2);
1085 assert!(capacity_after_high < 16);
1086 assert!(store.column_slots_capacity() < 16);
1087 assert!(store.component_capacity(low) >= 4);
1088 assert!(store.component_capacity(high) >= 4);
1089 }
1090
1091 #[test]
1092 fn slice_updates_reuse_blob_storage_and_reject_oversized_values_atomically() {
1093 let component_id = ComponentId::new(4);
1094 let descriptor = ComponentDescriptor::sparse_blob(
1095 component_id,
1096 "state",
1097 ComponentSyncMode::Delta,
1098 ComponentMigrationMode::Copy,
1099 8,
1100 );
1101 let handle = EntityHandle::new(1, 0);
1102 let mut store = ComponentStore::default();
1103 store
1104 .set_blob(&descriptor, handle, 1, vec![0; 8])
1105 .expect("initial blob should fit");
1106 let retained_bytes = store
1107 .get_blob(component_id, handle)
1108 .expect("blob exists")
1109 .bytes
1110 .as_ptr();
1111
1112 store
1113 .set_blob_from_slice(&descriptor, handle, 2, &[1, 2, 3, 4])
1114 .expect("slice update should fit");
1115 let blob = store.get_blob(component_id, handle).expect("blob exists");
1116 assert_eq!(blob.bytes, [1, 2, 3, 4]);
1117 assert_eq!(blob.bytes.as_ptr(), retained_bytes);
1118 assert_eq!(blob.version, 2);
1119 assert!(blob.dirty);
1120
1121 assert_eq!(
1122 store
1123 .set_blob_from_slice(&descriptor, handle, 3, &[9; 9])
1124 .expect_err("oversized update should fail"),
1125 ComponentStoreError::BlobTooLarge {
1126 component_id,
1127 actual: 9,
1128 max: 8,
1129 }
1130 );
1131 let blob = store.get_blob(component_id, handle).expect("blob remains");
1132 assert_eq!(blob.bytes, [1, 2, 3, 4]);
1133 assert_eq!(blob.version, 2);
1134 }
1135
1136 #[test]
1137 fn registry_rejects_duplicate_ids_and_names() {
1138 let mut registry = ComponentRegistry::default();
1139 let descriptor = ComponentDescriptor::sparse_blob(
1140 ComponentId::new(1),
1141 "health",
1142 ComponentSyncMode::Delta,
1143 ComponentMigrationMode::Copy,
1144 16,
1145 );
1146
1147 registry
1148 .register(descriptor.clone())
1149 .expect("first registration should work");
1150 assert_eq!(
1151 registry
1152 .register(descriptor.clone())
1153 .expect_err("duplicate id"),
1154 ComponentRegistryError::DuplicateId(ComponentId::new(1))
1155 );
1156 assert_eq!(
1157 registry
1158 .register(ComponentDescriptor::sparse_blob(
1159 ComponentId::new(2),
1160 "health",
1161 ComponentSyncMode::Delta,
1162 ComponentMigrationMode::Copy,
1163 16,
1164 ))
1165 .expect_err("duplicate name"),
1166 ComponentRegistryError::DuplicateName("health")
1167 );
1168 }
1169
1170 #[test]
1171 fn component_store_sets_clears_and_migrates_blobs() {
1172 let descriptor = ComponentDescriptor::sparse_blob(
1173 ComponentId::new(1),
1174 "health",
1175 ComponentSyncMode::Delta,
1176 ComponentMigrationMode::Copy,
1177 16,
1178 );
1179 let mut registry = ComponentRegistry::default();
1180 registry
1181 .register(descriptor.clone())
1182 .expect("descriptor should register");
1183 let mut store = ComponentStore::default();
1184 let source = EntityHandle::new(1, 0);
1185 let target = EntityHandle::new(2, 0);
1186
1187 store
1188 .set_blob(&descriptor, source, 7, vec![1, 2, 3])
1189 .expect("blob should fit");
1190 assert!(
1191 store
1192 .get_blob(ComponentId::new(1), source)
1193 .expect("blob")
1194 .dirty
1195 );
1196 assert_eq!(store.clear_dirty_for_entity(source), 1);
1197 assert!(
1198 !store
1199 .get_blob(ComponentId::new(1), source)
1200 .expect("blob")
1201 .dirty
1202 );
1203
1204 assert_eq!(store.copy_for_migration(®istry, source, target), 1);
1205 assert_eq!(
1206 store
1207 .get_blob(ComponentId::new(1), target)
1208 .expect("target blob")
1209 .bytes,
1210 vec![1, 2, 3]
1211 );
1212 }
1213
1214 #[test]
1215 fn component_entity_removal_supports_owned_reusable_and_discard_paths() {
1216 let descriptors = [
1217 ComponentDescriptor::sparse_blob(
1218 ComponentId::new(1),
1219 "health",
1220 ComponentSyncMode::Delta,
1221 ComponentMigrationMode::Copy,
1222 8,
1223 ),
1224 ComponentDescriptor::sparse_blob(
1225 ComponentId::new(2),
1226 "armor",
1227 ComponentSyncMode::Delta,
1228 ComponentMigrationMode::Copy,
1229 8,
1230 ),
1231 ];
1232 let entities = [
1233 EntityHandle::new(1, 0),
1234 EntityHandle::new(2, 0),
1235 EntityHandle::new(3, 0),
1236 EntityHandle::new(4, 0),
1237 ];
1238 let mut store = ComponentStore::default();
1239 for entity in entities {
1240 for descriptor in &descriptors {
1241 store
1242 .set_blob(
1243 descriptor,
1244 entity,
1245 1,
1246 vec![u8::try_from(descriptor.id.get()).expect("small component id"); 4],
1247 )
1248 .expect("bounded blob should write");
1249 }
1250 }
1251
1252 let owned = store.remove_entity(entities[0]);
1253 assert_eq!(owned.len(), 2);
1254 assert_eq!(owned[0].0, ComponentId::new(1));
1255 assert_eq!(owned[1].0, ComponentId::new(2));
1256
1257 let mut removed = Vec::with_capacity(2);
1258 let retained_pointer = removed.as_ptr();
1259 assert_eq!(store.remove_entity_into(entities[1], &mut removed), 2);
1260 assert_eq!(removed.as_ptr(), retained_pointer);
1261 let retained_capacity = removed.capacity();
1262 assert_eq!(store.remove_entity_into(entities[2], &mut removed), 2);
1263 assert_eq!(removed.as_ptr(), retained_pointer);
1264 assert_eq!(removed.capacity(), retained_capacity);
1265
1266 assert_eq!(store.clear_entity(entities[3]), 2);
1267 assert_eq!(store.clear_entity(entities[3]), 0);
1268 assert_eq!(store.blob_count(), 0);
1269 }
1270
1271 #[test]
1272 fn typed_component_codec_roundtrips_values() {
1273 let descriptor = ComponentDescriptor::sparse_blob(
1274 ComponentId::new(3),
1275 "velocity",
1276 ComponentSyncMode::Delta,
1277 ComponentMigrationMode::Copy,
1278 12,
1279 )
1280 .with_schema_hash(0xABCD);
1281 let schema = ComponentSchema::new(descriptor.clone(), &Vec3LeCodec);
1282 assert_eq!(schema.fixed_size, Some(12));
1283 assert_eq!(schema.descriptor.schema_hash, 0xABCD);
1284
1285 let mut store = ComponentStore::default();
1286 let entity = EntityHandle::new(7, 0);
1287 let value = Vec3::new(1.0, 2.0, 3.5);
1288
1289 store
1290 .set_typed(&descriptor, entity, 1, &Vec3LeCodec, &value)
1291 .expect("typed set should work");
1292 let decoded = store
1293 .get_typed(ComponentId::new(3), entity, &Vec3LeCodec)
1294 .expect("typed get should work");
1295 assert_eq!(decoded, value);
1296 }
1297
1298 #[test]
1299 fn typed_component_scratch_reuses_encoding_and_blob_capacity() {
1300 let component_id = ComponentId::new(5);
1301 let descriptor = ComponentDescriptor::sparse_blob(
1302 component_id,
1303 "score",
1304 ComponentSyncMode::Delta,
1305 ComponentMigrationMode::Copy,
1306 4,
1307 );
1308 let entity = EntityHandle::new(7, 0);
1309 let mut store = ComponentStore::default();
1310 let mut scratch = ComponentEncodeScratch::new();
1311
1312 store
1313 .set_typed_with_scratch(&descriptor, entity, 1, &U32LeCodec, &10, &mut scratch)
1314 .expect("initial typed write should work");
1315 let retained_scratch = scratch.retained_capacity();
1316 let retained_blob = store
1317 .get_blob(component_id, entity)
1318 .expect("blob exists")
1319 .bytes
1320 .as_ptr();
1321 store
1322 .set_typed_with_scratch(&descriptor, entity, 2, &U32LeCodec, &20, &mut scratch)
1323 .expect("repeated typed write should work");
1324
1325 assert_eq!(scratch.retained_capacity(), retained_scratch);
1326 assert_eq!(
1327 store
1328 .get_blob(component_id, entity)
1329 .expect("blob exists")
1330 .bytes
1331 .as_ptr(),
1332 retained_blob
1333 );
1334 assert_eq!(
1335 store
1336 .get_typed(component_id, entity, &U32LeCodec)
1337 .expect("typed value decodes"),
1338 20
1339 );
1340 }
1341
1342 #[test]
1343 fn selected_dirty_query_only_considers_requested_components() {
1344 let selected = ComponentDescriptor::sparse_blob(
1345 ComponentId::new(1),
1346 "selected",
1347 ComponentSyncMode::Delta,
1348 ComponentMigrationMode::Copy,
1349 4,
1350 );
1351 let ignored = ComponentDescriptor::sparse_blob(
1352 ComponentId::new(2),
1353 "ignored",
1354 ComponentSyncMode::Delta,
1355 ComponentMigrationMode::Copy,
1356 4,
1357 );
1358 let entity = EntityHandle::new(1, 0);
1359 let mut store = ComponentStore::default();
1360 store
1361 .set_blob(&selected, entity, 1, vec![1])
1362 .expect("selected blob should fit");
1363 store
1364 .set_blob(&ignored, entity, 1, vec![2])
1365 .expect("ignored blob should fit");
1366
1367 assert!(store.has_dirty_selected(entity, &[selected.id]));
1368 store
1369 .get_blob_mut(selected.id, entity)
1370 .expect("selected blob exists")
1371 .dirty = false;
1372 assert!(!store.has_dirty_selected(entity, &[selected.id]));
1373 assert!(store.has_dirty_selected(entity, &[ignored.id]));
1374 assert!(!store.has_dirty_selected(entity, &[]));
1375 }
1376
1377 #[test]
1378 fn generated_schema_builds_descriptor_and_registers() {
1379 const FIELDS: &[ComponentFieldDescriptor] = &[
1380 ComponentFieldDescriptor::new("position", ComponentFieldType::Vec3, 0),
1381 ComponentFieldDescriptor::new("health", ComponentFieldType::U32, 12),
1382 ];
1383 let generated = GeneratedComponentSchema::new(
1384 ComponentId::new(8),
1385 "unit_state",
1386 ComponentStorageKind::SparseBlob,
1387 ComponentSyncMode::Delta,
1388 ComponentMigrationMode::Copy,
1389 16,
1390 FIELDS,
1391 );
1392
1393 generated.validate().expect("schema should be valid");
1394 assert_eq!(generated.fixed_size(), Some(16));
1395 assert_ne!(generated.schema_hash(), 0);
1396
1397 let descriptor = generated.descriptor();
1398 assert_eq!(descriptor.id, ComponentId::new(8));
1399 assert_eq!(descriptor.schema_hash, generated.schema_hash());
1400
1401 let mut registry = ComponentRegistry::default();
1402 let schema = registry
1403 .register_generated_schema(&generated)
1404 .expect("generated schema should register");
1405 assert_eq!(schema.fixed_size, Some(16));
1406 assert_eq!(
1407 registry
1408 .get(ComponentId::new(8))
1409 .expect("registered descriptor")
1410 .schema_hash,
1411 generated.schema_hash()
1412 );
1413 }
1414
1415 #[test]
1416 fn generated_schema_validation_rejects_bad_layouts() {
1417 const DUP_FIELDS: &[ComponentFieldDescriptor] = &[
1418 ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 0),
1419 ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 4),
1420 ];
1421 const OVERLAP_FIELDS: &[ComponentFieldDescriptor] = &[
1422 ComponentFieldDescriptor::new("left", ComponentFieldType::U32, 0),
1423 ComponentFieldDescriptor::new("right", ComponentFieldType::U32, 2),
1424 ];
1425 const OOB_FIELDS: &[ComponentFieldDescriptor] = &[ComponentFieldDescriptor::new(
1426 "wide",
1427 ComponentFieldType::U64,
1428 4,
1429 )];
1430
1431 let duplicate = GeneratedComponentSchema::new(
1432 ComponentId::new(1),
1433 "duplicate",
1434 ComponentStorageKind::SparseBlob,
1435 ComponentSyncMode::Delta,
1436 ComponentMigrationMode::Copy,
1437 8,
1438 DUP_FIELDS,
1439 );
1440 assert_eq!(
1441 duplicate.validate().expect_err("duplicate should fail"),
1442 ComponentSchemaError::DuplicateFieldName("x")
1443 );
1444
1445 let overlap = GeneratedComponentSchema::new(
1446 ComponentId::new(2),
1447 "overlap",
1448 ComponentStorageKind::SparseBlob,
1449 ComponentSyncMode::Delta,
1450 ComponentMigrationMode::Copy,
1451 8,
1452 OVERLAP_FIELDS,
1453 );
1454 assert_eq!(
1455 overlap.validate().expect_err("overlap should fail"),
1456 ComponentSchemaError::FieldOverlap {
1457 left: "left",
1458 right: "right"
1459 }
1460 );
1461
1462 let out_of_bounds = GeneratedComponentSchema::new(
1463 ComponentId::new(3),
1464 "oob",
1465 ComponentStorageKind::SparseBlob,
1466 ComponentSyncMode::Delta,
1467 ComponentMigrationMode::Copy,
1468 8,
1469 OOB_FIELDS,
1470 );
1471 assert_eq!(
1472 out_of_bounds
1473 .validate()
1474 .expect_err("out of bounds should fail"),
1475 ComponentSchemaError::FieldOutOfBounds {
1476 name: "wide",
1477 offset: 4,
1478 size: 8,
1479 max_bytes: 8
1480 }
1481 );
1482 }
1483}