refactor SDL out of core files

This commit is contained in:
asrael 2025-10-17 17:54:33 -05:00
parent 82ed6b4ea9
commit 323bd2a7c1
21 changed files with 1427 additions and 1029 deletions

View file

@ -2,7 +2,7 @@
(var camera-angle 0) (var camera-angle 0)
(local camera-height 0) (local camera-height 0)
(local camera-distance 300) (local camera-distance 450)
(var fps 0) (var fps 0)
(var world nil) (var world nil)

View file

@ -383,6 +383,7 @@ case "$COMMAND" in
PXL8_SOURCE_FILES=" PXL8_SOURCE_FILES="
src/pxl8.c src/pxl8.c
src/pxl8_ase.c src/pxl8_ase.c
src/pxl8_atlas.c
src/pxl8_blit.c src/pxl8_blit.c
src/pxl8_bsp.c src/pxl8_bsp.c
src/pxl8_cart.c src/pxl8_cart.c
@ -391,6 +392,7 @@ case "$COMMAND" in
src/pxl8_io.c src/pxl8_io.c
src/pxl8_math.c src/pxl8_math.c
src/pxl8_script.c src/pxl8_script.c
src/pxl8_sdl3.c
src/pxl8_tilemap.c src/pxl8_tilemap.c
src/pxl8_tilesheet.c src/pxl8_tilesheet.c
src/pxl8_ui.c src/pxl8_ui.c

View file

@ -4,197 +4,34 @@
#include <pthread.h> #include <pthread.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <linenoise.h> #include <linenoise.h>
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "pxl8_cart.h" #include "pxl8_cart.h"
#include "pxl8_game.h"
#include "pxl8_hal.h"
#include "pxl8_macros.h" #include "pxl8_macros.h"
#include "pxl8_script.h" #include "pxl8_script.h"
#include "pxl8_sdl3.h"
#include "pxl8_types.h" #include "pxl8_types.h"
#include "pxl8_ui.h" #include "pxl8_ui.h"
#define PXL8_MAX_REPL_COMMANDS 4096 pxl8_game_result pxl8_init(pxl8_game* game, i32 argc, char* argv[]) {
if (!game) {
typedef struct pxl8_repl_command { return PXL8_GAME_FAILURE;
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;
pxl8_ui* ui;
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++) { #ifdef PXL8_TARGET_GBA
if (strncmp(buf, pxl8_functions[i], buf_len) == 0) { game->hal = &pxl8_hal_gba;
linenoiseAddCompletion(lc, pxl8_functions[i]); #else
} game->hal = &pxl8_hal_sdl3;
} #endif
}
static char* pxl8_repl_hints(const char* buf, int* color, int* bold) { game->color_mode = PXL8_COLOR_MODE_MEGA;
if (strncmp(buf, "pxl8.", 5) == 0 && strlen(buf) == 5) { game->resolution = PXL8_RESOLUTION_640x360;
*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; const char* script_arg = NULL;
bool pack_mode = false; bool pack_mode = false;
@ -203,7 +40,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
for (i32 i = 1; i < argc; i++) { for (i32 i = 1; i < argc; i++) {
if (strcmp(argv[i], "--repl") == 0) { if (strcmp(argv[i], "--repl") == 0) {
app.repl_mode = true; game->repl_mode = true;
} else if (strcmp(argv[i], "--pack") == 0) { } else if (strcmp(argv[i], "--pack") == 0) {
pack_mode = true; pack_mode = true;
if (i + 2 < argc) { if (i + 2 < argc) {
@ -211,7 +48,7 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
pack_output = argv[++i]; pack_output = argv[++i];
} else { } else {
pxl8_error("--pack requires <folder> <output.pxc>"); pxl8_error("--pack requires <folder> <output.pxc>");
return SDL_APP_FAILURE; return PXL8_GAME_FAILURE;
} }
} else if (!script_arg) { } else if (!script_arg) {
script_arg = argv[i]; script_arg = argv[i];
@ -220,39 +57,39 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
if (pack_mode) { if (pack_mode) {
pxl8_result result = pxl8_cart_pack(pack_input, pack_output); pxl8_result result = pxl8_cart_pack(pack_input, pack_output);
return (result == PXL8_OK) ? SDL_APP_SUCCESS : SDL_APP_FAILURE; return (result == PXL8_OK) ? PXL8_GAME_SUCCESS : PXL8_GAME_FAILURE;
} }
if (app.repl_mode) { if (game->repl_mode) {
fprintf(stderr, "\033[38;2;184;187;38m[pxl8]\033[0m Starting in REPL mode with script: %s\n", app.script_path); fprintf(stderr, "\033[38;2;184;187;38m[pxl8]\033[0m Starting in REPL mode with script: %s\n", game->script_path);
} }
pxl8_info("Starting up"); pxl8_info("Starting up");
app.gfx = pxl8_gfx_create(app.color_mode, app.resolution, "pxl8", 1280, 720); game->gfx = pxl8_gfx_create(game->hal, game->color_mode, game->resolution, "pxl8", 1280, 720);
if (!app.gfx) { if (!game->gfx) {
pxl8_error("Failed to create graphics context"); pxl8_error("Failed to create graphics context");
return SDL_APP_FAILURE; return PXL8_GAME_FAILURE;
} }
if (pxl8_gfx_load_font_atlas(app.gfx) != PXL8_OK) { if (pxl8_gfx_load_font_atlas(game->gfx) != PXL8_OK) {
pxl8_error("Failed to load font atlas"); pxl8_error("Failed to load font atlas");
return SDL_APP_FAILURE; return PXL8_GAME_FAILURE;
} }
app.ui = pxl8_ui_create(app.gfx); game->ui = pxl8_ui_create(game->gfx);
if (!app.ui) { if (!game->ui) {
pxl8_error("Failed to create UI"); pxl8_error("Failed to create UI");
pxl8_gfx_destroy(app.gfx); pxl8_gfx_destroy(game->gfx);
return SDL_APP_FAILURE; return PXL8_GAME_FAILURE;
} }
app.script = pxl8_script_create(); game->script = pxl8_script_create();
if (!app.script) { if (!game->script) {
pxl8_error("Failed to initialize scripting: %s", pxl8_script_get_last_error(app.script)); pxl8_error("Failed to initialize scripting: %s", pxl8_script_get_last_error(game->script));
pxl8_gfx_destroy(app.gfx); pxl8_gfx_destroy(game->gfx);
return SDL_APP_FAILURE; return PXL8_GAME_FAILURE;
} }
const char* cart_path = script_arg ? script_arg : "demo"; const char* cart_path = script_arg ? script_arg : "demo";
@ -263,244 +100,188 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
if (is_cart) { if (is_cart) {
char* original_cwd = getcwd(NULL, 0); char* original_cwd = getcwd(NULL, 0);
app.cart = pxl8_cart_create(); game->cart = pxl8_cart_create();
if (!app.cart) { if (!game->cart) {
pxl8_error("Failed to create cart"); pxl8_error("Failed to create cart");
return false; return PXL8_GAME_FAILURE;
} }
if (pxl8_cart_load(app.cart, cart_path) == PXL8_OK) { if (pxl8_cart_load(game->cart, cart_path) == PXL8_OK) {
pxl8_script_set_cart_path(app.script, pxl8_cart_get_base_path(app.cart), original_cwd); pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(game->cart), original_cwd);
pxl8_cart_mount(app.cart); pxl8_cart_mount(game->cart);
strcpy(app.script_path, "main.fnl"); strcpy(game->script_path, "main.fnl");
pxl8_info("Loaded cart: %s", pxl8_cart_get_name(app.cart)); pxl8_info("Loaded cart: %s", pxl8_cart_get_name(game->cart));
} else { } else {
pxl8_error("Failed to load cart: %s", cart_path); pxl8_error("Failed to load cart: %s", cart_path);
return SDL_APP_FAILURE; return PXL8_GAME_FAILURE;
} }
free(original_cwd); free(original_cwd);
} else if (script_arg) { } else if (script_arg) {
strncpy(app.script_path, script_arg, sizeof(app.script_path) - 1); strncpy(game->script_path, script_arg, sizeof(game->script_path) - 1);
app.script_path[sizeof(app.script_path) - 1] = '\0'; game->script_path[sizeof(game->script_path) - 1] = '\0';
} }
pxl8_script_set_gfx(app.script, app.gfx); pxl8_script_set_gfx(game->script, game->gfx);
pxl8_script_set_input(app.script, &app.input); pxl8_script_set_input(game->script, &game->input);
pxl8_script_set_ui(app.script, app.ui); pxl8_script_set_ui(game->script, game->ui);
if (app.script_path[0] != '\0') { if (game->script_path[0] != '\0') {
pxl8_result result = pxl8_script_load_main(app.script, app.script_path); pxl8_result result = pxl8_script_load_main(game->script, game->script_path);
app.script_loaded = (result == PXL8_OK); game->script_loaded = (result == PXL8_OK);
} }
if (app.repl_mode) { if (game->repl_mode) {
pxl8_repl_init(&app.repl); game->repl = pxl8_script_repl_create();
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 (game->repl) {
pxl8_script_repl_init(game->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) { if (pxl8_script_load_module(game->script, "pxl8") != PXL8_OK) {
fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(app.script)); fprintf(stderr, "Warning: Failed to setup pxl8 global: %s\n", pxl8_script_get_last_error(game->script));
}
} }
} }
app.last_time = SDL_GetTicksNS(); game->last_time = game->hal->get_ticks();
app.running = true; game->running = true;
*appstate = &app;
return SDL_APP_CONTINUE; return PXL8_GAME_CONTINUE;
} }
SDL_AppResult SDL_AppIterate(void* appstate) { pxl8_game_result pxl8_update(pxl8_game* game) {
pxl8_state* app = (pxl8_state*)appstate; if (!game) {
pxl8_bounds bounds = pxl8_gfx_get_bounds(app->gfx); return PXL8_GAME_FAILURE;
}
u64 current_time = SDL_GetTicksNS(); u64 current_time = game->hal->get_ticks();
f32 dt = (f32)(current_time - app->last_time) / 1000000000.0f; f32 dt = (f32)(current_time - game->last_time) / 1000000000.0f;
app->last_time = current_time; game->last_time = current_time;
app->time += dt; game->time += dt;
pxl8_script_check_reload(app->script); pxl8_script_check_reload(game->script);
if (app->repl_mode) { if (game->repl_mode && game->repl) {
pxl8_repl_command* cmd = pxl8_repl_pop_command(&app->repl); pxl8_script_repl_command* cmd = pxl8_script_repl_pop_command(game->repl);
if (cmd) { if (cmd) {
pxl8_result result = pxl8_script_eval(app->script, cmd->buffer); pxl8_result result = pxl8_script_eval(game->script, pxl8_script_repl_command_buffer(cmd));
if (result != PXL8_OK) { if (result != PXL8_OK) {
pxl8_error("%s", pxl8_script_get_last_error(app->script)); pxl8_error("%s", pxl8_script_get_last_error(game->script));
} }
SDL_free(cmd); pxl8_script_repl_command_free(cmd);
} }
} }
if (app->ui) { if (game->ui) {
pxl8_ui_input_mousemove(app->ui, app->input.mouse_x, app->input.mouse_y); i32 render_width, render_height;
if (app->input.mouse_wheel_x != 0 || app->input.mouse_wheel_y != 0) { pxl8_gfx_get_resolution_dimensions(game->resolution, &render_width, &render_height);
pxl8_ui_input_scroll(app->ui, app->input.mouse_wheel_x * 10, -app->input.mouse_wheel_y * 10);
pxl8_ui_input_mousemove(game->ui, game->input.mouse_x, game->input.mouse_y);
if (game->input.mouse_wheel_x != 0 || game->input.mouse_wheel_y != 0) {
pxl8_ui_input_scroll(game->ui, game->input.mouse_wheel_x * 10, -game->input.mouse_wheel_y * 10);
} }
pxl8_ui_frame_begin(app->ui);
for (i32 i = 0; i < 3; i++) { for (i32 i = 0; i < 3; i++) {
if (app->input.mouse_buttons_pressed[i]) { if (game->input.mouse_buttons_pressed[i]) {
pxl8_ui_input_mousedown(app->ui, app->input.mouse_x, app->input.mouse_y, i + 1); pxl8_ui_input_mousedown(game->ui, game->input.mouse_x, game->input.mouse_y, i + 1);
} }
if (app->input.mouse_buttons_released[i]) { if (game->input.mouse_buttons_released[i]) {
pxl8_ui_input_mouseup(app->ui, app->input.mouse_x, app->input.mouse_y, i + 1); pxl8_ui_input_mouseup(game->ui, game->input.mouse_x, game->input.mouse_y, i + 1);
} }
} }
for (i32 key = 0; key < 256; key++) { for (i32 key = 0; key < 256; key++) {
if (app->input.keys_pressed[key]) { if (game->input.keys_pressed[key]) {
pxl8_ui_input_keydown(app->ui, key); pxl8_ui_input_keydown(game->ui, key);
} }
if (!app->input.keys_down[key] && app->input.keys_pressed[key]) { if (!game->input.keys_down[key] && game->input.keys_pressed[key]) {
pxl8_ui_input_keyup(app->ui, key); pxl8_ui_input_keyup(game->ui, key);
} }
} }
pxl8_ui_frame_begin(game->ui);
} }
if (app->script_loaded) { if (game->script_loaded) {
pxl8_script_call_function_f32(app->script, "update", dt); pxl8_script_call_function_f32(game->script, "update", dt);
}
pxl8_result frame_result = pxl8_script_call_function(app->script, "frame"); return PXL8_GAME_CONTINUE;
}
pxl8_game_result pxl8_frame(pxl8_game* game) {
if (!game) {
return PXL8_GAME_FAILURE;
}
pxl8_bounds bounds = pxl8_gfx_get_bounds(game->gfx);
if (game->script_loaded) {
pxl8_result frame_result = pxl8_script_call_function(game->script, "frame");
if (frame_result == PXL8_ERROR_SCRIPT_ERROR) { if (frame_result == PXL8_ERROR_SCRIPT_ERROR) {
pxl8_error("Error calling frame: %s", pxl8_script_get_last_error(app->script)); pxl8_error("Error calling frame: %s", pxl8_script_get_last_error(game->script));
} }
} else { } else {
pxl8_clr(app->gfx, 32); pxl8_clr(game->gfx, 32);
i32 render_width, render_height; i32 render_width, render_height;
pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); pxl8_gfx_get_resolution_dimensions(game->resolution, &render_width, &render_height);
for (i32 y = 0; y < render_height; y += 24) { for (i32 y = 0; y < render_height; y += 24) {
for (i32 x = 0; x < render_width; x += 32) { for (i32 x = 0; x < render_width; x += 32) {
u32 color = ((x / 32) + (y / 24) + (i32)(app->time * 2)) % 8; u32 color = ((x / 32) + (y / 24) + (i32)(game->time * 2)) % 8;
pxl8_rect_fill(app->gfx, x, y, 31, 23, color); pxl8_rect_fill(game->gfx, x, y, 31, 23, color);
} }
} }
} }
i32 render_width, render_height; i32 render_width, render_height;
pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height); pxl8_gfx_get_resolution_dimensions(game->resolution, &render_width, &render_height);
if (app->ui) { if (game->ui) {
pxl8_ui_frame_end(app->ui); pxl8_ui_frame_end(game->ui);
} }
pxl8_gfx_set_viewport(app->gfx, pxl8_gfx_viewport(bounds, render_width, render_height)); pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, render_width, render_height));
pxl8_gfx_upload_framebuffer(app->gfx); pxl8_gfx_upload_framebuffer(game->gfx);
pxl8_gfx_upload_atlas(app->gfx); pxl8_gfx_upload_atlas(game->gfx);
pxl8_gfx_present(app->gfx); pxl8_gfx_present(game->gfx);
SDL_memset(app->input.keys_pressed, 0, sizeof(app->input.keys_pressed)); memset(game->input.keys_pressed, 0, sizeof(game->input.keys_pressed));
SDL_memset(app->input.keys_released, 0, sizeof(app->input.keys_released)); memset(game->input.keys_released, 0, sizeof(game->input.keys_released));
SDL_memset(app->input.mouse_buttons_pressed, 0, sizeof(app->input.mouse_buttons_pressed)); memset(game->input.mouse_buttons_pressed, 0, sizeof(game->input.mouse_buttons_pressed));
SDL_memset(app->input.mouse_buttons_released, 0, sizeof(app->input.mouse_buttons_released)); memset(game->input.mouse_buttons_released, 0, sizeof(game->input.mouse_buttons_released));
app->input.mouse_wheel_x = 0; game->input.mouse_wheel_x = 0;
app->input.mouse_wheel_y = 0; game->input.mouse_wheel_y = 0;
return app->running ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; return game->running ? PXL8_GAME_CONTINUE : PXL8_GAME_SUCCESS;
} }
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) { void pxl8_quit(pxl8_game* game) {
pxl8_state* app = (pxl8_state*)appstate; if (!game) {
return;
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_Scancode scancode = event->key.scancode;
if (scancode < 256) {
if (!app->input.keys_down[scancode]) {
app->input.keys_pressed[scancode] = true;
}
app->input.keys_down[scancode] = true;
app->input.keys_released[scancode] = false;
}
break;
}
case SDL_EVENT_KEY_UP: {
SDL_Scancode scancode = event->key.scancode;
if (scancode < 256) {
app->input.keys_down[scancode] = false;
app->input.keys_pressed[scancode] = false;
app->input.keys_released[scancode] = true;
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
u8 button = event->button.button - 1;
if (button < 3) {
if (!app->input.mouse_buttons_down[button]) {
app->input.mouse_buttons_pressed[button] = true;
}
app->input.mouse_buttons_down[button] = true;
app->input.mouse_buttons_released[button] = false;
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_UP: {
u8 button = event->button.button - 1;
if (button < 3) {
app->input.mouse_buttons_down[button] = false;
app->input.mouse_buttons_pressed[button] = false;
app->input.mouse_buttons_released[button] = true;
}
break;
}
case SDL_EVENT_MOUSE_MOTION: {
i32 window_mouse_x = (i32)event->motion.x;
i32 window_mouse_y = (i32)event->motion.y;
i32 render_width, render_height;
pxl8_gfx_get_resolution_dimensions(app->resolution, &render_width, &render_height);
pxl8_viewport vp = pxl8_gfx_viewport(pxl8_gfx_get_bounds(app->gfx), render_width, render_height);
app->input.mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale);
app->input.mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale);
break;
}
case SDL_EVENT_MOUSE_WHEEL: {
app->input.mouse_wheel_x = (i32)event->wheel.x;
app->input.mouse_wheel_y = (i32)event->wheel.y;
break;
}
} }
return SDL_APP_CONTINUE; pxl8_info("Shutting down");
}
void SDL_AppQuit(void* appstate, SDL_AppResult result) { if (game->repl_mode && game->repl) {
pxl8_state* app = (pxl8_state*)appstate; pxl8_script_repl_shutdown(game->repl);
(void)result; pxl8_script_repl_destroy(game->repl);
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);
if (app->ui) {
pxl8_ui_destroy(app->ui);
}
pxl8_gfx_destroy(app->gfx);
} }
SDL_Quit(); if (game->cart) {
pxl8_cart_unload(game->cart);
free(game->cart);
game->cart = NULL;
}
pxl8_script_destroy(game->script);
if (game->ui) {
pxl8_ui_destroy(game->ui);
}
pxl8_gfx_destroy(game->gfx);
} }

