pxl8/src/pxl8.c

529 lines
17 KiB
C
Raw Normal View History

2025-08-13 15:04:49 -05:00
#define PXL8_AUTHORS "asrael <asrael@pxl8.org>"
#define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org"
#define PXL8_VERSION "0.1.0"
#include <math.h>
#include <pthread.h>
2025-09-29 11:26:52 -05:00
#include <stdlib.h>
2025-08-13 15:04:49 -05:00
#include <sys/stat.h>
#include <unistd.h>
#include "linenoise.h"
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
2025-09-27 11:03:36 -05:00
#include "pxl8_cart.h"
2025-08-13 15:04:49 -05:00
#include "pxl8_lua.h"
#include "pxl8_macros.h"
#include "pxl8_types.h"
#define PXL8_MAX_REPL_COMMANDS 4096
typedef struct pxl8_repl_command {
char buffer[PXL8_MAX_REPL_COMMANDS];
struct pxl8_repl_command* next;
} pxl8_repl_command;
typedef struct pxl8_repl_state {
pthread_t thread;
pthread_mutex_t mutex;
pxl8_repl_command* queue_head;
pxl8_repl_command* queue_tail;
bool running;
} pxl8_repl_state;
2025-09-28 13:10:29 -05:00
typedef struct pxl8_state {
pxl8_cart* cart;
2025-08-13 15:04:49 -05:00
pxl8_color_mode color_mode;
pxl8_gfx_ctx gfx;
lua_State* lua;
pxl8_repl_state repl;
pxl8_resolution resolution;
f32 fps_timer;
i32 frame_count;
u64 last_time;
f32 time;
2025-09-27 11:03:36 -05:00
2025-08-13 15:04:49 -05:00
bool repl_mode;
bool running;
bool script_loaded;
char script_path[256];
time_t script_mod_time;
pxl8_input_state input;
2025-09-28 13:10:29 -05:00
} pxl8_state;
2025-08-13 15:04:49 -05:00
static void pxl8_repl_completion(const char* buf, linenoiseCompletions* lc) {
const char* fennel_keywords[] = {
"fn", "let", "var", "set", "global", "local",
"if", "when", "do", "while", "for", "each",
"lambda", "λ", "partial", "macro", "macros",
"require", "include", "import-macros",
"values", "select", "table", "length",
".", "..", ":", "->", "->>", "-?>", "-?>>",
"doto", "match", "case", "pick-values",
"collect", "icollect", "accumulate"
};
const char* pxl8_functions[] = {
"pxl8.clr", "pxl8.pixel", "pxl8.get_pixel",
"pxl8.line", "pxl8.rect", "pxl8.rect_fill",
"pxl8.circle", "pxl8.circle_fill", "pxl8.text",
"pxl8.get_screen", "pxl8.info", "pxl8.warn",
"pxl8.error", "pxl8.debug", "pxl8.trace"
};
auto buf_len = strlen(buf);
for (size_t i = 0; i < sizeof(fennel_keywords) / sizeof(fennel_keywords[0]); i++) {
if (strncmp(buf, fennel_keywords[i], buf_len) == 0) {
linenoiseAddCompletion(lc, fennel_keywords[i]);
}
}
for (size_t i = 0; i < sizeof(pxl8_functions) / sizeof(pxl8_functions[0]); i++) {
if (strncmp(buf, pxl8_functions[i], buf_len) == 0) {
linenoiseAddCompletion(lc, pxl8_functions[i]);
}
}
}
static char* pxl8_repl_hints(const char* buf, int* color, int* bold) {
if (strncmp(buf, "pxl8.", 5) == 0 && strlen(buf) == 5) {
*color = 35;
*bold = 0;
return "clr|pixel|line|rect|circle|text|get_screen";
}
if (strcmp(buf, "(fn") == 0) {
*color = 36;
*bold = 0;
return " [args] body)";
}
if (strcmp(buf, "(let") == 0) {
*color = 36;
*bold = 0;
return " [bindings] body)";
}
return NULL;
}
static void* pxl8_repl_stdin_thread(void* user_data) {
pxl8_repl_state* repl = (pxl8_repl_state*)user_data;
char* line;
const char* history_file = ".pxl8_history";
linenoiseHistorySetMaxLen(100);
linenoiseSetMultiLine(1);
linenoiseSetCompletionCallback(pxl8_repl_completion);
linenoiseSetHintsCallback(pxl8_repl_hints);
linenoiseHistoryLoad(history_file);
while (repl->running && (line = linenoise(">> "))) {
if (strlen(line) > 0) {
linenoiseHistoryAdd(line);
linenoiseHistorySave(history_file);
pxl8_repl_command* cmd = (pxl8_repl_command*)SDL_malloc(sizeof(pxl8_repl_command));
if (cmd) {
strncpy(cmd->buffer, line, PXL8_MAX_REPL_COMMANDS - 1);
cmd->buffer[PXL8_MAX_REPL_COMMANDS - 1] = '\0';
cmd->next = NULL;
pthread_mutex_lock(&repl->mutex);
if (repl->queue_tail) {
repl->queue_tail->next = cmd;
repl->queue_tail = cmd;
} else {
repl->queue_head = repl->queue_tail = cmd;
}
pthread_mutex_unlock(&repl->mutex);
}
}
linenoiseFree(line);
}
return NULL;
}
static void pxl8_repl_init(pxl8_repl_state* repl) {
repl->queue_head = NULL;
repl->queue_tail = NULL;
repl->running = true;
pthread_mutex_init(&repl->mutex, NULL);
pthread_create(&repl->thread, NULL, pxl8_repl_stdin_thread, repl);
}
static void pxl8_repl_shutdown(pxl8_repl_state* repl) {
repl->running = false;
pthread_join(repl->thread, NULL);
pthread_mutex_destroy(&repl->mutex);
pxl8_repl_command* cmd = repl->queue_head;
while (cmd) {
pxl8_repl_command* next = cmd->next;
SDL_free(cmd);
cmd = next;
}
}
static pxl8_repl_command* pxl8_repl_pop_command(pxl8_repl_state* repl) {
pthread_mutex_lock(&repl->mutex);
pxl8_repl_command* cmd = repl->queue_head;
if (cmd) {
repl->queue_head = cmd->next;
if (!repl->queue_head) {
repl->queue_tail = NULL;
}
}
pthread_mutex_unlock(&repl->mutex);
return cmd;
}
2025-09-28 13:10:29 -05:00
static void load_script(pxl8_state* app) {
2025-08-13 15:04:49 -05:00
const char* ext = strrchr(app->script_path, '.');
if (ext && strcmp(ext, ".fnl") == 0) {
pxl8_result result = pxl8_lua_run_fennel_file(app->lua, app->script_path);
if (result == PXL8_OK) {
pxl8_info("Loaded script: %s", app->script_path);
app->script_loaded = true;
lua_getglobal(app->lua, "init");
if (lua_isfunction(app->lua, -1)) {
lua_pcall(app->lua, 0, 0, 0);
} else {
pxl8_warn("No init function found (type: %s)", lua_typename(app->lua, lua_type(app->lua, -1)));
lua_pop(app->lua, 1);
}
} else {
pxl8_warn("Failed to load script: %s", app->script_path);
app->script_loaded = false;
}
} else if (ext && strcmp(ext, ".lua") == 0) {
pxl8_result result = pxl8_lua_run_file(app->lua, app->script_path);
if (result == PXL8_OK) {
pxl8_info("Loaded script: %s", app->script_path);
app->script_loaded = true;
lua_getglobal(app->lua, "init");
if (lua_isfunction(app->lua, -1)) {
lua_pcall(app->lua, 0, 0, 0);
} else {
pxl8_warn("No init function found (type: %s)", lua_typename(app->lua, lua_type(app->lua, -1)));
lua_pop(app->lua, 1);
}
} else {
pxl8_warn("Failed to load script: %s", app->script_path);
app->script_loaded = false;
}
} else {
pxl8_warn("Unknown script type for: %s (expected .fnl or .lua)", app->script_path);
app->script_loaded = false;
}
}
static time_t get_file_mod_time(const char* path) {
struct stat file_stat;
if (stat(path, &file_stat) == 0) {
return file_stat.st_mtime;
}
return 0;
}
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
2025-09-28 13:10:29 -05:00
static pxl8_state app = {0};
2025-08-13 15:04:49 -05:00
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
pxl8_error("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
app.color_mode = PXL8_COLOR_MODE_MEGA;
app.resolution = PXL8_RESOLUTION_640x360;
2025-09-27 11:03:36 -05:00
2025-08-13 15:04:49 -05:00
const char* script_arg = NULL;
2025-09-27 11:03:36 -05:00
bool pack_mode = false;
const char* pack_input = NULL;
const char* pack_output = NULL;
2025-08-13 15:04:49 -05:00
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--repl") == 0) {
app.repl_mode = true;
2025-09-27 11:03:36 -05:00
} else if (strcmp(argv[i], "--pack") == 0) {
pack_mode = true;
if (i + 2 < argc) {
pack_input = argv[++i];
pack_output = argv[++i];
} else {
pxl8_error("--pack requires <folder> <output.pxc>");
return SDL_APP_FAILURE;
}
2025-08-13 15:04:49 -05:00
} else if (!script_arg) {
script_arg = argv[i];
}
}
2025-09-27 11:03:36 -05:00
if (pack_mode) {
pxl8_result result = pxl8_cart_pack(pack_input, pack_output);
return (result == PXL8_OK) ? SDL_APP_SUCCESS : SDL_APP_FAILURE;
2025-08-13 15:04:49 -05:00
}
if (app.repl_mode) {
fprintf(stderr, "\033[38;2;184;187;38m[pxl8]\033[0m Starting in REPL mode with script: %s\n", app.script_path);
}
pxl8_info("Starting up");
if (pxl8_gfx_init(&app.gfx, app.color_mode, app.resolution, "pxl8", 1280, 720) != PXL8_OK) {
pxl8_error("Failed to initialize graphics context");
return SDL_APP_FAILURE;
}
if (pxl8_gfx_load_font_atlas(&app.gfx) != PXL8_OK) {
pxl8_error("Failed to load font atlas");
return SDL_APP_FAILURE;
}
if (pxl8_gfx_init_atlas(&app.gfx, 1024, 1024) != PXL8_OK) {
pxl8_error("Failed to initialize sprite atlas");
return SDL_APP_FAILURE;
}
pxl8_result lua_result = pxl8_lua_init(&app.lua);
if (lua_result != PXL8_OK) {
pxl8_error("Failed to initialize Lua scripting!");
pxl8_gfx_shutdown(&app.gfx);
return SDL_APP_FAILURE;
}
2025-09-27 11:03:36 -05:00
2025-09-28 13:10:29 -05:00
const char* cart_path = script_arg ? script_arg : "demo";
struct stat st;
bool is_cart = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) ||
(cart_path && strstr(cart_path, ".pxc"));
if (is_cart) {
char* original_cwd = getcwd(NULL, 0);
app.cart = calloc(1, sizeof(pxl8_cart));
if (!app.cart) {
pxl8_error("Failed to allocate memory for cart");
return false;
}
if (pxl8_cart_load(app.cart, cart_path) == PXL8_OK) {
pxl8_lua_setup_cart_path(app.lua, app.cart->base_path, original_cwd);
pxl8_cart_mount(app.cart);
strcpy(app.script_path, "main.fnl");
pxl8_info("Loaded cart: %s", app.cart->name);
2025-09-27 11:03:36 -05:00
} else {
2025-09-28 13:10:29 -05:00
pxl8_error("Failed to load cart: %s", cart_path);
return SDL_APP_FAILURE;
2025-09-27 11:03:36 -05:00
}
2025-09-28 13:10:29 -05:00
free(original_cwd);
} else if (script_arg) {
strncpy(app.script_path, script_arg, sizeof(app.script_path) - 1);
app.script_path[sizeof(app.script_path) - 1] = '\0';
2025-09-27 11:03:36 -05:00
}
2025-09-28 13:10:29 -05:00
2025-08-13 15:04:49 -05:00
pxl8_lua_setup_contexts(app.lua, &app.gfx, &app.input);
2025-09-28 13:10:29 -05:00
if (app.script_path[0] != '\0') {
app.script_mod_time = get_file_mod_time(app.script_path);
load_script(&app);
}
2025-08-13 15:04:49 -05:00
if (app.repl_mode) {
pxl8_repl_init(&app.repl);
fprintf(stderr, "\033[38;2;184;187;38m[pxl8 REPL]\033[0m Fennel %s - Tab for completions, Ctrl-C to exit\n", "1.5.1");
lua_getglobal(app.lua, "require");
lua_pushstring(app.lua, "pxl8");
if (lua_pcall(app.lua, 1, 1, 0) == 0) {
lua_setglobal(app.lua, "pxl8");
} else {
const char* error_msg = lua_tostring(app.lua, -1);
fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", error_msg);
lua_pop(app.lua, 1);
}
}
app.last_time = SDL_GetTicksNS();
app.running = true;
*appstate = &app;
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void* appstate) {
2025-09-28 13:10:29 -05:00
pxl8_state* app = (pxl8_state*)appstate;
2025-08-13 15:04:49 -05:00
int width, height;
SDL_GetWindowSize(app->gfx.window, &width, &height);
u64 current_time = SDL_GetTicksNS();
float dt = (float)(current_time - app->last_time) / 1000000000.0f;
app->frame_count++;
app->fps_timer += dt;
app->last_time = current_time;
app->time += dt;
if (app->fps_timer >= 3.0f) {
if (!app->repl_mode) {
float avg_fps = app->frame_count / app->fps_timer;
pxl8_info("FPS: %.1f", avg_fps);
}
app->frame_count = 0;
app->fps_timer = 0.0f;
}
time_t current_mod_time = get_file_mod_time(app->script_path);
if (current_mod_time != app->script_mod_time && current_mod_time != 0) {
pxl8_info("Script modified, reloading: %s", app->script_path);
app->script_mod_time = current_mod_time;
load_script(app);
}
if (app->repl_mode) {
pxl8_repl_command* cmd = pxl8_repl_pop_command(&app->repl);
if (cmd) {
pxl8_result result = pxl8_lua_eval_fennel(app->lua, cmd->buffer);
if (result == PXL8_OK) {
if (lua_gettop(app->lua) > 0 && !lua_isnil(app->lua, -1)) {
const char* result_str = lua_tostring(app->lua, -1);
if (result_str) {
fprintf(stdout, "%s\n", result_str);
} else if (lua_isuserdata(app->lua, -1)) {
fprintf(stdout, "<userdata>\n");
} else if (lua_iscfunction(app->lua, -1)) {
fprintf(stdout, "<function>\n");
} else if (lua_istable(app->lua, -1)) {
fprintf(stdout, "<table>\n");
} else {
fprintf(stdout, "<%s>\n", lua_typename(app->lua, lua_type(app->lua, -1)));
}
lua_pop(app->lua, 1);
}
} else {
const char* error_msg = lua_tostring(app->lua, -1);
pxl8_error("%s", error_msg ? error_msg : "Unknown error");
lua_pop(app->lua, 1);
}
SDL_free(cmd);
}
}
if (app->script_loaded) {
lua_getglobal(app->lua, "update");
if (lua_isfunction(app->lua, -1)) {
lua_pushnumber(app->lua, dt);
lua_pcall(app->lua, 1, 0, 0);
} else {
lua_pop(app->lua, 1);
}
lua_getglobal(app->lua, "draw");
if (lua_isfunction(app->lua, -1)) {
int result = lua_pcall(app->lua, 0, 0, 0);
if (result != 0) {
pxl8_error("Error calling draw: %s", lua_tostring(app->lua, -1));
lua_pop(app->lua, 1);
}
} else {
pxl8_warn("draw is not a function, type: %s", lua_typename(app->lua, lua_type(app->lua, -1)));
lua_pop(app->lua, 1);
}
} else {
pxl8_clr(&app->gfx, 32);
i32 render_width, render_height;
pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height);
for (int y = 0; y < render_height; y += 24) {
for (int x = 0; x < render_width; x += 32) {
u32 color = ((x / 32) + (y / 24) + (int)(app->time * 2)) % 8;
pxl8_rect_fill(&app->gfx, x, y, 31, 23, color);
}
}
}
i32 render_width, render_height;
pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height);
float scale = fminf(width / (float)render_width, height / (float)render_height);
int scaled_width = (int)(render_width * scale);
int scaled_height = (int)(render_height * scale);
int offset_x = (width - scaled_width) / 2;
int offset_y = (height - scaled_height) / 2;
pxl8_gfx_viewport(&app->gfx, offset_x, offset_y, scaled_width, scaled_height);
pxl8_gfx_upload_framebuffer(&app->gfx);
pxl8_gfx_upload_atlas(&app->gfx);
pxl8_gfx_present(&app->gfx);
SDL_memset(app->input.keys_pressed, 0, sizeof(app->input.keys_pressed));
return app->running ? SDL_APP_CONTINUE : SDL_APP_SUCCESS;
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
2025-09-28 13:10:29 -05:00
pxl8_state* app = (pxl8_state*)appstate;
2025-08-13 15:04:49 -05:00
switch (event->type) {
case SDL_EVENT_QUIT:
app->running = false;
return SDL_APP_SUCCESS;
case SDL_EVENT_KEY_DOWN: {
if (event->key.key == SDLK_ESCAPE) {
app->running = false;
return SDL_APP_SUCCESS;
}
SDL_Keycode key = event->key.key;
if (key < 256) {
if (!app->input.keys[key]) {
app->input.keys_pressed[key] = true;
}
app->input.keys[key] = true;
}
break;
}
case SDL_EVENT_KEY_UP: {
SDL_Keycode key = event->key.key;
if (key < 256) {
app->input.keys[key] = false;
app->input.keys_pressed[key] = false;
}
break;
}
}
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void* appstate, SDL_AppResult result) {
2025-09-28 13:10:29 -05:00
pxl8_state* app = (pxl8_state*)appstate;
2025-08-13 15:04:49 -05:00
(void)result;
if (app) {
pxl8_info("Shutting down");
if (app->repl_mode) {
pxl8_repl_shutdown(&app->repl);
}
2025-09-27 11:03:36 -05:00
if (app->cart) {
2025-09-28 13:10:29 -05:00
pxl8_cart_unload(app->cart);
free(app->cart);
2025-09-27 11:03:36 -05:00
app->cart = NULL;
}
2025-08-13 15:04:49 -05:00
pxl8_lua_shutdown(app->lua);
pxl8_gfx_shutdown(&app->gfx);
}
SDL_Quit();
}