diff --git a/demo/mod/music.fnl b/demo/mod/music.fnl index 2cb1e1e..564c4de 100644 --- a/demo/mod/music.fnl +++ b/demo/mod/music.fnl @@ -7,15 +7,32 @@ (var bass-params nil) (var playing false) -(local melody [60 64 67 72 67 64 60 64 67 72 76 72 67 64 62 66 69 74 69 66 62 66 69 74 78 74 69 66]) -(local bass [36 50 40 36 38 38 50 38]) -(local step-duration 0.15) +(local bpm 120) +(local beat (/ 60 bpm)) +(local whole (* 4 beat)) +(local half (* 2 beat)) +(local quarter beat) +(local eighth (/ beat 2)) +(local sixteenth (/ beat 4)) + +(local melody [[60 eighth] [64 eighth] [67 eighth] [72 eighth] + [67 eighth] [64 eighth] [60 eighth] [64 eighth] + [67 eighth] [72 eighth] [76 eighth] [72 eighth] + [67 eighth] [64 eighth] [62 eighth] [66 eighth] + [69 eighth] [74 eighth] [69 eighth] [66 eighth] + [62 eighth] [66 eighth] [69 eighth] [74 eighth] + [78 eighth] [74 eighth] [69 eighth] [66 eighth]]) + +; (local bass [[36 half] [40 half] [36 half] [38 half] +; [38 half] [36 half] [40 half] [38 half]]) + +(local step-duration eighth) (fn init [] (set ctx (pxl8.sfx_context_create)) - (local delay (pxl8.sfx_delay_create {:time_l 350 :time_r 500 :feedback 0.4 :mix 0.25})) - (local reverb (pxl8.sfx_reverb_create {:room 0.5 :damping 0.5 :mix 0.3})) + (local delay (pxl8.sfx_delay_create {:time_l 350 :time_r 500 :feedback 0.6 :mix 0.2})) + (local reverb (pxl8.sfx_reverb_create {:room 0.25 :damping 0.5 :mix 0.5})) (local compressor (pxl8.sfx_compressor_create {:threshold -12 :ratio 4 :attack 10 :release 100})) (pxl8.sfx_context_append_node ctx delay) @@ -26,19 +43,20 @@ (set params (pxl8.sfx_voice_params {:waveform pxl8.SFX_WAVE_TRIANGLE - :attack 0.005 :decay 0.08 :sustain 0.0 :release 0.05 + :attack 0.01 :decay 0.25 :sustain 0.0 :release 0.01 :filter_type pxl8.SFX_FILTER_LOWPASS - :filter_cutoff 2500 :filter_resonance 0.15 - :filter_attack 0.005 :filter_decay 0.1 :filter_sustain 0.1 :filter_release 0.05 + :filter_cutoff 2500 :filter_resonance 0.05 + :filter_attack 0.05 :filter_decay 0.1 :filter_sustain 0.2 :filter_release 0.05 :filter_env_depth 1500 - :fx_send 0.3})) + :fx_send 0.7}))) - (set bass-params (pxl8.sfx_voice_params - {:waveform pxl8.SFX_WAVE_SQUARE - :attack 0.005 :decay 0.1 :sustain 0.0 :release 0.05 - :filter_type pxl8.SFX_FILTER_LOWPASS - :filter_cutoff 600 :filter_resonance 0.2 - :fx_send 0.2}))) + ; (set bass-params (pxl8.sfx_voice_params + ; {:waveform pxl8.SFX_WAVE_SAW + ; :attack 0.02 :decay 0.3 :sustain 0.6 :release 0.4 + ; :filter_type pxl8.SFX_FILTER_LOWPASS + ; :filter_attack 0.02 :filter_decay 0.3 :filter_sustain 0.6 :filter_release 0.4 + ; :filter_cutoff 90 :filter_resonance 0.25 + ; :fx_send 0.2}))) (fn start [] (set playing true) @@ -54,11 +72,16 @@ (set time (+ time dt)) (when (>= time step-duration) (set time (- time step-duration)) - (local note (. melody (+ 1 (% step (length melody))))) - (pxl8.sfx_play_note ctx note params 0.4) - (when (= (% step 4) 0) - (local bass-note (. bass (+ 1 (% (math.floor (/ step 4)) (length bass))))) - (pxl8.sfx_play_note ctx bass-note bass-params 0.35)) + (local melody-entry (. melody (+ 1 (% step (length melody))))) + (local note (. melody-entry 1)) + (local dur (. melody-entry 2)) + (pxl8.sfx_play_note ctx note params 0.4 dur) + ; (when (= (% step 4) 0) + ; (local bass-idx (+ 1 (% (math.floor (/ step 4)) (length bass)))) + ; (local bass-entry (. bass bass-idx)) + ; (local bass-note (. bass-entry 1)) + ; (local bass-dur (. bass-entry 2)) + ; (pxl8.sfx_play_note ctx bass-note bass-params 0.35 bass-dur)) (set step (+ step 1))))) {:init init diff --git a/pxl8.sh b/pxl8.sh index b3a6d3e..41a46b0 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -344,6 +344,8 @@ case "$COMMAND" in src/pxl8_log.c src/pxl8_math.c src/pxl8_repl.c + src/pxl8_replay.c + src/pxl8_rng.c src/pxl8_save.c src/pxl8_script.c src/pxl8_sdl3.c @@ -380,9 +382,23 @@ case "$COMMAND" in obj_file="$OBJECT_DIR/$obj_name" OBJECTS="$OBJECTS $obj_file" + NEEDS_REBUILD=false if [[ "$src_file" -nt "$obj_file" ]] || \ [[ "src/pxl8_types.h" -nt "$obj_file" ]] || \ [[ "src/pxl8_macros.h" -nt "$obj_file" ]]; then + NEEDS_REBUILD=true + fi + + if [[ "$src_file" == "src/pxl8_script.c" ]]; then + for lua_file in src/lua/*.lua src/lua/pxl8/*.lua lib/fennel/fennel.lua; do + if [[ -f "$lua_file" ]] && [[ "$lua_file" -nt "$obj_file" ]]; then + NEEDS_REBUILD=true + break + fi + done + fi + + if [[ "$NEEDS_REBUILD" == true ]]; then NEED_LINK=true compile_source_file "$src_file" "$obj_file" "$COMPILE_FLAGS" SOURCES_COMPILED="yes" diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index 8963408..1aafe31 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -14,7 +14,7 @@ local transition = require("pxl8.transition") local anim = require("pxl8.anim") local sfx = require("pxl8.sfx") -core.init(_pxl8_gfx, _pxl8_input, _pxl8_sfx_mixer, _pxl8_sys) +core.init(_pxl8_gfx, _pxl8_input, _pxl8_rng, _pxl8_sfx_mixer, _pxl8_sys) local pxl8 = {} @@ -29,6 +29,11 @@ pxl8.debug = core.debug pxl8.trace = core.trace pxl8.quit = core.quit +pxl8.rng_seed = core.rng_seed +pxl8.rng_next = core.rng_next +pxl8.rng_f32 = core.rng_f32 +pxl8.rng_range = core.rng_range + pxl8.clear = gfx2d.clear pxl8.pixel = gfx2d.pixel pxl8.line = gfx2d.line diff --git a/src/lua/pxl8/core.lua b/src/lua/pxl8/core.lua index 6a6f6b1..680a03c 100644 --- a/src/lua/pxl8/core.lua +++ b/src/lua/pxl8/core.lua @@ -3,9 +3,10 @@ local C = ffi.C local core = {} -function core.init(gfx, input, sfx_mixer, sys) +function core.init(gfx, input, rng, sfx_mixer, sys) core.gfx = gfx core.input = input + core.rng = rng core.sfx_mixer = sfx_mixer core.sys = sys end @@ -72,4 +73,20 @@ function core.quit() C.pxl8_set_running(core.sys, false) end +function core.rng_seed(seed) + C.pxl8_rng_seed(core.rng, seed) +end + +function core.rng_next() + return C.pxl8_rng_next(core.rng) +end + +function core.rng_f32() + return C.pxl8_rng_f32(core.rng) +end + +function core.rng_range(min, max) + return C.pxl8_rng_range(core.rng, min, max) +end + return core diff --git a/src/lua/pxl8/particles.lua b/src/lua/pxl8/particles.lua index 6d0e5fb..4dc2e91 100644 --- a/src/lua/pxl8/particles.lua +++ b/src/lua/pxl8/particles.lua @@ -5,7 +5,7 @@ local core = require("pxl8.core") local particles = {} function particles.new(max_count) - return C.pxl8_particles_create(max_count or 1000) + return C.pxl8_particles_create(max_count or 1000, core.rng) end function particles.destroy(ps) diff --git a/src/lua/pxl8/sfx.lua b/src/lua/pxl8/sfx.lua index aeed71e..a1225e9 100644 --- a/src/lua/pxl8/sfx.lua +++ b/src/lua/pxl8/sfx.lua @@ -124,8 +124,8 @@ function sfx.note_to_freq(note) return C.pxl8_sfx_note_to_freq(note) end -function sfx.play_note(ctx, note, params, volume) - return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8) +function sfx.play_note(ctx, note, params, volume, duration) + return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8, duration or 0) end function sfx.release_voice(ctx, voice_id) diff --git a/src/pxl8.c b/src/pxl8.c index 2177e11..544ee69 100644 --- a/src/pxl8.c +++ b/src/pxl8.c @@ -15,7 +15,9 @@ #include "pxl8_log.h" #include "pxl8_macros.h" #include "pxl8_repl.h" +#include "pxl8_replay.h" #include "pxl8_script.h" +#include "pxl8_sfx.h" #include "pxl8_sys.h" struct pxl8 { @@ -27,6 +29,15 @@ struct pxl8 { void* platform_data; }; +#ifndef NDEBUG +static void pxl8_audio_event_callback(u8 event_type, u8 context_id, u8 note, f32 volume, void* userdata) { + pxl8_game* game = (pxl8_game*)userdata; + if (game && game->debug_replay) { + pxl8_replay_write_audio_event(game->debug_replay, game->frame_count, event_type, context_id, note, volume); + } +} +#endif + pxl8* pxl8_create(const pxl8_hal* hal) { pxl8* sys = (pxl8*)calloc(1, sizeof(pxl8)); if (!sys) return NULL; @@ -218,18 +229,26 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { return PXL8_ERROR_INITIALIZATION_FAILED; } - game->mixer = pxl8_sfx_mixer_create(); + game->mixer = pxl8_sfx_mixer_create(sys->hal); if (!game->mixer) { pxl8_error("failed to create audio mixer"); return PXL8_ERROR_INITIALIZATION_FAILED; } + pxl8_rng_seed(&game->rng, (u32)sys->hal->get_ticks()); + +#ifndef NDEBUG + game->debug_replay = pxl8_replay_create_buffer(60, 60); + pxl8_sfx_mixer_set_event_callback(game->mixer, pxl8_audio_event_callback, game); +#endif + if (game->repl_mode) { pxl8_info("starting in REPL mode with script: %s", game->script_path); } pxl8_script_set_gfx(game->script, game->gfx); pxl8_script_set_input(game->script, &game->input); + pxl8_script_set_rng(game->script, &game->rng); pxl8_script_set_sfx(game->script, game->mixer); pxl8_script_set_sys(game->script, sys); @@ -271,7 +290,18 @@ pxl8_result pxl8_update(pxl8* sys) { game->fps_frame_count = 0; } - pxl8_script_check_reload(game->script); +#ifndef NDEBUG + u32 rng_state_before_reload = game->rng.state; +#endif + bool reloaded = pxl8_script_check_reload(game->script); +#ifndef NDEBUG + if (reloaded) { + game->rng.state = rng_state_before_reload; + pxl8_debug("Hot-reload: restored RNG state 0x%08X", rng_state_before_reload); + } +#else + (void)reloaded; +#endif if (game->repl_mode && !game->repl_started) { if (game->script_loaded) { @@ -309,6 +339,7 @@ pxl8_result pxl8_update(pxl8* sys) { } pxl8_gfx_update(game->gfx, dt); + pxl8_sfx_mixer_process(game->mixer); if (game->script_loaded) { pxl8_script_call_function_f32(game->script, "update", dt); @@ -352,6 +383,18 @@ pxl8_result pxl8_frame(pxl8* sys) { memset(game->input.mouse_buttons_released, 0, sizeof(game->input.mouse_buttons_released)); game->frame_count++; + +#ifndef NDEBUG + if (game->debug_replay) { + if (game->frame_count % 60 == 0) { + pxl8_replay_write_keyframe(game->debug_replay, game->frame_count, game->time, &game->rng, &game->input); + } else { + pxl8_replay_write_input(game->debug_replay, game->frame_count, &game->prev_input, &game->input); + } + game->prev_input = game->input; + } +#endif + game->input.mouse_dx = 0; game->input.mouse_dy = 0; game->input.mouse_wheel_x = 0; @@ -371,6 +414,10 @@ void pxl8_quit(pxl8* sys) { pxl8_cart_unmount(sys->cart); } +#ifndef NDEBUG + pxl8_replay_destroy(game->debug_replay); +#endif + pxl8_sfx_mixer_destroy(game->mixer); pxl8_gfx_destroy(game->gfx); pxl8_script_destroy(game->script); diff --git a/src/pxl8_anim.c b/src/pxl8_anim.c index 740cbbf..ab9cd90 100644 --- a/src/pxl8_anim.c +++ b/src/pxl8_anim.c @@ -20,6 +20,15 @@ typedef struct pxl8_anim_state_machine { u16 current_state; } pxl8_anim_state_machine; +static inline pxl8_anim* pxl8_anim_get_active(pxl8_anim* anim) { + if (anim->state_machine && + anim->state_machine->current_state < anim->state_machine->state_count) { + pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim; + if (state_anim) return state_anim; + } + return anim; +} + pxl8_anim* pxl8_anim_create(const u32* frame_ids, const u16* frame_durations, u16 frame_count) { if (!frame_ids || frame_count == 0) { pxl8_error("Invalid animation parameters"); @@ -299,61 +308,26 @@ void pxl8_anim_reset(pxl8_anim* anim) { void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame) { if (!anim) return; - - if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) { - pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim; - if (state_anim) { - pxl8_anim_set_frame(state_anim, frame); - return; - } - } - - if (frame < anim->frame_count) { - anim->current_frame = frame; - anim->time_accumulator = 0.0f; + pxl8_anim* target = pxl8_anim_get_active(anim); + if (frame < target->frame_count) { + target->current_frame = frame; + target->time_accumulator = 0.0f; } } void pxl8_anim_set_loop(pxl8_anim* anim, bool loop) { if (!anim) return; - - if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) { - pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim; - if (state_anim) { - state_anim->loop = loop; - return; - } - } - - anim->loop = loop; + pxl8_anim_get_active(anim)->loop = loop; } void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse) { if (!anim) return; - - if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) { - pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim; - if (state_anim) { - state_anim->reverse = reverse; - return; - } - } - - anim->reverse = reverse; + pxl8_anim_get_active(anim)->reverse = reverse; } void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed) { if (!anim) return; - - if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) { - pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim; - if (state_anim) { - state_anim->speed = speed; - return; - } - } - - anim->speed = speed; + pxl8_anim_get_active(anim)->speed = speed; } pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name) { diff --git a/src/pxl8_blit.c b/src/pxl8_blit.c index 2bf3d94..7409066 100644 --- a/src/pxl8_blit.c +++ b/src/pxl8_blit.c @@ -17,21 +17,12 @@ void pxl8_blit_hicolor(u16* fb, u32 fb_width, const u16* sprite, u32 atlas_width col += 2; continue; } - u16 s0 = (u16)(pixels); - u16 s1 = (u16)(pixels >> 16); - u16 d0 = dest_row[col]; - u16 d1 = dest_row[col + 1]; - u16 m0 = (u16)(-(s0 != 0)); - u16 m1 = (u16)(-(s1 != 0)); - dest_row[col] = (s0 & m0) | (d0 & ~m0); - dest_row[col + 1] = (s1 & m1) | (d1 & ~m1); + dest_row[col] = pxl8_blend_hicolor((u16)(pixels), dest_row[col]); + dest_row[col + 1] = pxl8_blend_hicolor((u16)(pixels >> 16), dest_row[col + 1]); col += 2; } if (w & 1) { - u16 s = src_row[col]; - u16 d = dest_row[col]; - u16 m = (u16)(-(s != 0)); - dest_row[col] = (s & m) | (d & ~m); + dest_row[col] = pxl8_blend_hicolor(src_row[col], dest_row[col]); } } } @@ -53,29 +44,14 @@ void pxl8_blit_indexed(u8* fb, u32 fb_width, const u8* sprite, u32 atlas_width, col += 4; continue; } - u8 s0 = (u8)(pixels); - u8 s1 = (u8)(pixels >> 8); - u8 s2 = (u8)(pixels >> 16); - u8 s3 = (u8)(pixels >> 24); - u8 d0 = dest_row[col]; - u8 d1 = dest_row[col + 1]; - u8 d2 = dest_row[col + 2]; - u8 d3 = dest_row[col + 3]; - u8 m0 = (u8)(-(s0 != 0)); - u8 m1 = (u8)(-(s1 != 0)); - u8 m2 = (u8)(-(s2 != 0)); - u8 m3 = (u8)(-(s3 != 0)); - dest_row[col] = (s0 & m0) | (d0 & ~m0); - dest_row[col + 1] = (s1 & m1) | (d1 & ~m1); - dest_row[col + 2] = (s2 & m2) | (d2 & ~m2); - dest_row[col + 3] = (s3 & m3) | (d3 & ~m3); + dest_row[col] = pxl8_blend_indexed((u8)(pixels), dest_row[col]); + dest_row[col + 1] = pxl8_blend_indexed((u8)(pixels >> 8), dest_row[col + 1]); + dest_row[col + 2] = pxl8_blend_indexed((u8)(pixels >> 16), dest_row[col + 2]); + dest_row[col + 3] = pxl8_blend_indexed((u8)(pixels >> 24), dest_row[col + 3]); col += 4; } for (; col < w; col++) { - u8 s = src_row[col]; - u8 d = dest_row[col]; - u8 m = (u8)(-(s != 0)); - dest_row[col] = (s & m) | (d & ~m); + dest_row[col] = pxl8_blend_indexed(src_row[col], dest_row[col]); } } } diff --git a/src/pxl8_blit.h b/src/pxl8_blit.h index 09a940e..0ab5bd8 100644 --- a/src/pxl8_blit.h +++ b/src/pxl8_blit.h @@ -6,6 +6,16 @@ extern "C" { #endif +static inline u8 pxl8_blend_indexed(u8 src, u8 dst) { + u8 m = (u8)(-(src != 0)); + return (src & m) | (dst & ~m); +} + +static inline u16 pxl8_blend_hicolor(u16 src, u16 dst) { + u16 m = (u16)(-(src != 0)); + return (src & m) | (dst & ~m); +} + void pxl8_blit_hicolor( u16* fb, u32 fb_width, const u16* sprite, u32 atlas_width, diff --git a/src/pxl8_game.h b/src/pxl8_game.h index 61fe3cd..1dba5f4 100644 --- a/src/pxl8_game.h +++ b/src/pxl8_game.h @@ -1,15 +1,19 @@ #pragma once #include "pxl8_gfx.h" +#include "pxl8_rng.h" #include "pxl8_script.h" #include "pxl8_sfx.h" #include "pxl8_types.h" +typedef struct pxl8_replay pxl8_replay; + typedef struct pxl8_game { pxl8_gfx* gfx; pxl8_script* script; pxl8_sfx_mixer* mixer; + pxl8_rng rng; i32 frame_count; u64 last_time; f32 time; @@ -19,6 +23,11 @@ typedef struct pxl8_game { f32 fps; pxl8_input_state input; + pxl8_input_state prev_input; + +#ifndef NDEBUG + pxl8_replay* debug_replay; +#endif bool repl_mode; bool repl_started; diff --git a/src/pxl8_gen.c b/src/pxl8_gen.c index c03cc91..ba80b11 100644 --- a/src/pxl8_gen.c +++ b/src/pxl8_gen.c @@ -4,6 +4,7 @@ #include #include "pxl8_log.h" +#include "pxl8_rng.h" typedef struct room_grid { u8* cells; @@ -11,19 +12,6 @@ typedef struct room_grid { i32 height; } room_grid; -static u32 prng_state = 0; - -static void prng_seed(u32 seed) { - prng_state = seed; -} - -static u32 prng_next(void) { - prng_state ^= prng_state << 13; - prng_state ^= prng_state >> 17; - prng_state ^= prng_state << 5; - return prng_state; -} - static bool room_grid_init(room_grid* grid, i32 width, i32 height) { grid->width = width; grid->height = height; @@ -345,7 +333,8 @@ static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* param params->width, params->height, params->seed, params->min_room_size, params->max_room_size, params->num_rooms); - prng_seed(params->seed); + pxl8_rng rng; + pxl8_rng_seed(&rng, params->seed); room_grid grid; if (!room_grid_init(&grid, params->width, params->height)) { @@ -360,10 +349,10 @@ static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* param i32 max_attempts = params->num_rooms * 10; for (i32 attempt = 0; attempt < max_attempts && room_count < params->num_rooms && room_count < 256; attempt++) { - i32 w = params->min_room_size + (prng_next() % (params->max_room_size - params->min_room_size + 1)); - i32 h = params->min_room_size + (prng_next() % (params->max_room_size - params->min_room_size + 1)); - i32 x = 1 + (prng_next() % (params->width - w - 2)); - i32 y = 1 + (prng_next() % (params->height - h - 2)); + i32 w = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1)); + i32 h = params->min_room_size + (pxl8_rng_next(&rng) % (params->max_room_size - params->min_room_size + 1)); + i32 x = 1 + (pxl8_rng_next(&rng) % (params->width - w - 2)); + i32 y = 1 + (pxl8_rng_next(&rng) % (params->height - h - 2)); pxl8_bounds new_room = {x, y, w, h}; @@ -388,7 +377,7 @@ static pxl8_result procgen_rooms(pxl8_bsp* bsp, const pxl8_procgen_params* param i32 prev_cx = rooms[room_count - 1].x + rooms[room_count - 1].w / 2; i32 prev_cy = rooms[room_count - 1].y + rooms[room_count - 1].h / 2; - if (prng_next() % 2 == 0) { + if (pxl8_rng_next(&rng) % 2 == 0) { carve_corridor_h(&grid, prev_cx, new_cx, prev_cy); carve_corridor_v(&grid, prev_cy, new_cy, new_cx); } else { @@ -440,8 +429,6 @@ static u32 hash2d(i32 x, i32 y) { void pxl8_procgen_tex(u8* buffer, const pxl8_procgen_tex_params* params) { if (!buffer || !params) return; - prng_seed(params->seed); - for (i32 y = 0; y < params->height; y++) { for (i32 x = 0; x < params->width; x++) { f32 u = (f32)x / (f32)params->width; diff --git a/src/pxl8_gfx.c b/src/pxl8_gfx.c index ab7f015..5eac152 100644 --- a/src/pxl8_gfx.c +++ b/src/pxl8_gfx.c @@ -378,6 +378,19 @@ void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom) { (void)gfx; (void)left; (void)right; (void)top; (void)bottom; } +static inline void pxl8_fb_set(pxl8_gfx* gfx, i32 idx, u32 color) { + if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) + ((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color); + else + gfx->framebuffer[idx] = color & 0xFF; +} + +static inline u32 pxl8_fb_get(pxl8_gfx* gfx, i32 idx) { + return (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) + ? pxl8_rgb565_to_rgba32(((u16*)gfx->framebuffer)[idx]) + : gfx->framebuffer[idx]; +} + void pxl8_clear(pxl8_gfx* gfx, u32 color) { if (!gfx || !gfx->framebuffer) return; @@ -403,25 +416,13 @@ void pxl8_clear(pxl8_gfx* gfx, u32 color) { void pxl8_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { if (!gfx || !gfx->framebuffer) return; if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return; - - i32 idx = y * gfx->framebuffer_width + x; - if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { - ((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color); - } else { - gfx->framebuffer[idx] = color & 0xFF; - } + pxl8_fb_set(gfx, y * gfx->framebuffer_width + x, color); } u32 pxl8_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) { if (!gfx || !gfx->framebuffer) return 0; if (x < 0 || x >= gfx->framebuffer_width || y < 0 || y >= gfx->framebuffer_height) return 0; - - i32 idx = y * gfx->framebuffer_width + x; - if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { - return pxl8_rgb565_to_rgba32(((u16*)gfx->framebuffer)[idx]); - } else { - return gfx->framebuffer[idx]; - } + return pxl8_fb_get(gfx, y * gfx->framebuffer_width + x); } void pxl8_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) { @@ -460,12 +461,7 @@ void pxl8_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { } static inline void pxl8_pixel_unchecked(pxl8_gfx* gfx, i32 x, i32 y, u32 color) { - i32 idx = y * gfx->framebuffer_width + x; - if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { - ((u16*)gfx->framebuffer)[idx] = pxl8_rgba32_to_rgb565(color); - } else { - gfx->framebuffer[idx] = color & 0xFF; - } + pxl8_fb_set(gfx, y * gfx->framebuffer_width + x, color); } void pxl8_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) { @@ -648,15 +644,10 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h) { i32 dest_idx = (dest_y + py) * gfx->framebuffer_width + (dest_x + px); if (gfx->pixel_mode == PXL8_PIXEL_HICOLOR) { - u16 s = ((const u16*)atlas_pixels)[src_idx]; - u16 d = ((u16*)gfx->framebuffer)[dest_idx]; - u16 m = (u16)(-(s != 0)); - ((u16*)gfx->framebuffer)[dest_idx] = (s & m) | (d & ~m); + u16* fb16 = (u16*)gfx->framebuffer; + fb16[dest_idx] = pxl8_blend_hicolor(((const u16*)atlas_pixels)[src_idx], fb16[dest_idx]); } else { - u8 s = atlas_pixels[src_idx]; - u8 d = gfx->framebuffer[dest_idx]; - u8 m = (u8)(-(s != 0)); - gfx->framebuffer[dest_idx] = (s & m) | (d & ~m); + gfx->framebuffer[dest_idx] = pxl8_blend_indexed(atlas_pixels[src_idx], gfx->framebuffer[dest_idx]); } } } diff --git a/src/pxl8_hal.h b/src/pxl8_hal.h index 4c2b811..282cc90 100644 --- a/src/pxl8_hal.h +++ b/src/pxl8_hal.h @@ -13,4 +13,11 @@ typedef struct pxl8_hal { void (*set_relative_mouse_mode)(void* platform_data, bool enabled); void (*upload_texture)(void* platform_data, const u8* pixels, u32 w, u32 h, const u32* palette, u32 bpp); + + void* (*audio_create)(i32 sample_rate, i32 channels); + void (*audio_destroy)(void* audio_handle); + void (*audio_start)(void* audio_handle); + void (*audio_stop)(void* audio_handle); + bool (*upload_audio)(void* audio_handle, const f32* stereo_samples, i32 sample_count); + i32 (*audio_queued)(void* audio_handle); } pxl8_hal; diff --git a/src/pxl8_replay.c b/src/pxl8_replay.c new file mode 100644 index 0000000..32983fd --- /dev/null +++ b/src/pxl8_replay.c @@ -0,0 +1,619 @@ +#include "pxl8_replay.h" +#include "pxl8_log.h" +#include "pxl8_sys.h" + +#include +#include +#include + +typedef struct pxl8_replay_chunk { + u8 type; + u32 size; + u8* data; + struct pxl8_replay_chunk* next; +} pxl8_replay_chunk; + +typedef struct pxl8_keyframe_entry { + pxl8_keyframe keyframe; + pxl8_replay_chunk* input_deltas; + struct pxl8_keyframe_entry* next; + struct pxl8_keyframe_entry* prev; +} pxl8_keyframe_entry; + +struct pxl8_replay { + FILE* file; + pxl8_replay_header header; + bool recording; + bool playing; + u32 current_frame; + + pxl8_keyframe_entry* keyframes; + pxl8_keyframe_entry* current_keyframe; + u32 keyframe_count; + u32 max_keyframes; + + pxl8_replay_chunk* pending_inputs; + pxl8_replay_chunk* pending_inputs_tail; + + pxl8_replay_chunk* audio_events; + pxl8_replay_chunk* audio_events_tail; +}; + +static void pxl8_replay_chunk_free(pxl8_replay_chunk* chunk) { + while (chunk) { + pxl8_replay_chunk* next = chunk->next; + free(chunk->data); + free(chunk); + chunk = next; + } +} + +static void pxl8_replay_keyframe_entry_free(pxl8_keyframe_entry* entry) { + while (entry) { + pxl8_keyframe_entry* next = entry->next; + pxl8_replay_chunk_free(entry->input_deltas); + free(entry); + entry = next; + } +} + +pxl8_replay* pxl8_replay_create(const char* path, u32 keyframe_interval) { + FILE* f = fopen(path, "wb"); + if (!f) { + pxl8_error("Failed to create replay file: %s", path); + return NULL; + } + + pxl8_replay* r = calloc(1, sizeof(pxl8_replay)); + if (!r) { + fclose(f); + return NULL; + } + + r->file = f; + r->recording = true; + r->playing = false; + r->header.magic = PXL8_REPLAY_MAGIC; + r->header.version = PXL8_REPLAY_VERSION; + r->header.keyframe_interval = keyframe_interval; + + fwrite(&r->header, sizeof(pxl8_replay_header), 1, f); + fflush(f); + + pxl8_info("Created replay file: %s (keyframe interval: %u)", path, keyframe_interval); + return r; +} + +pxl8_replay* pxl8_replay_create_buffer(u32 keyframe_interval, u32 max_keyframes) { + pxl8_replay* r = calloc(1, sizeof(pxl8_replay)); + if (!r) return NULL; + + r->recording = true; + r->playing = false; + r->max_keyframes = max_keyframes; + r->header.magic = PXL8_REPLAY_MAGIC; + r->header.version = PXL8_REPLAY_VERSION; + r->header.keyframe_interval = keyframe_interval; + + pxl8_debug("Created replay buffer (keyframe interval: %u, max: %u)", keyframe_interval, max_keyframes); + return r; +} + +pxl8_replay* pxl8_replay_open(const char* path) { + FILE* f = fopen(path, "rb"); + if (!f) { + pxl8_error("Failed to open replay file: %s", path); + return NULL; + } + + pxl8_replay_header header; + if (fread(&header, sizeof(header), 1, f) != 1) { + pxl8_error("Failed to read replay header"); + fclose(f); + return NULL; + } + + if (header.magic != PXL8_REPLAY_MAGIC) { + pxl8_error("Invalid replay magic: 0x%08X (expected 0x%08X)", header.magic, PXL8_REPLAY_MAGIC); + fclose(f); + return NULL; + } + + if (header.version > PXL8_REPLAY_VERSION) { + pxl8_error("Unsupported replay version: %u (max supported: %u)", header.version, PXL8_REPLAY_VERSION); + fclose(f); + return NULL; + } + + pxl8_replay* r = calloc(1, sizeof(pxl8_replay)); + if (!r) { + fclose(f); + return NULL; + } + + r->file = f; + r->header = header; + r->recording = false; + r->playing = true; + + while (!feof(f)) { + u8 chunk_type; + if (fread(&chunk_type, 1, 1, f) != 1) break; + + if (chunk_type == PXL8_REPLAY_CHUNK_END) break; + + u8 size_bytes[3]; + if (fread(size_bytes, 3, 1, f) != 1) break; + u32 size = size_bytes[0] | (size_bytes[1] << 8) | (size_bytes[2] << 16); + + u8* data = malloc(size); + if (!data || fread(data, size, 1, f) != 1) { + free(data); + break; + } + + if (chunk_type == PXL8_REPLAY_CHUNK_KEYFRAME) { + pxl8_keyframe_entry* entry = calloc(1, sizeof(pxl8_keyframe_entry)); + if (entry && size >= sizeof(pxl8_keyframe)) { + memcpy(&entry->keyframe, data, sizeof(pxl8_keyframe)); + entry->prev = r->current_keyframe; + if (r->current_keyframe) { + r->current_keyframe->next = entry; + } + r->current_keyframe = entry; + if (!r->keyframes) { + r->keyframes = entry; + } + r->keyframe_count++; + } + free(data); + } else if (chunk_type == PXL8_REPLAY_CHUNK_INPUT) { + if (r->current_keyframe) { + pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk)); + if (chunk) { + chunk->type = chunk_type; + chunk->size = size; + chunk->data = data; + data = NULL; + + if (!r->current_keyframe->input_deltas) { + r->current_keyframe->input_deltas = chunk; + } else { + pxl8_replay_chunk* tail = r->current_keyframe->input_deltas; + while (tail->next) tail = tail->next; + tail->next = chunk; + } + } + } + free(data); + } else if (chunk_type == PXL8_REPLAY_CHUNK_AUDIO_EVENT) { + pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk)); + if (chunk) { + chunk->type = chunk_type; + chunk->size = size; + chunk->data = data; + data = NULL; + + if (!r->audio_events) { + r->audio_events = chunk; + r->audio_events_tail = chunk; + } else { + r->audio_events_tail->next = chunk; + r->audio_events_tail = chunk; + } + } + free(data); + } else { + free(data); + } + } + + r->current_keyframe = r->keyframes; + pxl8_info("Opened replay: %u frames, %u keyframes", r->header.total_frames, r->keyframe_count); + return r; +} + +void pxl8_replay_destroy(pxl8_replay* r) { + if (!r) return; + + if (r->file) { + if (r->recording) { + u8 end_chunk = PXL8_REPLAY_CHUNK_END; + fwrite(&end_chunk, 1, 1, r->file); + + fseek(r->file, 0, SEEK_SET); + fwrite(&r->header, sizeof(pxl8_replay_header), 1, r->file); + } + fclose(r->file); + } + + pxl8_replay_keyframe_entry_free(r->keyframes); + pxl8_replay_chunk_free(r->pending_inputs); + pxl8_replay_chunk_free(r->audio_events); + + free(r); +} + +bool pxl8_replay_is_recording(pxl8_replay* r) { + return r && r->recording; +} + +bool pxl8_replay_is_playing(pxl8_replay* r) { + return r && r->playing; +} + +u32 pxl8_replay_get_frame(pxl8_replay* r) { + return r ? r->current_frame : 0; +} + +u32 pxl8_replay_get_total_frames(pxl8_replay* r) { + return r ? r->header.total_frames : 0; +} + +static void write_chunk(FILE* f, u8 type, const void* data, u32 size) { + fwrite(&type, 1, 1, f); + u8 size_bytes[3] = { + (u8)(size & 0xFF), + (u8)((size >> 8) & 0xFF), + (u8)((size >> 16) & 0xFF) + }; + fwrite(size_bytes, 3, 1, f); + fwrite(data, size, 1, f); +} + +static void add_chunk_to_buffer(pxl8_replay* r, u8 type, const void* data, u32 size) { + pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk)); + if (!chunk) return; + + chunk->type = type; + chunk->size = size; + chunk->data = malloc(size); + if (!chunk->data) { + free(chunk); + return; + } + memcpy(chunk->data, data, size); + + if (!r->pending_inputs) { + r->pending_inputs = chunk; + r->pending_inputs_tail = chunk; + } else { + r->pending_inputs_tail->next = chunk; + r->pending_inputs_tail = chunk; + } +} + +static void pack_keys(const bool* keys, u8* packed) { + memset(packed, 0, 32); + for (int i = 0; i < 256; i++) { + if (keys[i]) { + packed[i / 8] |= (1 << (i % 8)); + } + } +} + +static void unpack_keys(const u8* packed, bool* keys) { + for (int i = 0; i < 256; i++) { + keys[i] = (packed[i / 8] >> (i % 8)) & 1; + } +} + +static u8 pack_mouse_buttons(const bool* buttons) { + return (buttons[0] ? 1 : 0) | (buttons[1] ? 2 : 0) | (buttons[2] ? 4 : 0); +} + +static void unpack_mouse_buttons(u8 packed, bool* buttons) { + buttons[0] = (packed & 1) != 0; + buttons[1] = (packed & 2) != 0; + buttons[2] = (packed & 4) != 0; +} + +void pxl8_replay_write_keyframe(pxl8_replay* r, u32 frame, f32 time, pxl8_rng* rng, pxl8_input_state* input) { + if (!r || !r->recording) return; + + pxl8_keyframe kf = {0}; + kf.frame_number = frame; + kf.time = time; + kf.rng_state = rng ? rng->state : 0; + + if (input) { + pack_keys(input->keys_down, kf.keys_down); + kf.mouse_buttons = pack_mouse_buttons(input->mouse_buttons_down); + kf.mouse_x = (i16)input->mouse_x; + kf.mouse_y = (i16)input->mouse_y; + } + + if (r->file) { + write_chunk(r->file, PXL8_REPLAY_CHUNK_KEYFRAME, &kf, sizeof(kf)); + fflush(r->file); + } else { + pxl8_keyframe_entry* entry = calloc(1, sizeof(pxl8_keyframe_entry)); + if (!entry) return; + + entry->keyframe = kf; + entry->input_deltas = r->pending_inputs; + r->pending_inputs = NULL; + r->pending_inputs_tail = NULL; + + if (r->keyframe_count >= r->max_keyframes && r->keyframes) { + pxl8_keyframe_entry* oldest = r->keyframes; + r->keyframes = oldest->next; + if (r->keyframes) { + r->keyframes->prev = NULL; + } + pxl8_replay_chunk_free(oldest->input_deltas); + free(oldest); + r->keyframe_count--; + } + + entry->prev = r->current_keyframe; + if (r->current_keyframe) { + r->current_keyframe->next = entry; + } + r->current_keyframe = entry; + if (!r->keyframes) { + r->keyframes = entry; + } + r->keyframe_count++; + } + + r->current_frame = frame; + r->header.total_frames = frame; +} + +void pxl8_replay_write_input(pxl8_replay* r, u32 frame, pxl8_input_state* prev, pxl8_input_state* curr) { + if (!r || !r->recording || !prev || !curr) return; + + u8 buffer[256]; + u32 offset = 0; + + buffer[offset++] = (u8)(frame & 0xFF); + buffer[offset++] = (u8)((frame >> 8) & 0xFF); + buffer[offset++] = (u8)((frame >> 16) & 0xFF); + buffer[offset++] = (u8)((frame >> 24) & 0xFF); + + u8 key_event_count = 0; + u8 key_events[64]; + u32 key_offset = 0; + + for (int i = 0; i < 256 && key_event_count < 32; i++) { + if (prev->keys_down[i] != curr->keys_down[i]) { + key_events[key_offset++] = (u8)i; + key_events[key_offset++] = curr->keys_down[i] ? 1 : 0; + key_event_count++; + } + } + + buffer[offset++] = key_event_count; + memcpy(buffer + offset, key_events, key_offset); + offset += key_offset; + + u8 prev_mouse_btns = pack_mouse_buttons(prev->mouse_buttons_down); + u8 curr_mouse_btns = pack_mouse_buttons(curr->mouse_buttons_down); + + u8 mouse_flags = 0; + if (curr->mouse_x != prev->mouse_x || curr->mouse_y != prev->mouse_y) { + mouse_flags |= 0x01; + } + if (curr_mouse_btns != prev_mouse_btns) { + mouse_flags |= 0x02; + } + if (curr->mouse_wheel_x != 0 || curr->mouse_wheel_y != 0) { + mouse_flags |= 0x04; + } + + buffer[offset++] = mouse_flags; + + if (mouse_flags & 0x01) { + i16 dx = (i16)(curr->mouse_x - prev->mouse_x); + i16 dy = (i16)(curr->mouse_y - prev->mouse_y); + buffer[offset++] = (u8)(dx & 0xFF); + buffer[offset++] = (u8)((dx >> 8) & 0xFF); + buffer[offset++] = (u8)(dy & 0xFF); + buffer[offset++] = (u8)((dy >> 8) & 0xFF); + } + + if (mouse_flags & 0x02) { + buffer[offset++] = curr_mouse_btns; + } + + if (mouse_flags & 0x04) { + buffer[offset++] = (i8)curr->mouse_wheel_x; + buffer[offset++] = (i8)curr->mouse_wheel_y; + } + + if (r->file) { + write_chunk(r->file, PXL8_REPLAY_CHUNK_INPUT, buffer, offset); + } else { + add_chunk_to_buffer(r, PXL8_REPLAY_CHUNK_INPUT, buffer, offset); + } + + r->current_frame = frame; + r->header.total_frames = frame; +} + +void pxl8_replay_write_audio_event(pxl8_replay* r, u32 frame, u8 event_type, u8 context_id, u8 note, f32 volume) { + if (!r || !r->recording) return; + + pxl8_audio_event evt = { + .frame_number = frame, + .event_type = event_type, + .context_id = context_id, + .note = note, + .volume = volume + }; + + if (r->file) { + write_chunk(r->file, PXL8_REPLAY_CHUNK_AUDIO_EVENT, &evt, sizeof(evt)); + } else { + pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk)); + if (!chunk) return; + + chunk->type = PXL8_REPLAY_CHUNK_AUDIO_EVENT; + chunk->size = sizeof(evt); + chunk->data = malloc(sizeof(evt)); + if (!chunk->data) { + free(chunk); + return; + } + memcpy(chunk->data, &evt, sizeof(evt)); + + if (!r->audio_events) { + r->audio_events = chunk; + r->audio_events_tail = chunk; + } else { + r->audio_events_tail->next = chunk; + r->audio_events_tail = chunk; + } + } +} + +void pxl8_replay_close(pxl8_replay* r) { + pxl8_replay_destroy(r); +} + +bool pxl8_replay_seek_frame(pxl8_replay* r, u32 frame) { + if (!r || !r->playing) return false; + + pxl8_keyframe_entry* target = NULL; + for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) { + if (e->keyframe.frame_number <= frame) { + target = e; + } else { + break; + } + } + + if (!target) return false; + + r->current_keyframe = target; + r->current_frame = target->keyframe.frame_number; + return true; +} + +bool pxl8_replay_read_frame(pxl8_replay* r, pxl8_input_state* input_out) { + if (!r || !r->playing || !r->current_keyframe) return false; + + u32 target_frame = r->current_frame + 1; + + if (target_frame > r->header.total_frames) return false; + + pxl8_replay_chunk* delta = r->current_keyframe->input_deltas; + while (delta) { + if (delta->size >= 5) { + u32 delta_frame = delta->data[0] | (delta->data[1] << 8) | (delta->data[2] << 16) | (delta->data[3] << 24); + if (delta_frame == target_frame) { + u32 offset = 4; + u8 key_event_count = delta->data[offset++]; + + for (u8 i = 0; i < key_event_count && offset + 1 < delta->size; i++) { + u8 scancode = delta->data[offset++]; + u8 pressed = delta->data[offset++]; + input_out->keys_down[scancode] = pressed != 0; + } + + if (offset < delta->size) { + u8 mouse_flags = delta->data[offset++]; + + if (mouse_flags & 0x01 && offset + 3 < delta->size) { + i16 dx = (i16)(delta->data[offset] | (delta->data[offset + 1] << 8)); + i16 dy = (i16)(delta->data[offset + 2] | (delta->data[offset + 3] << 8)); + offset += 4; + input_out->mouse_x += dx; + input_out->mouse_y += dy; + } + + if (mouse_flags & 0x02 && offset < delta->size) { + u8 mouse_btns = delta->data[offset++]; + unpack_mouse_buttons(mouse_btns, input_out->mouse_buttons_down); + } + + if (mouse_flags & 0x04 && offset + 1 < delta->size) { + input_out->mouse_wheel_x = (i8)delta->data[offset++]; + input_out->mouse_wheel_y = (i8)delta->data[offset++]; + } + } + + r->current_frame = target_frame; + return true; + } + } + delta = delta->next; + } + + if (r->current_keyframe->next && r->current_keyframe->next->keyframe.frame_number <= target_frame) { + r->current_keyframe = r->current_keyframe->next; + } + + r->current_frame = target_frame; + return true; +} + +bool pxl8_replay_get_keyframe(pxl8_replay* r, u32 frame, pxl8_keyframe* out) { + if (!r || !out) return false; + + pxl8_keyframe_entry* target = NULL; + for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) { + if (e->keyframe.frame_number <= frame) { + target = e; + } else { + break; + } + } + + if (!target) return false; + + *out = target->keyframe; + return true; +} + +void pxl8_replay_apply_keyframe(pxl8_replay* r, pxl8_keyframe* kf, pxl8_rng* rng, pxl8_input_state* input) { + if (!kf) return; + + if (rng) { + rng->state = kf->rng_state; + } + + if (input) { + unpack_keys(kf->keys_down, input->keys_down); + unpack_mouse_buttons(kf->mouse_buttons, input->mouse_buttons_down); + input->mouse_x = kf->mouse_x; + input->mouse_y = kf->mouse_y; + } + + if (r) { + r->current_frame = kf->frame_number; + } +} + +bool pxl8_replay_export(pxl8_replay* r, const char* path) { + if (!r || !r->keyframes) return false; + + FILE* f = fopen(path, "wb"); + if (!f) { + pxl8_error("Failed to create export file: %s", path); + return false; + } + + pxl8_replay_header header = r->header; + fwrite(&header, sizeof(header), 1, f); + + for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) { + write_chunk(f, PXL8_REPLAY_CHUNK_KEYFRAME, &e->keyframe, sizeof(pxl8_keyframe)); + + for (pxl8_replay_chunk* c = e->input_deltas; c; c = c->next) { + write_chunk(f, c->type, c->data, c->size); + } + } + + for (pxl8_replay_chunk* c = r->audio_events; c; c = c->next) { + write_chunk(f, c->type, c->data, c->size); + } + + u8 end_chunk = PXL8_REPLAY_CHUNK_END; + fwrite(&end_chunk, 1, 1, f); + + fclose(f); + pxl8_info("Exported replay to: %s (%u frames)", path, header.total_frames); + return true; +} diff --git a/src/pxl8_replay.h b/src/pxl8_replay.h new file mode 100644 index 0000000..7da652d --- /dev/null +++ b/src/pxl8_replay.h @@ -0,0 +1,84 @@ +#pragma once + +#include "pxl8_io.h" +#include "pxl8_rng.h" +#include "pxl8_types.h" + +#define PXL8_REPLAY_MAGIC 0x31525850 +#define PXL8_REPLAY_VERSION 1 + +#define PXL8_REPLAY_CHUNK_KEYFRAME 0x01 +#define PXL8_REPLAY_CHUNK_INPUT 0x02 +#define PXL8_REPLAY_CHUNK_AUDIO_EVENT 0x03 +#define PXL8_REPLAY_CHUNK_END 0xFF + +#define PXL8_REPLAY_FLAG_HAS_PALETTE (1 << 0) +#define PXL8_REPLAY_FLAG_HAS_GLOBALS (1 << 1) + +typedef struct pxl8_replay pxl8_replay; + +typedef struct pxl8_replay_header { + u32 magic; + u32 version; + u32 flags; + u32 keyframe_interval; + u32 total_frames; + u32 width; + u32 height; + u32 reserved; +} pxl8_replay_header; + +typedef struct pxl8_keyframe { + u32 frame_number; + f32 time; + u32 rng_state; + u8 keys_down[32]; + u8 mouse_buttons; + i16 mouse_x; + i16 mouse_y; + u8 flags; +} pxl8_keyframe; + +typedef struct pxl8_input_delta { + u32 frame_number; + u8 key_event_count; + u8 mouse_flags; +} pxl8_input_delta; + +typedef struct pxl8_audio_event { + u32 frame_number; + u8 event_type; + u8 context_id; + u8 note; + f32 volume; +} pxl8_audio_event; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_replay* pxl8_replay_create(const char* path, u32 keyframe_interval); +pxl8_replay* pxl8_replay_create_buffer(u32 keyframe_interval, u32 max_keyframes); +pxl8_replay* pxl8_replay_open(const char* path); +void pxl8_replay_destroy(pxl8_replay* r); + +bool pxl8_replay_is_recording(pxl8_replay* r); +bool pxl8_replay_is_playing(pxl8_replay* r); +u32 pxl8_replay_get_frame(pxl8_replay* r); +u32 pxl8_replay_get_total_frames(pxl8_replay* r); + +void pxl8_replay_write_keyframe(pxl8_replay* r, u32 frame, f32 time, pxl8_rng* rng, pxl8_input_state* input); +void pxl8_replay_write_input(pxl8_replay* r, u32 frame, pxl8_input_state* prev, pxl8_input_state* curr); +void pxl8_replay_write_audio_event(pxl8_replay* r, u32 frame, u8 event_type, u8 context_id, u8 note, f32 volume); +void pxl8_replay_close(pxl8_replay* r); + +bool pxl8_replay_seek_frame(pxl8_replay* r, u32 frame); +bool pxl8_replay_read_frame(pxl8_replay* r, pxl8_input_state* input_out); +bool pxl8_replay_get_keyframe(pxl8_replay* r, u32 frame, pxl8_keyframe* out); +void pxl8_replay_apply_keyframe(pxl8_replay* r, pxl8_keyframe* kf, pxl8_rng* rng, pxl8_input_state* input); + +bool pxl8_replay_export(pxl8_replay* r, const char* path); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_rng.c b/src/pxl8_rng.c new file mode 100644 index 0000000..ca12fff --- /dev/null +++ b/src/pxl8_rng.c @@ -0,0 +1,24 @@ +#include "pxl8_rng.h" + +void pxl8_rng_seed(pxl8_rng* rng, u32 seed) { + if (!rng) return; + rng->state = seed ? seed : 1; +} + +u32 pxl8_rng_next(pxl8_rng* rng) { + if (!rng) return 0; + rng->state ^= rng->state << 13; + rng->state ^= rng->state >> 17; + rng->state ^= rng->state << 5; + return rng->state; +} + +f32 pxl8_rng_f32(pxl8_rng* rng) { + return (f32)pxl8_rng_next(rng) / (f32)0xFFFFFFFF; +} + +i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max) { + if (min >= max) return min; + u32 range = (u32)(max - min); + return min + (i32)(pxl8_rng_next(rng) % range); +} diff --git a/src/pxl8_rng.h b/src/pxl8_rng.h new file mode 100644 index 0000000..2e8938b --- /dev/null +++ b/src/pxl8_rng.h @@ -0,0 +1,20 @@ +#pragma once + +#include "pxl8_types.h" + +typedef struct pxl8_rng { + u32 state; +} pxl8_rng; + +#ifdef __cplusplus +extern "C" { +#endif + +void pxl8_rng_seed(pxl8_rng* rng, u32 seed); +u32 pxl8_rng_next(pxl8_rng* rng); +f32 pxl8_rng_f32(pxl8_rng* rng); +i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_script.c b/src/pxl8_script.c index 67ab32d..bf1f70c 100644 --- a/src/pxl8_script.c +++ b/src/pxl8_script.c @@ -169,6 +169,12 @@ static const char* pxl8_ffi_cdefs = "typedef struct pxl8_gfx pxl8_gfx;\n" "typedef struct { int x, y, w, h; } pxl8_bounds;\n" "typedef struct { int x, y; } pxl8_point;\n" +"typedef struct pxl8_rng { u32 state; } pxl8_rng;\n" +"\n" +"void pxl8_rng_seed(pxl8_rng* rng, u32 seed);\n" +"u32 pxl8_rng_next(pxl8_rng* rng);\n" +"f32 pxl8_rng_f32(pxl8_rng* rng);\n" +"i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max);\n" "\n" "f32 pxl8_get_fps(const pxl8* sys);\n" "\n" @@ -266,7 +272,7 @@ static const char* pxl8_ffi_cdefs = "} pxl8_raster_bar;\n" "\n" "typedef struct pxl8_particles pxl8_particles;\n" -"pxl8_particles* pxl8_particles_create(u32 max_count);\n" +"pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng);\n" "void pxl8_particles_destroy(pxl8_particles* particles);\n" "void pxl8_particles_clear(pxl8_particles* particles);\n" "void pxl8_particles_emit(pxl8_particles* particles, u32 count);\n" @@ -460,7 +466,7 @@ static const char* pxl8_ffi_cdefs = "void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);\n" "void pxl8_sfx_node_destroy(pxl8_sfx_node* node);\n" "f32 pxl8_sfx_note_to_freq(u8 note);\n" -"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume);\n" +"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration);\n" "void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);\n" "pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);\n" "void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping);\n" @@ -684,6 +690,14 @@ void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) { } } +void pxl8_script_set_rng(pxl8_script* script, void* rng) { + if (!script) return; + if (script->L && rng) { + lua_pushlightuserdata(script->L, rng); + lua_setglobal(script->L, "_pxl8_rng"); + } +} + void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) { if (!script) return; script->mixer = mixer; @@ -747,6 +761,53 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { return result; } +static time_t get_file_mod_time(const char* path) { + struct stat file_stat; + if (stat(path, &file_stat) == 0) { + return file_stat.st_mtime; + } + return 0; +} + +static time_t get_latest_script_mod_time(const char* dir_path) { + DIR* dir = opendir(dir_path); + if (!dir) return 0; + + time_t latest = 0; + struct dirent* entry; + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') continue; + + char full_path[512]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); + + struct stat st; + if (stat(full_path, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + time_t subdir_time = get_latest_script_mod_time(full_path); + if (subdir_time > latest) { + latest = subdir_time; + } + } else { + size_t len = strlen(entry->d_name); + bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) || + (len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0); + + if (is_script) { + time_t mod_time = get_file_mod_time(full_path); + if (mod_time > latest) { + latest = mod_time; + } + } + } + } + } + + closedir(dir); + return latest; +} + void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) { if (!script || !script->L || !cart_path || !original_cwd) return; @@ -779,6 +840,9 @@ void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const lua_pop(script->L, 1); } lua_pop(script->L, 1); + + pxl8_strncpy(script->watch_dir, cart_path, sizeof(script->watch_dir)); + script->latest_mod_time = get_latest_script_mod_time(script->watch_dir); } pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) { @@ -985,7 +1049,7 @@ static bool pxl8_script_is_builtin_global(const char* name) { "print", "pxl8", "rawequal", "rawget", "rawset", "require", "select", "setfenv", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "update", "frame", - "xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_sfx_mixer", "_pxl8_sys", + "xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_rng", "_pxl8_sfx_mixer", "_pxl8_sys", NULL }; for (int i = 0; builtins[i]; i++) { @@ -1004,6 +1068,57 @@ static void pxl8_script_cleanup(pxl8_script* script) { } } +static void pxl8_script_save_upvalues(lua_State* L, const char* func_name, int saved_table) { + lua_getglobal(L, func_name); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + return; + } + + int func_idx = lua_gettop(L); + for (int i = 1; ; i++) { + const char* name = lua_getupvalue(L, func_idx, i); + if (!name) break; + + int vtype = lua_type(L, -1); + if (name[0] != '(' && + vtype != LUA_TFUNCTION && + vtype != LUA_TUSERDATA && + vtype != LUA_TLIGHTUSERDATA && + vtype != LUA_TTHREAD && + vtype != LUA_TTABLE) { + lua_pushstring(L, name); + lua_pushvalue(L, -2); + lua_settable(L, saved_table); + } + lua_pop(L, 1); + } + lua_pop(L, 1); +} + +static void pxl8_script_restore_upvalues(lua_State* L, const char* func_name, int saved_table) { + lua_getglobal(L, func_name); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + return; + } + + int func_idx = lua_gettop(L); + for (int i = 1; ; i++) { + const char* name = lua_getupvalue(L, func_idx, i); + if (!name) break; + lua_pop(L, 1); + + lua_getfield(L, saved_table, name); + if (!lua_isnil(L, -1)) { + lua_setupvalue(L, func_idx, i); + } else { + lua_pop(L, 1); + } + } + lua_pop(L, 1); +} + static void pxl8_script_save_globals(pxl8_script* script) { lua_State* L = script->L; @@ -1032,6 +1147,9 @@ static void pxl8_script_save_globals(pxl8_script* script) { } lua_pop(L, 1); + pxl8_script_save_upvalues(L, "update", saved_table); + pxl8_script_save_upvalues(L, "frame", saved_table); + lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); pxl8_debug("Hot reload state saved"); } @@ -1045,13 +1163,19 @@ static void pxl8_script_restore_globals(pxl8_script* script) { return; } + int saved_table = lua_gettop(L); + lua_pushnil(L); - while (lua_next(L, -2) != 0) { + while (lua_next(L, saved_table) != 0) { lua_pushvalue(L, -2); lua_pushvalue(L, -2); lua_settable(L, LUA_GLOBALSINDEX); lua_pop(L, 1); } + + pxl8_script_restore_upvalues(L, "update", saved_table); + pxl8_script_restore_upvalues(L, "frame", saved_table); + lua_pop(L, 1); lua_pushnil(L); @@ -1059,51 +1183,224 @@ static void pxl8_script_restore_globals(pxl8_script* script) { pxl8_debug("Hot reload state restored"); } -static time_t get_file_mod_time(const char* path) { - struct stat file_stat; - if (stat(path, &file_stat) == 0) { - return file_stat.st_mtime; - } - return 0; +#define SER_NIL 0 +#define SER_BOOL 1 +#define SER_NUMBER 2 +#define SER_STRING 3 +#define SER_TABLE 4 + +typedef struct { + u8* data; + u32 size; + u32 capacity; +} ser_buffer; + +static void ser_buffer_init(ser_buffer* buf) { + buf->capacity = 1024; + buf->data = malloc(buf->capacity); + buf->size = 0; } -static time_t get_latest_script_mod_time(const char* dir_path) { - DIR* dir = opendir(dir_path); - if (!dir) return 0; +static void ser_buffer_grow(ser_buffer* buf, u32 needed) { + if (buf->size + needed > buf->capacity) { + while (buf->size + needed > buf->capacity) { + buf->capacity *= 2; + } + buf->data = realloc(buf->data, buf->capacity); + } +} - time_t latest = 0; - struct dirent* entry; +static void ser_write_u8(ser_buffer* buf, u8 v) { + ser_buffer_grow(buf, 1); + buf->data[buf->size++] = v; +} - while ((entry = readdir(dir)) != NULL) { - if (entry->d_name[0] == '.') continue; +static void ser_write_u32(ser_buffer* buf, u32 v) { + ser_buffer_grow(buf, 4); + buf->data[buf->size++] = v & 0xFF; + buf->data[buf->size++] = (v >> 8) & 0xFF; + buf->data[buf->size++] = (v >> 16) & 0xFF; + buf->data[buf->size++] = (v >> 24) & 0xFF; +} - char full_path[512]; - snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); +static void ser_write_f64(ser_buffer* buf, f64 v) { + ser_buffer_grow(buf, 8); + memcpy(buf->data + buf->size, &v, 8); + buf->size += 8; +} - struct stat st; - if (stat(full_path, &st) == 0) { - if (S_ISDIR(st.st_mode)) { - time_t subdir_time = get_latest_script_mod_time(full_path); - if (subdir_time > latest) { - latest = subdir_time; - } - } else { - size_t len = strlen(entry->d_name); - bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) || - (len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0); +static void ser_write_bytes(ser_buffer* buf, const void* data, u32 len) { + ser_buffer_grow(buf, len); + memcpy(buf->data + buf->size, data, len); + buf->size += len; +} - if (is_script) { - time_t mod_time = get_file_mod_time(full_path); - if (mod_time > latest) { - latest = mod_time; - } - } +static void ser_write_value(ser_buffer* buf, lua_State* L, int idx, int depth) { + int t = lua_type(L, idx); + switch (t) { + case LUA_TNIL: + ser_write_u8(buf, SER_NIL); + break; + case LUA_TBOOLEAN: + ser_write_u8(buf, SER_BOOL); + ser_write_u8(buf, lua_toboolean(L, idx) ? 1 : 0); + break; + case LUA_TNUMBER: + ser_write_u8(buf, SER_NUMBER); + ser_write_f64(buf, lua_tonumber(L, idx)); + break; + case LUA_TSTRING: { + size_t len; + const char* str = lua_tolstring(L, idx, &len); + ser_write_u8(buf, SER_STRING); + ser_write_u32(buf, (u32)len); + ser_write_bytes(buf, str, (u32)len); + break; + } + case LUA_TTABLE: { + if (depth > 16) { ser_write_u8(buf, SER_NIL); break; } + ser_write_u8(buf, SER_TABLE); + lua_pushvalue(L, idx); + int tbl = lua_gettop(L); + lua_pushnil(L); + while (lua_next(L, tbl) != 0) { + ser_write_value(buf, L, -2, depth + 1); + ser_write_value(buf, L, -1, depth + 1); + lua_pop(L, 1); + } + lua_pop(L, 1); + ser_write_u8(buf, SER_NIL); + break; + } + default: + ser_write_u8(buf, SER_NIL); + break; + } +} + +u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data) { + if (!script || !out_data) return 0; + lua_State* L = script->L; + + ser_buffer buf; + ser_buffer_init(&buf); + + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + const char* name = lua_tostring(L, -2); + int vtype = lua_type(L, -1); + + if (!pxl8_script_is_builtin_global(name) && + vtype != LUA_TFUNCTION && + vtype != LUA_TUSERDATA && + vtype != LUA_TLIGHTUSERDATA && + vtype != LUA_TTHREAD && + vtype != 10) { + ser_write_value(&buf, L, -2, 0); + ser_write_value(&buf, L, -1, 0); } } + lua_pop(L, 1); + } + lua_pop(L, 1); + + ser_write_u8(&buf, SER_NIL); + + *out_data = buf.data; + return buf.size; +} + +typedef struct { + const u8* data; + u32 size; + u32 pos; +} deser_buffer; + +static u8 deser_read_u8(deser_buffer* buf) { + if (buf->pos >= buf->size) return 0; + return buf->data[buf->pos++]; +} + +static u32 deser_read_u32(deser_buffer* buf) { + if (buf->pos + 4 > buf->size) return 0; + u32 v = buf->data[buf->pos] | (buf->data[buf->pos+1] << 8) | + (buf->data[buf->pos+2] << 16) | (buf->data[buf->pos+3] << 24); + buf->pos += 4; + return v; +} + +static f64 deser_read_f64(deser_buffer* buf) { + if (buf->pos + 8 > buf->size) return 0; + f64 v; + memcpy(&v, buf->data + buf->pos, 8); + buf->pos += 8; + return v; +} + +static void deser_read_value(deser_buffer* buf, lua_State* L, int depth) { + u8 type = deser_read_u8(buf); + switch (type) { + case SER_NIL: + lua_pushnil(L); + break; + case SER_BOOL: + lua_pushboolean(L, deser_read_u8(buf)); + break; + case SER_NUMBER: + lua_pushnumber(L, deser_read_f64(buf)); + break; + case SER_STRING: { + u32 len = deser_read_u32(buf); + if (buf->pos + len <= buf->size) { + lua_pushlstring(L, (const char*)(buf->data + buf->pos), len); + buf->pos += len; + } else { + lua_pushnil(L); + } + break; + } + case SER_TABLE: { + if (depth > 16) { lua_pushnil(L); break; } + lua_newtable(L); + while (buf->pos < buf->size) { + u8 key_type = deser_read_u8(buf); + if (key_type == SER_NIL) break; + buf->pos--; + deser_read_value(buf, L, depth + 1); + deser_read_value(buf, L, depth + 1); + lua_settable(L, -3); + } + break; + } + default: + lua_pushnil(L); + break; + } +} + +void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size) { + if (!script || !data || size == 0) return; + lua_State* L = script->L; + + deser_buffer buf = { .data = data, .size = size, .pos = 0 }; + + while (buf.pos < buf.size) { + u8 key_type = deser_read_u8(&buf); + if (key_type == SER_NIL) break; + buf.pos--; + + deser_read_value(&buf, L, 0); + deser_read_value(&buf, L, 0); + lua_settable(L, LUA_GLOBALSINDEX); } - closedir(dir); - return latest; + pxl8_debug("Deserialized globals from %u bytes", size); +} + +void pxl8_script_free_serialized(u8* data) { + free(data); } pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) { @@ -1188,18 +1485,17 @@ bool pxl8_script_check_reload(pxl8_script* script) { if (ext && strcmp(ext, ".fnl") == 0) { if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) { - pxl8_script_call_function(script, "init"); reloaded = true; } } else if (ext && strcmp(ext, ".lua") == 0) { if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) { - pxl8_script_call_function(script, "init"); reloaded = true; } } if (reloaded) { pxl8_script_restore_globals(script); + pxl8_script_call_function(script, "init"); } return reloaded; diff --git a/src/pxl8_script.h b/src/pxl8_script.h index 0281bb1..0e83c85 100644 --- a/src/pxl8_script.h +++ b/src/pxl8_script.h @@ -20,6 +20,7 @@ bool pxl8_script_is_incomplete_input(pxl8_script* script); void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd); void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx); void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input); +void pxl8_script_set_rng(pxl8_script* script, void* rng); void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer); void pxl8_script_set_sys(pxl8_script* script, void* sys); @@ -34,6 +35,10 @@ 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); pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename); +u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data); +void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size); +void pxl8_script_free_serialized(u8* data); + #ifdef __cplusplus } #endif diff --git a/src/pxl8_sdl3.c b/src/pxl8_sdl3.c index 1a6c1ed..fe93912 100644 --- a/src/pxl8_sdl3.c +++ b/src/pxl8_sdl3.c @@ -318,6 +318,75 @@ static void sdl3_center_cursor(void* platform_data) { } } +typedef struct pxl8_sdl3_audio { + SDL_AudioStream* stream; + i32 sample_rate; + i32 channels; +} pxl8_sdl3_audio; + +static void* sdl3_audio_create(i32 sample_rate, i32 channels) { + pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)SDL_calloc(1, sizeof(pxl8_sdl3_audio)); + if (!audio) return NULL; + + SDL_AudioSpec spec = { + .freq = sample_rate, + .channels = channels, + .format = SDL_AUDIO_F32 + }; + + audio->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL); + if (!audio->stream) { + pxl8_error("Failed to open audio device: %s", SDL_GetError()); + SDL_free(audio); + return NULL; + } + + audio->sample_rate = sample_rate; + audio->channels = channels; + + return audio; +} + +static void sdl3_audio_destroy(void* audio_handle) { + if (!audio_handle) return; + + pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle; + if (audio->stream) { + SDL_DestroyAudioStream(audio->stream); + } + SDL_free(audio); +} + +static void sdl3_audio_start(void* audio_handle) { + if (!audio_handle) return; + + pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle; + SDL_ResumeAudioStreamDevice(audio->stream); +} + +static void sdl3_audio_stop(void* audio_handle) { + if (!audio_handle) return; + + pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle; + SDL_PauseAudioStreamDevice(audio->stream); +} + +static bool sdl3_upload_audio(void* audio_handle, const f32* stereo_samples, i32 sample_count) { + if (!audio_handle || !stereo_samples) return false; + + pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle; + return SDL_PutAudioStreamData(audio->stream, stereo_samples, + sample_count * audio->channels * sizeof(f32)); +} + +static i32 sdl3_audio_queued(void* audio_handle) { + if (!audio_handle) return 0; + + pxl8_sdl3_audio* audio = (pxl8_sdl3_audio*)audio_handle; + i32 bytes = SDL_GetAudioStreamQueued(audio->stream); + return bytes / (audio->channels * sizeof(f32)); +} + const pxl8_hal pxl8_hal_sdl3 = { .create = sdl3_create, .destroy = sdl3_destroy, @@ -325,6 +394,12 @@ const pxl8_hal pxl8_hal_sdl3 = { .center_cursor = sdl3_center_cursor, .present = sdl3_present, .set_cursor = sdl3_set_cursor, - .upload_texture = sdl3_upload_texture, .set_relative_mouse_mode = sdl3_set_relative_mouse_mode, + .upload_texture = sdl3_upload_texture, + .audio_create = sdl3_audio_create, + .audio_destroy = sdl3_audio_destroy, + .audio_start = sdl3_audio_start, + .audio_stop = sdl3_audio_stop, + .upload_audio = sdl3_upload_audio, + .audio_queued = sdl3_audio_queued, }; diff --git a/src/pxl8_sfx.c b/src/pxl8_sfx.c index 90a4edf..02357fe 100644 --- a/src/pxl8_sfx.c +++ b/src/pxl8_sfx.c @@ -3,8 +3,7 @@ #include #include -#include - +#include "pxl8_hal.h" #include "pxl8_log.h" #include "pxl8_math.h" @@ -62,6 +61,8 @@ typedef struct voice { f32 filter_env_depth; f32 fx_send; f32 start_time; + f32 duration; + bool released; oscillator osc; envelope amp_env; envelope filter_env; @@ -125,16 +126,21 @@ struct pxl8_sfx_node { struct pxl8_sfx_context { voice voices[PXL8_SFX_MAX_VOICES]; pxl8_sfx_node* fx_head; + pxl8_sfx_mixer* mixer; f32 volume; f32 current_time; u16 next_voice_id; + u8 context_id; }; struct pxl8_sfx_mixer { - SDL_AudioStream* stream; + const pxl8_hal* hal; + void* audio_handle; pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS]; f32 master_volume; f32* output_buffer; + pxl8_sfx_event_callback event_callback; + void* event_userdata; }; static void envelope_trigger(envelope* env) { @@ -334,7 +340,7 @@ static f32 lfo_process(lfo* l) { return sample * l->depth; } -static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params, f32 volume, u16 id, f32 time) { +static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params, f32 volume, u16 id, f32 time, f32 duration) { v->active = true; v->id = id; v->note = note; @@ -345,6 +351,8 @@ static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params v->filter_env_depth = params->filter_env_depth; v->fx_send = params->fx_send; v->start_time = time; + v->duration = duration; + v->released = false; oscillator_init(&v->osc, params->waveform, v->base_frequency); v->osc.pulse_width = params->pulse_width; @@ -603,10 +611,17 @@ static void context_process_sample(pxl8_sfx_context* ctx, f32* out_left, f32* ou f32 wet_left = 0.0f, wet_right = 0.0f; for (int v = 0; v < PXL8_SFX_MAX_VOICES; v++) { - if (!ctx->voices[v].active) continue; + voice* vp = &ctx->voices[v]; + if (!vp->active) continue; + + if (vp->duration > 0.0f && !vp->released && + ctx->current_time - vp->start_time >= vp->duration) { + voice_release(vp); + vp->released = true; + } f32 dl, dr, wl, wr; - voice_process(&ctx->voices[v], &dl, &dr, &wl, &wr); + voice_process(vp, &dl, &dr, &wl, &wr); dry_left += dl; dry_right += dr; @@ -631,45 +646,9 @@ static void context_process_sample(pxl8_sfx_context* ctx, f32* out_left, f32* ou *out_right = right; } -static void SDLCALL audio_callback(void* userdata, SDL_AudioStream* stream, int additional_amount, int total_amount) { - (void)total_amount; +pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal) { + if (!hal || !hal->audio_create) return NULL; - pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)userdata; - if (!mixer || !mixer->output_buffer) return; - - u32 samples_needed = additional_amount / (sizeof(f32) * 2); - if (samples_needed > PXL8_SFX_BUFFER_SIZE) samples_needed = PXL8_SFX_BUFFER_SIZE; - - for (u32 i = 0; i < samples_needed; i++) { - f32 left = 0.0f, right = 0.0f; - - for (int c = 0; c < PXL8_SFX_MAX_CONTEXTS; c++) { - pxl8_sfx_context* ctx = mixer->contexts[c]; - if (!ctx) continue; - - f32 ctx_left, ctx_right; - context_process_sample(ctx, &ctx_left, &ctx_right); - left += ctx_left; - right += ctx_right; - } - - left *= mixer->master_volume; - right *= mixer->master_volume; - - left = fmaxf(-1.0f, fminf(1.0f, left)); - right = fmaxf(-1.0f, fminf(1.0f, right)); - - if (!isfinite(left)) left = 0.0f; - if (!isfinite(right)) right = 0.0f; - - mixer->output_buffer[i * 2] = left; - mixer->output_buffer[i * 2 + 1] = right; - } - - SDL_PutAudioStreamData(stream, mixer->output_buffer, samples_needed * sizeof(f32) * 2); -} - -pxl8_sfx_mixer* pxl8_sfx_mixer_create(void) { pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)calloc(1, sizeof(pxl8_sfx_mixer)); if (!mixer) return NULL; @@ -679,23 +658,17 @@ pxl8_sfx_mixer* pxl8_sfx_mixer_create(void) { return NULL; } + mixer->hal = hal; mixer->master_volume = 0.8f; - SDL_AudioSpec spec = { - .channels = 2, - .format = SDL_AUDIO_F32, - .freq = PXL8_SFX_SAMPLE_RATE - }; - - mixer->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, mixer); - if (!mixer->stream) { - pxl8_error("Failed to open audio device: %s", SDL_GetError()); + mixer->audio_handle = hal->audio_create(PXL8_SFX_SAMPLE_RATE, 2); + if (!mixer->audio_handle) { free(mixer->output_buffer); free(mixer); return NULL; } - SDL_ResumeAudioStreamDevice(mixer->stream); + hal->audio_start(mixer->audio_handle); pxl8_info("Audio mixer initialized: %d Hz, stereo, %d context slots", PXL8_SFX_SAMPLE_RATE, PXL8_SFX_MAX_CONTEXTS); return mixer; @@ -704,14 +677,57 @@ pxl8_sfx_mixer* pxl8_sfx_mixer_create(void) { void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer) { if (!mixer) return; - if (mixer->stream) { - SDL_DestroyAudioStream(mixer->stream); + if (mixer->hal && mixer->audio_handle) { + mixer->hal->audio_destroy(mixer->audio_handle); } free(mixer->output_buffer); free(mixer); } +void pxl8_sfx_mixer_process(pxl8_sfx_mixer* mixer) { + if (!mixer || !mixer->hal || !mixer->audio_handle || !mixer->output_buffer) return; + + i32 queued = mixer->hal->audio_queued(mixer->audio_handle); + i32 target = PXL8_SFX_SAMPLE_RATE / 10; + + while (queued < target) { + i32 samples_to_generate = PXL8_SFX_BUFFER_SIZE; + if (samples_to_generate > target - queued) { + samples_to_generate = target - queued; + } + + for (i32 i = 0; i < samples_to_generate; i++) { + f32 left = 0.0f, right = 0.0f; + + for (int c = 0; c < PXL8_SFX_MAX_CONTEXTS; c++) { + pxl8_sfx_context* ctx = mixer->contexts[c]; + if (!ctx) continue; + + f32 ctx_left, ctx_right; + context_process_sample(ctx, &ctx_left, &ctx_right); + left += ctx_left; + right += ctx_right; + } + + left *= mixer->master_volume; + right *= mixer->master_volume; + + left = fmaxf(-1.0f, fminf(1.0f, left)); + right = fmaxf(-1.0f, fminf(1.0f, right)); + + if (!isfinite(left)) left = 0.0f; + if (!isfinite(right)) right = 0.0f; + + mixer->output_buffer[i * 2] = left; + mixer->output_buffer[i * 2 + 1] = right; + } + + mixer->hal->upload_audio(mixer->audio_handle, mixer->output_buffer, samples_to_generate); + queued += samples_to_generate; + } +} + void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) { if (!mixer || !ctx) return; @@ -722,6 +738,8 @@ void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) { for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) { if (!mixer->contexts[i]) { mixer->contexts[i] = ctx; + ctx->mixer = mixer; + ctx->context_id = (u8)i; return; } } @@ -755,6 +773,13 @@ void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume) { if (mixer) mixer->master_volume = fmaxf(0.0f, fminf(1.0f, volume)); } +void pxl8_sfx_mixer_set_event_callback(pxl8_sfx_mixer* mixer, pxl8_sfx_event_callback cb, void* userdata) { + if (mixer) { + mixer->event_callback = cb; + mixer->event_userdata = userdata; + } +} + f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer) { return mixer ? mixer->master_volume : 0.0f; } @@ -1047,7 +1072,7 @@ static i32 find_free_voice(pxl8_sfx_context* ctx) { return oldest_active; } -u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume) { +u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration) { if (!ctx || !params) return 0; i32 slot = find_free_voice(ctx); @@ -1056,7 +1081,11 @@ u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_para u16 id = ctx->next_voice_id++; if (ctx->next_voice_id == 0) ctx->next_voice_id = 1; - voice_trigger(&ctx->voices[slot], note, params, volume, id, ctx->current_time); + voice_trigger(&ctx->voices[slot], note, params, volume, id, ctx->current_time, duration); + + if (ctx->mixer && ctx->mixer->event_callback) { + ctx->mixer->event_callback(PXL8_SFX_EVENT_NOTE_ON, ctx->context_id, note, volume, ctx->mixer->event_userdata); + } return id; } diff --git a/src/pxl8_sfx.h b/src/pxl8_sfx.h index 780604d..587ade7 100644 --- a/src/pxl8_sfx.h +++ b/src/pxl8_sfx.h @@ -1,5 +1,6 @@ #pragma once +#include "pxl8_hal.h" #include "pxl8_types.h" #define PXL8_SFX_BUFFER_SIZE 1024 @@ -107,17 +108,24 @@ void pxl8_sfx_delay_set_feedback(pxl8_sfx_node* node, f32 feedback); void pxl8_sfx_delay_set_mix(pxl8_sfx_node* node, f32 mix); void pxl8_sfx_delay_set_time(pxl8_sfx_node* node, u32 time_l, u32 time_r); +#define PXL8_SFX_EVENT_NOTE_ON 1 +#define PXL8_SFX_EVENT_NOTE_OFF 2 + +typedef void (*pxl8_sfx_event_callback)(u8 event_type, u8 context_id, u8 note, f32 volume, void* userdata); + void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx); void pxl8_sfx_mixer_clear(pxl8_sfx_mixer* mixer); -pxl8_sfx_mixer* pxl8_sfx_mixer_create(void); +pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal); void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer); void pxl8_sfx_mixer_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx); f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer); +void pxl8_sfx_mixer_process(pxl8_sfx_mixer* mixer); +void pxl8_sfx_mixer_set_event_callback(pxl8_sfx_mixer* mixer, pxl8_sfx_event_callback cb, void* userdata); void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume); void pxl8_sfx_node_destroy(pxl8_sfx_node* node); f32 pxl8_sfx_note_to_freq(u8 note); -u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume); +u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration); void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id); pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg); diff --git a/src/pxl8_vfx.c b/src/pxl8_vfx.c index 032d3cb..bfb1761 100644 --- a/src/pxl8_vfx.c +++ b/src/pxl8_vfx.c @@ -3,10 +3,11 @@ #include #include -#define PXL8_RANDF() ((f32)rand() / (f32)RAND_MAX) +#include "pxl8_math.h" struct pxl8_particles { pxl8_particle* particles; + pxl8_rng* rng; u32 alive_count; u32 count; u32 max_count; @@ -22,7 +23,7 @@ struct pxl8_particles { f32 spawn_timer; void (*render_fn)(pxl8_gfx* gfx, pxl8_particle* p, void* userdata); - void (*spawn_fn)(pxl8_particle* p, void* userdata); + void (*spawn_fn)(pxl8_particles* particles, pxl8_particle* p); void (*update_fn)(pxl8_particle* p, f32 dt, void* userdata); void* userdata; }; @@ -170,7 +171,7 @@ void pxl8_vfx_water_ripple(pxl8_gfx* gfx, f32* height_map, i32 drop_x, i32 drop_ prev_height = temp; } -pxl8_particles* pxl8_particles_create(u32 max_count) { +pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng) { pxl8_particles* particles = calloc(1, sizeof(pxl8_particles)); if (!particles) return NULL; @@ -180,6 +181,7 @@ pxl8_particles* pxl8_particles_create(u32 max_count) { return NULL; } + particles->rng = rng; particles->max_count = max_count; particles->drag = 0.98f; particles->gravity_y = 100.0f; @@ -214,8 +216,8 @@ void pxl8_particles_emit(pxl8_particles* particles, u32 count) { pxl8_particle* p = &particles->particles[j]; p->life = 1.0f; p->max_life = 1.0f; - p->x = particles->x + ((PXL8_RANDF()) - 0.5f) * particles->spread_x; - p->y = particles->y + ((PXL8_RANDF()) - 0.5f) * particles->spread_y; + p->x = particles->x + (pxl8_rng_f32(particles->rng) - 0.5f) * particles->spread_x; + p->y = particles->y + (pxl8_rng_f32(particles->rng) - 0.5f) * particles->spread_y; p->z = 0; p->vx = p->vy = p->vz = 0; p->ax = particles->gravity_x; @@ -228,7 +230,7 @@ void pxl8_particles_emit(pxl8_particles* particles, u32 count) { p->flags = 1; if (particles->spawn_fn) { - particles->spawn_fn(p, particles->userdata); + particles->spawn_fn(particles, p); } particles->alive_count++; @@ -316,28 +318,28 @@ void pxl8_vfx_explosion(pxl8_particles* particles, i32 x, i32 y, u32 color, f32 for (u32 i = 0; i < 50 && i < particles->max_count; i++) { pxl8_particle* p = &particles->particles[i]; - f32 angle = (PXL8_RANDF()) * 6.28f; - f32 speed = force * (0.5f + (PXL8_RANDF()) * 0.5f); - + f32 angle = pxl8_rng_f32(particles->rng) * PXL8_TAU; + f32 speed = force * (0.5f + pxl8_rng_f32(particles->rng) * 0.5f); + p->x = x; p->y = y; p->vx = cosf(angle) * speed; p->vy = sinf(angle) * speed; p->life = 1.0f; - p->max_life = 1.0f + (PXL8_RANDF()); + p->max_life = 1.0f + pxl8_rng_f32(particles->rng); p->color = color; p->flags = 1; } } -static void fire_spawn(pxl8_particle* p, void* userdata) { - uintptr_t palette_start = (uintptr_t)userdata; - p->start_color = palette_start + 6 + (rand() % 3); +static void fire_spawn(pxl8_particles* particles, pxl8_particle* p) { + uintptr_t palette_start = (uintptr_t)particles->userdata; + p->start_color = palette_start + 6 + pxl8_rng_range(particles->rng, 0, 3); p->end_color = palette_start; p->color = p->start_color; - p->max_life = 1.5f + (PXL8_RANDF()) * 1.5f; - p->vy = -80.0f - (PXL8_RANDF()) * 120.0f; - p->vx = ((PXL8_RANDF()) - 0.5f) * 40.0f; + p->max_life = 1.5f + pxl8_rng_f32(particles->rng) * 1.5f; + p->vy = -80.0f - pxl8_rng_f32(particles->rng) * 120.0f; + p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 40.0f; } static void fire_update(pxl8_particle* p, f32 dt, void* userdata) { @@ -376,13 +378,12 @@ void pxl8_vfx_fire(pxl8_particles* particles, i32 x, i32 y, i32 width, u8 palett particles->userdata = (void*)(uintptr_t)palette_start; } -static void rain_spawn(pxl8_particle* p, void* userdata) { - (void)userdata; - p->start_color = 27 + (rand() % 3); +static void rain_spawn(pxl8_particles* particles, pxl8_particle* p) { + p->start_color = 27 + pxl8_rng_range(particles->rng, 0, 3); p->end_color = 29; p->color = p->start_color; p->max_life = 2.0f; - p->vy = 200.0f + (PXL8_RANDF()) * 100.0f; + p->vy = 200.0f + pxl8_rng_f32(particles->rng) * 100.0f; } void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) { @@ -400,15 +401,15 @@ void pxl8_vfx_rain(pxl8_particles* particles, i32 width, f32 wind) { particles->update_fn = NULL; } -static void smoke_spawn(pxl8_particle* p, void* userdata) { - uintptr_t base_color = (uintptr_t)userdata; +static void smoke_spawn(pxl8_particles* particles, pxl8_particle* p) { + uintptr_t base_color = (uintptr_t)particles->userdata; p->start_color = base_color; p->end_color = base_color + 4; p->color = p->start_color; - p->max_life = 3.0f + (PXL8_RANDF()) * 2.0f; - p->vy = -20.0f - (PXL8_RANDF()) * 30.0f; - p->vx = ((PXL8_RANDF()) - 0.5f) * 20.0f; - p->size = 1.0f + (PXL8_RANDF()) * 2.0f; + p->max_life = 3.0f + pxl8_rng_f32(particles->rng) * 2.0f; + p->vy = -20.0f - pxl8_rng_f32(particles->rng) * 30.0f; + p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 20.0f; + p->size = 1.0f + pxl8_rng_f32(particles->rng) * 2.0f; } void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color) { @@ -427,14 +428,13 @@ void pxl8_vfx_smoke(pxl8_particles* particles, i32 x, i32 y, u8 color) { particles->userdata = (void*)(uintptr_t)color; } -static void snow_spawn(pxl8_particle* p, void* userdata) { - (void)userdata; - p->start_color = 8 + (rand() % 3); +static void snow_spawn(pxl8_particles* particles, pxl8_particle* p) { + p->start_color = 8 + pxl8_rng_range(particles->rng, 0, 3); p->end_color = 10; p->color = p->start_color; p->max_life = 4.0f; - p->vx = ((PXL8_RANDF()) - 0.5f) * 20.0f; - p->vy = 30.0f + (PXL8_RANDF()) * 20.0f; + p->vx = (pxl8_rng_f32(particles->rng) - 0.5f) * 20.0f; + p->vy = 30.0f + pxl8_rng_f32(particles->rng) * 20.0f; } void pxl8_vfx_snow(pxl8_particles* particles, i32 width, f32 wind) { @@ -452,15 +452,15 @@ void pxl8_vfx_snow(pxl8_particles* particles, i32 width, f32 wind) { particles->update_fn = NULL; } -static void sparks_spawn(pxl8_particle* p, void* userdata) { - uintptr_t base_color = (uintptr_t)userdata; +static void sparks_spawn(pxl8_particles* particles, pxl8_particle* p) { + uintptr_t base_color = (uintptr_t)particles->userdata; p->start_color = base_color; p->end_color = base_color > 2 ? base_color - 2 : 0; p->color = p->start_color; - p->max_life = 0.5f + (PXL8_RANDF()) * 1.0f; - - f32 angle = (PXL8_RANDF()) * 6.28f; - f32 speed = 100.0f + (PXL8_RANDF()) * 200.0f; + p->max_life = 0.5f + pxl8_rng_f32(particles->rng) * 1.0f; + + f32 angle = pxl8_rng_f32(particles->rng) * PXL8_TAU; + f32 speed = 100.0f + pxl8_rng_f32(particles->rng) * 200.0f; p->vx = cosf(angle) * speed; p->vy = sinf(angle) * speed - 50.0f; } @@ -492,13 +492,13 @@ void pxl8_vfx_starfield(pxl8_particles* particles, f32 speed, f32 spread) { for (u32 i = 0; i < particles->max_count; i++) { pxl8_particle* p = &particles->particles[i]; - p->x = (PXL8_RANDF()) * spread * 2.0f - spread; - p->y = (PXL8_RANDF()) * spread * 2.0f - spread; - p->z = (PXL8_RANDF()) * spread; + p->x = pxl8_rng_f32(particles->rng) * spread * 2.0f - spread; + p->y = pxl8_rng_f32(particles->rng) * spread * 2.0f - spread; + p->z = pxl8_rng_f32(particles->rng) * spread; p->vz = -speed; p->life = 1000.0f; p->max_life = 1000.0f; - p->color = 8 + (rand() % 8); + p->color = 8 + pxl8_rng_range(particles->rng, 0, 8); p->flags = 1; } } diff --git a/src/pxl8_vfx.h b/src/pxl8_vfx.h index 14db362..bee53b6 100644 --- a/src/pxl8_vfx.h +++ b/src/pxl8_vfx.h @@ -1,6 +1,7 @@ #pragma once #include "pxl8_gfx.h" +#include "pxl8_rng.h" #include "pxl8_types.h" typedef struct pxl8_particles pxl8_particles; @@ -34,7 +35,7 @@ typedef struct pxl8_raster_bar { extern "C" { #endif -pxl8_particles* pxl8_particles_create(u32 max_count); +pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng); void pxl8_particles_destroy(pxl8_particles* particles); void pxl8_particles_clear(pxl8_particles* particles);