Add a second partitioner, so the vtable is a seam and not a decoration

A binary space partition on libakstdlib's tree links and lists, registered in
the factory as "bsp". It runs the same assertions the grid does, because
tests/partition.c is a table over partitioners and adding a row is all it takes
to be held to the contract.

**Use the grid.** This is here to prove the vtable works and to have something
to measure the grid against, and the file says so at the top. It rebuilds
whenever the proxy set changes, which is the shape PERFORMANCE.md records Phaser
using and capping out around five thousand bodies. It would earn its place in a
world with wildly non-uniform object sizes, or one larger than the grid's fixed
cell array covers.

The difference is visible in the code rather than buried in a benchmark: the
grid's `move` compares four integers and returns when a proxy has not left its
cells, and this one marks the whole partition stale. The incremental-move
assertion in the suite is therefore grid-only, and says why.

aksl_tree_iterate is not used, and the file carries the three reasons so the
next reader does not rediscover them: it is a complete traversal whose only
control signal stops the entire walk, so there is no way to prune a subtree --
which is the only operation a spatial query is made of; it carries no per-node
context, and a pruning descent needs each node's bounds and plane; and its
breadth-first modes allocate, while only the depth-first ones are malloc-free
and those are the ones without pruning. aksl_tree_insert is unusable for a
different reason again: it is a comparator-ordered BST, and a spatial insert has
to compare a leaf against a plane, which aksl_TreeCompareFunc cannot express.

What is used is the link structure and the lists, neither of which allocates,
which is why they fit here at all. The descent is an explicit stack rather than
recursion, so the depth bound is an array bound the compiler can see -- this
library already has one documented way to blow the C stack and does not need a
second.

Split planes are the median of the item centres on the longer axis, not the
spatial midpoint: actors in a tile game cluster on the floor, and a midpoint
split gives one empty child and one full one for several levels running. A split
that separates nothing degrades the node to a leaf rather than recursing forever
on the same set.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 07:08:54 -04:00
parent 5e213061af
commit cf930f68bb
7 changed files with 581 additions and 7 deletions

View File

@@ -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

View File

@@ -42,6 +42,7 @@
#include <SDL3/SDL_rect.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/types.h>
@@ -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.
*

View File

@@ -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_

View File

@@ -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);
}

473
src/collision_bsp.c Normal file
View File

@@ -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 <math.h>
#include <stddef.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/collision.h>
#include <akgl/error.h>
#include <akgl/heap.h>
/** @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);
}

View File

@@ -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);
}

View File

@@ -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