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>
101 lines
5.2 KiB
C
101 lines
5.2 KiB
C
/**
|
|
* @file error.h
|
|
* @brief Declares the public error API.
|
|
*/
|
|
|
|
#ifndef _AKGL_ERROR_H_
|
|
#define _AKGL_ERROR_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
/*
|
|
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
|
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
|
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
|
* the library gained an soname -- so a translation unit that pairs this header
|
|
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
|
*
|
|
* libakerror publishes no version macro, so this feature-tests on
|
|
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
|
* version number that does not exist. Without the guard an embedded build is
|
|
* fine but a stale installed header fails much further in, on the AKGL_ERR_*
|
|
* codes below and again inside src/heap.c.
|
|
*
|
|
* See deps/libakerror/UPGRADING.md.
|
|
*/
|
|
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
|
#error "libakgl requires libakerror >= 2.0.1: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
|
#endif
|
|
// 2.0.0 is an ABI break -- akerr_next_error() returns a context that already
|
|
// holds its reference, and __akerr_last_ignored is thread-local -- and both of
|
|
// those expand at *this* library's call sites through IGNORE and the FAIL
|
|
// macros. A libakgl built against a 1.x header and linked against 2.x
|
|
// double-counts every reference and never returns a slot to the pool. The
|
|
// soname moved to libakerror.so.2 so the two cannot be mixed by accident, but
|
|
// the header can still be stale in an install tree, so check it here too.
|
|
//
|
|
// AKERR_EXIT_STATUS_UNREPRESENTABLE arrived in 2.0.1 and is the narrowest thing
|
|
// to probe for: libakerror publishes no version macro.
|
|
#ifndef AKERR_EXIT_STATUS_UNREPRESENTABLE
|
|
#error "libakgl requires libakerror >= 2.0.1: the akerror.h on the include path predates akerr_exit(). Rebuild and reinstall libakerror."
|
|
#endif
|
|
|
|
// Silences -Wformat-truncation on a string concatenation that genuinely may not
|
|
// fit -- e.g. joining two PATH_MAX strings into one AKGL_MAX_STRING_LENGTH
|
|
// buffer. The line has to be drawn somewhere or the buffers grow forever to
|
|
// keep the compiler happy.
|
|
//
|
|
// **Nothing in libakgl uses these any more.** Both sites -- the path join in
|
|
// akgl_path_relative and the tileset image join in
|
|
// akgl_tilemap_load_layer_image -- now go through aksl_snprintf, which raises
|
|
// AKERR_OUTOFBOUNDS on truncation instead. That is the better answer: the
|
|
// compiler was right both times, and silencing it left the truncation
|
|
// happening and unreported. Kept because they are public and a consumer may
|
|
// have them; see TODO.md.
|
|
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
|
|
_Pragma("GCC diagnostic push") \
|
|
_Pragma("GCC diagnostic ignored \"-Wformat-truncation\"")
|
|
|
|
#define RESTORE_GCC_WARNINGS \
|
|
_Pragma("GCC diagnostic pop")
|
|
|
|
// libakerror reserves statuses 0-255 for the host's errno values and its own
|
|
// AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
|
|
// These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
|
|
// so that a libc which grows an errno cannot move them out from under us.
|
|
//
|
|
// akgl_error_init() reserves this whole band in one call and registers a name
|
|
// for every code below. Add a code here and you must name it there, or it
|
|
// prints as "Unknown Error" in every stack trace that carries it.
|
|
#define AKGL_ERR_OWNER "libakgl"
|
|
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
|
|
|
|
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */
|
|
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */
|
|
#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 + 6)
|
|
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
|
|
|
/**
|
|
* @brief Claim the libakgl status band and register a name for every code in it.
|
|
*
|
|
* Call this before anything else in libakgl. Every other subsystem raises
|
|
* AKGL_ERR_* codes, and a code raised before this runs carries no name into its
|
|
* stack trace. Repeating the call is a no-op, so a program that cannot order its
|
|
* initialization precisely may call it more than once.
|
|
*
|
|
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
|
|
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
|
|
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void);
|
|
|
|
#endif // _AKGL_ERROR_H_
|