improve script hot reload

This commit is contained in:
asrael 2026-01-08 01:19:25 -06:00
parent 01d6e09a91
commit 15041984f1
25 changed files with 1516 additions and 293 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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);

View file

@ -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) {

View file

@ -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]);
}
}
}

View file

@ -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,

View file

@ -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;

View file

@ -4,6 +4,7 @@
#include <string.h>
#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;

View file

@ -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]);
}
}
}

View file

@ -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;

619
src/pxl8_replay.c Normal file
View file

@ -0,0 +1,619 @@
#include "pxl8_replay.h"
#include "pxl8_log.h"
#include "pxl8_sys.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

84
src/pxl8_replay.h Normal file
View file

@ -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

24
src/pxl8_rng.c Normal file
View file

@ -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);
}

20
src/pxl8_rng.h Normal file
View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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,
};

View file

@ -3,8 +3,7 @@
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#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;
}

View file

@ -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);

View file

@ -3,10 +3,11 @@
#include <stdlib.h>
#include <string.h>
#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;
}
}

View file

@ -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);