/** * @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 #include #include #include #include /** * @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