Serialization : started to unify how to allocate entity whether its from MakeEntity or during deserialization.
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user