| git.druid.rocks | index | militantprogrammer | astral_canvas | src/ | lib/ | lib.c |
src/lib/lib.c
#include "lib/lib.h"
#include "lib/error.h"
#include "lib/id.h"
#include "lib/queue.h"
#include "api/node.h"
#include <errno.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
pthread_t thread;
int workerKnown;
int threadLive;
pthread_mutex_t stateMutex;
pthread_cond_t stateCond;
LibState state;
JobQueue toWorker;
JobQueue toMain;
int running;
int dirty;
int width, height;
char title[256];
Backend backend;
Mode mode;
float scale;
Platform platform;
Renderer renderer;
Style theme;
int useVulkan;
} GlobalVars;
static GlobalVars G;
static int isWorker(void)
{
return G.workerKnown && pthread_equal(pthread_self(), G.thread);
}
static int pushDirected(QueueDir dir, JobFn fn, void *data, int own)
{
JobQueue *q;
Job *job;
if (dir == QUEUE_TO_WORKER) {
if (isWorker()) {
fprintf(stderr, "lib: worker cannot push toWorker (would loop)\n");
if (own && data)
free(data);
return -1;
}
q = &G.toWorker;
} else {
if (!isWorker()) {
fprintf(stderr, "lib: only the worker can push toMain\n");
if (own && data)
free(data);
return -1;
}
q = &G.toMain;
}
job = jobCreate(fn, data, own);
if (!job)
return -1;
if (queuePush(q, job) < 0) {
jobFree(job);
return -1;
}
return 0;
}
int libPushToWorker(JobFn fn, void *data, int own)
{
return pushDirected(QUEUE_TO_WORKER, fn, data, own);
}
int libPushToMain(JobFn fn, void *data, int own)
{
return pushDirected(QUEUE_TO_MAIN, fn, data, own);
}
static void runJob(Job *job)
{
if (!job)
return;
if (job->fn)
job->fn(job->data);
jobFree(job);
}
int libPopMain(void)
{
Job *job = queuePop(&G.toMain);
if (!job)
return -1;
runJob(job);
return 0;
}
int libMainWakeFd(void)
{
return queueWakeFd(&G.toMain);
}
static void setState(LibState state)
{
pthread_mutex_lock(&G.stateMutex);
G.state = state;
pthread_cond_broadcast(&G.stateCond);
pthread_mutex_unlock(&G.stateMutex);
}
LibState libState(void)
{
LibState state;
pthread_mutex_lock(&G.stateMutex);
state = G.state;
pthread_mutex_unlock(&G.stateMutex);
return state;
}
void libMarkDirty(void)
{
G.dirty = 1;
}
Platform *libPlatform(void)
{
return &G.platform;
}
Renderer *libRenderer(void)
{
return &G.renderer;
}
static void jobQuit(void *data)
{
(void)data;
G.running = 0;
G.platform.running = false;
}
static int pollWorker(int timeoutMs)
{
Platform *platform = &G.platform;
struct pollfd fds[2];
nfds_t n = 0;
int displayFd = platform->fileDescriptor ? platform->fileDescriptor(platform) : -1;
int wakeFd = queueWakeFd(&G.toWorker);
int rc;
if (platform->flush)
platform->flush(platform);
if (displayFd >= 0) {
fds[n].fd = displayFd;
fds[n].events = POLLIN;
n++;
}
if (wakeFd >= 0) {
fds[n].fd = wakeFd;
fds[n].events = POLLIN;
n++;
}
rc = poll(fds, n, timeoutMs);
if (rc < 0 && errno != EINTR)
return -1;
queueDrainWake(&G.toWorker);
if (platform->pollEvents && platform->pollEvents(platform, 0) < 0)
return -1;
return 0;
}
static void processJobs(void)
{
Job *job;
while ((job = queuePop(&G.toWorker))) {
runJob(job);
G.dirty = 1;
}
}
static void *libMain(void *arg)
{
(void)arg;
G.thread = pthread_self();
G.workerKnown = 1;
memset(&G.platform, 0, sizeof(G.platform));
G.platform.backend = G.backend;
G.platform.mode = G.mode;
if (platformOpen(&G.platform, G.title, G.width, G.height) < 0) {
canvasErrorSet("platform open failed within lib");
setState(LIB_STATE_FAILED);
return NULL;
}
if (G.scale != 0.0f && G.platform.scale <= 0)
G.platform.scale = 1;
if (rendererInit(&G.renderer, &G.platform) != 0) {
if (!canvasErrorGet()[0])
canvasErrorSet("Vulkan instance failed within lib");
platformClose(&G.platform);
setState(LIB_STATE_FAILED);
return NULL;
}
G.useVulkan = 1;
if (nodeGraphInit() != 0) {
canvasErrorSet("widget tree init failed within lib");
rendererDestroy(&G.renderer);
platformClose(&G.platform);
setState(LIB_STATE_FAILED);
return NULL;
}
nodeApplyStyle(nodeRoot(), &G.theme);
G.running = 1;
G.dirty = 1;
setState(LIB_STATE_READY);
while (G.running && G.platform.running) {
if (pollWorker(G.dirty ? 0 : -1) < 0)
break;
processJobs();
{
Event event;
while (platformPop(&G.platform, &event)) {
G.dirty = 1;
if (event.kind == EVENT_QUIT)
G.running = 0;
}
}
if (G.dirty && G.running && G.platform.running) {
Hud hud;
rendererFillClear(&G.platform, &G.theme, G.mode, &hud);
if (rendererFrame(&G.renderer, &G.platform, &hud) < 0)
break;
G.dirty = 0;
}
}
nodeGraphDestroy();
if (G.useVulkan)
rendererDestroy(&G.renderer);
platformClose(&G.platform);
setState(LIB_STATE_STOPPED);
return NULL;
}
static void libCleanup(void)
{
queueDestroy(&G.toWorker);
queueDestroy(&G.toMain);
idPoolDestroy();
pthread_mutex_destroy(&G.stateMutex);
pthread_cond_destroy(&G.stateCond);
memset(&G, 0, sizeof(G));
}
static Backend mapBackend(A_Backend backend)
{
switch (backend) {
case A_BACKEND_X11:
return BACKEND_X11;
case A_BACKEND_WAYLAND:
return BACKEND_WAYLAND;
default:
return BACKEND_AUTO;
}
}
static Mode mapMode(A_Mode mode)
{
switch (mode) {
case A_MODE_SIMPLE:
return MODE_SIMPLE;
case A_MODE_TEXT:
return MODE_TEXT;
default:
return MODE_COMPLEX;
}
}
int libStart(int width, int height, const char *title, const CanvasOptions *opt)
{
LibState state;
int rc;
CanvasOptions defaults = {
.backend = A_BACKEND_AUTO,
.mode = A_MODE_COMPLEX,
.scale = 0.0f,
.theme = {
.backgroundColor = {16, 18, 24, 255},
.foregroundColor = {40, 44, 52, 255},
.textColor = {220, 224, 232, 255},
.fontSize = 14,
},
};
canvasErrorClear();
if (G.threadLive) {
canvasErrorSet("A_Init failed within lib: already running");
return -1;
}
if (!opt)
opt = &defaults;
memset(&G, 0, sizeof(G));
G.toWorker.wake_fd = -1;
G.toMain.wake_fd = -1;
G.width = width > 0 ? width : 800;
G.height = height > 0 ? height : 480;
G.backend = mapBackend(opt->backend);
G.mode = mapMode(opt->mode);
G.scale = opt->scale;
G.theme = opt->theme;
if (G.theme.backgroundColor.a == 0 && G.theme.backgroundColor.r == 0 &&
G.theme.backgroundColor.g == 0 && G.theme.backgroundColor.b == 0)
G.theme = defaults.theme;
snprintf(G.title, sizeof(G.title), "%s", title ? title : "window");
if (pthread_mutex_init(&G.stateMutex, NULL) != 0)
return -1;
if (pthread_cond_init(&G.stateCond, NULL) != 0) {
pthread_mutex_destroy(&G.stateMutex);
return -1;
}
if (idPoolInit() < 0) {
pthread_mutex_destroy(&G.stateMutex);
pthread_cond_destroy(&G.stateCond);
return -1;
}
if (queueInit(&G.toWorker) < 0 || queueInit(&G.toMain) < 0) {
libCleanup();
return -1;
}
G.state = LIB_STATE_STARTING;
rc = pthread_create(&G.thread, NULL, libMain, NULL);
if (rc != 0) {
libCleanup();
return rc;
}
G.threadLive = 1;
pthread_mutex_lock(&G.stateMutex);
while (G.state == LIB_STATE_STARTING)
pthread_cond_wait(&G.stateCond, &G.stateMutex);
state = G.state;
pthread_mutex_unlock(&G.stateMutex);
if (state != LIB_STATE_READY) {
pthread_join(G.thread, NULL);
G.threadLive = 0;
libCleanup();
return -1;
}
return 0;
}
void libWait(void)
{
if (!G.threadLive)
return;
pthread_join(G.thread, NULL);
G.threadLive = 0;
libCleanup();
}
int libIsOpen(void)
{
LibState state = libState();
return G.threadLive && (state == LIB_STATE_READY || state == LIB_STATE_BUSY);
}
void libStop(void)
{
if (!G.threadLive)
return;
libPushToWorker(jobQuit, NULL, 0);
pthread_join(G.thread, NULL);
G.threadLive = 0;
libCleanup();
}