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 reclaim_retained_capacity(&mut self) {
741 for column in self.dense_columns.iter_mut().filter_map(Option::as_mut) {
742 column.values.shrink_to_fit();
743 }
744 for column in self.sparse_columns.values_mut() {
745 column.values.shrink_to_fit();
746 }
747 self.dense_columns.shrink_to_fit();
748 }
749
750 pub fn column_slots_capacity(&self) -> usize {
752 self.dense_columns
753 .capacity()
754 .saturating_add(self.sparse_columns.len())
755 }
756
757 pub fn component_capacity(&self, component_id: ComponentId) -> usize {
759 self.column(component_id)
760 .map_or(0, |column| column.values.capacity())
761 }
762
763 pub fn set_blob(
765 &mut self,
766 descriptor: &ComponentDescriptor,
767 entity: EntityHandle,
768 version: u64,
769 bytes: Vec<u8>,
770 ) -> Result<(), ComponentStoreError> {
771 validate_blob_write(descriptor, bytes.len())?;
772
773 let column = self.column_mut(descriptor.id);
774 column.values.insert(
775 entity,
776 ComponentBlob {
777 version,
778 dirty: true,
779 bytes,
780 },
781 );
782 Ok(())
783 }
784
785 pub fn set_blob_from_slice(
790 &mut self,
791 descriptor: &ComponentDescriptor,
792 entity: EntityHandle,
793 version: u64,
794 bytes: &[u8],
795 ) -> Result<(), ComponentStoreError> {
796 validate_blob_write(descriptor, bytes.len())?;
797 let column = self.column_mut(descriptor.id);
798 if let Some(blob) = column.values.get_mut(&entity) {
799 blob.bytes.clear();
800 blob.bytes.extend_from_slice(bytes);
801 blob.version = version;
802 blob.dirty = true;
803 } else {
804 column.values.insert(
805 entity,
806 ComponentBlob {
807 version,
808 dirty: true,
809 bytes: bytes.to_vec(),
810 },
811 );
812 }
813 Ok(())
814 }
815
816 pub fn set_typed<T, C: ComponentCodec<T>>(
818 &mut self,
819 descriptor: &ComponentDescriptor,
820 entity: EntityHandle,
821 version: u64,
822 codec: &C,
823 value: &T,
824 ) -> Result<(), ComponentStoreError> {
825 let mut bytes = Vec::with_capacity(codec.fixed_size().unwrap_or(0));
826 codec
827 .encode(value, &mut bytes)
828 .map_err(ComponentStoreError::Codec)?;
829 self.set_blob(descriptor, entity, version, bytes)
830 }
831
832 pub fn set_typed_with_scratch<T, C: ComponentCodec<T>>(
835 &mut self,
836 descriptor: &ComponentDescriptor,
837 entity: EntityHandle,
838 version: u64,
839 codec: &C,
840 value: &T,
841 scratch: &mut ComponentEncodeScratch,
842 ) -> Result<(), ComponentStoreError> {
843 scratch.bytes.clear();
844 if let Some(size) = codec.fixed_size() {
845 scratch.bytes.reserve(size);
846 }
847 codec
848 .encode(value, &mut scratch.bytes)
849 .map_err(ComponentStoreError::Codec)?;
850 self.set_blob_from_slice(descriptor, entity, version, &scratch.bytes)
851 }
852
853 pub fn get_blob(
855 &self,
856 component_id: ComponentId,
857 entity: EntityHandle,
858 ) -> Option<&ComponentBlob> {
859 self.column(component_id)
860 .and_then(|column| column.values.get(&entity))
861 }
862
863 pub fn has_dirty_selected(&self, entity: EntityHandle, component_ids: &[ComponentId]) -> bool {
868 component_ids.iter().any(|component_id| {
869 self.get_blob(*component_id, entity)
870 .is_some_and(|blob| blob.dirty)
871 })
872 }
873
874 pub fn get_typed<T, C: ComponentCodec<T>>(
876 &self,
877 component_id: ComponentId,
878 entity: EntityHandle,
879 codec: &C,
880 ) -> Result<T, ComponentStoreError> {
881 let blob = self
882 .get_blob(component_id, entity)
883 .ok_or(ComponentStoreError::MissingBlob(component_id))?;
884 codec
885 .decode(&blob.bytes)
886 .map_err(ComponentStoreError::Codec)
887 }
888
889 pub fn get_blob_mut(
891 &mut self,
892 component_id: ComponentId,
893 entity: EntityHandle,
894 ) -> Option<&mut ComponentBlob> {
895 let index = usize::from(component_id.get());
896 if index < DENSE_COMPONENT_COLUMN_LIMIT {
897 self.dense_columns
898 .get_mut(index)
899 .and_then(Option::as_mut)
900 .and_then(|column| column.values.get_mut(&entity))
901 } else {
902 self.sparse_columns
903 .get_mut(&component_id)
904 .and_then(|column| column.values.get_mut(&entity))
905 }
906 }
907
908 pub fn clear_dirty_for_entity(&mut self, entity: EntityHandle) -> usize {
910 let mut cleared = 0;
911 for column in self
912 .dense_columns
913 .iter_mut()
914 .filter_map(Option::as_mut)
915 .chain(self.sparse_columns.values_mut())
916 {
917 if let Some(blob) = column.values.get_mut(&entity)
918 && blob.dirty
919 {
920 blob.dirty = false;
921 cleared += 1;
922 }
923 }
924 cleared
925 }
926
927 pub fn remove_entity(&mut self, entity: EntityHandle) -> Vec<(ComponentId, ComponentBlob)> {
929 let mut removed = Vec::new();
930 self.remove_entity_into(entity, &mut removed);
931 removed
932 }
933
934 pub fn remove_entity_into(
936 &mut self,
937 entity: EntityHandle,
938 removed: &mut Vec<(ComponentId, ComponentBlob)>,
939 ) -> usize {
940 removed.clear();
941 for (index, column) in self.dense_columns.iter_mut().enumerate() {
942 let Some(column) = column else {
943 continue;
944 };
945 if let Some(blob) = column.values.remove(&entity) {
946 removed.push((
947 ComponentId::new(u16::try_from(index).expect("dense component id fits u16")),
948 blob,
949 ));
950 }
951 }
952 for (component_id, column) in &mut self.sparse_columns {
953 if let Some(blob) = column.values.remove(&entity) {
954 removed.push((*component_id, blob));
955 }
956 }
957 removed.len()
958 }
959
960 pub fn clear_entity(&mut self, entity: EntityHandle) -> usize {
962 let mut removed = 0_usize;
963 for column in self
964 .dense_columns
965 .iter_mut()
966 .filter_map(Option::as_mut)
967 .chain(self.sparse_columns.values_mut())
968 {
969 removed = removed.saturating_add(usize::from(column.values.remove(&entity).is_some()));
970 }
971 removed
972 }
973
974 pub fn copy_for_migration(
976 &mut self,
977 registry: &ComponentRegistry,
978 source: EntityHandle,
979 target: EntityHandle,
980 ) -> usize {
981 let mut copied = 0;
982 for descriptor in registry.iter() {
983 if descriptor.migration != ComponentMigrationMode::Copy {
984 continue;
985 }
986 let Some(blob) = self.get_blob(descriptor.id, source).cloned() else {
987 continue;
988 };
989 self.column_mut(descriptor.id).values.insert(target, blob);
990 copied += 1;
991 }
992 copied
993 }
994
995 pub fn blob_count(&self) -> usize {
997 self.dense_columns
998 .iter()
999 .filter_map(Option::as_ref)
1000 .chain(self.sparse_columns.values())
1001 .map(|column| column.values.len())
1002 .sum()
1003 }
1004
1005 fn column_mut(&mut self, component_id: ComponentId) -> &mut ComponentColumn {
1006 let index = usize::from(component_id.get());
1007 if index < DENSE_COMPONENT_COLUMN_LIMIT {
1008 if self.dense_columns.len() <= index {
1009 self.dense_columns.resize_with(index + 1, || None);
1010 }
1011 self.dense_columns[index].get_or_insert_with(ComponentColumn::default)
1012 } else {
1013 self.sparse_columns.entry(component_id).or_default()
1014 }
1015 }
1016
1017 fn column(&self, component_id: ComponentId) -> Option<&ComponentColumn> {
1018 let index = usize::from(component_id.get());
1019 if index < DENSE_COMPONENT_COLUMN_LIMIT {
1020 self.dense_columns.get(index).and_then(Option::as_ref)
1021 } else {
1022 self.sparse_columns.get(&component_id)
1023 }
1024 }
1025
1026 pub fn column_count(&self) -> usize {
1028 self.dense_columns
1029 .iter()
1030 .filter(|column| column.is_some())
1031 .count()
1032 + self.sparse_columns.len()
1033 }
1034}
1035
1036fn validate_blob_write(
1037 descriptor: &ComponentDescriptor,
1038 bytes: usize,
1039) -> Result<(), ComponentStoreError> {
1040 if descriptor.storage != ComponentStorageKind::SparseBlob {
1041 return Err(ComponentStoreError::NotBlobStorage(descriptor.id));
1042 }
1043 if bytes > descriptor.max_bytes {
1044 return Err(ComponentStoreError::BlobTooLarge {
1045 component_id: descriptor.id,
1046 actual: bytes,
1047 max: descriptor.max_bytes,
1048 });
1049 }
1050 Ok(())
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056
1057 #[test]
1058 fn component_column_capacity_is_explicit_and_observable() {
1059 let component_id = ComponentId::new(3);
1060 let mut store = ComponentStore::default();
1061 store.reserve_component(component_id, 16);
1062
1063 assert!(store.column_slots_capacity() >= 1);
1064 assert_eq!(store.column_count(), 1);
1065 assert!(store.component_capacity(component_id) >= 16);
1066
1067 let descriptor = ComponentDescriptor::sparse_blob(
1068 component_id,
1069 "reserved",
1070 ComponentSyncMode::Delta,
1071 ComponentMigrationMode::Copy,
1072 4,
1073 );
1074 let handle = EntityHandle::new(1, 0);
1075 store
1076 .set_blob(&descriptor, handle, 1, vec![1, 2, 3, 4])
1077 .expect("reserved component should write");
1078 assert_eq!(
1079 store
1080 .get_blob(component_id, handle)
1081 .map(|blob| blob.bytes.as_slice()),
1082 Some(&[1, 2, 3, 4][..])
1083 );
1084 }
1085
1086 #[test]
1087 fn sparse_component_ids_only_allocate_registered_columns() {
1088 let low = ComponentId::new(1);
1089 let high = ComponentId::new(u16::MAX);
1090 let mut store = ComponentStore::default();
1091 store.reserve_component(high, 4);
1092 let capacity_after_high = store.column_slots_capacity();
1093 store.reserve_component(low, 4);
1094
1095 assert_eq!(store.column_count(), 2);
1096 assert!(capacity_after_high < 16);
1097 assert!(store.column_slots_capacity() < 16);
1098 assert!(store.component_capacity(low) >= 4);
1099 assert!(store.component_capacity(high) >= 4);
1100 }
1101
1102 #[test]
1103 fn slice_updates_reuse_blob_storage_and_reject_oversized_values_atomically() {
1104 let component_id = ComponentId::new(4);
1105 let descriptor = ComponentDescriptor::sparse_blob(
1106 component_id,
1107 "state",
1108 ComponentSyncMode::Delta,
1109 ComponentMigrationMode::Copy,
1110 8,
1111 );
1112 let handle = EntityHandle::new(1, 0);
1113 let mut store = ComponentStore::default();
1114 store
1115 .set_blob(&descriptor, handle, 1, vec![0; 8])
1116 .expect("initial blob should fit");
1117 let retained_bytes = store
1118 .get_blob(component_id, handle)
1119 .expect("blob exists")
1120 .bytes
1121 .as_ptr();
1122
1123 store
1124 .set_blob_from_slice(&descriptor, handle, 2, &[1, 2, 3, 4])
1125 .expect("slice update should fit");
1126 let blob = store.get_blob(component_id, handle).expect("blob exists");
1127 assert_eq!(blob.bytes, [1, 2, 3, 4]);
1128 assert_eq!(blob.bytes.as_ptr(), retained_bytes);
1129 assert_eq!(blob.version, 2);
1130 assert!(blob.dirty);
1131
1132 assert_eq!(
1133 store
1134 .set_blob_from_slice(&descriptor, handle, 3, &[9; 9])
1135 .expect_err("oversized update should fail"),
1136 ComponentStoreError::BlobTooLarge {
1137 component_id,
1138 actual: 9,
1139 max: 8,
1140 }
1141 );
1142 let blob = store.get_blob(component_id, handle).expect("blob remains");
1143 assert_eq!(blob.bytes, [1, 2, 3, 4]);
1144 assert_eq!(blob.version, 2);
1145 }
1146
1147 #[test]
1148 fn registry_rejects_duplicate_ids_and_names() {
1149 let mut registry = ComponentRegistry::default();
1150 let descriptor = ComponentDescriptor::sparse_blob(
1151 ComponentId::new(1),
1152 "health",
1153 ComponentSyncMode::Delta,
1154 ComponentMigrationMode::Copy,
1155 16,
1156 );
1157
1158 registry
1159 .register(descriptor.clone())
1160 .expect("first registration should work");
1161 assert_eq!(
1162 registry
1163 .register(descriptor.clone())
1164 .expect_err("duplicate id"),
1165 ComponentRegistryError::DuplicateId(ComponentId::new(1))
1166 );
1167 assert_eq!(
1168 registry
1169 .register(ComponentDescriptor::sparse_blob(
1170 ComponentId::new(2),
1171 "health",
1172 ComponentSyncMode::Delta,
1173 ComponentMigrationMode::Copy,
1174 16,
1175 ))
1176 .expect_err("duplicate name"),
1177 ComponentRegistryError::DuplicateName("health")
1178 );
1179 }
1180
1181 #[test]
1182 fn component_store_sets_clears_and_migrates_blobs() {
1183 let descriptor = ComponentDescriptor::sparse_blob(
1184 ComponentId::new(1),
1185 "health",
1186 ComponentSyncMode::Delta,
1187 ComponentMigrationMode::Copy,
1188 16,
1189 );
1190 let mut registry = ComponentRegistry::default();
1191 registry
1192 .register(descriptor.clone())
1193 .expect("descriptor should register");
1194 let mut store = ComponentStore::default();
1195 let source = EntityHandle::new(1, 0);
1196 let target = EntityHandle::new(2, 0);
1197
1198 store
1199 .set_blob(&descriptor, source, 7, vec![1, 2, 3])
1200 .expect("blob should fit");
1201 assert!(
1202 store
1203 .get_blob(ComponentId::new(1), source)
1204 .expect("blob")
1205 .dirty
1206 );
1207 assert_eq!(store.clear_dirty_for_entity(source), 1);
1208 assert!(
1209 !store
1210 .get_blob(ComponentId::new(1), source)
1211 .expect("blob")
1212 .dirty
1213 );
1214
1215 assert_eq!(store.copy_for_migration(®istry, source, target), 1);
1216 assert_eq!(
1217 store
1218 .get_blob(ComponentId::new(1), target)
1219 .expect("target blob")
1220 .bytes,
1221 vec![1, 2, 3]
1222 );
1223 }
1224
1225 #[test]
1226 fn component_entity_removal_supports_owned_reusable_and_discard_paths() {
1227 let descriptors = [
1228 ComponentDescriptor::sparse_blob(
1229 ComponentId::new(1),
1230 "health",
1231 ComponentSyncMode::Delta,
1232 ComponentMigrationMode::Copy,
1233 8,
1234 ),
1235 ComponentDescriptor::sparse_blob(
1236 ComponentId::new(2),
1237 "armor",
1238 ComponentSyncMode::Delta,
1239 ComponentMigrationMode::Copy,
1240 8,
1241 ),
1242 ];
1243 let entities = [
1244 EntityHandle::new(1, 0),
1245 EntityHandle::new(2, 0),
1246 EntityHandle::new(3, 0),
1247 EntityHandle::new(4, 0),
1248 ];
1249 let mut store = ComponentStore::default();
1250 for entity in entities {
1251 for descriptor in &descriptors {
1252 store
1253 .set_blob(
1254 descriptor,
1255 entity,
1256 1,
1257 vec![u8::try_from(descriptor.id.get()).expect("small component id"); 4],
1258 )
1259 .expect("bounded blob should write");
1260 }
1261 }
1262
1263 let owned = store.remove_entity(entities[0]);
1264 assert_eq!(owned.len(), 2);
1265 assert_eq!(owned[0].0, ComponentId::new(1));
1266 assert_eq!(owned[1].0, ComponentId::new(2));
1267
1268 let mut removed = Vec::with_capacity(2);
1269 let retained_pointer = removed.as_ptr();
1270 assert_eq!(store.remove_entity_into(entities[1], &mut removed), 2);
1271 assert_eq!(removed.as_ptr(), retained_pointer);
1272 let retained_capacity = removed.capacity();
1273 assert_eq!(store.remove_entity_into(entities[2], &mut removed), 2);
1274 assert_eq!(removed.as_ptr(), retained_pointer);
1275 assert_eq!(removed.capacity(), retained_capacity);
1276
1277 assert_eq!(store.clear_entity(entities[3]), 2);
1278 assert_eq!(store.clear_entity(entities[3]), 0);
1279 assert_eq!(store.blob_count(), 0);
1280 }
1281
1282 #[test]
1283 fn typed_component_codec_roundtrips_values() {
1284 let descriptor = ComponentDescriptor::sparse_blob(
1285 ComponentId::new(3),
1286 "velocity",
1287 ComponentSyncMode::Delta,
1288 ComponentMigrationMode::Copy,
1289 12,
1290 )
1291 .with_schema_hash(0xABCD);
1292 let schema = ComponentSchema::new(descriptor.clone(), &Vec3LeCodec);
1293 assert_eq!(schema.fixed_size, Some(12));
1294 assert_eq!(schema.descriptor.schema_hash, 0xABCD);
1295
1296 let mut store = ComponentStore::default();
1297 let entity = EntityHandle::new(7, 0);
1298 let value = Vec3::new(1.0, 2.0, 3.5);
1299
1300 store
1301 .set_typed(&descriptor, entity, 1, &Vec3LeCodec, &value)
1302 .expect("typed set should work");
1303 let decoded = store
1304 .get_typed(ComponentId::new(3), entity, &Vec3LeCodec)
1305 .expect("typed get should work");
1306 assert_eq!(decoded, value);
1307 }
1308
1309 #[test]
1310 fn typed_component_scratch_reuses_encoding_and_blob_capacity() {
1311 let component_id = ComponentId::new(5);
1312 let descriptor = ComponentDescriptor::sparse_blob(
1313 component_id,
1314 "score",
1315 ComponentSyncMode::Delta,
1316 ComponentMigrationMode::Copy,
1317 4,
1318 );
1319 let entity = EntityHandle::new(7, 0);
1320 let mut store = ComponentStore::default();
1321 let mut scratch = ComponentEncodeScratch::new();
1322
1323 store
1324 .set_typed_with_scratch(&descriptor, entity, 1, &U32LeCodec, &10, &mut scratch)
1325 .expect("initial typed write should work");
1326 let retained_scratch = scratch.retained_capacity();
1327 let retained_blob = store
1328 .get_blob(component_id, entity)
1329 .expect("blob exists")
1330 .bytes
1331 .as_ptr();
1332 store
1333 .set_typed_with_scratch(&descriptor, entity, 2, &U32LeCodec, &20, &mut scratch)
1334 .expect("repeated typed write should work");
1335
1336 assert_eq!(scratch.retained_capacity(), retained_scratch);
1337 assert_eq!(
1338 store
1339 .get_blob(component_id, entity)
1340 .expect("blob exists")
1341 .bytes
1342 .as_ptr(),
1343 retained_blob
1344 );
1345 assert_eq!(
1346 store
1347 .get_typed(component_id, entity, &U32LeCodec)
1348 .expect("typed value decodes"),
1349 20
1350 );
1351 }
1352
1353 #[test]
1354 fn selected_dirty_query_only_considers_requested_components() {
1355 let selected = ComponentDescriptor::sparse_blob(
1356 ComponentId::new(1),
1357 "selected",
1358 ComponentSyncMode::Delta,
1359 ComponentMigrationMode::Copy,
1360 4,
1361 );
1362 let ignored = ComponentDescriptor::sparse_blob(
1363 ComponentId::new(2),
1364 "ignored",
1365 ComponentSyncMode::Delta,
1366 ComponentMigrationMode::Copy,
1367 4,
1368 );
1369 let entity = EntityHandle::new(1, 0);
1370 let mut store = ComponentStore::default();
1371 store
1372 .set_blob(&selected, entity, 1, vec![1])
1373 .expect("selected blob should fit");
1374 store
1375 .set_blob(&ignored, entity, 1, vec![2])
1376 .expect("ignored blob should fit");
1377
1378 assert!(store.has_dirty_selected(entity, &[selected.id]));
1379 store
1380 .get_blob_mut(selected.id, entity)
1381 .expect("selected blob exists")
1382 .dirty = false;
1383 assert!(!store.has_dirty_selected(entity, &[selected.id]));
1384 assert!(store.has_dirty_selected(entity, &[ignored.id]));
1385 assert!(!store.has_dirty_selected(entity, &[]));
1386 }
1387
1388 #[test]
1389 fn reclaim_retained_capacity_preserves_component_blobs() {
1390 let descriptor = ComponentDescriptor::sparse_blob(
1391 ComponentId::new(1),
1392 "health",
1393 ComponentSyncMode::Delta,
1394 ComponentMigrationMode::Copy,
1395 4,
1396 );
1397 let handle = EntityHandle::new(1, 0);
1398 let mut store = ComponentStore::default();
1399 store.reserve_component(descriptor.id, 64);
1400 store
1401 .set_blob_from_slice(&descriptor, handle, 1, &[1, 2, 3, 4])
1402 .expect("component write");
1403
1404 store.reclaim_retained_capacity();
1405
1406 assert_eq!(
1407 store.get_blob(descriptor.id, handle).expect("blob").bytes,
1408 [1, 2, 3, 4]
1409 );
1410 assert!(store.component_capacity(descriptor.id) >= 1);
1411 }
1412
1413 #[test]
1414 fn generated_schema_builds_descriptor_and_registers() {
1415 const FIELDS: &[ComponentFieldDescriptor] = &[
1416 ComponentFieldDescriptor::new("position", ComponentFieldType::Vec3, 0),
1417 ComponentFieldDescriptor::new("health", ComponentFieldType::U32, 12),
1418 ];
1419 let generated = GeneratedComponentSchema::new(
1420 ComponentId::new(8),
1421 "unit_state",
1422 ComponentStorageKind::SparseBlob,
1423 ComponentSyncMode::Delta,
1424 ComponentMigrationMode::Copy,
1425 16,
1426 FIELDS,
1427 );
1428
1429 generated.validate().expect("schema should be valid");
1430 assert_eq!(generated.fixed_size(), Some(16));
1431 assert_ne!(generated.schema_hash(), 0);
1432
1433 let descriptor = generated.descriptor();
1434 assert_eq!(descriptor.id, ComponentId::new(8));
1435 assert_eq!(descriptor.schema_hash, generated.schema_hash());
1436
1437 let mut registry = ComponentRegistry::default();
1438 let schema = registry
1439 .register_generated_schema(&generated)
1440 .expect("generated schema should register");
1441 assert_eq!(schema.fixed_size, Some(16));
1442 assert_eq!(
1443 registry
1444 .get(ComponentId::new(8))
1445 .expect("registered descriptor")
1446 .schema_hash,
1447 generated.schema_hash()
1448 );
1449 }
1450
1451 #[test]
1452 fn generated_schema_validation_rejects_bad_layouts() {
1453 const DUP_FIELDS: &[ComponentFieldDescriptor] = &[
1454 ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 0),
1455 ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 4),
1456 ];
1457 const OVERLAP_FIELDS: &[ComponentFieldDescriptor] = &[
1458 ComponentFieldDescriptor::new("left", ComponentFieldType::U32, 0),
1459 ComponentFieldDescriptor::new("right", ComponentFieldType::U32, 2),
1460 ];
1461 const OOB_FIELDS: &[ComponentFieldDescriptor] = &[ComponentFieldDescriptor::new(
1462 "wide",
1463 ComponentFieldType::U64,
1464 4,
1465 )];
1466
1467 let duplicate = GeneratedComponentSchema::new(
1468 ComponentId::new(1),
1469 "duplicate",
1470 ComponentStorageKind::SparseBlob,
1471 ComponentSyncMode::Delta,
1472 ComponentMigrationMode::Copy,
1473 8,
1474 DUP_FIELDS,
1475 );
1476 assert_eq!(
1477 duplicate.validate().expect_err("duplicate should fail"),
1478 ComponentSchemaError::DuplicateFieldName("x")
1479 );
1480
1481 let overlap = GeneratedComponentSchema::new(
1482 ComponentId::new(2),
1483 "overlap",
1484 ComponentStorageKind::SparseBlob,
1485 ComponentSyncMode::Delta,
1486 ComponentMigrationMode::Copy,
1487 8,
1488 OVERLAP_FIELDS,
1489 );
1490 assert_eq!(
1491 overlap.validate().expect_err("overlap should fail"),
1492 ComponentSchemaError::FieldOverlap {
1493 left: "left",
1494 right: "right"
1495 }
1496 );
1497
1498 let out_of_bounds = GeneratedComponentSchema::new(
1499 ComponentId::new(3),
1500 "oob",
1501 ComponentStorageKind::SparseBlob,
1502 ComponentSyncMode::Delta,
1503 ComponentMigrationMode::Copy,
1504 8,
1505 OOB_FIELDS,
1506 );
1507 assert_eq!(
1508 out_of_bounds
1509 .validate()
1510 .expect_err("out of bounds should fail"),
1511 ComponentSchemaError::FieldOutOfBounds {
1512 name: "wide",
1513 offset: 4,
1514 size: 8,
1515 max_bytes: 8
1516 }
1517 );
1518 }
1519}