Give libccd a static arena so libakgl still allocates nothing
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>
This commit is contained in:
160
include/akgl/collision_arena.h
Normal file
160
include/akgl/collision_arena.h
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @file collision_arena.h
|
||||
* @brief The bump allocator libccd allocates from, so that libakgl does not.
|
||||
*
|
||||
* libakgl does not call `malloc` at runtime. Every object comes from a fixed
|
||||
* array declared in `akgl/heap.h`, and that is a promise the manual makes in
|
||||
* seven places and `AGENTS.md` makes as a rule. libccd's narrowphase does not
|
||||
* know that: its EPA path builds an expanding polytope out of `realloc` and
|
||||
* `free`, and `ccdGJKPenetration` documents a `-2` return for the case where
|
||||
* that fails.
|
||||
*
|
||||
* Rather than compile a hand-picked subset of libccd's sources -- which puts a
|
||||
* file list in this repository that really lives upstream, and rots silently on
|
||||
* the next bump -- the whole library is compiled and pointed at this arena.
|
||||
* `CMakeLists.txt` redirects `__CCD_ALLOC_MEMORY`, and `src/ccd_arena_shim.h`
|
||||
* redirects `free`, on libccd's translation units only.
|
||||
*
|
||||
* @section arena_lifetime The lifetime is one narrowphase call
|
||||
*
|
||||
* `akgl_collision_test` resets the arena on entry, so every allocation lives
|
||||
* exactly as long as the query that made it. libccd frees its polytope before
|
||||
* returning in any case, so nothing needs to survive, and that is what lets
|
||||
* `akgl_ccd_arena_free` be a no-op and the allocator be a pointer bump. There
|
||||
* is no free list, no coalescing and no fragmentation, because there is no
|
||||
* reuse within a call.
|
||||
*
|
||||
* @section arena_exhaustion Exhaustion is reported, not fatal
|
||||
*
|
||||
* A refused allocation returns `NULL`, which libccd already handles -- it
|
||||
* unwinds and returns `-2`, which `akgl_collision_test` turns into
|
||||
* #AKGL_ERR_COLLISION naming the high-water mark. A too-small arena is a loud
|
||||
* failure with a number in it, not a crash and not a silently missed collision.
|
||||
*
|
||||
* @warning **Not thread safe.** There is one arena and it has no lock. libakgl
|
||||
* serialises its frame behind `akgl_game.statelock`; a caller running
|
||||
* narrowphase queries from two threads needs a different design here,
|
||||
* not a mutex bolted on.
|
||||
*
|
||||
* @note These are exported so the test suite can check that libccd really is
|
||||
* allocating from here, and so a benchmark can record the high-water
|
||||
* mark. A game has no reason to call them.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_COLLISION_ARENA_H_
|
||||
#define _AKGL_COLLISION_ARENA_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/types.h>
|
||||
|
||||
/**
|
||||
* @brief Bytes of static storage the arena hands out.
|
||||
*
|
||||
* Sized by measurement rather than derivation: the benchmark records the
|
||||
* high-water mark and this is set above the worst observed case. It bounds
|
||||
* memory the same way `ccd_t::max_iterations` bounds time, and both must be
|
||||
* bounded -- libccd's own default for the latter is `(unsigned long)-1`.
|
||||
*/
|
||||
#ifndef AKGL_CCD_ARENA_BYTES
|
||||
#define AKGL_CCD_ARENA_BYTES (64 * 1024)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Allocate or grow a block, in the shape `realloc(3)` has.
|
||||
*
|
||||
* This is what `__CCD_ALLOC_MEMORY` expands to inside libccd. A `NULL` @p ptr
|
||||
* allocates; a non-`NULL` one allocates a new block and copies the old contents
|
||||
* across, because a bump allocator cannot grow in place unless the block is the
|
||||
* most recent one, and it is not worth a special case for a path that runs at
|
||||
* most a few times per query.
|
||||
*
|
||||
* @param ptr Block to grow, or `NULL` to allocate a new one.
|
||||
* @param size Bytes wanted. A @p size of 0 releases nothing and returns `NULL`,
|
||||
* matching what libccd does with the result.
|
||||
* @return A pointer into the arena, or `NULL` when the arena is full.
|
||||
*/
|
||||
void *akgl_ccd_arena_realloc(void *ptr, size_t size);
|
||||
|
||||
/**
|
||||
* @brief Release a block. A no-op, because the whole arena is released at once.
|
||||
*
|
||||
* @param ptr Block to release. `NULL` is accepted, as `free(3)` accepts it.
|
||||
*/
|
||||
void akgl_ccd_arena_free(void *ptr);
|
||||
|
||||
/** @brief Drop everything. Called at the top of each narrowphase query. */
|
||||
void akgl_ccd_arena_reset(void);
|
||||
|
||||
/**
|
||||
* @brief Bytes currently handed out.
|
||||
*
|
||||
* Zero after `akgl_ccd_arena_reset`, and non-zero after a narrowphase query
|
||||
* that allocated -- releasing a block does not lower it, because
|
||||
* akgl_ccd_arena_free is a no-op and the whole arena is released at once.
|
||||
*
|
||||
* Non-zero after a query is what proves libccd is allocating from here rather
|
||||
* than from the C library, which is worth checking rather than assuming: the
|
||||
* redirect lived in CMake's COMPILE_DEFINITIONS at first, where a function-like
|
||||
* macro cannot survive, and it was dropped without a diagnostic.
|
||||
*/
|
||||
size_t akgl_ccd_arena_used(void);
|
||||
|
||||
/** @brief The most bytes ever handed out at once, since the process started. */
|
||||
size_t akgl_ccd_arena_highwater(void);
|
||||
|
||||
/** @brief How many allocations have been refused for want of room. */
|
||||
uint32_t akgl_ccd_arena_exhausted(void);
|
||||
|
||||
/*
|
||||
* The following is part of the internal API. It is exposed so the test suite
|
||||
* can reach it and is not meant to be called by a game.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Run one narrowphase query through libccd's allocating path.
|
||||
*
|
||||
* Two boxes of half-extent 1 are placed @p separation apart on x and tested
|
||||
* with `ccdGJKPenetration` -- the EPA path, chosen because EPA is the half of
|
||||
* libccd that allocates and therefore the half that exercises the arena. A
|
||||
* narrowphase on the frame path prefers MPR, which allocates nothing.
|
||||
*
|
||||
* This exists so a test can reach two things nothing else can. That a real
|
||||
* libccd query allocates from the arena rather than the C library, which
|
||||
* akgl_ccd_arena_used() shows. And that no `free` inside libccd escapes to the
|
||||
* C library, which is shown by the process surviving: the real `free(3)` given
|
||||
* a pointer it never issued is undefined behaviour, and glibc aborts on it.
|
||||
*
|
||||
* @param separation Distance between the two box centres on x. Below 2.0 they
|
||||
* overlap; above it they do not.
|
||||
* @param hit Receives whether the boxes intersect. Required.
|
||||
* @param depth Receives the penetration depth, or 0 when they do not
|
||||
* intersect. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p hit or @p depth is `NULL`.
|
||||
* @throws AKGL_ERR_COLLISION If the arena ran out of room. The message carries
|
||||
* the high-water mark and the current ceiling.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_arena_selftest(float32_t separation, bool *hit, float32_t *depth);
|
||||
|
||||
/**
|
||||
* @brief Narrow the arena, so a test can drive a real query into exhaustion.
|
||||
*
|
||||
* The interesting failure is not that akgl_ccd_arena_realloc returns `NULL` --
|
||||
* that is one comparison. It is what happens afterwards: libccd has to notice,
|
||||
* unwind its half-built polytope, and return `-2`, and the caller has to turn
|
||||
* that into #AKGL_ERR_COLLISION rather than reporting a missed collision or
|
||||
* dereferencing the `NULL`. Reaching that path any other way means rebuilding
|
||||
* the library with a smaller #AKGL_CCD_ARENA_BYTES, which CI cannot do and a
|
||||
* reader cannot repeat.
|
||||
*
|
||||
* @param limit Usable bytes. 0, or anything above #AKGL_CCD_ARENA_BYTES,
|
||||
* restores the full arena.
|
||||
*/
|
||||
void akgl_ccd_arena_set_limit(size_t limit);
|
||||
|
||||
#endif
|
||||
@@ -75,11 +75,12 @@
|
||||
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
|
||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
||||
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
|
||||
|
||||
// One past the last libakgl status. The reservation is all-or-nothing -- a
|
||||
// subset or superset of an existing one is refused -- so this must stay one
|
||||
// past the highest code above.
|
||||
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 5)
|
||||
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 6)
|
||||
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user