remove sdl dependency

This commit is contained in:
2025-12-01 15:38:04 -06:00
parent fd4222f196
commit 6cfba975e5
15 changed files with 876 additions and 651 deletions

View File

@@ -129,7 +129,7 @@ meson test -C build_dbg
## File I/O
- `io.slurp(path)` - Reads a file as text
- `io.slurpbytes(path)` - Reads a file as an ArrayBuffer
- `io.slurp(path)` - Reads a file as an ArrayBuffer
- `io.slurpwrite(path, data)` - Writes data (string or ArrayBuffer) to a file
- `io.exists(path)` - Checks if a file exists

View File

@@ -14,6 +14,10 @@ sanitize: FORCE
meson setup -Db_sanitize=address -Db_sanitize=memory -Db_sanitize=leak -Db_sanitize=undefined build_sani
meson install -C build_sani
thread: FORCE
meson setup build_thread -Db_sanitize=thread -Dbuildtype=debugoptimized
meson install --only-changed -C build_thread
small: FORCE
meson setup -Dbuildtype=minsize -Db_lto=true -Db_ndebug=true build_small
meson install -C build_small

View File

@@ -11,7 +11,7 @@ The ```config.js``` module must return a single object that describes your game.
| **title** | `Prosperon [\${prosperon.version}-\${prosperon.revision}]` | string | Title of the game window, typically including version information. |
| **width** | `1280` | number | Initial width of the game window. |
| **height** | `720` | number | Initial height of the game window. |
| **icon** | `graphics.make_texture(io.slurpbytes('icons/moon.gif'))` | object | Icon texture for the game window, loaded from the provided file. |
| **icon** | `graphics.make_texture(io.slurp('icons/moon.gif'))` | object | Icon texture for the game window, loaded from the provided file. |
| **high_dpi** | `0` | number | Enables (1) or disables (0) High DPI support for the window. |
| **alpha** | `1` | number | Alpha channel setting for the window (0 to disable, 1 to enable transparency). |
| **fullscreen** | `0` | number | Sets whether the window should launch in fullscreen (1) or windowed (0). |

View File

@@ -65,17 +65,6 @@ endif
cmake = import('cmake')
sdl3_opts = cmake.subproject_options()
sdl3_opts.add_cmake_defines({
'SDL_STATIC': 'ON',
'SDL_SHARED': 'OFF',
'SDL_TEST': 'OFF',
'CMAKE_BUILD_TYPE': 'Release',
'SDL_THREADS': 'ON',
'SDL_PIPEWIRE': 'ON',
'SDL_PULSEAUDIO': 'ON',
})
cc = meson.get_compiler('c')
if host_machine.system() == 'linux'
@@ -93,63 +82,10 @@ if host_machine.system() == 'windows'
deps += cc.find_library('version')
deps += cc.find_library('cfgmgr32')
deps += cc.find_library('bcrypt')
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # Hack for MSYS2
sdl3_opts.add_cmake_defines({'HAVE_ISNAN': '1'})
link += ['-static', '-static-libgcc', '-static-libstdc++']
add_project_link_arguments('-static-libgcc', '-static-libstdc++', language: ['c', 'cpp'])
endif
if host_machine.system() == 'emscripten'
message('⚙ Building SDL3 subproject for Emscripten...')
sdl3_opts.append_compile_args(
'c',
'-pthread',
'-sUSE_PTHREADS=1',
)
sdl3_opts.append_compile_args(
'cpp',
'-pthread',
'-sUSE_PTHREADS=1',
)
# 3. And into every link step
sdl3_opts.append_link_args(
'-pthread',
'-sUSE_PTHREADS=1',
'-sPTHREAD_POOL_SIZE=4',
)
sdl3_proj = cmake.subproject('sdl3', options: sdl3_opts)
deps += sdl3_proj.dependency('SDL3-static')
add_project_arguments('-DPATH_MAX=4096', language: 'c')
add_project_arguments(
'-pthread',
'-sUSE_PTHREADS=1',
'-sPTHREAD_POOL_SIZE=4',
language: ['c', 'cpp'])
add_project_link_arguments(
'--use-port=emdawnwebgpu',
'-sUSE_PTHREADS=1',
'-pthread',
'-sPTHREAD_POOL_SIZE=4',
'-sMALLOC=mimalloc',
language: ['c','cpp'])
else
# Try to find system-installed SDL3 first
sdl3_dep = dependency('sdl3', static: true, required: false)
if not sdl3_dep.found()
message('⚙ System SDL3 not found, building subproject...')
sdl3_proj = cmake.subproject('sdl3', options : sdl3_opts)
deps += sdl3_proj.dependency('SDL3-static')
else
deps += sdl3_dep
endif
endif
miniz_dep = dependency('miniz', static: true, required: false)
if not miniz_dep.found()
message('⚙ System miniz not found, building subproject...')
@@ -216,7 +152,7 @@ src += [ # core
'wildmatch.c',
'qjs_actor.c',
'qjs_wota.c',
'scheduler.c'
'scheduler_threaded.c',
]
src += ['quickjs.c', 'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c']

View File

