libakgl does not call malloc at runtime. That is stated in seven places in the manual and is AGENTS.md's second standing rule, and every object comes from a fixed array in akgl/heap.h. libccd's EPA path does not know that: it builds its expanding polytope out of realloc and free, and ccdGJKPenetration documents a -2 return for when that fails. The alternative was to compile only the three files MPR needs and leave EPA out of the build. That works and it puts a copy of somebody else's source list in this repository, where it rots silently on the next submodule bump. This instead compiles all of libccd and points its allocator at a bump allocator over static BSS, which keeps the promise literally true -- and keeps EPA available rather than amputated, for the day a precise contact manifold is worth having. The arena is reset at the top of each query rather than freed block by block, so the lifetime is one narrowphase call, `free` is a no-op, and allocation is a pointer bump. Exhaustion returns NULL, which libccd already handles by unwinding to -2, which becomes AKGL_ERR_COLLISION naming the high-water mark -- a loud failure with a number in it rather than a silently missed collision, which would be a floor an actor falls through reported as success. One GJK/EPA box pair costs 7,264 bytes, measured and printed by the suite. The 64 KB ceiling is that with about nine times headroom, and the high-water mark is reported so the next reader can re-derive it rather than trust this line. The redirect lives in src/ccd_arena_shim.h, injected with -include, and not in CMake's COMPILE_DEFINITIONS. It was in COMPILE_DEFINITIONS first, and that is a mistake worth recording: CMake cannot carry a function-like macro through a -D, so it dropped __CCD_ALLOC_MEMORY without a diagnostic. libccd went on calling the C library's realloc while the shim's `free` quietly discarded the results -- strictly worse than doing nothing, and invisible, because it leaks rather than crashes. What caught it was insisting the test prove the wiring rather than the outcome. The first version asserted the arena balanced back to zero after a query, which turned out to be the wrong assertion for a different reason -- free is a no-op by design, so it cannot balance -- but a test that had merely checked "two boxes collide" would have passed throughout, against an allocator nothing was using. The suite now asserts what is actually provable: that a query allocates from the arena at all, and that the process survives, since glibc aborts when the real free(3) is handed a pointer it never issued. Also here: AKGL_ERR_COLLISION, with the name registered -- tests/error.c asserts the band and the names agree, and it caught the missing one immediately. -fvisibility=hidden and CCD_STATIC_DEFINE keep every ccd* symbol out of libakgl.so's dynamic table, which `nm -D` confirms is empty; AGENTS.md records a shipped defect where an exported `renderer` was preempted by a same-named symbol elsewhere, and a game linking a system libccd would hit exactly that. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
4.4 KiB
C
133 lines
4.4 KiB
C
/**
|
|
* @file collision.c
|
|
* @brief The collision narrowphase, and the only place libccd is visible.
|
|
*
|
|
* Nothing else in the tree includes `<ccd/ccd.h>`. The translation between
|
|
* libakgl's shapes and libccd's support-function protocol is `static` here, so
|
|
* a change of narrowphase library is a change to one file and no header.
|
|
*
|
|
* At present this holds only the arena's end-to-end check; the shapes, the
|
|
* contact and the resolver arrive with the rest of the collision work.
|
|
*/
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/collision_arena.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/types.h>
|
|
|
|
#include <ccd/ccd.h>
|
|
#include <ccd/quat.h>
|
|
#include <ccd/vec3.h>
|
|
|
|
/**
|
|
* @brief Ceiling on narrowphase iterations.
|
|
*
|
|
* libccd's own default, set by `CCD_INIT`, is `(unsigned long)-1` -- an
|
|
* unbounded loop inside a frame. A pair that reaches this cap is reported as
|
|
* not colliding, which is the same answer a missed collision gives, but it
|
|
* terminates. Bounding the iterations also bounds what the arena can be asked
|
|
* for, since the polytope grows with them.
|
|
*/
|
|
#define AKGL_COLLISION_MAX_ITERATIONS 100
|
|
|
|
/** @brief An axis-aligned box, in the shape libccd's callbacks want. */
|
|
typedef struct {
|
|
ccd_vec3_t pos; /**< World centre. */
|
|
ccd_vec3_t half; /**< Half-extents on each axis. */
|
|
} collision_box;
|
|
|
|
/**
|
|
* @brief Furthest point of a box in a given direction.
|
|
*
|
|
* The support function *is* the shape as far as libccd is concerned: hand it a
|
|
* direction, get back the extreme point. For an axis-aligned box that is the
|
|
* corner in the direction's octant, which is three sign tests.
|
|
*
|
|
* There is no rotation here, and none anywhere in this library yet -- util.h
|
|
* says so and both example games depend on it. Adding it means rotating @p dir
|
|
* into the box's local frame at the top of this function and rotating the
|
|
* result back, and nothing else in the narrowphase changes. See TODO.md,
|
|
* "Actor rotation".
|
|
*/
|
|
static void collision_box_support(const void *obj, const ccd_vec3_t *dir, ccd_vec3_t *dest)
|
|
{
|
|
const collision_box *box = (const collision_box *)obj;
|
|
|
|
ccdVec3Set(dest,
|
|
ccdSign(ccdVec3X(dir)) * ccdVec3X(&box->half),
|
|
ccdSign(ccdVec3Y(dir)) * ccdVec3Y(&box->half),
|
|
ccdSign(ccdVec3Z(dir)) * ccdVec3Z(&box->half));
|
|
ccdVec3Add(dest, &box->pos);
|
|
}
|
|
|
|
/**
|
|
* @brief Centre of a box.
|
|
*
|
|
* MPR requires this and GJK does not: `ccdMPRPenetration` calls `center1` and
|
|
* `center2` unconditionally, so leaving either `NULL` is a null dereference on
|
|
* the first contact rather than a degraded answer.
|
|
*/
|
|
static void collision_box_center(const void *obj, ccd_vec3_t *dest)
|
|
{
|
|
const collision_box *box = (const collision_box *)obj;
|
|
|
|
ccdVec3Copy(dest, &box->pos);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_collision_arena_selftest(float32_t separation, bool *hit, float32_t *depth)
|
|
{
|
|
collision_box a;
|
|
collision_box b;
|
|
ccd_t ccd;
|
|
ccd_real_t ccddepth = 0.0;
|
|
ccd_vec3_t dir;
|
|
ccd_vec3_t pos;
|
|
int result = 0;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, hit, AKERR_NULLPOINTER, "NULL hit flag reference");
|
|
FAIL_ZERO_RETURN(errctx, depth, AKERR_NULLPOINTER, "NULL depth reference");
|
|
|
|
CCD_INIT(&ccd);
|
|
ccd.support1 = collision_box_support;
|
|
ccd.support2 = collision_box_support;
|
|
ccd.center1 = collision_box_center;
|
|
ccd.center2 = collision_box_center;
|
|
ccd.max_iterations = AKGL_COLLISION_MAX_ITERATIONS;
|
|
|
|
ccdVec3Set(&a.pos, 0.0, 0.0, 0.0);
|
|
ccdVec3Set(&a.half, 1.0, 1.0, 1.0);
|
|
ccdVec3Set(&b.pos, (ccd_real_t)separation, 0.0, 0.0);
|
|
ccdVec3Set(&b.half, 1.0, 1.0, 1.0);
|
|
|
|
/*
|
|
* The arena's lifetime is one query. Resetting on entry rather than on exit
|
|
* means a query that returns early still leaves the arena clean for the
|
|
* next one.
|
|
*/
|
|
akgl_ccd_arena_reset();
|
|
|
|
/*
|
|
* ccdGJKPenetration and not ccdMPRPenetration, deliberately. This is the
|
|
* EPA path, and EPA is the half of libccd that allocates -- so it is the
|
|
* half that proves the arena is wired up. A narrowphase on the frame path
|
|
* would prefer MPR, which allocates nothing at all.
|
|
*/
|
|
result = ccdGJKPenetration(&a, &b, &ccd, &ccddepth, &dir, &pos);
|
|
|
|
if ( result == -2 ) {
|
|
FAIL_RETURN(
|
|
errctx,
|
|
AKGL_ERR_COLLISION,
|
|
"Collision arena exhausted at %zu of %d bytes; raise AKGL_CCD_ARENA_BYTES",
|
|
akgl_ccd_arena_highwater(),
|
|
AKGL_CCD_ARENA_BYTES
|
|
);
|
|
}
|
|
|
|
*hit = (result == 0);
|
|
*depth = (float32_t)ccddepth;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|