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:
@@ -200,6 +200,7 @@ set(AKGL_PUBLIC_HEADERS
|
||||
assets
|
||||
audio
|
||||
character
|
||||
collision_arena
|
||||
controller
|
||||
draw
|
||||
error
|
||||
@@ -255,11 +256,46 @@ add_custom_target(${AKGL_CONTROLLERDB_TARGET}
|
||||
COMMENT "Fetching controller mappings and rewriting ${GAMECONTROLLERDB_H} ..."
|
||||
)
|
||||
|
||||
# libccd supplies the collision narrowphase (GJK, EPA and MPR).
|
||||
#
|
||||
# Its sources are listed here rather than add_subdirectory()'d. Four things in
|
||||
# deps/libccd/CMakeLists.txt make it unusable as a subproject, and none of them
|
||||
# is ours to fix in a submodule:
|
||||
#
|
||||
# - project(libccd) declares no LANGUAGES, so it enables CXX and makes a C++
|
||||
# toolchain a requirement of every build of libakgl. CI installs gcc.
|
||||
# - it declares option(BUILD_SHARED_LIBS ... ON), a global cache variable, and
|
||||
# calls include(CTest), which would register its own suites into ours and
|
||||
# build the LGPL-licensed CU framework under src/testsuites/.
|
||||
# - install(FILES "${CMAKE_BINARY_DIR}/ccd.pc") reads the *top level* binary
|
||||
# dir while configure_file writes CMAKE_CURRENT_BINARY_DIR. Those are the
|
||||
# same directory standalone and different ones embedded, so `cmake --install`
|
||||
# fails on a missing file.
|
||||
# - cmake_minimum_required(VERSION 2.8.11) warns on CMake 3.28 and is a hard
|
||||
# error on CMake 4.
|
||||
#
|
||||
# Compiling the sources directly, the way deps/semver/semver.c already is, sidesteps
|
||||
# all four and never configures the LGPL testsuites at all.
|
||||
set(CCD_SINGLE 1)
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/deps/libccd/src/ccd/config.h.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/ccd/config.h)
|
||||
|
||||
set(AKGL_CCD_SOURCES
|
||||
deps/libccd/src/ccd.c
|
||||
deps/libccd/src/mpr.c
|
||||
deps/libccd/src/polytope.c
|
||||
deps/libccd/src/support.c
|
||||
deps/libccd/src/vec3.c)
|
||||
|
||||
# Add include directories
|
||||
include_directories(${SDL3_INCLUDE_DIRS})
|
||||
add_library(akgl SHARED
|
||||
deps/semver/semver.c
|
||||
${AKGL_CCD_SOURCES}
|
||||
src/actor.c
|
||||
src/collision.c
|
||||
src/collision_arena.c
|
||||
src/actor_state_string_names.c
|
||||
src/audio.c
|
||||
src/text.c
|
||||
@@ -304,6 +340,38 @@ target_compile_options(akgl PRIVATE ${AKGL_WARNING_FLAGS})
|
||||
# akerror or akstdlib -- those are separate targets.)
|
||||
set_source_files_properties(deps/semver/semver.c PROPERTIES COMPILE_OPTIONS "-w")
|
||||
|
||||
# libccd is vendored on the same terms as semver: -w so a future bump cannot fail
|
||||
# this build on a warning we do not own.
|
||||
#
|
||||
# src/ccd_arena_shim.h is what keeps the "libakgl does not call malloc at
|
||||
# runtime" promise true with the whole of libccd compiled in. Injected with
|
||||
# -include, it redirects both halves of libccd's allocation -- the
|
||||
# __CCD_ALLOC_MEMORY macro every CCD_ALLOC* expands to, and the four bare free()
|
||||
# calls -- at the arena in src/collision_arena.c. It applies to these five files
|
||||
# and nothing else.
|
||||
#
|
||||
# The redirect lives in that header rather than in COMPILE_DEFINITIONS because
|
||||
# CMake cannot carry a function-like macro through a -D: it dropped it silently,
|
||||
# and libccd went on calling the C library while free() became a no-op. Do not
|
||||
# move it back.
|
||||
#
|
||||
# CCD_STATIC_DEFINE plus -fvisibility=hidden keeps every ccd* symbol out of
|
||||
# libakgl.so's dynamic table. That is not tidiness: AGENTS.md records a shipped
|
||||
# defect where an exported `renderer` in this library was preempted by a
|
||||
# same-named symbol in a test executable, and a game linking both libakgl and a
|
||||
# system libccd would hit exactly that.
|
||||
set_source_files_properties(${AKGL_CCD_SOURCES} PROPERTIES
|
||||
COMPILE_OPTIONS "-w;-fvisibility=hidden;-include;${CMAKE_CURRENT_SOURCE_DIR}/src/ccd_arena_shim.h"
|
||||
COMPILE_DEFINITIONS "CCD_STATIC_DEFINE")
|
||||
|
||||
# PRIVATE, so that ccd/*.h never reaches a consumer's include path. The `headers`
|
||||
# suite compiles each public header as the first include of a translation unit
|
||||
# with only include/ available, so a public header that pulled in <ccd/ccd.h>
|
||||
# would fail there immediately.
|
||||
target_include_directories(akgl PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deps/libccd/src"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
add_library(akgl::akgl ALIAS akgl)
|
||||
|
||||
add_executable(charviewer util/charviewer.c)
|
||||
@@ -316,6 +384,7 @@ set(AKGL_TEST_SUITES
|
||||
audio
|
||||
bitmasks
|
||||
character
|
||||
collision_arena
|
||||
controller
|
||||
draw
|
||||
error
|
||||
|
||||
Reference in New Issue
Block a user