@@ -456,7 +456,7 @@ function globfs(globs, dir) {
return results
}
function slurpbytes(path) {
function slurp(path) {
return slurp(path)
}
@@ -479,7 +479,7 @@ cellfs.writepath = set_writepath
cellfs.basedir = basedir
cellfs.prefdir = prefdir
cellfs.realdir = realdir
cellfs.slurpbytes = slurpbytes
cellfs.slurp = slurp
cellfs.mount('.')

View File

@@ -55,7 +55,6 @@ static ssize_t js_fd_write_helper(JSContext *js, int fd, JSValue val)
// POSIX FILE DESCRIPTOR FUNCTIONS
JSC_SCALL(fd_open,
int flags = O_RDWR | O_CREAT;
mode_t mode = 0644;
@@ -338,10 +337,15 @@ JSC_CCALL(fd_fstat,
return obj;
)
JSC_SCALL(fd_stat,
JSC_CCALL(fd_stat,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
struct stat st;
if (stat(str, &st) != 0)
if (stat(path, &st) != 0) {
JS_FreeCString(js, path);
return JS_NewObject(js);
}
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.st_size));
@@ -372,6 +376,7 @@ JSC_SCALL(fd_stat,
JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, S_ISCHR(st.st_mode)));
JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, S_ISBLK(st.st_mode)));
JS_FreeCString(js, path);
return obj;
)
@@ -410,17 +415,31 @@ JSC_SCALL(fd_readdir,
#endif
)
JSC_SCALL(fd_is_file,
JSC_CCALL(fd_is_file,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
struct stat st;
if (stat(str, &st) != 0)
if (stat(path, &st) != 0) {
JS_FreeCString(js, path);
return JS_NewBool(js, false);
}
JS_FreeCString(js, path);
return JS_NewBool(js, S_ISREG(st.st_mode));
)
JSC_SCALL(fd_is_dir,
JSC_CCALL(fd_is_dir,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
struct stat st;
if (stat(str, &st) != 0)
if (stat(path, &st) != 0) {
JS_FreeCString(js, path);
return JS_NewBool(js, false);
}
JS_FreeCString(js, path);
return JS_NewBool(js, S_ISDIR(st.st_mode));
)
@@ -499,33 +518,22 @@ static void visit_directory(JSContext *js, JSValue results, int *result_count, c
#endif
}
JSC_CCALL(fd_enumerate,
const char *path = NULL;
JSC_SCALL(fd_enumerate,
const char *path = str;
if (!path) path = ".";
int recurse = 0;
if (argc > 0 && JS_IsString(argv[0])) {
path = JS_ToCString(js, argv[0]);
} else {
path = ".";
}
if (argc > 1) {
if (argc > 1)
recurse = JS_ToBool(js, argv[1]);
}
JSValue results = JS_NewArray(js);
int result_count = 0;
struct stat st;
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode))
visit_directory(js, results, &result_count, path, "", recurse);
}
if (path != NULL && strcmp(path, ".") != 0) {
JS_FreeCString(js, path);
}
return results;
ret = results;
)
static const JSCFunctionListEntry js_fd_funcs[] = {

View File

@@ -48,36 +48,43 @@ static JSClassDef js_dylib_class = {
#include <mach/mach_time.h>
JSC_CCALL(os_now,
uint64_t cell_ns()
{
#ifdef _WIN32
LARGE_INTEGER frequency, counter;
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&counter);
return number2js(js, (double)counter.QuadPart / (double)frequency.QuadPart);
return (uint64_t)((double)counter.QuadPart / (double)frequency.QuadPart * 1e9);
#elif defined(__APPLE__)
static mach_timebase_info_data_t timebase = {0, 0};
if (timebase.denom == 0) {
mach_timebase_info(&timebase);
}
uint64_t time = mach_absolute_time();
return number2js(js, (double)time * timebase.numer / timebase.denom / 1000000000.0);
return time * timebase.numer / timebase.denom;
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return number2js(js, (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
#endif
)
}
JSC_CCALL(os_sleep,
double secs = js2number(js,argv[0]);
JSC_CCALL(os_now, return number2js(js, cell_ns()); )
void cell_sleep(double seconds)
{
#ifdef _WIN32
Sleep((DWORD)(secs * 1000));
Sleep((DWORD)(seconds * 1000));
#else
struct timespec ts;
ts.tv_sec = (time_t)secs;
ts.tv_nsec = (long)((secs - ts.tv_sec) * 1000000000);
ts.tv_sec = (time_t)seconds;
ts.tv_nsec = (long)((seconds - ts.tv_sec) * 1000000000);
nanosleep(&ts, NULL);
#endif
}
JSC_CCALL(os_sleep,
double secs = js2number(js,argv[0]);
cell_sleep(secs);
)
static JSValue js_os_totalmem(JSContext *js, JSValue self, int argc, JSValue *argv) {
@@ -319,7 +326,7 @@ JSC_SCALL(os_system,
ret = number2js(js,err);
)
JSC_SCALL(os_exit,
JSC_CCALL(os_exit,
exit(0);
)

View File

@@ -103,7 +103,7 @@ static int js_qop_ensure_index(JSContext *js, qop_desc *qop) {
return 1;
}
JSC_SCALL(qop_open,
JSC_CCALL(qop_open,
size_t len;
void *data = js_get_blob_data(js, &len, argv[0]);
if (!data)
@@ -126,7 +126,7 @@ JSC_SCALL(qop_open,
}
)
JSC_SCALL(qop_write,
JSC_CCALL(qop_write,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;

View File

@@ -11,8 +11,6 @@
#include <windows.h>
#endif
#include <SDL3/SDL_atomic.h>
#define WOTA_IMPLEMENTATION
#include "wota.h"
@@ -50,148 +48,10 @@ int tracy_profiling_enabled = 0;
#define ENGINE "engine.cm"
static cell_rt **ready_queue = NULL;
static SDL_Semaphore *ready_sem;
static SDL_SpinLock queue_lock = 0;
static cell_rt **main_queue = NULL;
static SDL_Semaphore *main_sem;
static SDL_SpinLock main_queue_lock = 0;
static SDL_Mutex *actors_mutex = NULL;
static struct { char *key; cell_rt *value; } *actors = NULL;
static qop_desc qop_core;
static qop_file *qop_hashmap = NULL;
cell_rt *root_cell = NULL;
static SDL_AtomicInt shutting_down;
static SDL_AtomicInt runners_count;
static void exit_handler(void)
{
SDL_SetAtomicInt(&shutting_down, 1);
/* Signal all waiting threads */
int count = SDL_GetAtomicInt(&runners_count);
for (int i = 0; i < count; i++)
SDL_SignalSemaphore(ready_sem);
/* Signal main thread in case it's waiting */
SDL_SignalSemaphore(main_sem);
/* Wait for all runner threads to exit */
while (SDL_GetAtomicInt(&runners_count) > 0) {
SDL_Delay(10);
}
if (ready_sem)
SDL_DestroySemaphore(ready_sem);
if (main_sem)
SDL_DestroySemaphore(main_sem);
if (actors_mutex)
SDL_DestroyMutex(actors_mutex);
/* Clean up QOP resources */
if (qop_hashmap) {
free(qop_hashmap);
qop_hashmap = NULL;
}
if (qop_core.data) {
free(qop_core.data);
qop_core.data = NULL;
}
qop_close(&qop_core);
SDL_Quit();
exit(0);
}
void actor_free(cell_rt *actor)
{
// Delete it out of actors first so it can no longer get messages
SDL_LockMutex(actors_mutex);
shdel(actors, actor->id);
SDL_UnlockMutex(actors_mutex);
// If in a queue, remove it
SDL_LockSpinlock(&queue_lock);
for (int i = 0; i < arrlen(ready_queue); i++) {
if (ready_queue[i] == actor) {
arrdel(ready_queue, i);
break;
}
}
SDL_UnlockSpinlock(&queue_lock);
SDL_LockSpinlock(&main_queue_lock);
for (int i = 0; i < arrlen(main_queue); i++) {
if (main_queue[i] == actor) {
arrdel(main_queue, i);
break;
}
}
SDL_UnlockSpinlock(&main_queue_lock);
// Do not go forward with actor destruction until the actor is completely free
SDL_LockMutex(actor->msg_mutex);
SDL_LockMutex(actor->mutex);
JSContext *js = actor->context;
JS_FreeValue(js, actor->idx_buffer);
JS_FreeValue(js, actor->message_handle);
JS_FreeValue(js, actor->on_exception);
JS_FreeValue(js, actor->unneeded);
JS_FreeAtom(js, actor->actor_sym);
SDL_RemoveTimer(actor->ar);
/* Free timer callbacks stored in actor */
for (int i = 0; i < hmlen(actor->timers); i++) {
JS_FreeValue(js, actor->timers[i].value);
}
hmfree(actor->timers);
/* Free all letters in the queue */
for (int i = 0; i < arrlen(actor->letters); i++) {
if (actor->letters[i].type == LETTER_BLOB) {
blob_destroy(actor->letters[i].blob_data);
} else if (actor->letters[i].type == LETTER_CALLBACK) {
JS_FreeValue(js, actor->letters[i].callback);
}
}
arrfree(actor->letters);
JSRuntime *rt = JS_GetRuntime(js);
JS_SetInterruptHandler(rt, NULL, NULL);
JS_FreeContext(js);
JS_FreeRuntime(rt);
free(actor->id);
SDL_UnlockMutex(actor->mutex);
SDL_DestroyMutex(actor->mutex);
SDL_UnlockMutex(actor->msg_mutex);
SDL_DestroyMutex(actor->msg_mutex);
#ifdef HAVE_MIMALLOC
mi_heap_destroy(actor->heap);
#endif
free(actor);
SDL_LockMutex(actors_mutex);
if (shlen(actors) == 0)
exit(0);
SDL_UnlockMutex(actors_mutex);
}
void js_dofree(JSRuntime *rt, void *opaque, void *ptr)
{
js_free_rt(rt, ptr);
}
static size_t js_mi_malloc_usable_size(const void *ptr)
{
#if defined(__APPLE__)
@@ -419,298 +279,6 @@ int prosperon_mount_core(void)
return 1;
}
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
{
if (actor->disrupt) return;
JS_FreeValue(actor->context, actor->unneeded);
if (!JS_IsFunction(actor->context, fn)) {
actor->unneeded = JS_NULL;
goto END;
}
actor->unneeded = JS_DupValue(actor->context, fn);
actor->ar_secs = seconds;
END:
if (actor->ar) {
SDL_RemoveTimer(actor->ar);
actor->ar = 0;
}
set_actor_state(actor);
}
cell_rt *create_actor(void *wota)
{
cell_rt *actor = calloc(sizeof(*actor), 1);
#ifdef HAVE_MIMALLOC
actor->heap = mi_heap_new();
#endif
actor->init_wota = wota;
actor->idx_buffer = JS_NULL;
actor->message_handle = JS_NULL;
actor->unneeded = JS_NULL;
actor->on_exception = JS_NULL;
actor->actor_sym = JS_ATOM_NULL;
arrsetcap(actor->letters, 5);
actor->mutex = SDL_CreateMutex(); /* Protects JSContext + state */
actor->msg_mutex = SDL_CreateMutex(); /* Mailbox queue lock */
/* Lock actor->mutex while initializing JS runtime. */
SDL_LockMutex(actor->mutex);
script_startup(actor);
set_actor_state(actor);
SDL_UnlockMutex(actor->mutex);
return actor;
}
cell_rt *get_actor(char *id)
{
SDL_LockMutex(actors_mutex);
int idx = shgeti(actors, id);
if (idx == -1) {
SDL_UnlockMutex(actors_mutex);
return NULL;
}
cell_rt *actor = actors[idx].value;
SDL_UnlockMutex(actors_mutex);
return actor;
}
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
{
SDL_LockMutex(actors_mutex);
if (shgeti(actors, id) != -1) {
SDL_UnlockMutex(actors_mutex);
return "Actor with given ID already exists.";
}
actor->main_thread_only = mainthread;
actor->id = strdup(id);
actor->ar_secs = ar;
shput(actors, id, actor);
SDL_UnlockMutex(actors_mutex);
return NULL;
}
const char *send_message(const char *id, void *msg)
{
cell_rt *target = get_actor(id);
if (!target) {
blob_destroy((blob *)msg);
return "Could not get actor from id.";
}
letter l;
l.type = LETTER_BLOB;
l.blob_data = (blob *)msg;
SDL_LockMutex(target->msg_mutex);
arrput(target->letters, l);
if (target->ar) {
SDL_RemoveTimer(target->ar);
target->ar = 0;
}
SDL_UnlockMutex(target->msg_mutex);
set_actor_state(target);
return NULL;
}
static Uint32 actor_remove_cb(cell_rt *actor, Uint32 id, Uint32 interval)
{
actor->disrupt = 1;
if (!JS_IsNull(actor->unneeded)) {
SDL_LockMutex(actor->mutex);
JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL);
uncaught_exception(actor->context, ret);
SDL_UnlockMutex(actor->mutex);
}
actor_free(actor);
return 0;
}
/* Timer callback adds an event to the queue under evt_mutex. */
Uint32 actor_delay_cb(cell_rt *actor, SDL_TimerID id, Uint32 interval)
{
SDL_LockMutex(actor->msg_mutex);
int idx = hmgeti(actor->timers, id);
if (idx == -1) goto END;
JSValue cb = actor->timers[idx].value;
hmdel(actor->timers, id);
actor_clock(actor, cb);
JS_FreeValue(actor->context, cb);
END:
SDL_UnlockMutex(actor->msg_mutex);
return 0;
}
void set_actor_state(cell_rt *actor)
{
if (actor->disrupt) {
actor_free(actor);
return;
}
SDL_LockMutex(actor->msg_mutex);
switch(actor->state) {
case ACTOR_RUNNING:
case ACTOR_READY:
if (actor->ar) {
SDL_RemoveTimer(actor->ar);
actor->ar = 0;
}
break;
case ACTOR_IDLE:
if (arrlen(actor->letters)) {
actor->state = ACTOR_READY;
if (actor->main_thread_only) {
SDL_LockSpinlock(&main_queue_lock);
arrput(main_queue, actor);
SDL_UnlockSpinlock(&main_queue_lock);
SDL_SignalSemaphore(main_sem);
} else {
SDL_LockSpinlock(&queue_lock);
arrput(ready_queue, actor);
SDL_UnlockSpinlock(&queue_lock);
SDL_SignalSemaphore(ready_sem);
}
} else if (!arrlen(actor->letters) && !hmlen(actor->timers))
actor->ar = SDL_AddTimerNS(actor->ar_secs*1e9, actor_remove_cb, actor);
break;
}
SDL_UnlockMutex(actor->msg_mutex);
}
void actor_turn(cell_rt *actor)
{
SDL_LockMutex(actor->mutex);
#ifdef TRACY_ENABLE
int entered = 0;
if (tracy_profiling_enabled && TracyCIsConnected) {
TracyCFiberEnter(actor->name);
entered = 1;
}
#endif
actor->state = ACTOR_RUNNING;
TAKETURN:
SDL_LockMutex(actor->msg_mutex);
JSValue result;
if (!arrlen(actor->letters)) {
SDL_UnlockMutex(actor->msg_mutex);
goto ENDTURN;
}
letter l = actor->letters[0];
arrdel(actor->letters, 0);
SDL_UnlockMutex(actor->msg_mutex);
if (l.type == LETTER_BLOB) {
// Create a JS blob from the C blob
size_t size = l.blob_data->length / 8; // Convert bits to bytes
JSValue arg = js_new_blob_stoned_copy(actor->context, l.blob_data->data, size);
blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle, JS_NULL, 1, &arg);
uncaught_exception(actor->context, result);
JS_FreeValue(actor->context, arg);
} else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
uncaught_exception(actor->context, result);
JS_FreeValue(actor->context, l.callback);
}
if (actor->disrupt) goto ENDTURN;
// If there are no waiting threads, bail. otherwise, try for another turn
SDL_LockSpinlock(&queue_lock);
int someone_else_waiting = (arrlen(ready_queue) > 0);
SDL_UnlockSpinlock(&queue_lock);
if (!someone_else_waiting) goto TAKETURN;
ENDTURN:
actor->state = ACTOR_IDLE;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled && entered)
TracyCFiberLeave(actor->name);
#endif
set_actor_state(actor);
SDL_UnlockMutex(actor->mutex);
}
void actor_clock(cell_rt *actor, JSValue fn)
{
SDL_LockMutex(actor->msg_mutex);
letter l;
l.type = LETTER_CALLBACK;
l.callback = JS_DupValue(actor->context, fn);
arrput(actor->letters, l);
SDL_UnlockMutex(actor->msg_mutex);
set_actor_state(actor);
}
/* JS function that schedules a timer. */
JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (!JS_IsFunction(js, argv[0]))
return JS_ThrowReferenceError(js, "Argument must be a function.");
cell_rt *actor = JS_GetContextOpaque(js);
double seconds;
JS_ToFloat64(js, &seconds, argv[1]);
if (seconds <= 0) {
actor_clock(actor, argv[0]);
return JS_NULL;
}
SDL_LockMutex(actor->msg_mutex);
uint32_t id = SDL_AddTimerNS(seconds*1e9, actor_delay_cb, actor);
JSValue cb = JS_DupValue(js, argv[0]);
hmput(actor->timers, id, cb);
SDL_UnlockMutex(actor->msg_mutex);
return JS_NewUint32(js, id);
}
JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv)
{
cell_rt *actor = JS_GetContextOpaque(js);
uint32_t timer_id;
JS_ToUint32(js, &timer_id, argv[0]);
if (timer_id == -1) return JS_NULL;
SDL_RemoveTimer(timer_id);
JSValue cb = JS_NULL;
SDL_LockMutex(actor->msg_mutex);
int id = hmgeti(actor->timers, timer_id);
if (id != -1) {
cb = actor->timers[id].value;
hmdel(actor->timers, timer_id);
}
SDL_UnlockMutex(actor->msg_mutex);
JS_FreeValue(js,cb);
return JS_NULL;
}
// Wrapper struct to keep the array pointer stable
typedef struct {
#ifdef TRACY_ENABLE
@@ -720,10 +288,10 @@ typedef struct {
#endif
} tracy_stack_t;
// Global TLS ID for the Tracy stack
#ifdef TRACY_ENABLE
static SDL_TLSID tracy_stack_id = {0};
// Cleanup function for the wrapper struct
static void tracy_cleanup_stack(void *value)
{
tracy_stack_t *stack = value;
@@ -733,7 +301,6 @@ static void tracy_cleanup_stack(void *value)
}
}
// Get or initialize the thread-local Tracy stack
static tracy_stack_t *get_tracy_stack(void)
{
tracy_stack_t *stack = SDL_GetTLS(&tracy_stack_id);
@@ -748,7 +315,6 @@ static tracy_stack_t *get_tracy_stack(void)
void tracy_call_hook(JSContext *js, JSValue fn)
{
#ifdef TRACY_ENABLE
if (!tracy_profiling_enabled)
return;
@@ -760,21 +326,20 @@ void tracy_call_hook(JSContext *js, JSValue fn)
arrput(stack->arr, ___tracy_emit_zone_begin_alloc(srcloc, 1));
free_js_debug_info(js, &debug);
#endif
}
void tracy_end_hook(JSContext *js, JSValue fn)
{
#ifdef TRACY_ENABLE
if (!tracy_profiling_enabled)
return;
tracy_stack_t *stack = get_tracy_stack();
if (arrlen(stack->arr) > 0)
___tracy_emit_zone_end(arrpop(stack->arr));
#endif
}
#endif
void actor_disrupt(cell_rt *crt)
{
crt->disrupt = 1;
@@ -782,11 +347,6 @@ void actor_disrupt(cell_rt *crt)
actor_free(crt);
}
static int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt)
{
return SDL_GetAtomicInt(&shutting_down) || crt->disrupt;
}
JSValue js_os_use(JSContext *js);
void script_startup(cell_rt *prt)
@@ -884,49 +444,6 @@ void script_startup(cell_rt *prt)
set_actor_state(crt);
}
int uncaught_exception(JSContext *js, JSValue v)
{
cell_rt *rt = JS_GetContextOpaque(js);
SDL_LockMutex(rt->mutex);
if (!JS_HasException(js)) {
JS_FreeValue(js,v);
SDL_UnlockMutex(rt->mutex);
return 1;
}
JSValue exp = JS_GetException(js);
JSValue ret = JS_Call(js, rt->on_exception, JS_NULL, 1, &exp);
JS_FreeValue(js,ret);
JS_FreeValue(js, exp);
JS_FreeValue(js,v);
SDL_UnlockMutex(rt->mutex);
return 0;
}
static int actor_runner(void *data)
{
SDL_AddAtomicInt(&runners_count, 1);
while (!SDL_GetAtomicInt(&shutting_down)) {
SDL_WaitSemaphore(ready_sem);
SDL_LockSpinlock(&queue_lock);
cell_rt *actor = NULL;
if (arrlen(ready_queue) > 0) {
actor = ready_queue[0];
arrdel(ready_queue, 0);
}
SDL_UnlockSpinlock(&queue_lock);
if (actor)
actor_turn(actor);
}
SDL_AddAtomicInt(&runners_count, -1);
return 0;
}
static void signal_handler(int sig)
{
const char *str = NULL;
@@ -943,33 +460,6 @@ static void signal_handler(int sig)
exit_handler();
}
static void add_runners(int n)
{
/* Launch runner threads */
for (int i = 0; i < n; i++) {
char threadname[128];
snprintf(threadname, sizeof(threadname), "actor runner %d", i);
SDL_Thread *thread = SDL_CreateThread(actor_runner, threadname, NULL);
SDL_DetachThread(thread);
/* Thread is detached, no need to track */
}
}
static void loop()
{
while (!SDL_GetAtomicInt(&shutting_down)) {
SDL_WaitSemaphore(main_sem);
SDL_LockSpinlock(&main_queue_lock);
cell_rt *actor = NULL;
if (arrlen(main_queue) > 0) {
actor = main_queue[0];
arrdel(main_queue, 0);
}
SDL_UnlockSpinlock(&main_queue_lock);
actor_turn(actor);
}
}
int main(int argc, char **argv)
{
int profile_enabled = 0;
@@ -1009,13 +499,7 @@ int main(int argc, char **argv)
wota_write_text(&startwota, actor_argv[i]);
/* Initialize synchronization primitives */
ready_sem = SDL_CreateSemaphore(0);
main_sem = SDL_CreateSemaphore(0);
actors_mutex = SDL_CreateMutex();
SDL_SetAtomicInt(&shutting_down, 0);
SDL_SetAtomicInt(&runners_count, 0);
add_runners(SDL_GetNumLogicalCPUCores());
actor_initialize();
root_cell = create_actor(startwota.data);
@@ -1025,22 +509,11 @@ int main(int argc, char **argv)
signal(SIGSEGV, signal_handler);
signal(SIGABRT, signal_handler);
loop();
actor_loop();
return 0;
}
int actor_exists(const char *id)
{
SDL_LockMutex(actors_mutex);
int idx = shgeti(actors,id);
SDL_UnlockMutex(actors_mutex);
if (idx == -1)
return 0;
else
return 1;
}
int JS_ArrayLength(JSContext *js, JSValue a)
{
JSValue length = JS_GetPropertyStr(js, a, "length");

View File

@@ -72,16 +72,6 @@ void *value2wota(JSContext *js, JSValue v, JSValue replacer, size_t *bytes);
#define CGETSET_ADD(ID, ENTRY) MIST_CGETSET_DEF(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY)
#define CGETSET_ADD_HID(ID, ENTRY) MIST_CGETSET_BASE(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY, JS_PROP_CONFIGURABLE)
#define JSC_DCALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(); return JS_NULL; }
#define JSC_1CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0])); return JS_NULL; }
#define JSC_2CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1])); return JS_NULL; }
#define JSC_3CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2])); return JS_NULL; }
#define JSC_4CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3])); return JS_NULL; }
#define JSC_5CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3]), js2number(js,argv[4])); return JS_NULL; }
#define JSC_6CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3]), js2number(js,argv[4]), js2number(js,argv[5])); return JS_NULL; }
#define JSC_7CALL(FN) JSValue js_##FN (JSContext *js, JSValue self, int argc, JSValue *argv) { FN(js2number(js,argv[0]), js2number(js,argv[1]), js2number(js,argv[2]), js2number(js,argv[3]), js2number(js,argv[4]), js2number(js,argv[5]), js2number(js,argv[6])); return JS_NULL; }
#define GETSETPAIR(ID, ENTRY, TYPE, FN) \
JSValue js_##ID##_set_##ENTRY (JS_SETSIG) { \
js2##ID (js, self)->ENTRY = js2##TYPE (js,val); \

92
source/cell_internal.h Normal file
View File

@@ -0,0 +1,92 @@
#ifdef HAVE_MIMALLOC
typedef struct mi_heap_s mi_heap_t;
#endif
#include <pthread.h>
/* Letter type for unified message queue */
typedef enum {
LETTER_BLOB, /* Blob message */
LETTER_CALLBACK /* JSValue callback function */
} letter_type;
typedef struct letter {
letter_type type;
union {
blob *blob_data; /* For LETTER_BLOB */
JSValue callback; /* For LETTER_CALLBACK */
};
} letter;
#define ACTOR_IDLE 0 // Actor not doing anything
#define ACTOR_READY 1 // Actor ready for a turn
#define ACTOR_RUNNING 2 // Actor taking a turn
#define ACTOR_EXHAUSTED 3 // Actor waiting for GC
#define ACTOR_RECLAIMING 4 // Actor running GC
#define ACTOR_SLOW 5 // Actor going slowly; deprioritize
typedef struct cell_rt {
JSContext *context;
#ifdef HAVE_MIMALLOC
mi_heap_t *heap;
#endif
JSValue idx_buffer;
JSValue on_exception;
JSValue message_handle;
void *init_wota;
/* Protects JSContext usage */
pthread_mutex_t *mutex; /* for everything else */
pthread_mutex_t *msg_mutex; /* For message queue and timers queue */
char *id;
int idx_count;
/* The “mailbox” for incoming messages + a dedicated lock for it: */
letter *letters;
/* CHANGED FOR EVENTS: a separate lock for the actor->events queue */
struct { Uint32 key; JSValue value; } *timers;
int state;
Uint32 ar; // timer for unneeded
double ar_secs; // time for unneeded
JSValue unneeded; // fn to call before unneeded
int disrupt;
int main_thread_only;
int affinity;
JSAtom actor_sym;
const char *name; // human friendly name
} cell_rt;
cell_rt *create_actor(void *wota);
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
void actor_disrupt(cell_rt *actor);
JSAtom actor_sym(cell_rt *actor);
const char *send_message(const char *id, void *msg);
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar);
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds);
void script_startup(cell_rt *rt);
cell_rt *create_actor(void *wota);
int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(const char *id);
void set_actor_state(cell_rt *actor);
void actor_clock(cell_rt *actor, JSValue fn);
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds);
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id);
void exit_handler(void);
int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt);
void actor_loop();
void actor_initialize(void);
void actor_free(cell_rt *actor);
uint64_t cell_ns();
void cell_sleep(double seconds);

