v4k-git-backup/tools/labs/ecs.c

426 lines
14 KiB
C

// fast simple ecs
// - rlyeh, public domain
//
// features:
// - mostly heap allocation free
// - 2^64 systems max
// - 2^64 entities max
//
// cons:
// - 64 components max
// - memory requirements may be suboptimal. ie, using unions for the components: all components will equally size like the largest component.
//
// @note:
// - best perf when compiled with `/DNDEBUG /openmp /arch:AVX2 /Os /Ox /Gw /GL /MT`
// - runs 86M ops/sec on my old laptop: updates 50K entities out of 100K in 0.58ms/frame. 1000 frames in 0.57s
//
// @todo:
// - thread-safe
// - world/context management
// - dont use arrays: sequential access is killing performances when doing huge amount of entities (10M). worthy?
// compromise: use islands. 10K entities per island seems reasonable & best perf. the lower the better.
// we could parallelize islands as well (openmp? threads?).
// - file format spec:
// ; ecs data file format (.ini)
// [entity]
// components = mesh aabb
// position = 0 0 0 ; common
// rotation = 0 0 0 ; common
// scale = 2 2 2 ; common
// aabb.static = 1
// aabb.size = 3 3 3
// aabb.offset = 0 0 0
// mesh.model = cube
// mesh.texture = wood_03
// mesh.texture.tiling = 0.5 0.5
// - file format parser:
// for each_map_sorted_ptr(ini("ecs.ini"), char *, k, char *, v)
// printf("'%s'='%s'\n", *k, *v += strspn(*v, " "));
//
/* api */
#ifdef _OPENMP
# ifdef _MSC_VER
# define parallel __pragma(omp parallel for)
# define has_parallel 1
# else // __GNUC__
# define parallel _Pragma("omp parallel for") // C99
# define has_parallel 1
# endif
#else
# define parallel
# define has_parallel 0
#endif
void ecs_max_components(int max_components);
#define ecs_dump_world(fp, ...) ecs_dump_world(fp,ecs_mask64(__VA_ARGS__,-1))
#define ecs_add_entity(...) ecs_add_entity(ecs_mask64(__VA_ARGS__,-1))
union component_t* ecs_get_component( int eid, int cid );
union component_t** ecs_get_components( int eid );
#define ecs_has_component(eid,...) ecs_has_component(eid,ecs_mask64(__VA_ARGS__,-1))
#define ecs_add_component(eid,...) ecs_add_component(eid,ecs_mask64(__VA_ARGS__,-1))
#define ecs_del_component(eid,...) ecs_del_component(eid,ecs_mask64(__VA_ARGS__,-1))
#define ecs_use_component(eid,...) ecs_use_component(eid,ecs_mask64(__VA_ARGS__,-1))
#define ecs_off_component(eid,...) ecs_off_component(eid,ecs_mask64(__VA_ARGS__,-1))
#define each_ecs_component(obj, ...) \
( uint64_t sys_ = ecs_mask64(__VA_ARGS__,-1), ent_ = 0; ent_ < world.ne; ++ent_ ) \
for( component_t **obj = sys_ == (sys_ & world.entities[ent_ * 2 + 0]) ? ecs_get_components(ent_) : 0; obj ; obj = 0 )
#if has_parallel
#undef each_ecs_component
#define each_ecs_component(obj, ...) \
( ent_ = (sys_ = ecs_mask64(__VA_ARGS__,-1), 0); ent_ < world.ne; ++ent_ ) \
for( component_t **obj = sys_ == (sys_ & world.entities[ent_ * 2 + 0]) ? ecs_get_components(ent_) : 0; obj ; obj = 0 )
static int64_t ent_;
static uint64_t sys_;
#endif
/* internals */
typedef union component_t {
struct dummy { int dummy; };
#ifdef COMPONENT_HEADER
#include COMPONENT_HEADER
#endif
#ifdef COMPONENT_DATAS
COMPONENT_DATAS
#endif
#if defined ECS_DEMO || defined ECS_BENCH
struct position { float x,y,z; }; // c1
struct velocity { float vx,vy,vz; }; // c2
struct color { float r,g,b; }; // c3
struct health { float health; }; // c4
char *name; // c5
#endif
} component_t;
struct world_t {
int ne, nc; // ne: number of entities, nc: number of components (stride) (cN)
array(uint64_t) entities; // vtable entities (2 entries/entity) [ e1(cflags,offset) e2(cflags,offset) .. eN(cflags,offset) ]
array(component_t) components; // instanced components (nc entries/entity) [ e1(c1,c2..) e2(c1,c2..) .. ]
};
uint64_t ecs_mask64( unsigned id1, ... );
int (ecs_add_entity)( uint64_t component_mask );
bool (ecs_has_component)( int eid, uint64_t flags );
bool (ecs_add_component)( int eid, uint64_t flags );
bool (ecs_del_component)( int eid, uint64_t flags );
bool (ecs_use_component)( int eid, uint64_t flags );
bool (ecs_off_component)( int eid, uint64_t flags );
void (ecs_dump_world)( FILE *fp, uint64_t component_mask );
extern struct world_t world;
// impl
// static
struct world_t world = {0};
uint64_t ecs_mask64(unsigned id1, ... ) {
uint64_t flags = 0;
// update flags and entities of components
va_list ap;
va_start(ap, id1);
for( uint64_t id = id1; id != ((unsigned)-1); id = va_arg(ap, unsigned) ) {
flags |= 1ull << id;
}
va_end(ap);
return flags;
}
int (ecs_add_entity)( uint64_t component_mask ) {
component_t c = {0};
// add mask+offset into entities
array_push(world.entities, component_mask);
array_push(world.entities, array_count(world.components));
// add components into entity
for (uint64_t cid = 0; cid < world.nc; ++cid) {
if( (1ull << cid) & component_mask ) {
array_push( world.components, c );
}
}
int eid = world.ne;
return world.ne++;
}
void ecs_max_components(int max_comps) {
world.nc = max_comps;
}
component_t* ecs_get_component( int eid, int target_cid ) {
uint64_t sys = world.entities[ eid * 2 + 0 ];
uint64_t off = world.entities[ eid * 2 + 1 ];
for( uint64_t cid = 0; cid < target_cid; ++cid ) {
off += !!((1ull << cid) & sys);
}
bool has_cid = !!((1ull << target_cid) & sys);
return (component_t*)(has_cid * (uintptr_t)(&world.components[ off ]));
}
component_t** ecs_get_components( int eid ) {
static __thread component_t* local[8][64] = {0};
static __thread int counter = 0; counter = (counter + 1) % 8;
uint64_t sys = world.entities[ eid * 2 + 0 ];
uint64_t off = world.entities[ eid * 2 + 1 ];
for( uint64_t cid = 0, idx = 0; cid < world.nc; ++cid ) {
bool has_cid = !!((1ull << cid) & sys);
local[counter][ cid ] = (component_t*)(has_cid * (uintptr_t)(&world.components[ off ] + idx));
idx += has_cid;
}
return local[counter];
}
bool (ecs_has_component)( int eid, uint64_t flags ) {
uint64_t sys = world.entities[ eid * 2 + 0 ];
return flags == (sys & flags);
}
bool (ecs_add_component)( int eid, uint64_t flags ) {
uint64_t sys = world.entities[ eid * 2 + 0 ];
uint64_t off = world.entities[ eid * 2 + 1 ];
int eid2 = (ecs_add_entity)( sys | flags );
uint64_t sys2 = world.entities[ eid2 * 2 + 0 ];
uint64_t off2 = world.entities[ eid2 * 2 + 1 ];
for( uint64_t cid = 0, idx = 0, idx2 = 0; cid < world.nc; ++cid) {
if( (1ull << cid) & sys ) {
if( (1ull << cid) & sys2 ) {
memcpy( &world.components[ off2 ] + idx2, &world.components[ off ] + idx, sizeof(component_t));
}
}
if( (1ull << cid) & sys ) ++idx;
if( (1ull << cid) & sys2 ) ++idx2;
}
world.entities[ eid * 2 + 0 ] = sys2;
world.entities[ eid * 2 + 1 ] = off2;
world.entities[ eid2 * 2 + 0 ] = 0;
world.entities[ eid2 * 2 + 1 ] = 0;
return true;
}
bool (ecs_del_component)( int eid, uint64_t flags ) { // clr_component?
uint64_t sys = world.entities[ eid * 2 + 0 ];
uint64_t off = world.entities[ eid * 2 + 1 ];
for( uint64_t cid = 0, idx = 0; cid < world.nc; ++cid) {
if( (1ull << cid) & flags ) {
memset( &world.components[ off ] + idx, 0, sizeof(component_t));
}
if( (1ull << cid) & sys ) ++idx;
}
world.entities[ eid * 2 + 0 ] &= ~flags;
return true;
}
bool (ecs_use_component)( int eid, uint64_t flags ) {
world.entities[ eid * 2 + 0 ] |= flags;
return true;
}
bool (ecs_off_component)( int eid, uint64_t flags ) {
world.entities[ eid * 2 + 0 ] &= ~flags;
return true;
}
void (ecs_dump_world)( FILE *fp, uint64_t sys_mask ) {
if( sys_mask ) for( int eid = 0; eid < world.ne; ++eid ) {
uint64_t sys = world.entities[ eid * 2 + 0 ];
uint64_t off = world.entities[ eid * 2 + 1 ];
if( sys_mask != (sys & sys_mask)) continue;
fprintf(fp, "eid:%d sys:%#x ", eid, (unsigned)sys);
for( uint64_t cid = 0, idx = 0; cid < world.nc; ++cid ) {
if( (1ull << cid) & sys ) {
fprintf(fp, "cid:%d,%p+%d ", (int)cid, &world.components[ off ], (int)idx );
++idx;
}
}
fputc('\n', fp);
}
}
#ifdef ECS_BENCH
#include <stdio.h>
#include <assert.h>
int main(int argc, char **argv) {
int player;
/* entities to spawn */
#ifndef ECS_N
const int ECS_N = argc > 1 ? atoi(argv[1]) : 100000;
#endif
/* frames to benchmark */
#ifndef ECS_F
const int ECS_F = argc > 2 ? atoi(argv[2]) : 1000;
#endif
// declare components: c1, c2, ...
enum { POSITION, VELOCITY, COLOR, HEALTH, INPUT, NAME, TOTAL };
ecs_max_components(TOTAL);
// spawn entities
{
double start = time_ss();
player = ecs_add_entity(NAME, POSITION, VELOCITY, HEALTH, INPUT);
ecs_get_component(player, VELOCITY)->vx = 1;
ecs_get_component(player, VELOCITY)->vy = 2;
for (int i = 0; i < ECS_N; ++i) {
switch (i & 3) {
break; case 0:; /* static enemy */
int enemy0 = ecs_add_entity(NAME, POSITION, COLOR, HEALTH);
break; case 1:; /* dynamic enemy */
int enemy1 = ecs_add_entity(NAME, POSITION, COLOR, HEALTH, VELOCITY);
break; case 2:; /* static light */
int light0 = ecs_add_entity(NAME, POSITION, COLOR);
break; case 3:; /* dynamic light */
int light1 = ecs_add_entity(NAME, POSITION, COLOR, VELOCITY);
}
}
double end = time_ss();
double t = (end - start);
int T = 1 * ECS_N;
printf("%17s: %d frame(s) * %d num_entities = %d total ops, in %.3fs => %.3fM ops/s, %.2fms/frame\n",
"spawn benchmark", 1, ECS_N, T, t, (T / 1000000.0) / t, (t * 1000 / 1) );
}
// process & benchmark
{
double start = time_ss();
for( int frame = 0; frame < ECS_F; ++frame ) {
parallel
for each_ecs_component(obj, POSITION, VELOCITY) {
component_t *p = obj[POSITION];
component_t *v = obj[VELOCITY];
p->x += v->vx;
p->y += v->vy;
p->z += v->vz;
}
}
double end = time_ss();
double t = (end - start);
// stats
int num_iterated_entities = ECS_N;
int num_processed_entities = 0;
for each_ecs_component(obj, POSITION, VELOCITY) {
++num_processed_entities;
}
int T = ECS_F * num_processed_entities;
printf("%17s: %d frame(s) * %d num_entities = %d total ops, in %.3fs => %.3fM ops/s, %.2fms/frame\n",
"process benchmark", ECS_F, num_processed_entities, T, t, (T / 1000000.0) / t, (t * 1000 / ECS_F) );
}
{component_t *p = ecs_get_component(player, POSITION);
printf("eid:%d (position: %f,%f,%f)\n", player, p->x, p->y, p->z );}
assert( ecs_get_component(player, POSITION)->x == (ECS_F * 1));
assert( ecs_get_component(player, POSITION)->y == (ECS_F * 2));
assert( ~puts("Ok") );
}
#endif
#ifdef ECS_DEMO
#include <stdio.h>
#include <assert.h>
int main(int argc, char **argv) {
// declare components: c1, c2, ...
enum { POSITION, VELOCITY, COLOR, HEALTH, INPUT, TOTAL };
ecs_max_components(TOTAL);
// spawn entities
int player = ecs_add_entity(POSITION, VELOCITY, HEALTH, INPUT);
ecs_get_component(player, VELOCITY)->vx = 1;
ecs_get_component(player, VELOCITY)->vy = 2;
// some more
int enemy0, enemy1, light0, light1;
for (int i = 0; i < 10; ++i) {
if( 0 == (i&3)) /* static enemy */ enemy0 = ecs_add_entity(POSITION, COLOR, HEALTH);
if( 1 == (i&3)) /* dynamic enemy */ enemy1 = ecs_add_entity(POSITION, COLOR, HEALTH, VELOCITY);
if( 2 == (i&3)) /* static light */ light0 = ecs_add_entity(POSITION, COLOR);
if( 3 == (i&3)) /* dynamic light */ light1 = ecs_add_entity(POSITION, COLOR, VELOCITY), ecs_get_component(light1, VELOCITY)->vy = 2;
}
{
component_t *p = ecs_get_component(player, POSITION);
printf("eid:%d (position: %f,%f,%f)\n", player, p->x, p->y, p->z );
p = ecs_get_component(player, VELOCITY);
printf("eid:%d (velocity: %f,%f)\n", player, p->vx, p->vy );
p = ecs_get_component(light1, POSITION);
printf("eid:%d (position: %f,%f,%f)\n", light1, p->x, p->y, p->z );
p = ecs_get_component(light1, VELOCITY);
printf("eid:%d (velocity: %f,%f)\n", light1, p->vx, p->vy );
}
// simulate system processing
int frames = 1000;
for( int frame = 0; frame < frames; ++frame ) {
for each_ecs_component(obj, POSITION, VELOCITY) {
component_t *p = obj[POSITION];
component_t *v = obj[VELOCITY];
p->x += v->vx;
p->y += v->vy;
p->z += v->vz;
}
}
// verify (should display only player entity)
ecs_dump_world(stdout, INPUT);
{
component_t *p = ecs_get_component(player, POSITION);
printf("eid:%d (position: %f,%f,%f)\n", player, p->x, p->y, p->z );
p = ecs_get_component(light1, POSITION);
printf("eid:%d (position: %f,%f,%f)\n", light1, p->x, p->y, p->z );
}
assert( ecs_get_component(player, POSITION)->x == (frames * 1));
assert( ecs_get_component(player, POSITION)->y == (frames * 2));
assert( ecs_has_component(player, POSITION) );
assert(!ecs_has_component(player, COLOR) );
assert( ecs_add_component(player, COLOR) );
assert( ecs_has_component(player, COLOR) );
assert( ecs_off_component(player, POSITION) );
assert(!ecs_has_component(player, POSITION) );
assert( ecs_get_component(player, POSITION) == NULL);
assert( ecs_use_component(player, POSITION) );
assert( ecs_has_component(player, POSITION) );
assert( ecs_get_component(player, POSITION)->x == (frames * 1));
assert( ecs_del_component(player, POSITION) );
assert(!ecs_has_component(player, POSITION) );
assert( ecs_get_component(player, POSITION) == NULL );
assert( ~puts("Ok") );
}
#endif