Inital context stuff and tests

Linking and  building is successfull and inital tests pass
This commit is contained in:
chrisbell committed 2026-11-02 14:16:16 -06:00
1 parent 2296f68123
commit 363e9894e8
10 files changed
+410 -59

No files matched your search

+3
View File
@@ -3,3 +3,6 @@
docs/local/
build/
build-wasm/
compile_commands.json
/.emscripten-cache
+57 -28
View File
@@ -72,9 +72,20 @@ color emoji are out of scope for the first versions.
## Rendering
Rectangles, borders, and glyphs all reduce to quads. A backend only needs to
manage textures, draw quad batches, and apply a scissor rect. Rendering
backends are replaceable, and the SDL3 and OpenGL backend is the default.
The core never calls a backend. Each frame `rg_render` returns a draw list
made of texture operations (create, update region, destroy) followed by
batches of colored and textured quads with scissor rects. A backend is any
code that consumes that list. The SDL3 and OpenGL backend is the default, and
the null backend records the list for headless tests. A WASM host can consume
the list from JavaScript. The shipped backends share a small convenience
interface, and the core does not depend on it.
## Viewport and scale
All API coordinates are logical pixels. The host calls
`rg_set_viewport(ctx, w, h, scale)` when the window resizes or the display
scale changes. A scale change invalidates the glyph atlas and glyphs are
rasterized again at the new size.
## Documents
@@ -144,27 +155,6 @@ across the FFI boundary.
never by raw pointers.
- The user can supply an allocator, and internal allocation uses arenas.
## Build and environment
The build system is CMake with Ninja. Development happens on NixOS using
direnv and flakes, and the devShell provides CMake, Ninja, pkg-config, clang,
clang-tools, SDL3, Emscripten, and lua-language-server. Emscripten's cache
directory is set to `.emscripten-cache` inside the project. WASM builds go
through `emcmake` into `build-wasm/`. Sources live in a flat `src/` with no
generated files, so Zig and Odin users can compile them with their own
toolchain.
## Milestone 1
A `main.lua` builds a `VBox` of `Label` and `Button` nodes. A stylesheet with
classes and a `:hover` state styles them. Layout runs through Clay and draws
through the SDL3 and OpenGL backend with stb_truetype text. A button click
fires both a Lua handler and a named event in the polled queue. One
transition eases a button background on hover. A data-bound label shows a
counter that the button increments.
Text inputs and scrolling follow in milestone 2, along with focus handling.
## Context and ownership
All state lives in an `rg_context`, which is created with `rg_create` and
@@ -194,8 +184,47 @@ Gamepad navigation will be added as a semantic layer (up, down, left, right,
accept, cancel) that drives focus movement, so a game can map any device to
it.
## Open questions
## Errors and logging
- Whether texture uploads travel inside the draw list.
- Viewport and display scale handling.
- Error reporting and logging.
Functions that can fail return an `int` status where zero is success and
negative values are errors. `rg_last_error(ctx)` returns the message for the
most recent failure on that context. The config carries a log callback, a log
level, and a userdata pointer. The default callback writes to stderr and the
default level is trace, so logging is verbose until I lower it. Lua errors
such as syntax errors and handler failures are logged and never replace the
live tree, so a broken save during hot reload leaves the last good tree on
screen.
## Testing
`ctest` runs the smoke tests. The core smoke test installs a counting
allocator and fails if any bytes are live after `rg_destroy`. Every
allocation, including Lua's, goes through the context allocator.
### Build and environment
The build system is CMake with Ninja. Development happens on NixOS using
direnv and flakes, and the devShell provides CMake, Ninja, pkg-config, clang,
clang-tools, SDL3, Emscripten, and lua-language-server. Emscripten's cache
directory is set to `.emscripten-cache` inside the project. WASM builds go
through `emcmake` into `build-wasm/`. Sources live in a flat `src/` with no
generated files, so Zig and Odin users can compile them with their own
toolchain.
### Milestone 1
A `main.lua` builds a `VBox` of `Label` and `Button` nodes. A stylesheet with
classes and a `:hover` state styles them. Layout runs through Clay and draws
through the SDL3 and OpenGL backend with stb_truetype text. A button click
fires both a Lua handler and a named event in the polled queue. One
transition eases a button background on hover. A data-bound label shows a
counter that the button increments.
Text inputs and scrolling follow in milestone 2, along with focus handling.
### Open questions
- Node handle format.
- Property system: how properties are named, typed, and stored.
+28 -31
View File
@@ -6,24 +6,22 @@
Last edit date: 11-02-2026
*/
#ifndef REGOLITH_H
#define REGOLITH_H
#include <cstddef>
#include <cstdint>
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32) && defined(RG_SHARED)
#ifdef RG_BUILD
#define RG_API __declspec(dllexport)
#else
#define RG_API __declspec(dllimport)
#endif
#elif defined(__GNUC__)
#define RG_API __attribute__((visibility("default")))
#ifdef RG_BUILD
#define RG_API __declspec(dllexport)
#else
#define RG_API
#define RG_API __declspec(dllimport)
#endif
#elif defined(__GNUC__)
#define RG_API __attribute__((visibility("default")))
#else
#define RG_API
#endif
#ifdef __cplusplus
@@ -32,26 +30,27 @@ extern "C" {
#define RG_VERSION_STRING "0.0.1"
/*
TODO: Add documentation comments
*/
enum {
RG_LOG_TRACE = 0,
RG_LOG_DEBUG,
RG_LOG_INFO,
RG_LOG_WARN,
RG_LOG_ERROR
RG_OK = 0,
RG_ERR_GENERIC = -1,
RG_ERR_NO_MEMORY = -2,
RG_ERR_STATE = -3,
RG_ERR_LUA = -4
};
enum { RG_LOG_TRACE = 0, RG_LOG_DEBUG, RG_LOG_INFO, RG_LOG_WARN, RG_LOG_ERROR };
typedef struct rg_context rg_context;
typedef struct rg_allocator {
void* (*alloc)(size_t size, void* user);
void* (*realloc)(void* ptr, size_t old_size, size_t new_size, void* user);
void* (*free)(void* ptr, size_t size, void* user);
void* user;
void *(*alloc)(size_t size, void *user);
void *(*realloc)(void *ptr, size_t old_size, size_t new_size, void *user);
void (*free)(void *ptr, size_t size, void *user);
void *user;
} rg_allocator;
typedef void (*rg_log_fn)(int32_t level, const char *message, void *user);
@@ -60,20 +59,18 @@ typedef struct rg_config {
uint32_t struct_size;
rg_allocator allocator;
rg_log_fn log;
void* log_user;
void *log_user;
int32_t log_level;
int32_t viewport_w;
int32_t viewport_h;
float display_scale;
} rg_config;
RG_API const char* rg_version(void);
RG_API void rg_config_init(rg_config* config);
RG_API rg_context* rg_create(const rg_config* config);
RG_API void rg_destroy(rg_context* ctx);
RG_API const char* rg_last_error(const rg_context* ctx);
RG_API const char *rg_version(void);
RG_API void rg_config_init(rg_config *config);
RG_API rg_context *rg_create(const rg_config *config);
RG_API void rg_destroy(rg_context *ctx);
RG_API const char *rg_last_error(const rg_context *ctx);
#ifdef __cplusplus
}
+122
View File
@@ -0,0 +1,122 @@
#include "internal.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void *default_alloc(size_t size, void *user) {
(void)user;
return malloc(size);
}
static void *default_realloc(void *ptr, size_t old_size, size_t new_size,
void *user) {
(void)old_size;
(void)user;
return realloc(ptr, new_size);
}
static void default_free(void *ptr, size_t size, void *user) {
(void)size;
(void)user;
free(ptr);
}
static void default_log(int32_t level, const char *message, void *user) {
static const char *names[] = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR"};
(void)user;
fprintf(stderr, "[regolith %s] %s\n", names[level], message);
}
const char *rg_version(void) { return RG_VERSION_STRING; }
void rg_config_init(rg_config *config) {
memset(config, 0, sizeof *config);
config->struct_size = (uint32_t)sizeof *config;
config->allocator.alloc = default_alloc;
config->allocator.realloc = default_realloc;
config->allocator.free = default_free;
config->log = default_log;
config->log_level = RG_LOG_TRACE;
config->viewport_w = 1280;
config->viewport_h = 720;
config->display_scale = 1.0f;
}
void rg__log(rg_context *ctx, int32_t level, const char *fmt, ...) {
char buf[512];
va_list ap;
if (level < ctx->log_level)
return;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
ctx->log(level, buf, ctx->log_user);
}
int rg__fail(rg_context *ctx, int code, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(ctx->last_error, sizeof ctx->last_error, fmt, ap);
va_end(ap);
rg__log(ctx, RG_LOG_ERROR, "%s", ctx->last_error);
return code;
}
const char *rg_last_error(const rg_context *ctx) { return ctx->last_error; }
rg_context *rg_create(const rg_config *config) {
rg_config cfg;
rg_context *ctx;
size_t n;
rg_config_init(&cfg);
if (config) {
n = config->struct_size < sizeof cfg ? config->struct_size : sizeof cfg;
memcpy(&cfg, config, n);
}
if (!cfg.allocator.alloc || !cfg.allocator.realloc || !cfg.allocator.free) {
rg_config defaults;
rg_config_init(&defaults);
cfg.allocator = defaults.allocator;
}
if (!cfg.log)
cfg.log = default_log;
if (cfg.display_scale <= 0.0f)
cfg.display_scale = 1.0f;
ctx = cfg.allocator.alloc(sizeof *ctx, cfg.allocator.user);
if (!ctx)
return NULL;
memset(ctx, 0, sizeof *ctx);
ctx->alloc = cfg.allocator;
ctx->log = cfg.log;
ctx->log_user = cfg.log_user;
ctx->log_level = cfg.log_level;
ctx->viewport_w = cfg.viewport_w;
ctx->viewport_h = cfg.viewport_h;
ctx->display_scale = cfg.display_scale;
rg__log(ctx, RG_LOG_DEBUG, "clay minimum memory: %zu bytes",
rg__clay_min_memory());
rg__log(ctx, RG_LOG_INFO, "context created (%dx%d @ %.2fx)",
(int)ctx->viewport_w, (int)ctx->viewport_h,
(double)ctx->display_scale);
return ctx;
}
void rg_destroy(rg_context *ctx) {
rg_allocator a;
if (!ctx)
return;
rg__lua_close(ctx);
rg__log(ctx, RG_LOG_INFO, "context destroyed");
a = ctx->alloc;
a.free(ctx, sizeof *ctx, a.user);
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef RG_INTERNAL_H
#define RG_INTERNAL_H
#include "../include/regolith.h"
#if defined(__GNUC__)
#define RG_PRINTF(f, a) __attribute__((format(printf, f, a)))
#else
#define RG_PRINTF(f, a)
#endif
struct lua_State;
struct rg_context {
rg_allocator alloc;
rg_log_fn log;
void* log_user;
int32_t log_level;
int32_t viewport_w;
int32_t viewport_h;
float display_scale;
char last_error[256];
struct lua_State* L;
};
void rg__log(rg_context* ctx, int32_t level, const char* fmt, ...) RG_PRINTF(3, 4);
int rg__fail(rg_context* ctx, int code, const char* fmt, ...) RG_PRINTF(3, 4);
size_t rg__clay_min_memory(void);
void rg__lua_close(rg_context* ctx);
#endif
+10
View File
@@ -0,0 +1,10 @@
#define CLAY_IMPLEMENTATION
#include "../third_party/clay/clay.h"
#include "internal.h"
size_t rg__clay_min_memory(void)
{
return Clay_MinMemorySize();
}
+72
View File
@@ -0,0 +1,72 @@
#include "internal.h"
#include "../include/regolith_lua.h"
#include <string.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
static void *lua_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
{
rg_context *ctx = ud;
rg_allocator *a = &ctx->alloc;
if (nsize == 0) {
if (ptr) a->free(ptr, osize, a->user);
return NULL;
}
if (!ptr) return a->alloc(nsize, a->user);
return a->realloc(ptr, osize, nsize, a->user);
}
static int on_panic(lua_State *L)
{
void *ud;
const char *msg = lua_tostring(L, -1);
lua_getallocf(L, &ud);
rg__log(ud, RG_LOG_ERROR, "lua panic: %s", msg ? msg : "unknown");
return 0;
}
int rg_lua_open(rg_context *ctx)
{
lua_State *L;
if (ctx->L) return RG_OK;
L = lua_newstate(lua_alloc, ctx);
if (!L) return rg__fail(ctx, RG_ERR_NO_MEMORY, "could not create lua state");
lua_atpanic(L, on_panic);
luaL_openlibs(L);
ctx->L = L;
rg__log(ctx, RG_LOG_INFO, "lua opened (%s)", LUA_VERSION);
return RG_OK;
}
void rg__lua_close(rg_context *ctx)
{
if (!ctx->L) return;
lua_close(ctx->L);
ctx->L = NULL;
}
int rg_lua_dostring(rg_context *ctx, const char *code, const char *chunk_name)
{
lua_State *L = ctx->L;
if (!L) return rg__fail(ctx, RG_ERR_STATE, "lua is not open");
if (luaL_loadbuffer(L, code, strlen(code), chunk_name ? chunk_name : "=string") != LUA_OK ||
lua_pcall(L, 0, 0, 0) != LUA_OK) {
const char *msg = lua_tostring(L, -1);
int rc = rg__fail(ctx, RG_ERR_LUA, "%s", msg ? msg : "unknown lua error");
lua_pop(L, 1);
return rc;
}
return RG_OK;
}
struct lua_State *rg_lua_state(rg_context *ctx)
{
return ctx->L;
}
+6
View File
@@ -0,0 +1,6 @@
#define STB_RECT_PACK_IMPLEMENTATION
#include "../third_party/stb/stb_rect_pack.h"
#define STB_TRUETYPE_IMPLEMENTATION
#include "../third_party/stb/stb_truetype.h"
#include "internal.h"
+70
View File
@@ -0,0 +1,70 @@
#include "../include/regolith.h"
#include "../include/regolith_lua.h"
#include <stdio.h>
#include <stdlib.h>
typedef struct counter {
size_t live;
size_t peak;
} counter;
static void track(counter *c, size_t add, size_t sub) {
c->live = c->live + add - sub;
if (c->live > c->peak)
c->peak = c->live;
}
static void *c_alloc(size_t size, void *user) {
void *p = malloc(size);
if (p)
track(user, size, 0);
return p;
}
static void *c_realloc(void *ptr, size_t old_size, size_t new_size,
void *user) {
void *p = realloc(ptr, new_size);
if (p)
track(user, new_size, old_size);
return p;
}
static void c_free(void *ptr, size_t size, void *user) {
track(user, 0, size);
free(ptr);
}
int main(void) {
counter c = {0, 0};
rg_config cfg;
rg_context *ctx;
int rc;
rg_config_init(&cfg);
cfg.allocator.alloc = c_alloc;
cfg.allocator.realloc = c_realloc;
cfg.allocator.free = c_free;
cfg.allocator.user = &c;
ctx = rg_create(&cfg);
if (!ctx)
return 1;
rc = rg_lua_open(ctx);
if (rc == RG_OK)
rc = rg_lua_dostring(ctx, "print('hello from ' .. _VERSION)", "=smoke");
if (rc == RG_OK)
rc = rg_lua_dostring(ctx, "this is not lua", "=smoke_bad") == RG_ERR_LUA
? RG_OK
: RG_ERR_GENERIC;
printf("regolith %s, peak %zu bytes\n", rg_version(), c.peak);
rg_destroy(ctx);
if (c.live != 0) {
printf("leak: %zu bytes still live\n", c.live);
return 1;
}
return rc == RG_OK ? 0 : 1;
}
+9
View File
@@ -0,0 +1,9 @@
#include <SDL3/SDL.h>
#include <stdio.h>
int main(void)
{
int v = SDL_GetVersion();
printf("SDL %d.%d.%d\n", SDL_VERSIONNUM_MAJOR(v), SDL_VERSIONNUM_MINOR(v), SDL_VERSIONNUM_MICRO(v));
return 0;
}