diff --git a/.gitignore b/.gitignore index f7fb5a8..922fed5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ docs/local/ build/ +build-wasm/ +compile_commands.json +/.emscripten-cache diff --git a/docs/dd.md b/docs/dd.md index 9715b77..73f0929 100644 --- a/docs/dd.md +++ b/docs/dd.md @@ -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. diff --git a/include/regolith.h b/include/regolith.h index 59b0952..643b916 100644 --- a/include/regolith.h +++ b/include/regolith.h @@ -6,24 +6,22 @@ Last edit date: 11-02-2026 */ - - #ifndef REGOLITH_H #define REGOLITH_H -#include -#include +#include +#include #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 } diff --git a/src/context.c b/src/context.c new file mode 100644 index 0000000..20939cb --- /dev/null +++ b/src/context.c @@ -0,0 +1,122 @@ +#include "internal.h" + +#include +#include +#include +#include + +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); +} diff --git a/src/internal.h b/src/internal.h new file mode 100644 index 0000000..c1f5da5 --- /dev/null +++ b/src/internal.h @@ -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 diff --git a/src/layout_clay.c b/src/layout_clay.c new file mode 100644 index 0000000..ec2b31c --- /dev/null +++ b/src/layout_clay.c @@ -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(); +} diff --git a/src/lua_module.c b/src/lua_module.c new file mode 100644 index 0000000..2caa4c3 --- /dev/null +++ b/src/lua_module.c @@ -0,0 +1,72 @@ +#include "internal.h" +#include "../include/regolith_lua.h" + +#include + +#include +#include +#include + +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; +} diff --git a/src/text_stb.c b/src/text_stb.c new file mode 100644 index 0000000..fe23f38 --- /dev/null +++ b/src/text_stb.c @@ -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" diff --git a/tests/smoke.c b/tests/smoke.c new file mode 100644 index 0000000..e568a92 --- /dev/null +++ b/tests/smoke.c @@ -0,0 +1,70 @@ +#include "../include/regolith.h" +#include "../include/regolith_lua.h" + +#include +#include + +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; +} diff --git a/tests/smoke_sdl3.c b/tests/smoke_sdl3.c new file mode 100644 index 0000000..e92eb6b --- /dev/null +++ b/tests/smoke_sdl3.c @@ -0,0 +1,9 @@ +#include +#include + +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; +}