View file

@ -8,7 +8,6 @@
#define MINIZ_NO_DEFLATE_APIS #define MINIZ_NO_DEFLATE_APIS
#include <miniz.h> #include <miniz.h>
#include <SDL3/SDL.h>
#include "pxl8_ase.h" #include "pxl8_ase.h"
#include "pxl8_io.h" #include "pxl8_io.h"
@ -57,7 +56,7 @@ static pxl8_result parse_old_palette_chunk(pxl8_stream* stream, pxl8_ase_palette
palette->entry_count = total_colors; palette->entry_count = total_colors;
palette->first_color = 0; palette->first_color = 0;
palette->last_color = total_colors - 1; palette->last_color = total_colors - 1;
palette->colors = (u32*)SDL_malloc(total_colors * sizeof(u32)); palette->colors = (u32*)malloc(total_colors * sizeof(u32));
if (!palette->colors) { if (!palette->colors) {
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
} }
@ -96,7 +95,7 @@ static pxl8_result parse_layer_chunk(pxl8_stream* stream, pxl8_ase_layer* layer)
u16 name_len = pxl8_read_u16(stream); u16 name_len = pxl8_read_u16(stream);
if (name_len > 0) { if (name_len > 0) {
layer->name = (char*)SDL_malloc(name_len + 1); layer->name = (char*)malloc(name_len + 1);
if (!layer->name) return PXL8_ERROR_OUT_OF_MEMORY; if (!layer->name) return PXL8_ERROR_OUT_OF_MEMORY;
pxl8_read_bytes(stream, layer->name, name_len); pxl8_read_bytes(stream, layer->name, name_len);
layer->name[name_len] = '\0'; layer->name[name_len] = '\0';
@ -119,7 +118,7 @@ static pxl8_result parse_palette_chunk(pxl8_stream* stream, pxl8_ase_palette* pa
return PXL8_ERROR_ASE_MALFORMED_CHUNK; return PXL8_ERROR_ASE_MALFORMED_CHUNK;
} }
palette->colors = (u32*)SDL_malloc(color_count * sizeof(u32)); palette->colors = (u32*)malloc(color_count * sizeof(u32));
if (!palette->colors) { if (!palette->colors) {
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
} }
@ -167,7 +166,7 @@ static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase
u32 pixel_data_size = cel->width * cel->height; u32 pixel_data_size = cel->width * cel->height;
u32 compressed_data_size = chunk_size - 20; u32 compressed_data_size = chunk_size - 20;
cel->pixel_data = (u8*)SDL_malloc(pixel_data_size); cel->pixel_data = (u8*)malloc(pixel_data_size);
if (!cel->pixel_data) { if (!cel->pixel_data) {
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
} }
@ -177,7 +176,7 @@ static pxl8_result parse_cel_chunk(pxl8_stream* stream, u32 chunk_size, pxl8_ase
i32 result = mz_uncompress(cel->pixel_data, &dest_len, compressed_data, compressed_data_size); i32 result = mz_uncompress(cel->pixel_data, &dest_len, compressed_data, compressed_data_size);
if (result != MZ_OK) { if (result != MZ_OK) {
pxl8_error("Failed to decompress cel data: miniz error %d", result); pxl8_error("Failed to decompress cel data: miniz error %d", result);
SDL_free(cel->pixel_data); free(cel->pixel_data);
cel->pixel_data = NULL; cel->pixel_data = NULL;
return PXL8_ERROR_ASE_MALFORMED_CHUNK; return PXL8_ERROR_ASE_MALFORMED_CHUNK;
} }
@ -218,7 +217,7 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
} }
ase_file->frame_count = ase_file->header.frames; ase_file->frame_count = ase_file->header.frames;
ase_file->frames = (pxl8_ase_frame*)SDL_calloc(ase_file->frame_count, sizeof(pxl8_ase_frame)); ase_file->frames = (pxl8_ase_frame*)calloc(ase_file->frame_count, sizeof(pxl8_ase_frame));
if (!ase_file->frames) { if (!ase_file->frames) {
pxl8_io_free_binary_data(file_data); pxl8_io_free_binary_data(file_data);
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
@ -256,7 +255,7 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
frame->duration = frame_header.duration; frame->duration = frame_header.duration;
u32 pixel_count = frame->width * frame->height; u32 pixel_count = frame->width * frame->height;
frame->pixels = (u8*)SDL_calloc(pixel_count, sizeof(u8)); frame->pixels = (u8*)calloc(pixel_count, sizeof(u8));
if (!frame->pixels) { if (!frame->pixels) {
result = PXL8_ERROR_OUT_OF_MEMORY; result = PXL8_ERROR_OUT_OF_MEMORY;
break; break;
@ -285,7 +284,7 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
case PXL8_ASE_CHUNK_LAYER: { case PXL8_ASE_CHUNK_LAYER: {
ase_file->layers = ase_file->layers =
(pxl8_ase_layer*)SDL_realloc(ase_file->layers, (pxl8_ase_layer*)realloc(ase_file->layers,
(ase_file->layer_count + 1) * sizeof(pxl8_ase_layer)); (ase_file->layer_count + 1) * sizeof(pxl8_ase_layer));
if (!ase_file->layers) { if (!ase_file->layers) {
result = PXL8_ERROR_OUT_OF_MEMORY; result = PXL8_ERROR_OUT_OF_MEMORY;
@ -325,14 +324,14 @@ pxl8_result pxl8_ase_load(const char* filepath, pxl8_ase_file* ase_file) {
} }
} }
} }
SDL_free(cel.pixel_data); free(cel.pixel_data);
} }
break; break;
} }
case PXL8_ASE_CHUNK_PALETTE: case PXL8_ASE_CHUNK_PALETTE:
if (ase_file->palette.colors) { if (ase_file->palette.colors) {
SDL_free(ase_file->palette.colors); free(ase_file->palette.colors);
ase_file->palette.colors = NULL; ase_file->palette.colors = NULL;
} }
result = parse_palette_chunk(&stream, &ase_file->palette); result = parse_palette_chunk(&stream, &ase_file->palette);
@ -367,23 +366,23 @@ void pxl8_ase_destroy(pxl8_ase_file* ase_file) {
if (ase_file->frames) { if (ase_file->frames) {
for (u32 i = 0; i < ase_file->frame_count; i++) { for (u32 i = 0; i < ase_file->frame_count; i++) {
if (ase_file->frames[i].pixels) { if (ase_file->frames[i].pixels) {
SDL_free(ase_file->frames[i].pixels); free(ase_file->frames[i].pixels);
} }
} }
SDL_free(ase_file->frames); free(ase_file->frames);
} }
if (ase_file->palette.colors) { if (ase_file->palette.colors) {
SDL_free(ase_file->palette.colors); free(ase_file->palette.colors);
} }
if (ase_file->layers) { if (ase_file->layers) {
for (u32 i = 0; i < ase_file->layer_count; i++) { for (u32 i = 0; i < ase_file->layer_count; i++) {
if (ase_file->layers[i].name) { if (ase_file->layers[i].name) {
SDL_free(ase_file->layers[i].name); free(ase_file->layers[i].name);
} }
} }
SDL_free(ase_file->layers); free(ase_file->layers);
} }
memset(ase_file, 0, sizeof(pxl8_ase_file)); memset(ase_file, 0, sizeof(pxl8_ase_file));

360
src/pxl8_atlas.c Normal file
View file

@ -0,0 +1,360 @@
#include "pxl8_atlas.h"
#include "pxl8_macros.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct pxl8_skyline_fit {
bool found;
u32 node_idx;
pxl8_point pos;
} pxl8_skyline_fit;
typedef struct pxl8_skyline_node {
i32 x, y, width;
} pxl8_skyline_node;
typedef struct pxl8_skyline {
pxl8_skyline_node* nodes;
u32 count;
u32 capacity;
} pxl8_skyline;
struct pxl8_atlas {
u32 height, width;
u8* pixels;
bool dirty;
u32 entry_capacity, entry_count;
pxl8_atlas_entry* entries;
u32 free_capacity, free_count;
u32* free_list;
pxl8_skyline skyline;
};
static pxl8_skyline_fit pxl8_skyline_find_position(
const pxl8_skyline* skyline,
u32 atlas_w,
u32 atlas_h,
u32 rect_w,
u32 rect_h
) {
pxl8_skyline_fit result = {.found = false};
i32 best_y = INT32_MAX;
i32 best_x = 0;
u32 best_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
i32 x = skyline->nodes[i].x;
i32 y = skyline->nodes[i].y;
if (x + (i32)rect_w > (i32)atlas_w) continue;
i32 max_y = y;
for (u32 j = i; j < skyline->count && skyline->nodes[j].x < x + (i32)rect_w; j++) {
if (skyline->nodes[j].y > max_y) {
max_y = skyline->nodes[j].y;
}
}
if (max_y + (i32)rect_h > (i32)atlas_h) continue;
if (max_y < best_y || (max_y == best_y && x < best_x)) {
best_y = max_y;
best_x = x;
best_idx = i;
}
}
if (best_y != INT32_MAX) {
result.found = true;
result.pos.x = best_x;
result.pos.y = best_y;
result.node_idx = best_idx;
}
return result;
}
static void pxl8_skyline_add_rect(pxl8_skyline* skyline, pxl8_point pos, u32 w, u32 h) {
u32 node_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
if (skyline->nodes[i].x == pos.x) {
node_idx = i;
break;
}
}
u32 nodes_to_remove = 0;
for (u32 i = node_idx; i < skyline->count; i++) {
if (skyline->nodes[i].x < pos.x + (i32)w) {
nodes_to_remove++;
} else {
break;
}
}
if (skyline->count - nodes_to_remove + 1 > skyline->capacity) {
skyline->capacity = (skyline->count - nodes_to_remove + 1) * 2;
skyline->nodes = (pxl8_skyline_node*)realloc(
skyline->nodes,
skyline->capacity * sizeof(pxl8_skyline_node)
);
}
if (nodes_to_remove > 0) {
memmove(
&skyline->nodes[node_idx + 1],
&skyline->nodes[node_idx + nodes_to_remove],
(skyline->count - node_idx - nodes_to_remove) * sizeof(pxl8_skyline_node)
);
}
skyline->nodes[node_idx] = (pxl8_skyline_node){pos.x, pos.y + (i32)h, (i32)w};
skyline->count = skyline->count - nodes_to_remove + 1;
}
static void pxl8_skyline_compact(pxl8_skyline* skyline) {
for (u32 i = 0; i < skyline->count - 1; ) {
if (skyline->nodes[i].y == skyline->nodes[i + 1].y) {
skyline->nodes[i].width += skyline->nodes[i + 1].width;
memmove(
&skyline->nodes[i + 1],
&skyline->nodes[i + 2],
(skyline->count - i - 2) * sizeof(pxl8_skyline_node)
);
skyline->count--;
} else {
i++;
}
}
}
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_color_mode color_mode) {
pxl8_atlas* atlas = (pxl8_atlas*)calloc(1, sizeof(pxl8_atlas));
if (!atlas) return NULL;
atlas->height = height;
atlas->width = width;
i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
atlas->pixels = (u8*)calloc(width * height, bytes_per_pixel);
if (!atlas->pixels) {
free(atlas);
return NULL;
}
atlas->entry_capacity = 64;
atlas->entries = (pxl8_atlas_entry*)calloc(atlas->entry_capacity, sizeof(pxl8_atlas_entry));
if (!atlas->entries) {
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->free_capacity = 16;
atlas->free_list = (u32*)calloc(atlas->free_capacity, sizeof(u32));
if (!atlas->free_list) {
free(atlas->entries);
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->skyline.capacity = 16;
atlas->skyline.nodes =
(pxl8_skyline_node*)calloc(atlas->skyline.capacity, sizeof(pxl8_skyline_node));
if (!atlas->skyline.nodes) {
free(atlas->free_list);
free(atlas->entries);
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)width};
atlas->skyline.count = 1;
return atlas;
}
void pxl8_atlas_destroy(pxl8_atlas* atlas) {
if (!atlas) return;
free(atlas->entries);
free(atlas->free_list);
free(atlas->pixels);
free(atlas->skyline.nodes);
free(atlas);
}
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_color_mode color_mode) {
if (!atlas || atlas->width >= 4096) return false;
i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
u32 new_size = atlas->width * 2;
u32 old_width = atlas->width;
u8* new_pixels = (u8*)calloc(new_size * new_size, bytes_per_pixel);
if (!new_pixels) return false;
pxl8_skyline new_skyline;
new_skyline.nodes = (pxl8_skyline_node*)calloc(16, sizeof(pxl8_skyline_node));
if (!new_skyline.nodes) {
free(new_pixels);
return false;
}
new_skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)new_size};
new_skyline.count = 1;
new_skyline.capacity = 16;
for (u32 i = 0; i < atlas->entry_count; i++) {
if (!atlas->entries[i].active) continue;
pxl8_skyline_fit fit = pxl8_skyline_find_position(
&new_skyline,
new_size,
new_size,
atlas->entries[i].w,
atlas->entries[i].h
);
if (!fit.found) {
free(new_skyline.nodes);
free(new_pixels);
return false;
}
for (u32 y = 0; y < (u32)atlas->entries[i].h; y++) {
for (u32 x = 0; x < (u32)atlas->entries[i].w; x++) {
u32 src_idx = (atlas->entries[i].y + y) * old_width + (atlas->entries[i].x + x);
u32 dst_idx = (fit.pos.y + y) * new_size + (fit.pos.x + x);
if (bytes_per_pixel == 4) {
((u32*)new_pixels)[dst_idx] = ((u32*)atlas->pixels)[src_idx];
} else {
new_pixels[dst_idx] = atlas->pixels[src_idx];
}
}
}
atlas->entries[i].x = fit.pos.x;
atlas->entries[i].y = fit.pos.y;
pxl8_skyline_add_rect(&new_skyline, fit.pos, atlas->entries[i].w, atlas->entries[i].h);
pxl8_skyline_compact(&new_skyline);
}
free(atlas->pixels);
free(atlas->skyline.nodes);
atlas->pixels = new_pixels;
atlas->skyline = new_skyline;
atlas->width = new_size;
atlas->height = new_size;
atlas->dirty = true;
pxl8_debug("Atlas expanded to %ux%u", atlas->width, atlas->height);
return true;
}
u32 pxl8_atlas_add_texture(
pxl8_atlas* atlas,
const u8* pixels,
u32 w,
u32 h,
pxl8_color_mode color_mode
) {
if (!atlas || !pixels) return UINT32_MAX;
pxl8_skyline_fit fit =
pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) {
if (!pxl8_atlas_expand(atlas, color_mode)) {
return UINT32_MAX;
}
fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) return UINT32_MAX;
}
u32 texture_id;
if (atlas->free_count > 0) {
texture_id = atlas->free_list[--atlas->free_count];
} else {
if (atlas->entry_count >= atlas->entry_capacity) {
atlas->entry_capacity *= 2;
atlas->entries = (pxl8_atlas_entry*)realloc(
atlas->entries,
atlas->entry_capacity * sizeof(pxl8_atlas_entry)
);
}
texture_id = atlas->entry_count++;
}
pxl8_atlas_entry* entry = &atlas->entries[texture_id];
entry->active = true;
entry->texture_id = texture_id;
entry->x = fit.pos.x;
entry->y = fit.pos.y;
entry->w = w;
entry->h = h;
i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
for (u32 y = 0; y < h; y++) {
for (u32 x = 0; x < w; x++) {
u32 src_idx = y * w + x;
u32 dst_idx = (fit.pos.y + y) * atlas->width + (fit.pos.x + x);
if (bytes_per_pixel == 4) {
((u32*)atlas->pixels)[dst_idx] = ((const u32*)pixels)[src_idx];
} else {
atlas->pixels[dst_idx] = pixels[src_idx];
}
}
}
pxl8_skyline_add_rect(&atlas->skyline, fit.pos, w, h);
pxl8_skyline_compact(&atlas->skyline);
atlas->dirty = true;
return texture_id;
}
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id) {
if (!atlas || id >= atlas->entry_count) return NULL;
return &atlas->entries[id];
}
u32 pxl8_atlas_get_entry_count(const pxl8_atlas* atlas) {
return atlas ? atlas->entry_count : 0;
}
u32 pxl8_atlas_get_height(const pxl8_atlas* atlas) {
return atlas ? atlas->height : 0;
}
const u8* pxl8_atlas_get_pixels(const pxl8_atlas* atlas) {
return atlas ? atlas->pixels : NULL;
}
u32 pxl8_atlas_get_width(const pxl8_atlas* atlas) {
return atlas ? atlas->width : 0;
}
bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas) {
return atlas ? atlas->dirty : false;
}
void pxl8_atlas_mark_clean(pxl8_atlas* atlas) {
if (atlas) {
atlas->dirty = false;
}
}

