#define PXL8_AUTHORS "asrael " #define PXL8_COPYRIGHT "Copyright (c) 2024-2025 pxl8.org" #define PXL8_VERSION "0.1.0" #include #include #include #include #include #include #define SDL_MAIN_USE_CALLBACKS #include #include #include "pxl8_cart.h" #include "pxl8_macros.h" #include "pxl8_script.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* gfx; pxl8_repl_state repl; pxl8_resolution resolution; pxl8_script* script; f32 fps_timer; i32 frame_count; u64 last_time; f32 time; bool repl_mode; bool running; bool script_loaded; char script_path[256]; 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; } 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 (i32 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"); app.gfx = pxl8_gfx_create(app.color_mode, app.resolution, "pxl8", 1280, 720); if (!app.gfx) { pxl8_error("Failed to create 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; } app.script = pxl8_script_create(); if (!app.script) { pxl8_error("Failed to initialize scripting: %s", pxl8_script_get_last_error(app.script)); pxl8_gfx_destroy(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 = pxl8_cart_create(); if (!app.cart) { pxl8_error("Failed to create cart"); return false; } if (pxl8_cart_load(app.cart, cart_path) == PXL8_OK) { pxl8_script_set_cart_path(app.script, pxl8_cart_get_base_path(app.cart), original_cwd); pxl8_cart_mount(app.cart); strcpy(app.script_path, "main.fnl"); pxl8_info("Loaded cart: %s", pxl8_cart_get_name(app.cart)); } 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_script_set_gfx(app.script, app.gfx); pxl8_script_set_input(app.script, &app.input); if (app.script_path[0] != '\0') { pxl8_result result = pxl8_script_load_main(app.script, app.script_path); app.script_loaded = (result == PXL8_OK); } 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"); if (pxl8_script_load_module(app.script, "pxl8") != PXL8_OK) { fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(app.script)); } } 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; pxl8_bounds bounds = pxl8_gfx_get_bounds(app->gfx); u64 current_time = SDL_GetTicksNS(); f32 dt = (f32)(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) { f32 avg_fps = app->frame_count / app->fps_timer; pxl8_info("FPS: %.1f", avg_fps); } app->frame_count = 0; app->fps_timer = 0.0f; } pxl8_script_check_reload(app->script); if (app->repl_mode) { pxl8_repl_command* cmd = pxl8_repl_pop_command(&app->repl); if (cmd) { pxl8_result result = pxl8_script_eval(app->script, cmd->buffer); if (result != PXL8_OK) { pxl8_error("%s", pxl8_script_get_last_error(app->script)); } SDL_free(cmd); } } if (app->script_loaded) { pxl8_script_call_function_f32(app->script, "update", dt); pxl8_result frame_result = pxl8_script_call_function(app->script, "frame"); if (frame_result == PXL8_ERROR_SCRIPT_ERROR) { pxl8_error("Error calling frame: %s", pxl8_script_get_last_error(app->script)); } } else { pxl8_clr(app->gfx, 32); i32 render_width, render_height; pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); for (i32 y = 0; y < render_height; y += 24) { for (i32 x = 0; x < render_width; x += 32) { u32 color = ((x / 32) + (y / 24) + (i32)(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); f32 scale = fminf(bounds.w / (f32)render_width, bounds.h / (f32)render_height); i32 scaled_width = (i32)(render_width * scale); i32 scaled_height = (i32)(render_height * scale); i32 offset_x = (bounds.w - scaled_width) / 2; i32 offset_y = (bounds.h - 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_script_destroy(app->script); pxl8_gfx_destroy(app->gfx); } SDL_Quit(); }