Inital context stuff and tests
Linking and building is successfull and inital tests pass
This commit is contained in:
1 parent
2296f68123
commit
363e9894e8
10 files changed
+410
-59
No files matched your search
@@ -3,3 +3,6 @@
|
||||
docs/local/
|
||||
|
||||
build/
|
||||
build-wasm/
|
||||
compile_commands.json
|
||||
/.emscripten-cache
|
||||
+57
-28
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user