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:
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user