Add a pluggable broad phase, and the incremental uniform grid behind it
akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -63,6 +63,17 @@
|
||||
#ifndef AKGL_MAX_HEAP_COLLISION_PROXY
|
||||
#define AKGL_MAX_HEAP_COLLISION_PROXY (AKGL_MAX_HEAP_ACTOR * 2)
|
||||
#endif
|
||||
/**
|
||||
* @brief The grid cell-entry pool.
|
||||
*
|
||||
* One entry per proxy per cell it overlaps. The ceiling is provable rather than
|
||||
* hopeful: the grid spills anything covering more than
|
||||
* `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.
|
||||
*/
|
||||
#ifndef AKGL_MAX_HEAP_COLLISION_CELL
|
||||
#define AKGL_MAX_HEAP_COLLISION_CELL (AKGL_MAX_HEAP_COLLISION_PROXY * 16)
|
||||
#endif
|
||||
|
||||
/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */
|
||||
extern akgl_Actor akgl_heap_actors[AKGL_MAX_HEAP_ACTOR];
|
||||
@@ -76,6 +87,8 @@ extern akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER];
|
||||
extern akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
|
||||
/** @brief The collision proxy pool. Only the library allocates from it. */
|
||||
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 Zero every pool, marking every slot free.
|
||||
@@ -260,4 +273,26 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_collision_proxy(akgl_Collision
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_collision_proxy(akgl_CollisionProxy *ptr);
|
||||
|
||||
/**
|
||||
* @brief Find a free grid cell entry. Does not claim it; the initialize does.
|
||||
* @param dest Receives the entry. Required.
|
||||
* @throws AKGL_ERR_HEAP If every slot is in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_collision_cell(akgl_CollisionCell **dest);
|
||||
|
||||
/**
|
||||
* @brief Give a grid cell entry back.
|
||||
* @param ptr The entry. Required.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_collision_cell(akgl_CollisionCell *ptr);
|
||||
|
||||
/**
|
||||
* @brief Drop every grid cell entry without touching any other pool.
|
||||
*
|
||||
* The partitioner's `reset` calls this. Sibling of akgl_heap_init_actor, for the
|
||||
* same reason: a level change should not cost the sprite and character pools.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_collision_cells(void);
|
||||
|
||||
#endif //_AKGL_HEAP_H_
|
||||
|
||||
Reference in New Issue
Block a user