Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dd459e2
Add scriptable multi-channel WebM playback
DCurrent Aug 12, 2026
08b162c
Add forced WebM channel selection
DCurrent Aug 12, 2026
b892b63
Refactor WebM playback behind movie API
DCurrent Aug 12, 2026
ea51537
Route legacy WebM playback through movie API
DCurrent Aug 12, 2026
cae209a
Prevent movie rewind from stalling on audio
DCurrent Aug 13, 2026
1c6df0e
Fix WebM rewind and seek audio synchronization
DCurrent Aug 13, 2026
8f1dea8
Fix WebM playback audio synchronization regressions
DCurrent Aug 13, 2026
475a06e
Separate movie playback from screen composition
DCurrent Aug 13, 2026
5901cf0
Add adaptive caching mode for movie sources
DCurrent Aug 13, 2026
af2019e
Fix WebM playback initialization and stream I/O
DCurrent Aug 13, 2026
e200279
Harden audio-video time sync.
DCurrent Aug 14, 2026
ea4a479
Increase streamed PCM reserve and preroll WebM audio.
DCurrent Aug 15, 2026
d73fb4a
Optimize WebM audio buffering and scaled movie composition
DCurrent Aug 15, 2026
b96337d
Reduce WebM starvation under high-resolution playback
DCurrent Aug 15, 2026
a1e6358
Bound WebM frame catch-up per engine update
DCurrent Aug 16, 2026
b6b1822
Fix cross-thread SDL audio locking
DCurrent Aug 17, 2026
d344019
Make owned audio clock the video master
DCurrent Aug 17, 2026
db025c7
Replace WebM compressed backlog recovery
DCurrent Aug 17, 2026
70cc5c0
Make WebM decoder lifecycle asynchronous
DCurrent Aug 17, 2026
455c624
Disable reverse movie playback
DCurrent Aug 17, 2026
ae48540
Tune WebM decoder reserves
DCurrent Aug 17, 2026
702264c
Fix misnamed onblocka script -> onblocky.
DCurrent Aug 17, 2026
5eaadce
Fix reversed plane/index arguments for onblockwscript.
DCurrent Aug 17, 2026
0c44ea6
Add jitter-tolerant WebM frame presentation
DCurrent Aug 18, 2026
cfdcb8e
Stop WebM decoder from discarding read-ahead frames
DCurrent Aug 18, 2026
48e32ef
Polish WebM diagnostics and comments
DCurrent Aug 18, 2026
218910c
Merge pull request #361 from DCurrent/webn-playback-refactor
DCurrent Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 77 additions & 57 deletions engine/openbor.c
Original file line number Diff line number Diff line change
Expand Up @@ -1927,10 +1927,10 @@ void execute_onblockz_script(entity *ent)
}
}