26
src/pxl8_atlas.h Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_atlas_entry {
bool active;
u32 texture_id;
i32 x, y, w, h;
} pxl8_atlas_entry;
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_color_mode color_mode);
void pxl8_atlas_destroy(pxl8_atlas* atlas);
u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h,
pxl8_color_mode color_mode);
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_color_mode color_mode);
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id);
u32 pxl8_atlas_get_entry_count(const pxl8_atlas* atlas);
u32 pxl8_atlas_get_height(const pxl8_atlas* atlas);
const u8* pxl8_atlas_get_pixels(const pxl8_atlas* atlas);
u32 pxl8_atlas_get_width(const pxl8_atlas* atlas);
bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas);
void pxl8_atlas_mark_clean(pxl8_atlas* atlas);

View file

@ -1,7 +1,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <SDL3/SDL.h>
#include "pxl8_bsp.h" #include "pxl8_bsp.h"
#include "pxl8_gfx.h" #include "pxl8_gfx.h"
#include "pxl8_io.h" #include "pxl8_io.h"
@ -59,16 +59,34 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
memset(bsp, 0, sizeof(*bsp)); memset(bsp, 0, sizeof(*bsp));
size_t file_size; FILE* f = fopen(path, "rb");
u8* file_data = (u8*)SDL_LoadFile(path, &file_size); if (!f) {
if (!file_data) {
pxl8_error("Failed to load BSP file: %s", path); pxl8_error("Failed to load BSP file: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND; return PXL8_ERROR_FILE_NOT_FOUND;
} }
fseek(f, 0, SEEK_END);
size_t file_size = ftell(f);
fseek(f, 0, SEEK_SET);
u8* file_data = (u8*)malloc(file_size);
if (!file_data) {
fclose(f);
pxl8_error("Failed to allocate memory for BSP file: %s", path);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (fread(file_data, 1, file_size, f) != file_size) {
free(file_data);
fclose(f);
pxl8_error("Failed to read BSP file: %s", path);
return PXL8_ERROR_INVALID_FORMAT;
}
fclose(f);
if (file_size < sizeof(pxl8_bsp_header)) { if (file_size < sizeof(pxl8_bsp_header)) {
pxl8_error("BSP file too small: %s", path); pxl8_error("BSP file too small: %s", path);
SDL_free(file_data); free(file_data);
return PXL8_ERROR_INVALID_FORMAT; return PXL8_ERROR_INVALID_FORMAT;
} }
@ -79,7 +97,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (header.version != BSP_VERSION) { if (header.version != BSP_VERSION) {
pxl8_error("Invalid BSP version: %u (expected %d)", header.version, BSP_VERSION); pxl8_error("Invalid BSP version: %u (expected %d)", header.version, BSP_VERSION);
SDL_free(file_data); free(file_data);
return PXL8_ERROR_INVALID_FORMAT; return PXL8_ERROR_INVALID_FORMAT;
} }
@ -92,7 +110,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 12, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 12, file_size)) goto error_cleanup;
bsp->num_vertices = chunk->size / 12; bsp->num_vertices = chunk->size / 12;
if (bsp->num_vertices > 0) { if (bsp->num_vertices > 0) {
bsp->vertices = SDL_calloc(bsp->num_vertices, sizeof(pxl8_bsp_vertex)); bsp->vertices = calloc(bsp->num_vertices, sizeof(pxl8_bsp_vertex));
if (!bsp->vertices) goto error_cleanup; if (!bsp->vertices) goto error_cleanup;
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_vertices; i++) { for (u32 i = 0; i < bsp->num_vertices; i++) {
@ -104,7 +122,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
bsp->num_edges = chunk->size / 4; bsp->num_edges = chunk->size / 4;
if (bsp->num_edges > 0) { if (bsp->num_edges > 0) {
bsp->edges = SDL_calloc(bsp->num_edges, sizeof(pxl8_bsp_edge)); bsp->edges = calloc(bsp->num_edges, sizeof(pxl8_bsp_edge));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_edges; i++) { for (u32 i = 0; i < bsp->num_edges; i++) {
bsp->edges[i].vertex[0] = pxl8_read_u16(&stream); bsp->edges[i].vertex[0] = pxl8_read_u16(&stream);
@ -116,7 +134,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 4, file_size)) goto error_cleanup;
bsp->num_surfedges = chunk->size / 4; bsp->num_surfedges = chunk->size / 4;
if (bsp->num_surfedges > 0) { if (bsp->num_surfedges > 0) {
bsp->surfedges = SDL_calloc(bsp->num_surfedges, sizeof(i32)); bsp->surfedges = calloc(bsp->num_surfedges, sizeof(i32));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_surfedges; i++) { for (u32 i = 0; i < bsp->num_surfedges; i++) {
bsp->surfedges[i] = pxl8_read_i32(&stream); bsp->surfedges[i] = pxl8_read_i32(&stream);
@ -127,7 +145,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_planes = chunk->size / 20; bsp->num_planes = chunk->size / 20;
if (bsp->num_planes > 0) { if (bsp->num_planes > 0) {
bsp->planes = SDL_calloc(bsp->num_planes, sizeof(pxl8_bsp_plane)); bsp->planes = calloc(bsp->num_planes, sizeof(pxl8_bsp_plane));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_planes; i++) { for (u32 i = 0; i < bsp->num_planes; i++) {
bsp->planes[i].normal = read_vec3(&stream); bsp->planes[i].normal = read_vec3(&stream);
@ -140,7 +158,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 40, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 40, file_size)) goto error_cleanup;
bsp->num_texinfo = chunk->size / 40; bsp->num_texinfo = chunk->size / 40;
if (bsp->num_texinfo > 0) { if (bsp->num_texinfo > 0) {
bsp->texinfo = SDL_calloc(bsp->num_texinfo, sizeof(pxl8_bsp_texinfo)); bsp->texinfo = calloc(bsp->num_texinfo, sizeof(pxl8_bsp_texinfo));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_texinfo; i++) { for (u32 i = 0; i < bsp->num_texinfo; i++) {
bsp->texinfo[i].u_axis = read_vec3(&stream); bsp->texinfo[i].u_axis = read_vec3(&stream);
@ -155,7 +173,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 20, file_size)) goto error_cleanup;
bsp->num_faces = chunk->size / 20; bsp->num_faces = chunk->size / 20;
if (bsp->num_faces > 0) { if (bsp->num_faces > 0) {
bsp->faces = SDL_calloc(bsp->num_faces, sizeof(pxl8_bsp_face)); bsp->faces = calloc(bsp->num_faces, sizeof(pxl8_bsp_face));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_faces; i++) { for (u32 i = 0; i < bsp->num_faces; i++) {
bsp->faces[i].plane_id = pxl8_read_u16(&stream); bsp->faces[i].plane_id = pxl8_read_u16(&stream);
@ -175,7 +193,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 24, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 24, file_size)) goto error_cleanup;
bsp->num_nodes = chunk->size / 24; bsp->num_nodes = chunk->size / 24;
if (bsp->num_nodes > 0) { if (bsp->num_nodes > 0) {
bsp->nodes = SDL_calloc(bsp->num_nodes, sizeof(pxl8_bsp_node)); bsp->nodes = calloc(bsp->num_nodes, sizeof(pxl8_bsp_node));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_nodes; i++) { for (u32 i = 0; i < bsp->num_nodes; i++) {
bsp->nodes[i].plane_id = pxl8_read_u32(&stream); bsp->nodes[i].plane_id = pxl8_read_u32(&stream);
@ -192,7 +210,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 28, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 28, file_size)) goto error_cleanup;
bsp->num_leafs = chunk->size / 28; bsp->num_leafs = chunk->size / 28;
if (bsp->num_leafs > 0) { if (bsp->num_leafs > 0) {
bsp->leafs = SDL_calloc(bsp->num_leafs, sizeof(pxl8_bsp_leaf)); bsp->leafs = calloc(bsp->num_leafs, sizeof(pxl8_bsp_leaf));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_leafs; i++) { for (u32 i = 0; i < bsp->num_leafs; i++) {
bsp->leafs[i].contents = pxl8_read_i32(&stream); bsp->leafs[i].contents = pxl8_read_i32(&stream);
@ -209,7 +227,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 2, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 2, file_size)) goto error_cleanup;
bsp->num_marksurfaces = chunk->size / 2; bsp->num_marksurfaces = chunk->size / 2;
if (bsp->num_marksurfaces > 0) { if (bsp->num_marksurfaces > 0) {
bsp->marksurfaces = SDL_calloc(bsp->num_marksurfaces, sizeof(u16)); bsp->marksurfaces = calloc(bsp->num_marksurfaces, sizeof(u16));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_marksurfaces; i++) { for (u32 i = 0; i < bsp->num_marksurfaces; i++) {
bsp->marksurfaces[i] = pxl8_read_u16(&stream); bsp->marksurfaces[i] = pxl8_read_u16(&stream);
@ -220,7 +238,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 64, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 64, file_size)) goto error_cleanup;
bsp->num_models = chunk->size / 64; bsp->num_models = chunk->size / 64;
if (bsp->num_models > 0) { if (bsp->num_models > 0) {
bsp->models = SDL_calloc(bsp->num_models, sizeof(pxl8_bsp_model)); bsp->models = calloc(bsp->num_models, sizeof(pxl8_bsp_model));
pxl8_stream_seek(&stream, chunk->offset); pxl8_stream_seek(&stream, chunk->offset);
for (u32 i = 0; i < bsp->num_models; i++) { for (u32 i = 0; i < bsp->num_models; i++) {
for (u32 j = 0; j < 3; j++) bsp->models[i].mins[j] = pxl8_read_f32(&stream); for (u32 j = 0; j < 3; j++) bsp->models[i].mins[j] = pxl8_read_f32(&stream);
@ -237,7 +255,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
bsp->visdata_size = chunk->size; bsp->visdata_size = chunk->size;
if (bsp->visdata_size > 0) { if (bsp->visdata_size > 0) {
bsp->visdata = SDL_malloc(bsp->visdata_size); bsp->visdata = malloc(bsp->visdata_size);
memcpy(bsp->visdata, file_data + chunk->offset, bsp->visdata_size); memcpy(bsp->visdata, file_data + chunk->offset, bsp->visdata_size);
} }
@ -245,11 +263,11 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup; if (!validate_chunk(chunk, 1, file_size)) goto error_cleanup;
bsp->lightdata_size = chunk->size; bsp->lightdata_size = chunk->size;
if (bsp->lightdata_size > 0) { if (bsp->lightdata_size > 0) {
bsp->lightdata = SDL_malloc(bsp->lightdata_size); bsp->lightdata = malloc(bsp->lightdata_size);
memcpy(bsp->lightdata, file_data + chunk->offset, bsp->lightdata_size); memcpy(bsp->lightdata, file_data + chunk->offset, bsp->lightdata_size);
} }
SDL_free(file_data); free(file_data);
pxl8_debug("Loaded BSP: %u verts, %u faces, %u nodes, %u leafs", pxl8_debug("Loaded BSP: %u verts, %u faces, %u nodes, %u leafs",
bsp->num_vertices, bsp->num_faces, bsp->num_nodes, bsp->num_leafs); bsp->num_vertices, bsp->num_faces, bsp->num_nodes, bsp->num_leafs);
@ -258,7 +276,7 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
error_cleanup: error_cleanup:
pxl8_error("BSP chunk validation failed: %s", path); pxl8_error("BSP chunk validation failed: %s", path);
SDL_free(file_data); free(file_data);
pxl8_bsp_destroy(bsp); pxl8_bsp_destroy(bsp);
return PXL8_ERROR_INVALID_FORMAT; return PXL8_ERROR_INVALID_FORMAT;
} }
@ -266,18 +284,18 @@ error_cleanup:
void pxl8_bsp_destroy(pxl8_bsp* bsp) { void pxl8_bsp_destroy(pxl8_bsp* bsp) {
if (!bsp) return; if (!bsp) return;
SDL_free(bsp->edges); free(bsp->edges);
SDL_free(bsp->faces); free(bsp->faces);
SDL_free(bsp->leafs); free(bsp->leafs);
SDL_free(bsp->lightdata); free(bsp->lightdata);
SDL_free(bsp->marksurfaces); free(bsp->marksurfaces);
SDL_free(bsp->models); free(bsp->models);
SDL_free(bsp->nodes); free(bsp->nodes);
SDL_free(bsp->planes); free(bsp->planes);
SDL_free(bsp->surfedges); free(bsp->surfedges);
SDL_free(bsp->texinfo); free(bsp->texinfo);
SDL_free(bsp->vertices); free(bsp->vertices);
SDL_free(bsp->visdata); free(bsp->visdata);
memset(bsp, 0, sizeof(*bsp)); memset(bsp, 0, sizeof(*bsp));
} }

View file

@ -1,7 +1,6 @@
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <SDL3/SDL.h>
#include "pxl8_font.h" #include "pxl8_font.h"
pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height) { pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height) {
@ -16,7 +15,7 @@ pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32*
*atlas_height = rows_needed * font->default_height; *atlas_height = rows_needed * font->default_height;
i32 atlas_size = (*atlas_width) * (*atlas_height); i32 atlas_size = (*atlas_width) * (*atlas_height);
*atlas_data = (u8*)SDL_malloc(atlas_size); *atlas_data = (u8*)malloc(atlas_size);
if (!*atlas_data) { if (!*atlas_data) {
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
} }

49
src/pxl8_game.h Normal file
View file

@ -0,0 +1,49 @@
#pragma once
#include "pxl8_cart.h"
#include "pxl8_gfx.h"
#include "pxl8_hal.h"
#include "pxl8_script.h"
#include "pxl8_types.h"
#include "pxl8_ui.h"
typedef enum pxl8_game_result {
PXL8_GAME_CONTINUE,
PXL8_GAME_SUCCESS,
PXL8_GAME_FAILURE
} pxl8_game_result;
typedef struct pxl8_game {
const pxl8_hal* hal;
pxl8_cart* cart;
pxl8_color_mode color_mode;
pxl8_gfx* gfx;
pxl8_resolution resolution;
pxl8_script* script;
pxl8_ui* ui;
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_script_repl* repl;
} pxl8_game;
typedef struct pxl8_game_callbacks {
pxl8_game_result (*init)(pxl8_game* game, i32 argc, char* argv[]);
pxl8_game_result (*update)(pxl8_game* game);
pxl8_game_result (*frame)(pxl8_game* game);
void (*quit)(pxl8_game* game);
} pxl8_game_callbacks;
pxl8_game_result pxl8_init(pxl8_game* game, i32 argc, char* argv[]);
pxl8_game_result pxl8_update(pxl8_game* game);
pxl8_game_result pxl8_frame(pxl8_game* game);
void pxl8_quit(pxl8_game* game);

View file

@ -2,62 +2,30 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <SDL3/SDL.h>
#include "pxl8_ase.h" #include "pxl8_ase.h"
#include "pxl8_atlas.h"
#include "pxl8_blit.h" #include "pxl8_blit.h"
#include "pxl8_font.h" #include "pxl8_font.h"
#include "pxl8_gfx.h" #include "pxl8_gfx.h"
#include "pxl8_hal.h"
#include "pxl8_macros.h" #include "pxl8_macros.h"
#include "pxl8_math.h" #include "pxl8_math.h"
#include "pxl8_types.h" #include "pxl8_types.h"
typedef struct pxl8_atlas_entry { typedef struct pxl8_sprite_cache_entry {
bool active;
char path[256]; char path[256];
u32 texture_id; u32 sprite_id;
bool active;
i32 x, y, w, h; } pxl8_sprite_cache_entry;
} pxl8_atlas_entry;
typedef struct pxl8_skyline_fit {
bool found;
u32 node_idx;
pxl8_point pos;
} pxl8_skyline_fit;
typedef struct pxl8_skyline_node {
i32 x, y, width;
} pxl8_skyline_node;
typedef struct pxl8_skyline {
pxl8_skyline_node* nodes;
u32 count;
u32 capacity;
} pxl8_skyline;
struct pxl8_atlas {
u32 height, width;
u8* pixels;
SDL_Texture* texture;
bool dirty;
u32 entry_capacity, entry_count;
pxl8_atlas_entry* entries;
u32 free_capacity, free_count;
u32* free_list;
pxl8_skyline skyline;
};
struct pxl8_gfx { struct pxl8_gfx {
SDL_Renderer* renderer; const pxl8_hal* hal;
SDL_Texture* framebuffer_texture; void* platform_data;
SDL_Window* window;
pxl8_atlas* atlas; pxl8_atlas* atlas;
pxl8_sprite_cache_entry* sprite_cache;
u32 sprite_cache_capacity;
u32 sprite_cache_count;
pxl8_color_mode color_mode; pxl8_color_mode color_mode;
u8* framebuffer; u8* framebuffer;
@ -82,351 +50,6 @@ struct pxl8_gfx {
bool affine_textures; bool affine_textures;
}; };
static pxl8_skyline_fit pxl8_skyline_find_position(
const pxl8_skyline* skyline,
u32 atlas_w,
u32 atlas_h,
u32 rect_w,
u32 rect_h
) {
pxl8_skyline_fit result = {.found = false};
i32 best_y = INT32_MAX;
i32 best_x = 0;
u32 best_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
i32 x = skyline->nodes[i].x;
i32 y = skyline->nodes[i].y;
if (x + (i32)rect_w > (i32)atlas_w) continue;
i32 max_y = y;
for (u32 j = i; j < skyline->count && skyline->nodes[j].x < x + (i32)rect_w; j++) {
if (skyline->nodes[j].y > max_y) {
max_y = skyline->nodes[j].y;
}
}
if (max_y + (i32)rect_h > (i32)atlas_h) continue;
if (max_y < best_y || (max_y == best_y && x < best_x)) {
best_y = max_y;
best_x = x;
best_idx = i;
}
}
if (best_y != INT32_MAX) {
result.found = true;
result.pos.x = best_x;
result.pos.y = best_y;
result.node_idx = best_idx;
}
return result;
}
static void pxl8_skyline_add_rect(pxl8_skyline* skyline, pxl8_point pos, u32 w, u32 h) {
u32 node_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
if (skyline->nodes[i].x == pos.x) {
node_idx = i;
break;
}
}
u32 nodes_to_remove = 0;
for (u32 i = node_idx; i < skyline->count; i++) {
if (skyline->nodes[i].x < pos.x + (i32)w) {
nodes_to_remove++;
} else {
break;
}
}
if (skyline->count - nodes_to_remove + 1 > skyline->capacity) {
skyline->capacity = (skyline->count - nodes_to_remove + 1) * 2;
skyline->nodes = (pxl8_skyline_node*)SDL_realloc(
skyline->nodes,
skyline->capacity * sizeof(pxl8_skyline_node)
);
}
if (nodes_to_remove > 0) {
SDL_memmove(
&skyline->nodes[node_idx + 1],
&skyline->nodes[node_idx + nodes_to_remove],
(skyline->count - node_idx - nodes_to_remove) * sizeof(pxl8_skyline_node)
);
}
skyline->nodes[node_idx] = (pxl8_skyline_node){pos.x, pos.y + (i32)h, (i32)w};
skyline->count = skyline->count - nodes_to_remove + 1;
}
static void pxl8_skyline_compact(pxl8_skyline* skyline) {
for (u32 i = 0; i < skyline->count - 1; ) {
if (skyline->nodes[i].y == skyline->nodes[i + 1].y) {
skyline->nodes[i].width += skyline->nodes[i + 1].width;
SDL_memmove(
&skyline->nodes[i + 1],
&skyline->nodes[i + 2],
(skyline->count - i - 2) * sizeof(pxl8_skyline_node)
);
skyline->count--;
} else {
i++;
}
}
}
static pxl8_atlas* pxl8_atlas_create(
SDL_Renderer* renderer,
u32 width,
u32 height,
pxl8_color_mode color_mode
) {
pxl8_atlas* atlas = (pxl8_atlas*)SDL_calloc(1, sizeof(pxl8_atlas));
if (!atlas) return NULL;
atlas->height = height;
atlas->width = width;
i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
atlas->pixels = (u8*)SDL_calloc(width * height, bytes_per_pixel);
if (!atlas->pixels) {
SDL_free(atlas);
return NULL;
}
atlas->texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
width,
height
);
if (!atlas->texture) {
SDL_free(atlas->pixels);
SDL_free(atlas);
return NULL;
}
atlas->entry_capacity = 64;
atlas->entries = (pxl8_atlas_entry*)SDL_calloc(atlas->entry_capacity, sizeof(pxl8_atlas_entry));
if (!atlas->entries) {
SDL_DestroyTexture(atlas->texture);
SDL_free(atlas->pixels);
SDL_free(atlas);
return NULL;
}
atlas->free_capacity = 16;
atlas->free_list = (u32*)SDL_calloc(atlas->free_capacity, sizeof(u32));
if (!atlas->free_list) {
SDL_free(atlas->entries);
SDL_DestroyTexture(atlas->texture);
SDL_free(atlas->pixels);
SDL_free(atlas);
return NULL;
}
atlas->skyline.capacity = 16;
atlas->skyline.nodes =
(pxl8_skyline_node*)SDL_calloc(atlas->skyline.capacity, sizeof(pxl8_skyline_node));
if (!atlas->skyline.nodes) {
SDL_free(atlas->free_list);
SDL_free(atlas->entries);
SDL_DestroyTexture(atlas->texture);
SDL_free(atlas->pixels);
SDL_free(atlas);
return NULL;
}
atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)width};
atlas->skyline.count = 1;
return atlas;
}
static void pxl8_atlas_destroy(pxl8_atlas* atlas) {
if (!atlas) return;
SDL_free(atlas->entries);
SDL_free(atlas->free_list);
SDL_free(atlas->pixels);
SDL_free(atlas->skyline.nodes);
if (atlas->texture) SDL_DestroyTexture(atlas->texture);
SDL_free(atlas);
}
static bool pxl8_atlas_expand(
pxl8_atlas* atlas,
SDL_Renderer* renderer,
pxl8_color_mode color_mode
) {
if (!atlas || atlas->width >= 4096) return false;
u32 new_size = atlas->width * 2;
u32 old_width = atlas->width;
i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
u8* new_pixels = (u8*)SDL_calloc(new_size * new_size, bytes_per_pixel);
if (!new_pixels) return false;
SDL_Texture* new_texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
new_size,
new_size
);
if (!new_texture) {
SDL_free(new_pixels);
return false;
}
pxl8_skyline new_skyline;
new_skyline.nodes = (pxl8_skyline_node*)SDL_calloc(16, sizeof(pxl8_skyline_node));
if (!new_skyline.nodes) {
SDL_DestroyTexture(new_texture);
SDL_free(new_pixels);
return false;
}
new_skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)new_size};
new_skyline.count = 1;
new_skyline.capacity = 16;
for (u32 i = 0; i < atlas->entry_count; i++) {
if (!atlas->entries[i].active) continue;
pxl8_skyline_fit fit = pxl8_skyline_find_position(
&new_skyline,
new_size,
new_size,
atlas->entries[i].w,
atlas->entries[i].h
);
if (!fit.found) {
SDL_free(new_skyline.nodes);
SDL_DestroyTexture(new_texture);
SDL_free(new_pixels);
return false;
}
for (u32 y = 0; y < (u32)atlas->entries[i].h; y++) {
for (u32 x = 0; x < (u32)atlas->entries[i].w; x++) {
u32 src_idx = (atlas->entries[i].y + y) * old_width + (atlas->entries[i].x + x);
u32 dst_idx = (fit.pos.y + y) * new_size + (fit.pos.x + x);
if (bytes_per_pixel == 4) {
((u32*)new_pixels)[dst_idx] = ((u32*)atlas->pixels)[src_idx];
} else {
new_pixels[dst_idx] = atlas->pixels[src_idx];
}
}
}
atlas->entries[i].x = fit.pos.x;
atlas->entries[i].y = fit.pos.y;
pxl8_skyline_add_rect(&new_skyline, fit.pos, atlas->entries[i].w, atlas->entries[i].h);
pxl8_skyline_compact(&new_skyline);
}
SDL_DestroyTexture(atlas->texture);
SDL_free(atlas->pixels);
SDL_free(atlas->skyline.nodes);
atlas->pixels = new_pixels;
atlas->texture = new_texture;
atlas->skyline = new_skyline;
atlas->width = new_size;
atlas->height = new_size;
atlas->dirty = true;
pxl8_debug("Atlas expanded to %ux%u", atlas->width, atlas->height);
return true;
}
static u32 pxl8_atlas_add_texture(
pxl8_atlas* atlas,
const u8* pixels,
u32 w,
u32 h,
const char* path,
pxl8_color_mode color_mode
) {
if (!atlas || !pixels) return UINT32_MAX;
pxl8_skyline_fit fit =
pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) {
SDL_Renderer* renderer = atlas->texture ? SDL_GetRendererFromTexture(atlas->texture) : NULL;
if (!pxl8_atlas_expand(atlas, renderer, color_mode)) {
return UINT32_MAX;
}
fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) return UINT32_MAX;
}
u32 texture_id;
if (atlas->free_count > 0) {
texture_id = atlas->free_list[--atlas->free_count];
} else {
if (atlas->entry_count >= atlas->entry_capacity) {
atlas->entry_capacity *= 2;
atlas->entries = (pxl8_atlas_entry*)SDL_realloc(
atlas->entries,
atlas->entry_capacity * sizeof(pxl8_atlas_entry)
);
}
texture_id = atlas->entry_count++;
}
pxl8_atlas_entry* entry = &atlas->entries[texture_id];
entry->active = true;
entry->texture_id = texture_id;
entry->x = fit.pos.x;
entry->y = fit.pos.y;
entry->w = w;
entry->h = h;
if (path) {
strncpy(entry->path, path, sizeof(entry->path) - 1);
entry->path[sizeof(entry->path) - 1] = '\0';
} else {
snprintf(entry->path, sizeof(entry->path), "procgen_%u", texture_id);
}
i32 bytes_per_pixel = (color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
for (u32 y = 0; y < h; y++) {
for (u32 x = 0; x < w; x++) {
u32 src_idx = y * w + x;
u32 dst_idx = (fit.pos.y + y) * atlas->width + (fit.pos.x + x);
if (bytes_per_pixel == 4) {
((u32*)atlas->pixels)[dst_idx] = ((const u32*)pixels)[src_idx];
} else {
atlas->pixels[dst_idx] = pixels[src_idx];
}
}
}
pxl8_skyline_add_rect(&atlas->skyline, fit.pos, w, h);
pxl8_skyline_compact(&atlas->skyline);
atlas->dirty = true;
return texture_id;
}
static inline void pxl8_color_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) { static inline void pxl8_color_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) {
*r = color & 0xFF; *r = color & 0xFF;
*g = (color >> 8) & 0xFF; *g = (color >> 8) & 0xFF;
@ -448,7 +71,7 @@ static u32 pxl8_get_palette_size(pxl8_color_mode mode) {
case PXL8_COLOR_MODE_FAMI: return 64; case PXL8_COLOR_MODE_FAMI: return 64;
case PXL8_COLOR_MODE_MEGA: return 512; case PXL8_COLOR_MODE_MEGA: return 512;
case PXL8_COLOR_MODE_GBA: return 32768; case PXL8_COLOR_MODE_GBA: return 32768;
case PXL8_COLOR_MODE_SUPERFAMI: return 32768; case PXL8_COLOR_MODE_SNES: return 32768;
default: return 256; default: return 256;
} }
} }
@ -476,11 +99,11 @@ void pxl8_gfx_get_resolution_dimensions(pxl8_resolution resolution, i32* width,
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) { pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) {
pxl8_bounds bounds = {0}; pxl8_bounds bounds = {0};
if (!gfx || !gfx->window) { if (!gfx) {
return bounds; return bounds;
} }
SDL_GetWindowPosition(gfx->window, &bounds.x, &bounds.y); bounds.w = gfx->framebuffer_width;
SDL_GetWindowSize(gfx->window, &bounds.w, &bounds.h); bounds.h = gfx->framebuffer_height;
return bounds; return bounds;
} }
@ -505,18 +128,21 @@ u32 pxl8_gfx_get_palette_size(const pxl8_gfx* gfx) {
} }
pxl8_gfx* pxl8_gfx_create( pxl8_gfx* pxl8_gfx_create(
const pxl8_hal* hal,
pxl8_color_mode mode, pxl8_color_mode mode,
pxl8_resolution resolution, pxl8_resolution resolution,
const char* title, const char* title,
i32 window_width, i32 window_width,
i32 window_height i32 window_height
) { ) {
pxl8_gfx* gfx = (pxl8_gfx*)SDL_calloc(1, sizeof(*gfx)); pxl8_gfx* gfx = (pxl8_gfx*)calloc(1, sizeof(*gfx));
if (!gfx) { if (!gfx) {
pxl8_error("Failed to allocate graphics context"); pxl8_error("Failed to allocate graphics context");
return NULL; return NULL;
} }
gfx->hal = hal;
gfx->color_mode = mode; gfx->color_mode = mode;
pxl8_gfx_get_resolution_dimensions( pxl8_gfx_get_resolution_dimensions(
resolution, resolution,
@ -524,60 +150,25 @@ pxl8_gfx* pxl8_gfx_create(
&gfx->framebuffer_height &gfx->framebuffer_height
); );
gfx->window = SDL_CreateWindow( gfx->platform_data = gfx->hal->create(mode, resolution, title, window_width, window_height);
title, if (!gfx->platform_data) {
window_width, window_height, pxl8_error("Failed to create platform context");
SDL_WINDOW_RESIZABLE free(gfx);
);
if (!gfx->window) {
pxl8_error("Failed to create window: %s", SDL_GetError());
pxl8_gfx_destroy(gfx);
return NULL; return NULL;
} }
gfx->renderer = SDL_CreateRenderer(gfx->window, NULL);
if (!gfx->renderer) {
pxl8_error("Failed to create renderer: %s", SDL_GetError());
pxl8_gfx_destroy(gfx);
return NULL;
}
SDL_SetRenderLogicalPresentation(
gfx->renderer,
gfx->framebuffer_width,
gfx->framebuffer_height,
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE
);
i32 bytes_per_pixel = (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1; i32 bytes_per_pixel = (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) ? 4 : 1;
i32 fb_size = gfx->framebuffer_width * gfx->framebuffer_height * bytes_per_pixel; i32 fb_size = gfx->framebuffer_width * gfx->framebuffer_height * bytes_per_pixel;
gfx->framebuffer = (u8*)SDL_calloc(1, fb_size); gfx->framebuffer = (u8*)calloc(1, fb_size);
if (!gfx->framebuffer) { if (!gfx->framebuffer) {
pxl8_error("Failed to allocate framebuffer"); pxl8_error("Failed to allocate framebuffer");
pxl8_gfx_destroy(gfx); pxl8_gfx_destroy(gfx);
return NULL; return NULL;
} }
gfx->framebuffer_texture = SDL_CreateTexture(
gfx->renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
gfx->framebuffer_width,
gfx->framebuffer_height
);
SDL_SetTextureScaleMode(gfx->framebuffer_texture, SDL_SCALEMODE_NEAREST);
if (!gfx->framebuffer_texture) {
pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError());
pxl8_gfx_destroy(gfx);
return NULL;
}
gfx->palette_size = pxl8_get_palette_size(mode); gfx->palette_size = pxl8_get_palette_size(mode);
if (gfx->palette_size > 0) { if (gfx->palette_size > 0) {
gfx->palette = (u32*)SDL_calloc(gfx->palette_size, sizeof(u32)); gfx->palette = (u32*)calloc(gfx->palette_size, sizeof(u32));
if (!gfx->palette) { if (!gfx->palette) {
pxl8_error("Failed to allocate palette"); pxl8_error("Failed to allocate palette");
pxl8_gfx_destroy(gfx); pxl8_gfx_destroy(gfx);
@ -614,27 +205,17 @@ void pxl8_gfx_destroy(pxl8_gfx* gfx) {
if (!gfx) return; if (!gfx) return;
pxl8_atlas_destroy(gfx->atlas); pxl8_atlas_destroy(gfx->atlas);
free(gfx->sprite_cache);
if (gfx->framebuffer_texture) { if (gfx->hal && gfx->platform_data) {
SDL_DestroyTexture(gfx->framebuffer_texture); gfx->hal->destroy(gfx->platform_data);
gfx->framebuffer_texture = NULL;
} }
if (gfx->renderer) { free(gfx->framebuffer);
SDL_DestroyRenderer(gfx->renderer); free(gfx->palette);
gfx->renderer = NULL; free(gfx->zbuffer);
}
if (gfx->window) { free(gfx);
SDL_DestroyWindow(gfx->window);
gfx->window = NULL;
}
SDL_free(gfx->framebuffer);
SDL_free(gfx->palette);
SDL_free(gfx->zbuffer);
SDL_free(gfx);
} }
@ -642,41 +223,41 @@ pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width,
if (!gfx || !gfx->initialized || !pixels) return PXL8_ERROR_INVALID_ARGUMENT; if (!gfx || !gfx->initialized || !pixels) return PXL8_ERROR_INVALID_ARGUMENT;
if (!gfx->atlas) { if (!gfx->atlas) {
gfx->atlas = pxl8_atlas_create(gfx->renderer, 1024, 1024, gfx->color_mode); gfx->atlas = pxl8_atlas_create(1024, 1024, gfx->color_mode);
if (!gfx->atlas) return PXL8_ERROR_OUT_OF_MEMORY; if (!gfx->atlas) return PXL8_ERROR_OUT_OF_MEMORY;
} }
u32 texture_id = u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->color_mode);
pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, NULL, gfx->color_mode);
if (texture_id == UINT32_MAX) { if (texture_id == UINT32_MAX) {
pxl8_error("Texture doesn't fit in atlas"); pxl8_error("Texture doesn't fit in atlas");
return PXL8_ERROR_INVALID_SIZE; return PXL8_ERROR_INVALID_SIZE;
} }
pxl8_debug(
"Created texture %u: %ux%u at (%d,%d)",
texture_id, width, height,
gfx->atlas->entries[texture_id].x,
gfx->atlas->entries[texture_id].y
);
return texture_id; return texture_id;
} }
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) { pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) {
if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT; if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT;
if (!gfx->atlas) { if (!gfx->sprite_cache) {
gfx->atlas = pxl8_atlas_create(gfx->renderer, 1024, 1024, gfx->color_mode); gfx->sprite_cache_capacity = 64;
if (!gfx->atlas) return PXL8_ERROR_OUT_OF_MEMORY; gfx->sprite_cache = (pxl8_sprite_cache_entry*)calloc(
gfx->sprite_cache_capacity, sizeof(pxl8_sprite_cache_entry)
);
if (!gfx->sprite_cache) return PXL8_ERROR_OUT_OF_MEMORY;
} }
for (u32 i = 0; i < gfx->atlas->entry_count; i++) { for (u32 i = 0; i < gfx->sprite_cache_count; i++) {
if (gfx->atlas->entries[i].active && strcmp(gfx->atlas->entries[i].path, path) == 0) { if (gfx->sprite_cache[i].active && strcmp(gfx->sprite_cache[i].path, path) == 0) {
return gfx->atlas->entries[i].texture_id; return gfx->sprite_cache[i].sprite_id;
} }
} }
if (!gfx->atlas) {
gfx->atlas = pxl8_atlas_create(1024, 1024, gfx->color_mode);
if (!gfx->atlas) return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_ase_file ase_file; pxl8_ase_file ase_file;
pxl8_result result = pxl8_ase_load(path, &ase_file); pxl8_result result = pxl8_ase_load(path, &ase_file);
if (result != PXL8_OK) { if (result != PXL8_OK) {
@ -690,31 +271,46 @@ pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) {
return PXL8_ERROR_INVALID_FORMAT; return PXL8_ERROR_INVALID_FORMAT;
} }
u32 sprite_w = ase_file.header.width; u32 sprite_id = pxl8_atlas_add_texture(
u32 sprite_h = ase_file.header.height;
u32 texture_id = pxl8_atlas_add_texture(
gfx->atlas, gfx->atlas,
ase_file.frames[0].pixels, ase_file.frames[0].pixels,
sprite_w, ase_file.header.width,
sprite_h, ase_file.header.height,
path,
gfx->color_mode gfx->color_mode
); );
pxl8_ase_destroy(&ase_file); pxl8_ase_destroy(&ase_file);
if (texture_id == UINT32_MAX) { if (sprite_id == UINT32_MAX) {
pxl8_error("Sprite doesn't fit in atlas"); pxl8_error("Sprite doesn't fit in atlas");
return PXL8_ERROR_INVALID_SIZE; return PXL8_ERROR_INVALID_SIZE;
} }
pxl8_debug("Loaded sprite %u: %ux%u at (%d,%d)", if (gfx->sprite_cache_count >= gfx->sprite_cache_capacity) {
texture_id, sprite_w, sprite_h, gfx->sprite_cache_capacity *= 2;
gfx->atlas->entries[texture_id].x, gfx->sprite_cache = (pxl8_sprite_cache_entry*)realloc(
gfx->atlas->entries[texture_id].y); gfx->sprite_cache,
gfx->sprite_cache_capacity * sizeof(pxl8_sprite_cache_entry)
);
}
return texture_id; pxl8_sprite_cache_entry* entry = &gfx->sprite_cache[gfx->sprite_cache_count++];
entry->active = true;
entry->sprite_id = sprite_id;
strncpy(entry->path, path, sizeof(entry->path) - 1);
entry->path[sizeof(entry->path) - 1] = '\0';
return sprite_id;
}
pxl8_atlas* pxl8_gfx_get_atlas(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized) return NULL;
if (!gfx->atlas) {
gfx->atlas = pxl8_atlas_create(1024, 1024, gfx->color_mode);
}
return gfx->atlas;
} }
pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path) { pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path) {
@ -758,94 +354,32 @@ pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx) {
return PXL8_OK; return PXL8_OK;
} }
static void pxl8_upload_indexed_texture( void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) {
SDL_Texture* texture, if (!gfx || !gfx->initialized || !gfx->atlas) return;
const u8* indexed,
const u32* palette,
u32 palette_size,
i32 width,
i32 height,
u32 default_color
) {
static u32* rgba_buffer = NULL;
static size_t buffer_size = 0;
size_t needed_size = width * height;
if (buffer_size < needed_size) { if (gfx->hal && gfx->hal->upload_atlas) {
rgba_buffer = (u32*)SDL_realloc(rgba_buffer, needed_size * 4); gfx->hal->upload_atlas(gfx->platform_data, gfx->atlas, gfx->palette, gfx->color_mode);
buffer_size = needed_size; pxl8_atlas_mark_clean(gfx->atlas);
} }
if (!rgba_buffer) return;
for (i32 i = 0; i < width * height; i++) {
u8 index = indexed[i];
rgba_buffer[i] = (index < palette_size) ? palette[index] : default_color;
}
SDL_UpdateTexture(texture, NULL, rgba_buffer, width * 4);
} }
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) { void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->framebuffer_texture) return; if (!gfx || !gfx->initialized || !gfx->hal) return;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { gfx->hal->upload_framebuffer(
SDL_UpdateTexture( gfx->platform_data,
gfx->framebuffer_texture, gfx->framebuffer,
NULL, gfx->framebuffer_width,
gfx->framebuffer, gfx->framebuffer_height,
gfx->framebuffer_width * 4 gfx->palette,
); gfx->color_mode
} else { );
pxl8_upload_indexed_texture(
gfx->framebuffer_texture,
gfx->framebuffer,
gfx->palette,
gfx->palette_size,
gfx->framebuffer_width,
gfx->framebuffer_height,
0xFF000000
);
}
}
void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->atlas || !gfx->atlas->texture || !gfx->atlas->dirty) return;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
SDL_UpdateTexture(
gfx->atlas->texture,
NULL,
gfx->atlas->pixels,
gfx->atlas->width * 4
);
} else {
pxl8_upload_indexed_texture(
gfx->atlas->texture,
gfx->atlas->pixels,
gfx->palette,
gfx->palette_size,
gfx->atlas->width,
gfx->atlas->height,
0x00000000
);
}
gfx->atlas->dirty = false;
pxl8_debug("Atlas uploaded to GPU");
} }
void pxl8_gfx_present(pxl8_gfx* gfx) { void pxl8_gfx_present(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized) return; if (!gfx || !gfx->initialized || !gfx->hal) return;
SDL_SetRenderDrawColor(gfx->renderer, 0, 0, 0, 255); gfx->hal->present(gfx->platform_data);
SDL_RenderClear(gfx->renderer);
if (gfx->framebuffer_texture) {
SDL_RenderTexture(gfx->renderer, gfx->framebuffer_texture, NULL, NULL);
}
SDL_RenderPresent(gfx->renderer);
} }
pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height) { pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height) {
@ -1057,10 +591,10 @@ void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) {
void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
if (!gfx || !gfx->atlas || !gfx->framebuffer || sprite_id >= gfx->atlas->entry_count) return; if (!gfx || !gfx->atlas || !gfx->framebuffer) return;
if (!gfx->atlas->entries[sprite_id].active) return;
pxl8_atlas_entry* entry = &gfx->atlas->entries[sprite_id]; const pxl8_atlas_entry* entry = pxl8_atlas_get_entry(gfx->atlas, sprite_id);
if (!entry || !entry->active) return;
i32 clip_left = (x < 0) ? -x : 0; i32 clip_left = (x < 0) ? -x : 0;
i32 clip_top = (y < 0) ? -y : 0; i32 clip_top = (y < 0) ? -y : 0;
@ -1078,15 +612,18 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
bool is_1to1_scale = (w == entry->w && h == entry->h); bool is_1to1_scale = (w == entry->w && h == entry->h);
bool is_unclipped = (clip_left == 0 && clip_top == 0 && clip_right == 0 && clip_bottom == 0); bool is_unclipped = (clip_left == 0 && clip_top == 0 && clip_right == 0 && clip_bottom == 0);
u32 atlas_width = pxl8_atlas_get_width(gfx->atlas);
const u8* atlas_pixels = pxl8_atlas_get_pixels(gfx->atlas);
if (is_1to1_scale && is_unclipped && pxl8_is_simd_aligned(w)) { if (is_1to1_scale && is_unclipped && pxl8_is_simd_aligned(w)) {
const u8* sprite_data = gfx->atlas->pixels + entry->y * gfx->atlas->width + entry->x; const u8* sprite_data = atlas_pixels + entry->y * atlas_width + entry->x;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
pxl8_blit_simd_hicolor( pxl8_blit_simd_hicolor(
(u32*)gfx->framebuffer, (u32*)gfx->framebuffer,
gfx->framebuffer_width, gfx->framebuffer_width,
(const u32*)sprite_data, (const u32*)sprite_data,
gfx->atlas->width, atlas_width,
x, y, w, h x, y, w, h
); );
} else { } else {
@ -1094,7 +631,7 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
gfx->framebuffer, gfx->framebuffer,
gfx->framebuffer_width, gfx->framebuffer_width,
sprite_data, sprite_data,
gfx->atlas->width, atlas_width,
x, y, w, h x, y, w, h
); );
} }
@ -1103,16 +640,16 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) {
for (i32 px = 0; px < draw_width; px++) { for (i32 px = 0; px < draw_width; px++) {
i32 src_x = entry->x + ((px + clip_left) * entry->w) / w; i32 src_x = entry->x + ((px + clip_left) * entry->w) / w;
i32 src_y = entry->y + ((py + clip_top) * entry->h) / h; i32 src_y = entry->y + ((py + clip_top) * entry->h) / h;
i32 src_idx = src_y * gfx->atlas->width + src_x; i32 src_idx = src_y * atlas_width + src_x;
i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px); i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px);
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
u32 pixel = ((u32*)gfx->atlas->pixels)[src_idx]; u32 pixel = ((const u32*)atlas_pixels)[src_idx];
if (pixel & 0xFF000000) { if (pixel & 0xFF000000) {
((u32*)gfx->framebuffer)[dest_idx] = pixel; ((u32*)gfx->framebuffer)[dest_idx] = pixel;
} }
} else { } else {
u8 pixel = gfx->atlas->pixels[src_idx]; u8 pixel = atlas_pixels[src_idx];
if (pixel != 0) { if (pixel != 0) {
gfx->framebuffer[dest_idx] = pixel; gfx->framebuffer[dest_idx] = pixel;
} }
@ -1238,7 +775,7 @@ static bool pxl8_3d_init_zbuffer(pxl8_gfx* gfx) {
gfx->zbuffer_width = gfx->framebuffer_width; gfx->zbuffer_width = gfx->framebuffer_width;
gfx->zbuffer_height = gfx->framebuffer_height; gfx->zbuffer_height = gfx->framebuffer_height;
gfx->zbuffer = (f32*)SDL_calloc(gfx->zbuffer_width * gfx->zbuffer_height, sizeof(f32)); gfx->zbuffer = (f32*)calloc(gfx->zbuffer_width * gfx->zbuffer_height, sizeof(f32));
if (!gfx->zbuffer) { if (!gfx->zbuffer) {
return false; return false;
} }
@ -1349,10 +886,10 @@ static inline void pxl8_fill_scanline(pxl8_gfx* gfx, i32 y, i32 xs, i32 xe, f32
} }
static inline u32 pxl8_sample_texture(pxl8_gfx* gfx, u32 texture_id, f32 u, f32 v) { static inline u32 pxl8_sample_texture(pxl8_gfx* gfx, u32 texture_id, f32 u, f32 v) {
if (!gfx->atlas || texture_id >= gfx->atlas->entry_count) return 0; if (!gfx->atlas) return 0;
if (!gfx->atlas->entries[texture_id].active) return 0;
pxl8_atlas_entry* entry = &gfx->atlas->entries[texture_id]; const pxl8_atlas_entry* entry = pxl8_atlas_get_entry(gfx->atlas, texture_id);
if (!entry || !entry->active) return 0;
u = u - floorf(u); u = u - floorf(u);
v = v - floorf(v); v = v - floorf(v);
@ -1362,12 +899,14 @@ static inline u32 pxl8_sample_texture(pxl8_gfx* gfx, u32 texture_id, f32 u, f32
i32 atlas_x = entry->x + tx; i32 atlas_x = entry->x + tx;
i32 atlas_y = entry->y + ty; i32 atlas_y = entry->y + ty;
i32 idx = atlas_y * gfx->atlas->width + atlas_x; u32 atlas_width = pxl8_atlas_get_width(gfx->atlas);
const u8* atlas_pixels = pxl8_atlas_get_pixels(gfx->atlas);
i32 idx = atlas_y * atlas_width + atlas_x;
if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) { if (gfx->color_mode == PXL8_COLOR_MODE_HICOLOR) {
return ((u32*)gfx->atlas->pixels)[idx]; return ((const u32*)atlas_pixels)[idx];
} else { } else {
return gfx->atlas->pixels[idx]; return atlas_pixels[idx];
} }
} }

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "pxl8_hal.h"
#include "pxl8_math.h" #include "pxl8_math.h"
#include "pxl8_types.h" #include "pxl8_types.h"
@ -56,7 +57,7 @@ typedef struct pxl8_triangle {
extern "C" { extern "C" {
#endif #endif
pxl8_gfx* pxl8_gfx_create(pxl8_color_mode mode, pxl8_resolution resolution, const char* title, i32 window_width, i32 window_height); pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, pxl8_color_mode mode, pxl8_resolution resolution, const char* title, i32 window_width, i32 window_height);
void pxl8_gfx_destroy(pxl8_gfx* gfx); void pxl8_gfx_destroy(pxl8_gfx* gfx);
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx); pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx);

20
src/pxl8_hal.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_hal {
void* (*create)(pxl8_color_mode mode, pxl8_resolution res,
const char* title, i32 win_w, i32 win_h);
void (*destroy)(void* platform_data);
u64 (*get_ticks)(void);
void (*present)(void* platform_data);
void (*upload_atlas)(void* platform_data, const pxl8_atlas* atlas,
const u32* palette, pxl8_color_mode mode);
void (*upload_framebuffer)(void* platform_data, const u8* fb,
i32 w, i32 h, const u32* palette,
pxl8_color_mode mode);
} pxl8_hal;

View file

@ -1,7 +1,9 @@
#include <ctype.h> #include <ctype.h>
#include <SDL3/SDL.h> #include <stdlib.h>
#include <string.h>
#include "pxl8_io.h" #include "pxl8_io.h"
#include "pxl8_types.h"
pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) { pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) {
if (!path || !content || !size) return PXL8_ERROR_NULL_POINTER; if (!path || !content || !size) return PXL8_ERROR_NULL_POINTER;
@ -20,7 +22,7 @@ pxl8_result pxl8_io_read_file(const char* path, char** content, size_t* size) {
return PXL8_ERROR_SYSTEM_FAILURE; return PXL8_ERROR_SYSTEM_FAILURE;
} }
*content = SDL_malloc(file_size + 1); *content = malloc(file_size + 1);
if (!*content) { if (!*content) {
fclose(file); fclose(file);
return PXL8_ERROR_OUT_OF_MEMORY; return PXL8_ERROR_OUT_OF_MEMORY;
@ -88,21 +90,48 @@ pxl8_result pxl8_io_create_directory(const char* path) {
void pxl8_io_free_file_content(char* content) { void pxl8_io_free_file_content(char* content) {
if (content) { if (content) {
SDL_free(content); free(content);
} }
} }
void pxl8_io_free_binary_data(u8* data) { void pxl8_io_free_binary_data(u8* data) {
if (data) { if (data) {
SDL_free(data); free(data);
} }
} }
static i32 pxl8_key_code(const char* key_name) { static i32 pxl8_key_code(const char* key_name) {
if (!key_name || !key_name[0]) return 0; if (!key_name || !key_name[0]) return 0;
SDL_Scancode scancode = SDL_GetScancodeFromName(key_name); typedef struct { const char* name; i32 code; } KeyMapping;
return (i32)scancode; static const KeyMapping keys[] = {
{"a", 4}, {"b", 5}, {"c", 6}, {"d", 7}, {"e", 8}, {"f", 9}, {"g", 10}, {"h", 11},
{"i", 12}, {"j", 13}, {"k", 14}, {"l", 15}, {"m", 16}, {"n", 17}, {"o", 18}, {"p", 19},
{"q", 20}, {"r", 21}, {"s", 22}, {"t", 23}, {"u", 24}, {"v", 25}, {"w", 26}, {"x", 27},
{"y", 28}, {"z", 29},
{"1", 30}, {"2", 31}, {"3", 32}, {"4", 33}, {"5", 34},
{"6", 35}, {"7", 36}, {"8", 37}, {"9", 38}, {"0", 39},
{"return", 40}, {"escape", 41}, {"backspace", 42}, {"tab", 43}, {"space", 44},
{"left", 80}, {"right", 79}, {"up", 82}, {"down", 81},
{"f1", 58}, {"f2", 59}, {"f3", 60}, {"f4", 61}, {"f5", 62}, {"f6", 63},
{"f7", 64}, {"f8", 65}, {"f9", 66}, {"f10", 67}, {"f11", 68}, {"f12", 69},
{NULL, 0}
};
char lower_name[64];
size_t i;
for (i = 0; i < sizeof(lower_name) - 1 && key_name[i]; i++) {
lower_name[i] = (char)tolower((unsigned char)key_name[i]);
}
lower_name[i] = '\0';
for (i = 0; keys[i].name; i++) {
if (strcmp(lower_name, keys[i].name) == 0) {
return keys[i].code;
}
}
return 0;
} }
bool pxl8_key_down(const pxl8_input_state* input, const char* key_name) { bool pxl8_key_down(const pxl8_input_state* input, const char* key_name) {

View file

@ -7,7 +7,6 @@
#include <lauxlib.h> #include <lauxlib.h>
#include <lua.h> #include <lua.h>
#include <lualib.h> #include <lualib.h>
#include <SDL3/SDL.h>
#include "pxl8_macros.h" #include "pxl8_macros.h"
#include "pxl8_script.h" #include "pxl8_script.h"
@ -222,12 +221,12 @@ const char* pxl8_script_get_last_error(pxl8_script* script) {
} }
pxl8_script* pxl8_script_create(void) { pxl8_script* pxl8_script_create(void) {
pxl8_script* script = SDL_calloc(1, sizeof(pxl8_script)); pxl8_script* script = (pxl8_script*)calloc(1, sizeof(pxl8_script));
if (!script) return NULL; if (!script) return NULL;
script->L = luaL_newstate(); script->L = luaL_newstate();
if (!script->L) { if (!script->L) {
SDL_free(script); free(script);
return NULL; return NULL;
} }
@ -285,7 +284,7 @@ void pxl8_script_destroy(pxl8_script* script) {
if (script->L) { if (script->L) {
lua_close(script->L); lua_close(script->L);
} }
SDL_free(script); free(script);
} }
void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx) { void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx) {
@ -600,3 +599,175 @@ bool pxl8_script_check_reload(pxl8_script* script) {
return false; return false;
} }
#include <pthread.h>
#include <string.h>
#include <linenoise.h>
#define PXL8_MAX_REPL_COMMANDS 4096
struct pxl8_script_repl_command {
char buffer[PXL8_MAX_REPL_COMMANDS];
struct pxl8_script_repl_command* next;
};
struct pxl8_script_repl {
pthread_t thread;
pthread_mutex_t mutex;
pxl8_script_repl_command* queue_head;
pxl8_script_repl_command* queue_tail;
bool running;
};
static void pxl8_script_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"
};
size_t 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_script_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_script_repl_stdin_thread(void* user_data) {
pxl8_script_repl* repl = (pxl8_script_repl*)user_data;
char* line;
const char* history_file = ".pxl8_history";
linenoiseHistorySetMaxLen(100);
linenoiseSetMultiLine(1);
linenoiseSetCompletionCallback(pxl8_script_repl_completion);
linenoiseSetHintsCallback(pxl8_script_repl_hints);
linenoiseHistoryLoad(history_file);
while (repl->running && (line = linenoise(">> "))) {
if (strlen(line) > 0) {
linenoiseHistoryAdd(line);
linenoiseHistorySave(history_file);
pxl8_script_repl_command* cmd = (pxl8_script_repl_command*)malloc(sizeof(pxl8_script_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;
}
pxl8_script_repl* pxl8_script_repl_create(void) {
pxl8_script_repl* repl = (pxl8_script_repl*)calloc(1, sizeof(pxl8_script_repl));
return repl;
}
void pxl8_script_repl_destroy(pxl8_script_repl* repl) {
if (!repl) return;
free(repl);
}
void pxl8_script_repl_init(pxl8_script_repl* repl) {
if (!repl) return;
repl->queue_head = NULL;
repl->queue_tail = NULL;
repl->running = true;
pthread_mutex_init(&repl->mutex, NULL);
pthread_create(&repl->thread, NULL, pxl8_script_repl_stdin_thread, repl);
}
void pxl8_script_repl_shutdown(pxl8_script_repl* repl) {
if (!repl) return;
repl->running = false;
pthread_join(repl->thread, NULL);
pthread_mutex_destroy(&repl->mutex);
pxl8_script_repl_command* cmd = repl->queue_head;
while (cmd) {
pxl8_script_repl_command* next = cmd->next;
free(cmd);
cmd = next;
}
}
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl) {
if (!repl) return NULL;
pthread_mutex_lock(&repl->mutex);
pxl8_script_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;
}
const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd) {
return cmd ? cmd->buffer : NULL;
}
void pxl8_script_repl_command_free(pxl8_script_repl_command* cmd) {
free(cmd);
}

View file

@ -5,6 +5,8 @@
#include "pxl8_ui.h" #include "pxl8_ui.h"
typedef struct pxl8_script pxl8_script; typedef struct pxl8_script pxl8_script;
typedef struct pxl8_script_repl pxl8_script_repl;
typedef struct pxl8_script_repl_command pxl8_script_repl_command;
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -29,6 +31,14 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename);
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path); pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path);
bool pxl8_script_check_reload(pxl8_script* script); bool pxl8_script_check_reload(pxl8_script* script);
pxl8_script_repl* pxl8_script_repl_create(void);
void pxl8_script_repl_destroy(pxl8_script_repl* repl);
void pxl8_script_repl_init(pxl8_script_repl* repl);
void pxl8_script_repl_shutdown(pxl8_script_repl* repl);
pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl);
const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd);
void pxl8_script_repl_command_free(pxl8_script_repl_command* cmd);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