View File

@@ -113,6 +113,30 @@ JSC_CCALL(actor_clock,
actor_clock(actor, argv[0]);
)
JSC_CCALL(actor_delay,
if (!JS_IsFunction(js, argv[0]))
return JS_ThrowReferenceError(js, "Argument must be a function.");
cell_rt *actor = JS_GetContextOpaque(js);
double seconds;
JS_ToFloat64(js, &seconds, argv[1]);
if (seconds <= 0) {
actor_clock(actor, argv[0]);
return JS_NULL;
}
uint32_t id = actor_delay(actor, argv[0], seconds);
return JS_NewUint32(js, id);
)
JSC_CCALL(actor_removetimer,
cell_rt *actor = JS_GetContextOpaque(js);
uint32_t timer_id;
JS_ToUint32(js, &timer_id, argv[0]);
JSValue removed = actor_remove_timer(actor, timer_id);
JS_FreeValue(js, removed);
)
static const JSCFunctionListEntry js_actor_funcs[] = {
MIST_FUNC_DEF(os, createactor, 1),
MIST_FUNC_DEF(os, mailbox_push, 2),

View File

@@ -96,7 +96,7 @@
/* dump objects freed by the garbage collector */
//#define DUMP_GC_FREE
/* dump objects leaking when freeing the runtime */
//#define DUMP_LEAKS 1
#define DUMP_LEAKS 1
/* dump memory usage before running the garbage collector */
//#define DUMP_MEM
//#define DUMP_OBJECTS /* dump objects in JS_FreeContext */
@@ -1409,6 +1409,7 @@ void JS_SetRuntimeInfo(JSRuntime *rt, const char *s)
void JS_FreeRuntime(JSRuntime *rt)
{
struct list_head *el, *el1;
int i;
JS_FreeValueRT(rt, rt->current_exception);

View File

@@ -1 +0,0 @@
#include <SDL3/SDL_atomic.h>

691
source/scheduler_threaded.c Normal file
View File

@@ -0,0 +1,691 @@
#include <pthread.h>
#include <sys/time.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdatomic.h>
#include "stb_ds.h"
#include "cell.h"
#include "cell_internal.h"
typedef struct actor_node {
cell_rt *actor;
struct actor_node *next;
} actor_node;
typedef enum {
TIMER_JS,
TIMER_NATIVE_REMOVE
} timer_type;
typedef struct {
uint64_t execute_at_ns;
cell_rt *actor;
uint32_t timer_id;
timer_type type;
} timer_node;
static timer_node *timer_heap = NULL;
// --- 3. The Global Engine State ---
static struct {
pthread_mutex_t lock; // Protects queue, shutdown flag, and timers
pthread_cond_t wake_cond; // Wakes up workers
pthread_cond_t timer_cond; // Wakes up the timer thread
pthread_cond_t main_cond; // Wakes up the main thread
actor_node *head; // Ready Queue Head
actor_node *tail; // Ready Queue Tail
actor_node *main_head; // Main Thread Queue Head
actor_node *main_tail; // Main Thread Queue Tail
int shutting_down;
pthread_t *worker_threads;
int num_workers;
pthread_t timer_thread;
} engine;
static pthread_mutex_t *actors_mutex;
static struct { char *key; cell_rt *value; } *actors = NULL;
#define lockless_shdel(NAME, KEY) pthread_mutex_lock(NAME##_mutex); shdel(NAME, KEY); pthread_mutex_unlock(NAME##_mutex);
#define lockless_shlen(NAME) ({ \
pthread_mutex_lock(NAME##_mutex); \
size_t _len = shlen(NAME); \
pthread_mutex_unlock(NAME##_mutex); \
_len; \
})
#define lockless_shgeti(NAME, KEY) ({ \
pthread_mutex_lock(NAME##_mutex); \
int _idx = shgeti(NAME, KEY); \
pthread_mutex_unlock(NAME##_mutex); \
_idx; \
})
#define lockless_shget(NAME, KEY) ({ \
pthread_mutex_lock(NAME##_mutex); \
cell_rt *_actor = shget(NAME, KEY); \
pthread_mutex_unlock(NAME##_mutex); \
_actor; \
})
#define lockless_shput_unique(NAME, KEY, VALUE) ({ \
pthread_mutex_lock(NAME##_mutex); \
int _exists = shgeti(NAME, KEY) != -1; \
if (!_exists) shput(NAME, KEY, VALUE); \
pthread_mutex_unlock(NAME##_mutex); \
!_exists; \
})
// Forward declarations
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval);
void actor_turn(cell_rt *actor);
void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type) {
timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type };
arrput(timer_heap, node);
// Bubble up
int i = arrlen(timer_heap) - 1;
while (i > 0) {
int parent = (i - 1) / 2;
if (timer_heap[i].execute_at_ns >= timer_heap[parent].execute_at_ns) break;
timer_node tmp = timer_heap[i];
timer_heap[i] = timer_heap[parent];
timer_heap[parent] = tmp;
i = parent;
}
}
// Helper: Heap Pop
int heap_pop(timer_node *out) {
if (arrlen(timer_heap) == 0) return 0;
*out = timer_heap[0];
timer_node last = arrpop(timer_heap);
if (arrlen(timer_heap) > 0) {
timer_heap[0] = last;
// Bubble down
int i = 0;
int n = arrlen(timer_heap);
while (1) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int smallest = i;
if (left < n && timer_heap[left].execute_at_ns < timer_heap[smallest].execute_at_ns)
smallest = left;
if (right < n && timer_heap[right].execute_at_ns < timer_heap[smallest].execute_at_ns)
smallest = right;
if (smallest == i) break;
timer_node tmp = timer_heap[i];
timer_heap[i] = timer_heap[smallest];
timer_heap[smallest] = tmp;
i = smallest;
}
}
return 1;
}
void *timer_thread_func(void *arg) {
while (1) {
pthread_mutex_lock(&engine.lock);
if (engine.shutting_down) {
pthread_mutex_unlock(&engine.lock);
return NULL;
}
if (arrlen(timer_heap) == 0) {
pthread_cond_wait(&engine.timer_cond, &engine.lock);
} else {
uint64_t now = cell_ns();
if (timer_heap[0].execute_at_ns <= now) {
// --- TIMER FIRED ---
timer_node t;
heap_pop(&t);
pthread_mutex_unlock(&engine.lock);
if (t.type == TIMER_NATIVE_REMOVE) {
// Execute native remove callback
actor_remove_cb(t.actor, t.timer_id, 0);
} else {
// Inject event into Actor
pthread_mutex_lock(t.actor->msg_mutex);
int idx = hmgeti(t.actor->timers, t.timer_id);
if (idx != -1) {
JSValue cb = t.actor->timers[idx].value;
hmdel(t.actor->timers, t.timer_id);
actor_clock(t.actor, cb);
JS_FreeValue(t.actor->context, cb);
}
pthread_mutex_unlock(t.actor->msg_mutex);
}
// Loop immediately to check for other expired timers
continue;
} else {
// --- WAIT FOR DEADLINE ---
struct timespec ts;
uint64_t ns = timer_heap[0].execute_at_ns;
ts.tv_sec = ns / 1000000000ULL;
ts.tv_nsec = ns % 1000000000ULL;
pthread_cond_timedwait(&engine.timer_cond, &engine.lock, &ts);
}
}
pthread_mutex_unlock(&engine.lock);
}
return NULL;
}
void *actor_runner(void *arg) {
while (1) {
pthread_mutex_lock(&engine.lock);
// Wait while queue is empty AND not shutting down
while (engine.head == NULL && !engine.shutting_down) {
pthread_cond_wait(&engine.wake_cond, &engine.lock);
}
if (engine.shutting_down && engine.head == NULL) {
pthread_mutex_unlock(&engine.lock);
break; // Exit thread
}
// Pop from Linked List
actor_node *node = engine.head;
engine.head = node->next;
if (engine.head == NULL) engine.tail = NULL;
pthread_mutex_unlock(&engine.lock);
if (node) {
actor_turn(node->actor);
free(node);
}
}
return NULL;
}
void actor_initialize(void) {
pthread_mutex_init(&engine.lock, NULL);
pthread_cond_init(&engine.wake_cond, NULL);
pthread_cond_init(&engine.timer_cond, NULL);
pthread_cond_init(&engine.main_cond, NULL);
engine.shutting_down = 0;
engine.head = NULL;
engine.tail = NULL;
engine.main_head = NULL;
engine.main_tail = NULL;
actors_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(actors_mutex, NULL);
// Start Timer Thread
pthread_create(&engine.timer_thread, NULL, timer_thread_func, NULL);
// Start Workers
long n = sysconf(_SC_NPROCESSORS_ONLN);
engine.num_workers = (int)n;
engine.worker_threads = malloc(sizeof(pthread_t) * n);
for (int i=0; i < n; i++) {
pthread_create(&engine.worker_threads[i], NULL, actor_runner, NULL);
}
}
void actor_free(cell_rt *actor)
{
lockless_shdel(actors, actor->id);
// Note: Removing from ready queue is hard with a singly linked list.
// We assume actor_turn handles disrupted/freed actors gracefully or they run once more.
// The old code did lockless_rm(ready_queue, actor), which was O(N).
// Here we rely on the actor->disrupt flag checked in actor_turn.
// Do not go forward with actor destruction until the actor is completely free
pthread_mutex_lock(actor->msg_mutex);
pthread_mutex_lock(actor->mutex);
JSContext *js = actor->context;
JS_FreeValue(js, actor->idx_buffer);
JS_FreeValue(js, actor->message_handle);
JS_FreeValue(js, actor->on_exception);
JS_FreeValue(js, actor->unneeded);
JS_FreeAtom(js, actor->actor_sym);
for (int i = 0; i < hmlen(actor->timers); i++) {
JS_FreeValue(js, actor->timers[i].value);
}
hmfree(actor->timers);
/* Free all letters in the queue */
for (int i = 0; i < arrlen(actor->letters); i++) {
if (actor->letters[i].type == LETTER_BLOB) {
blob_destroy(actor->letters[i].blob_data);
} else if (actor->letters[i].type == LETTER_CALLBACK) {
JS_FreeValue(js, actor->letters[i].callback);
}
}
arrfree(actor->letters);
JSRuntime *rt = JS_GetRuntime(js);
JS_SetInterruptHandler(rt, NULL, NULL);
JS_FreeContext(js);
JS_FreeRuntime(rt);
free(actor->id);
pthread_mutex_unlock(actor->mutex);
pthread_mutex_destroy(actor->mutex);
free(actor->mutex);
pthread_mutex_unlock(actor->msg_mutex);
pthread_mutex_destroy(actor->msg_mutex);
free(actor->msg_mutex);
#ifdef HAVE_MIMALLOC
mi_heap_destroy(actor->heap);
#endif
free(actor);
int actor_count = lockless_shlen(actors);
if (actor_count == 0) exit(0);
}
void exit_handler(void) {
pthread_mutex_lock(&engine.lock);
engine.shutting_down = 1;
pthread_cond_broadcast(&engine.wake_cond);
pthread_cond_broadcast(&engine.timer_cond);
pthread_cond_broadcast(&engine.main_cond);
pthread_mutex_unlock(&engine.lock);
pthread_join(engine.timer_thread, NULL);
for (int i=0; i < engine.num_workers; i++) {
pthread_join(engine.worker_threads[i], NULL);
}
free(engine.worker_threads);
pthread_mutex_destroy(&engine.lock);
pthread_cond_destroy(&engine.wake_cond);
pthread_cond_destroy(&engine.timer_cond);
pthread_cond_destroy(&engine.main_cond);
pthread_mutex_destroy(actors_mutex);
free(actors_mutex);
arrfree(timer_heap);
exit(0);
}
int actor_exists(const char *id)
{
int idx = lockless_shgeti(actors, id);
return idx != -1;
}
void set_actor_state(cell_rt *actor)
{
if (actor->disrupt) {
actor_free(actor);
return;
}
pthread_mutex_lock(actor->msg_mutex);
switch(actor->state) {
case ACTOR_RUNNING:
case ACTOR_READY:
if (actor->ar)
actor->ar = 0;
break;
case ACTOR_IDLE:
if (arrlen(actor->letters)) {
actor->state = ACTOR_READY;
actor->ar = 0;
actor_node *n = malloc(sizeof(actor_node));
n->actor = actor;
n->next = NULL;
pthread_mutex_lock(&engine.lock);
if (actor->main_thread_only) {
if (engine.main_tail) {
engine.main_tail->next = n;
} else {
engine.main_head = n;
}
engine.main_tail = n;
pthread_cond_signal(&engine.main_cond);
} else {
if (engine.tail) {
engine.tail->next = n;
} else {
engine.head = n;
}
engine.tail = n;
pthread_cond_signal(&engine.wake_cond);
}
pthread_mutex_unlock(&engine.lock);
} else if (!arrlen(actor->letters) && !hmlen(actor->timers)) {
// Schedule remove timer
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
actor->ar = id;
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(actor->ar_secs * 1e9);
pthread_mutex_lock(&engine.lock);
heap_push(execute_at, actor, id, TIMER_NATIVE_REMOVE);
if (timer_heap[0].timer_id == id) {
pthread_cond_signal(&engine.timer_cond);
}
pthread_mutex_unlock(&engine.lock);
}
break;
}
pthread_mutex_unlock(actor->msg_mutex);
}
uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval)
{
pthread_mutex_lock(actor->mutex);
// Check if this timer is still valid (match actor->ar)
if (actor->ar != id && id != 0) { // id 0 means force (optional)
pthread_mutex_unlock(actor->mutex);
return 0;
}
actor->disrupt = 1;
if (!JS_IsNull(actor->unneeded)) {
JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL);
uncaught_exception(actor->context, ret);
}
int should_free = (actor->state == ACTOR_IDLE);
pthread_mutex_unlock(actor->mutex);
if (should_free) actor_free(actor);
return 0;
}
void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
{
if (actor->disrupt) return;
JS_FreeValue(actor->context, actor->unneeded);
if (!JS_IsFunction(actor->context, fn)) {
actor->unneeded = JS_NULL;
goto END;
}
actor->unneeded = JS_DupValue(actor->context, fn);
actor->ar_secs = seconds;
END:
if (actor->ar)
actor->ar = 0;
set_actor_state(actor);
}
cell_rt *get_actor(char *id)
{
int idx = lockless_shgeti(actors, id);
if (idx == -1) {
return NULL;
}
return lockless_shget(actors, id);
}
int uncaught_exception(JSContext *js, JSValue v)
{
cell_rt *rt = JS_GetContextOpaque(js);
if (!JS_HasException(js)) {
JS_FreeValue(js,v);
return 1;
}
JSValue exp = JS_GetException(js);
JSValue ret = JS_Call(js, rt->on_exception, JS_NULL, 1, &exp);
JS_FreeValue(js,ret);
JS_FreeValue(js, exp);
JS_FreeValue(js,v);
return 0;
}
void actor_loop()
{
while (!engine.shutting_down) { // Direct read safe enough here or use lock
pthread_mutex_lock(&engine.lock);
while (engine.main_head == NULL && !engine.shutting_down) {
pthread_cond_wait(&engine.main_cond, &engine.lock);
}
if (engine.shutting_down && engine.main_head == NULL) {
pthread_mutex_unlock(&engine.lock);
break;
}
actor_node *node = engine.main_head;
engine.main_head = node->next;
if (engine.main_head == NULL) engine.main_tail = NULL;
pthread_mutex_unlock(&engine.lock);
if (node) {
actor_turn(node->actor);
free(node);
}
}
}
cell_rt *create_actor(void *wota)
{
cell_rt *actor = calloc(sizeof(*actor), 1);
#ifdef HAVE_MIMALLOC
actor->heap = mi_heap_new();
#endif
actor->init_wota = wota;
actor->idx_buffer = JS_NULL;
actor->message_handle = JS_NULL;
actor->unneeded = JS_NULL;
actor->on_exception = JS_NULL;
actor->actor_sym = JS_ATOM_NULL;
arrsetcap(actor->letters, 5);
actor->mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(actor->mutex, &attr);
actor->msg_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(actor->msg_mutex, &attr); // msg_mutex can be recursive too to be safe
pthread_mutexattr_destroy(&attr);
/* Lock actor->mutex while initializing JS runtime. */
pthread_mutex_lock(actor->mutex);
script_startup(actor);
set_actor_state(actor);
pthread_mutex_unlock(actor->mutex);
return actor;
}
const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
{
actor->main_thread_only = mainthread;
actor->id = strdup(id);
actor->ar_secs = ar;
int added = lockless_shput_unique(actors, id, actor);
if (!added) {
free(actor->id);
return "Actor with given ID already exists.";
}
return NULL;
}
int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt)
{
// Locking engine.lock for shutting_down might be too expensive for interrupt?
// Check atomic-like access or just access it.
// int s; pthread_mutex_lock(&engine.lock); s = engine.shutting_down; pthread_mutex_unlock(&engine.lock);
// But engine.shutting_down is int, atomic read on x86/arm usually ok.
return engine.shutting_down || crt->disrupt;
}
const char *send_message(const char *id, void *msg)
{
cell_rt *target = get_actor(id);
if (!target) {
blob_destroy((blob *)msg);
return "Could not get actor from id.";
}
letter l;
l.type = LETTER_BLOB;
l.blob_data = (blob *)msg;
pthread_mutex_lock(target->msg_mutex);
arrput(target->letters, l);
pthread_mutex_unlock(target->msg_mutex);
if (target->ar)
target->ar = 0;
set_actor_state(target);
return NULL;
}
void actor_turn(cell_rt *actor)
{
pthread_mutex_lock(actor->mutex);
#ifdef TRACY_ENABLE
int entered = 0;
if (tracy_profiling_enabled && TracyCIsConnected) {
TracyCFiberEnter(actor->name);
entered = 1;
}
#endif
actor->state = ACTOR_RUNNING;
TAKETURN:
pthread_mutex_lock(actor->msg_mutex);
JSValue result;
if (!arrlen(actor->letters)) {
pthread_mutex_unlock(actor->msg_mutex);
goto ENDTURN;
}
letter l = actor->letters[0];
arrdel(actor->letters, 0); // O(N) but we kept array as requested
pthread_mutex_unlock(actor->msg_mutex);
if (l.type == LETTER_BLOB) {
// Create a JS blob from the C blob
size_t size = l.blob_data->length / 8; // Convert bits to bytes
JSValue arg = js_new_blob_stoned_copy(actor->context, l.blob_data->data, size);
blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle, JS_NULL, 1, &arg);
uncaught_exception(actor->context, result);
JS_FreeValue(actor->context, arg);
} else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
uncaught_exception(actor->context, result);
JS_FreeValue(actor->context, l.callback);
}
if (actor->disrupt) goto ENDTURN;
// Check if anyone else is waiting?
// In the new system, checking the global queue is expensive (lock).
// And "someone else waiting" logic was to yield.
// With threads, we don't need to yield as much, let the OS schedule.
// But we might want to prevent one actor hogging the worker?
// For now, remove the yield check or implement it with try_lock or simple check.
// int someone_else_waiting = (lockless_arrlen(ready_queue) > 0);
// We'll just remove the optimization/yield for now to simplify.
// if (!someone_else_waiting) goto TAKETURN;
ENDTURN:
actor->state = ACTOR_IDLE;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled && entered)
TracyCFiberLeave(actor->name);
#endif
set_actor_state(actor);
pthread_mutex_unlock(actor->mutex);
}
void actor_clock(cell_rt *actor, JSValue fn)
{
letter l;
l.type = LETTER_CALLBACK;
l.callback = JS_DupValue(actor->context, fn);
pthread_mutex_lock(actor->msg_mutex);
arrput(actor->letters, l);
pthread_mutex_unlock(actor->msg_mutex);
set_actor_state(actor);
}
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds)
{
pthread_mutex_lock(actor->msg_mutex);
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
JSValue cb = JS_DupValue(actor->context, fn);
hmput(actor->timers, id, cb);
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(seconds * 1e9);
pthread_mutex_lock(&engine.lock);
heap_push(execute_at, actor, id, TIMER_JS);
if (timer_heap[0].timer_id == id) {
pthread_cond_signal(&engine.timer_cond);
}
pthread_mutex_unlock(&engine.lock);
pthread_mutex_unlock(actor->msg_mutex);
return id;
}
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id)
{
JSValue cb = JS_NULL;
pthread_mutex_lock(actor->msg_mutex);
int id = hmgeti(actor->timers, timer_id);
if (id != -1) {
cb = actor->timers[id].value;
hmdel(actor->timers, timer_id);
}
pthread_mutex_unlock(actor->msg_mutex);
// Note: We don't remove from heap, it will misfire safely
return cb;
}