void execute_onblocka_script(entity *ent, entity *other)
void execute_onblocky_script(entity *ent, entity *other)
{
ScriptVariant tempvar;
Script *cs = ent->scripts->onblocka_script;
Script *cs = ent->scripts->onblocky_script;
if(Script_IsInitialized(cs))
{
ScriptVariant_Init(&tempvar);
Expand Down Expand Up @@ -17256,8 +17256,8 @@ s_model *load_cached_model(char *name, char *owner, char unload)
case CMD_MODEL_ONBLOCKZSCRIPT:
pos += lcmHandleCommandScripts(&arglist, buf + pos, newchar->scripts->onblockz_script, "onblockzscript", filename, 1, 0);
break;
case CMD_MODEL_ONBLOCKASCRIPT:
pos += lcmHandleCommandScripts(&arglist, buf + pos, newchar->scripts->onblocka_script, "onblockascript", filename, 1, 0);
case CMD_MODEL_ONBLOCKYSCRIPT:
pos += lcmHandleCommandScripts(&arglist, buf + pos, newchar->scripts->onblocky_script, "onblockyscript", filename, 1, 0);
break;
case CMD_MODEL_ONMOVEXSCRIPT:
pos += lcmHandleCommandScripts(&arglist, buf + pos, newchar->scripts->onmovex_script, "onmovexscript", filename, 1, 0);
Expand Down Expand Up @@ -31837,7 +31837,7 @@ void check_gravity(entity *e)
{
self->velocity.y = 0;
self->hithead = other;
execute_onblocka_script(self, other);
execute_onblocky_script(self, other);
}
}
else
Expand Down Expand Up @@ -39974,13 +39974,13 @@ int common_trymove(float xdir, float zdir)
{
xdir = 0;
if ( self->falling && (self->modeldata.hitwalltype < 0 || (self->modeldata.hitwalltype >= 0 && level->walls[wall].type == self->modeldata.hitwalltype)) ) hit |= 1;
execute_onblockw_script(self, &level->walls[wall], PLANE_X, wall);
execute_onblockw_script(self, &level->walls[wall], wall, PLANE_X);
}
if(zdir && (wall = checkwall_below(self->position.x, z, T_MAX_CHECK_ALTITUDE)) >= 0 && level->walls[wall].height > self->position.y)
{
zdir = 0;
if ( self->falling && (self->modeldata.hitwalltype < 0 || (self->modeldata.hitwalltype >= 0 && level->walls[wall].type == self->modeldata.hitwalltype)) ) hit |= 1;
execute_onblockw_script(self, &level->walls[wall], PLANE_Z, wall);
execute_onblockw_script(self, &level->walls[wall], wall, PLANE_Z);
}

if ( hit && !self->hitwall && validanim(self, ANI_HITWALL) ) ent_set_anim(self, ANI_HITWALL, 0);
Expand Down Expand Up @@ -50914,6 +50914,11 @@ void update(int ingame, int usevwait)
getinterval();
if(playrecstatus->status == A_REC_PLAY && !_pause && level) if ( !playRecordedInputs() ) stopRecordInputs();
inputrefresh(playrecstatus->status);
#ifdef WEBM
movie_playback_update(
(bothnewkeys & (FLAG_ESC | FLAG_ANYBUTTON)) != 0
);
#endif
if(playrecstatus->status == A_REC_REC && !_pause && level) if ( !recordInputs() ) stopRecordInputs();

if ((!_pause && ingame == 1) || alwaysupdate)
Expand Down Expand Up @@ -51861,71 +51866,86 @@ int playgif(char *filename, int x, int y, int noskip)


#ifdef WEBM
// Returns 0 on error, -1 on escape
/*
* Caskey, Damon V.
* 2026-08-12
*
* Preserve blocking playwebm() behavior as a wrapper around
* the reusable movie APIs. Return 1 on completion, 0 on error,
* or -1 when creator-enabled input interrupts playback.
*/
int playwebm(const char *path, int noskip)
{
int retval = 1;
webm_context *ctx = NULL;
yuv_video_mode info;
s_screen *rgb_frame = NULL;

ctx = webm_start_playback(path, savedata.musicvol);
if(ctx == NULL) {retval=0; goto quit;}

// set video output to YUV mode
webm_get_video_info(ctx, &info);
int status = video_setup_yuv_overlay(&info);
if(!status) {retval=0; goto quit;}
int source_id = -1;
s_movie_playback *playback = NULL;

// allocate s_screen for screenshot capture
yuv_init(2);
rgb_frame = allocscreen(info.width, info.height, PIXEL_16);
if(!rgb_frame) {retval=0; goto quit;}
movie_playback_stop_all();
source_id = movie_source_load(path, MOVIE_LOADING_STREAM);
if(source_id < 0) {
return 0;
}
playback = movie_playback_play(
source_id,
MOVIE_CHANNEL_AUTO,
savedata.musicvol,
true
);
if(!playback) {
retval = 0;
goto quit;
}
if(!movie_playback_set_interrupt(playback, !noskip)) {
retval = 0;
goto quit;
}

u64 start_time = timer_uticks();
u64 next_frame_time = 0;
yuv_frame *frame = NULL;
clearscreen(vscreen);
video_copy_screen(vscreen);
while(playback->active) {
int interrupt_requested;
int present_frame;

while(1)
{
inputrefresh(playrecstatus->status);
if(!noskip && (bothnewkeys & (FLAG_ESC | FLAG_ANYBUTTON)))
{
interrupt_requested =
(bothnewkeys & (FLAG_ESC | FLAG_ANYBUTTON)) != 0;
if(interrupt_requested && playback->interrupt) {
retval = -1;
yuv_frame_destroy(frame);
break;
}
else if(frame && !noscreenshot && (bothnewkeys & FLAG_SCREENSHOT))
{
yuv_to_rgb(frame, rgb_frame);
screenshot(rgb_frame, NULL, 0);
movie_playback_update(interrupt_requested);
if(!playback->active) {
break;
}

u64 time_passed = timer_uticks() - start_time;

if(next_frame_time <= time_passed)
{
// display the current frame
if(frame)
{
video_display_yuv_frame();
yuv_frame_destroy(frame);
present_frame = playback->frame_dirty;
if(present_frame) {
clearscreen(vscreen);
if(!movie_playback_draw_to_screen(vscreen, playback->index)) {
retval = 0;
break;
}

// prepare the next frame for display
frame = webm_get_next_frame(ctx);
if(frame == NULL) break;
video_prepare_yuv_frame(frame);
next_frame_time = frame->timestamp / 1000;
}
else usleep(next_frame_time - time_passed);
if(playback->current_frame &&
!noscreenshot &&
(bothnewkeys & FLAG_SCREENSHOT)) {
screenshot(vscreen, getpal, 1);
}
if(present_frame) {
video_copy_screen(vscreen);
}
usleep(1000);
}

quit:
if(ctx) webm_close(ctx);
if(rgb_frame) freescreen(&rgb_frame);
yuv_clear();
video_set_mode(videomodes);
if(playback && playback->failed && retval > 0) {
retval = 0;
}
if(playback && playback->active) {
movie_playback_stop(playback);
}
while(source_id >= 0 && !movie_source_unload(source_id)) {
movie_playback_update(0);
usleep(1000);
}
return retval;
}
#endif
Expand Down
5 changes: 3 additions & 2 deletions engine/openbor.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
#ifdef WEBM
#include "yuv.h"
#include "vidplay.h"
#include "movie_playback.h"
#endif

/*
Expand Down Expand Up @@ -2975,7 +2976,7 @@ typedef struct
Script *onblockp_script; //execute when blocked by platform.
Script *onblocko_script; //execute when blocked by obstacle.
Script *onblockz_script; //execute when blocked by Z.
Script *onblocka_script; //execute when "hit head".
Script *onblocky_script; //execute when "hit head".
Script *onmovex_script; //execute when moving along X axis.
Script *onmovez_script; //execute when moving along Z axis.
Script *onmovea_script; //execute when moving along A axis.
Expand Down Expand Up @@ -4458,7 +4459,7 @@ void execute_onblockw_script (entity *ent, s_terrain *wall, i
void execute_onblockp_script (entity *ent, int plane, entity *platform);
void execute_onblocko_script (entity *ent, int plane, entity *other);
void execute_onblockz_script (entity *ent);
void execute_onblocka_script (entity *ent, entity *other);
void execute_onblocky_script (entity *ent, entity *other);
void execute_onmovex_script (entity *ent);
void execute_onmovez_script (entity *ent);
void execute_onmovea_script (entity *ent);
Expand Down
5 changes: 4 additions & 1 deletion engine/openborscript.c
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ void _freeheapnode(void *ptr)
void Script_Global_Clear()
{
int i, size;
#ifdef WEBM
movie_playback_shutdown();
#endif
List_Clear(&theFunctionList);
// dump all un-freed variants
size = List_GetSize(&scriptheap);
Expand Down Expand Up @@ -15865,7 +15868,7 @@ HRESULT openbor_hallfame(ScriptVariant **varlist , ScriptVariant **pretvar, int
HRESULT openbor_playwebm(ScriptVariant **varlist , ScriptVariant **pretvar, int paramCount)
{
LONG temp = 0; //noskip
extern int playwebm(char * filename, int noskip); // avoid implicit declaration
extern int playwebm(const char *filename, int noskip); // avoid implicit declaration

if(paramCount < 1)
{
Expand Down
41 changes: 8 additions & 33 deletions engine/sdl/sblaster.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ static SDL_AudioSpec cspec;
static SDL_AudioDeviceID audio_dev;
static int voicevol = 15;
static int buffsize = 4096;
static unsigned int audio_lock_depth;

static void callback(void *userdata, Uint8 *stream, int len)
{
Expand Down Expand Up @@ -80,55 +79,31 @@ void SB_playstop()
return;
}

started = 0;
//SDL_CloseAudio();
SDL_CloseAudioDevice(audio_dev);
started = 0;
audio_dev = 0;
audio_lock_depth = 0;
}

/*
* Caskey, Damon V.
* 2026-08-04
* 2026-08-17
*
* Exclude the SDL callback while the main thread
* publishes or tears down shared mixer state. Nested
* engine helpers share one device lock.
* Delegate every lock pair to SDL. Its audio device
* mutex owns recursive depth per thread while excluding
* the callback and all other threads. A process-wide
* depth counter cannot represent that ownership safely.
*/
void SB_lock_audio()
{
if(!started)
{
return;
}

if(audio_lock_depth++ == 0)
{
SB_lock_audio_direct();
}
SB_lock_audio_direct();
}

void SB_unlock_audio()
{
if(!started || audio_lock_depth == 0)
{
return;
}

if(--audio_lock_depth == 0)
{
SB_unlock_audio_direct();
}
SB_unlock_audio_direct();
}

/*
* Caskey, Damon V.
* 2026-08-05
*
* Lock the SDL device without participating in the
* main-thread nesting counter. Live PCM producer
* threads use this pair exactly once per publication.
*/
void SB_lock_audio_direct()
{
if(started)
Expand Down
2 changes: 2 additions & 0 deletions engine/source/gamelib/sound_stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ int sound_stream_fill(
buffer->frame_count = frames_to_read;
buffer->terminal = !stream->looping && frames_to_read == available_frames;
buffer->ready = 1;
stream->ready_buffer_count++;
stream->next_source_frame += frames_to_read;
stream->write_buffer = (stream->write_buffer + 1U) % SOUND_STREAM_BUFFER_COUNT;
SB_unlock_audio();
Expand Down Expand Up @@ -273,6 +274,7 @@ int sound_stream_push_locked(
buffer->frame_count = frame_count;
buffer->terminal = terminal ? 1 : 0;
buffer->ready = 1;
stream->ready_buffer_count++;
stream->next_source_frame += frame_count;
stream->source_frame_count = stream->next_source_frame;
stream->producer_finished = terminal ? 1 : 0;
Expand Down
18 changes: 10 additions & 8 deletions engine/source/gamelib/sound_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@

/*
* Caskey, Damon V.
* 2026-08-01
* 2026-08-15
*
* Streamed channels use SOUND_STREAM_BUFFER_COUNT
* retained PCM buffers. At the largest supported input
* format, each SOUND_STREAM_BUFFER_SIZE buffer supplies
* approximately 57 milliseconds of data. Total reserve
* is approximately 57 milliseconds multiplied by
* SOUND_STREAM_BUFFER_COUNT.
* Streamed channels retain SOUND_STREAM_BUFFER_COUNT PCM buffers.
* Eight buffers absorb source I/O, decoder, and demux scheduling
* jitter without forcing the real-time mixer into silence. At the
* largest supported input format, each buffer supplies approximately
* 57 milliseconds of data.
*/
#define SOUND_STREAM_BUFFER_COUNT 4U
#define SOUND_STREAM_BUFFER_COUNT 8U
#define SOUND_STREAM_BUFFER_SIZE (16U * 1024U)
#define SOUND_STREAM_HANDLE_CLOSED (-1)

Expand All @@ -44,10 +43,13 @@ typedef struct s_sound_stream {
uint64_t source_frame_count;
uint64_t loop_start_frame;
uint64_t fp_buffer_position;
uint64_t underrun_count;
size_t block_align;
unsigned int ready_buffer_count;
int handle;
int looping;
int producer_finished;
int underrun_active;
} s_sound_stream;

typedef bool (*sound_stream_read_callback)(
Expand Down
Loading
Loading