392
src/pxl8_sdl3.c Normal file
View file

@ -0,0 +1,392 @@
#include "pxl8_sdl3.h"
#include "pxl8_atlas.h"
#include "pxl8_game.h"
#include "pxl8_macros.h"
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
typedef struct pxl8_sdl3_context {
SDL_Window* window;
SDL_Renderer* renderer;
SDL_Texture* framebuffer_texture;
SDL_Texture* atlas_texture;
u32* rgba_buffer;
size_t rgba_buffer_size;
u32 atlas_width;
u32 atlas_height;
} pxl8_sdl3_context;
static void* sdl3_create(pxl8_color_mode mode, pxl8_resolution resolution,
const char* title, i32 win_w, i32 win_h) {
(void)mode;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)SDL_calloc(1, sizeof(*ctx));
if (!ctx) {
pxl8_error("Failed to allocate SDL3 context");
return NULL;
}
i32 fb_w, fb_h;
switch (resolution) {
case PXL8_RESOLUTION_240x160: fb_w = 240; fb_h = 160; break;
case PXL8_RESOLUTION_320x180: fb_w = 320; fb_h = 180; break;
case PXL8_RESOLUTION_320x240: fb_w = 320; fb_h = 240; break;
case PXL8_RESOLUTION_640x360: fb_w = 640; fb_h = 360; break;
case PXL8_RESOLUTION_640x480: fb_w = 640; fb_h = 480; break;
case PXL8_RESOLUTION_800x600: fb_w = 800; fb_h = 600; break;
case PXL8_RESOLUTION_960x540: fb_w = 960; fb_h = 540; break;
default: fb_w = 640; fb_h = 360; break;
}
ctx->window = SDL_CreateWindow(title, win_w, win_h, SDL_WINDOW_RESIZABLE);
if (!ctx->window) {
pxl8_error("Failed to create window: %s", SDL_GetError());
SDL_free(ctx);
return NULL;
}
ctx->renderer = SDL_CreateRenderer(ctx->window, NULL);
if (!ctx->renderer) {
pxl8_error("Failed to create renderer: %s", SDL_GetError());
SDL_DestroyWindow(ctx->window);
SDL_free(ctx);
return NULL;
}
SDL_SetRenderLogicalPresentation(ctx->renderer, fb_w, fb_h,
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
ctx->framebuffer_texture = SDL_CreateTexture(ctx->renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
fb_w, fb_h);
if (!ctx->framebuffer_texture) {
pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError());
SDL_DestroyRenderer(ctx->renderer);
SDL_DestroyWindow(ctx->window);
SDL_free(ctx);
return NULL;
}
SDL_SetTextureScaleMode(ctx->framebuffer_texture, SDL_SCALEMODE_NEAREST);
ctx->rgba_buffer = NULL;
ctx->rgba_buffer_size = 0;
return ctx;
}
static void sdl3_destroy(void* platform_data) {
if (!platform_data) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
if (ctx->rgba_buffer) {
SDL_free(ctx->rgba_buffer);
}
if (ctx->atlas_texture) {
SDL_DestroyTexture(ctx->atlas_texture);
}
if (ctx->framebuffer_texture) {
SDL_DestroyTexture(ctx->framebuffer_texture);
}
if (ctx->renderer) {
SDL_DestroyRenderer(ctx->renderer);
}
if (ctx->window) {
SDL_DestroyWindow(ctx->window);
}
SDL_free(ctx);
}
static u64 sdl3_get_ticks(void) {
return SDL_GetTicksNS();
}
static void sdl3_present(void* platform_data) {
if (!platform_data) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
SDL_SetRenderDrawColor(ctx->renderer, 0, 0, 0, 255);
SDL_RenderClear(ctx->renderer);
if (ctx->framebuffer_texture) {
SDL_RenderTexture(ctx->renderer, ctx->framebuffer_texture, NULL, NULL);
}
SDL_RenderPresent(ctx->renderer);
}
static void sdl3_upload_framebuffer(void* platform_data, const u8* fb,
i32 w, i32 h, const u32* palette,
pxl8_color_mode mode) {
if (!platform_data || !fb) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
if (mode == PXL8_COLOR_MODE_HICOLOR) {
SDL_UpdateTexture(ctx->framebuffer_texture, NULL, fb, w * 4);
} else {
size_t needed_size = w * h;
if (ctx->rgba_buffer_size < needed_size) {
ctx->rgba_buffer = (u32*)SDL_realloc(ctx->rgba_buffer, needed_size * 4);
ctx->rgba_buffer_size = needed_size;
}
if (!ctx->rgba_buffer) return;
u32 palette_size;
switch (mode) {
case PXL8_COLOR_MODE_FAMI: palette_size = 64; break;
case PXL8_COLOR_MODE_MEGA: palette_size = 512; break;
case PXL8_COLOR_MODE_GBA: palette_size = 32768; break;
case PXL8_COLOR_MODE_SNES: palette_size = 32768; break;
default: palette_size = 256; break;
}
for (i32 i = 0; i < w * h; i++) {
u8 index = fb[i];
ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0xFF000000;
}
SDL_UpdateTexture(ctx->framebuffer_texture, NULL, ctx->rgba_buffer, w * 4);
}
}
static void sdl3_upload_atlas(void* platform_data, const pxl8_atlas* atlas,
const u32* palette, pxl8_color_mode mode) {
if (!platform_data || !atlas) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
if (!pxl8_atlas_is_dirty(atlas)) return;
u32 atlas_w = pxl8_atlas_get_width(atlas);
u32 atlas_h = pxl8_atlas_get_height(atlas);
const u8* atlas_pixels = pxl8_atlas_get_pixels(atlas);
if (!ctx->atlas_texture || ctx->atlas_width != atlas_w || ctx->atlas_height != atlas_h) {
if (ctx->atlas_texture) {
SDL_DestroyTexture(ctx->atlas_texture);
}
ctx->atlas_texture = SDL_CreateTexture(
ctx->renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
atlas_w,
atlas_h
);
if (!ctx->atlas_texture) {
pxl8_error("Failed to create atlas texture: %s", SDL_GetError());
return;
}
SDL_SetTextureScaleMode(ctx->atlas_texture, SDL_SCALEMODE_NEAREST);
SDL_SetTextureBlendMode(ctx->atlas_texture, SDL_BLENDMODE_BLEND);
ctx->atlas_width = atlas_w;
ctx->atlas_height = atlas_h;
}
if (mode == PXL8_COLOR_MODE_HICOLOR) {
SDL_UpdateTexture(ctx->atlas_texture, NULL, atlas_pixels, atlas_w * 4);
} else {
size_t needed_size = atlas_w * atlas_h;
if (ctx->rgba_buffer_size < needed_size) {
ctx->rgba_buffer = (u32*)SDL_realloc(ctx->rgba_buffer, needed_size * 4);
ctx->rgba_buffer_size = needed_size;
}
if (!ctx->rgba_buffer) return;
u32 palette_size;
switch (mode) {
case PXL8_COLOR_MODE_FAMI: palette_size = 64; break;
case PXL8_COLOR_MODE_MEGA: palette_size = 512; break;
case PXL8_COLOR_MODE_GBA: palette_size = 32768; break;
case PXL8_COLOR_MODE_SNES: palette_size = 32768; break;
default: palette_size = 256; break;
}
for (u32 i = 0; i < atlas_w * atlas_h; i++) {
u8 index = atlas_pixels[i];
ctx->rgba_buffer[i] = (index < palette_size) ? palette[index] : 0x00000000;
}
SDL_UpdateTexture(ctx->atlas_texture, NULL, ctx->rgba_buffer, atlas_w * 4);
}
}
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
pxl8_error("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
pxl8_game* game = (pxl8_game*)SDL_calloc(1, sizeof(pxl8_game));
if (!game) {
pxl8_error("Failed to allocate game instance");
return SDL_APP_FAILURE;
}
pxl8_game_result result = pxl8_init(game, argc, argv);
*appstate = game;
switch (result) {
case PXL8_GAME_CONTINUE:
case PXL8_GAME_SUCCESS:
return SDL_APP_CONTINUE;
case PXL8_GAME_FAILURE:
default:
return SDL_APP_FAILURE;
}
}
SDL_AppResult SDL_AppIterate(void* appstate) {
pxl8_game* game = (pxl8_game*)appstate;
if (!game) {
return SDL_APP_FAILURE;
}
pxl8_game_result update_result = pxl8_update(game);
if (update_result == PXL8_GAME_FAILURE) {
return SDL_APP_FAILURE;
}
if (update_result == PXL8_GAME_SUCCESS) {
return SDL_APP_SUCCESS;
}
pxl8_game_result frame_result = pxl8_frame(game);
if (frame_result == PXL8_GAME_FAILURE) {
return SDL_APP_FAILURE;
}
return (frame_result == PXL8_GAME_SUCCESS) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
pxl8_game* game = (pxl8_game*)appstate;
if (!game) {
return SDL_APP_CONTINUE;
}
switch (event->type) {
case SDL_EVENT_QUIT:
game->running = false;
break;
case SDL_EVENT_KEY_DOWN: {
if (event->key.key == SDLK_ESCAPE) {
game->running = false;
}
SDL_Scancode scancode = event->key.scancode;
if (scancode < 256) {
if (!game->input.keys_down[scancode]) {
game->input.keys_pressed[scancode] = true;
}
game->input.keys_down[scancode] = true;
game->input.keys_released[scancode] = false;
}
break;
}
case SDL_EVENT_KEY_UP: {
SDL_Scancode scancode = event->key.scancode;
if (scancode < 256) {
game->input.keys_down[scancode] = false;
game->input.keys_pressed[scancode] = false;
game->input.keys_released[scancode] = true;
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
u8 button = event->button.button - 1;
if (button < 3) {
if (!game->input.mouse_buttons_down[button]) {
game->input.mouse_buttons_pressed[button] = true;
}
game->input.mouse_buttons_down[button] = true;
game->input.mouse_buttons_released[button] = false;
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_UP: {
u8 button = event->button.button - 1;
if (button < 3) {
game->input.mouse_buttons_down[button] = false;
game->input.mouse_buttons_pressed[button] = false;
game->input.mouse_buttons_released[button] = true;
}
break;
}
case SDL_EVENT_MOUSE_MOTION: {
if (!game->gfx) break;
i32 window_mouse_x = (i32)event->motion.x;
i32 window_mouse_y = (i32)event->motion.y;
SDL_Window* window = SDL_GetWindowFromID(event->motion.windowID);
if (!window) break;
i32 window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
i32 render_width, render_height;
pxl8_gfx_get_resolution_dimensions(game->resolution, &render_width, &render_height);
pxl8_bounds window_bounds = {0, 0, window_width, window_height};
pxl8_viewport vp = pxl8_gfx_viewport(window_bounds, render_width, render_height);
game->input.mouse_x = (i32)((window_mouse_x - vp.offset_x) / vp.scale);
game->input.mouse_y = (i32)((window_mouse_y - vp.offset_y) / vp.scale);
break;
}
case SDL_EVENT_MOUSE_WHEEL: {
game->input.mouse_wheel_x = (i32)event->wheel.x;
game->input.mouse_wheel_y = (i32)event->wheel.y;
break;
}
}
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void* appstate, SDL_AppResult result) {
pxl8_game* game = (pxl8_game*)appstate;
(void)result;
if (game) {
pxl8_quit(game);
SDL_free(game);
}
SDL_Quit();
}
const pxl8_hal pxl8_hal_sdl3 = {
.create = sdl3_create,
.destroy = sdl3_destroy,
.get_ticks = sdl3_get_ticks,
.present = sdl3_present,
.upload_atlas = sdl3_upload_atlas,
.upload_framebuffer = sdl3_upload_framebuffer,
};

5
src/pxl8_sdl3.h Normal file
View file

@ -0,0 +1,5 @@
#pragma once
#include "pxl8_hal.h"
extern const pxl8_hal pxl8_hal_sdl3;

View file

@ -25,7 +25,7 @@ typedef enum pxl8_color_mode {
PXL8_COLOR_MODE_GBA, PXL8_COLOR_MODE_GBA,
PXL8_COLOR_MODE_HICOLOR, PXL8_COLOR_MODE_HICOLOR,
PXL8_COLOR_MODE_MEGA, PXL8_COLOR_MODE_MEGA,
PXL8_COLOR_MODE_SUPERFAMI PXL8_COLOR_MODE_SNES
} pxl8_color_mode; } pxl8_color_mode;
typedef enum pxl8_resolution { typedef enum pxl8_resolution {

View file

@ -2,7 +2,6 @@
#include "../lib/microui/src/microui.h" #include "../lib/microui/src/microui.h"
#include "pxl8_font.h" #include "pxl8_font.h"
#include <SDL3/SDL.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>

View file

@ -1,7 +1,6 @@
#include <math.h> #include <math.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "pxl8_vfx.h" #include "pxl8_vfx.h"
@ -82,10 +81,10 @@ void pxl8_vfx_rotozoom(pxl8_gfx* gfx, f32 angle, f32 zoom, i32 cx, i32 cy) {
f32 cos_a = cosf(angle); f32 cos_a = cosf(angle);
f32 sin_a = sinf(angle); f32 sin_a = sinf(angle);
u8* temp_buffer = (u8*)SDL_malloc(pxl8_gfx_get_width(gfx) * pxl8_gfx_get_height(gfx)); u8* temp_buffer = (u8*)malloc(pxl8_gfx_get_width(gfx) * pxl8_gfx_get_height(gfx));
if (!temp_buffer) return; if (!temp_buffer) return;
SDL_memcpy(temp_buffer, pxl8_gfx_get_framebuffer(gfx), pxl8_gfx_get_width(gfx) * pxl8_gfx_get_height(gfx)); memcpy(temp_buffer, pxl8_gfx_get_framebuffer(gfx), pxl8_gfx_get_width(gfx) * pxl8_gfx_get_height(gfx));
for (i32 y = 0; y < pxl8_gfx_get_height(gfx); y++) { for (i32 y = 0; y < pxl8_gfx_get_height(gfx); y++) {
for (i32 x = 0; x < pxl8_gfx_get_width(gfx); x++) { for (i32 x = 0; x < pxl8_gfx_get_width(gfx); x++) {
@ -104,7 +103,7 @@ void pxl8_vfx_rotozoom(pxl8_gfx* gfx, f32 angle, f32 zoom, i32 cx, i32 cy) {
} }
} }
SDL_free(temp_buffer); free(temp_buffer);
} }
void pxl8_vfx_tunnel(pxl8_gfx* gfx, f32 time, f32 speed, f32 twist) { void pxl8_vfx_tunnel(pxl8_gfx* gfx, f32 time, f32 speed, f32 twist) {
@ -145,7 +144,7 @@ void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_
static f32* prev_height = NULL; static f32* prev_height = NULL;
if (!prev_height) { if (!prev_height) {
prev_height = (f32*)SDL_calloc(w * h, sizeof(f32)); prev_height = (f32*)calloc(w * h, sizeof(f32));
if (!prev_height) return; if (!prev_height) return;
} }
@ -169,12 +168,12 @@ void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_
} }
pxl8_particles* pxl8_particles_create(u32 max_count) { pxl8_particles* pxl8_particles_create(u32 max_count) {
pxl8_particles* particles = SDL_calloc(1, sizeof(pxl8_particles)); pxl8_particles* particles = calloc(1, sizeof(pxl8_particles));
if (!particles) return NULL; if (!particles) return NULL;
particles->particles = SDL_calloc(max_count, sizeof(pxl8_particle)); particles->particles = calloc(max_count, sizeof(pxl8_particle));
if (!particles->particles) { if (!particles->particles) {
SDL_free(particles); free(particles);
return NULL; return NULL;
} }
@ -188,8 +187,8 @@ pxl8_particles* pxl8_particles_create(u32 max_count) {
void pxl8_particles_destroy(pxl8_particles* particles) { void pxl8_particles_destroy(pxl8_particles* particles) {
if (!particles) return; if (!particles) return;
SDL_free(particles->particles); free(particles->particles);
SDL_free(particles); free(particles);
} }
void pxl8_particles_clear(pxl8_particles* particles) { void pxl8_particles_clear(pxl8_particles* particles) {

View file

@ -1,7 +1,6 @@
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <SDL3/SDL.h>
#include "pxl8_bsp.h" #include "pxl8_bsp.h"
#include "pxl8_macros.h" #include "pxl8_macros.h"
#include "pxl8_world.h" #include "pxl8_world.h"
@ -13,7 +12,7 @@ struct pxl8_world {
}; };
pxl8_world* pxl8_world_create(void) { pxl8_world* pxl8_world_create(void) {
pxl8_world* world = (pxl8_world*)SDL_calloc(1, sizeof(pxl8_world)); pxl8_world* world = (pxl8_world*)calloc(1, sizeof(pxl8_world));
if (!world) { if (!world) {
pxl8_error("Failed to allocate world"); pxl8_error("Failed to allocate world");
return NULL; return NULL;
@ -32,7 +31,7 @@ void pxl8_world_destroy(pxl8_world* world) {
pxl8_bsp_destroy(&world->bsp); pxl8_bsp_destroy(&world->bsp);
} }
SDL_free(world); free(world);
} }
pxl8_result pxl8_world_load(pxl8_world* world, const char* path) { pxl8_result pxl8_world_load(pxl8_world* world, const char* path) {