279 lines
10 KiB
C
279 lines
10 KiB
C
|
|
/**
|
||
|
|
* @file collision_arena.c
|
||
|
|
* @brief The bump allocator libccd allocates from, and the proof it is wired up.
|
||
|
|
*
|
||
|
|
* Two things have to be true and they are proved differently.
|
||
|
|
*
|
||
|
|
* **libccd allocates from the arena and not from the C library.** Proved by
|
||
|
|
* `akgl_ccd_arena_used()` being non-zero after a query that allocated. Before
|
||
|
|
* the redirect was moved out of CMake's COMPILE_DEFINITIONS -- which cannot
|
||
|
|
* carry a function-like macro and dropped it without a word -- this read zero
|
||
|
|
* while libccd happily called the real `realloc`, so it is not a formality.
|
||
|
|
*
|
||
|
|
* **No `free()` inside libccd reaches the C library.** Proved by the process
|
||
|
|
* still being alive. Handing an arena pointer to the real `free(3)` is
|
||
|
|
* undefined behaviour, and glibc aborts on it rather than tolerating it, so a
|
||
|
|
* suite that runs to completion after a query that allocated has demonstrated
|
||
|
|
* the interception. There is no balance to check: `akgl_ccd_arena_free` is a
|
||
|
|
* no-op by design, because the whole arena is released at once.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akgl/collision_arena.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
akerr_ErrorContext *test_arena_bump_and_reset(void)
|
||
|
|
{
|
||
|
|
void *a = NULL;
|
||
|
|
void *b = NULL;
|
||
|
|
size_t used = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() == 0), "a reset arena is not empty");
|
||
|
|
|
||
|
|
a = akgl_ccd_arena_realloc(NULL, 64);
|
||
|
|
TEST_ASSERT(errctx, (a != NULL), "a 64 byte allocation from an empty arena failed");
|
||
|
|
used = akgl_ccd_arena_used();
|
||
|
|
TEST_ASSERT(errctx, (used >= 64), "64 bytes were handed out but only %zu accounted for", used);
|
||
|
|
|
||
|
|
b = akgl_ccd_arena_realloc(NULL, 64);
|
||
|
|
TEST_ASSERT(errctx, (b != NULL), "a second allocation failed");
|
||
|
|
TEST_ASSERT(errctx, (b != a), "two allocations returned the same block");
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() > used), "a second allocation did not advance the arena");
|
||
|
|
|
||
|
|
// Every block is aligned, because libccd allocates structures of doubles
|
||
|
|
// and pointers and this allocator does not know which.
|
||
|
|
TEST_ASSERT(errctx, ((((uintptr_t)a) % 16) == 0), "block a is not 16 byte aligned");
|
||
|
|
TEST_ASSERT(errctx, ((((uintptr_t)b) % 16) == 0), "block b is not 16 byte aligned");
|
||
|
|
|
||
|
|
// Freeing is a no-op by design: the whole arena goes at once.
|
||
|
|
akgl_ccd_arena_free(a);
|
||
|
|
akgl_ccd_arena_free(NULL);
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() > 0), "free() released a block it should have ignored");
|
||
|
|
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() == 0), "reset did not empty the arena");
|
||
|
|
} CLEANUP {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *test_arena_realloc_preserves_contents(void)
|
||
|
|
{
|
||
|
|
unsigned char *block = NULL;
|
||
|
|
unsigned char *grown = NULL;
|
||
|
|
int i = 0;
|
||
|
|
bool intact = true;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
block = (unsigned char *)akgl_ccd_arena_realloc(NULL, 32);
|
||
|
|
TEST_ASSERT(errctx, (block != NULL), "the initial allocation failed");
|
||
|
|
for ( i = 0; i < 32; i++ ) {
|
||
|
|
block[i] = (unsigned char)(i + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
grown = (unsigned char *)akgl_ccd_arena_realloc(block, 128);
|
||
|
|
TEST_ASSERT(errctx, (grown != NULL), "growing a block failed");
|
||
|
|
for ( i = 0; i < 32; i++ ) {
|
||
|
|
if ( grown[i] != (unsigned char)(i + 1) ) {
|
||
|
|
intact = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
TEST_ASSERT(errctx, (intact == true), "growing a block did not carry its contents across");
|
||
|
|
|
||
|
|
// Shrinking copies only what fits, and must not read past the source.
|
||
|
|
grown = (unsigned char *)akgl_ccd_arena_realloc(grown, 16);
|
||
|
|
TEST_ASSERT(errctx, (grown != NULL), "shrinking a block failed");
|
||
|
|
TEST_ASSERT(errctx, (grown[0] == 1), "shrinking a block lost its contents");
|
||
|
|
|
||
|
|
// A zero-size request is not an allocation.
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_realloc(NULL, 0) == NULL),
|
||
|
|
"a zero byte allocation returned a block");
|
||
|
|
} CLEANUP {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *test_arena_exhaustion_refuses(void)
|
||
|
|
{
|
||
|
|
void *block = NULL;
|
||
|
|
uint32_t before = 0;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
before = akgl_ccd_arena_exhausted();
|
||
|
|
|
||
|
|
// A single request larger than the whole arena is refused rather than
|
||
|
|
// satisfied by running off the end of the static array.
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_realloc(NULL, AKGL_CCD_ARENA_BYTES * 2) == NULL),
|
||
|
|
"an allocation larger than the arena was satisfied");
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_exhausted() > before),
|
||
|
|
"a refused allocation was not counted");
|
||
|
|
|
||
|
|
// And filling it a block at a time ends in a refusal, not a corruption.
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
for ( i = 0; i < ((AKGL_CCD_ARENA_BYTES / 1024) + 8); i++ ) {
|
||
|
|
block = akgl_ccd_arena_realloc(NULL, 1024);
|
||
|
|
if ( block == NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
TEST_ASSERT(errctx, (block == NULL), "the arena never refused, so it never filled");
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() <= AKGL_CCD_ARENA_BYTES),
|
||
|
|
"the arena handed out %zu bytes of a %d byte pool",
|
||
|
|
akgl_ccd_arena_used(), AKGL_CCD_ARENA_BYTES);
|
||
|
|
} CLEANUP {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A real libccd query, and the balance that proves the shim is complete.
|
||
|
|
*
|
||
|
|
* `akgl_collision_arena_selftest` runs `ccdGJKPenetration`, which is the EPA
|
||
|
|
* path and therefore the allocating one. If any of libccd's four bare `free()`
|
||
|
|
* calls had escaped `src/ccd_arena_shim.h` it would have been handed an arena
|
||
|
|
* pointer, and glibc would have aborted rather than returned -- so reaching the
|
||
|
|
* assertion at all is half the proof. The other half is the balance: the arena
|
||
|
|
* must be empty afterwards, because libccd frees its polytope before returning
|
||
|
|
* and every one of those frees is a no-op that leaves `arena_used` alone only
|
||
|
|
* if it reached ours.
|
||
|
|
*/
|
||
|
|
akerr_ErrorContext *test_arena_drives_a_real_query(void)
|
||
|
|
{
|
||
|
|
bool hit = false;
|
||
|
|
float32_t depth = 0.0f;
|
||
|
|
size_t highwater = 0;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
|
||
|
|
// Half-extent 1 each, centres 1 apart: overlapping by 1 on x.
|
||
|
|
CATCH(errctx, akgl_collision_arena_selftest(1.0f, &hit, &depth));
|
||
|
|
TEST_ASSERT(errctx, (hit == true), "two overlapping boxes were reported as not colliding");
|
||
|
|
TEST_ASSERT_FEQ(errctx, depth, 1.0f, "overlapping boxes penetrate by %f, expected 1", depth);
|
||
|
|
|
||
|
|
highwater = akgl_ccd_arena_highwater();
|
||
|
|
TEST_ASSERT(errctx, (highwater > 0),
|
||
|
|
"a GJK/EPA query allocated nothing, so it did not reach the arena");
|
||
|
|
/*
|
||
|
|
* Printed because it is the number AKGL_CCD_ARENA_BYTES is sized from,
|
||
|
|
* and because it is only meaningful before the exhaustion case
|
||
|
|
* deliberately fills the arena -- the high-water mark is never reset.
|
||
|
|
*/
|
||
|
|
printf("one GJK/EPA box pair: high-water %zu bytes of %d\n",
|
||
|
|
highwater, AKGL_CCD_ARENA_BYTES);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The arena is what libccd allocated from. If the __CCD_ALLOC_MEMORY
|
||
|
|
* redirect were missing -- as it was, silently, while it lived in
|
||
|
|
* CMake's COMPILE_DEFINITIONS -- this would be zero and libccd would be
|
||
|
|
* calling the C library instead.
|
||
|
|
*
|
||
|
|
* That the process is still running is the other half: every free()
|
||
|
|
* inside libccd was handed an arena pointer, and glibc aborts when the
|
||
|
|
* real free(3) is given one it did not issue.
|
||
|
|
*/
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() > 0),
|
||
|
|
"a query allocated nothing from the arena, so libccd is not using it");
|
||
|
|
|
||
|
|
// Boxes well apart do not collide, and still leave the arena balanced.
|
||
|
|
CATCH(errctx, akgl_collision_arena_selftest(8.0f, &hit, &depth));
|
||
|
|
TEST_ASSERT(errctx, (hit == false), "two separated boxes were reported as colliding");
|
||
|
|
|
||
|
|
// Exactly touching. Degenerate configurations are where a narrowphase
|
||
|
|
// divides by zero, and the libccd revision pinned here (v2.1) is the one
|
||
|
|
// that fixed exactly that in ccdMPRPenetration.
|
||
|
|
CATCH(errctx, akgl_collision_arena_selftest(2.0f, &hit, &depth));
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Each query resets the arena on entry, so a thousand of them in a row
|
||
|
|
* cost what one costs. Without that, a no-op free and a bump allocator
|
||
|
|
* would fill 64 KB in nine queries.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < 64; i++ ) {
|
||
|
|
CATCH(errctx, akgl_collision_arena_selftest(1.0f, &hit, &depth));
|
||
|
|
}
|
||
|
|
TEST_ASSERT(errctx, (akgl_ccd_arena_used() <= highwater),
|
||
|
|
"64 queries used %zu bytes where one used %zu; the arena is not being reset",
|
||
|
|
akgl_ccd_arena_used(), highwater);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Starve a real query and require it to report. This is the path that
|
||
|
|
* matters and the one a unit test of the allocator cannot reach: libccd
|
||
|
|
* has to notice the NULL, unwind its half-built polytope, and return -2,
|
||
|
|
* and the caller has to turn that into AKGL_ERR_COLLISION rather than
|
||
|
|
* quietly reporting "no collision" -- which would be a floor an actor
|
||
|
|
* falls through, reported as success.
|
||
|
|
*/
|
||
|
|
akgl_ccd_arena_set_limit(32);
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKGL_ERR_COLLISION,
|
||
|
|
akgl_collision_arena_selftest(1.0f, &hit, &depth),
|
||
|
|
"a query against a starved arena");
|
||
|
|
akgl_ccd_arena_set_limit(0);
|
||
|
|
|
||
|
|
// And the full arena still works afterwards, so the limit is a knob and
|
||
|
|
// not a one-way door.
|
||
|
|
CATCH(errctx, akgl_collision_arena_selftest(1.0f, &hit, &depth));
|
||
|
|
TEST_ASSERT(errctx, (hit == true), "the query failed after the arena was restored");
|
||
|
|
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||
|
|
akgl_collision_arena_selftest(1.0f, NULL, &depth),
|
||
|
|
"the selftest with a NULL hit flag");
|
||
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||
|
|
akgl_collision_arena_selftest(1.0f, &hit, NULL),
|
||
|
|
"the selftest with a NULL depth");
|
||
|
|
} CLEANUP {
|
||
|
|
akgl_ccd_arena_set_limit(0);
|
||
|
|
akgl_ccd_arena_reset();
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_error_init());
|
||
|
|
CATCH(errctx, test_arena_bump_and_reset());
|
||
|
|
CATCH(errctx, test_arena_realloc_preserves_contents());
|
||
|
|
/*
|
||
|
|
* The real query runs before the exhaustion case, so the high-water mark
|
||
|
|
* it prints is what a narrowphase query actually costs rather than what
|
||
|
|
* a test deliberately filling the arena costs.
|
||
|
|
*/
|
||
|
|
CATCH(errctx, test_arena_drives_a_real_query());
|
||
|
|
CATCH(errctx, test_arena_exhaustion_refuses());
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH_NORETURN(errctx);
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|