// // ecs.c // gl_project // // Created by Scott on 08/09/2022. // #include "ecs.h" #include #include #include #include #include typedef unsigned char BYTE; typedef struct ECSsystem { ecsSystemFn fn; ecsComponentQuery query; int maxThreads; int execOrder; } ECSsystem; /** * \brief Structure to represent a task the ECS needs to perform after systems finish running. * \note Not every member is used by type and thus some might be able to be left uninitialized. */ typedef struct ecsTask { enum ECS_TASKTYPE { ECS_ENTITY_DESTROY, //! Uses .entity ECS_COMPONENTS_DETACH, //! Uses .entity and .components.mask ECS_SYSTEM_CREATE, //! Uses .system and .components ECS_SYSTEM_DESTROY, //! Uses .system } type; ecsEntityId entity; //! relevant entity id ECSsystem system; //! relevant system function pointer ecsComponentQuery components; //! relevant components } ecsTask; typedef struct ECSentityData { ecsEntityId id; ecsComponentMask mask; } ECSentityData; typedef struct ECScomponentType { ecsComponentMask id; size_t stride; size_t componentSize; size_t size; void* begin; pthread_mutex_t mutex; } ECScomponentType; typedef struct ECScomponentList { size_t size; ECScomponentType* begin; pthread_mutex_t mutex; } ECScomponentList; typedef struct ECSentityList { size_t size; size_t nextValidId; ECSentityData* begin; pthread_mutex_t mutex; } ECSentityList; typedef struct ECSsystemList { size_t size; ECSsystem* begin; pthread_mutex_t mutex; } ECSsystemList; typedef struct ECStaskQueue { size_t size; ecsTask* begin; pthread_mutex_t mutex; } ECStaskQueue; pthread_mutexattr_t ecsSharedMutexAttr; // forward declare helper functions static inline int ecsResizeComponents(size_t size); static inline int ecsResizeComponentType(ECScomponentType* type, size_t size); static inline int ecsResizeEntities(size_t size); static inline int ecsResizeSystems(size_t size); static inline int ecsPushTaskStack(void); static inline void ecsClearTasks(void); static inline ECSentityData* ecsFindEntityData(ecsEntityId id); static inline ECScomponentType* ecsFindComponentType(ecsComponentMask id); static inline ECSsystem* ecsFindSystem(ecsSystemFn fn); static inline void* ecsFindComponentFor(ECScomponentType* type, ecsEntityId id); void ecsPushTask(ecsTask task); ECSentityList ecsEntities; ECScomponentList ecsComponents; ECSsystemList ecsSystems; ECStaskQueue ecsTasks; int ecsIsInit = 0; void ecsInit() { assert(!ecsIsInit); pthread_mutexattr_settype(&ecsSharedMutexAttr, PTHREAD_MUTEX_RECURSIVE); ecsEntities.nextValidId = 1; ecsEntities.begin = ecsComponents.begin = ecsSystems.begin = ecsTasks.begin = NULL; pthread_mutex_init(&ecsEntities.mutex, &ecsSharedMutexAttr); pthread_mutex_init(&ecsComponents.mutex, &ecsSharedMutexAttr); pthread_mutex_init(&ecsSystems.mutex, &ecsSharedMutexAttr); pthread_mutex_init(&ecsTasks.mutex, &ecsSharedMutexAttr); ecsEntities.size = ecsComponents.size = ecsSystems.size = ecsTasks.size = 0; ecsIsInit = 1; } void ecsTerminate() { assert(ecsIsInit); if(ecsEntities.begin) free(ecsEntities.begin); if(ecsSystems.begin) free(ecsSystems.begin); if(ecsTasks.begin) free(ecsTasks.begin); pthread_mutex_destroy(&ecsSystems.mutex); pthread_mutex_destroy(&ecsEntities.mutex); pthread_mutex_destroy(&ecsTasks.mutex); if(ecsComponents.begin) { ECScomponentType* type; for(size_t i = 0; i < ecsComponents.size; i++) { type = ecsComponents.begin + i; if(type->begin) free(type->begin); pthread_mutex_destroy(&type->mutex); } free(ecsComponents.begin); } ecsIsInit = 0; } ecsComponentMask ecsMakeComponentType(size_t stride) { // avoid going out of bounds on the bitmask if (ecsComponents.size == sizeof(ecsComponentMask) * 8) return nocomponent; ecsComponentMask mask = (0x1ll << ecsComponents.size); // calculate component mask pthread_mutex_lock(&ecsComponents.mutex); // add an element to end of array if(ecsResizeComponents(ecsComponents.size + 1)) { ECScomponentType ntype = (ECScomponentType) { // prepare specs of new component type .size = 0, .begin = NULL, .id = mask, .stride = (stride + sizeof(ecsEntityId)), .componentSize = stride }; // initialize mutex pthread_mutex_init(&ntype.mutex, &ecsSharedMutexAttr); // copy prepared component data memmove(ecsComponents.begin + ecsComponents.size-1, &ntype, sizeof(ntype)); } else { mask = nocomponent; } pthread_mutex_unlock(&ecsComponents.mutex); return mask; } // // COMPONENTS // void ecsSortComponents(ECScomponentType* type) { pthread_mutex_lock(&type->mutex); int swaps; void* a; void* b; ecsEntityId enta; ecsEntityId entb; void* temp = malloc(type->stride); // linear sort do { swaps = 0; for(size_t i = 1; i < type->size; ++i) { a = ((BYTE*)type->begin) + type->stride * (i-1); b = ((BYTE*)type->begin) + type->stride * i; enta = *(ecsEntityId*)a; entb = *(ecsEntityId*)b; if(enta > entb) { swaps++; memcpy(temp, b, type->stride); memcpy(b, a, type->stride); memcpy(a, temp, type->stride); } } } while(swaps > 0); pthread_mutex_unlock(&type->mutex); } void* ecsGetComponentPtr(ecsEntityId e, ecsComponentMask c) { ECScomponentType* ctype = ecsFindComponentType(c); ecsEntityId* ptr = ecsFindComponentFor(ctype, e); if(ptr == NULL) return NULL; // component for e, c combination does not exist return (void*)(ptr + 1); } void ecsAttachComponent(ecsEntityId e, ecsComponentMask c) { ECSentityData* entity = ecsFindEntityData(e); ECScomponentType* ctype = ecsFindComponentType(c); if(ctype == NULL) return; // component type does not exist if(entity == NULL) return; // no such entity if(((entity->mask) & c) != 0) return; // component already exists pthread_mutex_lock(&ctype->mutex); if(ecsResizeComponentType(ctype, ctype->size + 1)) { BYTE* eid = ((BYTE*)ctype->begin) + ((ctype->size-1) * ctype->stride); // get last item of the list as its entityId block memset(eid, 0x0, ctype->stride); // zero new component memcpy(eid, &e, sizeof(ecsEntityId)); // set entityId block entity->mask |= c; // register that component was added to entity ecsSortComponents(ctype); } pthread_mutex_unlock(&ctype->mutex); } void ecsAttachComponents(ecsEntityId e, ecsComponentMask q) { ecsComponentMask c; // single component mask for(size_t i = 0; i < ecsComponents.size; i++) { c = (0x1ll << i); if((q & c) != 0) // query contains this mask ecsAttachComponent(e, c); } } void ecsDetachComponent(ecsEntityId e, ecsComponentMask c) { ECScomponentType* ctype = ecsFindComponentType(c); if(ctype == NULL) return; // no such component type ECSentityData* entity = ecsFindEntityData(e); if(entity == NULL) return; // no such entity if((entity->mask & c) == 0) return; // entity does not have component void* block = ecsFindComponentFor(ctype, e); if(block == NULL) return; // no component block for entity found pthread_mutex_lock(&ctype->mutex); uintptr_t lenafter = (uintptr_t)(((BYTE*)ctype->begin + ctype->size * ctype->stride) - (BYTE*)block); lenafter -= ctype->stride; // move last element into to-be-destroyed element memmove(block, block + ctype->stride, lenafter); // shorten array by one stride ecsResizeComponentType(ctype, (ctype->size)-1); entity->mask &= ~c; pthread_mutex_unlock(&ctype->mutex); } void ecsDetachComponents(ecsEntityId e, ecsComponentMask c) { ecsPushTask((ecsTask){.type=ECS_COMPONENTS_DETACH, .components={ .mask=c }, .entity=e}); } void ecsTaskDetachComponents(ecsEntityId e, ecsComponentMask q) { ecsComponentMask id; for(size_t i = 0; i < ecsComponents.size; i++) { id = (0x1ll << i); if((q & id) != 0) // query contains component type at i ecsDetachComponent(e, id); } } // // ENTITIES // ecsEntityId ecsCreateEntity(ecsComponentMask components) { // register an id that is unique for the runtime of the ecs ecsEntityId id = ecsEntities.nextValidId; ecsEntities.nextValidId++; // prepare values ECSentityData entity = (ECSentityData) { .mask = 0x0, .id = id }; pthread_mutex_lock(&ecsEntities.mutex); // resize entities list if(ecsResizeEntities(ecsEntities.size + 1)) { // copy prepared values memmove((ecsEntities.begin + ecsEntities.size - 1), &entity, sizeof(entity)); // attach requested components ecsAttachComponents(id, components); } else { id = noentity; } pthread_mutex_unlock(&ecsEntities.mutex); return id; } ecsEntityId ecsGetComponentMask(ecsEntityId entity) { ECSentityData* data = ecsFindEntityData(entity); return data != NULL ? data->mask : nocomponent; } int ecsValidEntity(ecsEntityId entity) { return ecsFindEntityData(entity) != NULL; } void ecsDestroyEntity(ecsEntityId e) { ecsPushTask((ecsTask){.type=ECS_ENTITY_DESTROY, .entity=e}); } void ecsTaskDestroyEntity(ecsEntityId e) { ECSentityData* data = ecsFindEntityData(e); if(data == NULL) return; // no such entity // destroy all components owned by entity ecsTaskDetachComponents(e, data->mask); // get the last element of the entities array uintptr_t countAfter = (uintptr_t)((ecsEntities.begin + ecsEntities.size) - data); assert(countAfter < ecsEntities.size); // copy last into to-be-deleted entity memmove(data, data+1, sizeof(ECSentityData) * countAfter); // resize ecsResizeEntities(ecsEntities.size - 1); } // // SYSTEMS // int matchQuery(ecsComponentQuery query, ecsComponentMask mask) { if(query.comparison == ECS_QUERY_ANY) return (mask & query.mask) != 0; else if(query.comparison == ECS_QUERY_ALL) return (mask & query.mask) == query.mask; return 0; } typedef struct ecsRunSystemArgs { ecsSystemFn fn; ecsEntityId* entities; ecsComponentMask* components; size_t count; float deltaTime; } ecsRunSystemArgs; void* ecsRunSystem(void* args) { ecsRunSystemArgs* arg = args; arg->fn(arg->entities, arg->components, arg->count, arg->deltaTime); return NULL; } void ecsRunSystems(float deltaTime) { ECSsystem system; ECSentityData entity; size_t entityCount = ecsEntities.size; pthread_t* threads = NULL; ecsRunSystemArgs* threadArgs = NULL; for(size_t i = 0; i < ecsSystems.size; ++i) { system = ecsSystems.begin[i]; // ECS_NOQUERY systems get run exactly once per ecsRunSystems call // with entity and components arguments on NULL // and count argument on 0 if(system.query.comparison == ECS_NOQUERY) { system.fn(NULL, NULL, 0, deltaTime); } else { // look for all entities matching the query // first allocate entity and component lists the size of the number of entities ecsEntityId* entityList = malloc((entityCount + 1) * sizeof(ecsEntityId)); ecsComponentMask* componentList = malloc((entityCount + 1) * sizeof(ecsComponentMask)); assert(entityList != NULL); assert(componentList != NULL); // then search for entities that match the query size_t total = 0; for(size_t j = 0; j < entityCount; ++j) { entity = ecsEntities.begin[j]; if (matchQuery(system.query, entity.mask)) { assert(total < entityCount); entityList[total] = entity.id; componentList[total] = entity.mask; total++; } } size_t threadCount = system.maxThreads; if(threadCount > 0) threadCount = threadCount > total ? total : threadCount; else threadCount = 1; // dont use threads if(threadCount <= 1) { system.fn(entityList, componentList, total, deltaTime); } // use threads else { // avoid creating more threads than there are matching entities threads = realloc(threads, threadCount * sizeof(pthread_t)); threadArgs = realloc(threadArgs, threadCount * sizeof(ecsRunSystemArgs)); // for each thread, create a runsystemargs instance describing it's area of influence // then create the thread size_t perThreadCount = total - (total % (threadCount-1)); perThreadCount = perThreadCount / (threadCount-1); size_t remainder = total % (threadCount-1); for(int j = 0; j < threadCount; ++j) { threadArgs[j].fn = system.fn; threadArgs[j].entities = entityList + perThreadCount * j; threadArgs[j].components = componentList + perThreadCount * j; threadArgs[j].count = (j == threadCount-1) ? remainder : perThreadCount; threadArgs[j].deltaTime = deltaTime; pthread_create(threads + j, NULL, &ecsRunSystem, threadArgs + j); } // wait for completion of all threads for(int j = 0; j < threadCount; ++j) { pthread_join(threads[j], NULL); } } // clean up free(entityList); free(componentList); } } if(threads != NULL) free(threads); if(threadArgs != NULL) free(threadArgs); ecsRunTasks(); } void ecsSortSystems() { int swaps; ECSsystem tmp; do { swaps = 0; for(int i = 1; i < ecsSystems.size; ++i) { if(ecsSystems.begin[i-1].execOrder > ecsSystems.begin[i].execOrder) { memcpy(&tmp, &ecsSystems.begin[i-1], sizeof(ECSsystem)); memcpy(&ecsSystems.begin[i-1], &ecsSystems.begin[i], sizeof(ECSsystem)); memcpy(&ecsSystems.begin[i], &tmp, sizeof(ECSsystem)); swaps++; } } } while(swaps > 0); } void ecsEnableSystem(ecsSystemFn fn, ecsComponentMask query, ecsQueryComparison comp, int maxThreads, int execOrder) { ecsPushTask((ecsTask) { .type=ECS_SYSTEM_CREATE, .system=(ECSsystem) { .fn = fn, .maxThreads = maxThreads, .execOrder = execOrder, .query=(ecsComponentQuery) { .mask=query, .comparison=comp } } }); } void ecsTaskEnableSystem(ECSsystem system) { if(ecsResizeSystems(ecsSystems.size + 1)) { ECSsystem* last = (ecsSystems.begin + ecsSystems.size - 1); memcpy(last, &system, sizeof(ECSsystem)); ecsSortSystems(); } } void ecsDisableSystem(ecsSystemFn fn) { ecsPushTask((ecsTask){ .type=ECS_SYSTEM_DESTROY, .system=fn }); } void ecsTaskDisableSystem(ecsSystemFn fn) { // calculate distance between end and to replace ECSsystem* to_replace = ecsFindSystem(fn); ECSsystem* end = ecsSystems.begin + ecsSystems.size; size_t dist = (end - to_replace) - 1; // shift everything after to-be-deleted item back by one (overwriting to-be-deleted) memmove(to_replace, to_replace + 1, dist * sizeof(ECSsystem)); // resize array ecsResizeSystems(ecsSystems.size - 1); } // // TASKS // void ecsPushTask(ecsTask task) { if(ecsPushTaskStack()) { ecsTask* last = ecsTasks.begin + ecsTasks.size - 1; memmove(last, &task, sizeof(ecsTask)); } } static inline void ecsRunTask(ecsTask task) { switch(task.type) { default: return; case ECS_ENTITY_DESTROY: ecsTaskDestroyEntity(task.entity); return; case ECS_COMPONENTS_DETACH: ecsTaskDetachComponents(task.entity, task.components.mask); return; case ECS_SYSTEM_CREATE: ecsTaskEnableSystem(task.system); return; case ECS_SYSTEM_DESTROY: ecsTaskDisableSystem(task.system.fn); return; } } void ecsRunTasks() { for(size_t i = 0; i < ecsTasks.size; i++) ecsRunTask(ecsTasks.begin[i]); ecsClearTasks(); } // // FIND HELPERS // static inline ECScomponentType* ecsFindComponentType(ecsComponentMask id) { for(size_t i = 0; i < ecsComponents.size; ++i) { if(ecsComponents.begin[i].id == id) return (ecsComponents.begin + i); } return NULL; } void* ecsFindComponentFor(ECScomponentType* type, ecsEntityId id) { if(type->size == 0) return NULL; BYTE* sptr; ecsEntityId* eptr; int l = 0; int r = type->size - 1; int m; while(l <= r) { m = round((double)(l+r)/2.f); sptr = ((BYTE*)type->begin) + m * type->stride; // median element eptr = sptr; // found the correct component if(*eptr == id) return sptr; // go up else if(*eptr < id) l = m + 1; // go down else if(*eptr > id) r = m - 1; } return NULL; } static inline ECSentityData* ecsFindEntityData(ecsEntityId id) { for(size_t i = 0; i < ecsEntities.size; ++i) { if(ecsEntities.begin[i].id == id) return (ecsEntities.begin + i); } return NULL; } static inline ECSsystem* ecsFindSystem(ecsSystemFn fn) { for(size_t i = 0; i < ecsSystems.size; ++i) { if(ecsSystems.begin[i].fn == fn) return (ecsSystems.begin + i); } return NULL; } // // RESIZE HELPERS // static inline int ecsResizeSystems(size_t size) { if(size == 0) { free(ecsSystems.begin); ecsSystems.begin = NULL; ecsSystems.size = 0; } else { ECSsystem* nptr = realloc(ecsSystems.begin, size * sizeof(ECSsystem)); if(nptr == NULL) return 0; ecsSystems.size = size; ecsSystems.begin = nptr; } return 1; } static inline int ecsPushTaskStack() { size_t size = ecsTasks.size + 1; void* nptr = realloc(ecsTasks.begin, size * sizeof(ecsTask)); if(nptr == NULL) return 0; ecsTasks.size = size; ecsTasks.begin = nptr; return 1; } static inline void ecsClearTasks() { if(ecsTasks.begin == NULL || ecsTasks.size == 0) return; // no tasks ecsTasks.size = 0; free(ecsTasks.begin); ecsTasks.begin = NULL; } static inline int ecsResizeEntities(size_t size) { if(size == 0) { free(ecsEntities.begin); ecsEntities.begin = NULL; ecsEntities.size = 0; } else { ECSentityData* nptr = realloc(ecsEntities.begin, size * sizeof(ECSentityData)); if(nptr == NULL) return 0; ecsEntities.size = size; ecsEntities.begin = nptr; } return 1; } static inline int ecsResizeComponentType(ECScomponentType* type, size_t size) { if(size == 0) { if (type->begin == NULL) return 1; free(type->begin); type->begin = NULL; type->size = 0; } else { void* nptr = realloc(type->begin, size * (type->stride)); assert(nptr != NULL); if(nptr == NULL) return 0; type->size = size; type->begin = nptr; } return 1; } static inline int ecsResizeComponents(size_t size) { if(size == 0) { free(ecsComponents.begin); ecsComponents.size = 0; ecsComponents.begin = NULL; } else { ECScomponentType* nptr = realloc(ecsComponents.begin, size * sizeof(ECScomponentType)); if(nptr == NULL) return 0; ecsComponents.begin = nptr; ecsComponents.size = size; } return 1; }