refactor: add helpers for file I/O, main script detection, safe strncpy, & make hal generic

This commit is contained in:
asrael 2025-12-06 15:04:53 -06:00
parent a33d4c0068
commit 097e3a604d
No known key found for this signature in database
GPG key ID: 2786557804DFAE24
19 changed files with 323 additions and 454 deletions

4
demo/cart.fnl Normal file
View file

@ -0,0 +1,4 @@
{:title "pxl8 demo"
:resolution "640x360"
:window-size [1280 720]
:pixel-mode "indexed"}

View file

@ -61,8 +61,6 @@
:height 64
:base_color 4})]
(pxl8.upload_atlas)
(let [result (pxl8.world_apply_textures world [
{:name "floor"
:texture_id floor-tex

View file

@ -343,7 +343,6 @@ case "$COMMAND" in
src/pxl8_io.c
src/pxl8_log.c
src/pxl8_math.c
src/pxl8_rec.c
src/pxl8_repl.c
src/pxl8_save.c
src/pxl8_script.c

View file

@ -39,7 +39,6 @@ pxl8.sprite = gfx2d.sprite
pxl8.load_palette = gfx2d.load_palette
pxl8.load_sprite = gfx2d.load_sprite
pxl8.create_texture = gfx2d.create_texture
pxl8.upload_atlas = gfx2d.upload_atlas
pxl8.gfx_color_ramp = gfx2d.color_ramp
pxl8.gfx_fade_palette = gfx2d.fade_palette
pxl8.gfx_cycle_palette = gfx2d.cycle_palette

View file

@ -70,10 +70,6 @@ function graphics.create_texture(pixels, width, height)
return result
end
function graphics.upload_atlas()
C.pxl8_gfx_upload_atlas(core.gfx)
end
function graphics.color_ramp(start, count, from_color, to_color)
C.pxl8_gfx_color_ramp(core.gfx, start, count, from_color, to_color)
end

View file

@ -9,14 +9,13 @@
#include <sys/stat.h>
#include <unistd.h>
#include "pxl8_cart.h"
#include "pxl8_game.h"
#include "pxl8_hal.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
#include "pxl8_repl.h"
#include "pxl8_script.h"
#include "pxl8_sys.h"
#include "pxl8_types.h"
struct pxl8 {
pxl8_cart* cart;
@ -65,8 +64,6 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
if (!sys || !sys->game) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_game* game = sys->game;
pxl8_pixel_mode pixel_mode = PXL8_PIXEL_INDEXED;
pxl8_resolution resolution = PXL8_RESOLUTION_640x360;
const char* script_arg = NULL;
bool bundle_mode = false;
bool pack_mode = false;
@ -116,13 +113,61 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
return result;
}
if (game->repl_mode) {
pxl8_info("starting in REPL mode with script: %s", game->script_path);
}
pxl8_info("starting up");
sys->platform_data = sys->hal->create(pixel_mode, resolution, "pxl8", 1280, 720);
game->script = pxl8_script_create(game->repl_mode);
if (!game->script) {
pxl8_error("failed to initialize scripting: %s", pxl8_script_get_last_error(game->script));
return PXL8_ERROR_INITIALIZATION_FAILED;
}
bool has_embedded = pxl8_cart_has_embedded(argv[0]);
const char* cart_path = script_arg;
char* original_cwd = getcwd(NULL, 0);
bool load_embedded = has_embedded && !script_arg;
bool load_from_path = false;
if (!load_embedded && (cart_path || !has_embedded)) {
if (!cart_path) cart_path = "demo";
struct stat st;
load_from_path = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) ||
(cart_path && strstr(cart_path, ".pxc"));
}
if (load_embedded || load_from_path) {
sys->cart = pxl8_cart_create();
pxl8_result load_result = load_embedded
? pxl8_cart_load_embedded(sys->cart, argv[0])
: pxl8_cart_load(sys->cart, cart_path);
if (!sys->cart || load_result != PXL8_OK) {
pxl8_error("failed to load cart%s%s", load_from_path ? ": " : "", load_from_path ? cart_path : "");
if (sys->cart) pxl8_cart_destroy(sys->cart);
sys->cart = NULL;
free(original_cwd);
return PXL8_ERROR_INITIALIZATION_FAILED;
}
pxl8_cart_mount(sys->cart);
pxl8_script_load_cart_manifest(game->script, sys->cart);
if (load_from_path) {
pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd);
}
pxl8_strncpy(game->script_path, "main.fnl", sizeof(game->script_path));
} else if (script_arg) {
pxl8_strncpy(game->script_path, script_arg, sizeof(game->script_path));
}
free(original_cwd);
const char* window_title = pxl8_cart_get_title(sys->cart);
if (!window_title) window_title = "pxl8";
pxl8_resolution resolution = pxl8_cart_get_resolution(sys->cart);
pxl8_pixel_mode pixel_mode = pxl8_cart_get_pixel_mode(sys->cart);
pxl8_size window_size = pxl8_cart_get_window_size(sys->cart);
pxl8_size render_size = pxl8_get_resolution_dimensions(resolution);
sys->platform_data = sys->hal->create(render_size.w, render_size.h, window_title, window_size.w, window_size.h);
if (!sys->platform_data) {
pxl8_error("failed to create platform context");
return PXL8_ERROR_INITIALIZATION_FAILED;
@ -139,58 +184,8 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
return PXL8_ERROR_INITIALIZATION_FAILED;
}
game->script = pxl8_script_create(game->repl_mode);
if (!game->script) {
pxl8_error("failed to initialize scripting: %s", pxl8_script_get_last_error(game->script));
return PXL8_ERROR_INITIALIZATION_FAILED;
}
bool has_embedded = pxl8_cart_has_embedded(argv[0]);
const char* cart_path = script_arg;
if (has_embedded && !script_arg) {
sys->cart = pxl8_cart_create();
if (!sys->cart) {
pxl8_error("failed to create cart");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
if (pxl8_cart_load_embedded(sys->cart, argv[0]) == PXL8_OK) {
pxl8_cart_mount(sys->cart);
strncpy(game->script_path, "main.fnl", sizeof(game->script_path) - 1);
game->script_path[sizeof(game->script_path) - 1] = '\0';
pxl8_info("running embedded cart");
} else {
pxl8_error("failed to load embedded cart");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
} else if (cart_path || !has_embedded) {
if (!cart_path) cart_path = "demo";
struct stat st;
bool is_cart = (stat(cart_path, &st) == 0 && S_ISDIR(st.st_mode)) ||
(cart_path && strstr(cart_path, ".pxc"));
if (is_cart) {
char* original_cwd = getcwd(NULL, 0);
sys->cart = pxl8_cart_create();
if (!sys->cart) {
pxl8_error("failed to create cart");
return PXL8_ERROR_INITIALIZATION_FAILED;
}
if (pxl8_cart_load(sys->cart, cart_path) == PXL8_OK) {
pxl8_script_set_cart_path(game->script, pxl8_cart_get_base_path(sys->cart), original_cwd);
pxl8_cart_mount(sys->cart);
strncpy(game->script_path, "main.fnl", sizeof(game->script_path) - 1);
game->script_path[sizeof(game->script_path) - 1] = '\0';
} else {
pxl8_error("failed to load cart: %s", cart_path);
return PXL8_ERROR_INITIALIZATION_FAILED;
}
free(original_cwd);
} else if (script_arg) {
strncpy(game->script_path, script_arg, sizeof(game->script_path) - 1);
game->script_path[sizeof(game->script_path) - 1] = '\0';
}
if (game->repl_mode) {
pxl8_info("starting in REPL mode with script: %s", game->script_path);
}
pxl8_script_set_gfx(game->script, game->gfx);
@ -202,7 +197,10 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) {
game->script_loaded = (result == PXL8_OK);
if (game->script_loaded && !game->repl_mode) {
pxl8_script_call_function(game->script, "init");
pxl8_result init_result = pxl8_script_call_function(game->script, "init");
if (init_result != PXL8_OK) {
pxl8_script_error("%s", pxl8_script_get_last_error(game->script));
}
}
}
@ -305,7 +303,6 @@ pxl8_result pxl8_frame(pxl8* sys) {
pxl8_gfx_set_viewport(game->gfx, pxl8_gfx_viewport(bounds, pxl8_gfx_get_width(game->gfx), pxl8_gfx_get_height(game->gfx)));
pxl8_gfx_upload_framebuffer(game->gfx);
pxl8_gfx_upload_atlas(game->gfx);
pxl8_gfx_present(game->gfx);
memset(game->input.keys_pressed, 0, sizeof(game->input.keys_pressed));

View file

@ -98,31 +98,14 @@ pxl8_result pxl8_bsp_load(const char* path, pxl8_bsp* bsp) {
memset(bsp, 0, sizeof(*bsp));
FILE* f = fopen(path, "rb");
if (!f) {
u8* file_data = NULL;
size_t file_size = 0;
pxl8_result result = pxl8_io_read_binary_file(path, &file_data, &file_size);
if (result != PXL8_OK) {
pxl8_error("Failed to load BSP file: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
return result;
}
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)) {
pxl8_error("BSP file too small: %s", path);
free(file_data);

View file

@ -45,7 +45,10 @@ struct pxl8_cart {
pxl8_cart_file* files;
u32 file_count;
char* base_path;
char* name;
char* title;
pxl8_resolution resolution;
pxl8_size window_size;
pxl8_pixel_mode pixel_mode;
bool is_folder;
bool is_mounted;
};
@ -63,19 +66,12 @@ static bool is_pxc_file(const char* path) {
return len > 4 && strcmp(path + len - 4, ".pxc") == 0;
}
static char* get_cart_name(const char* path) {
char* name = strdup(path);
size_t len = strlen(name);
if (len > 4 && strcmp(name + len - 4, ".pxc") == 0) {
name[len - 4] = '\0';
}
char* last_slash = strrchr(name, '/');
if (last_slash) {
char* result = strdup(last_slash + 1);
free(name);
return result;
}
return name;
static bool has_main_script(const char* base_path) {
char path[512];
snprintf(path, sizeof(path), "%s/main.fnl", base_path);
if (access(path, F_OK) == 0) return true;
snprintf(path, sizeof(path), "%s/main.lua", base_path);
return access(path, F_OK) == 0;
}
static void collect_files_recursive(const char* dir_path, const char* prefix,
@ -155,7 +151,13 @@ static pxl8_result load_packed_cart(pxl8_cart* cart, const u8* data, u32 size) {
}
pxl8_cart* pxl8_cart_create(void) {
return calloc(1, sizeof(pxl8_cart));
pxl8_cart* cart = calloc(1, sizeof(pxl8_cart));
if (cart) {
cart->resolution = PXL8_RESOLUTION_640x360;
cart->window_size = (pxl8_size){1280, 720};
cart->pixel_mode = PXL8_PIXEL_INDEXED;
}
return cart;
}
pxl8_cart* pxl8_cart_current(void) {
@ -171,7 +173,6 @@ void pxl8_cart_destroy(pxl8_cart* cart) {
pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
if (!cart || !path) return PXL8_ERROR_NULL_POINTER;
pxl8_cart_unload(cart);
cart->name = get_cart_name(path);
if (is_directory(path)) {
cart->base_path = realpath(path, NULL);
@ -181,17 +182,12 @@ pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
}
cart->is_folder = true;
char main_path[512];
snprintf(main_path, sizeof(main_path), "%s/main.fnl", cart->base_path);
if (access(main_path, F_OK) != 0) {
snprintf(main_path, sizeof(main_path), "%s/main.lua", cart->base_path);
if (access(main_path, F_OK) != 0) {
pxl8_error("No main.fnl or main.lua found in cart: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
if (!has_main_script(cart->base_path)) {
pxl8_error("No main.fnl or main.lua found in cart: %s", path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
pxl8_info("Loaded cart folder: %s", cart->name);
pxl8_info("Loaded cart");
return PXL8_OK;
}
@ -218,7 +214,7 @@ pxl8_result pxl8_cart_load(pxl8_cart* cart, const char* path) {
free(data);
if (result == PXL8_OK) {
pxl8_info("Loaded cart: %s", cart->name);
pxl8_info("Loaded cart");
}
return result;
}
@ -254,12 +250,11 @@ pxl8_result pxl8_cart_load_embedded(pxl8_cart* cart, const char* exe_path) {
}
fclose(file);
cart->name = get_cart_name(exe_path);
pxl8_result result = load_packed_cart(cart, data, trailer.cart_size);
free(data);
if (result == PXL8_OK) {
pxl8_info("Loaded embedded cart: %s", cart->name);
pxl8_info("Loaded embedded cart");
}
return result;
}
@ -281,10 +276,12 @@ void pxl8_cart_unload(pxl8_cart* cart) {
cart->data = NULL;
cart->data_size = 0;
if (cart->name) {
pxl8_info("Unloaded cart: %s", cart->name);
free(cart->name);
cart->name = NULL;
if (cart->title) {
pxl8_info("Unloaded cart: %s", cart->title);
free(cart->title);
cart->title = NULL;
} else {
pxl8_info("Unloaded cart");
}
free(cart->base_path);
@ -312,7 +309,11 @@ pxl8_result pxl8_cart_mount(pxl8_cart* cart) {
cart->is_mounted = true;
pxl8_current_cart = cart;
pxl8_info("Mounted cart: %s", cart->name);
if (cart->title) {
pxl8_info("Mounted cart: %s", cart->title);
} else {
pxl8_info("Mounted cart");
}
return PXL8_OK;
}
@ -335,8 +336,38 @@ const char* pxl8_cart_get_base_path(const pxl8_cart* cart) {
return cart ? cart->base_path : NULL;
}
const char* pxl8_cart_get_name(const pxl8_cart* cart) {
return cart ? cart->name : NULL;
const char* pxl8_cart_get_title(const pxl8_cart* cart) {
return cart ? cart->title : NULL;
}
void pxl8_cart_set_title(pxl8_cart* cart, const char* title) {
if (!cart || !title) return;
free(cart->title);
cart->title = strdup(title);
}
pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart) {
return cart ? cart->resolution : PXL8_RESOLUTION_640x360;
}
void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution) {
if (cart) cart->resolution = resolution;
}
pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart) {
return cart ? cart->window_size : (pxl8_size){1280, 720};
}
void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size) {
if (cart) cart->window_size = size;
}
pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart) {
return cart ? cart->pixel_mode : PXL8_PIXEL_INDEXED;
}
void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode) {
if (cart) cart->pixel_mode = mode;
}
bool pxl8_cart_is_packed(const pxl8_cart* cart) {
@ -429,14 +460,9 @@ pxl8_result pxl8_cart_pack(const char* folder_path, const char* output_path) {
return PXL8_ERROR_FILE_NOT_FOUND;
}
char main_path[512];
snprintf(main_path, sizeof(main_path), "%s/main.fnl", folder_path);
if (access(main_path, F_OK) != 0) {
snprintf(main_path, sizeof(main_path), "%s/main.lua", folder_path);
if (access(main_path, F_OK) != 0) {
pxl8_error("No main.fnl or main.lua found in cart: %s", folder_path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
if (!has_main_script(folder_path)) {
pxl8_error("No main.fnl or main.lua found in cart: %s", folder_path);
return PXL8_ERROR_FILE_NOT_FOUND;
}
char** paths = NULL;

View file

@ -22,7 +22,14 @@ pxl8_result pxl8_cart_mount(pxl8_cart* cart);
void pxl8_cart_unmount(pxl8_cart* cart);
const char* pxl8_cart_get_base_path(const pxl8_cart* cart);
const char* pxl8_cart_get_name(const pxl8_cart* cart);
const char* pxl8_cart_get_title(const pxl8_cart* cart);
pxl8_resolution pxl8_cart_get_resolution(const pxl8_cart* cart);
pxl8_size pxl8_cart_get_window_size(const pxl8_cart* cart);
pxl8_pixel_mode pxl8_cart_get_pixel_mode(const pxl8_cart* cart);
void pxl8_cart_set_title(pxl8_cart* cart, const char* title);
void pxl8_cart_set_resolution(pxl8_cart* cart, pxl8_resolution resolution);
void pxl8_cart_set_window_size(pxl8_cart* cart, pxl8_size size);
void pxl8_cart_set_pixel_mode(pxl8_cart* cart, pxl8_pixel_mode mode);
bool pxl8_cart_is_packed(const pxl8_cart* cart);
bool pxl8_cart_has_embedded(const char* exe_path);

View file

@ -10,6 +10,7 @@
#include "pxl8_font.h"
#include "pxl8_hal.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
#include "pxl8_math.h"
#include "pxl8_sys.h"
#include "pxl8_types.h"
@ -273,8 +274,7 @@ pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) {
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';
pxl8_strncpy(entry->path, path, sizeof(entry->path));
return sprite_id;
}
@ -327,25 +327,17 @@ pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx) {
return PXL8_OK;
}
void pxl8_gfx_upload_atlas(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->atlas) return;
if (gfx->hal && gfx->hal->upload_atlas) {
gfx->hal->upload_atlas(gfx->platform_data, gfx->atlas, gfx->palette, gfx->pixel_mode);
pxl8_atlas_mark_clean(gfx->atlas);
}
}
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->hal) return;
gfx->hal->upload_framebuffer(
u32 bpp = (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) ? 2 : 1;
gfx->hal->upload_texture(
gfx->platform_data,
gfx->framebuffer,
gfx->framebuffer_width,
gfx->framebuffer_height,
gfx->palette,
gfx->pixel_mode
bpp
);
}

View file

@ -4,7 +4,6 @@
#include "pxl8_math.h"
#include "pxl8_types.h"
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_gfx pxl8_gfx;
typedef struct pxl8_vertex {
@ -42,7 +41,6 @@ pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path);
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path);
void pxl8_gfx_present(pxl8_gfx* gfx);
void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom);
void pxl8_gfx_upload_atlas(pxl8_gfx* gfx);
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx);
pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height);

View file

@ -2,23 +2,15 @@
#include "pxl8_types.h"
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_game pxl8_game;
typedef struct pxl8_hal {
void* (*create)(pxl8_pixel_mode mode, pxl8_resolution res,
void* (*create)(i32 render_w, i32 render_h,
const char* title, i32 win_w, i32 win_h);
void (*destroy)(void* platform_data);
u64 (*get_ticks)(void);
void (*center_cursor)(void* platform_data);
void (*present)(void* platform_data);
void (*set_cursor)(void* platform_data, pxl8_cursor cursor);
void (*set_cursor)(void* platform_data, u32 cursor);
void (*set_relative_mouse_mode)(void* platform_data, bool enabled);
void (*upload_atlas)(void* platform_data, const pxl8_atlas* atlas,
const u32* palette, pxl8_pixel_mode mode);
void (*upload_framebuffer)(void* platform_data, const u8* fb,
i32 w, i32 h, const u32* palette,
pxl8_pixel_mode mode);
void (*upload_texture)(void* platform_data, const u8* pixels, u32 w, u32 h,
const u32* palette, u32 bpp);
} pxl8_hal;

View file

@ -28,3 +28,4 @@ void pxl8_log_write_error(const char* file, int line, const char* fmt, ...);
#define pxl8_info(...) pxl8_log_write_info(__VA_ARGS__)
#define pxl8_warn(...) pxl8_log_write_warn(__FILE__, __LINE__, __VA_ARGS__)
#define pxl8_error(...) pxl8_log_write_error(__FILE__, __LINE__, __VA_ARGS__)
#define pxl8_script_error(...) pxl8_log_write_error(NULL, 0, __VA_ARGS__)

View file

@ -1,5 +1,7 @@
#pragma once
#include <string.h>
#ifndef pxl8_min
#define pxl8_min(a, b) ((a) < (b) ? (a) : (b))
#endif
@ -7,3 +9,10 @@
#ifndef pxl8_max
#define pxl8_max(a, b) ((a) > (b) ? (a) : (b))
#endif
#ifndef pxl8_strncpy
#define pxl8_strncpy(dst, src, size) do { \
strncpy((dst), (src), (size) - 1); \
(dst)[(size) - 1] = '\0'; \
} while (0)
#endif

View file

@ -1,137 +0,0 @@
#include "pxl8_rec.h"
#include "pxl8_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
struct pxl8_recorder {
i32 height;
i32 width;
pxl8_recorder_format format;
i32 framerate;
char output_path[512];
FILE* ffmpeg_pipe;
u32 frame_count;
bool recording;
};
static void generate_default_output_path(pxl8_recorder* rec) {
time_t now = time(NULL);
struct tm* t = localtime(&now);
const char* ext = (rec->format == PXL8_RECORDER_GIF) ? "gif" : "mp4";
snprintf(rec->output_path, sizeof(rec->output_path),
"recording_%04d%02d%02d_%02d%02d%02d.%s",
t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, ext);
}
pxl8_recorder* pxl8_recorder_create(i32 width, i32 height) {
pxl8_recorder* rec = (pxl8_recorder*)calloc(1, sizeof(pxl8_recorder));
if (!rec) return NULL;
rec->height = height;
rec->width = width;
rec->format = PXL8_RECORDER_MP4;
rec->framerate = 60;
rec->output_path[0] = '\0';
rec->ffmpeg_pipe = NULL;
rec->frame_count = 0;
rec->recording = false;
return rec;
}
void pxl8_recorder_destroy(pxl8_recorder* rec) {
if (!rec) return;
if (rec->recording) pxl8_recorder_stop(rec);
free(rec);
}
void pxl8_recorder_set_format(pxl8_recorder* rec, pxl8_recorder_format format) {
if (rec && !rec->recording) rec->format = format;
}
void pxl8_recorder_set_framerate(pxl8_recorder* rec, i32 fps) {
if (rec && !rec->recording) rec->framerate = fps > 0 ? fps : 60;
}
void pxl8_recorder_set_output_path(pxl8_recorder* rec, const char* path) {
if (rec && !rec->recording && path) strncpy(rec->output_path, path, sizeof(rec->output_path) - 1);
}
void pxl8_recorder_start(pxl8_recorder* rec) {
if (!rec || rec->recording) return;
if (rec->output_path[0] == '\0') generate_default_output_path(rec);
char cmd[1024];
if (rec->format == PXL8_RECORDER_GIF) {
snprintf(cmd, sizeof(cmd),
"ffmpeg -y -f rawvideo -pix_fmt rgba -s %dx%d -r %d -i - "
"-vf \"split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse\" "
"\"%s\" 2>/dev/null",
rec->width, rec->height, rec->framerate, rec->output_path);
} else {
snprintf(cmd, sizeof(cmd),
"ffmpeg -y -f rawvideo -pix_fmt rgba -s %dx%d -r %d -i - "
"-c:v libx264 -pix_fmt yuv420p -crf 18 -preset fast "
"\"%s\" 2>/dev/null",
rec->width, rec->height, rec->framerate, rec->output_path);
}
rec->ffmpeg_pipe = popen(cmd, "w");
if (!rec->ffmpeg_pipe) {
pxl8_error("Failed to start ffmpeg. Is ffmpeg installed?");
return;
}
rec->frame_count = 0;
rec->recording = true;
pxl8_info("Recording started: %s (%dx%d @ %dfps)",
rec->output_path, rec->width, rec->height, rec->framerate);
}
void pxl8_recorder_stop(pxl8_recorder* rec) {
if (!rec || !rec->recording) return;
rec->recording = false;
if (rec->ffmpeg_pipe) {
pclose(rec->ffmpeg_pipe);
rec->ffmpeg_pipe = NULL;
}
pxl8_info("Recording stopped: %u frames -> %s", rec->frame_count, rec->output_path);
}
bool pxl8_recorder_is_recording(pxl8_recorder* rec) {
return rec && rec->recording;
}
void pxl8_recorder_capture_frame(pxl8_recorder* rec, const u32* rgba_pixels) {
if (!rec || !rec->recording || !rgba_pixels || !rec->ffmpeg_pipe) return;
size_t written = fwrite(rgba_pixels, 4, rec->width * rec->height, rec->ffmpeg_pipe);
if (written == (size_t)(rec->width * rec->height)) {
rec->frame_count++;
} else {
pxl8_error("Failed to write frame %u", rec->frame_count);
}
}
u32 pxl8_recorder_get_frame_count(pxl8_recorder* rec) {
return rec ? rec->frame_count : 0;
}
const char* pxl8_recorder_get_output_path(pxl8_recorder* rec) {
return rec ? rec->output_path : NULL;
}

View file

@ -1,31 +0,0 @@
#pragma once
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum pxl8_recorder_format {
PXL8_RECORDER_GIF,
PXL8_RECORDER_MP4
} pxl8_recorder_format;
typedef struct pxl8_recorder pxl8_recorder;
pxl8_recorder* pxl8_recorder_create(i32 width, i32 height);
void pxl8_recorder_destroy(pxl8_recorder* rec);
void pxl8_recorder_capture_frame(pxl8_recorder* rec, const u32* rgba_pixels);
u32 pxl8_recorder_get_frame_count(pxl8_recorder* rec);
const char* pxl8_recorder_get_output_path(pxl8_recorder* rec);
bool pxl8_recorder_is_recording(pxl8_recorder* rec);
void pxl8_recorder_set_format(pxl8_recorder* rec, pxl8_recorder_format format);
void pxl8_recorder_set_framerate(pxl8_recorder* rec, i32 fps);
void pxl8_recorder_set_output_path(pxl8_recorder* rec, const char* path);
void pxl8_recorder_start(pxl8_recorder* rec);
void pxl8_recorder_stop(pxl8_recorder* rec);
#ifdef __cplusplus
}
#endif

View file

@ -14,8 +14,9 @@
#include "pxl8_cart.h"
#include "pxl8_embed.h"
#include "pxl8_log.h"
#include "pxl8_gui.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
struct pxl8_script {
lua_State* L;
@ -193,7 +194,6 @@ static const char* pxl8_ffi_cdefs =
"i32 pxl8_gfx_load_palette(pxl8_gfx* ctx, const char* filepath);\n"
"i32 pxl8_gfx_load_sprite(pxl8_gfx* ctx, const char* filepath, u32* sprite_id);\n"
"i32 pxl8_gfx_create_texture(pxl8_gfx* ctx, const u8* pixels, u32 width, u32 height);\n"
"void pxl8_gfx_upload_atlas(pxl8_gfx* ctx);\n"
"typedef struct pxl8_input_state pxl8_input_state;\n"
"bool pxl8_key_down(const pxl8_input_state* input, const char* key_name);\n"
"bool pxl8_key_pressed(const pxl8_input_state* input, const char* key_name);\n"
@ -422,17 +422,52 @@ static const char* pxl8_ffi_cdefs =
void pxl8_lua_log(int level, const char* file, int line, const char* msg) {
if (file && (file[0] == '?' || file[0] == '\0')) file = NULL;
switch (level) {
case 0: pxl8_log_write_info("%s", msg); break;
case 1: pxl8_log_write_warn(file, line, "%s", msg); break;
case 2: pxl8_log_write_error(file, line, "%s", msg); break;
case 3: pxl8_log_write_debug(file, line, "%s", msg); break;
case 4: pxl8_log_write_trace(file, line, "%s", msg); break;
case PXL8_LOG_LEVEL_TRACE: pxl8_log_write_trace(file, line, "%s", msg); break;
case PXL8_LOG_LEVEL_DEBUG: pxl8_log_write_debug(file, line, "%s", msg); break;
case PXL8_LOG_LEVEL_INFO: pxl8_log_write_info("%s", msg); break;
case PXL8_LOG_LEVEL_WARN: pxl8_log_write_warn(file, line, "%s", msg); break;
case PXL8_LOG_LEVEL_ERROR: pxl8_log_write_error(file, line, "%s", msg); break;
}
}
static void pxl8_script_set_error(pxl8_script* script, const char* error) {
if (!script) return;
snprintf(script->last_error, sizeof(script->last_error), "%s", error ? error : "Unknown error");
if (!error) {
snprintf(script->last_error, sizeof(script->last_error), "Unknown error");
return;
}
const char* src = error;
char* dst = script->last_error;
char* end = dst + sizeof(script->last_error) - 1;
while (*src && dst < end) {
if (strncmp(src, "[string \"", 9) == 0) {
src += 9;
const char* mod_start = src;
while (*src && !(*src == '"' && *(src+1) == ']')) src++;
size_t mod_len = src - mod_start;
if (mod_len > 4 && strncmp(mod_start, "pxl8", 4) == 0) {
const char* prefix = "src/lua/";
while (*prefix && dst < end) *dst++ = *prefix++;
for (size_t i = 0; i < mod_len && dst < end; i++) {
*dst++ = (mod_start[i] == '.') ? '/' : mod_start[i];
}
const char* suffix = ".lua";
while (*suffix && dst < end) *dst++ = *suffix++;
} else {
for (size_t i = 0; i < mod_len && dst < end; i++) {
*dst++ = mod_start[i];
}
}
if (*src == '"' && *(src+1) == ']') src += 2;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
}
const char* pxl8_script_get_last_error(pxl8_script* script) {
@ -610,8 +645,7 @@ void pxl8_script_set_sys(pxl8_script* script, void* sys) {
static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char* out_basename, size_t basename_size) {
char filename_copy[PATH_MAX];
strncpy(filename_copy, filename, sizeof(filename_copy) - 1);
filename_copy[sizeof(filename_copy) - 1] = '\0';
pxl8_strncpy(filename_copy, filename, sizeof(filename_copy));
char* last_slash = strrchr(filename_copy, '/');
@ -933,8 +967,7 @@ static time_t get_latest_script_mod_time(const char* dir_path) {
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
if (!script || !path) return PXL8_ERROR_NULL_POINTER;
strncpy(script->main_path, path, sizeof(script->main_path) - 1);
script->main_path[sizeof(script->main_path) - 1] = '\0';
pxl8_strncpy(script->main_path, path, sizeof(script->main_path));
char* last_slash = strrchr(script->main_path, '/');
if (last_slash) {
@ -1006,3 +1039,90 @@ bool pxl8_script_is_incomplete_input(pxl8_script* script) {
if (!script) return false;
return pxl8_script_is_incomplete_error(script->last_error);
}
static pxl8_resolution parse_resolution(const char* str) {
if (strcmp(str, "240x160") == 0) return PXL8_RESOLUTION_240x160;
if (strcmp(str, "320x180") == 0) return PXL8_RESOLUTION_320x180;
if (strcmp(str, "320x240") == 0) return PXL8_RESOLUTION_320x240;
if (strcmp(str, "640x360") == 0) return PXL8_RESOLUTION_640x360;
if (strcmp(str, "640x480") == 0) return PXL8_RESOLUTION_640x480;
if (strcmp(str, "800x600") == 0) return PXL8_RESOLUTION_800x600;
if (strcmp(str, "960x540") == 0) return PXL8_RESOLUTION_960x540;
return PXL8_RESOLUTION_640x360;
}
static pxl8_pixel_mode parse_pixel_mode(const char* str) {
if (strcmp(str, "indexed") == 0) return PXL8_PIXEL_INDEXED;
if (strcmp(str, "hicolor") == 0) return PXL8_PIXEL_HICOLOR;
return PXL8_PIXEL_INDEXED;
}
pxl8_result pxl8_script_load_cart_manifest(pxl8_script* script, pxl8_cart* cart) {
if (!script || !script->L || !cart) return PXL8_ERROR_NULL_POINTER;
if (!pxl8_cart_file_exists(cart, "cart.fnl")) {
return PXL8_OK;
}
u8* data = NULL;
u32 size = 0;
if (pxl8_cart_read_file(cart, "cart.fnl", &data, &size) != PXL8_OK) {
return PXL8_OK;
}
lua_getglobal(script->L, "fennel");
if (lua_isnil(script->L, -1)) {
lua_pop(script->L, 1);
pxl8_cart_free_file(data);
return PXL8_OK;
}
lua_getfield(script->L, -1, "eval");
lua_pushlstring(script->L, (const char*)data, size);
lua_createtable(script->L, 0, 1);
lua_pushstring(script->L, "cart.fnl");
lua_setfield(script->L, -2, "filename");
pxl8_cart_free_file(data);
if (lua_pcall(script->L, 2, 1, 0) != 0) {
pxl8_warn("Failed to load cart.fnl: %s", lua_tostring(script->L, -1));
lua_pop(script->L, 2);
return PXL8_OK;
}
if (lua_istable(script->L, -1)) {
lua_getfield(script->L, -1, "title");
if (lua_isstring(script->L, -1)) {
pxl8_cart_set_title(cart, lua_tostring(script->L, -1));
}
lua_pop(script->L, 1);
lua_getfield(script->L, -1, "resolution");
if (lua_isstring(script->L, -1)) {
pxl8_cart_set_resolution(cart, parse_resolution(lua_tostring(script->L, -1)));
}
lua_pop(script->L, 1);
lua_getfield(script->L, -1, "pixel-mode");
if (lua_isstring(script->L, -1)) {
pxl8_cart_set_pixel_mode(cart, parse_pixel_mode(lua_tostring(script->L, -1)));
}
lua_pop(script->L, 1);
lua_getfield(script->L, -1, "window-size");
if (lua_istable(script->L, -1)) {
lua_rawgeti(script->L, -1, 1);
lua_rawgeti(script->L, -2, 2);
if (lua_isnumber(script->L, -2) && lua_isnumber(script->L, -1)) {
pxl8_size size = {(i32)lua_tonumber(script->L, -2), (i32)lua_tonumber(script->L, -1)};
pxl8_cart_set_window_size(cart, size);
}
lua_pop(script->L, 2);
}
lua_pop(script->L, 1);
}
lua_pop(script->L, 2);
return PXL8_OK;
}

View file

@ -1,5 +1,6 @@
#pragma once
#include "pxl8_cart.h"
#include "pxl8_gfx.h"
#include "pxl8_types.h"
@ -25,6 +26,7 @@ pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name,
bool pxl8_script_check_reload(pxl8_script* script);
pxl8_result pxl8_script_eval(pxl8_script* script, const char* code);
pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code);
pxl8_result pxl8_script_load_cart_manifest(pxl8_script* script, pxl8_cart* cart);
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path);
pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name);
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename);

View file

@ -4,37 +4,27 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "pxl8_atlas.h"
#include "pxl8_color.h"
#include "pxl8_log.h"
#include "pxl8_rec.h"
#include "pxl8_sys.h"
typedef struct pxl8_sdl3_context {
SDL_Texture* atlas_texture;
SDL_Texture* framebuffer_texture;
SDL_Texture* framebuffer;
SDL_Renderer* renderer;
SDL_Window* window;
u32* rgba_buffer;
size_t rgba_buffer_size;
u32 atlas_width;
u32 atlas_height;
} pxl8_sdl3_context;
static void* sdl3_create(pxl8_pixel_mode mode, pxl8_resolution resolution,
static void* sdl3_create(i32 render_w, i32 render_h,
const char* title, i32 win_w, i32 win_h) {
(void)mode;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)SDL_calloc(1, sizeof(pxl8_sdl3_context));
if (!ctx) {
pxl8_error("Failed to allocate SDL3 context");
return NULL;
}
pxl8_size fb_size = pxl8_get_resolution_dimensions(resolution);
ctx->window = SDL_CreateWindow(title, win_w, win_h, SDL_WINDOW_RESIZABLE);
if (!ctx->window) {
pxl8_error("Failed to create window: %s", SDL_GetError());
@ -54,11 +44,11 @@ static void* sdl3_create(pxl8_pixel_mode mode, pxl8_resolution resolution,
pxl8_error("Failed to set vsync: %s", SDL_GetError());
}
ctx->framebuffer_texture = SDL_CreateTexture(ctx->renderer,
ctx->framebuffer = SDL_CreateTexture(ctx->renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
fb_size.w, fb_size.h);
if (!ctx->framebuffer_texture) {
render_w, render_h);
if (!ctx->framebuffer) {
pxl8_error("Failed to create framebuffer texture: %s", SDL_GetError());
SDL_DestroyRenderer(ctx->renderer);
SDL_DestroyWindow(ctx->window);
@ -66,7 +56,7 @@ static void* sdl3_create(pxl8_pixel_mode mode, pxl8_resolution resolution,
return NULL;
}
SDL_SetTextureScaleMode(ctx->framebuffer_texture, SDL_SCALEMODE_NEAREST);
SDL_SetTextureScaleMode(ctx->framebuffer, SDL_SCALEMODE_NEAREST);
ctx->rgba_buffer = NULL;
ctx->rgba_buffer_size = 0;
@ -79,21 +69,10 @@ static void sdl3_destroy(void* platform_data) {
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);
}
if (ctx->rgba_buffer) SDL_free(ctx->rgba_buffer);
if (ctx->framebuffer) SDL_DestroyTexture(ctx->framebuffer);
if (ctx->renderer) SDL_DestroyRenderer(ctx->renderer);
if (ctx->window) SDL_DestroyWindow(ctx->window);
SDL_free(ctx);
}
@ -111,17 +90,16 @@ static void sdl3_present(void* 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);
if (ctx->framebuffer) {
SDL_RenderTexture(ctx->renderer, ctx->framebuffer, 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_pixel_mode mode) {
if (!platform_data || !fb) return;
static void sdl3_upload_texture(void* platform_data, const u8* pixels, u32 w, u32 h,
const u32* palette, u32 bpp) {
if (!platform_data || !pixels) return;
pxl8_sdl3_context* ctx = (pxl8_sdl3_context*)platform_data;
size_t needed_size = w * h;
@ -133,81 +111,18 @@ static void sdl3_upload_framebuffer(void* platform_data, const u8* fb,
ctx->rgba_buffer_size = needed_size;
}
if (mode == PXL8_PIXEL_HICOLOR) {
const u16* fb16 = (const u16*)fb;
for (i32 i = 0; i < w * h; i++) {
ctx->rgba_buffer[i] = pxl8_rgb565_to_rgba32(fb16[i]);
if (bpp == 2) {
const u16* pixels16 = (const u16*)pixels;
for (u32 i = 0; i < w * h; i++) {
ctx->rgba_buffer[i] = pxl8_rgb565_to_rgba32(pixels16[i]);
}
} else {
for (i32 i = 0; i < w * h; i++) {
u8 index = fb[i];
ctx->rgba_buffer[i] = palette[index];
for (u32 i = 0; i < w * h; i++) {
ctx->rgba_buffer[i] = palette[pixels[i]];
}
}
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_pixel_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;
}
size_t needed_size = atlas_w * atlas_h;
if (ctx->rgba_buffer_size < needed_size) {
u32* new_buffer = (u32*)SDL_realloc(ctx->rgba_buffer, needed_size * 4);
if (!new_buffer) return;
ctx->rgba_buffer = new_buffer;
ctx->rgba_buffer_size = needed_size;
}
if (mode == PXL8_PIXEL_HICOLOR) {
const u16* atlas16 = (const u16*)atlas_pixels;
for (u32 i = 0; i < atlas_w * atlas_h; i++) {
u16 pixel = atlas16[i];
ctx->rgba_buffer[i] = (pixel != 0) ? pxl8_rgb565_to_rgba32(pixel) : 0x00000000;
}
} else {
for (u32 i = 0; i < atlas_w * atlas_h; i++) {
u8 index = atlas_pixels[i];
ctx->rgba_buffer[i] = palette[index];
}
}
SDL_UpdateTexture(ctx->atlas_texture, NULL, ctx->rgba_buffer, atlas_w * 4);
SDL_UpdateTexture(ctx->framebuffer, NULL, ctx->rgba_buffer, w * 4);
}
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
@ -371,7 +286,7 @@ static void sdl3_set_relative_mouse_mode(void* platform_data, bool enabled) {
}
}
static void sdl3_set_cursor(void* platform_data, pxl8_cursor cursor) {
static void sdl3_set_cursor(void* platform_data, u32 cursor) {
if (!platform_data) return;
SDL_SystemCursor sdl_cursor;
@ -410,7 +325,6 @@ const pxl8_hal pxl8_hal_sdl3 = {
.center_cursor = sdl3_center_cursor,
.present = sdl3_present,
.set_cursor = sdl3_set_cursor,
.upload_atlas = sdl3_upload_atlas,
.upload_framebuffer = sdl3_upload_framebuffer,
.upload_texture = sdl3_upload_texture,
.set_relative_mouse_mode = sdl3_set_relative_mouse_mode,
};