diff --git a/.gitignore b/.gitignore index be3171cae7..a88c6f5e13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,12 @@ -.DS_Store bin -.bake_cache -.bake -.vscode gcov -.idea *.pdb deps cmake-build-debug bazel-* **/CMakeFiles/* -.vs out docs/html MODULE.bazel.lock +.* +.*/* \ No newline at end of file diff --git a/distr/flecs.c b/distr/flecs.c index 95495a79c2..2a2e995a8c 100644 --- a/distr/flecs.c +++ b/distr/flecs.c @@ -23711,13 +23711,14 @@ int32_t ecs_delete_empty_tables( ecs_os_perf_trace_push("flecs.delete_empty_tables"); ecs_time_t start = {0}, cur = {0}; - int32_t delete_count = 0; bool time_budget = false; int32_t measure_budget_after = 100; + int32_t result = 0; uint16_t clear_generation = desc->clear_generation; uint16_t delete_generation = desc->delete_generation; double time_budget_seconds = desc->time_budget_seconds; + int32_t offset = desc->offset; if (ECS_NEQZERO(time_budget_seconds) || (ecs_should_log_1() && ecs_os_has_time())) { ecs_time_measure(&start); @@ -23727,13 +23728,34 @@ int32_t ecs_delete_empty_tables( time_budget = true; } - int32_t i, count = flecs_sparse_count(&world->store.tables); + int32_t count = flecs_sparse_count(&world->store.tables); + if (!count) { + goto done; + } + + if (offset >= count || offset < 0) { + offset = 0; + } + + int32_t remaining = count; + int32_t i = offset; + + while (remaining > 0) { + count = flecs_sparse_count(&world->store.tables); + if (!count) { + break; + } + + if (i >= count) { + i = 0; + } - for (i = count - 1; i >= 0; i --) { ecs_table_t *table = flecs_sparse_get_dense_t(&world->store.tables, ecs_table_t, i); if (table->keep) { + i ++; + remaining --; continue; } @@ -23742,6 +23764,11 @@ int32_t ecs_delete_empty_tables( if (time_budget && !measure_budget_after) { cur = start; if (ecs_time_measure(&cur) > time_budget_seconds) { + count = flecs_sparse_count(&world->store.tables); + result = i + 1; + if (result >= count) { + result = 0; + } goto done; } @@ -23749,24 +23776,30 @@ int32_t ecs_delete_empty_tables( } if (!table->id || ecs_table_count(table) != 0) { + i ++; + remaining --; continue; } uint16_t gen = ++ table->_->generation; if (delete_generation && (gen > delete_generation)) { flecs_table_fini(world, table); - delete_count ++; measure_budget_after = 1; + remaining --; + continue; } else if (clear_generation && (gen > clear_generation)) { flecs_table_shrink(world, table); measure_budget_after = 1; } + + i ++; + remaining --; } done: ecs_os_perf_trace_pop("flecs.delete_empty_tables"); - return delete_count; + return result; } ecs_entities_t ecs_get_entities( diff --git a/distr/flecs.h b/distr/flecs.h index bda043f817..6b3cd857a6 100644 --- a/distr/flecs.h +++ b/distr/flecs.h @@ -7741,6 +7741,10 @@ typedef struct ecs_delete_empty_tables_desc_t { /** Amount of time operation is allowed to spend. */ double time_budget_seconds; + + /** Table index to start scanning at. The function loops around until it + * reaches this offset again, or until the time budget is exceeded. */ + int32_t offset; } ecs_delete_empty_tables_desc_t; /** Clean up empty tables. @@ -7768,9 +7772,15 @@ typedef struct ecs_delete_empty_tables_desc_t { * * The time budget specifies how long the operation should take at most. * + * The offset parameter specifies the table index at which to start scanning. + * The function loops around until it reaches this offset again, or until the + * time budget is exceeded. + * * @param world The world. * @param desc Configuration parameters. - * @return The number of deleted tables. + * @return The index + 1 of the table where the function stopped, or 0 if the + * function scanned all tables. The return value can be used as the + * offset for the next call. */ FLECS_API int32_t ecs_delete_empty_tables( diff --git a/include/flecs.h b/include/flecs.h index d62ded1eef..b830d0f7ec 100644 --- a/include/flecs.h +++ b/include/flecs.h @@ -2736,6 +2736,10 @@ typedef struct ecs_delete_empty_tables_desc_t { /** Amount of time operation is allowed to spend. */ double time_budget_seconds; + + /** Table index to start scanning at. The function loops around until it + * reaches this offset again, or until the time budget is exceeded. */ + int32_t offset; } ecs_delete_empty_tables_desc_t; /** Clean up empty tables. @@ -2763,9 +2767,15 @@ typedef struct ecs_delete_empty_tables_desc_t { * * The time budget specifies how long the operation should take at most. * + * The offset parameter specifies the table index at which to start scanning. + * The function loops around until it reaches this offset again, or until the + * time budget is exceeded. + * * @param world The world. * @param desc Configuration parameters. - * @return The number of deleted tables. + * @return The index + 1 of the table where the function stopped, or 0 if the + * function scanned all tables. The return value can be used as the + * offset for the next call. */ FLECS_API int32_t ecs_delete_empty_tables( diff --git a/src/world.c b/src/world.c index 254fc88c3a..3db4f4a7e7 100644 --- a/src/world.c +++ b/src/world.c @@ -1696,13 +1696,14 @@ int32_t ecs_delete_empty_tables( ecs_os_perf_trace_push("flecs.delete_empty_tables"); ecs_time_t start = {0}, cur = {0}; - int32_t delete_count = 0; bool time_budget = false; int32_t measure_budget_after = 100; + int32_t result = 0; uint16_t clear_generation = desc->clear_generation; uint16_t delete_generation = desc->delete_generation; double time_budget_seconds = desc->time_budget_seconds; + int32_t offset = desc->offset; if (ECS_NEQZERO(time_budget_seconds) || (ecs_should_log_1() && ecs_os_has_time())) { ecs_time_measure(&start); @@ -1712,13 +1713,34 @@ int32_t ecs_delete_empty_tables( time_budget = true; } - int32_t i, count = flecs_sparse_count(&world->store.tables); + int32_t count = flecs_sparse_count(&world->store.tables); + if (!count) { + goto done; + } + + if (offset >= count || offset < 0) { + offset = 0; + } + + int32_t remaining = count; + int32_t i = offset; + + while (remaining > 0) { + count = flecs_sparse_count(&world->store.tables); + if (!count) { + break; + } + + if (i >= count) { + i = 0; + } - for (i = count - 1; i >= 0; i --) { ecs_table_t *table = flecs_sparse_get_dense_t(&world->store.tables, ecs_table_t, i); if (table->keep) { + i ++; + remaining --; continue; } @@ -1727,6 +1749,11 @@ int32_t ecs_delete_empty_tables( if (time_budget && !measure_budget_after) { cur = start; if (ecs_time_measure(&cur) > time_budget_seconds) { + count = flecs_sparse_count(&world->store.tables); + result = i + 1; + if (result >= count) { + result = 0; + } goto done; } @@ -1734,24 +1761,30 @@ int32_t ecs_delete_empty_tables( } if (!table->id || ecs_table_count(table) != 0) { + i ++; + remaining --; continue; } uint16_t gen = ++ table->_->generation; if (delete_generation && (gen > delete_generation)) { flecs_table_fini(world, table); - delete_count ++; measure_budget_after = 1; + remaining --; + continue; } else if (clear_generation && (gen > clear_generation)) { flecs_table_shrink(world, table); measure_budget_after = 1; } + + i ++; + remaining --; } done: ecs_os_perf_trace_pop("flecs.delete_empty_tables"); - return delete_count; + return result; } ecs_entities_t ecs_get_entities( diff --git a/test/core/project.json b/test/core/project.json index 71aa8f7a05..6893163d5b 100644 --- a/test/core/project.json +++ b/test/core/project.json @@ -2802,7 +2802,11 @@ "add_dont_fragment_after_pair_query", "add_can_toggle_after_pair_query", "add_traversable_after_pair_query", - "set_component_after_in_use" + "set_component_after_in_use", + "delete_empty_tables_w_offset", + "delete_empty_tables_w_offset_out_of_range", + "delete_empty_tables_w_offset_wrap_around", + "delete_empty_tables_return_value" ] }, { "id": "ExclusiveAccess", diff --git a/test/core/src/World.c b/test/core/src/World.c index ca3658e84a..864f3ee963 100644 --- a/test/core/src/World.c +++ b/test/core/src/World.c @@ -1154,15 +1154,12 @@ void World_delete_empty_tables_after_mini(void) { empty_table_count --; // correct for root table - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1}); /* Increase to 1 */ - test_int(deleted, 0); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1}); /* Delete */ - test_assert(deleted == empty_table_count); - test_int(info->table_count + deleted, old_table_count); + test_int(info->table_count, old_table_count - empty_table_count); ecs_fini(world); } @@ -1170,6 +1167,8 @@ void World_delete_empty_tables_after_mini(void) { void World_delete_empty_tables_after_init(void) { ecs_world_t *world = ecs_init(); + const ecs_world_info_t *info = ecs_get_world_info(world); + ecs_query_t *q = ecs_query(world, { .terms = {{ .id = EcsAny }}, .flags = EcsQueryMatchEmptyTables @@ -1187,15 +1186,14 @@ void World_delete_empty_tables_after_init(void) { ecs_query_fini(q); empty_table_count --; // correct for root table + int32_t old_table_count = info->table_count; - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); /* Increase to 1 */ - test_int(deleted, 0); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); /* Delete */ - test_assert(deleted == empty_table_count); + test_int(info->table_count, old_table_count - empty_table_count); ecs_fini(world); } @@ -1838,15 +1836,11 @@ void World_delete_1000_empty_tables(void) { ecs_delete(world, e); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); /* Increase to 1 */ - test_int(deleted, 0); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); /* Delete */ - test_assert(deleted != 0); - test_assert(deleted >= 1000); test_assert(info->table_count <= old_table_count); @@ -1864,13 +1858,10 @@ void World_use_after_delete_empty(void) { ecs_add(world, e, TagB); ecs_remove(world, e, TagA); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); - test_assert(deleted == 0); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); - test_assert(deleted != 0); ecs_add(world, e, TagA); test_assert( ecs_has(world, e, TagA)); @@ -1890,13 +1881,10 @@ void World_use_after_clear_empty(void) { ecs_add(world, e, TagB); ecs_remove(world, e, TagA); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); ecs_add(world, e, TagA); test_assert( ecs_has(world, e, TagA)); @@ -1921,21 +1909,18 @@ void World_use_after_delete_empty_w_component(void) { test_assert( ecs_has(world, e, Position)); test_assert( !ecs_has(world, e, Velocity)); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); - test_assert(deleted == 0); test_bool(true, ecs_is_alive(world, ecs_id(Position))); test_bool(true, ecs_is_alive(world, ecs_id(Velocity))); test_assert( ecs_has(world, e, Position)); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .delete_generation = 1 }); - test_assert(deleted != 0); test_bool(true, ecs_is_alive(world, ecs_id(Position))); test_bool(true, ecs_is_alive(world, ecs_id(Velocity))); test_assert( ecs_has(world, e, Position)); - + ecs_add(world, e, Velocity); test_assert( ecs_has(world, e, Position)); @@ -1960,21 +1945,18 @@ void World_use_after_clear_empty_w_component(void) { test_assert( ecs_has(world, e, Position)); test_assert( !ecs_has(world, e, Velocity)); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); test_bool(true, ecs_is_alive(world, ecs_id(Position))); test_bool(true, ecs_is_alive(world, ecs_id(Velocity))); test_assert( ecs_has(world, e, Position)); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); test_bool(true, ecs_is_alive(world, ecs_id(Position))); test_bool(true, ecs_is_alive(world, ecs_id(Velocity))); test_assert( ecs_has(world, e, Position)); - + ecs_add(world, e, Velocity); test_assert( ecs_has(world, e, Position)); @@ -2006,21 +1988,18 @@ void World_use_after_clear_empty_w_component_w_lifecycle(void) { test_assert( ecs_has(world, e, Position)); test_assert( !ecs_has(world, e, Velocity)); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); test_bool(true, ecs_is_alive(world, ecs_id(Position))); test_bool(true, ecs_is_alive(world, ecs_id(Velocity))); test_assert( ecs_has(world, e, Position)); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); test_bool(true, ecs_is_alive(world, ecs_id(Position))); test_bool(true, ecs_is_alive(world, ecs_id(Velocity))); test_assert( ecs_has(world, e, Position)); - + ecs_add(world, e, Velocity); test_assert( ecs_has(world, e, Position)); @@ -2035,13 +2014,10 @@ void World_use_after_clear_unused(void) { ECS_TAG(world, TagA); ECS_TAG(world, TagB); - int32_t deleted; - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); - deleted = ecs_delete_empty_tables(world, + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ .clear_generation = 1 }); - test_assert(deleted == 0); ecs_entity_t e = ecs_new(world); ecs_add(world, e, TagA); @@ -3845,3 +3821,158 @@ void World_set_component_after_in_use(void) { ecs_fini(world); } + +void World_delete_empty_tables_w_offset(void) { + ecs_world_t *world = ecs_mini(); + + ECS_TAG(world, Tag); + ecs_run_aperiodic(world, 0); + + const ecs_world_info_t *info = ecs_get_world_info(world); + int32_t old_table_count = info->table_count; + + ecs_entity_t e = ecs_new_w(world, Tag); + for (int i = 0; i < 100; i ++) { + ecs_add_id(world, e, ecs_new(world)); + } + + test_int(info->table_count, old_table_count + 100 + 1); + + ecs_delete(world, e); + + /* First call increases generation to 1 */ + int32_t result; + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = 5 + }); + test_int(result, 0); /* Full scan completed */ + + /* Second call deletes the tables */ + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = 5 + }); + test_int(result, 0); /* Full scan completed */ + + test_assert(info->table_count <= old_table_count); + + ecs_fini(world); +} + +void World_delete_empty_tables_w_offset_out_of_range(void) { + ecs_world_t *world = ecs_mini(); + + ECS_TAG(world, TagA); + ECS_TAG(world, TagB); + + ecs_entity_t e = ecs_new(world); + ecs_add(world, e, TagA); + ecs_add(world, e, TagB); + ecs_remove(world, e, TagA); + + const ecs_world_info_t *info = ecs_get_world_info(world); + int32_t table_count = info->table_count; + + /* Offset way out of range, should clamp to 0 */ + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = 999999 + }); + + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = 999999 + }); + + /* Table should still be deleted despite out of range offset */ + test_assert(info->table_count < table_count); + + /* Negative offset, should clamp to 0 */ + ecs_add(world, e, TagA); + ecs_remove(world, e, TagA); + + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = -1 + }); + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = -1 + }); + + ecs_fini(world); +} + +void World_delete_empty_tables_w_offset_wrap_around(void) { + ecs_world_t *world = ecs_mini(); + + ECS_TAG(world, Tag); + ecs_run_aperiodic(world, 0); + + const ecs_world_info_t *info = ecs_get_world_info(world); + int32_t old_table_count = info->table_count; + + /* Create empty tables by adding unique components and removing them */ + ecs_entity_t e = ecs_new_w(world, Tag); + for (int i = 0; i < 50; i ++) { + ecs_add_id(world, e, ecs_new(world)); + } + + test_int(info->table_count, old_table_count + 50 + 1); + + ecs_delete(world, e); + + /* Use offset in the middle of the table range */ + int32_t offset = info->table_count / 2; + + /* First call increases generation */ + int32_t result; + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = offset + }); + test_int(result, 0); + + /* Second call deletes tables */ + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = offset + }); + test_int(result, 0); + + /* Verify all empty tables were cleaned up */ + test_assert(info->table_count <= old_table_count); + + ecs_fini(world); +} + +void World_delete_empty_tables_return_value(void) { + ecs_world_t *world = ecs_mini(); + + ECS_TAG(world, TagA); + ECS_TAG(world, TagB); + + /* Without time budget, should return 0 (full scan) */ + int32_t result; + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1 + }); + test_int(result, 0); + + /* With offset, should also return 0 for full scan */ + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = 1 + }); + test_int(result, 0); + + /* With offset=0, should return 0 */ + result = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t){ + .delete_generation = 1, + .offset = 0 + }); + test_int(result, 0); + + ecs_fini(world); +} diff --git a/test/core/src/main.c b/test/core/src/main.c index 3c6153d689..693eb836ae 100644 --- a/test/core/src/main.c +++ b/test/core/src/main.c @@ -2722,6 +2722,10 @@ void World_add_dont_fragment_after_pair_query(void); void World_add_can_toggle_after_pair_query(void); void World_add_traversable_after_pair_query(void); void World_set_component_after_in_use(void); +void World_delete_empty_tables_w_offset(void); +void World_delete_empty_tables_w_offset_out_of_range(void); +void World_delete_empty_tables_w_offset_wrap_around(void); +void World_delete_empty_tables_return_value(void); // Testsuite 'ExclusiveAccess' void ExclusiveAccess_self(void); @@ -13872,6 +13876,22 @@ bake_test_case World_testcases[] = { { "set_component_after_in_use", World_set_component_after_in_use + }, + { + "delete_empty_tables_w_offset", + World_delete_empty_tables_w_offset + }, + { + "delete_empty_tables_w_offset_out_of_range", + World_delete_empty_tables_w_offset_out_of_range + }, + { + "delete_empty_tables_w_offset_wrap_around", + World_delete_empty_tables_w_offset_wrap_around + }, + { + "delete_empty_tables_return_value", + World_delete_empty_tables_return_value } }; @@ -16148,7 +16168,7 @@ static bake_test_suite suites[] = { "World", World_setup, NULL, - 154, + 158, World_testcases }, { diff --git a/test/query/src/Cached.c b/test/query/src/Cached.c index c442656146..324bd1b619 100644 --- a/test/query/src/Cached.c +++ b/test/query/src/Cached.c @@ -3788,13 +3788,12 @@ void Cached_cascade_default_group_reinsert_after_empty_table_delete(void) { ecs_delete(world, e1); - int32_t deleted = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { .delete_generation = 1 }); - deleted += ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { .delete_generation = 1 }); - test_assert(deleted > 0); ecs_entity_t e2 = ecs_new(world); ecs_set(world, e2, Position, {3, 4}); @@ -3804,13 +3803,12 @@ void Cached_cascade_default_group_reinsert_after_empty_table_delete(void) { /* Reproduces assert in flecs_query_cache_remove_group: * ecs_assert(cur != NULL, ECS_INTERNAL_ERROR, NULL); */ - deleted = ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { .delete_generation = 1 }); - deleted += ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { + ecs_delete_empty_tables(world, &(ecs_delete_empty_tables_desc_t) { .delete_generation = 1 }); - test_assert(deleted > 0); ecs_fini(world); }