Vendor clay and bring the UI subsystem up: arena, lifecycle, status band
clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -21,5 +21,6 @@ akerr_ErrorContext *akgl_error_init(void)
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_COLLISION, "Collision Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_UI, "UI Error"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
153
src/ui.c
Normal file
153
src/ui.c
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @file ui.c
|
||||
* @brief Implements the UI subsystem: clay's arena, lifecycle and error stash.
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akstdlib.h>
|
||||
#include <clay.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/ui.h>
|
||||
|
||||
/**
|
||||
* @brief Bytes of the first clay error's text kept for reporting.
|
||||
*
|
||||
* Sized to hold every message clay 0.14 actually emits -- they are one
|
||||
* sentence each -- with room for a longer one to truncate into rather than
|
||||
* overflow.
|
||||
*/
|
||||
#define UI_ERROR_TEXT_LENGTH 256
|
||||
|
||||
/** @brief The arena clay lays its structures out in. Static storage, so this file allocates nothing. */
|
||||
static uint8_t ui_arena_bytes[AKGL_UI_ARENA_BYTES];
|
||||
/** @brief Usable bytes. #AKGL_UI_ARENA_BYTES unless a test has narrowed it. */
|
||||
static size_t ui_arena_usable = AKGL_UI_ARENA_BYTES;
|
||||
/** @brief The context Clay_Initialize handed back, or NULL before init / after shutdown. */
|
||||
static Clay_Context *ui_context;
|
||||
|
||||
/**
|
||||
* @brief First error clay reported since the stash was last cleared.
|
||||
*
|
||||
* clay's error handler is a void callback with no way to refuse or return, so
|
||||
* a layout error cannot surface at the call that caused it -- it fires in the
|
||||
* middle of Clay_EndLayout with libakgl nowhere on the stack. The handler
|
||||
* stashes the first message here and akgl_ui_frame_end raises it, which is
|
||||
* the earliest point the error protocol can carry it. Later errors in the
|
||||
* same frame are counted but not kept: the first one is almost always the
|
||||
* cause and the rest its consequences.
|
||||
*/
|
||||
static char ui_clay_error_text[UI_ERROR_TEXT_LENGTH];
|
||||
/** @brief How many errors clay has reported since the stash was cleared. */
|
||||
static uint32_t ui_clay_error_count;
|
||||
|
||||
/**
|
||||
* @brief The error handler given to Clay_Initialize.
|
||||
*
|
||||
* Logs every report as it happens -- an operator watching the log should not
|
||||
* have to wait for frame_end -- and stashes the first for the error protocol.
|
||||
* Clay_String is a length and a pointer, not a C string, so the copy is
|
||||
* bounded by hand before aksl_strncpy sees it.
|
||||
*/
|
||||
static void ui_clay_error(Clay_ErrorData error)
|
||||
{
|
||||
size_t length = 0;
|
||||
|
||||
SDL_Log("clay: %.*s", (int)error.errorText.length, error.errorText.chars);
|
||||
ui_clay_error_count += 1;
|
||||
if ( ui_clay_error_count > 1 ) {
|
||||
return;
|
||||
}
|
||||
length = (size_t)error.errorText.length;
|
||||
if ( length > (sizeof(ui_clay_error_text) - 1) ) {
|
||||
length = sizeof(ui_clay_error_text) - 1;
|
||||
}
|
||||
IGNORE(aksl_strncpy(ui_clay_error_text, sizeof(ui_clay_error_text), error.errorText.chars, length));
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_init(int width, int height)
|
||||
{
|
||||
uint32_t needed = 0;
|
||||
Clay_Arena arena;
|
||||
Clay_ErrorHandler handler = { &ui_clay_error, NULL };
|
||||
Clay_Dimensions dimensions;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(errctx, (width <= 0), AKERR_OUTOFBOUNDS, "Layout width %d is not positive", width);
|
||||
FAIL_NONZERO_RETURN(errctx, (height <= 0), AKERR_OUTOFBOUNDS, "Layout height %d is not positive", height);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(ui_context != NULL),
|
||||
AKGL_ERR_UI,
|
||||
"The UI subsystem is already initialized; akgl_ui_shutdown first");
|
||||
|
||||
// Ceilings before measurement: Clay_MinMemorySize reports what the
|
||||
// *current* ceilings need, so they must be set first. Element count
|
||||
// before word count, and not the other way around -- with no context
|
||||
// yet, Clay_SetMaxElementCount also overwrites the word-cache default
|
||||
// with elements * 2, and would silently undo a word count set before it.
|
||||
Clay_SetMaxElementCount(AKGL_UI_MAX_ELEMENTS);
|
||||
Clay_SetMaxMeasureTextCacheWordCount(AKGL_UI_MAX_MEASURE_WORDS);
|
||||
needed = Clay_MinMemorySize();
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((size_t)needed > ui_arena_usable),
|
||||
AKGL_ERR_UI,
|
||||
"clay needs %u bytes for %d elements and %d measured words; the arena holds %u. Raise AKGL_UI_ARENA_BYTES.",
|
||||
needed,
|
||||
AKGL_UI_MAX_ELEMENTS,
|
||||
AKGL_UI_MAX_MEASURE_WORDS,
|
||||
(uint32_t)ui_arena_usable);
|
||||
|
||||
ui_clay_error_text[0] = '\0';
|
||||
ui_clay_error_count = 0;
|
||||
arena = Clay_CreateArenaWithCapacityAndMemory(ui_arena_usable, ui_arena_bytes);
|
||||
dimensions.width = (float)width;
|
||||
dimensions.height = (float)height;
|
||||
ui_context = Clay_Initialize(arena, dimensions, handler);
|
||||
// The capacity check above is the one that should fail; this one exists
|
||||
// because Clay_Initialize reports refusal by returning NULL and a NULL
|
||||
// context would otherwise surface as a crash inside the first CLAY()
|
||||
// block, far from the cause.
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "Clay_Initialize refused: %s", ui_clay_error_text);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_shutdown(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
// clay holds no resource but the arena, and the arena is ours: there is
|
||||
// nothing to hand back, so shutdown is forgetting. The next init lays a
|
||||
// fresh context over the same bytes.
|
||||
ui_context = NULL;
|
||||
ui_clay_error_text[0] = '\0';
|
||||
ui_clay_error_count = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_resize(int width, int height)
|
||||
{
|
||||
Clay_Dimensions dimensions;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "The UI subsystem is not initialized");
|
||||
FAIL_NONZERO_RETURN(errctx, (width <= 0), AKERR_OUTOFBOUNDS, "Layout width %d is not positive", width);
|
||||
FAIL_NONZERO_RETURN(errctx, (height <= 0), AKERR_OUTOFBOUNDS, "Layout height %d is not positive", height);
|
||||
dimensions.width = (float)width;
|
||||
dimensions.height = (float)height;
|
||||
Clay_SetLayoutDimensions(dimensions);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
void akgl_ui_arena_limit(size_t limit)
|
||||
{
|
||||
if ( (limit == 0) || (limit > AKGL_UI_ARENA_BYTES) ) {
|
||||
ui_arena_usable = AKGL_UI_ARENA_BYTES;
|
||||
} else {
|
||||
ui_arena_usable = limit;
|
||||
}
|
||||
}
|
||||
21
src/ui_clay.c
Normal file
21
src/ui_clay.c
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @file ui_clay.c
|
||||
* @brief The one translation unit that compiles clay's implementation.
|
||||
*
|
||||
* clay is a single-header library: every other file that includes <clay.h>
|
||||
* gets declarations only, and exactly one file in the whole program defines
|
||||
* CLAY_IMPLEMENTATION before the include to get the definitions. This is that
|
||||
* file, and it must stay the only one -- a second definition anywhere,
|
||||
* including in a consuming game, is two copies of clay's globals and the
|
||||
* loader picking between them silently. The warning in akgl/ui.h says so to
|
||||
* consumers; this comment says so to us.
|
||||
*
|
||||
* Nothing else belongs here. libakgl's own UI code is src/ui.c, which
|
||||
* includes <clay.h> like any other consumer. CMakeLists.txt compiles this
|
||||
* file with -w on the same terms as deps/semver and deps/libccd: 99% of what
|
||||
* this TU contains is vendored code, and a future clay bump must not be able
|
||||
* to fail the build on a warning we do not own.
|
||||
*/
|
||||
|
||||
#define CLAY_IMPLEMENTATION
|
||||
#include <clay.h>
|
||||
Reference in New Issue
Block a user