diff --git a/CMakeLists.txt b/CMakeLists.txt index e8b4f75..9bba9c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -297,6 +297,7 @@ add_library(akgl SHARED src/actor.c src/collision.c src/collision_arena.c + src/collision_bsp.c src/collision_grid.c src/collision_shape.c src/actor_state_string_names.c diff --git a/include/akgl/collision.h b/include/akgl/collision.h index a55810c..0dc9621 100644 --- a/include/akgl/collision.h +++ b/include/akgl/collision.h @@ -42,6 +42,7 @@ #include #include +#include #include @@ -153,8 +154,26 @@ typedef struct akgl_CollisionProxy { int32_t cx1; /**< Grid cell rectangle currently occupied. */ int32_t cy1; /**< Grid cell rectangle currently occupied. */ int16_t first; /**< Head of this proxy's chain of cell entries, or -1 when it is not in the grid. Written by the uniform grid only. */ + aksl_ListNode node; /**< Membership of one BSP node's item list. Written by the BSP partitioner only. A proxy is in exactly one node at a time, so one link suffices and the BSP needs no pool of its own for these. */ } akgl_CollisionProxy; +/** + * @brief One node of the BSP partitioner. Pool object. + * + * The tree links are libakstdlib's, but only the links: the traversal is written + * by hand. See `src/collision_bsp.c` for why aksl_tree_iterate cannot be used + * for a spatial query. + */ +typedef struct akgl_BspNode { + uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */ + aksl_TreeNode node; /**< left/right/parent. `leaf` points back at this struct so a walk can recover it. */ + aksl_List items; /**< Proxies stored here, threaded through akgl_CollisionProxy::node. */ + SDL_FRect bounds; /**< The region this node covers. aksl_TreeNode does not carry it and a pruning descent cannot work without it. */ + uint8_t axis; /**< 0 splits on x, 1 splits on y, 2 is a leaf. */ + float32_t split; /**< World coordinate of the plane on #axis. Meaningless in a leaf. */ + uint8_t depth; /**< Distance from the root, so the build terminates. */ +} akgl_BspNode; + /** * @brief One proxy's membership of one grid cell. * @@ -351,6 +370,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_factory(akgl_Partitioner *se */ akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_init_grid(akgl_Partitioner *self); +/** + * @brief Install the binary space partition. + * + * **Use the grid.** This ships so that "pluggable" means something -- a vtable + * with one implementation is a vtable nobody has tested -- and so the two can be + * measured against each other rather than argued about. It rebuilds from scratch + * whenever the proxy set changes, which is the shape `PERFORMANCE.md` records + * Phaser using and capping out around five thousand bodies. + * + * It earns its place where a uniform grid degenerates: a world with wildly + * non-uniform object sizes, or one far larger than the fixed cell array covers. + * + * @param self The partitioner to fill in. Required. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER If @p self is `NULL`. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_init_bsp(akgl_Partitioner *self); + /** * @brief Make a map's solid layers count as collision geometry. * diff --git a/include/akgl/heap.h b/include/akgl/heap.h index ea9de11..2f83152 100644 --- a/include/akgl/heap.h +++ b/include/akgl/heap.h @@ -71,6 +71,10 @@ * `AKGL_COLLISION_GRID_MAX_SPAN` cells onto a single chain instead of celling * it, so no proxy can ever hold more than that many entries. */ +/** @brief The BSP node pool. A median-split tree over N proxies at 8 per leaf needs about N/2 nodes. */ +#ifndef AKGL_MAX_HEAP_BSPNODE +#define AKGL_MAX_HEAP_BSPNODE (AKGL_MAX_HEAP_COLLISION_PROXY) +#endif #ifndef AKGL_MAX_HEAP_COLLISION_CELL #define AKGL_MAX_HEAP_COLLISION_CELL (AKGL_MAX_HEAP_COLLISION_PROXY * 16) #endif @@ -89,6 +93,8 @@ extern akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING]; extern akgl_CollisionProxy akgl_heap_collision_proxies[AKGL_MAX_HEAP_COLLISION_PROXY]; /** @brief The grid cell-entry pool. Only the uniform grid allocates from it. */ extern akgl_CollisionCell akgl_heap_collision_cells[AKGL_MAX_HEAP_COLLISION_CELL]; +/** @brief The BSP node pool. Only the BSP partitioner allocates from it. */ +extern akgl_BspNode akgl_heap_bspnodes[AKGL_MAX_HEAP_BSPNODE]; /** * @brief Zero every pool, marking every slot free. @@ -295,4 +301,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_collision_cell(akgl_Collisi */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_collision_cells(void); +/** @brief Find a free BSP node. Does not claim it. @throws AKGL_ERR_HEAP when full. */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_bspnode(akgl_BspNode **dest); +/** @brief Give a BSP node back. @throws AKERR_NULLPOINTER If @p ptr is `NULL`. */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_bspnode(akgl_BspNode *ptr); +/** @brief Drop every BSP node without touching any other pool. */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_bspnodes(void); + #endif //_AKGL_HEAP_H_ diff --git a/src/collision.c b/src/collision.c index b18293f..26f88d2 100644 --- a/src/collision.c +++ b/src/collision.c @@ -464,6 +464,10 @@ akerr_ErrorContext *akgl_partitioner_factory(akgl_Partitioner *self, char *type) PASS(errctx, akgl_partitioner_init_grid(self)); SUCCEED_RETURN(errctx); } + if ( strncmp(type, "bsp", 3) == 0 ) { + PASS(errctx, akgl_partitioner_init_bsp(self)); + SUCCEED_RETURN(errctx); + } FAIL_RETURN(errctx, AKERR_KEY, "No partitioner named \"%s\"", type); } diff --git a/src/collision_bsp.c b/src/collision_bsp.c new file mode 100644 index 0000000..7e224e4 --- /dev/null +++ b/src/collision_bsp.c @@ -0,0 +1,473 @@ +/** + * @file collision_bsp.c + * @brief A binary space partition, and why it is not the default. + * + * This exists so that "pluggable" means something. A vtable with one + * implementation behind it has never been asked to be a vtable, and the + * partitioner suite runs its whole contract against every entry in a table -- + * so a second implementation is what turns that contract from a comment into a + * check. + * + * It rebuilds the tree whenever the proxy set changes rather than maintaining it + * incrementally, which is the shape `PERFORMANCE.md` records Phaser using and + * capping out around five thousand bodies. The grid stays the default for the + * reason recorded there: an object that has not left its cell costs a grid + * nothing, and costs this a whole rebuild. + * + * @section bsp_no_iterate Why aksl_tree_iterate is not used + * + * It cannot be, for three independent reasons, and a comment is cheaper than the + * next person rediscovering them: + * + * 1. **It is a complete traversal.** Every search mode visits every node, and + * the only control signal the callback has is `AKERR_ITERATOR_BREAK`, which + * stops the *entire* walk. There is no way to say "do not descend into this + * subtree", and that is the only operation a spatial query is made of. Using + * it would visit every node in the tree -- strictly worse than the linear + * scan the tree is supposed to replace. + * 2. **It carries no per-node context.** A pruning descent needs each node's + * bounds and split plane; the callback gets a node and one `void *` that is + * the same object at every node. + * 3. **Its breadth-first modes allocate.** Only the depth-first ones are + * malloc-free, and those are the ones with no pruning. + * + * `aksl_tree_insert` and `aksl_tree_find` are equally unusable and for a + * different reason: they are a comparator-ordered binary search tree. + * `aksl_TreeCompareFunc` is handed two leaves and asked which is smaller, and a + * spatial insert has to compare a leaf against a *plane*, which that signature + * cannot express. Inserting through it builds a tree ordered by pointer value. + * + * What is used from libakstdlib is the link structure and the lists: aksl_TreeNode + * for left/right/parent, aksl_List and aksl_ListNode for a node's items. Neither + * allocates, which is why they fit here at all. + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include + +/** @brief Proxies at or below which a node stops splitting. */ +#define AKGL_COLLISION_BSP_LEAF_ITEMS 8 +/** @brief Deepest the tree may go. Bounds the descent stack, which is an array. */ +#define AKGL_COLLISION_BSP_MAX_DEPTH 16 + +/** @brief The root, or NULL when there is nothing to index. */ +static akgl_BspNode *bsp_root; +/** @brief Every proxy currently registered, in insertion order. */ +static akgl_CollisionProxy *bsp_members[AKGL_MAX_HEAP_COLLISION_PROXY]; +static int bsp_count; +/** @brief Set when the tree no longer describes bsp_members. */ +static bool bsp_dirty; +/** @brief World extent, for the root node's bounds. */ +static SDL_FRect bsp_extent; +/** @brief Bumped per query, stamped on proxies so one is reported once. */ +static uint32_t bsp_sweep; + +/** @brief Give every node back and forget the tree. */ +static akerr_ErrorContext *bsp_drop_tree(void) +{ + PREPARE_ERROR(errctx); + PASS(errctx, akgl_heap_init_bspnodes()); + bsp_root = NULL; + SUCCEED_RETURN(errctx); +} + +/** @brief Claim a node covering a region. */ +static akerr_ErrorContext *bsp_node(akgl_BspNode **dest, SDL_FRect *bounds, uint8_t depth) +{ + PREPARE_ERROR(errctx); + PASS(errctx, akgl_heap_next_bspnode(dest)); + memset(*dest, 0x00, sizeof(akgl_BspNode)); + (*dest)->refcount = 1; + (*dest)->bounds = *bounds; + (*dest)->depth = depth; + (*dest)->axis = 2; + PASS(errctx, aksl_tree_node_init(&(*dest)->node, (void *)(*dest))); + PASS(errctx, aksl_list_init(&(*dest)->items)); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Split a node and push down whatever fits entirely on one side. + * + * Median of the item centres on the longer axis, not the spatial midpoint. + * Actors in a tile game cluster -- everything is on the floor -- and a midpoint + * split gives one empty child and one full one for several levels running. + * + * A proxy straddling the plane stays at this node and is tested against + * everything below on both sides. That is the standard BSP tax, and it is why + * one inline list link per proxy is enough. + */ +static akerr_ErrorContext *bsp_build(akgl_BspNode *node, akgl_CollisionProxy **items, int count) +{ + akgl_CollisionProxy *left[AKGL_MAX_HEAP_COLLISION_PROXY]; + akgl_CollisionProxy *right[AKGL_MAX_HEAP_COLLISION_PROXY]; + akgl_BspNode *child = NULL; + SDL_FRect half; + float32_t total = 0.0f; + int leftn = 0; + int rightn = 0; + int i = 0; + + PREPARE_ERROR(errctx); + + if ( (count <= AKGL_COLLISION_BSP_LEAF_ITEMS) || + (node->depth >= AKGL_COLLISION_BSP_MAX_DEPTH) ) { + for ( i = 0; i < count; i++ ) { + PASS(errctx, aksl_list_node_init(&items[i]->node, (void *)items[i])); + PASS(errctx, aksl_list_push(&node->items, &items[i]->node)); + } + SUCCEED_RETURN(errctx); + } + + node->axis = (node->bounds.w >= node->bounds.h) ? 0 : 1; + for ( i = 0; i < count; i++ ) { + if ( node->axis == 0 ) { + total += items[i]->bounds.x + (items[i]->bounds.w / 2.0f); + } else { + total += items[i]->bounds.y + (items[i]->bounds.h / 2.0f); + } + } + node->split = total / (float32_t)count; + + for ( i = 0; i < count; i++ ) { + if ( node->axis == 0 ) { + if ( (items[i]->bounds.x + items[i]->bounds.w) <= node->split ) { + left[leftn++] = items[i]; + continue; + } + if ( items[i]->bounds.x >= node->split ) { + right[rightn++] = items[i]; + continue; + } + } else { + if ( (items[i]->bounds.y + items[i]->bounds.h) <= node->split ) { + left[leftn++] = items[i]; + continue; + } + if ( items[i]->bounds.y >= node->split ) { + right[rightn++] = items[i]; + continue; + } + } + // Straddles the plane: it stays here. + PASS(errctx, aksl_list_node_init(&items[i]->node, (void *)items[i])); + PASS(errctx, aksl_list_push(&node->items, &items[i]->node)); + } + + // A split that separated nothing would recurse forever on the same set. + if ( (leftn == 0) || (rightn == 0) ) { + for ( i = 0; i < leftn; i++ ) { + PASS(errctx, aksl_list_node_init(&left[i]->node, (void *)left[i])); + PASS(errctx, aksl_list_push(&node->items, &left[i]->node)); + } + for ( i = 0; i < rightn; i++ ) { + PASS(errctx, aksl_list_node_init(&right[i]->node, (void *)right[i])); + PASS(errctx, aksl_list_push(&node->items, &right[i]->node)); + } + node->axis = 2; + SUCCEED_RETURN(errctx); + } + + half = node->bounds; + if ( node->axis == 0 ) { + half.w = node->split - node->bounds.x; + } else { + half.h = node->split - node->bounds.y; + } + PASS(errctx, bsp_node(&child, &half, (uint8_t)(node->depth + 1))); + node->node.left = &child->node; + child->node.parent = &node->node; + PASS(errctx, bsp_build(child, left, leftn)); + + half = node->bounds; + if ( node->axis == 0 ) { + half.x = node->split; + half.w = (node->bounds.x + node->bounds.w) - node->split; + } else { + half.y = node->split; + half.h = (node->bounds.y + node->bounds.h) - node->split; + } + PASS(errctx, bsp_node(&child, &half, (uint8_t)(node->depth + 1))); + node->node.right = &child->node; + child->node.parent = &node->node; + PASS(errctx, bsp_build(child, right, rightn)); + SUCCEED_RETURN(errctx); +} + +/** @brief Rebuild the tree from the registered proxies, if it is stale. */ +static akerr_ErrorContext *bsp_rebuild(void) +{ + SDL_FRect bounds; + int i = 0; + + PREPARE_ERROR(errctx); + + if ( bsp_dirty == false ) { + SUCCEED_RETURN(errctx); + } + PASS(errctx, bsp_drop_tree()); + bsp_dirty = false; + + if ( bsp_count == 0 ) { + SUCCEED_RETURN(errctx); + } + + // The root covers the world, widened to hold anything outside it. A proxy + // that fell off the map is still a collider. + bounds = bsp_extent; + for ( i = 0; i < bsp_count; i++ ) { + if ( bsp_members[i]->bounds.x < bounds.x ) { + bounds.w += bounds.x - bsp_members[i]->bounds.x; + bounds.x = bsp_members[i]->bounds.x; + } + if ( bsp_members[i]->bounds.y < bounds.y ) { + bounds.h += bounds.y - bsp_members[i]->bounds.y; + bounds.y = bsp_members[i]->bounds.y; + } + if ( (bsp_members[i]->bounds.x + bsp_members[i]->bounds.w) > (bounds.x + bounds.w) ) { + bounds.w = (bsp_members[i]->bounds.x + bsp_members[i]->bounds.w) - bounds.x; + } + if ( (bsp_members[i]->bounds.y + bsp_members[i]->bounds.h) > (bounds.y + bounds.h) ) { + bounds.h = (bsp_members[i]->bounds.y + bsp_members[i]->bounds.h) - bounds.y; + } + } + + PASS(errctx, bsp_node(&bsp_root, &bounds, 0)); + PASS(errctx, bsp_build(bsp_root, bsp_members, bsp_count)); + SUCCEED_RETURN(errctx); +} + +static akerr_ErrorContext *bsp_reset(akgl_Partitioner *self, akgl_CollisionWorld *world) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + FAIL_ZERO_RETURN(errctx, world, AKERR_NULLPOINTER, "NULL collision world reference"); + FAIL_NONZERO_RETURN(errctx, (world->cellwidth <= 0.0f), AKERR_VALUE, + "Cell width %f is not positive", world->cellwidth); + FAIL_NONZERO_RETURN(errctx, (world->cellheight <= 0.0f), AKERR_VALUE, + "Cell height %f is not positive", world->cellheight); + + PASS(errctx, bsp_drop_tree()); + bsp_count = 0; + bsp_dirty = true; + bsp_sweep = 0; + bsp_extent.x = world->originx; + bsp_extent.y = world->originy; + bsp_extent.w = world->cellwidth * 128.0f; + bsp_extent.h = world->cellheight * 128.0f; + SUCCEED_RETURN(errctx); +} + +static akerr_ErrorContext *bsp_insert(akgl_Partitioner *self, akgl_CollisionProxy *proxy) +{ + int i = 0; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + FAIL_ZERO_RETURN(errctx, proxy, AKERR_NULLPOINTER, "NULL proxy reference"); + + for ( i = 0; i < bsp_count; i++ ) { + if ( bsp_members[i] == proxy ) { + SUCCEED_RETURN(errctx); + } + } + FAIL_NONZERO_RETURN(errctx, (bsp_count >= AKGL_MAX_HEAP_COLLISION_PROXY), AKGL_ERR_HEAP, + "More proxies registered than the pool can hold"); + bsp_members[bsp_count] = proxy; + bsp_count += 1; + bsp_dirty = true; + SUCCEED_RETURN(errctx); +} + +static akerr_ErrorContext *bsp_remove(akgl_Partitioner *self, akgl_CollisionProxy *proxy) +{ + int i = 0; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + FAIL_ZERO_RETURN(errctx, proxy, AKERR_NULLPOINTER, "NULL proxy reference"); + + for ( i = 0; i < bsp_count; i++ ) { + if ( bsp_members[i] != proxy ) { + continue; + } + bsp_members[i] = bsp_members[bsp_count - 1]; + bsp_count -= 1; + bsp_dirty = true; + SUCCEED_RETURN(errctx); + } + // Removing something that is not in is success, as it is for the grid. + SUCCEED_RETURN(errctx); +} + +static akerr_ErrorContext *bsp_move(akgl_Partitioner *self, akgl_CollisionProxy *proxy) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + FAIL_ZERO_RETURN(errctx, proxy, AKERR_NULLPOINTER, "NULL proxy reference"); + + /* + * A tree cannot answer this cheaply. The grid compares four integers and + * returns; this has to assume the partition is wrong now, and the next query + * pays for a rebuild. That difference is the whole reason the grid is the + * default, and it is visible right here rather than buried in a benchmark. + */ + bsp_dirty = true; + PASS(errctx, bsp_insert(self, proxy)); + SUCCEED_RETURN(errctx); +} + +/** + * @brief Visit the items of every node whose region the query can still reach. + * + * An explicit stack, not recursion. The depth bound is then an array bound the + * compiler can see rather than a hope, and this library already has one + * documented way to blow the C stack -- akgl_heap_release_actor recursing over + * children with no cycle check -- which is one more than it needs. + */ +static akerr_ErrorContext *bsp_descend(SDL_FRect *area, uint32_t mask, + akgl_CollisionVisitFunc visit, void *data) +{ + akgl_BspNode *stack[AKGL_COLLISION_BSP_MAX_DEPTH + 2]; + akgl_BspNode *node = NULL; + akgl_CollisionProxy *proxy = NULL; + aksl_ListNode *walk = NULL; + akerr_ErrorContext *inner = NULL; + bool stop = false; + int top = 0; + + PREPARE_ERROR(errctx); + + if ( bsp_root == NULL ) { + SUCCEED_RETURN(errctx); + } + stack[top++] = bsp_root; + + while ( (top > 0) && (stop == false) ) { + node = stack[--top]; + + walk = node->items.head; + while ( (walk != NULL) && (stop == false) ) { + proxy = (akgl_CollisionProxy *)walk->data; + walk = walk->next; + if ( proxy->stamp == bsp_sweep ) { + continue; + } + proxy->stamp = bsp_sweep; + if ( (proxy->shape.layermask & mask) == 0 ) { + continue; + } + if ( !SDL_HasRectIntersectionFloat(&proxy->bounds, area) ) { + continue; + } + // Taken into a local rather than handed to CATCH: a break here would + // leave this loop, not the function, and the failure would look like + // the end of the list. + inner = visit(proxy, data); + if ( inner != NULL ) { + stop = true; + PASS(errctx, inner); + } + } + + if ( node->axis == 2 ) { + continue; + } + + /* + * The pruning. A query entirely on one side of the plane pushes one + * child and never touches the other subtree -- which is the operation + * aksl_tree_iterate has no way to express, and the reason this walk is + * written by hand. + */ + if ( node->axis == 0 ) { + if ( (area->x < node->split) && (node->node.left != NULL) && (top < AKGL_COLLISION_BSP_MAX_DEPTH) ) { + stack[top++] = (akgl_BspNode *)node->node.left->leaf; + } + if ( ((area->x + area->w) >= node->split) && (node->node.right != NULL) && (top < AKGL_COLLISION_BSP_MAX_DEPTH) ) { + stack[top++] = (akgl_BspNode *)node->node.right->leaf; + } + } else { + if ( (area->y < node->split) && (node->node.left != NULL) && (top < AKGL_COLLISION_BSP_MAX_DEPTH) ) { + stack[top++] = (akgl_BspNode *)node->node.left->leaf; + } + if ( ((area->y + area->h) >= node->split) && (node->node.right != NULL) && (top < AKGL_COLLISION_BSP_MAX_DEPTH) ) { + stack[top++] = (akgl_BspNode *)node->node.right->leaf; + } + } + } + SUCCEED_RETURN(errctx); +} + +static akerr_ErrorContext *bsp_query(akgl_Partitioner *self, SDL_FRect *area, uint32_t mask, + akgl_CollisionVisitFunc visit, void *data) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + FAIL_ZERO_RETURN(errctx, area, AKERR_NULLPOINTER, "NULL query area reference"); + FAIL_ZERO_RETURN(errctx, visit, AKERR_NULLPOINTER, "NULL visitor reference"); + + PASS(errctx, bsp_rebuild()); + bsp_sweep += 1; + PASS(errctx, bsp_descend(area, mask, visit, data)); + SUCCEED_RETURN(errctx); +} + +static akerr_ErrorContext *bsp_each_pair(akgl_Partitioner *self, akgl_CollisionPairFunc visit, void *data) +{ + akerr_ErrorContext *inner = NULL; + bool stop = false; + int i = 0; + int j = 0; + + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + FAIL_ZERO_RETURN(errctx, visit, AKERR_NULLPOINTER, "NULL visitor reference"); + + PASS(errctx, bsp_rebuild()); + + /* + * Every pair whose bounds overlap, straight off the member list. A tree + * offers no cheap way to enumerate pairs -- the grid gets that for free from + * cell membership -- so this is honest about being the naive form rather + * than dressing a linear scan up as a spatial one. + */ + for ( i = 0; (i < bsp_count) && (stop == false); i++ ) { + for ( j = i + 1; (j < bsp_count) && (stop == false); j++ ) { + if ( !SDL_HasRectIntersectionFloat(&bsp_members[i]->bounds, &bsp_members[j]->bounds) ) { + continue; + } + inner = visit(bsp_members[i], bsp_members[j], data); + if ( inner != NULL ) { + stop = true; + PASS(errctx, inner); + } + } + } + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext *akgl_partitioner_init_bsp(akgl_Partitioner *self) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL partitioner reference"); + + memset(self, 0x00, sizeof(akgl_Partitioner)); + PASS(errctx, aksl_strncpy(self->name, sizeof(self->name), "bsp", sizeof(self->name) - 1)); + self->reset = bsp_reset; + self->insert = bsp_insert; + self->remove = bsp_remove; + self->move = bsp_move; + self->query = bsp_query; + self->each_pair = bsp_each_pair; + self->state = NULL; + SUCCEED_RETURN(errctx); +} diff --git a/src/heap.c b/src/heap.c index 3751c85..8e48044 100644 --- a/src/heap.c +++ b/src/heap.c @@ -21,6 +21,7 @@ akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER]; akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING]; akgl_CollisionProxy akgl_heap_collision_proxies[AKGL_MAX_HEAP_COLLISION_PROXY]; akgl_CollisionCell akgl_heap_collision_cells[AKGL_MAX_HEAP_COLLISION_CELL]; +akgl_BspNode akgl_heap_bspnodes[AKGL_MAX_HEAP_BSPNODE]; akerr_ErrorContext *akgl_heap_init(void) { @@ -44,6 +45,7 @@ akerr_ErrorContext *akgl_heap_init(void) akgl_heap_collision_proxies[i].first = -1; } PASS(errctx, akgl_heap_init_collision_cells()); + PASS(errctx, akgl_heap_init_bspnodes()); SUCCEED_RETURN(errctx); } @@ -300,3 +302,39 @@ akerr_ErrorContext *akgl_heap_init_collision_cells(void) } SUCCEED_RETURN(errctx); } + +akerr_ErrorContext *akgl_heap_next_bspnode(akgl_BspNode **dest) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination reference"); + for (int i = 0; i < AKGL_MAX_HEAP_BSPNODE; i++ ) { + if ( akgl_heap_bspnodes[i].refcount != 0 ) { + continue; + } + *dest = &akgl_heap_bspnodes[i]; + SUCCEED_RETURN(errctx); + } + FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused BSP node on the heap"); +} + +akerr_ErrorContext *akgl_heap_release_bspnode(akgl_BspNode *ptr) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL BSP node reference"); + if ( ptr->refcount > 0 ) { + ptr->refcount -= 1; + } + if ( ptr->refcount == 0 ) { + memset(ptr, 0x00, sizeof(akgl_BspNode)); + } + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext *akgl_heap_init_bspnodes(void) +{ + PREPARE_ERROR(errctx); + for ( int i = 0; i < AKGL_MAX_HEAP_BSPNODE; i++ ) { + memset(&akgl_heap_bspnodes[i], 0x00, sizeof(akgl_BspNode)); + } + SUCCEED_RETURN(errctx); +} diff --git a/tests/partition.c b/tests/partition.c index 67f7158..4688845 100644 --- a/tests/partition.c +++ b/tests/partition.c @@ -31,7 +31,7 @@ #include "testutil.h" /** @brief Every partitioner the factory knows, so the same tests run over each. */ -static char *partitioners[] = { "grid" }; +static char *partitioners[] = { "grid", "bsp" }; /** @brief What a visit callback accumulates. */ typedef struct { @@ -283,12 +283,20 @@ akerr_ErrorContext *test_move_is_incremental(char *name) * checked rather than trusted: the proxy's chain head is the cheapest * observable proof that no entry was released and re-claimed. */ - firstbefore = proxy->first; - CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 201.0f, 201.0f, 0.0f)); - CATCH(errctx, world.partitioner.move(&world.partitioner, proxy)); - TEST_ASSERT(errctx, (proxy->first == firstbefore), - "%s re-celled a proxy that did not leave its cells; the structure is not " - "incremental and the argument for choosing it does not hold", name); + /* + * Only the grid claims to be incremental, and this is where that claim + * is checked rather than believed. The BSP is exempt by construction: it + * marks itself stale on any move and rebuilds, which is exactly the cost + * the grid was chosen to avoid and exactly why it is not the default. + */ + if ( strcmp(name, "grid") == 0 ) { + firstbefore = proxy->first; + CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 201.0f, 201.0f, 0.0f)); + CATCH(errctx, world.partitioner.move(&world.partitioner, proxy)); + TEST_ASSERT(errctx, (proxy->first == firstbefore), + "%s re-celled a proxy that did not leave its cells; the structure is " + "not incremental and the argument for choosing it does not hold", name); + } /* * Moving repeatedly must not accumulate index entries. A move that links