#define PXL8_AUTHORS "asrael " #define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org" #define PXL8_VERSION "0.1.0" #include #include #include #include #include "linenoise.h" #define SDL_MAIN_USE_CALLBACKS #include #include #include "pxl8_cart.h" #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; typedef struct pxl8_state { pxl8_cart* cart; 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; bool repl_mode; bool running; bool script_loaded; char script_path[256]; time_t script_mod_time; pxl8_input_state input; } pxl8_state; 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; } static void load_script(pxl8_state* app) { 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[]) { static pxl8_state app = {0}; 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; const char* script_arg = NULL; bool pack_mode = false; const char* pack_input = NULL; const char* pack_output = NULL; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--repl") == 0) { app.repl_mode = true; } 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 "); return SDL_APP_FAILURE; } } else if (!script_arg) { script_arg = argv[i]; } } if (pack_mode) { pxl8_result result = pxl8_cart_pack(pack_input, pack_output); return (result == PXL8_OK) ? SDL_APP_SUCCESS : SDL_APP_FAILURE; } 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; } 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); } else { pxl8_error("Failed to load cart: %s", cart_path); return SDL_APP_FAILURE; } 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'; } pxl8_lua_setup_contexts(app.lua, &app.gfx, &app.input); if (app.script_path[0] != '\0') { app.script_mod_time = get_file_mod_time(app.script_path); load_script(&app); } 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) { pxl8_state* app = (pxl8_state*)appstate; 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, "\n"); } else if (lua_iscfunction(app->lua, -1)) { fprintf(stdout, "\n"); } else if (lua_istable(app->lua, -1)) { fprintf(stdout, "\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) { pxl8_state* app = (pxl8_state*)appstate; 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) { pxl8_state* app = (pxl8_state*)appstate; (void)result; if (app) { pxl8_info("Shutting down"); if (app->repl_mode) { pxl8_repl_shutdown(&app->repl); } if (app->cart) { pxl8_cart_unload(app->cart); free(app->cart); app->cart = NULL; } pxl8_lua_shutdown(app->lua); pxl8_gfx_shutdown(&app->gfx); } SDL_Quit(); }