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
+1 -1
View File
@@ -93,7 +93,7 @@ void Serialize(Archive& ar, World& /*world*/, String filename)
{ {
// Todo : utils // Todo : utils
// Get base entity from type // Get base entity from type
Entity* entity = reinterpret_cast<entity_template*>(rawElement + (idx * stride))->base; Entity* entity = reinterpret_cast<EntityTemplate*>(rawElement + (idx * stride))->base;
serialize(ar, entity); serialize(ar, entity);
} }
} }
+8 -16
View File
@@ -2,22 +2,6 @@
#include <Core/Common/serialization.h> #include <Core/Common/serialization.h>
namespace
{
void serialize_entity(Archive& ar, uint16 /*version*/, void* payload)
{
Assert(payload);
Entity* entity = static_cast<Entity*>(payload);
SERIALIZE(ar, id, entity->ID);
SERIALIZE(ar, position, entity->position);
}
} // namespace
DEFINE_CLASS_VERSIONED(Entity, 1, nullptr, serialize_entity)
DEFINE_ENTITY_VERSIONED(Inert, 1, nullptr)
void serialize(Archive& ar, NonNullPtr<Entity> entity) void serialize(Archive& ar, NonNullPtr<Entity> entity)
{ {
// Entity fields // Entity fields
@@ -29,3 +13,11 @@ void serialize(Archive& ar, NonNullPtr<Entity> entity)
serialize(ar, entity->derived_kind, entity->derived); serialize(ar, entity->derived_kind, entity->derived);
} }
} }
DEFINE_CLASS_VERSIONED(Entity, 1, nullptr)
internal void serialize(Archive& ar, uint16 /*version*/, Entity& entity)
{
SERIALIZE(ar, id, entity.ID);
SERIALIZE(ar, position, entity.position);
}
+13 -61
View File
@@ -1,24 +1,10 @@
#pragma once #pragma once
#include <Core/Common/CoreUtils.h> #include <Core/Common/CoreUtils.h>
#include <Core/Common/EnumUtils.h>
#include <Core/Math/Vector.h> #include <Core/Math/Vector.h>
#include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h>
#include <Engine/Class.h> #include <Engine/Class.h>
#include <Entity/entity_common.h>
#define DECLARE_ENTITY() \ #include <Entity/EntityManager.h>
Entity* base; \
DECLARE_CLASS()
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \
constexpr Class entityKind##entity = MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), \
&classKindEntity, sizeof(entity), alignof(entity), (serialize_fct)); \
Class* entity::kind = const_cast<Class*>(&entityKind##entity);
struct EntityManager;
using DerivedType = void*;
using EntityID = uint64_t;
struct Entity final struct Entity final
{ {
@@ -30,44 +16,8 @@ struct Entity final
Vector4 position = {}; Vector4 position = {};
}; };
struct Inert
{
DECLARE_ENTITY()
index_t MeshInstance = indexMax;
};
// clang-format off
#define ENTITY_TYPE_LIST(X) X(Inert),
#define AS_ENUM(name) name
enum class Entity_Type : uint8
{
ENTITY_TYPE_LIST(AS_ENUM)
Count
};
#undef AS_ENUM
#define AS_STR(name) #name
inline const char* kEntity_type_names[] = {
ENTITY_TYPE_LIST(AS_STR)
"Count"
};
#undef AS_STR
#define AS_CLASS(name) name::kind
inline Class* kEntity_type_class_ptr[]
{
ENTITY_TYPE_LIST(AS_CLASS)
nullptr
};
#undef AS_CLASS
#define ENTITY(kind) ToUnderlying(Entity_Type::kind)
// clang-format on
// Can reinterpret cast to this to have the offset of Base and Kind for any entity // Can reinterpret cast to this to have the offset of Base and Kind for any entity
struct entity_template struct EntityTemplate
{ {
DECLARE_ENTITY(); DECLARE_ENTITY();
}; };
@@ -91,15 +41,17 @@ template <typename EntityType>
requires EntityConcept<EntityType> requires EntityConcept<EntityType>
[[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z) [[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z)
{ {
EntityType result; Entity* base_ptr = allocate_entity(manager, EntityType::kind);
Entity base; Assert(base_ptr);
base.position.x = x;
base.position.y = y;
base.position.z = z;
base.position.w = 1.0f;
base.derived_kind = EntityType::kind;
return (EntityType*)RegisterEntity(manager, &base, &result); 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;
return (EntityType*)base_ptr->derived;
} }
template <typename EntityType> template <typename EntityType>
+41 -4
View File
@@ -3,6 +3,7 @@
#include <Core/Common/EnumUtils.h> #include <Core/Common/EnumUtils.h>
#include <Data/World.h> #include <Data/World.h>
#include <Entity/Entity.h> #include <Entity/Entity.h>
#include <Entity/entity_types.h>
#include <game.h> #include <game.h>
#include <Graphics/MeshRenderer.h> #include <Graphics/MeshRenderer.h>
@@ -43,16 +44,16 @@ EntityManager& GetEntityManager()
return *entityManager; return *entityManager;
} }
entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity) EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity)
{ {
base->ID = EntityManager::ID++; base->ID = EntityManager::ID++;
base->derived = entity; base->derived = entity;
manager.Entities.PushBack(*base); manager.Entities.PushBack(*base);
auto* ptr = (entity_template*)ArenaPushSize(manager.by_type[base->derived_kind->kind].arena, auto* ptr = (EntityTemplate*)ArenaPushSize(manager.by_type[base->derived_kind->kind].arena,
base->derived_kind->size_of, base->derived_kind->alignment, base->derived_kind->size_of, base->derived_kind->alignment,
false JULIET_DEBUG_PARAM(kEntity_type_names[base->derived_kind->kind])); false JULIET_DEBUG_PARAM(kEntity_type_names[base->derived_kind->kind]));
MemCopy(ptr, entity, base->derived_kind->size_of); MemCopy(ptr, entity, base->derived_kind->size_of);
manager.by_type[base->derived_kind->kind].count += 1; manager.by_type[base->derived_kind->kind].count += 1;
@@ -66,6 +67,42 @@ entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedTyp
return ptr; return ptr;
} }
Entity* allocate_entity(EntityManager& manager, NonNullPtr<Class> derived_type_class)
{
Assert(derived_type_class->kind < ENTITY(Count));
Assert(derived_type_class->size_of >= sizeof(EntityTemplate));
Assert(derived_type_class->alignment > 0);
Assert(derived_type_class->initialize_fct);
Entity base_template = {};
base_template.derived_kind = derived_type_class;
manager.Entities.PushBack(base_template);
Entity* base_ptr = manager.Entities.Back();
Assert(base_ptr);
typed_entity_array& typed_array = manager.by_type[derived_type_class->kind];
Assert(typed_array.arena);
void* derived = ArenaPushSize(typed_array.arena, derived_type_class->size_of, derived_type_class->alignment,
false JULIET_DEBUG_PARAM(kEntity_type_names[derived_type_class->kind]));
Assert(derived);
derived_type_class->initialize_fct(derived);
auto* derived_template = (EntityTemplate*)derived;
base_ptr->derived = derived;
derived_template->base = base_ptr;
if (typed_array.array == nullptr)
{
typed_array.array = derived_template;
}
typed_array.count += 1;
return base_ptr;
}
void UpdateEntityManager(EntityManager& manager) void UpdateEntityManager(EntityManager& manager)
{ {
// Todo : inert by definition dont move, but this is for test // Todo : inert by definition dont move, but this is for test
+13 -10
View File
@@ -1,16 +1,18 @@
#pragma once #pragma once
#include <Core/Container/Vector.h> #include <Core/Container/Vector.h>
#include <Entity/Entity.h> #include <Entity/entity_common.h>
struct Entity;
struct World; struct World;
struct EntityTemplate;
struct Entity;
struct Class;
struct typed_entity_array struct typed_entity_array
{ {
Arena* arena; Arena* arena;
entity_template* array; EntityTemplate* array;
size_t count; size_t count;
}; };
struct EntityManager struct EntityManager
@@ -23,8 +25,9 @@ struct EntityManager
typed_entity_array by_type[ENTITY(Count)]; typed_entity_array by_type[ENTITY(Count)];
}; };
void InitEntityManager(NonNullPtr<World> world); void InitEntityManager(NonNullPtr<World> world);
void ShutdownEntityManager(); void ShutdownEntityManager();
EntityManager& GetEntityManager(); EntityManager& GetEntityManager();
entity_template* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity); EntityTemplate* RegisterEntity(EntityManager& manager, Entity* base, DerivedType entity);
void UpdateEntityManager(EntityManager& manager); [[nodiscard]] Entity* allocate_entity(EntityManager& manager, NonNullPtr<Class> derived_type_class);
void UpdateEntityManager(EntityManager& manager);
+21
View File
@@ -0,0 +1,21 @@
#include <Entity/entity_common.h>
#include <Entity/Entity.h>
#include <Entity/entity_types.h>
// clang-format off
#define AS_STR(name) #name
const char* kEntity_type_names[] = {
ENTITY_TYPE_LIST(AS_STR)
"Count"
};
#undef AS_STR
#define AS_CLASS(name) name::kind
const Class* kEntity_type_class_ptr[]
{
ENTITY_TYPE_LIST(AS_CLASS)
nullptr
};
#undef AS_CLASS
// clang-format on
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <Core/Common/EnumUtils.h>
#include <Engine/Class.h>
struct Class;
using DerivedType = void*;
using EntityID = uint64_t;
// clang-format off
#define ENTITY_TYPE_LIST(X) X(Inert),
#define AS_ENUM(name) name
enum class Entity_Type : uint8
{
ENTITY_TYPE_LIST(AS_ENUM)
Count
};
#undef AS_ENUM
extern const char* kEntity_type_names[];
extern const Class* kEntity_type_class_ptr[];
#define ENTITY(kind) ToUnderlying(Entity_Type::kind)
// clang-format on
struct Entity;
#define DECLARE_ENTITY() \
Entity* base; \
DECLARE_CLASS()
#define DEFINE_ENTITY_VERSIONED(entity, version) \
constexpr Class entityKind##entity = \
MakeClass(ConstString(#entity), (uint8)Entity_Type::entity, (version), &class_kind_Entity, sizeof(entity), \
alignof(entity), (&initialize_thunk<entity>), (&serialize_thunk<entity>)); \
Class* entity::kind = const_cast<Class*>(&entityKind##entity);
// Note: Needed by every classes see define above
extern const Class class_kind_Entity;
+4
View File
@@ -0,0 +1,4 @@
#include <Entity/entity_types.h>
DEFINE_ENTITY_VERSIONED(Inert, 1)
internal void serialize(Archive& /*ar*/, uint16 /*version*/, Inert& /*inert*/) {}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <Entity/entity_common.h>
struct Inert
{
DECLARE_ENTITY()
index_t MeshInstance = indexMax;
};
+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: Every serializable entity or component is described by an immutable `Class` instance:
```cpp ```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 struct Class
{ {
uint32 CRC; uint32 CRC;
uint8 kind; uint8 kind;
uint16 version; uint16 version;
const Class* base_class; const Class* base_class;
serialize_fct_type serialize_fct; serialize_fct_type serialize_fct;
size_t size_of; default_init_fct_type default_init_fct;
size_t alignment; size_t size_of;
size_t alignment;
#if JULIET_DEBUG #if JULIET_DEBUG
String Name; String Name;
@@ -281,8 +283,10 @@ struct Class
static Class* kind; static Class* kind;
#define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \ #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 = \ 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); 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() DECLARE_CLASS()
#define DEFINE_ENTITY_VERSIONED(entity, version, serialize_fct) \ #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), \ 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); Class* entity::kind = const_cast<Class*>(&entityKind##entity);
``` ```
@@ -192,11 +192,11 @@ The canonical allocation function is defined in `EntityManager.h`:
#### Postconditions: #### Postconditions:
- A new `Entity` record is appended to `manager.Entities`. - 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`. - 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. - `base->derived` points to the derived struct.
- `derived->base` points to the base `Entity`. - `derived->base` points to the base `Entity`.
- `base->derived_kind` is assigned to `class_ptr`. - `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`. - `base->is_dirty` is initialized to `true`.
- `typed_entity_array::count` is incremented. - `typed_entity_array::count` is incremented.
- `typed_entity_array::array` is initialized if this is the first entity of this type. - `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->size_of >= sizeof(entity_template));
Assert(derivedClassPtr->alignment > 0); 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{}; Entity baseTemplate{};
baseTemplate.ID = EntityManager::ID++; baseTemplate.ID = 0;
baseTemplate.derived_kind = derivedClassPtr; baseTemplate.derived_kind = derivedClassPtr;
baseTemplate.derived = nullptr; baseTemplate.derived = nullptr;
baseTemplate.position = {}; baseTemplate.position = {};
@@ -224,7 +224,7 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
Entity* basePtr = manager.Entities.Back(); Entity* basePtr = manager.Entities.Back();
Assert(basePtr != nullptr); 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]; typed_entity_array& typedArray = manager.by_type[derivedClassPtr->kind];
Assert(typedArray.arena != nullptr); Assert(typedArray.arena != nullptr);
@@ -232,16 +232,26 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
typedArray.arena, typedArray.arena,
derivedClassPtr->size_of, derivedClassPtr->size_of,
derivedClassPtr->alignment, derivedClassPtr->alignment,
true JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind])); false JULIET_DEBUG_PARAM(kEntity_type_names[derivedClassPtr->kind]));
Assert(rawMemory != nullptr); 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); auto* derivedTemplate = reinterpret_cast<entity_template*>(rawMemory);
// 3. Establish mutual back-pointers // 4. Establish mutual back-pointers
basePtr->Derived = rawMemory; basePtr->derived = rawMemory;
derivedTemplate->Base = basePtr; derivedTemplate->base = basePtr;
// 4. Update typed array tracking // 5. Update typed array tracking
if (typedArray.array == nullptr) if (typedArray.array == nullptr)
{ {
typedArray.array = derivedTemplate; typedArray.array = derivedTemplate;
@@ -252,33 +262,49 @@ The implementation replaces the flawed `RegisterEntity` routine in [`Game/Entity
} }
``` ```
### 3.3 Pure C-Style Zeroing & Initialization ### 3.3 Default Struct Initialization via `Class::default_init_fct`
`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`.
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: #### The Problem with Zero-Only Initialization
1. **Direct assignment or C-style init function**: 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 ```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 ```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 ### 3.4 Bidirectional Pointer Wiring
Notice the sequence: Notice the sequence:
1. `manager.Entities.PushBack(baseTemplate)` places the struct at its final, fixed arena address. 1. `manager.Entities.PushBack(baseTemplate)` places the struct at its final, fixed arena address.
2. `basePtr = manager.Entities.Back()` retrieves the persistent memory pointer. 2. `base_ptr = manager.Entities.Back()` retrieves the persistent memory pointer.
3. `derivedTemplate->Base = basePtr` wires the derived back-pointer directly to this permanent location. 3. `derived_class_ptr->default_init_fct(raw_memory)` initializes canonical struct defaults.
4. `basePtr->Derived = rawMemory` wires the base forward-pointer to the arena-allocated derived struct. 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. 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.count`: The exact count of active entities of this type.
- `typedArray.array`: Pointer to the first element in the arena. - `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 ### 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 ```cpp
template <typename EntityType> template <typename EntityType>
requires EntityConcept<EntityType> requires EntityConcept<EntityType>
[[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z) [[nodiscard]] EntityType* MakeEntity(EntityManager& manager, float x, float y, float z)
{ {
Entity* basePtr = AllocateEntity(manager, EntityType::Kind); Entity* base_ptr = AllocateEntity(manager, EntityType::kind);
Assert(basePtr != nullptr); Assert(base_ptr != nullptr);
basePtr->X = x; base_ptr->ID = EntityManager::ID++;
basePtr->Y = y; base_ptr->position.x = x;
basePtr->Z = z; base_ptr->position.y = y;
base_ptr->position.z = z;
base_ptr->position.w = 1.0f;
auto* derivedPtr = static_cast<EntityType*>(basePtr->Derived); return static_cast<EntityType*>(base_ptr->derived);
ConstructDerivedDefaults<EntityType>(derivedPtr);
// Re-establish Base pointer after placement-new
derivedPtr->Base = basePtr;
return derivedPtr;
} }
``` ```
@@ -429,9 +451,15 @@ To ensure fast and safe type lookup during file deserialization:
Entity* base_ptr = AllocateEntity(manager, class_ptr); Entity* base_ptr = AllocateEntity(manager, class_ptr);
Assert(base_ptr != nullptr); 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)); 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 // Freshly loaded entity matches disk state exactly
base_ptr->is_dirty = false; base_ptr->is_dirty = false;
@@ -708,9 +736,11 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
## 7. Step-by-Step Implementation Roadmap ## 7. Step-by-Step Implementation Roadmap
### Phase 1: Data Structures & Header Definitions ### 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`. - 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`:** 2. **Update `EntityManager.h`:**
- Declare `[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* class_ptr);`. - Declare `[[nodiscard]] Entity* AllocateEntity(EntityManager& manager, Class* class_ptr);`.
- Declare `void DestroyEntity(EntityManager& manager, EntityID id);`. - 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` ### Phase 2: Core Memory Allocation & Wiring in `EntityManager.cpp`
1. Implement `AllocateEntity`: 1. Implement `AllocateEntity`:
- Enforce parameter assertions. - Enforce parameter assertions.
- Push to `manager.Entities`. - Push to `manager.Entities` with `baseTemplate.ID = 0` (unassigned).
- Allocate zeroed block in `manager.by_type[kind].arena`. - 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`). - Wire mutual pointers (`base->derived` and `derived->base`).
- Increment `typedArray.count` and initialize `typedArray.array`. - Increment `typedArray.count` and initialize `typedArray.array`.
2. Implement `DestroyEntity` & `RemoveDerivedComponent`: 2. Implement `DestroyEntity` & `RemoveDerivedComponent`:
@@ -736,6 +767,7 @@ if (ImGui::DragFloat3("Position", pos, 0.1f))
2. In `World.cpp`: 2. In `World.cpp`:
- Implement `serialize_entity_asset(Archive& ar, NonNullPtr<Entity> entity, String filepath)`. - Implement `serialize_entity_asset(Archive& ar, NonNullPtr<Entity> entity, String filepath)`.
- Implement `deserialize_entity_asset(EntityManager& manager, Archive& ar, 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 ### Phase 4: World Save/Load Pipeline & Disk Deletion
1. In `World.cpp`: 1. In `World.cpp`:
@@ -803,6 +835,8 @@ namespace UnitTest
// 1. Allocate Inert Entity via AllocateEntity // 1. Allocate Inert Entity via AllocateEntity
Entity* base_entity = AllocateEntity(manager, Inert::kind); Entity* base_entity = AllocateEntity(manager, Inert::kind);
Assert(base_entity != nullptr); 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->ID > 0);
Assert(base_entity->derived_kind == Inert::kind); Assert(base_entity->derived_kind == Inert::kind);
Assert(base_entity->derived != nullptr); Assert(base_entity->derived != nullptr);
@@ -865,6 +899,7 @@ namespace UnitTest
Assert(loaded_base != nullptr); Assert(loaded_base != nullptr);
Assert(loaded_base->ID == original_id); 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.x == 12.5f);
Assert(loaded_base->position.y == -44.0f); Assert(loaded_base->position.y == -44.0f);
Assert(loaded_base->position.z == 108.2f); Assert(loaded_base->position.z == 108.2f);
+1
View File
@@ -9,6 +9,7 @@
#include <Core/Memory/MemoryArena.h> #include <Core/Memory/MemoryArena.h>
#include <Data/World.h> #include <Data/World.h>
#include <Entity/Entity.h> #include <Entity/Entity.h>
#include <Entity/entity_types.h>
#include <Entity/EntityManager.h> #include <Entity/EntityManager.h>
#include <Graphics/Camera.h> #include <Graphics/Camera.h>
#include <Graphics/MeshRenderer.h> #include <Graphics/MeshRenderer.h>
+35 -15
View File
@@ -7,7 +7,25 @@
struct Archive; struct Archive;
using serialize_fct_type = void (*)(Archive&, uint16 version, void* payload); using serialize_fct_type = void(Archive&, uint16 version, void* payload);
using serialize_fct_ptr = serialize_fct_type*;
using initialize_fct_type = void(void* payload);
using initialize_fct_ptr = initialize_fct_type*;
template <typename Type>
void serialize_thunk(Archive& ar, uint16 version, void* payload)
{
Assert(payload);
serialize(ar, version, *((Type*)payload));
}
template <typename Type>
void initialize_thunk(void* payload)
{
auto& typed_val = *(Type*)payload;
typed_val = {};
}
struct Class struct Class
{ {
@@ -15,7 +33,8 @@ struct Class
uint8 kind; uint8 kind;
uint16 version; uint16 version;
const Class* base_class; const Class* base_class;
serialize_fct_type serialize_fct; initialize_fct_ptr initialize_fct;
serialize_fct_ptr serialize_fct;
size_t size_of; size_t size_of;
size_t alignment; size_t alignment;
@@ -26,22 +45,23 @@ struct Class
#define DECLARE_CLASS() static Class* kind; #define DECLARE_CLASS() static Class* kind;
#define DEFINE_CLASS_VERSIONED(cls, version, base_class, serialize_fct) \ #define DEFINE_CLASS_VERSIONED(cls, version, base_class) \
constexpr Class classKind##cls = \ constexpr Class class_kind_##cls = MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), \
MakeClass(ConstString(#cls), 0, (version), (base_class), sizeof(cls), alignof(cls), (serialize_fct)); \ alignof(cls), (&initialize_thunk<cls>), (&serialize_thunk<cls>)); \
Class* cls::kind = const_cast<Class*>(&classKind##cls); Class* cls::kind = const_cast<Class*>(&class_kind_##cls);
consteval Class MakeClass(String name, uint8 kind, uint16 version, const Class* base_class, size_t size, size_t align, consteval Class MakeClass(String name, uint8 kind, uint16 version, const Class* base_class, size_t size, size_t align,
serialize_fct_type fct) initialize_fct_ptr init_fct, serialize_fct_ptr serde_fct)
{ {
Class cls = {}; Class cls = {};
cls.CRC = crc32(name.Str, name.Size); cls.CRC = crc32(name.Str, name.Size);
cls.kind = kind; cls.kind = kind;
cls.version = version; cls.version = version;
cls.base_class = base_class; cls.base_class = base_class;
cls.size_of = size; cls.size_of = size;
cls.alignment = align; cls.alignment = align;
cls.serialize_fct = fct; cls.initialize_fct = init_fct;
cls.serialize_fct = serde_fct;
#if JULIET_DEBUG #if JULIET_DEBUG
cls.Name = name; cls.Name = name;
+23 -31
View File
@@ -63,20 +63,17 @@ namespace UnitTest
float pos_x = 10.0f; float pos_x = 10.0f;
}; };
internal void serialize_dummy_vehicle(Archive& ar, uint16 /*version*/, void* payload) DEFINE_CLASS_VERSIONED(DummyVehicle, 1, nullptr)
internal void serialize(Archive& ar, uint16 /*version*/, DummyVehicle& vehicle)
{ {
Assert(payload != nullptr); SERIALIZE(ar, max_speed, vehicle.max_speed);
auto* vehicle = static_cast<DummyVehicle*>(payload); SERIALIZE(ar, gear_count, vehicle.gear_count);
SERIALIZE(ar, chassis_uuid, vehicle.chassis_uuid);
SERIALIZE(ar, max_speed, vehicle->max_speed); SERIALIZE(ar, turbo, vehicle.turbo);
SERIALIZE(ar, gear_count, vehicle->gear_count); SERIALIZE(ar, pos_x, vehicle.pos_x);
SERIALIZE(ar, chassis_uuid, vehicle->chassis_uuid);
SERIALIZE(ar, turbo, vehicle->turbo);
SERIALIZE(ar, pos_x, vehicle->pos_x);
} }
DEFINE_CLASS_VERSIONED(DummyVehicle, 1, nullptr, serialize_dummy_vehicle)
internal void test_default_value_retention() internal void test_default_value_retention()
{ {
TempArena temp = scratch_begin(nullptr, 0); TempArena temp = scratch_begin(nullptr, 0);
@@ -116,14 +113,12 @@ namespace UnitTest
float base_damage = 25.0f; float base_damage = 25.0f;
}; };
internal void serialize_dummy_weapon_base(Archive& ar, uint16 /*version*/, void* payload) DEFINE_CLASS_VERSIONED(DummyWeaponBase, 1, nullptr)
{
Assert(payload != nullptr);
auto* base_weapon = static_cast<DummyWeaponBase*>(payload);
SERIALIZE(ar, base_damage, base_weapon->base_damage);
}
DEFINE_CLASS_VERSIONED(DummyWeaponBase, 1, nullptr, serialize_dummy_weapon_base) internal void serialize(Archive& ar, uint16 /*version*/, DummyWeaponBase& base_weapon)
{
SERIALIZE(ar, base_damage, base_weapon.base_damage);
}
// Derived test class for testing version migration (v1 -> v2) // Derived test class for testing version migration (v1 -> v2)
struct LegacyWeapon struct LegacyWeapon
@@ -134,11 +129,11 @@ namespace UnitTest
float velocity_y = 0.0f; float velocity_y = 0.0f;
}; };
internal void serialize_legacy_weapon(Archive& ar, uint16 version, void* payload) // Inherits from DummyWeaponBase (base_class != nullptr) to verify derived class_version handling
{ DEFINE_CLASS_VERSIONED(LegacyWeapon, 2, &class_kind_DummyWeaponBase)
Assert(payload != nullptr);
auto* weapon = static_cast<LegacyWeapon*>(payload);
internal void serialize(Archive& ar, uint16 version, LegacyWeapon& weapon)
{
if (ar.loading) if (ar.loading)
{ {
if (version < 2) if (version < 2)
@@ -147,26 +142,23 @@ namespace UnitTest
float deprecated_speed = 0.0f; float deprecated_speed = 0.0f;
if (SERIALIZE(ar, speed, deprecated_speed)) if (SERIALIZE(ar, speed, deprecated_speed))
{ {
weapon->velocity_x = deprecated_speed; weapon.velocity_x = deprecated_speed;
weapon->velocity_y = 0.0f; weapon.velocity_y = 0.0f;
} }
} }
else else
{ {
SERIALIZE(ar, velocity_x, weapon->velocity_x); SERIALIZE(ar, velocity_x, weapon.velocity_x);
SERIALIZE(ar, velocity_y, weapon->velocity_y); SERIALIZE(ar, velocity_y, weapon.velocity_y);
} }
} }
else else
{ {
SERIALIZE(ar, velocity_x, weapon->velocity_x); SERIALIZE(ar, velocity_x, weapon.velocity_x);
SERIALIZE(ar, velocity_y, weapon->velocity_y); SERIALIZE(ar, velocity_y, weapon.velocity_y);
} }
} }
// Inherits from DummyWeaponBase (base_class != nullptr) to verify derived class_version handling
DEFINE_CLASS_VERSIONED(LegacyWeapon, 2, &classKindDummyWeaponBase, serialize_legacy_weapon)
// Test 3: Deprecation migration from v1 to v2 via stack variable // Test 3: Deprecation migration from v1 to v2 via stack variable
internal void test_deprecation_migration() internal void test_deprecation_migration()
{ {