Serialization : started to unify how to allocate entity whether its from MakeEntity or during deserialization.

This commit is contained in:
2026-09-08 23:24:48 -04:00
parent 3123d2f2e6
commit 615d36b09c
14 changed files with 307 additions and 194 deletions
+16 -10
View File
@@ -257,17 +257,19 @@ To prevent monolithic engine updates from forcing all gameplay assets to re-vers
Every serializable entity or component is described by an immutable `Class` instance:
```cpp
using serialize_fct_type = void (*)(Archive& ar, uint16 version, void* payload);
using serialize_fct_type = void (*)(Archive& ar, uint16 version, void* payload);
using default_init_fct_type = void (*)(void* payload);
struct Class
{
uint32 CRC;
uint8 kind;
uint16 version;
const Class* base_class;
serialize_fct_type serialize_fct;
size_t size_of;
size_t alignment;
uint32 CRC;
uint8 kind;
uint16 version;
const Class* base_class;
serialize_fct_type serialize_fct;
default_init_fct_type default_init_fct;
size_t size_of;
size_t alignment;
#if JULIET_DEBUG
String Name;
@@ -281,8 +283,10 @@ struct Class
static Class* kind;
#define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \
inline void default_init_##cls(void* payload) { *static_cast<cls*>(payload) = cls{}; } \
constexpr Class classKind##cls = \
MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \
MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), \
(serialize_fct), default_init_##cls); \
Class* cls::kind = const_cast<Class*>(&classKind##cls);
```
@@ -293,8 +297,10 @@ For derived entity types, `DECLARE_ENTITY()` and `DEFINE_ENTITY_VERSIONED` compo
DECLARE_CLASS()
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
inline void default_init_##entity(void* payload) { *static_cast<entity*>(payload) = entity{}; } \
constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \
&classKindEntity, sizeof(entity), alignof(entity), (serialize_fct)); \
&classKindEntity, sizeof(entity), alignof(entity), \
(serialize_fct), default_init_##entity); \
Class* entity::kind = const_cast<Class*>(&entityKind##entity);
```
@@ -192,11 +192,11 @@ The canonical allocation function is defined in `EntityManager.h`:
#### Postconditions:
- A new `Entity` record is appended to `manager.Entities`.
- A new typed block of `classPtr->size_of` bytes is allocated in `manager.by_type[classPtr->kind].arena`.
- The derived memory is zeroed.
- The derived memory is initialized with C++ struct defaults via `classPtr->default_init_fct` (or zeroed via `MemZero` if null).
- `base->derived` points to the derived struct.
- `derived->base` points to the base `Entity`.
- `base->derived_kind` is assigned to `class_ptr`.
- `base->ID` is assigned the next unique `EntityManager::ID`.
- `base->ID` is initialized to `0` (unassigned; populated by `MakeEntity` or deserialization).
- `base->is_dirty` is initialized to `true`.
- `typed_entity_array::count` is incremented.
- `typed_entity_array::array` is initialized if this is the first entity of this type.
@@ -212,9 +212,9 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
Assert(derivedClassPtr->size_of >= sizeof(entity_template));
Assert(derivedClassPtr->alignment > 0);
// 1. Allocate uninitialized Base Entity in the contiguous VectorArena
// 1. Allocate Base Entity in the contiguous VectorArena (ID is 0 until assigned by MakeEntity or deserialization)
Entity baseTemplate{};
baseTemplate.ID = EntityManager::ID++;
baseTemplate.ID = 0;
baseTemplate.derived_kind = derivedClassPtr;
baseTemplate.derived = nullptr;
baseTemplate.position = {};
@@ -224,7 +224,7 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
Entity* basePtr = manager.Entities.Back();
Assert(basePtr != nullptr);
// 2. Allocate zeroed derived component memory in the typed arena
// 2. Allocate derived component memory in the typed arena
typed_entity_array& typedArray = manager.by_type[derivedClassPtr->kind];
Assert(typedArray.arena != nullptr);
@@ -232,16 +232,26 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
typedArray.arena,
derivedClassPtr->size_of,
derivedClassPtr->alignment,
true JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind]));
false JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind]));
Assert(rawMemory != nullptr);
// 3. Initialize derived component defaults via Class reflection stub
if (derivedClassPtr->default_init_fct != nullptr)
{
derivedClassPtr->default_init_fct(rawMemory);
}
else
{
MemZero(rawMemory, derivedClassPtr->size_of);
}
auto* derivedTemplate = reinterpret_cast<entity_template*>(rawMemory);
// 3. Establish mutual back-pointers
basePtr->Derived = rawMemory;
derivedTemplate->Base = basePtr;
// 4. Establish mutual back-pointers
basePtr->derived = rawMemory;
derivedTemplate->base = basePtr;
// 4. Update typed array tracking
// 5. Update typed array tracking
if (typedArray.array == nullptr)
{
typedArray.array = derivedTemplate;
@@ -252,33 +262,49 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
}
```
### 3.3 Pure C-Style Zeroing & Initialization
`ArenaPushSize` is called with `shouldBeZeroed = true` (or followed by `MemZero`). This guarantees that:
- All bytes are zeroed (`0x00`).
- Pointers inside derived structs default to `nullptr`.
- Numerical fields default to `0`.
### 3.3 Default Struct Initialization via `Class::default_init_fct`
For structs that require specific non-zero sentinel values (such as `Inert::MeshInstance = indexMax`), Juliet follows a pure C approach without C++ constructor or placement-new machinery:
1. **Direct assignment or C-style init function**:
#### The Problem with Zero-Only Initialization
If allocation only zeroes memory (`MemZero` / `0x00`), any C++ member variables with non-zero defaults (such as `index_t MeshInstance = indexMax;` or `float Density = 1.0f;`) are populated with `0`. During deserialization, if a `.jasset` file lacks that property (e.g. an older file version or an optional field), `SERIALIZE` leaves the field untouched, meaning it incorrectly remains `0` rather than its intended default sentinel value!
#### The Solution: Compile-Time Default Stub in `DEFINE_ENTITY_VERSIONED`
Every entity descriptor `Class` includes a function pointer:
```cpp
inline void InitInert(Inert* inert)
using default_init_fct_type = void (*)(void* payload);
struct Class
{
Assert(inert != nullptr);
inert->MeshInstance = indexMax;
}
...
default_init_fct_type default_init_fct = nullptr;
};
```
2. **Or simple struct literal assignment**:
When registering an entity type with `DEFINE_ENTITY_VERSIONED`, the macro automatically defines a tiny type-safe stub that aggregate value-initializes the struct:
```cpp
*inert = Inert{ .MeshInstance = indexMax };
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
inline void default_init_##entity(void* payload) \
{ \
*static_cast<entity*>(payload) = entity{}; \
} \
constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \
&classKindEntity, sizeof(entity), alignof(entity), \
(serialize_fct), default_init_##entity); \
Class* entity::kind = const_cast<Class*>(&entityKind##entity);
```
No C++ placement `new`, `<new>` headers, or hidden constructor/destructor calls are ever used. Data structures remain pure C-style aggregates.
#### Architectural Advantages:
1. **Zero Boilerplate**: Developers write member initializers once in the struct definition (`index_t MeshInstance = indexMax;`).
2. **Type-Agnostic Core**: `AllocateEntity` does not need to know any C++ struct types; it unconditionally calls `derivedClassPtr->default_init_fct(rawMemory)`.
3. **Robust Deserialization**: In `deserialize_entity_in_place`, newly allocated entities already hold their canonical C++ defaults. Any properties absent in the `.jasset` file naturally retain their correct initial values.
4. **No Dynamic Heap / Placement-New**: `*static_cast<entity*>(payload) = entity{}` is pure aggregate value-assignment without `<new>` headers or exceptions.
### 3.4 Bidirectional Pointer Wiring
Notice the sequence:
1. `manager.Entities.PushBack(baseTemplate)` places the struct at its final, fixed arena address.
2. `basePtr = manager.Entities.Back()` retrieves the persistent memory pointer.
3. `derivedTemplate->Base = basePtr` wires the derived back-pointer directly to this permanent location.
4. `basePtr->Derived = rawMemory` wires the base forward-pointer to the arena-allocated derived struct.
2. `base_ptr = manager.Entities.Back()` retrieves the persistent memory pointer.
3. `derived_class_ptr->default_init_fct(raw_memory)` initializes canonical struct defaults.
4. `derived_template->base = base_ptr` wires the derived back-pointer directly to this permanent location.
5. `base_ptr->derived = raw_memory` wires the base forward-pointer to the arena-allocated derived struct.
No stack copying occurs. Neither pointer is ever left dangling.
@@ -287,30 +313,26 @@ No stack copying occurs. Neither pointer is ever left dangling.
- `typedArray.count`: The exact count of active entities of this type.
- `typedArray.array`: Pointer to the first element in the arena.
When the first entity of a given type is allocated, `typedArray.array` is set to `derivedTemplate`. Because the arena allocates sequentially, `typedArray.array[i]` can be indexed directly with stride `classPtr->size_of` as long as memory remains contiguous.
When the first entity of a given type is allocated, `typedArray.array` is set to `derived_template`. Because the arena allocates sequentially, `typedArray.array[i]` can be indexed directly with stride `class_ptr->size_of` as long as memory remains contiguous.
### 3.6 Refactoring `MakeEntity<EntityType>` Template
With `AllocateEntity` operational, `MakeEntity` in [`Game/Entity/Entity.h`](file:///w:/Classified/Juliet/Game/Entity/Entity.h#L95-L107) becomes clean, safe, and stack-free:
With `AllocateEntity` handling memory allocation, default member initialization, and mutual pointer wiring, `MakeEntity` in [`Game/Entity/Entity.h`](file:///w:/Classified/Juliet/Game/Entity/Entity.h) becomes clean, safe, and stack-free:
```cpp
template <typename EntityType>
requires EntityConcept<EntityType>
[[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z)
{
Entity* basePtr = AllocateEntity(manager, EntityType::Kind);
Assert(basePtr != nullptr);
Entity* base_ptr = AllocateEntity(manager, EntityType::kind);
Assert(base_ptr != nullptr);
basePtr->X = x;
basePtr->Y = y;
basePtr->Z = z;
base_ptr->ID = EntityManager::ID++;
base_ptr->position.x = x;
base_ptr->position.y = y;
base_ptr->position.z = z;
base_ptr->position.w = 1.0f;
auto* derivedPtr = static_cast<EntityType*>(basePtr->Derived);
ConstructDerivedDefaults<EntityType>(derivedPtr);
// Re-establish Base pointer after placement-new
derivedPtr->Base = basePtr;
return derivedPtr;
return static_cast<EntityType*>(base_ptr->derived);
}
```
@@ -429,9 +451,15 @@ To ensure fast and safe type lookup during file deserialization:
Entity* base_ptr = AllocateEntity(manager, class_ptr);
Assert(base_ptr != nullptr);
// 3. Serialize Base Entity and Derived in-place
// 3. Serialize Base Entity and Derived in-place (loads ID and properties from disk)
serialize(ar, NonNullPtr<Entity>(base_ptr));
// 4. Advance generator counter to avoid collisions with loaded IDs
if (base_ptr->ID >= EntityManager::ID)
{
EntityManager::ID = base_ptr->ID + 1;
}
// Freshly loaded entity matches disk state exactly
base_ptr->is_dirty = false;
@@ -708,9 +736,11 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
## 7. Step-by-Step Implementation Roadmap
### Phase 1: Data Structures & Header Definitions
1. **Update `Entity.h`:**
1. **Update `Class.h` & `Entity.h`:**
- Add `using default_init_fct_type = void (*)(void* payload);` and `default_init_fct` to `struct Class` and `MakeClass`.
- Update `DEFINE_ENTITY_VERSIONED` and `DEFINE_CLASS_VERSIONED` to define `default_init_##entity` and pass it to `MakeClass`.
- Add `bool is_dirty = false;` to `struct Entity`.
- Update `MakeEntity<EntityType>` to delegate to `AllocateEntity`.
- Update `MakeEntity<EntityType>` to assign `base_ptr->ID = EntityManager::ID++;` and delegate allocation and defaults cleanly to `AllocateEntity`.
2. **Update `EntityManager.h`:**
- Declare `[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* class_ptr);`.
- Declare `void DestroyEntity(EntityManager& manager, EntityID id);`.
@@ -722,8 +752,9 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
### Phase 2: Core Memory Allocation & Wiring in `EntityManager.cpp`
1. Implement `AllocateEntity`:
- Enforce parameter assertions.
- Push to `manager.Entities`.
- Allocate zeroed block in `manager.by_type[kind].arena`.
- Push to `manager.Entities` with `baseTemplate.ID = 0` (unassigned).
- Allocate block in `manager.by_type[kind].arena`.
- Call `derivedClassPtr->default_init_fct(rawMemory)` (or `MemZero` if null) to initialize struct defaults.
- Wire mutual pointers (`base->derived` and `derived->base`).
- Increment `typedArray.count` and initialize `typedArray.array`.
2. Implement `DestroyEntity` & `RemoveDerivedComponent`:
@@ -736,6 +767,7 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
2. In `World.cpp`:
- Implement `serialize_entity_asset(Archive& ar, NonNullPtr<Entity> entity, String filepath)`.
- Implement `deserialize_entity_asset(EntityManager& manager, Archive& ar, String filepath)`.
- In `deserialize_entity_in_place`, advance `EntityManager::ID` past `base_ptr->ID` to prevent ID collisions.
### Phase 4: World Save/Load Pipeline & Disk Deletion
1. In `World.cpp`:
@@ -803,6 +835,8 @@ namespace UnitTest
// 1. Allocate Inert Entity via AllocateEntity
Entity* base_entity = AllocateEntity(manager, Inert::kind);
Assert(base_entity != nullptr);
Assert(base_entity->ID == 0); // Pure memory allocator leaves ID unassigned (0) until MakeEntity or deserialization
base_entity->ID = EntityManager::ID++;
Assert(base_entity->ID > 0);
Assert(base_entity->derived_kind == Inert::kind);
Assert(base_entity->derived != nullptr);
@@ -865,6 +899,7 @@ namespace UnitTest
Assert(loaded_base != nullptr);
Assert(loaded_base->ID == original_id);
Assert(EntityManager::ID > original_id); // Counter was advanced past loaded ID to prevent collisions
Assert(loaded_base->position.x == 12.5f);
Assert(loaded_base->position.y == -44.0f);
Assert(loaded_base->position.z == 108.2f);