From dd459e2edf424e2b6f15826739fdcba6d48e80e9 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Wed, 12 Aug 2026 18:37:34 -0400 Subject: [PATCH 01/26] Add scriptable multi-channel WebM playback --- engine/openbor.c | 7 + engine/openbor.h | 1 + engine/openborscript.c | 9 + engine/source/gamelib/soundmix.c | 56 +- engine/source/gamelib/soundmix.h | 3 + engine/source/gamelib/types.h | 4 +- engine/source/openborscript/config.h | 1 + engine/source/openborscript/constants.c | 28 + engine/source/openborscript/index.c | 42 ++ engine/source/openborscript/webm.c | 479 ++++++++++++ engine/source/openborscript/webm.h | 68 ++ engine/source/webmlib/vidplay.c | 646 +++++++++++++--- engine/source/webmlib/vidplay.h | 15 +- engine/source/webmlib/webm_playback.c | 954 ++++++++++++++++++++++++ engine/source/webmlib/webm_playback.h | 127 ++++ engine/source/webmlib/yuv.c | 35 +- 16 files changed, 2347 insertions(+), 128 deletions(-) create mode 100644 engine/source/openborscript/webm.c create mode 100644 engine/source/openborscript/webm.h create mode 100644 engine/source/webmlib/webm_playback.c create mode 100644 engine/source/webmlib/webm_playback.h diff --git a/engine/openbor.c b/engine/openbor.c index 328648652..e6a017d97 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -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 + webm_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) @@ -51869,6 +51874,7 @@ int playwebm(const char *path, int noskip) yuv_video_mode info; s_screen *rgb_frame = NULL; + webm_playback_stop_all(); ctx = webm_start_playback(path, savedata.musicvol); if(ctx == NULL) {retval=0; goto quit;} @@ -51925,6 +51931,7 @@ int playwebm(const char *path, int noskip) if(ctx) webm_close(ctx); if(rgb_frame) freescreen(&rgb_frame); yuv_clear(); + webm_playback_restore_yuv(); video_set_mode(videomodes); return retval; } diff --git a/engine/openbor.h b/engine/openbor.h index 030563a35..0f2d9065b 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -54,6 +54,7 @@ #ifdef WEBM #include "yuv.h" #include "vidplay.h" +#include "webm_playback.h" #endif /* diff --git a/engine/openborscript.c b/engine/openborscript.c index 7a1163aee..94b0b34b0 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -416,6 +416,12 @@ void _freeheapnode(void *ptr) free(((s_sprite *)ptr)->mask); } } +#ifdef WEBM + else if(((s_screen *)ptr)->magic == screen_magic) + { + webm_playback_detach_screen((s_screen *)ptr); + } +#endif free(ptr); } @@ -423,6 +429,9 @@ void _freeheapnode(void *ptr) void Script_Global_Clear() { int i, size; +#ifdef WEBM + webm_playback_shutdown(); +#endif List_Clear(&theFunctionList); // dump all un-freed variants size = List_GetSize(&scriptheap); diff --git a/engine/source/gamelib/soundmix.c b/engine/source/gamelib/soundmix.c index 538fd64da..df48fdc05 100644 --- a/engine/source/gamelib/soundmix.c +++ b/engine/source/gamelib/soundmix.c @@ -76,7 +76,7 @@ Caution: move vorbis headers here otherwise the structs will This way we don't need to increase the volume too much in the audio files, preventing distortions and quality loss */ #define MAX_SAMPLE_VOLUME 100 // 64 for backw. compat -#define MAX_MUSIC_VOLUME 60 // 64 for backw. compat +#define MAX_MUSIC_VOLUME SOUND_VOLUME_DIVISOR_MUSIC // 64 for backw. compat // Hardware settings for SoundBlaster (change only if latency is too big) #define SB_BUFFER_SIZE 0x8000 #define SB_BUFFER_SIZE_MASK 0x7FFF @@ -3124,6 +3124,60 @@ bool sound_set_channel_period(int channel, uint64_t period) { return true; } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Change producer stream speed only while its play ID +* still owns the requested generic sound channel. +*/ +bool sound_set_channel_speed_owned(int channel, int play_id, unsigned int speed) { + channelstruct *record; + + if(play_id < 0 || speed == 0) { + return false; + } + + SB_lock_audio(); + record = sound_channel_pool_get(&sound_channel_pool, channel); + if(!record || + record->playid != play_id || + record->stream_source != SOUND_CHANNEL_STREAM_SOURCE_PUSH || + record->frequency <= 0) { + SB_unlock_audio(); + return false; + } + + record->fp_period = sound_sample_period_calculate(speed, record->frequency); + SB_unlock_audio(); + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Pause a producer stream only while its play ID still +* identifies the live channel record. +*/ +bool sound_pause_channel_owned(int channel, int play_id, int toggle) { + channelstruct *record; + + SB_lock_audio(); + record = sound_channel_pool_get(&sound_channel_pool, channel); + if(play_id < 0 || + !record || + record->playid != play_id || + record->stream_source != SOUND_CHANNEL_STREAM_SOURCE_PUSH) { + SB_unlock_audio(); + return false; + } + + sound_channel_pool_pause(&sound_channel_pool, channel, toggle); + SB_unlock_audio(); + return true; +} + /* * Caskey, Damon V. * 2026-08-04 diff --git a/engine/source/gamelib/soundmix.h b/engine/source/gamelib/soundmix.h index 72929c4b1..4fc8586b0 100644 --- a/engine/source/gamelib/soundmix.h +++ b/engine/source/gamelib/soundmix.h @@ -26,6 +26,7 @@ #define SOUND_OUTPUT_BITS_DEFAULT 24 #define SOUND_OUTPUT_FREQUENCY_DEFAULT 48000 #define SOUND_CHANNEL_MUSIC_DEFAULT 0 +#define SOUND_VOLUME_DIVISOR_MUSIC 60 #define SOUND_PLAY_CHANCE_MAX 100U #define CHANNEL_PLAYING 1 #define CHANNEL_LOOPING 2 @@ -162,6 +163,8 @@ size_t sound_group_set_position( void sound_volume_sample(int channel, int lvolume, int rvolume); bool sound_set_channel_loop_offset(int channel, uint64_t loop_start_frame); bool sound_set_channel_period(int channel, uint64_t period); +bool sound_set_channel_speed_owned(int channel, int play_id, unsigned int speed); +bool sound_pause_channel_owned(int channel, int play_id, int toggle); bool sound_set_channel_priority(int channel, unsigned int priority); bool sound_set_channel_position(int channel, uint64_t sample_position); bool sound_set_channel_volume(int channel, unsigned int spatial_channel, int volume); diff --git a/engine/source/gamelib/types.h b/engine/source/gamelib/types.h index 6bdf14dbd..4d85d703a 100644 --- a/engine/source/gamelib/types.h +++ b/engine/source/gamelib/types.h @@ -115,7 +115,8 @@ typedef enum e_object_type { OBJECT_TYPE_FLASH, OBJECT_TYPE_GLOBAL_CONFIG, OBJECT_TYPE_MODEL, - OBJECT_TYPE_SOUND + OBJECT_TYPE_SOUND, + OBJECT_TYPE_WEBM } e_object_type; typedef struct @@ -352,4 +353,3 @@ typedef struct #endif #endif - diff --git a/engine/source/openborscript/config.h b/engine/source/openborscript/config.h index c4535e3f2..44c186292 100644 --- a/engine/source/openborscript/config.h +++ b/engine/source/openborscript/config.h @@ -29,6 +29,7 @@ #include "recursive_effect.h" #include "spawn_hud.h" #include "status_dial.h" +#include "webm.h" //osc diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index 34a00d87c..3ff4bf4ac 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1142,6 +1142,34 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) ICMPCONST(SOUND_PROPERTY_OWNER_ID) ICMPCONST(SOUND_PROPERTY_END) + /* WebM playback object property access. */ + ICMPCONST(WEBM_PROPERTY_ACTIVE) + ICMPCONST(WEBM_PROPERTY_CHANNEL) + ICMPCONST(WEBM_PROPERTY_DISPLAY_MODE) + ICMPCONST(WEBM_PROPERTY_DURATION) + ICMPCONST(WEBM_PROPERTY_HEIGHT) + ICMPCONST(WEBM_PROPERTY_INTERRUPT) + ICMPCONST(WEBM_PROPERTY_LOADING) + ICMPCONST(WEBM_PROPERTY_OFFSET_X) + ICMPCONST(WEBM_PROPERTY_OFFSET_Y) + ICMPCONST(WEBM_PROPERTY_PAUSED) + ICMPCONST(WEBM_PROPERTY_POSITION) + ICMPCONST(WEBM_PROPERTY_REPEAT) + ICMPCONST(WEBM_PROPERTY_SCREEN) + ICMPCONST(WEBM_PROPERTY_SOUND_CHANNEL) + ICMPCONST(WEBM_PROPERTY_SPEED) + ICMPCONST(WEBM_PROPERTY_WIDTH) + ICMPCONST(WEBM_PROPERTY_END) +#ifdef WEBM + ICMPCONST(WEBM_PLAYBACK_CHANNEL_COUNT) + ICMPCONST(WEBM_DISPLAY_CLIP) + ICMPCONST(WEBM_DISPLAY_FILL) + ICMPCONST(WEBM_DISPLAY_END) + ICMPCONST(WEBM_LOADING_STREAM) + ICMPCONST(WEBM_LOADING_CACHE) + ICMPCONST(WEBM_LOADING_END) +#endif + /* Key flags. */ ICMPCONST(FLAG_ANYBUTTON) ICMPCONST(FLAG_ATTACK) diff --git a/engine/source/openborscript/index.c b/engine/source/openborscript/index.c index c3fe37039..ce7bade8f 100644 --- a/engine/source/openborscript/index.c +++ b/engine/source/openborscript/index.c @@ -925,6 +925,34 @@ const char *Script_GetFunctionName(void *functionRef) { return "playwebm"; } + else if (functionRef == ((void *)openbor_open_webm)) + { + return "open_webm"; + } + else if (functionRef == ((void *)openbor_get_webm_channel_object)) + { + return "get_webm_channel_object"; + } + else if (functionRef == ((void *)openbor_get_webm_channel_index)) + { + return "get_webm_channel_index"; + } + else if (functionRef == ((void *)openbor_get_webm_channel_mask)) + { + return "get_webm_channel_mask"; + } + else if (functionRef == ((void *)openbor_get_webm_property)) + { + return "get_webm_property"; + } + else if (functionRef == ((void *)openbor_set_webm_property)) + { + return "set_webm_property"; + } + else if (functionRef == ((void *)openbor_stop_webm)) + { + return "stop_webm"; + } else if (functionRef == ((void *)openbor_playgif)) { return "playgif"; @@ -1755,6 +1783,20 @@ void Script_LoadSystemFunctions() (void *)openbor_menu_options, "options"); List_InsertAfter(&theFunctionList, (void *)openbor_playwebm, "playwebm"); + List_InsertAfter(&theFunctionList, + (void *)openbor_open_webm, "open_webm"); + List_InsertAfter(&theFunctionList, + (void *)openbor_get_webm_channel_object, "get_webm_channel_object"); + List_InsertAfter(&theFunctionList, + (void *)openbor_get_webm_channel_index, "get_webm_channel_index"); + List_InsertAfter(&theFunctionList, + (void *)openbor_get_webm_channel_mask, "get_webm_channel_mask"); + List_InsertAfter(&theFunctionList, + (void *)openbor_get_webm_property, "get_webm_property"); + List_InsertAfter(&theFunctionList, + (void *)openbor_set_webm_property, "set_webm_property"); + List_InsertAfter(&theFunctionList, + (void *)openbor_stop_webm, "stop_webm"); List_InsertAfter(&theFunctionList, (void *)openbor_playgif, "playgif"); List_InsertAfter(&theFunctionList, diff --git a/engine/source/openborscript/webm.c b/engine/source/openborscript/webm.c new file mode 100644 index 000000000..7771d8857 --- /dev/null +++ b/engine/source/openborscript/webm.c @@ -0,0 +1,479 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#include "scriptcommon.h" + +extern s_savedata savedata; + +#ifdef WEBM + +typedef struct s_webm_property_info { + e_webm_properties property; + e_property_access_config_flags config_flags; + size_t offset; + const char *id_string; + VARTYPE type; +} s_webm_property_info; + +#define PROPERTY_MEMBER_OFFSET(type, member) ((size_t)&(((type*)0)->member)) + +static const s_webm_property_info webm_properties[] = { + { WEBM_PROPERTY_ACTIVE, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_webm_playback, active), + "WEBM_PROPERTY_ACTIVE", VT_INTEGER }, + { WEBM_PROPERTY_CHANNEL, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_webm_playback, index), + "WEBM_PROPERTY_CHANNEL", VT_INTEGER }, + { WEBM_PROPERTY_DISPLAY_MODE, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, display_mode), + "WEBM_PROPERTY_DISPLAY_MODE", VT_INTEGER }, + { WEBM_PROPERTY_DURATION, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_webm_playback, duration), + "WEBM_PROPERTY_DURATION", VT_UINTEGER64 }, + { WEBM_PROPERTY_HEIGHT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, height), + "WEBM_PROPERTY_HEIGHT", VT_INTEGER }, + { WEBM_PROPERTY_INTERRUPT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, interrupt), + "WEBM_PROPERTY_INTERRUPT", VT_INTEGER }, + { WEBM_PROPERTY_LOADING, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, loading_mode), + "WEBM_PROPERTY_LOADING", VT_INTEGER }, + { WEBM_PROPERTY_OFFSET_X, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, offset_x), + "WEBM_PROPERTY_OFFSET_X", VT_INTEGER }, + { WEBM_PROPERTY_OFFSET_Y, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, offset_y), + "WEBM_PROPERTY_OFFSET_Y", VT_INTEGER }, + { WEBM_PROPERTY_PAUSED, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, paused), + "WEBM_PROPERTY_PAUSED", VT_INTEGER }, + { WEBM_PROPERTY_POSITION, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, position), + "WEBM_PROPERTY_POSITION", VT_UINTEGER64 }, + { WEBM_PROPERTY_REPEAT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, repeat), + "WEBM_PROPERTY_REPEAT", VT_INTEGER }, + { WEBM_PROPERTY_SCREEN, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, screen), + "WEBM_PROPERTY_SCREEN", VT_PTR }, + { WEBM_PROPERTY_SOUND_CHANNEL, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, sound_channel), + "WEBM_PROPERTY_SOUND_CHANNEL", VT_INTEGER }, + { WEBM_PROPERTY_SPEED, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, speed), + "WEBM_PROPERTY_SPEED", VT_DECIMAL }, + { WEBM_PROPERTY_WIDTH, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_webm_playback, width), + "WEBM_PROPERTY_WIDTH", VT_INTEGER }, + { WEBM_PROPERTY_END, PROPERTY_ACCESS_CONFIG_NONE, 0, + "WebM", VT_EMPTY } +}; + +#undef PROPERTY_MEMBER_OFFSET + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Build property metadata for a stable WebM channel object. +*/ +static const s_property_access_map webm_get_property_map( + const void *acting_object_param, + const unsigned int property_index_param +) +{ + s_property_access_map property_map = { 0 }; + const s_webm_playback *acting_object = acting_object_param; + const s_webm_property_info *info = NULL; + size_t property_cursor; + + for(property_cursor = 0; + property_cursor < sizeof(webm_properties) / sizeof(webm_properties[0]); + property_cursor++) { + if(webm_properties[property_cursor].property == property_index_param) { + info = &webm_properties[property_cursor]; + break; + } + } + + if(info) { + property_map.config_flags = info->config_flags; + property_map.field = (const void*)((const char*)acting_object + info->offset); + property_map.id_string = info->id_string; + property_map.type = info->type; + } else { + property_map.config_flags = PROPERTY_ACCESS_CONFIG_NONE; + property_map.field = NULL; + property_map.id_string = "WebM"; + property_map.type = VT_EMPTY; + } + return property_map; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Start nonblocking playback into a creator-owned 32-bit +* screen and return the acquired channel object. +*/ +HRESULT openbor_open_webm( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + const char *self_name = + "open_webm(string path, void screen, int sound_channel, int interrupt, int loading)"; + s_webm_playback *playback; + s_screen *screen; + LONG sound_channel = SOUND_CHANNEL_MUSIC_DEFAULT; + LONG interrupt = 1; + LONG loading_mode = WEBM_LOADING_STREAM; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 2 || + varlist[0]->vt != VT_STR || + varlist[1]->vt != VT_PTR || + !varlist[1]->ptrVal || + (paramCount > 2 && FAILED(ScriptVariant_IntegerValue(varlist[2], &sound_channel))) || + (paramCount > 3 && FAILED(ScriptVariant_IntegerValue(varlist[3], &interrupt))) || + (paramCount > 4 && FAILED(ScriptVariant_IntegerValue(varlist[4], &loading_mode))) || + sound_channel < 0 || + (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || + loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { + printf("\nScript error: %s. Invalid path, screen, sound channel, interrupt, or loading mode.\n", + self_name); + *pretvar = NULL; + return E_FAIL; + } + + screen = (s_screen*)varlist[1]->ptrVal; + playback = webm_playback_open( + StrCache_Get(varlist[0]->strVal), + screen, + (int)sound_channel, + interrupt != 0, + (e_webm_loading_mode)loading_mode, + savedata.musicvol + ); + if(playback) { + ScriptVariant_ChangeType(*pretvar, VT_PTR); + (*pretvar)->ptrVal = playback; + return S_OK; + } + + printf("\nScript error: %s. Playback could not acquire a channel, screen, or decoder.\n", + self_name); + *pretvar = NULL; + return E_FAIL; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Return one stable object from the single 64-channel bank. +*/ +HRESULT openbor_get_webm_channel_object( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + LONG channel; + s_webm_playback *playback; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 1 || + FAILED(ScriptVariant_IntegerValue(varlist[0], &channel)) || + channel < 0 || + (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT || + !webm_playback_init()) { + printf("\nScript error: get_webm_channel_object(int channel). Channel must be from 0 through %u.\n", + WEBM_PLAYBACK_CHANNEL_COUNT - 1U); + *pretvar = NULL; + return E_FAIL; + } + + playback = webm_playback_get((int)channel); + if(playback) { + ScriptVariant_ChangeType(*pretvar, VT_PTR); + (*pretvar)->ptrVal = playback; + } + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Resolve a validated playback pointer to its channel index. +*/ +HRESULT openbor_get_webm_channel_index( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + int channel; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 1 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + (channel = webm_playback_get_index(varlist[0]->ptrVal)) < 0) { + printf("\nScript error: get_webm_channel_index(void playback). You must provide a valid WebM object.\n"); + *pretvar = NULL; + return E_FAIL; + } + + ScriptVariant_ChangeType(*pretvar, VT_INTEGER); + (*pretvar)->lVal = (LONG)channel; + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Return the active mask for the single 64-channel video bank. +*/ +HRESULT openbor_get_webm_channel_mask( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + (void)varlist; + (void)paramCount; + + ScriptVariant_ChangeType(*pretvar, VT_UINTEGER64); + (*pretvar)->ullVal = webm_playback_get_active_mask(); + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Return a synchronized snapshot property or diagnostic dump. +*/ +HRESULT openbor_get_webm_property( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + s_webm_playback snapshot; + int property_index; + s_property_access_map property_map; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 2 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + varlist[1]->vt != VT_INTEGER || + !webm_playback_get_snapshot(varlist[0]->ptrVal, &snapshot)) { + printf("\nScript error: get_webm_property(void playback, int property). Invalid object or property.\n"); + *pretvar = NULL; + return E_FAIL; + } + + property_index = (int)varlist[1]->lVal; + if(property_index >= 0 && property_index < WEBM_PROPERTY_END) { + property_map = webm_get_property_map(&snapshot, (unsigned int)property_index); + return property_access_get_member(&property_map, *pretvar); + } + if(property_index == PROPERTY_ACCESS_DUMP) { + property_access_dump_members( + webm_get_property_map, + WEBM_PROPERTY_END, + &snapshot + ); + return S_OK; + } + + printf("\nScript error: get_webm_property(void playback, int property). Unknown property id (%d).\n", + property_index); + *pretvar = NULL; + return E_FAIL; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Route mutable WebM fields through lifecycle-aware setters +* so seek, audio, clocks, scaling, and screen ownership agree. +*/ +HRESULT openbor_set_webm_property( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + s_webm_playback *playback; + int property_index; + LONG integer_value; + DOUBLE decimal_value; + uint64_t unsigned_value; + bool result = false; + + *pretvar = NULL; + if(paramCount < 3 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + varlist[1]->vt != VT_INTEGER || + webm_playback_get_index(varlist[0]->ptrVal) < 0) { + goto error_object; + } + + playback = varlist[0]->ptrVal; + property_index = (int)varlist[1]->lVal; + switch((e_webm_properties)property_index) { + case WEBM_PROPERTY_DISPLAY_MODE: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_display_mode(playback, (e_webm_display_mode)integer_value); + break; + case WEBM_PROPERTY_HEIGHT: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_height(playback, (int)integer_value); + break; + case WEBM_PROPERTY_INTERRUPT: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_interrupt(playback, integer_value != 0); + break; + case WEBM_PROPERTY_LOADING: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_loading_mode( + playback, + (e_webm_loading_mode)integer_value, + savedata.musicvol + ); + break; + case WEBM_PROPERTY_OFFSET_X: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_offset_x(playback, (int)integer_value); + break; + case WEBM_PROPERTY_OFFSET_Y: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_offset_y(playback, (int)integer_value); + break; + case WEBM_PROPERTY_PAUSED: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_paused(playback, integer_value != 0); + break; + case WEBM_PROPERTY_POSITION: + result = SUCCEEDED(ScriptVariant_Unsigned64Value(varlist[2], &unsigned_value)) && + webm_playback_set_position(playback, unsigned_value, savedata.musicvol); + break; + case WEBM_PROPERTY_REPEAT: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_repeat(playback, integer_value != 0); + break; + case WEBM_PROPERTY_SCREEN: + result = varlist[2]->vt == VT_PTR && varlist[2]->ptrVal && + webm_playback_set_screen(playback, varlist[2]->ptrVal); + break; + case WEBM_PROPERTY_SOUND_CHANNEL: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_sound_channel( + playback, + (int)integer_value, + savedata.musicvol + ); + break; + case WEBM_PROPERTY_SPEED: + result = SUCCEEDED(ScriptVariant_DecimalValue(varlist[2], &decimal_value)) && + webm_playback_set_speed(playback, decimal_value); + break; + case WEBM_PROPERTY_WIDTH: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + webm_playback_set_width(playback, (int)integer_value); + break; + case WEBM_PROPERTY_ACTIVE: + case WEBM_PROPERTY_CHANNEL: + case WEBM_PROPERTY_DURATION: + case WEBM_PROPERTY_END: + default: + printf("\nScript error: set_webm_property(void playback, int property, value). Property id %d is read-only or unknown.\n", + property_index); + return E_FAIL; + } + + if(result) { + return S_OK; + } + printf("\nScript error: set_webm_property(void playback, int property, value). Invalid value for property id %d.\n", + property_index); + return E_FAIL; + +error_object: + printf("\nScript error: set_webm_property(void playback, int property, value). Invalid WebM object or property.\n"); + return E_FAIL; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Explicitly stop and recycle one WebM playback channel. +*/ +HRESULT openbor_stop_webm( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + *pretvar = NULL; + if(paramCount < 1 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + webm_playback_get_index(varlist[0]->ptrVal) < 0) { + printf("\nScript error: stop_webm(void playback). You must provide a valid WebM object.\n"); + return E_FAIL; + } + + webm_playback_stop(varlist[0]->ptrVal); + return S_OK; +} + +#else + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Keep WebM script names available on builds that exclude +* the decoder while reporting the unsupported configuration. +*/ +static HRESULT openbor_webm_unsupported(ScriptVariant **pretvar) +{ + ScriptVariant_Clear(*pretvar); + printf("WebM playback is not supported on this platform.\n"); + return S_OK; +} + +HRESULT openbor_open_webm(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } +HRESULT openbor_get_webm_channel_object(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } +HRESULT openbor_get_webm_channel_index(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } +HRESULT openbor_get_webm_channel_mask(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } +HRESULT openbor_get_webm_property(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } +HRESULT openbor_set_webm_property(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } +HRESULT openbor_stop_webm(ScriptVariant **v, ScriptVariant **r, int c) +{ (void)v; (void)c; return openbor_webm_unsupported(r); } + +#endif diff --git a/engine/source/openborscript/webm.h b/engine/source/openborscript/webm.h new file mode 100644 index 000000000..3feca5bcc --- /dev/null +++ b/engine/source/openborscript/webm.h @@ -0,0 +1,68 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#ifndef OPENBORSCRIPT_WEBM_H +#define OPENBORSCRIPT_WEBM_H + +typedef enum e_webm_properties { + WEBM_PROPERTY_ACTIVE, + WEBM_PROPERTY_CHANNEL, + WEBM_PROPERTY_DISPLAY_MODE, + WEBM_PROPERTY_DURATION, /* Milliseconds, read only. */ + WEBM_PROPERTY_HEIGHT, + WEBM_PROPERTY_INTERRUPT, + WEBM_PROPERTY_LOADING, + WEBM_PROPERTY_OFFSET_X, + WEBM_PROPERTY_OFFSET_Y, + WEBM_PROPERTY_PAUSED, + WEBM_PROPERTY_POSITION, /* Milliseconds; writing seeks. */ + WEBM_PROPERTY_REPEAT, + WEBM_PROPERTY_SCREEN, + WEBM_PROPERTY_SOUND_CHANNEL, + WEBM_PROPERTY_SPEED, + WEBM_PROPERTY_WIDTH, + WEBM_PROPERTY_END +} e_webm_properties; + +HRESULT openbor_open_webm( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); +HRESULT openbor_get_webm_channel_object( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); +HRESULT openbor_get_webm_channel_index( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); +HRESULT openbor_get_webm_channel_mask( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); +HRESULT openbor_get_webm_property( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); +HRESULT openbor_set_webm_property( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); +HRESULT openbor_stop_webm( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +); + +#endif diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 338f1c91f..48de24c35 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -12,6 +12,7 @@ #include #include #include +#include #include "nestegg/nestegg.h" // libvpx @@ -29,16 +30,26 @@ #include "borendian.h" #include "soundmix.h" +extern int buffer_pakfile(const char *filename, char **pbuffer, size_t *psize); + // lowering these might save a bit of memory but could also cause lag #define PACKET_QUEUE_SIZE 20 #define FRAME_QUEUE_SIZE 10 #define debug_printf(...) //printf(__VA_ARGS__) +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Decoder queues and track contexts retain a pointer to +* their owning playback's independent stop state. +*/ typedef struct { int start; int size; int max_size; + volatile int *quit; bor_mutex *mutex; bor_cond *not_full; bor_cond *not_empty; @@ -53,6 +64,8 @@ typedef struct { int last_samples; int channel; int stream_play_id; + int output_active; + volatile int *quit; uint8_t pcm_buffer[SOUND_STREAM_BUFFER_SIZE]; } audio_context; @@ -65,11 +78,19 @@ typedef struct { int display_width; int display_height; uint64_t frame_delay; + volatile int *quit; } video_context; +typedef struct { + int packhandle; + unsigned char *cache_buffer; + size_t cache_size; + size_t cache_position; +} webm_io_context; + // see header for typedef struct webm_context { - int packhandle; + webm_io_context io_ctx; nestegg *nestegg_ctx; audio_context audio_ctx; video_context video_ctx; @@ -79,39 +100,117 @@ struct webm_context { bor_thread *the_demux_thread; bor_thread *the_video_thread; bor_thread *the_audio_thread; + uint64_t duration; + volatile int quit; }; +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Read or seek either a creator-requested memory cache or +* the ordinary packfile stream through the same Nestegg I/O. +*/ +static int webm_io_read(void *buffer, size_t length, void *userdata) +{ + webm_io_context *io_ctx = userdata; + int bytesRead; -static int quit_video; + if(io_ctx->cache_buffer) { + size_t remaining = io_ctx->cache_size - io_ctx->cache_position; -int webm_read(void *buffer, size_t length, void *userdata) -{ - int bytesRead = readpackfile((int)(size_t)userdata, buffer, length); + if(remaining < length) { + return 0; + } + memcpy( + buffer, + io_ctx->cache_buffer + io_ctx->cache_position, + length + ); + io_ctx->cache_position += length; + return 1; + } + + if(length > (size_t)INT_MAX) { + return -1; + } + bytesRead = readpackfile(io_ctx->packhandle, buffer, (int)length); if (bytesRead < 0) return -1; - else if (bytesRead == 0) return 0; + else if ((size_t)bytesRead != length) return 0; else return 1; } -int webm_seek(int64_t offset, int whence, void *userdata) +static int webm_io_seek(int64_t offset, int whence, void *userdata) { - return seekpackfile((int)(size_t)userdata, (int)offset, whence); + webm_io_context *io_ctx = userdata; + + if(io_ctx->cache_buffer) { + int64_t base; + int64_t position; + + switch(whence) { + case SEEK_SET: + base = 0; + break; + case SEEK_CUR: + base = (int64_t)io_ctx->cache_position; + break; + case SEEK_END: + base = (int64_t)io_ctx->cache_size; + break; + default: + return -1; + } + + position = base + offset; + if(position < 0 || (uint64_t)position > (uint64_t)io_ctx->cache_size) { + return -1; + } + io_ctx->cache_position = (size_t)position; + return 0; + } + + if(offset < INT_MIN || offset > INT_MAX) { + return -1; + } + return seekpackfile(io_ctx->packhandle, (int)offset, whence); } -int64_t webm_tell(void *userdata) +static int64_t webm_io_tell(void *userdata) { - return seekpackfile((int)(size_t)userdata, 0, SEEK_CUR); + webm_io_context *io_ctx = userdata; + + if(io_ctx->cache_buffer) { + return (int64_t)io_ctx->cache_position; + } + return seekpackfile(io_ctx->packhandle, 0, SEEK_CUR); } -FixedSizeQueue *queue_init(int max_size) +static FixedSizeQueue *queue_init(int max_size, volatile int *quit) { FixedSizeQueue *queue; + + if(max_size < 1 || !quit) { + return NULL; + } queue = malloc(sizeof(*queue) - sizeof(queue->data) + (max_size * sizeof(void *))); + if(!queue) { + return NULL; + } queue->start = 0; queue->size = 0; queue->max_size = max_size; + queue->quit = quit; queue->mutex = mutex_create(); queue->not_full = cond_create(); queue->not_empty = cond_create(); + if(!queue->mutex || !queue->not_full || !queue->not_empty) { + if(queue->not_full) cond_destroy(queue->not_full); + if(queue->not_empty) cond_destroy(queue->not_empty); + if(queue->mutex) mutex_destroy(queue->mutex); + free(queue); + return NULL; + } return queue; } @@ -127,7 +226,7 @@ int queue_insert(FixedSizeQueue *queue, void *data) { while(cond_wait_timed(queue->not_full, queue->mutex, 10) != 0) { - if (quit_video) + if (*queue->quit) { mutex_unlock(queue->mutex); return -1; @@ -155,7 +254,7 @@ void *queue_get(FixedSizeQueue *queue) { while (cond_wait_timed(queue->not_empty, queue->mutex, 10) != 0) { - if (quit_video) + if (*queue->quit) { mutex_unlock(queue->mutex); return NULL; @@ -173,6 +272,33 @@ void *queue_get(FixedSizeQueue *queue) return data; } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Remove one queued item without stalling the main +* engine thread when a decoder has not produced it yet. +*/ +static int queue_try_get(FixedSizeQueue *queue, void **data) +{ + if(!queue || !data) { + return 0; + } + + mutex_lock(queue->mutex); + if(queue->size == 0) { + mutex_unlock(queue->mutex); + return 0; + } + + *data = queue->data[queue->start]; + --queue->size; + queue->start = (queue->start + 1) % queue->max_size; + cond_signal(queue->not_full); + mutex_unlock(queue->mutex); + return 1; +} + void queue_destroy(FixedSizeQueue *queue) { cond_destroy(queue->not_full); @@ -184,7 +310,9 @@ void queue_destroy(FixedSizeQueue *queue) // used to keep playing current BGM in videos with no audio track static int bgm_update_thread(void *data) { - while (!quit_video) + webm_context *ctx = data; + + while (!ctx->quit) { sound_update_music(); usleep(5000); @@ -253,7 +381,7 @@ static int audio_thread(void *data) int queue_result; int terminal; - while(!quit_video) + while(!*audio_ctx->quit) { decoded_bytes = audio_decode_frame( audio_ctx, @@ -278,20 +406,32 @@ static int audio_thread(void *data) } #endif - do { - queue_result = sound_queue_channel_pcm_stream( - audio_ctx->channel, - audio_ctx->stream_play_id, - frame_count > 0 ? audio_ctx->pcm_buffer : NULL, - (uint64_t)frame_count, - terminal - ); - if(queue_result == 0) { - usleep(1000); + queue_result = 1; + if(audio_ctx->output_active) { + do { + queue_result = sound_queue_channel_pcm_stream( + audio_ctx->channel, + audio_ctx->stream_play_id, + frame_count > 0 ? audio_ctx->pcm_buffer : NULL, + (uint64_t)frame_count, + terminal + ); + if(queue_result == 0) { + usleep(1000); + } + } while(!*audio_ctx->quit && queue_result == 0); + + if(queue_result < 0) { + /* + * Keep draining this track when another producer replaces + * its sound channel. Otherwise the full audio packet queue + * would also stall video demuxing. + */ + audio_ctx->output_active = 0; } - } while(!quit_video && queue_result == 0); + } - if(queue_result < 0 || terminal) { + if(terminal) { return 0; } } @@ -299,29 +439,62 @@ static int audio_thread(void *data) return 0; } -static void init_audio(nestegg *ctx, int track, audio_context *audio_ctx, int volume) +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Initialize WebM audio on an explicit generic channel with +* play-ID ownership and optional legacy replace-all behavior. +*/ +static int init_audio( + nestegg *ctx, + int track, + audio_context *audio_ctx, + int volume, + int sound_channel, + int replace_all_audio, + volatile int *quit +) { // read vorbis header and initialize vorbis decoding unsigned chunk, chunks; + nestegg_audio_params audioParams; + vorbis_init(&(audio_ctx->vorbis_ctx)); - nestegg_track_codec_data_count(ctx, track, &chunks); - assert(chunks == 3); + if(nestegg_track_codec_data_count(ctx, track, &chunks) < 0 || chunks != 3) { + vorbis_destroy(&(audio_ctx->vorbis_ctx)); + return -1; + } for (chunk=0; chunkvorbis_ctx)); + return -1; + } vorbis_headerpacket(&(audio_ctx->vorbis_ctx), data, data_size, chunk); } // initialize audio decoding context vorbis_prepare(&(audio_ctx->vorbis_ctx)); - nestegg_audio_params audioParams; - nestegg_track_audio_params(ctx, track, &audioParams); + if(nestegg_track_audio_params(ctx, track, &audioParams) < 0 || + audioParams.rate < 1.0 || + audioParams.rate > (double)INT_MAX || + (audioParams.channels != CHANNEL_TYPE_MONO && + audioParams.channels != CHANNEL_TYPE_STEREO)) { + vorbis_destroy(&(audio_ctx->vorbis_ctx)); + return -1; + } audio_ctx->vorbis_ctx.channels = audioParams.channels; audio_ctx->frequency = (int)audioParams.rate; audio_ctx->avail_samples = audio_ctx->last_samples = 0; - audio_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE); + audio_ctx->quit = quit; + audio_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE, quit); + if(!audio_ctx->packet_queue) { + vorbis_destroy(&(audio_ctx->vorbis_ctx)); + return -1; + } printf("Audio track: %f Hz, %d channels, %d bits/sample\n", audioParams.rate, audioParams.channels, audioParams.depth / audioParams.channels); if(audio_ctx->frequency < SOUND_MUSIC_FREQUENCY_MIN || @@ -333,28 +506,53 @@ static void init_audio(nestegg *ctx, int track, audio_context *audio_ctx, int vo SOUND_MUSIC_FREQUENCY_MAX); } - /* WebM audio retains its legacy behavior by replacing all playback. */ - sound_stopall_sample(true); - audio_ctx->channel = SOUND_CHANNEL_MUSIC_DEFAULT; + if(replace_all_audio) { + /* Legacy fullscreen playback retains its replace-all behavior. */ + sound_stopall_sample(true); + } + audio_ctx->channel = sound_channel; audio_ctx->stream_play_id = sound_open_channel_pcm_stream( audio_ctx->channel, audio_ctx->frequency, audioParams.channels, volume ); - assert(audio_ctx->stream_play_id >= 0); - sound_volume_music(volume, volume); + if(audio_ctx->stream_play_id < 0) { + queue_destroy(audio_ctx->packet_queue); + audio_ctx->packet_queue = NULL; + vorbis_destroy(&(audio_ctx->vorbis_ctx)); + return -1; + } + + audio_ctx->output_active = 1; + sound_set_channel_volume( + audio_ctx->channel, + SOUND_SPATIAL_CHANNEL_LEFT, + volume + ); + sound_set_channel_volume( + audio_ctx->channel, + SOUND_SPATIAL_CHANNEL_RIGHT, + volume + ); + sound_set_channel_volume_divisor( + audio_ctx->channel, + SOUND_VOLUME_DIVISOR_MUSIC + ); + return 0; } static void close_audio(audio_context *audio_ctx) { // empty and free the packet queue - while(audio_ctx->packet_queue->size) + while(audio_ctx->packet_queue && audio_ctx->packet_queue->size) { nestegg_packet *packet = queue_get(audio_ctx->packet_queue); if(packet) nestegg_free_packet(packet); } - queue_destroy(audio_ctx->packet_queue); + if(audio_ctx->packet_queue) { + queue_destroy(audio_ctx->packet_queue); + } // close the vorbis decoding context vorbis_destroy(&(audio_ctx->vorbis_ctx)); @@ -370,14 +568,14 @@ static int video_thread(void *data) video_context *ctx = (video_context*) data; uint64_t timestamp; - while(!quit_video) + while(!*ctx->quit) { unsigned int chunk, chunks; nestegg_packet *pkt; debug_printf("video queue size=%i\n", ctx->packet_queue->size); pkt = queue_get(ctx->packet_queue); - if (quit_video || pkt == NULL) break; + if (*ctx->quit || pkt == NULL) break; nestegg_packet_count(pkt, &chunks); nestegg_packet_tstamp(pkt, ×tamp); @@ -392,14 +590,23 @@ static int video_thread(void *data) if (vpx_codec_decode(&ctx->vpx_ctx, data, data_size, NULL, 0)) { printf("Error: libvpx failed to decode chunk\n"); - quit_video = 1; + *ctx->quit = 1; break; } while((img = vpx_codec_get_frame(&ctx->vpx_ctx, &iter))) { - assert(img->d_w == ctx->width); - assert(img->d_h == ctx->height); + if(img->d_w != (unsigned int)ctx->width || + img->d_h != (unsigned int)ctx->height) { + printf("Error: WebM frame dimensions changed during playback\n"); + *ctx->quit = 1; + break; + } yuv_frame *frame = yuv_frame_create(img->d_w, img->d_h); + if(!frame) { + printf("Error: Unable to allocate a decoded WebM frame\n"); + *ctx->quit = 1; + break; + } frame->timestamp = timestamp; int y; @@ -419,6 +626,7 @@ static int video_thread(void *data) } timestamp += ctx->frame_delay; } + if(*ctx->quit) break; } nestegg_free_packet(pkt); } @@ -428,11 +636,35 @@ static int video_thread(void *data) } // returns 0 on success, -1 on error -static int init_video(nestegg *nestegg_ctx, int track, video_context *video_ctx) +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Initialize video queues against their owning decoder's +* stop state so multiple contexts can run independently. +*/ +static int init_video( + nestegg *nestegg_ctx, + int track, + video_context *video_ctx, + volatile int *quit +) { nestegg_video_params video_params; - nestegg_track_video_params(nestegg_ctx, track, &video_params); - assert(video_params.stereo_mode == NESTEGG_VIDEO_MONO); + if(nestegg_track_video_params(nestegg_ctx, track, &video_params) < 0 || + video_params.stereo_mode != NESTEGG_VIDEO_MONO || + video_params.width < 2 || + video_params.height < 2 || + (video_params.width & 1U) || + (video_params.height & 1U) || + video_params.width > (unsigned int)(INT_MAX >> 16) || + video_params.height > (unsigned int)(INT_MAX >> 16) || + video_params.width > (unsigned int)INT_MAX / video_params.height || + video_params.display_width > (unsigned int)INT_MAX || + video_params.display_height > (unsigned int)INT_MAX) { + printf("Error: Unsupported WebM video dimensions or stereo mode\n"); + return -1; + } if (vpx_codec_dec_init(&(video_ctx->vpx_ctx), vpx_codec_vp8_dx(), NULL, 0)) { @@ -443,13 +675,29 @@ static int init_video(nestegg *nestegg_ctx, int track, video_context *video_ctx) video_ctx->height = video_params.height; video_ctx->display_width = video_params.display_width; video_ctx->display_height = video_params.display_height; - nestegg_track_default_duration(nestegg_ctx, track, &(video_ctx->frame_delay)); + if(nestegg_track_default_duration( + nestegg_ctx, + track, + &(video_ctx->frame_delay) + ) < 0 || !video_ctx->frame_delay) { + vpx_codec_destroy(&(video_ctx->vpx_ctx)); + return -1; + } printf("Video track: resolution=%i*%i, display resolution=%i*%i, %.2f frames/second\n", video_params.width, video_params.height, video_params.display_width, video_params.display_height, 1000000000.0 / video_ctx->frame_delay); - video_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE); - video_ctx->frame_queue = queue_init(FRAME_QUEUE_SIZE); + video_ctx->quit = quit; + video_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE, quit); + video_ctx->frame_queue = queue_init(FRAME_QUEUE_SIZE, quit); + if(!video_ctx->packet_queue || !video_ctx->frame_queue) { + if(video_ctx->packet_queue) queue_destroy(video_ctx->packet_queue); + if(video_ctx->frame_queue) queue_destroy(video_ctx->frame_queue); + video_ctx->packet_queue = NULL; + video_ctx->frame_queue = NULL; + vpx_codec_destroy(&(video_ctx->vpx_ctx)); + return -1; + } return 0; } @@ -504,115 +752,214 @@ static int demux_thread(void *data) break; } } + else + { + nestegg_free_packet(pkt); + } - if (quit_video) break; + if (ctx->quit) break; } queue_insert(ctx->video_ctx.packet_queue, NULL); if (ctx->audio_track >= 0) queue_insert(ctx->audio_ctx.packet_queue, NULL); return 0; } -webm_context *webm_start_playback(const char *path, int volume) +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Open one independently owned decoder. Optional memory +* caching, initial seek, and sound routing are established +* before its worker threads become visible. +*/ +webm_context *webm_start_playback_ex( + const char *path, + int volume, + int sound_channel, + int cache, + uint64_t seek_timestamp, + int replace_all_audio +) { webm_context *ctx; nestegg_io io; int video_track = -1, audio_track = -1; + unsigned int num_tracks, i; - quit_video = 0; - ctx = malloc(sizeof(*ctx)); - if(!ctx) return NULL; - memset(ctx, 0, sizeof(*ctx)); - - // set up I/O callbacks - io.read = webm_read; - io.seek = webm_seek; - io.tell = webm_tell; + if(!path || !path[0] || + sound_channel < 0 || + (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX) { + return NULL; + } - // open video file - ctx->packhandle = openpackfile(path, packfile); - if(ctx->packhandle < 0) - { - printf("Error: Unable to open file %s for playback\n", path); - goto error1; + ctx = calloc(1, sizeof(*ctx)); + if(!ctx) return NULL; + ctx->io_ctx.packhandle = -1; + ctx->audio_track = -1; + ctx->video_track = -1; + + io.read = webm_io_read; + io.seek = webm_io_seek; + io.tell = webm_io_tell; + io.userdata = &ctx->io_ctx; + + if(cache) { + if(buffer_pakfile( + path, + (char**)&ctx->io_ctx.cache_buffer, + &ctx->io_ctx.cache_size + ) != 1) { + printf("Error: Unable to cache file %s for playback\n", path); + goto error_io; + } + } else { + ctx->io_ctx.packhandle = openpackfile(path, packfile); + if(ctx->io_ctx.packhandle < 0) { + printf("Error: Unable to open file %s for playback\n", path); + goto error_io; + } } - io.userdata = (void*)(size_t)ctx->packhandle; - if(nestegg_init(&(ctx->nestegg_ctx), io, NULL, -1) < 0) goto error2; - // get number of tracks - unsigned int num_tracks, i; - if(nestegg_track_count(ctx->nestegg_ctx, &num_tracks) < 0) goto error3; + if(nestegg_init(&(ctx->nestegg_ctx), io, NULL, -1) < 0) goto error_io; + nestegg_duration(ctx->nestegg_ctx, &ctx->duration); + if(nestegg_track_count(ctx->nestegg_ctx, &num_tracks) < 0) goto error_nestegg; - // find the first video and audio tracks - for (i = 0; i < num_tracks; i++) - { + for(i = 0; i < num_tracks; i++) { int track_type = nestegg_track_type(ctx->nestegg_ctx, i); int codec = nestegg_track_codec_id(ctx->nestegg_ctx, i); - if (track_type == NESTEGG_TRACK_VIDEO) - { - if(codec != NESTEGG_CODEC_VP8) - { + + if(track_type == NESTEGG_TRACK_VIDEO && video_track < 0) { + if(codec != NESTEGG_CODEC_VP8) { printf("Error: unsupported video codec; only VP8 is supported\n"); - goto error3; + goto error_nestegg; } - video_track = i; - } - else if (track_type == NESTEGG_TRACK_AUDIO) - { - if(codec != NESTEGG_CODEC_VORBIS) - { + video_track = (int)i; + } else if(track_type == NESTEGG_TRACK_AUDIO && audio_track < 0) { + if(codec != NESTEGG_CODEC_VORBIS) { printf("Error: unsupported audio codec; only Vorbis is supported\n"); - goto error3; + goto error_nestegg; } - audio_track = i; + audio_track = (int)i; } } - // set up video + if(video_track < 0) { + printf("Error: WebM file %s does not contain a video track\n", path); + goto error_nestegg; + } + + if(ctx->duration && seek_timestamp >= ctx->duration) { + seek_timestamp = ctx->duration - 1; + } + if(seek_timestamp && + nestegg_track_seek(ctx->nestegg_ctx, (unsigned int)video_track, seek_timestamp) < 0) { + printf("Error: Unable to seek WebM file %s\n", path); + goto error_nestegg; + } + ctx->video_track = video_track; - init_video(ctx->nestegg_ctx, ctx->video_track, &(ctx->video_ctx)); + if(init_video( + ctx->nestegg_ctx, + ctx->video_track, + &(ctx->video_ctx), + &ctx->quit + ) < 0) { + goto error_nestegg; + } ctx->the_video_thread = thread_create(video_thread, "video", &(ctx->video_ctx)); - assert(ctx->the_video_thread); + if(!ctx->the_video_thread) { + webm_close(ctx); + return NULL; + } - // set up audio, if applicable ctx->audio_track = audio_track; - if (audio_track >= 0) - { - // use the audio track of this file - init_audio(ctx->nestegg_ctx, ctx->audio_track, &(ctx->audio_ctx), volume); - ctx->the_audio_thread = thread_create(audio_thread, "audio", &(ctx->audio_ctx)); - assert(ctx->the_audio_thread); - } - else if (sound_query_music(NULL, NULL)) - { - // continue to play the BGM that's already playing - ctx->the_audio_thread = thread_create(bgm_update_thread, "bgm", NULL); - assert(ctx->the_audio_thread); + if(audio_track >= 0) { + if(init_audio( + ctx->nestegg_ctx, + ctx->audio_track, + &(ctx->audio_ctx), + volume, + sound_channel, + replace_all_audio, + &ctx->quit + ) == 0) { + ctx->the_audio_thread = thread_create(audio_thread, "audio", &(ctx->audio_ctx)); + if(!ctx->the_audio_thread) { + webm_close(ctx); + return NULL; + } + } else { + printf("Warning: Unable to open the WebM audio track on channel %d\n", sound_channel); + ctx->audio_track = -1; + } + } else if(replace_all_audio && sound_query_music(NULL, NULL)) { + /* Blocking legacy playback must service existing channel zero audio. */ + ctx->the_audio_thread = thread_create(bgm_update_thread, "bgm", ctx); + if(!ctx->the_audio_thread) { + webm_close(ctx); + return NULL; + } } - // finally, start the demuxing thread ctx->the_demux_thread = thread_create(demux_thread, "demux", ctx); - assert(ctx->the_demux_thread); + if(!ctx->the_demux_thread) { + webm_close(ctx); + return NULL; + } return ctx; -error3: +error_nestegg: nestegg_destroy(ctx->nestegg_ctx); -error2: - closepackfile(ctx->packhandle); -error1: +error_io: + if(ctx->io_ctx.packhandle >= 0) { + closepackfile(ctx->io_ctx.packhandle); + } + free(ctx->io_ctx.cache_buffer); free(ctx); return NULL; } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Preserve the original fullscreen decoder entrypoint with +* channel zero and replace-all audio defaults. +*/ +webm_context *webm_start_playback(const char *path, int volume) +{ + return webm_start_playback_ex( + path, + volume, + SOUND_CHANNEL_MUSIC_DEFAULT, + 0, + 0, + 1 + ); +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stop one decoder without signaling any other WebM +* object, then release its queues, codec, and I/O source. +*/ void webm_close(webm_context *ctx) { - quit_video = 1; - thread_join(ctx->the_demux_thread); - thread_join(ctx->the_video_thread); + if(!ctx) { + return; + } + + ctx->quit = 1; + if(ctx->the_demux_thread) thread_join(ctx->the_demux_thread); + if(ctx->the_video_thread) thread_join(ctx->the_video_thread); close_video(&(ctx->video_ctx)); - if (ctx->the_audio_thread) thread_join(ctx->the_audio_thread); - if (ctx->audio_track >= 0) close_audio(&(ctx->audio_ctx)); - nestegg_destroy(ctx->nestegg_ctx); - closepackfile(ctx->packhandle); + if(ctx->the_audio_thread) thread_join(ctx->the_audio_thread); + if(ctx->audio_track >= 0) close_audio(&(ctx->audio_ctx)); + if(ctx->nestegg_ctx) nestegg_destroy(ctx->nestegg_ctx); + if(ctx->io_ctx.packhandle >= 0) closepackfile(ctx->io_ctx.packhandle); + free(ctx->io_ctx.cache_buffer); free(ctx); } @@ -626,8 +973,69 @@ void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims) dims->display_height = ctx->video_ctx.display_height; } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Return the container duration in nanoseconds. +*/ +uint64_t webm_get_duration(webm_context *ctx) +{ + return ctx ? ctx->duration : 0; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Apply pause and speed changes only while this decoder's +* play ID still owns its selected generic sound channel. +*/ +void webm_set_audio_paused(webm_context *ctx, int paused) +{ + if(ctx && ctx->audio_track >= 0) { + sound_pause_channel_owned( + ctx->audio_ctx.channel, + ctx->audio_ctx.stream_play_id, + paused + ); + } +} + +void webm_set_audio_speed(webm_context *ctx, unsigned int speed) +{ + if(ctx && ctx->audio_track >= 0) { + sound_set_channel_speed_owned( + ctx->audio_ctx.channel, + ctx->audio_ctx.stream_play_id, + speed + ); + } +} + yuv_frame *webm_get_next_frame(webm_context *ctx) { debug_printf("frame queue size=%i\n", ctx->video_ctx.frame_queue->size); return (yuv_frame *)queue_get(ctx->video_ctx.frame_queue); } + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Poll decoded video without blocking the main engine update. +*/ +int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame) +{ + void *queued_frame; + + if(!ctx || !frame) { + return -1; + } + if(!queue_try_get(ctx->video_ctx.frame_queue, &queued_frame)) { + return ctx->quit ? -1 : 0; + } + + *frame = queued_frame; + return queued_frame ? 1 : -1; +} diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index 6276e3e03..2bb5243c4 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -9,15 +9,28 @@ #ifndef VIDPLAY_H #define VIDPLAY_H +#include + #include "yuv.h" struct webm_context; typedef struct webm_context webm_context; webm_context *webm_start_playback(const char *path, int volume); +webm_context *webm_start_playback_ex( + const char *path, + int volume, + int sound_channel, + int cache, + uint64_t seek_timestamp, + int replace_all_audio +); void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims); +uint64_t webm_get_duration(webm_context *ctx); +void webm_set_audio_paused(webm_context *ctx, int paused); +void webm_set_audio_speed(webm_context *ctx, unsigned int speed); void webm_close(webm_context *ctx); yuv_frame *webm_get_next_frame(webm_context *ctx); +int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame); #endif - diff --git a/engine/source/webmlib/webm_playback.c b/engine/source/webmlib/webm_playback.c new file mode 100644 index 000000000..870d988ac --- /dev/null +++ b/engine/source/webmlib/webm_playback.c @@ -0,0 +1,954 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#include +#include +#include +#include +#include +#include + +#include "screen.h" +#include "sound_channel.h" +#include "soundmix.h" +#include "timer.h" +#include "vidplay.h" +#include "webm_playback.h" + +typedef struct s_webm_playback_pool { + s_webm_playback channel[WEBM_PLAYBACK_CHANNEL_COUNT]; + uint64_t active_mask; + int initialized; + int yuv_initialized; +} s_webm_playback_pool; + +static s_webm_playback_pool webm_playback_pool; + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Private validation, allocation, and cleanup helpers for +* records retained by the fixed playback bank. +*/ +static bool webm_playback_screen_valid(const s_screen *screen) +{ + return screen && + screen->magic == screen_magic && + screen->pixelformat == PIXEL_32 && + screen->width > 0 && + screen->height > 0; +} + +static bool webm_playback_dimensions_valid(int width, int height) +{ + int allocation_width; + + if(width < 1 || height < 1 || width > INT_MAX - 3) { + return false; + } + allocation_width = (width + 3) & ~3; + return allocation_width <= + INT_MAX / height / (int)sizeof(uint32_t); +} + +static s_screen *webm_playback_alloc_screen(int width, int height) +{ + s_screen *screen; + + if(!webm_playback_dimensions_valid(width, height)) { + return NULL; + } + + screen = allocscreen((width + 3) & ~3, height, PIXEL_32); + if(screen) { + /* Retain the logical width while keeping aligned allocation space. */ + screen->width = width; + } + return screen; +} + +static char *webm_playback_copy_path(const char *path) +{ + char *copy; + size_t length; + + if(!path || !path[0]) { + return NULL; + } + + length = strlen(path) + 1; + copy = malloc(length); + if(copy) { + memcpy(copy, path, length); + } + return copy; +} + +static void webm_playback_close_media(s_webm_playback *playback) +{ + if(!playback) { + return; + } + + if(playback->context) { + webm_close(playback->context); + playback->context = NULL; + } + yuv_frame_destroy(playback->current_frame); + yuv_frame_destroy(playback->next_frame); + playback->current_frame = NULL; + playback->next_frame = NULL; + if(playback->rgb_frame) { + freescreen(&playback->rgb_frame); + } + if(playback->scaled_frame) { + freescreen(&playback->scaled_frame); + } +} + +static void webm_playback_reset_record(s_webm_playback *playback) +{ + e_object_type object_type; + int index; + + if(!playback) { + return; + } + + object_type = playback->object_type; + index = playback->index; + webm_playback_close_media(playback); + free(playback->path); + memset(playback, 0, sizeof(*playback)); + playback->object_type = object_type; + playback->index = index; + playback->sound_channel = SOUND_CHANNEL_MUSIC_DEFAULT; + playback->speed = 1.0; + playback->interrupt = 1; + playback->display_mode = WEBM_DISPLAY_FILL; + playback->loading_mode = WEBM_LOADING_STREAM; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Initialize one stable bank of 64 video objects and +* the shared 32-bit YUV conversion tables. +*/ +bool webm_playback_init(void) +{ + unsigned int channel; + + if(webm_playback_pool.initialized) { + return true; + } + + memset(&webm_playback_pool, 0, sizeof(webm_playback_pool)); + for(channel = 0; channel < WEBM_PLAYBACK_CHANNEL_COUNT; channel++) { + webm_playback_pool.channel[channel].object_type = OBJECT_TYPE_WEBM; + webm_playback_pool.channel[channel].index = (int)channel; + webm_playback_reset_record(&webm_playback_pool.channel[channel]); + } + + yuv_init(4); + webm_playback_pool.yuv_initialized = 1; + webm_playback_pool.initialized = 1; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stop the playback bank and release its shared converter. +*/ +void webm_playback_shutdown(void) +{ + if(!webm_playback_pool.initialized) { + return; + } + + webm_playback_stop_all(); + if(webm_playback_pool.yuv_initialized) { + yuv_clear(); + } + memset(&webm_playback_pool, 0, sizeof(webm_playback_pool)); +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Restore 32-bit conversion after legacy fullscreen playback +* temporarily uses the shared YUV converter in 16-bit mode. +*/ +void webm_playback_restore_yuv(void) +{ + if(webm_playback_pool.initialized) { + yuv_init(4); + webm_playback_pool.yuv_initialized = 1; + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Return a stable object from the fixed channel bank. +*/ +s_webm_playback *webm_playback_get(int channel) +{ + if(!webm_playback_pool.initialized || + channel < 0 || + (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT) { + return NULL; + } + return &webm_playback_pool.channel[channel]; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Validate a caller-provided object by exact address and +* stable channel metadata without dereferencing it first. +*/ +int webm_playback_get_index(const s_webm_playback *playback) +{ + const s_webm_playback *indexed_playback; + uintptr_t base_address; + uintptr_t playback_address; + uintptr_t playback_offset; + unsigned int channel; + + if(!webm_playback_pool.initialized || !playback) { + return -1; + } + + base_address = (uintptr_t)(const void*)&webm_playback_pool.channel[0]; + playback_address = (uintptr_t)(const void*)playback; + if(playback_address < base_address) { + return -1; + } + + playback_offset = playback_address - base_address; + if(playback_offset >= sizeof(webm_playback_pool.channel) || + playback_offset % sizeof(webm_playback_pool.channel[0])) { + return -1; + } + + channel = (unsigned int)(playback_offset / sizeof(webm_playback_pool.channel[0])); + indexed_playback = &webm_playback_pool.channel[channel]; + if((uintptr_t)(const void*)indexed_playback != playback_address || + indexed_playback->object_type != OBJECT_TYPE_WEBM || + indexed_playback->index != (int)channel) { + return -1; + } + + return (int)channel; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Copy stable public state and expose the active channel mask +* without publishing mutable pool internals. +*/ +bool webm_playback_get_snapshot( + const s_webm_playback *playback, + s_webm_playback *snapshot +) +{ + int channel = webm_playback_get_index(playback); + + if(channel < 0 || !snapshot) { + return false; + } + *snapshot = webm_playback_pool.channel[channel]; + return true; +} + +uint64_t webm_playback_get_active_mask(void) +{ + return webm_playback_pool.initialized + ? webm_playback_pool.active_mask + : UINT64_C(0); +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Resolve wall-clock position and open or reopen decoder media +* while retaining the surrounding playback object's settings. +*/ +static uint64_t webm_playback_position_now( + const s_webm_playback *playback, + uint64_t now +) +{ + double elapsed; + double position; + + if(playback->paused) { + return playback->position_anchor; + } + + elapsed = now >= playback->clock_anchor + ? (double)(now - playback->clock_anchor) * 1000.0 * playback->speed + : 0.0; + position = (double)playback->position_anchor + elapsed; + if(position >= (double)UINT64_MAX) { + return UINT64_MAX; + } + return (uint64_t)position; +} + +static bool webm_playback_open_media( + s_webm_playback *playback, + uint64_t position, + int volume +) +{ + uint64_t duration; + unsigned int speed_percent; + + webm_playback_close_media(playback); + playback->volume = volume; + playback->context = webm_start_playback_ex( + playback->path, + volume, + playback->sound_channel, + playback->loading_mode == WEBM_LOADING_CACHE, + position, + 0 + ); + if(!playback->context) { + return false; + } + + webm_get_video_info(playback->context, &playback->video_mode); + duration = webm_get_duration(playback->context); + playback->duration = duration / UINT64_C(1000000); + if((playback->video_mode.width & 1) || + (playback->video_mode.height & 1) || + playback->video_mode.width > (INT_MAX >> 16) || + playback->video_mode.height > (INT_MAX >> 16) || + !webm_playback_dimensions_valid( + playback->video_mode.width, + playback->video_mode.height + )) { + printf( + "Error: WebM frame dimensions %d*%d cannot be represented by the 32-bit screen renderer.\n", + playback->video_mode.width, + playback->video_mode.height + ); + webm_playback_close_media(playback); + return false; + } + playback->rgb_frame = webm_playback_alloc_screen( + playback->video_mode.width, + playback->video_mode.height + ); + if(!playback->rgb_frame) { + webm_playback_close_media(playback); + return false; + } + + speed_percent = (unsigned int)(playback->speed * 100.0 + 0.5); + if(speed_percent < 1U) { + speed_percent = 1U; + } + webm_set_audio_speed(playback->context, speed_percent); + if(playback->paused) { + webm_set_audio_paused(playback->context, 1); + } + + playback->position_anchor = position; + playback->clock_anchor = timer_uticks(); + playback->position = position / UINT64_C(1000000); + playback->render_dirty = 1; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Acquire the lowest free video channel and return its +* stable object after decoder and destination validation. +*/ +s_webm_playback *webm_playback_open( + const char *path, + s_screen *screen, + int sound_channel, + int interrupt, + e_webm_loading_mode loading_mode, + int volume +) +{ + s_webm_playback *playback; + uint64_t available_mask; + int channel; + + if(!webm_playback_init() || + !path || !path[0] || + !webm_playback_screen_valid(screen) || + sound_channel < 0 || + (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || + loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { + return NULL; + } + + available_mask = ~webm_playback_pool.active_mask; + channel = sound_channel_mask_first(available_mask); + if(channel < 0 || (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT) { + return NULL; + } + + playback = &webm_playback_pool.channel[channel]; + webm_playback_reset_record(playback); + playback->path = webm_playback_copy_path(path); + if(!playback->path) { + return NULL; + } + playback->screen = screen; + playback->sound_channel = sound_channel; + playback->interrupt = interrupt != 0; + playback->loading_mode = loading_mode; + + if(!webm_playback_open_media(playback, 0, volume)) { + webm_playback_reset_record(playback); + return NULL; + } + + playback->active = 1; + webm_playback_pool.active_mask |= UINT64_C(1) << channel; + return playback; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stop individual, grouped, or screen-detached playback records +* and return their fixed slots to the available mask. +*/ +void webm_playback_stop(s_webm_playback *playback) +{ + int channel = webm_playback_get_index(playback); + + if(channel < 0) { + return; + } + + webm_playback_pool.active_mask &= ~(UINT64_C(1) << channel); + webm_playback_reset_record(&webm_playback_pool.channel[channel]); +} + +void webm_playback_stop_all(void) +{ + uint64_t active_mask; + int channel; + + if(!webm_playback_pool.initialized) { + return; + } + + active_mask = webm_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + active_mask &= ~(UINT64_C(1) << channel); + webm_playback_stop(&webm_playback_pool.channel[channel]); + } +} + +void webm_playback_detach_screen(const s_screen *screen) +{ + uint64_t active_mask; + int channel; + + if(!screen || !webm_playback_pool.initialized) { + return; + } + + active_mask = webm_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + s_webm_playback *playback = &webm_playback_pool.channel[channel]; + active_mask &= ~(UINT64_C(1) << channel); + if(playback->screen == screen) { + webm_playback_stop(playback); + } + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Prepare scaled output and copy it into a creator-owned screen +* with bounds independent from global draw clipping. +*/ +static bool webm_playback_scale_frame( + s_webm_playback *playback, + int width, + int height, + s_screen **render_frame +) +{ + if(width == playback->rgb_frame->width && + height == playback->rgb_frame->height) { + *render_frame = playback->rgb_frame; + return true; + } + + if(!webm_playback_dimensions_valid(width, height)) { + return false; + } + + if(playback->scaled_frame && + (playback->scaled_frame->width != width || + playback->scaled_frame->height != height)) { + freescreen(&playback->scaled_frame); + } + if(!playback->scaled_frame) { + playback->scaled_frame = webm_playback_alloc_screen(width, height); + } + if(!playback->scaled_frame) { + return false; + } + + scalescreen32(playback->scaled_frame, playback->rgb_frame); + *render_frame = playback->scaled_frame; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Copy a 32-bit video frame with destination clipping +* independent of the engine's current draw-method clip state. +*/ +static void webm_playback_copy_frame( + s_screen *destination, + const s_screen *source, + int offset_x, + int offset_y +) +{ + const unsigned char *source_row; + unsigned char *destination_row; + int64_t destination_x = offset_x; + int64_t destination_y = offset_y; + int source_x = 0; + int source_y = 0; + int width = source->width; + int height = source->height; + int row; + + if(destination_x < 0) { + int64_t clipped_width = -destination_x; + if(clipped_width >= width) { + return; + } + source_x = (int)clipped_width; + width -= source_x; + destination_x = 0; + } + if(destination_y < 0) { + int64_t clipped_height = -destination_y; + if(clipped_height >= height) { + return; + } + source_y = (int)clipped_height; + height -= source_y; + destination_y = 0; + } + if(destination_x >= destination->width || destination_y >= destination->height) { + return; + } + if(width > destination->width - destination_x) { + width = (int)(destination->width - destination_x); + } + if(height > destination->height - destination_y) { + height = (int)(destination->height - destination_y); + } + if(width <= 0 || height <= 0) { + return; + } + + source_row = source->data + + ((source_y * source->width + source_x) * (int)sizeof(uint32_t)); + destination_row = destination->data + + (((int)destination_y * destination->width + (int)destination_x) * + (int)sizeof(uint32_t)); + for(row = 0; row < height; row++) { + memcpy(destination_row, source_row, (size_t)width * sizeof(uint32_t)); + source_row += source->width * (int)sizeof(uint32_t); + destination_row += destination->width * (int)sizeof(uint32_t); + } +} + +static bool webm_playback_render(s_webm_playback *playback) +{ + s_screen *render_frame; + int width; + int height; + + if(!playback->current_frame || + !playback->rgb_frame || + !webm_playback_screen_valid(playback->screen)) { + return false; + } + + yuv_to_rgb(playback->current_frame, playback->rgb_frame); + width = playback->width; + height = playback->height; + if(width == 0) { + width = playback->display_mode == WEBM_DISPLAY_FILL + ? playback->screen->width + : (playback->video_mode.display_width > 0 + ? playback->video_mode.display_width + : playback->video_mode.width); + } + if(height == 0) { + height = playback->display_mode == WEBM_DISPLAY_FILL + ? playback->screen->height + : (playback->video_mode.display_height > 0 + ? playback->video_mode.display_height + : playback->video_mode.height); + } + if(width < 1 || height < 1 || + !webm_playback_scale_frame(playback, width, height, &render_frame)) { + return false; + } + + webm_playback_copy_frame( + playback->screen, + render_frame, + playback->offset_x, + playback->offset_y + ); + playback->render_dirty = 0; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Advance all active objects without blocking on decoder +* queues, render due frames, and recycle terminal channels. +*/ +void webm_playback_update(int interrupt_requested) +{ + uint64_t active_mask; + uint64_t now; + int channel; + + if(!webm_playback_pool.initialized) { + return; + } + + now = timer_uticks(); + active_mask = webm_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + s_webm_playback *playback = &webm_playback_pool.channel[channel]; + uint64_t position; + int terminal = 0; + + active_mask &= ~(UINT64_C(1) << channel); + if(interrupt_requested && playback->interrupt) { + webm_playback_stop(playback); + continue; + } + if(!webm_playback_screen_valid(playback->screen)) { + webm_playback_stop(playback); + continue; + } + + position = webm_playback_position_now(playback, now); + playback->position = position / UINT64_C(1000000); + if(playback->duration && playback->position > playback->duration) { + playback->position = playback->duration; + } + + if(!playback->paused) { + while(1) { + int result; + + if(!playback->next_frame) { + result = webm_try_get_next_frame( + playback->context, + &playback->next_frame + ); + if(result == 0) { + break; + } + if(result < 0) { + terminal = 1; + break; + } + } + + if(playback->next_frame->timestamp > position) { + break; + } + + yuv_frame_destroy(playback->current_frame); + playback->current_frame = playback->next_frame; + playback->next_frame = NULL; + playback->render_dirty = 1; + } + } + + if(playback->render_dirty && playback->current_frame && + !webm_playback_render(playback)) { + webm_playback_stop(playback); + continue; + } + + if(terminal) { + if(playback->repeat) { + if(!webm_playback_open_media(playback, 0, playback->volume)) { + webm_playback_stop(playback); + } + } else { + webm_playback_stop(playback); + } + } + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Property mutation helpers preserve decoder, timing, audio, +* scaling, and externally owned screen invariants. +*/ +bool webm_playback_set_display_mode( + s_webm_playback *playback, + e_webm_display_mode display_mode +) +{ + if(webm_playback_get_index(playback) < 0 || + display_mode < 0 || display_mode >= WEBM_DISPLAY_END) { + return false; + } + playback->display_mode = display_mode; + playback->render_dirty = 1; + return true; +} + +bool webm_playback_set_height(s_webm_playback *playback, int height) +{ + if(webm_playback_get_index(playback) < 0 || height < 0) { + return false; + } + playback->height = height; + playback->render_dirty = 1; + return true; +} + +bool webm_playback_set_interrupt(s_webm_playback *playback, int interrupt) +{ + if(webm_playback_get_index(playback) < 0) { + return false; + } + playback->interrupt = interrupt != 0; + return true; +} + +bool webm_playback_set_loading_mode( + s_webm_playback *playback, + e_webm_loading_mode loading_mode, + int volume +) +{ + uint64_t position; + e_webm_loading_mode previous_mode; + + if(webm_playback_get_index(playback) < 0 || + !playback->active || + loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { + return false; + } + if(playback->loading_mode == loading_mode) { + return true; + } + + position = webm_playback_position_now(playback, timer_uticks()); + previous_mode = playback->loading_mode; + playback->loading_mode = loading_mode; + if(!webm_playback_open_media(playback, position, volume)) { + playback->loading_mode = previous_mode; + webm_playback_stop(playback); + return false; + } + return true; +} + +bool webm_playback_set_offset_x(s_webm_playback *playback, int offset_x) +{ + if(webm_playback_get_index(playback) < 0) { + return false; + } + playback->offset_x = offset_x; + playback->render_dirty = 1; + return true; +} + +bool webm_playback_set_offset_y(s_webm_playback *playback, int offset_y) +{ + if(webm_playback_get_index(playback) < 0) { + return false; + } + playback->offset_y = offset_y; + playback->render_dirty = 1; + return true; +} + +bool webm_playback_set_paused(s_webm_playback *playback, int paused) +{ + uint64_t now; + + if(webm_playback_get_index(playback) < 0 || !playback->active) { + return false; + } + paused = paused != 0; + if(playback->paused == paused) { + return true; + } + + now = timer_uticks(); + if(paused) { + playback->position_anchor = webm_playback_position_now(playback, now); + playback->position = playback->position_anchor / UINT64_C(1000000); + } else { + playback->clock_anchor = now; + } + playback->paused = paused; + webm_set_audio_paused(playback->context, paused); + return true; +} + +bool webm_playback_set_position( + s_webm_playback *playback, + uint64_t position, + int volume +) +{ + uint64_t position_ns; + + if(webm_playback_get_index(playback) < 0 || !playback->active) { + return false; + } + if(playback->duration && position >= playback->duration) { + position = playback->duration - 1; + } + if(position > UINT64_MAX / UINT64_C(1000000)) { + return false; + } + + position_ns = position * UINT64_C(1000000); + if(!webm_playback_open_media(playback, position_ns, volume)) { + webm_playback_stop(playback); + return false; + } + return true; +} + +bool webm_playback_set_repeat(s_webm_playback *playback, int repeat) +{ + if(webm_playback_get_index(playback) < 0) { + return false; + } + playback->repeat = repeat != 0; + return true; +} + +bool webm_playback_set_screen(s_webm_playback *playback, s_screen *screen) +{ + if(webm_playback_get_index(playback) < 0 || + !webm_playback_screen_valid(screen)) { + return false; + } + playback->screen = screen; + playback->render_dirty = 1; + return true; +} + +bool webm_playback_set_sound_channel( + s_webm_playback *playback, + int sound_channel, + int volume +) +{ + uint64_t position; + int previous_channel; + + if(webm_playback_get_index(playback) < 0 || + !playback->active || + sound_channel < 0 || + (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX) { + return false; + } + if(playback->sound_channel == sound_channel) { + return true; + } + + position = webm_playback_position_now(playback, timer_uticks()); + previous_channel = playback->sound_channel; + playback->sound_channel = sound_channel; + if(!webm_playback_open_media(playback, position, volume)) { + playback->sound_channel = previous_channel; + webm_playback_stop(playback); + return false; + } + return true; +} + +bool webm_playback_set_speed(s_webm_playback *playback, double speed) +{ + uint64_t now; + unsigned int speed_percent; + + if(webm_playback_get_index(playback) < 0 || + !playback->active || + !isfinite(speed) || + speed < WEBM_PLAYBACK_SPEED_MIN || + speed > WEBM_PLAYBACK_SPEED_MAX) { + return false; + } + + now = timer_uticks(); + playback->position_anchor = webm_playback_position_now(playback, now); + playback->clock_anchor = now; + playback->speed = speed; + speed_percent = (unsigned int)(speed * 100.0 + 0.5); + if(speed_percent < 1U) { + speed_percent = 1U; + } + webm_set_audio_speed(playback->context, speed_percent); + return true; +} + +bool webm_playback_set_width(s_webm_playback *playback, int width) +{ + if(webm_playback_get_index(playback) < 0 || + width < 0) { + return false; + } + playback->width = width; + playback->render_dirty = 1; + return true; +} diff --git a/engine/source/webmlib/webm_playback.h b/engine/source/webmlib/webm_playback.h new file mode 100644 index 000000000..88fca740f --- /dev/null +++ b/engine/source/webmlib/webm_playback.h @@ -0,0 +1,127 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#ifndef WEBM_PLAYBACK_H +#define WEBM_PLAYBACK_H + +#include +#include + +#include "types.h" +#include "yuv.h" + +#define WEBM_PLAYBACK_CHANNEL_COUNT 64U +#define WEBM_PLAYBACK_SPEED_MIN 0.01 +#define WEBM_PLAYBACK_SPEED_MAX 16.0 + +typedef enum e_webm_display_mode { + WEBM_DISPLAY_FILL, /* Zero-value default: scale to destination. */ + WEBM_DISPLAY_CLIP, + WEBM_DISPLAY_END +} e_webm_display_mode; + +typedef enum e_webm_loading_mode { + WEBM_LOADING_STREAM, /* Decode through ordinary packfile I/O. */ + WEBM_LOADING_CACHE, /* Cache the complete WebM before decoding. */ + WEBM_LOADING_END +} e_webm_loading_mode; + +struct webm_context; + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stable script-visible WebM playback object. Decoder, +* frame, and clock members remain engine-managed. +*/ +typedef struct s_webm_playback { + e_object_type object_type; + int index; + int active; + int paused; + int repeat; + int interrupt; + int offset_x; + int offset_y; + int width; + int height; + int sound_channel; + float speed; + e_webm_display_mode display_mode; + e_webm_loading_mode loading_mode; + uint64_t duration; + uint64_t position; + s_screen *screen; + + char *path; + struct webm_context *context; + yuv_video_mode video_mode; + yuv_frame *current_frame; + yuv_frame *next_frame; + s_screen *rgb_frame; + s_screen *scaled_frame; + uint64_t clock_anchor; + uint64_t position_anchor; + int volume; + int render_dirty; +} s_webm_playback; + +bool webm_playback_init(void); +void webm_playback_shutdown(void); +void webm_playback_restore_yuv(void); +s_webm_playback *webm_playback_open( + const char *path, + s_screen *screen, + int sound_channel, + int interrupt, + e_webm_loading_mode loading_mode, + int volume +); +s_webm_playback *webm_playback_get(int channel); +int webm_playback_get_index(const s_webm_playback *playback); +bool webm_playback_get_snapshot( + const s_webm_playback *playback, + s_webm_playback *snapshot +); +uint64_t webm_playback_get_active_mask(void); +void webm_playback_update(int interrupt_requested); +void webm_playback_stop(s_webm_playback *playback); +void webm_playback_stop_all(void); +void webm_playback_detach_screen(const s_screen *screen); + +bool webm_playback_set_display_mode( + s_webm_playback *playback, + e_webm_display_mode display_mode +); +bool webm_playback_set_height(s_webm_playback *playback, int height); +bool webm_playback_set_interrupt(s_webm_playback *playback, int interrupt); +bool webm_playback_set_loading_mode( + s_webm_playback *playback, + e_webm_loading_mode loading_mode, + int volume +); +bool webm_playback_set_offset_x(s_webm_playback *playback, int offset_x); +bool webm_playback_set_offset_y(s_webm_playback *playback, int offset_y); +bool webm_playback_set_paused(s_webm_playback *playback, int paused); +bool webm_playback_set_position( + s_webm_playback *playback, + uint64_t position, + int volume +); +bool webm_playback_set_repeat(s_webm_playback *playback, int repeat); +bool webm_playback_set_screen(s_webm_playback *playback, s_screen *screen); +bool webm_playback_set_sound_channel( + s_webm_playback *playback, + int sound_channel, + int volume +); +bool webm_playback_set_speed(s_webm_playback *playback, double speed); +bool webm_playback_set_width(s_webm_playback *playback, int width); + +#endif diff --git a/engine/source/webmlib/yuv.c b/engine/source/webmlib/yuv.c index 9b474fa11..7048f5e47 100644 --- a/engine/source/webmlib/yuv.c +++ b/engine/source/webmlib/yuv.c @@ -117,12 +117,38 @@ static int free_bits_at_bottom(uint32_t a) return 1 + free_bits_at_bottom ( a >> 1); } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Allocate one complete YUV 4:2:0 frame with checked +* dimensions and exact chroma plane capacity. +*/ yuv_frame *yuv_frame_create(int width, int height) { - yuv_frame *frame = malloc(sizeof(yuv_frame)); - frame->lum = malloc(width * height); - frame->cr = malloc(width * height / 2); - frame->cb = malloc(width * height / 2); + yuv_frame *frame; + size_t luma_size; + size_t chroma_size; + + if(width < 1 || height < 1 || + (width & 1) || (height & 1) || + (size_t)width > SIZE_MAX / (size_t)height) { + return NULL; + } + + luma_size = (size_t)width * (size_t)height; + chroma_size = luma_size / 4U; + frame = calloc(1, sizeof(*frame)); + if(!frame) { + return NULL; + } + frame->lum = malloc(luma_size); + frame->cr = malloc(chroma_size); + frame->cb = malloc(chroma_size); + if(!frame->lum || !frame->cr || !frame->cb) { + yuv_frame_destroy(frame); + return NULL; + } return frame; } @@ -396,4 +422,3 @@ void yuv_to_rgb(yuv_frame *in, s_screen *out) convert(in->lum, in->cr, in->cb, out->data, out->height, out->width, 0); #endif } - From 08b162c5956ab22d8d79af1ec9fea6f22f572a58 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Wed, 12 Aug 2026 18:38:07 -0400 Subject: [PATCH 02/26] Add forced WebM channel selection --- engine/source/openborscript/constants.c | 1 + engine/source/openborscript/webm.c | 22 ++++++++++++++-------- engine/source/webmlib/webm_playback.c | 23 ++++++++++++++++------- engine/source/webmlib/webm_playback.h | 2 ++ 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index 3ff4bf4ac..a3905dcdd 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1162,6 +1162,7 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) ICMPCONST(WEBM_PROPERTY_END) #ifdef WEBM ICMPCONST(WEBM_PLAYBACK_CHANNEL_COUNT) + ICMPCONST(WEBM_PLAYBACK_CHANNEL_AUTO) ICMPCONST(WEBM_DISPLAY_CLIP) ICMPCONST(WEBM_DISPLAY_FILL) ICMPCONST(WEBM_DISPLAY_END) diff --git a/engine/source/openborscript/webm.c b/engine/source/openborscript/webm.c index 7771d8857..6bb51506b 100644 --- a/engine/source/openborscript/webm.c +++ b/engine/source/openborscript/webm.c @@ -120,8 +120,8 @@ static const s_property_access_map webm_get_property_map( * Caskey, Damon V. * 2026-08-12 * -* Start nonblocking playback into a creator-owned 32-bit -* screen and return the acquired channel object. +* Start nonblocking playback on an automatic or forced video +* channel and render into a creator-owned 32-bit screen. */ HRESULT openbor_open_webm( ScriptVariant **varlist, @@ -130,9 +130,10 @@ HRESULT openbor_open_webm( ) { const char *self_name = - "open_webm(string path, void screen, int sound_channel, int interrupt, int loading)"; + "open_webm(string path, void screen, int video_channel, int sound_channel, int interrupt, int loading)"; s_webm_playback *playback; s_screen *screen; + LONG video_channel = WEBM_PLAYBACK_CHANNEL_AUTO; LONG sound_channel = SOUND_CHANNEL_MUSIC_DEFAULT; LONG interrupt = 1; LONG loading_mode = WEBM_LOADING_STREAM; @@ -142,13 +143,17 @@ HRESULT openbor_open_webm( varlist[0]->vt != VT_STR || varlist[1]->vt != VT_PTR || !varlist[1]->ptrVal || - (paramCount > 2 && FAILED(ScriptVariant_IntegerValue(varlist[2], &sound_channel))) || - (paramCount > 3 && FAILED(ScriptVariant_IntegerValue(varlist[3], &interrupt))) || - (paramCount > 4 && FAILED(ScriptVariant_IntegerValue(varlist[4], &loading_mode))) || + (paramCount > 2 && FAILED(ScriptVariant_IntegerValue(varlist[2], &video_channel))) || + (paramCount > 3 && FAILED(ScriptVariant_IntegerValue(varlist[3], &sound_channel))) || + (paramCount > 4 && FAILED(ScriptVariant_IntegerValue(varlist[4], &interrupt))) || + (paramCount > 5 && FAILED(ScriptVariant_IntegerValue(varlist[5], &loading_mode))) || + (video_channel != WEBM_PLAYBACK_CHANNEL_AUTO && + (video_channel < 0 || + (unsigned int)video_channel >= WEBM_PLAYBACK_CHANNEL_COUNT)) || sound_channel < 0 || (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { - printf("\nScript error: %s. Invalid path, screen, sound channel, interrupt, or loading mode.\n", + printf("\nScript error: %s. Invalid path, screen, video channel, sound channel, interrupt, or loading mode.\n", self_name); *pretvar = NULL; return E_FAIL; @@ -158,6 +163,7 @@ HRESULT openbor_open_webm( playback = webm_playback_open( StrCache_Get(varlist[0]->strVal), screen, + (int)video_channel, (int)sound_channel, interrupt != 0, (e_webm_loading_mode)loading_mode, @@ -169,7 +175,7 @@ HRESULT openbor_open_webm( return S_OK; } - printf("\nScript error: %s. Playback could not acquire a channel, screen, or decoder.\n", + printf("\nScript error: %s. Playback could not acquire or replace the requested channel, screen, or decoder.\n", self_name); *pretvar = NULL; return E_FAIL; diff --git a/engine/source/webmlib/webm_playback.c b/engine/source/webmlib/webm_playback.c index 870d988ac..9dc36b31b 100644 --- a/engine/source/webmlib/webm_playback.c +++ b/engine/source/webmlib/webm_playback.c @@ -383,12 +383,13 @@ static bool webm_playback_open_media( * Caskey, Damon V. * 2026-08-12 * -* Acquire the lowest free video channel and return its -* stable object after decoder and destination validation. +* Acquire an automatic or explicitly requested video channel. +* Explicit selection replaces the channel's current playback. */ s_webm_playback *webm_playback_open( const char *path, s_screen *screen, + int video_channel, int sound_channel, int interrupt, e_webm_loading_mode loading_mode, @@ -402,20 +403,28 @@ s_webm_playback *webm_playback_open( if(!webm_playback_init() || !path || !path[0] || !webm_playback_screen_valid(screen) || + (video_channel != WEBM_PLAYBACK_CHANNEL_AUTO && + (video_channel < 0 || + (unsigned int)video_channel >= WEBM_PLAYBACK_CHANNEL_COUNT)) || sound_channel < 0 || (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { return NULL; } - available_mask = ~webm_playback_pool.active_mask; - channel = sound_channel_mask_first(available_mask); - if(channel < 0 || (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT) { - return NULL; + if(video_channel == WEBM_PLAYBACK_CHANNEL_AUTO) { + available_mask = ~webm_playback_pool.active_mask; + channel = sound_channel_mask_first(available_mask); + if(channel < 0 || + (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT) { + return NULL; + } + } else { + channel = video_channel; } playback = &webm_playback_pool.channel[channel]; - webm_playback_reset_record(playback); + webm_playback_stop(playback); playback->path = webm_playback_copy_path(path); if(!playback->path) { return NULL; diff --git a/engine/source/webmlib/webm_playback.h b/engine/source/webmlib/webm_playback.h index 88fca740f..8ab4e1364 100644 --- a/engine/source/webmlib/webm_playback.h +++ b/engine/source/webmlib/webm_playback.h @@ -16,6 +16,7 @@ #include "yuv.h" #define WEBM_PLAYBACK_CHANNEL_COUNT 64U +#define WEBM_PLAYBACK_CHANNEL_AUTO (-1) #define WEBM_PLAYBACK_SPEED_MIN 0.01 #define WEBM_PLAYBACK_SPEED_MAX 16.0 @@ -78,6 +79,7 @@ void webm_playback_restore_yuv(void); s_webm_playback *webm_playback_open( const char *path, s_screen *screen, + int video_channel, int sound_channel, int interrupt, e_webm_loading_mode loading_mode, From b892b63c63053ad209b2aab8179faeb83034c973 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Wed, 12 Aug 2026 18:38:51 -0400 Subject: [PATCH 03/26] Refactor WebM playback behind movie API --- engine/openbor.c | 10 +- engine/openbor.h | 2 +- engine/openborscript.c | 4 +- engine/source/gamelib/types.h | 2 +- engine/source/openborscript/config.h | 2 +- engine/source/openborscript/constants.c | 49 +- engine/source/openborscript/index.c | 66 +- engine/source/openborscript/movie.c | 579 ++++++++++ engine/source/openborscript/movie.h | 44 + engine/source/openborscript/webm.c | 485 --------- engine/source/openborscript/webm.h | 68 -- engine/source/webmlib/movie_playback.c | 1283 +++++++++++++++++++++++ engine/source/webmlib/movie_playback.h | 120 +++ engine/source/webmlib/vidplay.c | 25 +- engine/source/webmlib/vidplay.h | 4 +- engine/source/webmlib/webm_playback.c | 963 ----------------- engine/source/webmlib/webm_playback.h | 129 --- 17 files changed, 2119 insertions(+), 1716 deletions(-) create mode 100644 engine/source/openborscript/movie.c create mode 100644 engine/source/openborscript/movie.h delete mode 100644 engine/source/openborscript/webm.c delete mode 100644 engine/source/openborscript/webm.h create mode 100644 engine/source/webmlib/movie_playback.c create mode 100644 engine/source/webmlib/movie_playback.h delete mode 100644 engine/source/webmlib/webm_playback.c delete mode 100644 engine/source/webmlib/webm_playback.h diff --git a/engine/openbor.c b/engine/openbor.c index e6a017d97..26f2692ef 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -50915,9 +50915,10 @@ void update(int ingame, int usevwait) if(playrecstatus->status == A_REC_PLAY && !_pause && level) if ( !playRecordedInputs() ) stopRecordInputs(); inputrefresh(playrecstatus->status); #ifdef WEBM - webm_playback_update( + movie_playback_update( (bothnewkeys & (FLAG_ESC | FLAG_ANYBUTTON)) != 0 ); + movie_playback_render_subscreens(); #endif if(playrecstatus->status == A_REC_REC && !_pause && level) if ( !recordInputs() ) stopRecordInputs(); @@ -51095,6 +51096,9 @@ void update(int ingame, int usevwait) /********** update screen **************/ spriteq_draw(vscreen, 0, MIN_INT, MAX_INT, 0, 0); // notice, always draw sprites at the very end of other methods +#ifdef WEBM + movie_playback_render_main(vscreen); +#endif if(_pause != 2 && !noscreenshot && (bothnewkeys & FLAG_SCREENSHOT)) { @@ -51874,7 +51878,7 @@ int playwebm(const char *path, int noskip) yuv_video_mode info; s_screen *rgb_frame = NULL; - webm_playback_stop_all(); + movie_playback_stop_all(); ctx = webm_start_playback(path, savedata.musicvol); if(ctx == NULL) {retval=0; goto quit;} @@ -51931,7 +51935,7 @@ int playwebm(const char *path, int noskip) if(ctx) webm_close(ctx); if(rgb_frame) freescreen(&rgb_frame); yuv_clear(); - webm_playback_restore_yuv(); + movie_playback_restore_yuv(); video_set_mode(videomodes); return retval; } diff --git a/engine/openbor.h b/engine/openbor.h index 0f2d9065b..04c1d59cf 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -54,7 +54,7 @@ #ifdef WEBM #include "yuv.h" #include "vidplay.h" -#include "webm_playback.h" +#include "movie_playback.h" #endif /* diff --git a/engine/openborscript.c b/engine/openborscript.c index 94b0b34b0..266e85ccd 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -419,7 +419,7 @@ void _freeheapnode(void *ptr) #ifdef WEBM else if(((s_screen *)ptr)->magic == screen_magic) { - webm_playback_detach_screen((s_screen *)ptr); + movie_playback_detach_screen((s_screen *)ptr); } #endif free(ptr); @@ -430,7 +430,7 @@ void Script_Global_Clear() { int i, size; #ifdef WEBM - webm_playback_shutdown(); + movie_playback_shutdown(); #endif List_Clear(&theFunctionList); // dump all un-freed variants diff --git a/engine/source/gamelib/types.h b/engine/source/gamelib/types.h index 4d85d703a..1fda72529 100644 --- a/engine/source/gamelib/types.h +++ b/engine/source/gamelib/types.h @@ -116,7 +116,7 @@ typedef enum e_object_type { OBJECT_TYPE_GLOBAL_CONFIG, OBJECT_TYPE_MODEL, OBJECT_TYPE_SOUND, - OBJECT_TYPE_WEBM + OBJECT_TYPE_MOVIE_PLAYBACK } e_object_type; typedef struct diff --git a/engine/source/openborscript/config.h b/engine/source/openborscript/config.h index 44c186292..18eb6c541 100644 --- a/engine/source/openborscript/config.h +++ b/engine/source/openborscript/config.h @@ -29,7 +29,7 @@ #include "recursive_effect.h" #include "spawn_hud.h" #include "status_dial.h" -#include "webm.h" +#include "movie.h" //osc diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index a3905dcdd..294d610a3 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1142,33 +1142,30 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) ICMPCONST(SOUND_PROPERTY_OWNER_ID) ICMPCONST(SOUND_PROPERTY_END) - /* WebM playback object property access. */ - ICMPCONST(WEBM_PROPERTY_ACTIVE) - ICMPCONST(WEBM_PROPERTY_CHANNEL) - ICMPCONST(WEBM_PROPERTY_DISPLAY_MODE) - ICMPCONST(WEBM_PROPERTY_DURATION) - ICMPCONST(WEBM_PROPERTY_HEIGHT) - ICMPCONST(WEBM_PROPERTY_INTERRUPT) - ICMPCONST(WEBM_PROPERTY_LOADING) - ICMPCONST(WEBM_PROPERTY_OFFSET_X) - ICMPCONST(WEBM_PROPERTY_OFFSET_Y) - ICMPCONST(WEBM_PROPERTY_PAUSED) - ICMPCONST(WEBM_PROPERTY_POSITION) - ICMPCONST(WEBM_PROPERTY_REPEAT) - ICMPCONST(WEBM_PROPERTY_SCREEN) - ICMPCONST(WEBM_PROPERTY_SOUND_CHANNEL) - ICMPCONST(WEBM_PROPERTY_SPEED) - ICMPCONST(WEBM_PROPERTY_WIDTH) - ICMPCONST(WEBM_PROPERTY_END) + /* Movie playback object property access. */ + ICMPCONST(MOVIE_PROPERTY_ACTIVE) + ICMPCONST(MOVIE_PROPERTY_BLACK_FILTER) + ICMPCONST(MOVIE_PROPERTY_CHANNEL) + ICMPCONST(MOVIE_PROPERTY_DURATION) + ICMPCONST(MOVIE_PROPERTY_HEIGHT) + ICMPCONST(MOVIE_PROPERTY_INTERRUPT) + ICMPCONST(MOVIE_PROPERTY_OFFSET_X) + ICMPCONST(MOVIE_PROPERTY_OFFSET_Y) + ICMPCONST(MOVIE_PROPERTY_PAUSED) + ICMPCONST(MOVIE_PROPERTY_POSITION) + ICMPCONST(MOVIE_PROPERTY_REPEAT) + ICMPCONST(MOVIE_PROPERTY_SCREEN) + ICMPCONST(MOVIE_PROPERTY_SOUND_CHANNEL) + ICMPCONST(MOVIE_PROPERTY_SOURCE) + ICMPCONST(MOVIE_PROPERTY_SPEED) + ICMPCONST(MOVIE_PROPERTY_WIDTH) + ICMPCONST(MOVIE_PROPERTY_END) #ifdef WEBM - ICMPCONST(WEBM_PLAYBACK_CHANNEL_COUNT) - ICMPCONST(WEBM_PLAYBACK_CHANNEL_AUTO) - ICMPCONST(WEBM_DISPLAY_CLIP) - ICMPCONST(WEBM_DISPLAY_FILL) - ICMPCONST(WEBM_DISPLAY_END) - ICMPCONST(WEBM_LOADING_STREAM) - ICMPCONST(WEBM_LOADING_CACHE) - ICMPCONST(WEBM_LOADING_END) + ICMPCONST(MOVIE_CHANNEL_COUNT) + ICMPCONST(MOVIE_CHANNEL_AUTO) + ICMPCONST(MOVIE_LOADING_STREAM) + ICMPCONST(MOVIE_LOADING_CACHE) + ICMPCONST(MOVIE_LOADING_END) #endif /* Key flags. */ diff --git a/engine/source/openborscript/index.c b/engine/source/openborscript/index.c index ce7bade8f..41f962130 100644 --- a/engine/source/openborscript/index.c +++ b/engine/source/openborscript/index.c @@ -925,33 +925,49 @@ const char *Script_GetFunctionName(void *functionRef) { return "playwebm"; } - else if (functionRef == ((void *)openbor_open_webm)) + else if (functionRef == ((void *)openbor_movie_load)) { - return "open_webm"; + return "movie_load"; } - else if (functionRef == ((void *)openbor_get_webm_channel_object)) + else if (functionRef == ((void *)openbor_movie_unload)) { - return "get_webm_channel_object"; + return "movie_unload"; } - else if (functionRef == ((void *)openbor_get_webm_channel_index)) + else if (functionRef == ((void *)openbor_movie_play)) { - return "get_webm_channel_index"; + return "movie_play"; } - else if (functionRef == ((void *)openbor_get_webm_channel_mask)) + else if (functionRef == ((void *)openbor_movie_set_screen)) { - return "get_webm_channel_mask"; + return "movie_set_screen"; } - else if (functionRef == ((void *)openbor_get_webm_property)) + else if (functionRef == ((void *)openbor_movie_set_sound_channel)) { - return "get_webm_property"; + return "movie_set_sound_channel"; } - else if (functionRef == ((void *)openbor_set_webm_property)) + else if (functionRef == ((void *)openbor_movie_stop)) { - return "set_webm_property"; + return "movie_stop"; } - else if (functionRef == ((void *)openbor_stop_webm)) + else if (functionRef == ((void *)openbor_movie_get_channel_object)) { - return "stop_webm"; + return "movie_get_channel_object"; + } + else if (functionRef == ((void *)openbor_movie_get_channel_index)) + { + return "movie_get_channel_index"; + } + else if (functionRef == ((void *)openbor_movie_get_channel_mask)) + { + return "movie_get_channel_mask"; + } + else if (functionRef == ((void *)openbor_movie_get_property)) + { + return "movie_get_property"; + } + else if (functionRef == ((void *)openbor_movie_set_property)) + { + return "movie_set_property"; } else if (functionRef == ((void *)openbor_playgif)) { @@ -1784,19 +1800,27 @@ void Script_LoadSystemFunctions() List_InsertAfter(&theFunctionList, (void *)openbor_playwebm, "playwebm"); List_InsertAfter(&theFunctionList, - (void *)openbor_open_webm, "open_webm"); + (void *)openbor_movie_load, "movie_load"); + List_InsertAfter(&theFunctionList, + (void *)openbor_movie_unload, "movie_unload"); + List_InsertAfter(&theFunctionList, + (void *)openbor_movie_play, "movie_play"); + List_InsertAfter(&theFunctionList, + (void *)openbor_movie_set_screen, "movie_set_screen"); + List_InsertAfter(&theFunctionList, + (void *)openbor_movie_set_sound_channel, "movie_set_sound_channel"); List_InsertAfter(&theFunctionList, - (void *)openbor_get_webm_channel_object, "get_webm_channel_object"); + (void *)openbor_movie_stop, "movie_stop"); List_InsertAfter(&theFunctionList, - (void *)openbor_get_webm_channel_index, "get_webm_channel_index"); + (void *)openbor_movie_get_channel_object, "movie_get_channel_object"); List_InsertAfter(&theFunctionList, - (void *)openbor_get_webm_channel_mask, "get_webm_channel_mask"); + (void *)openbor_movie_get_channel_index, "movie_get_channel_index"); List_InsertAfter(&theFunctionList, - (void *)openbor_get_webm_property, "get_webm_property"); + (void *)openbor_movie_get_channel_mask, "movie_get_channel_mask"); List_InsertAfter(&theFunctionList, - (void *)openbor_set_webm_property, "set_webm_property"); + (void *)openbor_movie_get_property, "movie_get_property"); List_InsertAfter(&theFunctionList, - (void *)openbor_stop_webm, "stop_webm"); + (void *)openbor_movie_set_property, "movie_set_property"); List_InsertAfter(&theFunctionList, (void *)openbor_playgif, "playgif"); List_InsertAfter(&theFunctionList, diff --git a/engine/source/openborscript/movie.c b/engine/source/openborscript/movie.c new file mode 100644 index 000000000..fa28ad2cd --- /dev/null +++ b/engine/source/openborscript/movie.c @@ -0,0 +1,579 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#include "scriptcommon.h" + +extern s_savedata savedata; + +#ifdef WEBM + +typedef struct s_movie_property_info { + e_movie_properties property; + e_property_access_config_flags config_flags; + size_t offset; + const char *id_string; + VARTYPE type; +} s_movie_property_info; + +#define PROPERTY_MEMBER_OFFSET(type, member) ((size_t)&(((type*)0)->member)) + +static const s_movie_property_info movie_properties[] = { + { MOVIE_PROPERTY_ACTIVE, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_movie_playback, active), + "MOVIE_PROPERTY_ACTIVE", VT_INTEGER }, + { MOVIE_PROPERTY_BLACK_FILTER, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, black_filter), + "MOVIE_PROPERTY_BLACK_FILTER", VT_INTEGER }, + { MOVIE_PROPERTY_CHANNEL, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_movie_playback, index), + "MOVIE_PROPERTY_CHANNEL", VT_INTEGER }, + { MOVIE_PROPERTY_DURATION, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_movie_playback, duration), + "MOVIE_PROPERTY_DURATION", VT_UINTEGER64 }, + { MOVIE_PROPERTY_HEIGHT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, height), + "MOVIE_PROPERTY_HEIGHT", VT_INTEGER }, + { MOVIE_PROPERTY_INTERRUPT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, interrupt), + "MOVIE_PROPERTY_INTERRUPT", VT_INTEGER }, + { MOVIE_PROPERTY_OFFSET_X, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, offset_x), + "MOVIE_PROPERTY_OFFSET_X", VT_INTEGER }, + { MOVIE_PROPERTY_OFFSET_Y, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, offset_y), + "MOVIE_PROPERTY_OFFSET_Y", VT_INTEGER }, + { MOVIE_PROPERTY_PAUSED, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, paused), + "MOVIE_PROPERTY_PAUSED", VT_INTEGER }, + { MOVIE_PROPERTY_POSITION, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, position), + "MOVIE_PROPERTY_POSITION", VT_UINTEGER64 }, + { MOVIE_PROPERTY_REPEAT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, repeat), + "MOVIE_PROPERTY_REPEAT", VT_INTEGER }, + { MOVIE_PROPERTY_SCREEN, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_movie_playback, screen), + "MOVIE_PROPERTY_SCREEN", VT_PTR }, + { MOVIE_PROPERTY_SOUND_CHANNEL, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_movie_playback, sound_channel), + "MOVIE_PROPERTY_SOUND_CHANNEL", VT_INTEGER }, + { MOVIE_PROPERTY_SOURCE, PROPERTY_ACCESS_CONFIG_READ, + PROPERTY_MEMBER_OFFSET(s_movie_playback, source_id), + "MOVIE_PROPERTY_SOURCE", VT_INTEGER }, + { MOVIE_PROPERTY_SPEED, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, speed), + "MOVIE_PROPERTY_SPEED", VT_DECIMAL }, + { MOVIE_PROPERTY_WIDTH, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, + PROPERTY_MEMBER_OFFSET(s_movie_playback, width), + "MOVIE_PROPERTY_WIDTH", VT_INTEGER }, + { MOVIE_PROPERTY_END, PROPERTY_ACCESS_CONFIG_NONE, 0, + "Movie", VT_EMPTY } +}; + +#undef PROPERTY_MEMBER_OFFSET + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Build metadata for the stable movie playback object API. +*/ +static const s_property_access_map movie_get_property_map( + const void *acting_object_param, + const unsigned int property_index_param +) +{ + s_property_access_map property_map = { 0 }; + const s_movie_playback *acting_object = acting_object_param; + const s_movie_property_info *info = NULL; + size_t property_cursor; + + for(property_cursor = 0; + property_cursor < sizeof(movie_properties) / sizeof(movie_properties[0]); + property_cursor++) { + if(movie_properties[property_cursor].property == property_index_param) { + info = &movie_properties[property_cursor]; + break; + } + } + + if(info) { + property_map.config_flags = info->config_flags; + property_map.field = + (const void*)((const char*)acting_object + info->offset); + property_map.id_string = info->id_string; + property_map.type = info->type; + } else { + property_map.config_flags = PROPERTY_ACCESS_CONFIG_NONE; + property_map.id_string = "Movie"; + property_map.type = VT_EMPTY; + } + return property_map; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Load one reusable streamed or cached movie source and return +* its stable integer ID. +*/ +HRESULT openbor_movie_load( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + const char *self_name = "movie_load(string path, int loading)"; + LONG loading = MOVIE_LOADING_STREAM; + int source_id; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 1 || + varlist[0]->vt != VT_STR || + (paramCount > 1 && + FAILED(ScriptVariant_IntegerValue(varlist[1], &loading))) || + loading < 0 || loading >= MOVIE_LOADING_END) { + printf("\nScript error: %s. Invalid path or loading mode.\n", + self_name); + *pretvar = NULL; + return E_FAIL; + } + + source_id = movie_source_load( + StrCache_Get(varlist[0]->strVal), + (e_movie_loading_mode)loading + ); + if(source_id < 0) { + printf("\nScript error: %s. Source could not be loaded.\n", + self_name); + *pretvar = NULL; + return E_FAIL; + } + + ScriptVariant_ChangeType(*pretvar, VT_INTEGER); + (*pretvar)->lVal = (LONG)source_id; + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Release one source ID when no playback still references it. +*/ +HRESULT openbor_movie_unload( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + LONG source_id; + + *pretvar = NULL; + if(paramCount < 1 || + FAILED(ScriptVariant_IntegerValue(varlist[0], &source_id)) || + !movie_source_unload((int)source_id)) { + printf("\nScript error: movie_unload(int source). Source is invalid or still in use.\n"); + return E_FAIL; + } + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Play a reusable source on the first free movie channel or +* replace the explicitly selected channel. +*/ +HRESULT openbor_movie_play( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + const char *self_name = "movie_play(int source, int channel)"; + LONG source_id; + LONG channel = MOVIE_CHANNEL_AUTO; + s_movie_playback *playback; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 1 || + FAILED(ScriptVariant_IntegerValue(varlist[0], &source_id))) { + goto error; + } + if(paramCount > 1 && + varlist[1]->vt != VT_EMPTY && + !(varlist[1]->vt == VT_PTR && !varlist[1]->ptrVal) && + FAILED(ScriptVariant_IntegerValue(varlist[1], &channel))) { + goto error; + } + if(channel != MOVIE_CHANNEL_AUTO && + (channel < 0 || (unsigned int)channel >= MOVIE_CHANNEL_COUNT)) { + goto error; + } + + playback = movie_playback_play( + (int)source_id, + (int)channel, + savedata.musicvol + ); + if(!playback) { + printf("\nScript error: %s. Source is invalid or no channel could be acquired.\n", + self_name); + *pretvar = NULL; + return E_FAIL; + } + + ScriptVariant_ChangeType(*pretvar, VT_PTR); + (*pretvar)->ptrVal = playback; + return S_OK; + +error: + printf("\nScript error: %s. Source or channel is invalid.\n", + self_name); + *pretvar = NULL; + return E_FAIL; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Assign a creator-owned 32-bit screen. Null restores the +* default main-screen fill behavior. +*/ +HRESULT openbor_movie_set_screen( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + s_screen *screen = NULL; + + *pretvar = NULL; + if(paramCount < 2 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + movie_playback_get_index(varlist[0]->ptrVal) < 0 || + (varlist[1]->vt != VT_PTR && varlist[1]->vt != VT_EMPTY)) { + goto error; + } + if(varlist[1]->vt == VT_PTR) { + screen = varlist[1]->ptrVal; + } + if(!movie_playback_set_screen(varlist[0]->ptrVal, screen)) { + goto error; + } + return S_OK; + +error: + printf("\nScript error: movie_set_screen(void playback, void screen). Playback or screen is invalid.\n"); + return E_FAIL; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Reopen a playback at its current position on a selected +* sound channel without changing its movie channel. +*/ +HRESULT openbor_movie_set_sound_channel( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + LONG channel; + + *pretvar = NULL; + if(paramCount < 2 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + FAILED(ScriptVariant_IntegerValue(varlist[1], &channel)) || + !movie_playback_set_sound_channel( + varlist[0]->ptrVal, + (int)channel, + savedata.musicvol + )) { + printf("\nScript error: movie_set_sound_channel(void playback, int channel). Playback or channel is invalid.\n"); + return E_FAIL; + } + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stop and recycle one stable movie playback channel. +*/ +HRESULT openbor_movie_stop( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + *pretvar = NULL; + if(paramCount < 1 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + movie_playback_get_index(varlist[0]->ptrVal) < 0) { + printf("\nScript error: movie_stop(void playback). Playback is invalid.\n"); + return E_FAIL; + } + movie_playback_stop(varlist[0]->ptrVal); + return S_OK; +} + +HRESULT openbor_movie_get_channel_object( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + LONG channel; + s_movie_playback *playback; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 1 || + FAILED(ScriptVariant_IntegerValue(varlist[0], &channel)) || + channel < 0 || + (unsigned int)channel >= MOVIE_CHANNEL_COUNT || + !movie_playback_init()) { + printf("\nScript error: movie_get_channel_object(int channel). Channel must be from 0 through %u.\n", + MOVIE_CHANNEL_COUNT - 1U); + *pretvar = NULL; + return E_FAIL; + } + + playback = movie_playback_get((int)channel); + ScriptVariant_ChangeType(*pretvar, VT_PTR); + (*pretvar)->ptrVal = playback; + return S_OK; +} + +HRESULT openbor_movie_get_channel_index( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + int channel; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 1 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + (channel = movie_playback_get_index(varlist[0]->ptrVal)) < 0) { + printf("\nScript error: movie_get_channel_index(void playback). Playback is invalid.\n"); + *pretvar = NULL; + return E_FAIL; + } + + ScriptVariant_ChangeType(*pretvar, VT_INTEGER); + (*pretvar)->lVal = (LONG)channel; + return S_OK; +} + +HRESULT openbor_movie_get_channel_mask( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + (void)varlist; + (void)paramCount; + ScriptVariant_ChangeType(*pretvar, VT_UINTEGER64); + (*pretvar)->ullVal = movie_playback_get_active_mask(); + return S_OK; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Read one synchronized movie playback property or dump its +* complete public property map for diagnostics. +*/ +HRESULT openbor_movie_get_property( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + s_movie_playback snapshot; + int property_index; + s_property_access_map property_map; + + ScriptVariant_Clear(*pretvar); + if(paramCount < 2 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + varlist[1]->vt != VT_INTEGER || + !movie_playback_get_snapshot(varlist[0]->ptrVal, &snapshot)) { + goto error; + } + + property_index = (int)varlist[1]->lVal; + if(property_index >= 0 && property_index < MOVIE_PROPERTY_END) { + property_map = movie_get_property_map( + &snapshot, + (unsigned int)property_index + ); + return property_access_get_member(&property_map, *pretvar); + } + if(property_index == PROPERTY_ACCESS_DUMP) { + property_access_dump_members( + movie_get_property_map, + MOVIE_PROPERTY_END, + &snapshot + ); + return S_OK; + } + +error: + printf("\nScript error: movie_get_property(void playback, int property). Playback or property is invalid.\n"); + *pretvar = NULL; + return E_FAIL; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Route writable properties through lifecycle-aware helpers so +* clocks, decoding, scaling, and composition remain synchronized. +*/ +HRESULT openbor_movie_set_property( + ScriptVariant **varlist, + ScriptVariant **pretvar, + int paramCount +) +{ + s_movie_playback *playback; + int property_index; + LONG integer_value; + DOUBLE decimal_value; + uint64_t unsigned_value; + bool result = false; + + *pretvar = NULL; + if(paramCount < 3 || + varlist[0]->vt != VT_PTR || + !varlist[0]->ptrVal || + varlist[1]->vt != VT_INTEGER || + movie_playback_get_index(varlist[0]->ptrVal) < 0) { + goto error_object; + } + + playback = varlist[0]->ptrVal; + property_index = (int)varlist[1]->lVal; + switch((e_movie_properties)property_index) { + case MOVIE_PROPERTY_BLACK_FILTER: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_black_filter(playback, integer_value != 0); + break; + case MOVIE_PROPERTY_HEIGHT: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_height(playback, (int)integer_value); + break; + case MOVIE_PROPERTY_INTERRUPT: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_interrupt(playback, integer_value != 0); + break; + case MOVIE_PROPERTY_OFFSET_X: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_offset_x(playback, (int)integer_value); + break; + case MOVIE_PROPERTY_OFFSET_Y: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_offset_y(playback, (int)integer_value); + break; + case MOVIE_PROPERTY_PAUSED: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_paused(playback, integer_value != 0); + break; + case MOVIE_PROPERTY_POSITION: + result = SUCCEEDED(ScriptVariant_Unsigned64Value(varlist[2], &unsigned_value)) && + movie_playback_set_position(playback, unsigned_value, savedata.musicvol); + break; + case MOVIE_PROPERTY_REPEAT: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_repeat(playback, integer_value != 0); + break; + case MOVIE_PROPERTY_SPEED: + result = SUCCEEDED(ScriptVariant_DecimalValue(varlist[2], &decimal_value)) && + movie_playback_set_speed(playback, decimal_value); + break; + case MOVIE_PROPERTY_WIDTH: + result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && + movie_playback_set_width(playback, (int)integer_value); + break; + case MOVIE_PROPERTY_ACTIVE: + case MOVIE_PROPERTY_CHANNEL: + case MOVIE_PROPERTY_DURATION: + case MOVIE_PROPERTY_SCREEN: + case MOVIE_PROPERTY_SOUND_CHANNEL: + case MOVIE_PROPERTY_SOURCE: + case MOVIE_PROPERTY_END: + default: + printf("\nScript error: movie_set_property(void playback, int property, value). Property id %d is read-only or unknown.\n", + property_index); + return E_FAIL; + } + + if(result) { + return S_OK; + } + printf("\nScript error: movie_set_property(void playback, int property, value). Value is invalid for property id %d.\n", + property_index); + return E_FAIL; + +error_object: + printf("\nScript error: movie_set_property(void playback, int property, value). Playback or property is invalid.\n"); + return E_FAIL; +} + +#else + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Keep the generic movie API visible on decoder-free builds +* while reporting that playback is unavailable. +*/ +static HRESULT openbor_movie_unsupported(ScriptVariant **pretvar) +{ + ScriptVariant_Clear(*pretvar); + printf("Movie playback is not supported on this platform.\n"); + return S_OK; +} + +#define MOVIE_UNSUPPORTED(name) \ + HRESULT name(ScriptVariant **v, ScriptVariant **r, int c) \ + { (void)v; (void)c; return openbor_movie_unsupported(r); } + +MOVIE_UNSUPPORTED(openbor_movie_load) +MOVIE_UNSUPPORTED(openbor_movie_unload) +MOVIE_UNSUPPORTED(openbor_movie_play) +MOVIE_UNSUPPORTED(openbor_movie_set_screen) +MOVIE_UNSUPPORTED(openbor_movie_set_sound_channel) +MOVIE_UNSUPPORTED(openbor_movie_stop) +MOVIE_UNSUPPORTED(openbor_movie_get_channel_object) +MOVIE_UNSUPPORTED(openbor_movie_get_channel_index) +MOVIE_UNSUPPORTED(openbor_movie_get_channel_mask) +MOVIE_UNSUPPORTED(openbor_movie_get_property) +MOVIE_UNSUPPORTED(openbor_movie_set_property) + +#undef MOVIE_UNSUPPORTED + +#endif diff --git a/engine/source/openborscript/movie.h b/engine/source/openborscript/movie.h new file mode 100644 index 000000000..69aed4920 --- /dev/null +++ b/engine/source/openborscript/movie.h @@ -0,0 +1,44 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#ifndef OPENBORSCRIPT_MOVIE_H +#define OPENBORSCRIPT_MOVIE_H + +typedef enum e_movie_properties { + MOVIE_PROPERTY_ACTIVE, + MOVIE_PROPERTY_BLACK_FILTER, + MOVIE_PROPERTY_CHANNEL, + MOVIE_PROPERTY_DURATION, /* Milliseconds, read only. */ + MOVIE_PROPERTY_HEIGHT, + MOVIE_PROPERTY_INTERRUPT, + MOVIE_PROPERTY_OFFSET_X, + MOVIE_PROPERTY_OFFSET_Y, + MOVIE_PROPERTY_PAUSED, + MOVIE_PROPERTY_POSITION, /* Milliseconds; writing seeks. */ + MOVIE_PROPERTY_REPEAT, + MOVIE_PROPERTY_SCREEN, + MOVIE_PROPERTY_SOUND_CHANNEL, + MOVIE_PROPERTY_SOURCE, + MOVIE_PROPERTY_SPEED, + MOVIE_PROPERTY_WIDTH, + MOVIE_PROPERTY_END +} e_movie_properties; + +HRESULT openbor_movie_load(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_unload(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_play(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_set_screen(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_set_sound_channel(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_stop(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_get_channel_object(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_get_channel_index(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_get_channel_mask(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_get_property(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_set_property(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); + +#endif diff --git a/engine/source/openborscript/webm.c b/engine/source/openborscript/webm.c deleted file mode 100644 index 6bb51506b..000000000 --- a/engine/source/openborscript/webm.c +++ /dev/null @@ -1,485 +0,0 @@ -/* - * OpenBOR - http://www.chronocrash.com - * ----------------------------------------------------------------------- - * All rights reserved. See LICENSE in OpenBOR root for license details. - * - * Copyright (c) OpenBOR Team - */ - -#include "scriptcommon.h" - -extern s_savedata savedata; - -#ifdef WEBM - -typedef struct s_webm_property_info { - e_webm_properties property; - e_property_access_config_flags config_flags; - size_t offset; - const char *id_string; - VARTYPE type; -} s_webm_property_info; - -#define PROPERTY_MEMBER_OFFSET(type, member) ((size_t)&(((type*)0)->member)) - -static const s_webm_property_info webm_properties[] = { - { WEBM_PROPERTY_ACTIVE, PROPERTY_ACCESS_CONFIG_READ, - PROPERTY_MEMBER_OFFSET(s_webm_playback, active), - "WEBM_PROPERTY_ACTIVE", VT_INTEGER }, - { WEBM_PROPERTY_CHANNEL, PROPERTY_ACCESS_CONFIG_READ, - PROPERTY_MEMBER_OFFSET(s_webm_playback, index), - "WEBM_PROPERTY_CHANNEL", VT_INTEGER }, - { WEBM_PROPERTY_DISPLAY_MODE, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, display_mode), - "WEBM_PROPERTY_DISPLAY_MODE", VT_INTEGER }, - { WEBM_PROPERTY_DURATION, PROPERTY_ACCESS_CONFIG_READ, - PROPERTY_MEMBER_OFFSET(s_webm_playback, duration), - "WEBM_PROPERTY_DURATION", VT_UINTEGER64 }, - { WEBM_PROPERTY_HEIGHT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, height), - "WEBM_PROPERTY_HEIGHT", VT_INTEGER }, - { WEBM_PROPERTY_INTERRUPT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, interrupt), - "WEBM_PROPERTY_INTERRUPT", VT_INTEGER }, - { WEBM_PROPERTY_LOADING, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, loading_mode), - "WEBM_PROPERTY_LOADING", VT_INTEGER }, - { WEBM_PROPERTY_OFFSET_X, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, offset_x), - "WEBM_PROPERTY_OFFSET_X", VT_INTEGER }, - { WEBM_PROPERTY_OFFSET_Y, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, offset_y), - "WEBM_PROPERTY_OFFSET_Y", VT_INTEGER }, - { WEBM_PROPERTY_PAUSED, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, paused), - "WEBM_PROPERTY_PAUSED", VT_INTEGER }, - { WEBM_PROPERTY_POSITION, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, position), - "WEBM_PROPERTY_POSITION", VT_UINTEGER64 }, - { WEBM_PROPERTY_REPEAT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, repeat), - "WEBM_PROPERTY_REPEAT", VT_INTEGER }, - { WEBM_PROPERTY_SCREEN, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, screen), - "WEBM_PROPERTY_SCREEN", VT_PTR }, - { WEBM_PROPERTY_SOUND_CHANNEL, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, sound_channel), - "WEBM_PROPERTY_SOUND_CHANNEL", VT_INTEGER }, - { WEBM_PROPERTY_SPEED, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, speed), - "WEBM_PROPERTY_SPEED", VT_DECIMAL }, - { WEBM_PROPERTY_WIDTH, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, - PROPERTY_MEMBER_OFFSET(s_webm_playback, width), - "WEBM_PROPERTY_WIDTH", VT_INTEGER }, - { WEBM_PROPERTY_END, PROPERTY_ACCESS_CONFIG_NONE, 0, - "WebM", VT_EMPTY } -}; - -#undef PROPERTY_MEMBER_OFFSET - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Build property metadata for a stable WebM channel object. -*/ -static const s_property_access_map webm_get_property_map( - const void *acting_object_param, - const unsigned int property_index_param -) -{ - s_property_access_map property_map = { 0 }; - const s_webm_playback *acting_object = acting_object_param; - const s_webm_property_info *info = NULL; - size_t property_cursor; - - for(property_cursor = 0; - property_cursor < sizeof(webm_properties) / sizeof(webm_properties[0]); - property_cursor++) { - if(webm_properties[property_cursor].property == property_index_param) { - info = &webm_properties[property_cursor]; - break; - } - } - - if(info) { - property_map.config_flags = info->config_flags; - property_map.field = (const void*)((const char*)acting_object + info->offset); - property_map.id_string = info->id_string; - property_map.type = info->type; - } else { - property_map.config_flags = PROPERTY_ACCESS_CONFIG_NONE; - property_map.field = NULL; - property_map.id_string = "WebM"; - property_map.type = VT_EMPTY; - } - return property_map; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Start nonblocking playback on an automatic or forced video -* channel and render into a creator-owned 32-bit screen. -*/ -HRESULT openbor_open_webm( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - const char *self_name = - "open_webm(string path, void screen, int video_channel, int sound_channel, int interrupt, int loading)"; - s_webm_playback *playback; - s_screen *screen; - LONG video_channel = WEBM_PLAYBACK_CHANNEL_AUTO; - LONG sound_channel = SOUND_CHANNEL_MUSIC_DEFAULT; - LONG interrupt = 1; - LONG loading_mode = WEBM_LOADING_STREAM; - - ScriptVariant_Clear(*pretvar); - if(paramCount < 2 || - varlist[0]->vt != VT_STR || - varlist[1]->vt != VT_PTR || - !varlist[1]->ptrVal || - (paramCount > 2 && FAILED(ScriptVariant_IntegerValue(varlist[2], &video_channel))) || - (paramCount > 3 && FAILED(ScriptVariant_IntegerValue(varlist[3], &sound_channel))) || - (paramCount > 4 && FAILED(ScriptVariant_IntegerValue(varlist[4], &interrupt))) || - (paramCount > 5 && FAILED(ScriptVariant_IntegerValue(varlist[5], &loading_mode))) || - (video_channel != WEBM_PLAYBACK_CHANNEL_AUTO && - (video_channel < 0 || - (unsigned int)video_channel >= WEBM_PLAYBACK_CHANNEL_COUNT)) || - sound_channel < 0 || - (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || - loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { - printf("\nScript error: %s. Invalid path, screen, video channel, sound channel, interrupt, or loading mode.\n", - self_name); - *pretvar = NULL; - return E_FAIL; - } - - screen = (s_screen*)varlist[1]->ptrVal; - playback = webm_playback_open( - StrCache_Get(varlist[0]->strVal), - screen, - (int)video_channel, - (int)sound_channel, - interrupt != 0, - (e_webm_loading_mode)loading_mode, - savedata.musicvol - ); - if(playback) { - ScriptVariant_ChangeType(*pretvar, VT_PTR); - (*pretvar)->ptrVal = playback; - return S_OK; - } - - printf("\nScript error: %s. Playback could not acquire or replace the requested channel, screen, or decoder.\n", - self_name); - *pretvar = NULL; - return E_FAIL; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Return one stable object from the single 64-channel bank. -*/ -HRESULT openbor_get_webm_channel_object( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - LONG channel; - s_webm_playback *playback; - - ScriptVariant_Clear(*pretvar); - if(paramCount < 1 || - FAILED(ScriptVariant_IntegerValue(varlist[0], &channel)) || - channel < 0 || - (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT || - !webm_playback_init()) { - printf("\nScript error: get_webm_channel_object(int channel). Channel must be from 0 through %u.\n", - WEBM_PLAYBACK_CHANNEL_COUNT - 1U); - *pretvar = NULL; - return E_FAIL; - } - - playback = webm_playback_get((int)channel); - if(playback) { - ScriptVariant_ChangeType(*pretvar, VT_PTR); - (*pretvar)->ptrVal = playback; - } - return S_OK; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Resolve a validated playback pointer to its channel index. -*/ -HRESULT openbor_get_webm_channel_index( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - int channel; - - ScriptVariant_Clear(*pretvar); - if(paramCount < 1 || - varlist[0]->vt != VT_PTR || - !varlist[0]->ptrVal || - (channel = webm_playback_get_index(varlist[0]->ptrVal)) < 0) { - printf("\nScript error: get_webm_channel_index(void playback). You must provide a valid WebM object.\n"); - *pretvar = NULL; - return E_FAIL; - } - - ScriptVariant_ChangeType(*pretvar, VT_INTEGER); - (*pretvar)->lVal = (LONG)channel; - return S_OK; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Return the active mask for the single 64-channel video bank. -*/ -HRESULT openbor_get_webm_channel_mask( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - (void)varlist; - (void)paramCount; - - ScriptVariant_ChangeType(*pretvar, VT_UINTEGER64); - (*pretvar)->ullVal = webm_playback_get_active_mask(); - return S_OK; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Return a synchronized snapshot property or diagnostic dump. -*/ -HRESULT openbor_get_webm_property( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - s_webm_playback snapshot; - int property_index; - s_property_access_map property_map; - - ScriptVariant_Clear(*pretvar); - if(paramCount < 2 || - varlist[0]->vt != VT_PTR || - !varlist[0]->ptrVal || - varlist[1]->vt != VT_INTEGER || - !webm_playback_get_snapshot(varlist[0]->ptrVal, &snapshot)) { - printf("\nScript error: get_webm_property(void playback, int property). Invalid object or property.\n"); - *pretvar = NULL; - return E_FAIL; - } - - property_index = (int)varlist[1]->lVal; - if(property_index >= 0 && property_index < WEBM_PROPERTY_END) { - property_map = webm_get_property_map(&snapshot, (unsigned int)property_index); - return property_access_get_member(&property_map, *pretvar); - } - if(property_index == PROPERTY_ACCESS_DUMP) { - property_access_dump_members( - webm_get_property_map, - WEBM_PROPERTY_END, - &snapshot - ); - return S_OK; - } - - printf("\nScript error: get_webm_property(void playback, int property). Unknown property id (%d).\n", - property_index); - *pretvar = NULL; - return E_FAIL; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Route mutable WebM fields through lifecycle-aware setters -* so seek, audio, clocks, scaling, and screen ownership agree. -*/ -HRESULT openbor_set_webm_property( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - s_webm_playback *playback; - int property_index; - LONG integer_value; - DOUBLE decimal_value; - uint64_t unsigned_value; - bool result = false; - - *pretvar = NULL; - if(paramCount < 3 || - varlist[0]->vt != VT_PTR || - !varlist[0]->ptrVal || - varlist[1]->vt != VT_INTEGER || - webm_playback_get_index(varlist[0]->ptrVal) < 0) { - goto error_object; - } - - playback = varlist[0]->ptrVal; - property_index = (int)varlist[1]->lVal; - switch((e_webm_properties)property_index) { - case WEBM_PROPERTY_DISPLAY_MODE: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_display_mode(playback, (e_webm_display_mode)integer_value); - break; - case WEBM_PROPERTY_HEIGHT: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_height(playback, (int)integer_value); - break; - case WEBM_PROPERTY_INTERRUPT: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_interrupt(playback, integer_value != 0); - break; - case WEBM_PROPERTY_LOADING: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_loading_mode( - playback, - (e_webm_loading_mode)integer_value, - savedata.musicvol - ); - break; - case WEBM_PROPERTY_OFFSET_X: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_offset_x(playback, (int)integer_value); - break; - case WEBM_PROPERTY_OFFSET_Y: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_offset_y(playback, (int)integer_value); - break; - case WEBM_PROPERTY_PAUSED: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_paused(playback, integer_value != 0); - break; - case WEBM_PROPERTY_POSITION: - result = SUCCEEDED(ScriptVariant_Unsigned64Value(varlist[2], &unsigned_value)) && - webm_playback_set_position(playback, unsigned_value, savedata.musicvol); - break; - case WEBM_PROPERTY_REPEAT: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_repeat(playback, integer_value != 0); - break; - case WEBM_PROPERTY_SCREEN: - result = varlist[2]->vt == VT_PTR && varlist[2]->ptrVal && - webm_playback_set_screen(playback, varlist[2]->ptrVal); - break; - case WEBM_PROPERTY_SOUND_CHANNEL: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_sound_channel( - playback, - (int)integer_value, - savedata.musicvol - ); - break; - case WEBM_PROPERTY_SPEED: - result = SUCCEEDED(ScriptVariant_DecimalValue(varlist[2], &decimal_value)) && - webm_playback_set_speed(playback, decimal_value); - break; - case WEBM_PROPERTY_WIDTH: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - webm_playback_set_width(playback, (int)integer_value); - break; - case WEBM_PROPERTY_ACTIVE: - case WEBM_PROPERTY_CHANNEL: - case WEBM_PROPERTY_DURATION: - case WEBM_PROPERTY_END: - default: - printf("\nScript error: set_webm_property(void playback, int property, value). Property id %d is read-only or unknown.\n", - property_index); - return E_FAIL; - } - - if(result) { - return S_OK; - } - printf("\nScript error: set_webm_property(void playback, int property, value). Invalid value for property id %d.\n", - property_index); - return E_FAIL; - -error_object: - printf("\nScript error: set_webm_property(void playback, int property, value). Invalid WebM object or property.\n"); - return E_FAIL; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Explicitly stop and recycle one WebM playback channel. -*/ -HRESULT openbor_stop_webm( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -) -{ - *pretvar = NULL; - if(paramCount < 1 || - varlist[0]->vt != VT_PTR || - !varlist[0]->ptrVal || - webm_playback_get_index(varlist[0]->ptrVal) < 0) { - printf("\nScript error: stop_webm(void playback). You must provide a valid WebM object.\n"); - return E_FAIL; - } - - webm_playback_stop(varlist[0]->ptrVal); - return S_OK; -} - -#else - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Keep WebM script names available on builds that exclude -* the decoder while reporting the unsupported configuration. -*/ -static HRESULT openbor_webm_unsupported(ScriptVariant **pretvar) -{ - ScriptVariant_Clear(*pretvar); - printf("WebM playback is not supported on this platform.\n"); - return S_OK; -} - -HRESULT openbor_open_webm(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } -HRESULT openbor_get_webm_channel_object(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } -HRESULT openbor_get_webm_channel_index(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } -HRESULT openbor_get_webm_channel_mask(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } -HRESULT openbor_get_webm_property(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } -HRESULT openbor_set_webm_property(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } -HRESULT openbor_stop_webm(ScriptVariant **v, ScriptVariant **r, int c) -{ (void)v; (void)c; return openbor_webm_unsupported(r); } - -#endif diff --git a/engine/source/openborscript/webm.h b/engine/source/openborscript/webm.h deleted file mode 100644 index 3feca5bcc..000000000 --- a/engine/source/openborscript/webm.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * OpenBOR - http://www.chronocrash.com - * ----------------------------------------------------------------------- - * All rights reserved. See LICENSE in OpenBOR root for license details. - * - * Copyright (c) OpenBOR Team - */ - -#ifndef OPENBORSCRIPT_WEBM_H -#define OPENBORSCRIPT_WEBM_H - -typedef enum e_webm_properties { - WEBM_PROPERTY_ACTIVE, - WEBM_PROPERTY_CHANNEL, - WEBM_PROPERTY_DISPLAY_MODE, - WEBM_PROPERTY_DURATION, /* Milliseconds, read only. */ - WEBM_PROPERTY_HEIGHT, - WEBM_PROPERTY_INTERRUPT, - WEBM_PROPERTY_LOADING, - WEBM_PROPERTY_OFFSET_X, - WEBM_PROPERTY_OFFSET_Y, - WEBM_PROPERTY_PAUSED, - WEBM_PROPERTY_POSITION, /* Milliseconds; writing seeks. */ - WEBM_PROPERTY_REPEAT, - WEBM_PROPERTY_SCREEN, - WEBM_PROPERTY_SOUND_CHANNEL, - WEBM_PROPERTY_SPEED, - WEBM_PROPERTY_WIDTH, - WEBM_PROPERTY_END -} e_webm_properties; - -HRESULT openbor_open_webm( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); -HRESULT openbor_get_webm_channel_object( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); -HRESULT openbor_get_webm_channel_index( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); -HRESULT openbor_get_webm_channel_mask( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); -HRESULT openbor_get_webm_property( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); -HRESULT openbor_set_webm_property( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); -HRESULT openbor_stop_webm( - ScriptVariant **varlist, - ScriptVariant **pretvar, - int paramCount -); - -#endif diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c new file mode 100644 index 000000000..f63eb254a --- /dev/null +++ b/engine/source/webmlib/movie_playback.c @@ -0,0 +1,1283 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#include +#include +#include +#include +#include +#include + +#include "globals.h" +#include "screen.h" +#include "sound_channel.h" +#include "soundmix.h" +#include "timer.h" +#include "vidplay.h" +#include "movie_playback.h" + +#define MOVIE_SOURCE_CAPACITY_INITIAL 64U +#define MOVIE_REVERSE_SEEK_INTERVAL UINT64_C(33333333) + +extern int buffer_pakfile(const char *filename, char **pbuffer, size_t *psize); + +typedef struct s_movie_source { + char *path; + unsigned char *cache_buffer; + size_t cache_size; + unsigned int references; + e_movie_loading_mode loading_mode; + int active; + int id; +} s_movie_source; + +typedef struct s_movie_playback_pool { + s_movie_playback channel[MOVIE_CHANNEL_COUNT]; + s_movie_source *source; + size_t source_capacity; + int next_source_id; + uint64_t active_mask; + int initialized; + int yuv_initialized; +} s_movie_playback_pool; + +static s_movie_playback_pool movie_playback_pool; + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Validate screens and allocate exact logical dimensions +* over the engine's four-pixel-aligned screen storage. +*/ +static bool movie_playback_screen_valid(const s_screen *screen) +{ + return screen && + screen->magic == screen_magic && + screen->pixelformat == PIXEL_32 && + screen->width > 0 && + screen->height > 0; +} + +static bool movie_playback_dimensions_valid(int width, int height) +{ + int allocation_width; + + if(width < 1 || height < 1 || width > INT_MAX - 3) { + return false; + } + allocation_width = (width + 3) & ~3; + return allocation_width <= + INT_MAX / height / (int)sizeof(uint32_t); +} + +static s_screen *movie_playback_alloc_screen(int width, int height) +{ + s_screen *screen; + + if(!movie_playback_dimensions_valid(width, height)) { + return NULL; + } + + screen = allocscreen((width + 3) & ~3, height, PIXEL_32); + if(screen) { + screen->width = width; + } + return screen; +} + +static char *movie_copy_path(const char *path) +{ + char *copy; + size_t length; + + if(!path || !path[0]) { + return NULL; + } + + length = strlen(path) + 1; + copy = malloc(length); + if(copy) { + memcpy(copy, path, length); + } + return copy; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Grow stable integer-addressed movie source storage. IDs are +* never reused during a session, so unloaded IDs stay invalid. +*/ +static bool movie_source_reserve(size_t required_capacity) +{ + s_movie_source *source; + size_t previous_capacity; + size_t capacity; + + if(required_capacity <= movie_playback_pool.source_capacity) { + return true; + } + + previous_capacity = movie_playback_pool.source_capacity; + capacity = previous_capacity + ? previous_capacity + : MOVIE_SOURCE_CAPACITY_INITIAL; + while(capacity < required_capacity) { + if(capacity > SIZE_MAX / 2U) { + return false; + } + capacity *= 2U; + } + if(capacity > SIZE_MAX / sizeof(*source)) { + return false; + } + + source = realloc( + movie_playback_pool.source, + capacity * sizeof(*source) + ); + if(!source) { + return false; + } + memset( + source + previous_capacity, + 0, + (capacity - previous_capacity) * sizeof(*source) + ); + movie_playback_pool.source = source; + movie_playback_pool.source_capacity = capacity; + return true; +} + +static s_movie_source *movie_source_get(int source_id) +{ + s_movie_source *source; + + if(!movie_playback_pool.initialized || + source_id < 0 || + (size_t)source_id >= movie_playback_pool.source_capacity) { + return NULL; + } + + source = &movie_playback_pool.source[source_id]; + return source->active && source->id == source_id ? source : NULL; +} + +static void movie_source_release(int source_id) +{ + s_movie_source *source = movie_source_get(source_id); + + if(source && source->references) { + --source->references; + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Close decoder-owned resources separately from the retained +* current frame so seeks can display the old frame until ready. +*/ +static void movie_playback_close_decoder(s_movie_playback *playback) +{ + if(playback->context) { + webm_close(playback->context); + playback->context = NULL; + } + yuv_frame_destroy(playback->next_frame); + playback->next_frame = NULL; +} + +static void movie_playback_close_media(s_movie_playback *playback) +{ + if(!playback) { + return; + } + + movie_playback_close_decoder(playback); + yuv_frame_destroy(playback->current_frame); + playback->current_frame = NULL; + if(playback->rgb_frame) { + freescreen(&playback->rgb_frame); + } + if(playback->scaled_frame) { + freescreen(&playback->scaled_frame); + } +} + +static void movie_playback_reset_record(s_movie_playback *playback) +{ + e_object_type object_type; + int index; + int source_id; + + if(!playback) { + return; + } + + object_type = playback->object_type; + index = playback->index; + source_id = playback->source_id; + movie_playback_close_media(playback); + movie_source_release(source_id); + memset(playback, 0, sizeof(*playback)); + playback->object_type = object_type; + playback->index = index; + playback->source_id = -1; + playback->sound_channel = SOUND_CHANNEL_MUSIC_DEFAULT; + playback->speed = 1.0; + playback->interrupt = 1; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Initialize one stable bank of 64 movie playback objects, +* dynamic source storage, and shared 32-bit YUV conversion. +*/ +bool movie_playback_init(void) +{ + unsigned int channel; + + if(movie_playback_pool.initialized) { + return true; + } + + memset(&movie_playback_pool, 0, sizeof(movie_playback_pool)); + movie_playback_pool.initialized = 1; + for(channel = 0; channel < MOVIE_CHANNEL_COUNT; channel++) { + movie_playback_pool.channel[channel].object_type = + OBJECT_TYPE_MOVIE_PLAYBACK; + movie_playback_pool.channel[channel].index = (int)channel; + movie_playback_pool.channel[channel].source_id = -1; + movie_playback_reset_record(&movie_playback_pool.channel[channel]); + } + + if(!movie_source_reserve(MOVIE_SOURCE_CAPACITY_INITIAL)) { + memset(&movie_playback_pool, 0, sizeof(movie_playback_pool)); + return false; + } + + yuv_init(4); + movie_playback_pool.yuv_initialized = 1; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stop every playback, release loaded sources, and clear the +* shared converter during script or engine shutdown. +*/ +void movie_playback_shutdown(void) +{ + size_t source_id; + + if(!movie_playback_pool.initialized) { + return; + } + + movie_playback_stop_all(); + for(source_id = 0; + source_id < movie_playback_pool.source_capacity; + source_id++) { + free(movie_playback_pool.source[source_id].path); + free(movie_playback_pool.source[source_id].cache_buffer); + } + free(movie_playback_pool.source); + if(movie_playback_pool.yuv_initialized) { + yuv_clear(); + } + memset(&movie_playback_pool, 0, sizeof(movie_playback_pool)); +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Restore 32-bit conversion after legacy fullscreen WebM +* playback temporarily uses the shared 16-bit converter. +*/ +void movie_playback_restore_yuv(void) +{ + if(movie_playback_pool.initialized) { + yuv_init(4); + movie_playback_pool.yuv_initialized = 1; + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Load one reusable movie source. Stream mode validates the +* path; cache mode retains one shared read-only byte buffer. +*/ +int movie_source_load(const char *path, e_movie_loading_mode loading_mode) +{ + s_movie_source source = { 0 }; + int handle = -1; + int source_id; + + if(!movie_playback_init() || + !path || !path[0] || + loading_mode < 0 || loading_mode >= MOVIE_LOADING_END || + movie_playback_pool.next_source_id == INT_MAX) { + return -1; + } + + source.path = movie_copy_path(path); + if(!source.path) { + return -1; + } + + if(loading_mode == MOVIE_LOADING_CACHE) { + if(buffer_pakfile( + path, + (char**)&source.cache_buffer, + &source.cache_size + ) != 1 || !source.cache_size) { + free(source.path); + free(source.cache_buffer); + return -1; + } + } else { + handle = openpackfile(path, packfile); + if(handle < 0) { + free(source.path); + return -1; + } + closepackfile(handle); + } + + source_id = movie_playback_pool.next_source_id; + if(!movie_source_reserve((size_t)source_id + 1U)) { + free(source.path); + free(source.cache_buffer); + return -1; + } + + source.id = source_id; + source.loading_mode = loading_mode; + source.active = 1; + movie_playback_pool.source[source_id] = source; + ++movie_playback_pool.next_source_id; + return source_id; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Unload an inactive source. Live playbacks retain their +* source until stopped and therefore reject premature unload. +*/ +bool movie_source_unload(int source_id) +{ + s_movie_source *source = movie_source_get(source_id); + + if(!source || source->references) { + return false; + } + + free(source->path); + free(source->cache_buffer); + memset(source, 0, sizeof(*source)); + source->id = -1; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Validate playback pointers by exact address arithmetic and +* stable channel metadata without dereferencing foreign data. +*/ +s_movie_playback *movie_playback_get(int channel) +{ + if(!movie_playback_pool.initialized || + channel < 0 || + (unsigned int)channel >= MOVIE_CHANNEL_COUNT) { + return NULL; + } + return &movie_playback_pool.channel[channel]; +} + +int movie_playback_get_index(const s_movie_playback *playback) +{ + const s_movie_playback *indexed_playback; + uintptr_t base_address; + uintptr_t playback_address; + uintptr_t playback_offset; + unsigned int channel; + + if(!movie_playback_pool.initialized || !playback) { + return -1; + } + + base_address = (uintptr_t)(const void*)&movie_playback_pool.channel[0]; + playback_address = (uintptr_t)(const void*)playback; + if(playback_address < base_address) { + return -1; + } + + playback_offset = playback_address - base_address; + if(playback_offset >= sizeof(movie_playback_pool.channel) || + playback_offset % sizeof(movie_playback_pool.channel[0])) { + return -1; + } + + channel = (unsigned int)( + playback_offset / sizeof(movie_playback_pool.channel[0]) + ); + indexed_playback = &movie_playback_pool.channel[channel]; + if((uintptr_t)(const void*)indexed_playback != playback_address || + indexed_playback->object_type != OBJECT_TYPE_MOVIE_PLAYBACK || + indexed_playback->index != (int)channel) { + return -1; + } + return (int)channel; +} + +bool movie_playback_get_snapshot( + const s_movie_playback *playback, + s_movie_playback *snapshot +) +{ + int channel = movie_playback_get_index(playback); + + if(channel < 0 || !snapshot) { + return false; + } + *snapshot = movie_playback_pool.channel[channel]; + return true; +} + +uint64_t movie_playback_get_active_mask(void) +{ + return movie_playback_pool.initialized + ? movie_playback_pool.active_mask + : UINT64_C(0); +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Resolve signed playback rate into a bounded nanosecond +* position, including stationary and reverse playback. +*/ +static uint64_t movie_playback_position_now( + const s_movie_playback *playback, + uint64_t now +) +{ + double elapsed; + double position; + + if(playback->paused || playback->speed == 0.0) { + return playback->position_anchor; + } + + elapsed = now >= playback->clock_anchor + ? (double)(now - playback->clock_anchor) * 1000.0 * playback->speed + : 0.0; + position = (double)playback->position_anchor + elapsed; + if(position <= 0.0) { + return 0; + } + if(position >= (double)UINT64_MAX) { + return UINT64_MAX; + } + return (uint64_t)position; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Open or seek the WebM backend from a reusable movie source +* while preserving the last frame until replacement is ready. +*/ +static bool movie_playback_open_media( + s_movie_playback *playback, + uint64_t position, + int volume +) +{ + s_movie_source *source = movie_source_get(playback->source_id); + yuv_video_mode previous_mode = playback->video_mode; + uint64_t duration; + unsigned int speed_percent; + + if(!source) { + return false; + } + + movie_playback_close_decoder(playback); + playback->volume = volume; + playback->context = webm_start_playback_ex( + source->path, + volume, + playback->sound_channel, + source->cache_buffer, + source->cache_size, + position, + 0 + ); + if(!playback->context) { + return false; + } + + webm_get_video_info(playback->context, &playback->video_mode); + duration = webm_get_duration(playback->context); + playback->duration = duration / UINT64_C(1000000); + if((playback->video_mode.width & 1) || + (playback->video_mode.height & 1) || + playback->video_mode.width > (INT_MAX >> 16) || + playback->video_mode.height > (INT_MAX >> 16) || + !movie_playback_dimensions_valid( + playback->video_mode.width, + playback->video_mode.height + )) { + printf( + "Error: Movie frame dimensions %d*%d cannot be represented by the 32-bit screen renderer.\n", + playback->video_mode.width, + playback->video_mode.height + ); + movie_playback_close_decoder(playback); + return false; + } + + if(previous_mode.width != playback->video_mode.width || + previous_mode.height != playback->video_mode.height) { + yuv_frame_destroy(playback->current_frame); + playback->current_frame = NULL; + if(playback->rgb_frame) freescreen(&playback->rgb_frame); + if(playback->scaled_frame) freescreen(&playback->scaled_frame); + } + if(!playback->rgb_frame) { + playback->rgb_frame = movie_playback_alloc_screen( + playback->video_mode.width, + playback->video_mode.height + ); + } + if(!playback->rgb_frame) { + movie_playback_close_decoder(playback); + return false; + } + + if(playback->speed > 0.0) { + speed_percent = (unsigned int)(playback->speed * 100.0 + 0.5); + if(speed_percent < 1U) speed_percent = 1U; + webm_set_audio_speed(playback->context, speed_percent); + } + webm_set_audio_paused( + playback->context, + playback->paused || playback->speed <= 0.0 + ); + + playback->position_anchor = position; + playback->clock_anchor = timer_uticks(); + playback->position = position / UINT64_C(1000000); + playback->reverse_pending = playback->speed < 0.0; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Create one playback from a reusable source. Automatic mode +* takes the lowest free slot; explicit selection replaces it. +*/ +s_movie_playback *movie_playback_play( + int source_id, + int movie_channel, + int volume +) +{ + s_movie_source *source; + s_movie_playback *playback; + uint64_t available_mask; + int channel; + + if(!movie_playback_init() || + !(source = movie_source_get(source_id)) || + (movie_channel != MOVIE_CHANNEL_AUTO && + (movie_channel < 0 || + (unsigned int)movie_channel >= MOVIE_CHANNEL_COUNT))) { + return NULL; + } + + if(movie_channel == MOVIE_CHANNEL_AUTO) { + available_mask = ~movie_playback_pool.active_mask; + channel = sound_channel_mask_first(available_mask); + if(channel < 0 || (unsigned int)channel >= MOVIE_CHANNEL_COUNT) { + return NULL; + } + } else { + channel = movie_channel; + } + + playback = &movie_playback_pool.channel[channel]; + movie_playback_stop(playback); + playback->source_id = source_id; + ++source->references; + if(!movie_playback_open_media(playback, 0, volume)) { + movie_playback_reset_record(playback); + return NULL; + } + + playback->active = 1; + movie_playback_pool.active_mask |= UINT64_C(1) << channel; + return playback; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stop individual, grouped, or screen-detached playbacks and +* release their source references and fixed channel slots. +*/ +void movie_playback_stop(s_movie_playback *playback) +{ + int channel = movie_playback_get_index(playback); + + if(channel < 0) { + return; + } + movie_playback_pool.active_mask &= ~(UINT64_C(1) << channel); + movie_playback_reset_record(&movie_playback_pool.channel[channel]); +} + +void movie_playback_stop_all(void) +{ + uint64_t active_mask; + int channel; + + if(!movie_playback_pool.initialized) { + return; + } + + active_mask = movie_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + active_mask &= ~(UINT64_C(1) << channel); + movie_playback_stop(&movie_playback_pool.channel[channel]); + } +} + +void movie_playback_detach_screen(const s_screen *screen) +{ + uint64_t active_mask; + int channel; + + if(!screen || !movie_playback_pool.initialized) { + return; + } + + active_mask = movie_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + s_movie_playback *playback = &movie_playback_pool.channel[channel]; + active_mask &= ~(UINT64_C(1) << channel); + if(playback->screen == screen) { + movie_playback_stop(playback); + } + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Convert and scale a current frame only when its pixels or +* destination dimensions change, then reuse it for composition. +*/ +static bool movie_playback_prepare_frame( + s_movie_playback *playback, + const s_screen *destination, + s_screen **render_frame +) +{ + int refresh = playback->frame_dirty; + int width = playback->width ? playback->width : destination->width; + int height = playback->height ? playback->height : destination->height; + + if(!playback->current_frame || !playback->rgb_frame) { + return false; + } + if(width < 1 || height < 1 || + !movie_playback_dimensions_valid(width, height)) { + return false; + } + + if(refresh) { + size_t pixel_count; + size_t pixel_index; + uint32_t *pixel; + uint32_t protected_black; + + yuv_to_rgb(playback->current_frame, playback->rgb_frame); + if(playback->black_filter) { + pixel_count = + (size_t)playback->rgb_frame->width * + (size_t)playback->rgb_frame->height; + pixel = (uint32_t*)playback->rgb_frame->data; + protected_black = colour32(1, 1, 1); + for(pixel_index = 0; + pixel_index < pixel_count; + pixel_index++) { + if(pixel[pixel_index] == 0) { + pixel[pixel_index] = protected_black; + } + } + } + } + if(width == playback->rgb_frame->width && + height == playback->rgb_frame->height) { + playback->frame_dirty = 0; + *render_frame = playback->rgb_frame; + return true; + } + + if(playback->scaled_frame && + (playback->scaled_frame->width != width || + playback->scaled_frame->height != height)) { + freescreen(&playback->scaled_frame); + } + if(!playback->scaled_frame) { + playback->scaled_frame = movie_playback_alloc_screen(width, height); + refresh = 1; + } + if(!playback->scaled_frame) { + return false; + } + if(refresh) { + scalescreen32(playback->scaled_frame, playback->rgb_frame); + } + + playback->frame_dirty = 0; + *render_frame = playback->scaled_frame; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Copy a prepared 32-bit movie frame with signed offsets and +* destination clipping independent of global draw state. +*/ +static void movie_playback_copy_frame( + s_screen *destination, + const s_screen *source, + int offset_x, + int offset_y +) +{ + const unsigned char *source_row; + unsigned char *destination_row; + int64_t destination_x = offset_x; + int64_t destination_y = offset_y; + int source_x = 0; + int source_y = 0; + int width = source->width; + int height = source->height; + int row; + + if(destination_x < 0) { + int64_t clipped_width = -destination_x; + if(clipped_width >= width) return; + source_x = (int)clipped_width; + width -= source_x; + destination_x = 0; + } + if(destination_y < 0) { + int64_t clipped_height = -destination_y; + if(clipped_height >= height) return; + source_y = (int)clipped_height; + height -= source_y; + destination_y = 0; + } + if(destination_x >= destination->width || + destination_y >= destination->height) { + return; + } + if(width > destination->width - destination_x) { + width = (int)(destination->width - destination_x); + } + if(height > destination->height - destination_y) { + height = (int)(destination->height - destination_y); + } + if(width <= 0 || height <= 0) { + return; + } + + source_row = source->data + + ((source_y * source->width + source_x) * (int)sizeof(uint32_t)); + destination_row = destination->data + + (((int)destination_y * destination->width + (int)destination_x) * + (int)sizeof(uint32_t)); + for(row = 0; row < height; row++) { + memcpy(destination_row, source_row, (size_t)width * sizeof(uint32_t)); + source_row += source->width * (int)sizeof(uint32_t); + destination_row += destination->width * (int)sizeof(uint32_t); + } +} + +static bool movie_playback_render_to( + s_movie_playback *playback, + s_screen *destination +) +{ + s_screen *render_frame; + + if(!movie_playback_screen_valid(destination)) { + return false; + } + if(!playback->current_frame) { + return true; + } + if(!movie_playback_prepare_frame( + playback, + destination, + &render_frame + )) { + return false; + } + + movie_playback_copy_frame( + destination, + render_frame, + playback->offset_x, + playback->offset_y + ); + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Poll forward-decoded frames without blocking. Reverse seeks +* retain pending state until reaching the requested timestamp. +*/ +static void movie_playback_poll_frames( + s_movie_playback *playback, + uint64_t position, + int *terminal +) +{ + int promoted = 0; + + while(1) { + int result; + + if(!playback->next_frame) { + result = webm_try_get_next_frame( + playback->context, + &playback->next_frame + ); + if(result == 0) break; + if(result < 0) { + *terminal = 1; + break; + } + } + + if(playback->next_frame->timestamp > position) { + if((!playback->current_frame || playback->reverse_pending) && + !promoted) { + yuv_frame_destroy(playback->current_frame); + playback->current_frame = playback->next_frame; + playback->next_frame = NULL; + playback->frame_dirty = 1; + promoted = 1; + } + if(playback->reverse_pending) { + playback->reverse_pending = 0; + } + break; + } + + yuv_frame_destroy(playback->current_frame); + playback->current_frame = playback->next_frame; + playback->next_frame = NULL; + playback->frame_dirty = 1; + promoted = 1; + + if(playback->speed < 0.0 && + playback->current_frame->timestamp == position) { + playback->reverse_pending = 0; + break; + } + } + + if(*terminal && playback->reverse_pending) { + playback->reverse_pending = 0; + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Advance playback clocks and decoder state independently from +* composition. Reverse rates periodically seek and decode forward. +*/ +void movie_playback_update(int interrupt_requested) +{ + uint64_t active_mask; + uint64_t now; + int channel; + + if(!movie_playback_pool.initialized) { + return; + } + + now = timer_uticks(); + active_mask = movie_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + s_movie_playback *playback = &movie_playback_pool.channel[channel]; + uint64_t position; + int terminal = 0; + + active_mask &= ~(UINT64_C(1) << channel); + if(interrupt_requested && playback->interrupt) { + movie_playback_stop(playback); + continue; + } + + position = movie_playback_position_now(playback, now); + playback->position = position / UINT64_C(1000000); + if(playback->duration && playback->position > playback->duration) { + playback->position = playback->duration; + } + + if(playback->speed < 0.0 && + !playback->paused && + !playback->reverse_pending && + playback->position_anchor >= position && + playback->position_anchor - position >= + MOVIE_REVERSE_SEEK_INTERVAL) { + if(!movie_playback_open_media( + playback, + position, + playback->volume + )) { + movie_playback_stop(playback); + continue; + } + } + + if((playback->speed > 0.0 && !playback->paused) || + !playback->current_frame || + playback->reverse_pending) { + movie_playback_poll_frames(playback, position, &terminal); + } + + if(playback->speed < 0.0 && + position == 0 && + !playback->reverse_pending) { + if(playback->repeat && playback->duration) { + uint64_t repeat_position = + (playback->duration - 1U) * UINT64_C(1000000); + if(!movie_playback_open_media( + playback, + repeat_position, + playback->volume + )) { + movie_playback_stop(playback); + } + } else { + movie_playback_stop(playback); + } + continue; + } + + if(terminal && playback->speed >= 0.0) { + if(playback->repeat) { + if(!movie_playback_open_media( + playback, + 0, + playback->volume + )) { + movie_playback_stop(playback); + } + } else { + movie_playback_stop(playback); + } + } + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Compose bound subscreens before creator scripts, and compose +* unbound playback over the main screen at its presentation stage. +*/ +void movie_playback_render_subscreens(void) +{ + uint64_t active_mask = movie_playback_pool.active_mask; + int channel; + + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + s_movie_playback *playback = &movie_playback_pool.channel[channel]; + active_mask &= ~(UINT64_C(1) << channel); + if(playback->screen && + !movie_playback_render_to(playback, playback->screen)) { + movie_playback_stop(playback); + } + } +} + +void movie_playback_render_main(s_screen *screen) +{ + uint64_t active_mask; + int channel; + + if(!movie_playback_screen_valid(screen)) { + return; + } + + active_mask = movie_playback_pool.active_mask; + while((channel = sound_channel_mask_first(active_mask)) >= 0) { + s_movie_playback *playback = &movie_playback_pool.channel[channel]; + active_mask &= ~(UINT64_C(1) << channel); + if(!playback->screen && !movie_playback_render_to(playback, screen)) { + movie_playback_stop(playback); + } + } +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Property mutation helpers preserve decoder, timing, audio, +* scaling, signed-rate, and external screen invariants. +*/ +bool movie_playback_set_height(s_movie_playback *playback, int height) +{ + if(movie_playback_get_index(playback) < 0 || height < 0) { + return false; + } + playback->height = height; + playback->frame_dirty = 1; + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Protect exact-black movie pixels from the subscreen color key +* without modifying any nonzero near-black shades. +*/ +bool movie_playback_set_black_filter( + s_movie_playback *playback, + int black_filter +) +{ + if(movie_playback_get_index(playback) < 0) { + return false; + } + playback->black_filter = black_filter != 0; + playback->frame_dirty = 1; + return true; +} + +bool movie_playback_set_interrupt(s_movie_playback *playback, int interrupt) +{ + if(movie_playback_get_index(playback) < 0) { + return false; + } + playback->interrupt = interrupt != 0; + return true; +} + +bool movie_playback_set_offset_x(s_movie_playback *playback, int offset_x) +{ + if(movie_playback_get_index(playback) < 0) { + return false; + } + playback->offset_x = offset_x; + return true; +} + +bool movie_playback_set_offset_y(s_movie_playback *playback, int offset_y) +{ + if(movie_playback_get_index(playback) < 0) { + return false; + } + playback->offset_y = offset_y; + return true; +} + +bool movie_playback_set_paused(s_movie_playback *playback, int paused) +{ + uint64_t now; + + if(movie_playback_get_index(playback) < 0 || !playback->active) { + return false; + } + paused = paused != 0; + if(playback->paused == paused) { + return true; + } + + now = timer_uticks(); + if(paused) { + playback->position_anchor = movie_playback_position_now(playback, now); + playback->position = + playback->position_anchor / UINT64_C(1000000); + } else { + playback->clock_anchor = now; + } + playback->paused = paused; + webm_set_audio_paused( + playback->context, + paused || playback->speed <= 0.0 + ); + return true; +} + +bool movie_playback_set_position( + s_movie_playback *playback, + uint64_t position, + int volume +) +{ + uint64_t position_ns; + + if(movie_playback_get_index(playback) < 0 || !playback->active) { + return false; + } + if(playback->duration && position >= playback->duration) { + position = playback->duration - 1U; + } + if(position > UINT64_MAX / UINT64_C(1000000)) { + return false; + } + + position_ns = position * UINT64_C(1000000); + if(!movie_playback_open_media(playback, position_ns, volume)) { + movie_playback_stop(playback); + return false; + } + return true; +} + +bool movie_playback_set_repeat(s_movie_playback *playback, int repeat) +{ + if(movie_playback_get_index(playback) < 0) { + return false; + } + playback->repeat = repeat != 0; + return true; +} + +bool movie_playback_set_screen(s_movie_playback *playback, s_screen *screen) +{ + if(movie_playback_get_index(playback) < 0 || + (screen && !movie_playback_screen_valid(screen))) { + return false; + } + playback->screen = screen; + playback->frame_dirty = 1; + return true; +} + +bool movie_playback_set_sound_channel( + s_movie_playback *playback, + int sound_channel, + int volume +) +{ + uint64_t position; + + if(movie_playback_get_index(playback) < 0 || + !playback->active || + sound_channel < 0 || + (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX) { + return false; + } + if(playback->sound_channel == sound_channel) { + return true; + } + + position = movie_playback_position_now(playback, timer_uticks()); + playback->sound_channel = sound_channel; + if(!movie_playback_open_media(playback, position, volume)) { + movie_playback_stop(playback); + return false; + } + return true; +} + +bool movie_playback_set_speed(s_movie_playback *playback, double speed) +{ + uint64_t now; + uint64_t position; + double previous_speed; + unsigned int speed_percent; + + if(movie_playback_get_index(playback) < 0 || + !playback->active || + !isfinite(speed) || + speed < MOVIE_SPEED_MIN || + speed > MOVIE_SPEED_MAX) { + return false; + } + if(playback->speed == speed) { + return true; + } + + now = timer_uticks(); + position = movie_playback_position_now(playback, now); + previous_speed = playback->speed; + playback->position_anchor = position; + playback->clock_anchor = now; + playback->speed = speed; + playback->position = position / UINT64_C(1000000); + + if(speed > 0.0 && previous_speed <= 0.0) { + if(!movie_playback_open_media(playback, position, playback->volume)) { + movie_playback_stop(playback); + return false; + } + } else if(speed > 0.0) { + speed_percent = (unsigned int)(speed * 100.0 + 0.5); + if(speed_percent < 1U) speed_percent = 1U; + webm_set_audio_speed(playback->context, speed_percent); + webm_set_audio_paused(playback->context, playback->paused); + } else { + playback->reverse_pending = 0; + webm_set_audio_paused(playback->context, 1); + } + return true; +} + +bool movie_playback_set_width(s_movie_playback *playback, int width) +{ + if(movie_playback_get_index(playback) < 0 || width < 0) { + return false; + } + playback->width = width; + playback->frame_dirty = 1; + return true; +} diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h new file mode 100644 index 000000000..9be1a3a7f --- /dev/null +++ b/engine/source/webmlib/movie_playback.h @@ -0,0 +1,120 @@ +/* + * OpenBOR - http://www.chronocrash.com + * ----------------------------------------------------------------------- + * All rights reserved. See LICENSE in OpenBOR root for license details. + * + * Copyright (c) OpenBOR Team + */ + +#ifndef MOVIE_PLAYBACK_H +#define MOVIE_PLAYBACK_H + +#include +#include + +#include "types.h" +#include "yuv.h" + +#define MOVIE_CHANNEL_COUNT 64U +#define MOVIE_CHANNEL_AUTO (-1) +#define MOVIE_SPEED_MIN (-16.0) +#define MOVIE_SPEED_MAX 16.0 + +typedef enum e_movie_loading_mode { + MOVIE_LOADING_STREAM, + MOVIE_LOADING_CACHE, + MOVIE_LOADING_END +} e_movie_loading_mode; + +struct webm_context; + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Stable script-visible movie playback object. The source, +* decoder, frame, and clock members remain engine-managed. +*/ +typedef struct s_movie_playback { + e_object_type object_type; + int index; + int active; + int paused; + int repeat; + int interrupt; + int black_filter; + int offset_x; + int offset_y; + int width; + int height; + int sound_channel; + int source_id; + float speed; + uint64_t duration; + uint64_t position; + s_screen *screen; + + struct webm_context *context; + yuv_video_mode video_mode; + yuv_frame *current_frame; + yuv_frame *next_frame; + s_screen *rgb_frame; + s_screen *scaled_frame; + uint64_t clock_anchor; + uint64_t position_anchor; + int volume; + int frame_dirty; + int reverse_pending; +} s_movie_playback; + +bool movie_playback_init(void); +void movie_playback_shutdown(void); +void movie_playback_restore_yuv(void); + +int movie_source_load(const char *path, e_movie_loading_mode loading_mode); +bool movie_source_unload(int source_id); + +s_movie_playback *movie_playback_play( + int source_id, + int movie_channel, + int volume +); +s_movie_playback *movie_playback_get(int channel); +int movie_playback_get_index(const s_movie_playback *playback); +bool movie_playback_get_snapshot( + const s_movie_playback *playback, + s_movie_playback *snapshot +); +uint64_t movie_playback_get_active_mask(void); +void movie_playback_update(int interrupt_requested); +void movie_playback_render_subscreens(void); +void movie_playback_render_main(s_screen *screen); +void movie_playback_stop(s_movie_playback *playback); +void movie_playback_stop_all(void); +void movie_playback_detach_screen(const s_screen *screen); + +bool movie_playback_set_height(s_movie_playback *playback, int height); +bool movie_playback_set_black_filter( + s_movie_playback *playback, + int black_filter +); +bool movie_playback_set_interrupt(s_movie_playback *playback, int interrupt); +bool movie_playback_set_offset_x(s_movie_playback *playback, int offset_x); +bool movie_playback_set_offset_y(s_movie_playback *playback, int offset_y); +bool movie_playback_set_paused(s_movie_playback *playback, int paused); +bool movie_playback_set_position( + s_movie_playback *playback, + uint64_t position, + int volume +); +bool movie_playback_set_repeat(s_movie_playback *playback, int repeat); +bool movie_playback_set_screen(s_movie_playback *playback, s_screen *screen); +bool movie_playback_set_sound_channel( + s_movie_playback *playback, + int sound_channel, + int volume +); +bool movie_playback_set_speed(s_movie_playback *playback, double speed); +bool movie_playback_set_width(s_movie_playback *playback, int width); + +#endif diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 48de24c35..bdcb43332 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -30,8 +30,6 @@ #include "borendian.h" #include "soundmix.h" -extern int buffer_pakfile(const char *filename, char **pbuffer, size_t *psize); - // lowering these might save a bit of memory but could also cause lag #define PACKET_QUEUE_SIZE 20 #define FRAME_QUEUE_SIZE 10 @@ -83,7 +81,7 @@ typedef struct { typedef struct { int packhandle; - unsigned char *cache_buffer; + const unsigned char *cache_buffer; size_t cache_size; size_t cache_position; } webm_io_context; @@ -768,15 +766,16 @@ static int demux_thread(void *data) * Caskey, Damon V. * 2026-08-12 * -* Open one independently owned decoder. Optional memory -* caching, initial seek, and sound routing are established +* Open one independently owned decoder. Optional shared cache, +* initial seek, and sound routing are established * before its worker threads become visible. */ webm_context *webm_start_playback_ex( const char *path, int volume, int sound_channel, - int cache, + const unsigned char *cache_buffer, + size_t cache_size, uint64_t seek_timestamp, int replace_all_audio ) @@ -803,15 +802,12 @@ webm_context *webm_start_playback_ex( io.tell = webm_io_tell; io.userdata = &ctx->io_ctx; - if(cache) { - if(buffer_pakfile( - path, - (char**)&ctx->io_ctx.cache_buffer, - &ctx->io_ctx.cache_size - ) != 1) { - printf("Error: Unable to cache file %s for playback\n", path); + if(cache_buffer) { + if(!cache_size) { goto error_io; } + ctx->io_ctx.cache_buffer = cache_buffer; + ctx->io_ctx.cache_size = cache_size; } else { ctx->io_ctx.packhandle = openpackfile(path, packfile); if(ctx->io_ctx.packhandle < 0) { @@ -914,7 +910,6 @@ webm_context *webm_start_playback_ex( if(ctx->io_ctx.packhandle >= 0) { closepackfile(ctx->io_ctx.packhandle); } - free(ctx->io_ctx.cache_buffer); free(ctx); return NULL; } @@ -932,6 +927,7 @@ webm_context *webm_start_playback(const char *path, int volume) path, volume, SOUND_CHANNEL_MUSIC_DEFAULT, + NULL, 0, 0, 1 @@ -959,7 +955,6 @@ void webm_close(webm_context *ctx) if(ctx->audio_track >= 0) close_audio(&(ctx->audio_ctx)); if(ctx->nestegg_ctx) nestegg_destroy(ctx->nestegg_ctx); if(ctx->io_ctx.packhandle >= 0) closepackfile(ctx->io_ctx.packhandle); - free(ctx->io_ctx.cache_buffer); free(ctx); } diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index 2bb5243c4..0fbf3d951 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -9,6 +9,7 @@ #ifndef VIDPLAY_H #define VIDPLAY_H +#include #include #include "yuv.h" @@ -21,7 +22,8 @@ webm_context *webm_start_playback_ex( const char *path, int volume, int sound_channel, - int cache, + const unsigned char *cache_buffer, + size_t cache_size, uint64_t seek_timestamp, int replace_all_audio ); diff --git a/engine/source/webmlib/webm_playback.c b/engine/source/webmlib/webm_playback.c deleted file mode 100644 index 9dc36b31b..000000000 --- a/engine/source/webmlib/webm_playback.c +++ /dev/null @@ -1,963 +0,0 @@ -/* - * OpenBOR - http://www.chronocrash.com - * ----------------------------------------------------------------------- - * All rights reserved. See LICENSE in OpenBOR root for license details. - * - * Copyright (c) OpenBOR Team - */ - -#include -#include -#include -#include -#include -#include - -#include "screen.h" -#include "sound_channel.h" -#include "soundmix.h" -#include "timer.h" -#include "vidplay.h" -#include "webm_playback.h" - -typedef struct s_webm_playback_pool { - s_webm_playback channel[WEBM_PLAYBACK_CHANNEL_COUNT]; - uint64_t active_mask; - int initialized; - int yuv_initialized; -} s_webm_playback_pool; - -static s_webm_playback_pool webm_playback_pool; - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Private validation, allocation, and cleanup helpers for -* records retained by the fixed playback bank. -*/ -static bool webm_playback_screen_valid(const s_screen *screen) -{ - return screen && - screen->magic == screen_magic && - screen->pixelformat == PIXEL_32 && - screen->width > 0 && - screen->height > 0; -} - -static bool webm_playback_dimensions_valid(int width, int height) -{ - int allocation_width; - - if(width < 1 || height < 1 || width > INT_MAX - 3) { - return false; - } - allocation_width = (width + 3) & ~3; - return allocation_width <= - INT_MAX / height / (int)sizeof(uint32_t); -} - -static s_screen *webm_playback_alloc_screen(int width, int height) -{ - s_screen *screen; - - if(!webm_playback_dimensions_valid(width, height)) { - return NULL; - } - - screen = allocscreen((width + 3) & ~3, height, PIXEL_32); - if(screen) { - /* Retain the logical width while keeping aligned allocation space. */ - screen->width = width; - } - return screen; -} - -static char *webm_playback_copy_path(const char *path) -{ - char *copy; - size_t length; - - if(!path || !path[0]) { - return NULL; - } - - length = strlen(path) + 1; - copy = malloc(length); - if(copy) { - memcpy(copy, path, length); - } - return copy; -} - -static void webm_playback_close_media(s_webm_playback *playback) -{ - if(!playback) { - return; - } - - if(playback->context) { - webm_close(playback->context); - playback->context = NULL; - } - yuv_frame_destroy(playback->current_frame); - yuv_frame_destroy(playback->next_frame); - playback->current_frame = NULL; - playback->next_frame = NULL; - if(playback->rgb_frame) { - freescreen(&playback->rgb_frame); - } - if(playback->scaled_frame) { - freescreen(&playback->scaled_frame); - } -} - -static void webm_playback_reset_record(s_webm_playback *playback) -{ - e_object_type object_type; - int index; - - if(!playback) { - return; - } - - object_type = playback->object_type; - index = playback->index; - webm_playback_close_media(playback); - free(playback->path); - memset(playback, 0, sizeof(*playback)); - playback->object_type = object_type; - playback->index = index; - playback->sound_channel = SOUND_CHANNEL_MUSIC_DEFAULT; - playback->speed = 1.0; - playback->interrupt = 1; - playback->display_mode = WEBM_DISPLAY_FILL; - playback->loading_mode = WEBM_LOADING_STREAM; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Initialize one stable bank of 64 video objects and -* the shared 32-bit YUV conversion tables. -*/ -bool webm_playback_init(void) -{ - unsigned int channel; - - if(webm_playback_pool.initialized) { - return true; - } - - memset(&webm_playback_pool, 0, sizeof(webm_playback_pool)); - for(channel = 0; channel < WEBM_PLAYBACK_CHANNEL_COUNT; channel++) { - webm_playback_pool.channel[channel].object_type = OBJECT_TYPE_WEBM; - webm_playback_pool.channel[channel].index = (int)channel; - webm_playback_reset_record(&webm_playback_pool.channel[channel]); - } - - yuv_init(4); - webm_playback_pool.yuv_initialized = 1; - webm_playback_pool.initialized = 1; - return true; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Stop the playback bank and release its shared converter. -*/ -void webm_playback_shutdown(void) -{ - if(!webm_playback_pool.initialized) { - return; - } - - webm_playback_stop_all(); - if(webm_playback_pool.yuv_initialized) { - yuv_clear(); - } - memset(&webm_playback_pool, 0, sizeof(webm_playback_pool)); -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Restore 32-bit conversion after legacy fullscreen playback -* temporarily uses the shared YUV converter in 16-bit mode. -*/ -void webm_playback_restore_yuv(void) -{ - if(webm_playback_pool.initialized) { - yuv_init(4); - webm_playback_pool.yuv_initialized = 1; - } -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Return a stable object from the fixed channel bank. -*/ -s_webm_playback *webm_playback_get(int channel) -{ - if(!webm_playback_pool.initialized || - channel < 0 || - (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT) { - return NULL; - } - return &webm_playback_pool.channel[channel]; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Validate a caller-provided object by exact address and -* stable channel metadata without dereferencing it first. -*/ -int webm_playback_get_index(const s_webm_playback *playback) -{ - const s_webm_playback *indexed_playback; - uintptr_t base_address; - uintptr_t playback_address; - uintptr_t playback_offset; - unsigned int channel; - - if(!webm_playback_pool.initialized || !playback) { - return -1; - } - - base_address = (uintptr_t)(const void*)&webm_playback_pool.channel[0]; - playback_address = (uintptr_t)(const void*)playback; - if(playback_address < base_address) { - return -1; - } - - playback_offset = playback_address - base_address; - if(playback_offset >= sizeof(webm_playback_pool.channel) || - playback_offset % sizeof(webm_playback_pool.channel[0])) { - return -1; - } - - channel = (unsigned int)(playback_offset / sizeof(webm_playback_pool.channel[0])); - indexed_playback = &webm_playback_pool.channel[channel]; - if((uintptr_t)(const void*)indexed_playback != playback_address || - indexed_playback->object_type != OBJECT_TYPE_WEBM || - indexed_playback->index != (int)channel) { - return -1; - } - - return (int)channel; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Copy stable public state and expose the active channel mask -* without publishing mutable pool internals. -*/ -bool webm_playback_get_snapshot( - const s_webm_playback *playback, - s_webm_playback *snapshot -) -{ - int channel = webm_playback_get_index(playback); - - if(channel < 0 || !snapshot) { - return false; - } - *snapshot = webm_playback_pool.channel[channel]; - return true; -} - -uint64_t webm_playback_get_active_mask(void) -{ - return webm_playback_pool.initialized - ? webm_playback_pool.active_mask - : UINT64_C(0); -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Resolve wall-clock position and open or reopen decoder media -* while retaining the surrounding playback object's settings. -*/ -static uint64_t webm_playback_position_now( - const s_webm_playback *playback, - uint64_t now -) -{ - double elapsed; - double position; - - if(playback->paused) { - return playback->position_anchor; - } - - elapsed = now >= playback->clock_anchor - ? (double)(now - playback->clock_anchor) * 1000.0 * playback->speed - : 0.0; - position = (double)playback->position_anchor + elapsed; - if(position >= (double)UINT64_MAX) { - return UINT64_MAX; - } - return (uint64_t)position; -} - -static bool webm_playback_open_media( - s_webm_playback *playback, - uint64_t position, - int volume -) -{ - uint64_t duration; - unsigned int speed_percent; - - webm_playback_close_media(playback); - playback->volume = volume; - playback->context = webm_start_playback_ex( - playback->path, - volume, - playback->sound_channel, - playback->loading_mode == WEBM_LOADING_CACHE, - position, - 0 - ); - if(!playback->context) { - return false; - } - - webm_get_video_info(playback->context, &playback->video_mode); - duration = webm_get_duration(playback->context); - playback->duration = duration / UINT64_C(1000000); - if((playback->video_mode.width & 1) || - (playback->video_mode.height & 1) || - playback->video_mode.width > (INT_MAX >> 16) || - playback->video_mode.height > (INT_MAX >> 16) || - !webm_playback_dimensions_valid( - playback->video_mode.width, - playback->video_mode.height - )) { - printf( - "Error: WebM frame dimensions %d*%d cannot be represented by the 32-bit screen renderer.\n", - playback->video_mode.width, - playback->video_mode.height - ); - webm_playback_close_media(playback); - return false; - } - playback->rgb_frame = webm_playback_alloc_screen( - playback->video_mode.width, - playback->video_mode.height - ); - if(!playback->rgb_frame) { - webm_playback_close_media(playback); - return false; - } - - speed_percent = (unsigned int)(playback->speed * 100.0 + 0.5); - if(speed_percent < 1U) { - speed_percent = 1U; - } - webm_set_audio_speed(playback->context, speed_percent); - if(playback->paused) { - webm_set_audio_paused(playback->context, 1); - } - - playback->position_anchor = position; - playback->clock_anchor = timer_uticks(); - playback->position = position / UINT64_C(1000000); - playback->render_dirty = 1; - return true; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Acquire an automatic or explicitly requested video channel. -* Explicit selection replaces the channel's current playback. -*/ -s_webm_playback *webm_playback_open( - const char *path, - s_screen *screen, - int video_channel, - int sound_channel, - int interrupt, - e_webm_loading_mode loading_mode, - int volume -) -{ - s_webm_playback *playback; - uint64_t available_mask; - int channel; - - if(!webm_playback_init() || - !path || !path[0] || - !webm_playback_screen_valid(screen) || - (video_channel != WEBM_PLAYBACK_CHANNEL_AUTO && - (video_channel < 0 || - (unsigned int)video_channel >= WEBM_PLAYBACK_CHANNEL_COUNT)) || - sound_channel < 0 || - (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || - loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { - return NULL; - } - - if(video_channel == WEBM_PLAYBACK_CHANNEL_AUTO) { - available_mask = ~webm_playback_pool.active_mask; - channel = sound_channel_mask_first(available_mask); - if(channel < 0 || - (unsigned int)channel >= WEBM_PLAYBACK_CHANNEL_COUNT) { - return NULL; - } - } else { - channel = video_channel; - } - - playback = &webm_playback_pool.channel[channel]; - webm_playback_stop(playback); - playback->path = webm_playback_copy_path(path); - if(!playback->path) { - return NULL; - } - playback->screen = screen; - playback->sound_channel = sound_channel; - playback->interrupt = interrupt != 0; - playback->loading_mode = loading_mode; - - if(!webm_playback_open_media(playback, 0, volume)) { - webm_playback_reset_record(playback); - return NULL; - } - - playback->active = 1; - webm_playback_pool.active_mask |= UINT64_C(1) << channel; - return playback; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Stop individual, grouped, or screen-detached playback records -* and return their fixed slots to the available mask. -*/ -void webm_playback_stop(s_webm_playback *playback) -{ - int channel = webm_playback_get_index(playback); - - if(channel < 0) { - return; - } - - webm_playback_pool.active_mask &= ~(UINT64_C(1) << channel); - webm_playback_reset_record(&webm_playback_pool.channel[channel]); -} - -void webm_playback_stop_all(void) -{ - uint64_t active_mask; - int channel; - - if(!webm_playback_pool.initialized) { - return; - } - - active_mask = webm_playback_pool.active_mask; - while((channel = sound_channel_mask_first(active_mask)) >= 0) { - active_mask &= ~(UINT64_C(1) << channel); - webm_playback_stop(&webm_playback_pool.channel[channel]); - } -} - -void webm_playback_detach_screen(const s_screen *screen) -{ - uint64_t active_mask; - int channel; - - if(!screen || !webm_playback_pool.initialized) { - return; - } - - active_mask = webm_playback_pool.active_mask; - while((channel = sound_channel_mask_first(active_mask)) >= 0) { - s_webm_playback *playback = &webm_playback_pool.channel[channel]; - active_mask &= ~(UINT64_C(1) << channel); - if(playback->screen == screen) { - webm_playback_stop(playback); - } - } -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Prepare scaled output and copy it into a creator-owned screen -* with bounds independent from global draw clipping. -*/ -static bool webm_playback_scale_frame( - s_webm_playback *playback, - int width, - int height, - s_screen **render_frame -) -{ - if(width == playback->rgb_frame->width && - height == playback->rgb_frame->height) { - *render_frame = playback->rgb_frame; - return true; - } - - if(!webm_playback_dimensions_valid(width, height)) { - return false; - } - - if(playback->scaled_frame && - (playback->scaled_frame->width != width || - playback->scaled_frame->height != height)) { - freescreen(&playback->scaled_frame); - } - if(!playback->scaled_frame) { - playback->scaled_frame = webm_playback_alloc_screen(width, height); - } - if(!playback->scaled_frame) { - return false; - } - - scalescreen32(playback->scaled_frame, playback->rgb_frame); - *render_frame = playback->scaled_frame; - return true; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Copy a 32-bit video frame with destination clipping -* independent of the engine's current draw-method clip state. -*/ -static void webm_playback_copy_frame( - s_screen *destination, - const s_screen *source, - int offset_x, - int offset_y -) -{ - const unsigned char *source_row; - unsigned char *destination_row; - int64_t destination_x = offset_x; - int64_t destination_y = offset_y; - int source_x = 0; - int source_y = 0; - int width = source->width; - int height = source->height; - int row; - - if(destination_x < 0) { - int64_t clipped_width = -destination_x; - if(clipped_width >= width) { - return; - } - source_x = (int)clipped_width; - width -= source_x; - destination_x = 0; - } - if(destination_y < 0) { - int64_t clipped_height = -destination_y; - if(clipped_height >= height) { - return; - } - source_y = (int)clipped_height; - height -= source_y; - destination_y = 0; - } - if(destination_x >= destination->width || destination_y >= destination->height) { - return; - } - if(width > destination->width - destination_x) { - width = (int)(destination->width - destination_x); - } - if(height > destination->height - destination_y) { - height = (int)(destination->height - destination_y); - } - if(width <= 0 || height <= 0) { - return; - } - - source_row = source->data + - ((source_y * source->width + source_x) * (int)sizeof(uint32_t)); - destination_row = destination->data + - (((int)destination_y * destination->width + (int)destination_x) * - (int)sizeof(uint32_t)); - for(row = 0; row < height; row++) { - memcpy(destination_row, source_row, (size_t)width * sizeof(uint32_t)); - source_row += source->width * (int)sizeof(uint32_t); - destination_row += destination->width * (int)sizeof(uint32_t); - } -} - -static bool webm_playback_render(s_webm_playback *playback) -{ - s_screen *render_frame; - int width; - int height; - - if(!playback->current_frame || - !playback->rgb_frame || - !webm_playback_screen_valid(playback->screen)) { - return false; - } - - yuv_to_rgb(playback->current_frame, playback->rgb_frame); - width = playback->width; - height = playback->height; - if(width == 0) { - width = playback->display_mode == WEBM_DISPLAY_FILL - ? playback->screen->width - : (playback->video_mode.display_width > 0 - ? playback->video_mode.display_width - : playback->video_mode.width); - } - if(height == 0) { - height = playback->display_mode == WEBM_DISPLAY_FILL - ? playback->screen->height - : (playback->video_mode.display_height > 0 - ? playback->video_mode.display_height - : playback->video_mode.height); - } - if(width < 1 || height < 1 || - !webm_playback_scale_frame(playback, width, height, &render_frame)) { - return false; - } - - webm_playback_copy_frame( - playback->screen, - render_frame, - playback->offset_x, - playback->offset_y - ); - playback->render_dirty = 0; - return true; -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Advance all active objects without blocking on decoder -* queues, render due frames, and recycle terminal channels. -*/ -void webm_playback_update(int interrupt_requested) -{ - uint64_t active_mask; - uint64_t now; - int channel; - - if(!webm_playback_pool.initialized) { - return; - } - - now = timer_uticks(); - active_mask = webm_playback_pool.active_mask; - while((channel = sound_channel_mask_first(active_mask)) >= 0) { - s_webm_playback *playback = &webm_playback_pool.channel[channel]; - uint64_t position; - int terminal = 0; - - active_mask &= ~(UINT64_C(1) << channel); - if(interrupt_requested && playback->interrupt) { - webm_playback_stop(playback); - continue; - } - if(!webm_playback_screen_valid(playback->screen)) { - webm_playback_stop(playback); - continue; - } - - position = webm_playback_position_now(playback, now); - playback->position = position / UINT64_C(1000000); - if(playback->duration && playback->position > playback->duration) { - playback->position = playback->duration; - } - - if(!playback->paused) { - while(1) { - int result; - - if(!playback->next_frame) { - result = webm_try_get_next_frame( - playback->context, - &playback->next_frame - ); - if(result == 0) { - break; - } - if(result < 0) { - terminal = 1; - break; - } - } - - if(playback->next_frame->timestamp > position) { - break; - } - - yuv_frame_destroy(playback->current_frame); - playback->current_frame = playback->next_frame; - playback->next_frame = NULL; - playback->render_dirty = 1; - } - } - - if(playback->render_dirty && playback->current_frame && - !webm_playback_render(playback)) { - webm_playback_stop(playback); - continue; - } - - if(terminal) { - if(playback->repeat) { - if(!webm_playback_open_media(playback, 0, playback->volume)) { - webm_playback_stop(playback); - } - } else { - webm_playback_stop(playback); - } - } - } -} - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Property mutation helpers preserve decoder, timing, audio, -* scaling, and externally owned screen invariants. -*/ -bool webm_playback_set_display_mode( - s_webm_playback *playback, - e_webm_display_mode display_mode -) -{ - if(webm_playback_get_index(playback) < 0 || - display_mode < 0 || display_mode >= WEBM_DISPLAY_END) { - return false; - } - playback->display_mode = display_mode; - playback->render_dirty = 1; - return true; -} - -bool webm_playback_set_height(s_webm_playback *playback, int height) -{ - if(webm_playback_get_index(playback) < 0 || height < 0) { - return false; - } - playback->height = height; - playback->render_dirty = 1; - return true; -} - -bool webm_playback_set_interrupt(s_webm_playback *playback, int interrupt) -{ - if(webm_playback_get_index(playback) < 0) { - return false; - } - playback->interrupt = interrupt != 0; - return true; -} - -bool webm_playback_set_loading_mode( - s_webm_playback *playback, - e_webm_loading_mode loading_mode, - int volume -) -{ - uint64_t position; - e_webm_loading_mode previous_mode; - - if(webm_playback_get_index(playback) < 0 || - !playback->active || - loading_mode < 0 || loading_mode >= WEBM_LOADING_END) { - return false; - } - if(playback->loading_mode == loading_mode) { - return true; - } - - position = webm_playback_position_now(playback, timer_uticks()); - previous_mode = playback->loading_mode; - playback->loading_mode = loading_mode; - if(!webm_playback_open_media(playback, position, volume)) { - playback->loading_mode = previous_mode; - webm_playback_stop(playback); - return false; - } - return true; -} - -bool webm_playback_set_offset_x(s_webm_playback *playback, int offset_x) -{ - if(webm_playback_get_index(playback) < 0) { - return false; - } - playback->offset_x = offset_x; - playback->render_dirty = 1; - return true; -} - -bool webm_playback_set_offset_y(s_webm_playback *playback, int offset_y) -{ - if(webm_playback_get_index(playback) < 0) { - return false; - } - playback->offset_y = offset_y; - playback->render_dirty = 1; - return true; -} - -bool webm_playback_set_paused(s_webm_playback *playback, int paused) -{ - uint64_t now; - - if(webm_playback_get_index(playback) < 0 || !playback->active) { - return false; - } - paused = paused != 0; - if(playback->paused == paused) { - return true; - } - - now = timer_uticks(); - if(paused) { - playback->position_anchor = webm_playback_position_now(playback, now); - playback->position = playback->position_anchor / UINT64_C(1000000); - } else { - playback->clock_anchor = now; - } - playback->paused = paused; - webm_set_audio_paused(playback->context, paused); - return true; -} - -bool webm_playback_set_position( - s_webm_playback *playback, - uint64_t position, - int volume -) -{ - uint64_t position_ns; - - if(webm_playback_get_index(playback) < 0 || !playback->active) { - return false; - } - if(playback->duration && position >= playback->duration) { - position = playback->duration - 1; - } - if(position > UINT64_MAX / UINT64_C(1000000)) { - return false; - } - - position_ns = position * UINT64_C(1000000); - if(!webm_playback_open_media(playback, position_ns, volume)) { - webm_playback_stop(playback); - return false; - } - return true; -} - -bool webm_playback_set_repeat(s_webm_playback *playback, int repeat) -{ - if(webm_playback_get_index(playback) < 0) { - return false; - } - playback->repeat = repeat != 0; - return true; -} - -bool webm_playback_set_screen(s_webm_playback *playback, s_screen *screen) -{ - if(webm_playback_get_index(playback) < 0 || - !webm_playback_screen_valid(screen)) { - return false; - } - playback->screen = screen; - playback->render_dirty = 1; - return true; -} - -bool webm_playback_set_sound_channel( - s_webm_playback *playback, - int sound_channel, - int volume -) -{ - uint64_t position; - int previous_channel; - - if(webm_playback_get_index(playback) < 0 || - !playback->active || - sound_channel < 0 || - (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX) { - return false; - } - if(playback->sound_channel == sound_channel) { - return true; - } - - position = webm_playback_position_now(playback, timer_uticks()); - previous_channel = playback->sound_channel; - playback->sound_channel = sound_channel; - if(!webm_playback_open_media(playback, position, volume)) { - playback->sound_channel = previous_channel; - webm_playback_stop(playback); - return false; - } - return true; -} - -bool webm_playback_set_speed(s_webm_playback *playback, double speed) -{ - uint64_t now; - unsigned int speed_percent; - - if(webm_playback_get_index(playback) < 0 || - !playback->active || - !isfinite(speed) || - speed < WEBM_PLAYBACK_SPEED_MIN || - speed > WEBM_PLAYBACK_SPEED_MAX) { - return false; - } - - now = timer_uticks(); - playback->position_anchor = webm_playback_position_now(playback, now); - playback->clock_anchor = now; - playback->speed = speed; - speed_percent = (unsigned int)(speed * 100.0 + 0.5); - if(speed_percent < 1U) { - speed_percent = 1U; - } - webm_set_audio_speed(playback->context, speed_percent); - return true; -} - -bool webm_playback_set_width(s_webm_playback *playback, int width) -{ - if(webm_playback_get_index(playback) < 0 || - width < 0) { - return false; - } - playback->width = width; - playback->render_dirty = 1; - return true; -} diff --git a/engine/source/webmlib/webm_playback.h b/engine/source/webmlib/webm_playback.h deleted file mode 100644 index 8ab4e1364..000000000 --- a/engine/source/webmlib/webm_playback.h +++ /dev/null @@ -1,129 +0,0 @@ -/* - * OpenBOR - http://www.chronocrash.com - * ----------------------------------------------------------------------- - * All rights reserved. See LICENSE in OpenBOR root for license details. - * - * Copyright (c) OpenBOR Team - */ - -#ifndef WEBM_PLAYBACK_H -#define WEBM_PLAYBACK_H - -#include -#include - -#include "types.h" -#include "yuv.h" - -#define WEBM_PLAYBACK_CHANNEL_COUNT 64U -#define WEBM_PLAYBACK_CHANNEL_AUTO (-1) -#define WEBM_PLAYBACK_SPEED_MIN 0.01 -#define WEBM_PLAYBACK_SPEED_MAX 16.0 - -typedef enum e_webm_display_mode { - WEBM_DISPLAY_FILL, /* Zero-value default: scale to destination. */ - WEBM_DISPLAY_CLIP, - WEBM_DISPLAY_END -} e_webm_display_mode; - -typedef enum e_webm_loading_mode { - WEBM_LOADING_STREAM, /* Decode through ordinary packfile I/O. */ - WEBM_LOADING_CACHE, /* Cache the complete WebM before decoding. */ - WEBM_LOADING_END -} e_webm_loading_mode; - -struct webm_context; - -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Stable script-visible WebM playback object. Decoder, -* frame, and clock members remain engine-managed. -*/ -typedef struct s_webm_playback { - e_object_type object_type; - int index; - int active; - int paused; - int repeat; - int interrupt; - int offset_x; - int offset_y; - int width; - int height; - int sound_channel; - float speed; - e_webm_display_mode display_mode; - e_webm_loading_mode loading_mode; - uint64_t duration; - uint64_t position; - s_screen *screen; - - char *path; - struct webm_context *context; - yuv_video_mode video_mode; - yuv_frame *current_frame; - yuv_frame *next_frame; - s_screen *rgb_frame; - s_screen *scaled_frame; - uint64_t clock_anchor; - uint64_t position_anchor; - int volume; - int render_dirty; -} s_webm_playback; - -bool webm_playback_init(void); -void webm_playback_shutdown(void); -void webm_playback_restore_yuv(void); -s_webm_playback *webm_playback_open( - const char *path, - s_screen *screen, - int video_channel, - int sound_channel, - int interrupt, - e_webm_loading_mode loading_mode, - int volume -); -s_webm_playback *webm_playback_get(int channel); -int webm_playback_get_index(const s_webm_playback *playback); -bool webm_playback_get_snapshot( - const s_webm_playback *playback, - s_webm_playback *snapshot -); -uint64_t webm_playback_get_active_mask(void); -void webm_playback_update(int interrupt_requested); -void webm_playback_stop(s_webm_playback *playback); -void webm_playback_stop_all(void); -void webm_playback_detach_screen(const s_screen *screen); - -bool webm_playback_set_display_mode( - s_webm_playback *playback, - e_webm_display_mode display_mode -); -bool webm_playback_set_height(s_webm_playback *playback, int height); -bool webm_playback_set_interrupt(s_webm_playback *playback, int interrupt); -bool webm_playback_set_loading_mode( - s_webm_playback *playback, - e_webm_loading_mode loading_mode, - int volume -); -bool webm_playback_set_offset_x(s_webm_playback *playback, int offset_x); -bool webm_playback_set_offset_y(s_webm_playback *playback, int offset_y); -bool webm_playback_set_paused(s_webm_playback *playback, int paused); -bool webm_playback_set_position( - s_webm_playback *playback, - uint64_t position, - int volume -); -bool webm_playback_set_repeat(s_webm_playback *playback, int repeat); -bool webm_playback_set_screen(s_webm_playback *playback, s_screen *screen); -bool webm_playback_set_sound_channel( - s_webm_playback *playback, - int sound_channel, - int volume -); -bool webm_playback_set_speed(s_webm_playback *playback, double speed); -bool webm_playback_set_width(s_webm_playback *playback, int width); - -#endif From ea5153794889c91d5c1bab6fdcc19541ef08e698 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Wed, 12 Aug 2026 18:39:18 -0400 Subject: [PATCH 04/26] Route legacy WebM playback through movie API --- engine/openbor.c | 104 +++++++++++++------------ engine/openborscript.c | 2 +- engine/source/openborscript/movie.c | 3 +- engine/source/webmlib/movie_playback.c | 49 +++++++----- engine/source/webmlib/movie_playback.h | 8 +- engine/source/webmlib/vidplay.c | 26 ------- engine/source/webmlib/vidplay.h | 2 - 7 files changed, 90 insertions(+), 104 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index 26f2692ef..e153428a9 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -51870,73 +51870,75 @@ 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; + int source_id = -1; + s_movie_playback *playback = NULL; movie_playback_stop_all(); - 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;} - - // 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;} + 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; - 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); + } + movie_playback_update(interrupt_requested); + if(!playback->active) { break; } - else if(frame && !noscreenshot && (bothnewkeys & FLAG_SCREENSHOT)) - { - yuv_to_rgb(frame, rgb_frame); - screenshot(rgb_frame, NULL, 0); + if(!movie_playback_render_main(vscreen)) { + retval = 0; + 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); - } - - // 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; + if(playback->current_frame && + !noscreenshot && + (bothnewkeys & FLAG_SCREENSHOT)) { + screenshot(vscreen, getpal, 1); } - else usleep(next_frame_time - time_passed); + video_copy_screen(vscreen); + usleep(1000); } quit: - if(ctx) webm_close(ctx); - if(rgb_frame) freescreen(&rgb_frame); - yuv_clear(); - movie_playback_restore_yuv(); - video_set_mode(videomodes); + if(playback && playback->active) { + movie_playback_stop(playback); + } + if(source_id >= 0 && !movie_source_unload(source_id)) { + retval = 0; + } return retval; } #endif diff --git a/engine/openborscript.c b/engine/openborscript.c index 266e85ccd..2d9d9f505 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -15874,7 +15874,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) { diff --git a/engine/source/openborscript/movie.c b/engine/source/openborscript/movie.c index fa28ad2cd..0f45581e4 100644 --- a/engine/source/openborscript/movie.c +++ b/engine/source/openborscript/movie.c @@ -222,7 +222,8 @@ HRESULT openbor_movie_play( playback = movie_playback_play( (int)source_id, (int)channel, - savedata.musicvol + savedata.musicvol, + false ); if(!playback) { printf("\nScript error: %s. Source is invalid or no channel could be acquired.\n", diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index f63eb254a..82b6d3ff8 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -301,21 +301,6 @@ void movie_playback_shutdown(void) memset(&movie_playback_pool, 0, sizeof(movie_playback_pool)); } -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Restore 32-bit conversion after legacy fullscreen WebM -* playback temporarily uses the shared 16-bit converter. -*/ -void movie_playback_restore_yuv(void) -{ - if(movie_playback_pool.initialized) { - yuv_init(4); - movie_playback_pool.yuv_initialized = 1; - } -} - /* * Caskey, Damon V. * 2026-08-12 @@ -534,7 +519,7 @@ static bool movie_playback_open_media( source->cache_buffer, source->cache_size, position, - 0 + playback->replace_all_audio ); if(!playback->context) { return false; @@ -592,6 +577,7 @@ static bool movie_playback_open_media( playback->clock_anchor = timer_uticks(); playback->position = position / UINT64_C(1000000); playback->reverse_pending = playback->speed < 0.0; + playback->terminal_pending = 0; return true; } @@ -601,11 +587,13 @@ static bool movie_playback_open_media( * * Create one playback from a reusable source. Automatic mode * takes the lowest free slot; explicit selection replaces it. +* Legacy callers may request their historical audio takeover. */ s_movie_playback *movie_playback_play( int source_id, int movie_channel, - int volume + int volume, + bool replace_all_audio ) { s_movie_source *source; @@ -634,6 +622,7 @@ s_movie_playback *movie_playback_play( playback = &movie_playback_pool.channel[channel]; movie_playback_stop(playback); playback->source_id = source_id; + playback->replace_all_audio = replace_all_audio; ++source->references; if(!movie_playback_open_media(playback, 0, volume)) { movie_playback_reset_record(playback); @@ -959,6 +948,20 @@ void movie_playback_update(int interrupt_requested) movie_playback_stop(playback); continue; } + if(playback->terminal_pending && !playback->frame_dirty) { + if(playback->repeat) { + if(!movie_playback_open_media( + playback, + 0, + playback->volume + )) { + movie_playback_stop(playback); + } + } else { + movie_playback_stop(playback); + } + continue; + } position = movie_playback_position_now(playback, now); playback->position = position / UINT64_C(1000000); @@ -1008,7 +1011,9 @@ void movie_playback_update(int interrupt_requested) } if(terminal && playback->speed >= 0.0) { - if(playback->repeat) { + if(playback->frame_dirty) { + playback->terminal_pending = 1; + } else if(playback->repeat) { if(!movie_playback_open_media( playback, 0, @@ -1045,13 +1050,14 @@ void movie_playback_render_subscreens(void) } } -void movie_playback_render_main(s_screen *screen) +bool movie_playback_render_main(s_screen *screen) { uint64_t active_mask; int channel; + bool result = true; if(!movie_playback_screen_valid(screen)) { - return; + return false; } active_mask = movie_playback_pool.active_mask; @@ -1060,8 +1066,10 @@ void movie_playback_render_main(s_screen *screen) active_mask &= ~(UINT64_C(1) << channel); if(!playback->screen && !movie_playback_render_to(playback, screen)) { movie_playback_stop(playback); + result = false; } } + return result; } /* @@ -1267,6 +1275,7 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) webm_set_audio_paused(playback->context, playback->paused); } else { playback->reverse_pending = 0; + playback->terminal_pending = 0; webm_set_audio_paused(playback->context, 1); } return true; diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index 9be1a3a7f..e1822fff7 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -63,13 +63,14 @@ typedef struct s_movie_playback { uint64_t clock_anchor; uint64_t position_anchor; int volume; + bool replace_all_audio; int frame_dirty; int reverse_pending; + int terminal_pending; } s_movie_playback; bool movie_playback_init(void); void movie_playback_shutdown(void); -void movie_playback_restore_yuv(void); int movie_source_load(const char *path, e_movie_loading_mode loading_mode); bool movie_source_unload(int source_id); @@ -77,7 +78,8 @@ bool movie_source_unload(int source_id); s_movie_playback *movie_playback_play( int source_id, int movie_channel, - int volume + int volume, + bool replace_all_audio ); s_movie_playback *movie_playback_get(int channel); int movie_playback_get_index(const s_movie_playback *playback); @@ -88,7 +90,7 @@ bool movie_playback_get_snapshot( uint64_t movie_playback_get_active_mask(void); void movie_playback_update(int interrupt_requested); void movie_playback_render_subscreens(void); -void movie_playback_render_main(s_screen *screen); +bool movie_playback_render_main(s_screen *screen); void movie_playback_stop(s_movie_playback *playback); void movie_playback_stop_all(void); void movie_playback_detach_screen(const s_screen *screen); diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index bdcb43332..7dca8732f 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -914,26 +914,6 @@ webm_context *webm_start_playback_ex( return NULL; } -/* -* Caskey, Damon V. -* 2026-08-12 -* -* Preserve the original fullscreen decoder entrypoint with -* channel zero and replace-all audio defaults. -*/ -webm_context *webm_start_playback(const char *path, int volume) -{ - return webm_start_playback_ex( - path, - volume, - SOUND_CHANNEL_MUSIC_DEFAULT, - NULL, - 0, - 0, - 1 - ); -} - /* * Caskey, Damon V. * 2026-08-12 @@ -1008,12 +988,6 @@ void webm_set_audio_speed(webm_context *ctx, unsigned int speed) } } -yuv_frame *webm_get_next_frame(webm_context *ctx) -{ - debug_printf("frame queue size=%i\n", ctx->video_ctx.frame_queue->size); - return (yuv_frame *)queue_get(ctx->video_ctx.frame_queue); -} - /* * Caskey, Damon V. * 2026-08-12 diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index 0fbf3d951..301108d6c 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -17,7 +17,6 @@ struct webm_context; typedef struct webm_context webm_context; -webm_context *webm_start_playback(const char *path, int volume); webm_context *webm_start_playback_ex( const char *path, int volume, @@ -32,7 +31,6 @@ uint64_t webm_get_duration(webm_context *ctx); void webm_set_audio_paused(webm_context *ctx, int paused); void webm_set_audio_speed(webm_context *ctx, unsigned int speed); void webm_close(webm_context *ctx); -yuv_frame *webm_get_next_frame(webm_context *ctx); int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame); #endif From cae209af85b16e02eeccb1686c80365bc12ca55a Mon Sep 17 00:00:00 2001 From: DCurrent Date: Wed, 12 Aug 2026 23:42:01 -0400 Subject: [PATCH 05/26] Prevent movie rewind from stalling on audio Open reverse movie decoder contexts without an active audio track. This allows interleaved audio packets to be discarded during demuxing instead of filling paused PCM buffers and blocking video delivery. Restore embedded audio when playback returns to a positive speed while preserving normal forward and legacy playback behavior. --- engine/source/webmlib/movie_playback.c | 1 + engine/source/webmlib/vidplay.c | 14 +++++++++----- engine/source/webmlib/vidplay.h | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 82b6d3ff8..4e68edb19 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -519,6 +519,7 @@ static bool movie_playback_open_media( source->cache_buffer, source->cache_size, position, + playback->speed > 0.0, playback->replace_all_audio ); if(!playback->context) { diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 7dca8732f..46bfe6d46 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -767,8 +767,9 @@ static int demux_thread(void *data) * 2026-08-12 * * Open one independently owned decoder. Optional shared cache, -* initial seek, and sound routing are established -* before its worker threads become visible. +* initial seek, and sound routing are established before its worker +* threads become visible. Reverse contexts omit the audio track so +* paused producer queues cannot back-pressure video demuxing. */ webm_context *webm_start_playback_ex( const char *path, @@ -777,6 +778,7 @@ webm_context *webm_start_playback_ex( const unsigned char *cache_buffer, size_t cache_size, uint64_t seek_timestamp, + int play_audio, int replace_all_audio ) { @@ -868,8 +870,8 @@ webm_context *webm_start_playback_ex( return NULL; } - ctx->audio_track = audio_track; - if(audio_track >= 0) { + if(audio_track >= 0 && play_audio) { + ctx->audio_track = audio_track; if(init_audio( ctx->nestegg_ctx, ctx->audio_track, @@ -888,7 +890,9 @@ webm_context *webm_start_playback_ex( printf("Warning: Unable to open the WebM audio track on channel %d\n", sound_channel); ctx->audio_track = -1; } - } else if(replace_all_audio && sound_query_music(NULL, NULL)) { + } else if(audio_track < 0 && + replace_all_audio && + sound_query_music(NULL, NULL)) { /* Blocking legacy playback must service existing channel zero audio. */ ctx->the_audio_thread = thread_create(bgm_update_thread, "bgm", ctx); if(!ctx->the_audio_thread) { diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index 301108d6c..ce61f17d8 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -24,6 +24,7 @@ webm_context *webm_start_playback_ex( const unsigned char *cache_buffer, size_t cache_size, uint64_t seek_timestamp, + int play_audio, int replace_all_audio ); void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims); From 1c6df0e1e1aaa18442704efde94271e0591db34a Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 13 Aug 2026 00:19:01 -0400 Subject: [PATCH 06/26] Fix WebM rewind and seek audio synchronization Disable audio decoding during stationary or reverse playback to prevent audio queues from blocking video demuxing. Align decoded Vorbis PCM with the requested seek position. Account for codec delay, discard samples preceding the target, and insert silence when the first available audio packet begins after it. Keep the sound stream paused until the movie playback clock is anchored. --- engine/source/webmlib/movie_playback.c | 12 +-- engine/source/webmlib/vidplay.c | 130 +++++++++++++++++++++++++ engine/source/webmlib/vorbis.c | 22 +++++ engine/source/webmlib/vorbis.h | 1 + 4 files changed, 159 insertions(+), 6 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 4e68edb19..dfd819172 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -564,6 +564,12 @@ static bool movie_playback_open_media( return false; } + playback->position_anchor = position; + playback->clock_anchor = timer_uticks(); + playback->position = position / UINT64_C(1000000); + playback->reverse_pending = playback->speed < 0.0; + playback->terminal_pending = 0; + if(playback->speed > 0.0) { speed_percent = (unsigned int)(playback->speed * 100.0 + 0.5); if(speed_percent < 1U) speed_percent = 1U; @@ -573,12 +579,6 @@ static bool movie_playback_open_media( playback->context, playback->paused || playback->speed <= 0.0 ); - - playback->position_anchor = position; - playback->clock_anchor = timer_uticks(); - playback->position = position / UINT64_C(1000000); - playback->reverse_pending = playback->speed < 0.0; - playback->terminal_pending = 0; return true; } diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 46bfe6d46..2b2eb7231 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -63,6 +63,9 @@ typedef struct { int channel; int stream_play_id; int output_active; + int aligning; + uint64_t output_timestamp; + uint64_t leading_silence_frames; volatile int *quit; uint8_t pcm_buffer[SOUND_STREAM_BUFFER_SIZE]; } audio_context; @@ -318,6 +321,91 @@ static int bgm_update_thread(void *data) return 0; } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Convert a nanosecond interval to the number of complete PCM +* frames needed to reach or pass its end without overflowing. +*/ +static uint64_t audio_nanoseconds_to_frames( + uint64_t nanoseconds, + int frequency +) +{ + const uint64_t nanoseconds_per_second = UINT64_C(1000000000); + uint64_t whole_seconds; + uint64_t remainder; + uint64_t frames; + uint64_t remainder_frames; + + if(frequency <= 0) { + return UINT64_MAX; + } + + whole_seconds = nanoseconds / nanoseconds_per_second; + remainder = nanoseconds % nanoseconds_per_second; + if(whole_seconds > UINT64_MAX / (uint64_t)frequency) { + return UINT64_MAX; + } + frames = whole_seconds * (uint64_t)frequency; + remainder_frames = remainder * (uint64_t)frequency; + if(remainder_frames) { + remainder_frames = + ((remainder_frames - 1U) / nanoseconds_per_second) + 1U; + } + if(frames > UINT64_MAX - remainder_frames) { + return UINT64_MAX; + } + return frames + remainder_frames; +} + +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Align decoded audio to the requested movie timestamp. Samples +* preceding the target are consumed silently. If Nestegg returns +* the first audio packet after the target, equivalent silence is +* inserted so audio retains its position on the movie timeline. +*/ +static void audio_align_packet( + audio_context *audio_ctx, + uint64_t packet_timestamp +) +{ + uint64_t frame_count; + int available_samples; + int skipped_samples; + + if(!audio_ctx->aligning || audio_ctx->avail_samples <= 0) { + return; + } + + if(packet_timestamp < audio_ctx->output_timestamp) { + available_samples = audio_ctx->avail_samples; + frame_count = audio_nanoseconds_to_frames( + audio_ctx->output_timestamp - packet_timestamp, + audio_ctx->frequency + ); + skipped_samples = frame_count < (uint64_t)available_samples + ? (int)frame_count + : available_samples; + vorbis_skip_pcm(&audio_ctx->vorbis_ctx, (size_t)skipped_samples); + audio_ctx->avail_samples -= skipped_samples; + if(skipped_samples < available_samples) { + audio_ctx->aligning = 0; + } + return; + } + + audio_ctx->leading_silence_frames = audio_nanoseconds_to_frames( + packet_timestamp - audio_ctx->output_timestamp, + audio_ctx->frequency + ); + audio_ctx->aligning = 0; +} + static int audio_decode_frame( audio_context *audio_ctx, uint8_t *audio_buf, @@ -334,6 +422,25 @@ static int audio_decode_frame( while (samples) { + if(audio_ctx->leading_silence_frames) { + int silence_samples = audio_ctx->leading_silence_frames < + (uint64_t)samples + ? (int)audio_ctx->leading_silence_frames + : samples; + size_t silence_bytes = + (size_t)silence_samples * + (size_t)vorbis_ctx->channels * + sizeof(int16_t); + + memset(audio_buf, 0, silence_bytes); + audio_buf += silence_bytes; + audio_ctx->leading_silence_frames -= + (uint64_t)silence_samples; + samples -= silence_samples; + samples_written += silence_samples; + continue; + } + if (audio_ctx->avail_samples == 0) { nestegg_packet *pkt; @@ -357,6 +464,8 @@ static int audio_decode_frame( audio_ctx->avail_samples = vorbis_packet(vorbis_ctx, data, data_size); } nestegg_free_packet(pkt); + audio_align_packet(audio_ctx, timestamp); + continue; } int samples_read = MIN(audio_ctx->avail_samples, samples); @@ -450,6 +559,7 @@ static int init_audio( audio_context *audio_ctx, int volume, int sound_channel, + uint64_t seek_timestamp, int replace_all_audio, volatile int *quit ) @@ -487,6 +597,11 @@ static int init_audio( audio_ctx->vorbis_ctx.channels = audioParams.channels; audio_ctx->frequency = (int)audioParams.rate; audio_ctx->avail_samples = audio_ctx->last_samples = 0; + audio_ctx->aligning = 1; + audio_ctx->output_timestamp = seek_timestamp <= + UINT64_MAX - audioParams.codec_delay + ? seek_timestamp + audioParams.codec_delay + : UINT64_MAX; audio_ctx->quit = quit; audio_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE, quit); if(!audio_ctx->packet_queue) { @@ -537,6 +652,20 @@ static int init_audio( audio_ctx->channel, SOUND_VOLUME_DIVISOR_MUSIC ); + if(!sound_pause_channel_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + 1 + )) { + sound_close_channel_pcm_stream( + audio_ctx->channel, + audio_ctx->stream_play_id + ); + queue_destroy(audio_ctx->packet_queue); + audio_ctx->packet_queue = NULL; + vorbis_destroy(&(audio_ctx->vorbis_ctx)); + return -1; + } return 0; } @@ -878,6 +1007,7 @@ webm_context *webm_start_playback_ex( &(ctx->audio_ctx), volume, sound_channel, + seek_timestamp, replace_all_audio, &ctx->quit ) == 0) { diff --git a/engine/source/webmlib/vorbis.c b/engine/source/webmlib/vorbis.c index b55112001..5829d660d 100644 --- a/engine/source/webmlib/vorbis.c +++ b/engine/source/webmlib/vorbis.c @@ -92,5 +92,27 @@ void vorbis_getpcm(vorbis_context *ctx, void *buffer, size_t samples) vorbis_synthesis_read(&ctx->v, samples); } +/* +* Caskey, Damon V. +* 2026-08-12 +* +* Consume decoded PCM without packing it into an output buffer. +* Seek alignment uses this to discard samples preceding the +* requested movie position. +*/ +void vorbis_skip_pcm(vorbis_context *ctx, size_t samples) +{ +#if TREMOR + ogg_int32_t **pcm; +#else + float **pcm; +#endif + int avail_samples; + + if(!samples) return; + avail_samples = vorbis_synthesis_pcmout(&ctx->v, &pcm); + assert(avail_samples >= 0 && (size_t)avail_samples >= samples); + vorbis_synthesis_read(&ctx->v, samples); +} diff --git a/engine/source/webmlib/vorbis.h b/engine/source/webmlib/vorbis.h index f206add6b..edc681755 100644 --- a/engine/source/webmlib/vorbis.h +++ b/engine/source/webmlib/vorbis.h @@ -31,5 +31,6 @@ void vorbis_prepare(vorbis_context *ctx); void vorbis_headerpacket(vorbis_context *ctx, void *data, size_t size, int packetCount); int vorbis_packet(vorbis_context *ctx, void *data, size_t size); void vorbis_getpcm(vorbis_context *ctx, void *buffer, size_t samples); +void vorbis_skip_pcm(vorbis_context *ctx, size_t samples); #endif From 8f1dea822afbe291c02604e38a1b79f0f94191a2 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 13 Aug 2026 00:35:50 -0400 Subject: [PATCH 07/26] Fix WebM playback audio synchronization regressions Disable audio decoding during stationary and reverse playback to prevent unconsumed audio queues from blocking video demuxing. Synchronize audio after seeking by accounting for codec delay, discarding PCM before the requested position, and inserting silence when the first available packet begins after it. Hold the sound channel paused until the movie clock is anchored. Preserve decimal playback rates through the audio period calculation instead of rounding them to integer percentages. --- engine/source/gamelib/soundmix.c | 52 +++++++++++++++++++++++--- engine/source/gamelib/soundmix.h | 2 +- engine/source/webmlib/movie_playback.c | 10 +---- engine/source/webmlib/vidplay.c | 2 +- engine/source/webmlib/vidplay.h | 2 +- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/engine/source/gamelib/soundmix.c b/engine/source/gamelib/soundmix.c index df48fdc05..5c89b67b7 100644 --- a/engine/source/gamelib/soundmix.c +++ b/engine/source/gamelib/soundmix.c @@ -1864,6 +1864,41 @@ static sound_sample_fixed_t sound_sample_period_calculate(unsigned int speed, in return (sound_sample_fixed_t)sample_period; } +/* +* Caskey, Damon V. +* 2026-08-13 +* +* Calculate a producer stream period directly from its decimal +* playback rate. Retaining the caller's full rate until the final +* fixed-point conversion prevents movie and audio clocks from +* diverging through integer percentage rounding. +*/ +static sound_sample_fixed_t sound_sample_period_calculate_decimal( + double speed, + int sample_frequency +) +{ + long double sample_period; + const long double period_max = (long double)UINT64_MAX; + + if(!(speed > 0.0) || sample_frequency <= 0 || playfrequency <= 0) { + return 0; + } + + sample_period = + (long double)SOUND_SAMPLE_FIXED_ONE * + (long double)speed * + (long double)sample_frequency / + (long double)playfrequency; + if(sample_period >= period_max - 0.5L) { + return UINT64_MAX; + } + if(sample_period < 1.0L) { + return 1; + } + return (sound_sample_fixed_t)(sample_period + 0.5L); +} + #define SOUND_STREAM_UPDATE_BYTE_BUDGET (256U * 1024U) static unsigned int sound_stream_update_cursor; @@ -3128,13 +3163,15 @@ bool sound_set_channel_period(int channel, uint64_t period) { * Caskey, Damon V. * 2026-08-12 * -* Change producer stream speed only while its play ID -* still owns the requested generic sound channel. +* Change producer stream speed only while its play ID still owns +* the requested generic sound channel. Decimal rates retain their +* precision through the final fixed-point period conversion. */ -bool sound_set_channel_speed_owned(int channel, int play_id, unsigned int speed) { +bool sound_set_channel_speed_owned(int channel, int play_id, double speed) { channelstruct *record; + sound_sample_fixed_t period; - if(play_id < 0 || speed == 0) { + if(play_id < 0 || !(speed > 0.0)) { return false; } @@ -3148,7 +3185,12 @@ bool sound_set_channel_speed_owned(int channel, int play_id, unsigned int speed) return false; } - record->fp_period = sound_sample_period_calculate(speed, record->frequency); + period = sound_sample_period_calculate_decimal(speed, record->frequency); + if(!period) { + SB_unlock_audio(); + return false; + } + record->fp_period = period; SB_unlock_audio(); return true; } diff --git a/engine/source/gamelib/soundmix.h b/engine/source/gamelib/soundmix.h index 4fc8586b0..6ace9aeb3 100644 --- a/engine/source/gamelib/soundmix.h +++ b/engine/source/gamelib/soundmix.h @@ -163,7 +163,7 @@ size_t sound_group_set_position( void sound_volume_sample(int channel, int lvolume, int rvolume); bool sound_set_channel_loop_offset(int channel, uint64_t loop_start_frame); bool sound_set_channel_period(int channel, uint64_t period); -bool sound_set_channel_speed_owned(int channel, int play_id, unsigned int speed); +bool sound_set_channel_speed_owned(int channel, int play_id, double speed); bool sound_pause_channel_owned(int channel, int play_id, int toggle); bool sound_set_channel_priority(int channel, unsigned int priority); bool sound_set_channel_position(int channel, uint64_t sample_position); diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index dfd819172..f285c8ad9 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -504,7 +504,6 @@ static bool movie_playback_open_media( s_movie_source *source = movie_source_get(playback->source_id); yuv_video_mode previous_mode = playback->video_mode; uint64_t duration; - unsigned int speed_percent; if(!source) { return false; @@ -571,9 +570,7 @@ static bool movie_playback_open_media( playback->terminal_pending = 0; if(playback->speed > 0.0) { - speed_percent = (unsigned int)(playback->speed * 100.0 + 0.5); - if(speed_percent < 1U) speed_percent = 1U; - webm_set_audio_speed(playback->context, speed_percent); + webm_set_audio_speed(playback->context, playback->speed); } webm_set_audio_paused( playback->context, @@ -1243,7 +1240,6 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) uint64_t now; uint64_t position; double previous_speed; - unsigned int speed_percent; if(movie_playback_get_index(playback) < 0 || !playback->active || @@ -1270,9 +1266,7 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) return false; } } else if(speed > 0.0) { - speed_percent = (unsigned int)(speed * 100.0 + 0.5); - if(speed_percent < 1U) speed_percent = 1U; - webm_set_audio_speed(playback->context, speed_percent); + webm_set_audio_speed(playback->context, speed); webm_set_audio_paused(playback->context, playback->paused); } else { playback->reverse_pending = 0; diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 2b2eb7231..b1abcf525 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -1111,7 +1111,7 @@ void webm_set_audio_paused(webm_context *ctx, int paused) } } -void webm_set_audio_speed(webm_context *ctx, unsigned int speed) +void webm_set_audio_speed(webm_context *ctx, double speed) { if(ctx && ctx->audio_track >= 0) { sound_set_channel_speed_owned( diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index ce61f17d8..c3d545f90 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -30,7 +30,7 @@ webm_context *webm_start_playback_ex( void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims); uint64_t webm_get_duration(webm_context *ctx); void webm_set_audio_paused(webm_context *ctx, int paused); -void webm_set_audio_speed(webm_context *ctx, unsigned int speed); +void webm_set_audio_speed(webm_context *ctx, double speed); void webm_close(webm_context *ctx); int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame); From 475a06ea5784480c528350986370c9d72f6628ef Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 13 Aug 2026 00:53:11 -0400 Subject: [PATCH 08/26] Separate movie playback from screen composition Add movie_draw_to_screen() so creators explicitly draw a retained movie frame to a selected screen and control its position in the composition order. Remove automatic main-screen and subscreen rendering, persistent screen ownership, movie_set_screen(), and MOVIE_PROPERTY_SCREEN. Update legacy playwebm() to clear and present only when a new movie frame arrives instead of presenting on every one-millisecond decoder poll. Preserve final-frame presentation and invalidate scaled-frame caches correctly when drawing one movie to multiple target sizes. --- engine/openbor.c | 19 +++--- engine/openborscript.c | 6 -- engine/source/openborscript/constants.c | 1 - engine/source/openborscript/index.c | 6 +- engine/source/openborscript/movie.c | 30 ++++----- engine/source/openborscript/movie.h | 3 +- engine/source/webmlib/movie_playback.c | 87 ++++++------------------- engine/source/webmlib/movie_playback.h | 7 +- 8 files changed, 51 insertions(+), 108 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index e153428a9..dc856a473 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -50918,7 +50918,6 @@ void update(int ingame, int usevwait) movie_playback_update( (bothnewkeys & (FLAG_ESC | FLAG_ANYBUTTON)) != 0 ); - movie_playback_render_subscreens(); #endif if(playrecstatus->status == A_REC_REC && !_pause && level) if ( !recordInputs() ) stopRecordInputs(); @@ -51096,9 +51095,6 @@ void update(int ingame, int usevwait) /********** update screen **************/ spriteq_draw(vscreen, 0, MIN_INT, MAX_INT, 0, 0); // notice, always draw sprites at the very end of other methods -#ifdef WEBM - movie_playback_render_main(vscreen); -#endif if(_pause != 2 && !noscreenshot && (bothnewkeys & FLAG_SCREENSHOT)) { @@ -51908,6 +51904,7 @@ int playwebm(const char *path, int noskip) video_copy_screen(vscreen); while(playback->active) { int interrupt_requested; + int present_frame; inputrefresh(playrecstatus->status); interrupt_requested = @@ -51919,16 +51916,22 @@ int playwebm(const char *path, int noskip) if(!playback->active) { break; } - if(!movie_playback_render_main(vscreen)) { - retval = 0; - break; + present_frame = playback->frame_dirty; + if(present_frame) { + clearscreen(vscreen); + if(!movie_playback_draw_to_screen(vscreen, playback->index)) { + retval = 0; + break; + } } if(playback->current_frame && !noscreenshot && (bothnewkeys & FLAG_SCREENSHOT)) { screenshot(vscreen, getpal, 1); } - video_copy_screen(vscreen); + if(present_frame) { + video_copy_screen(vscreen); + } usleep(1000); } diff --git a/engine/openborscript.c b/engine/openborscript.c index 2d9d9f505..655dd6ce7 100644 --- a/engine/openborscript.c +++ b/engine/openborscript.c @@ -416,12 +416,6 @@ void _freeheapnode(void *ptr) free(((s_sprite *)ptr)->mask); } } -#ifdef WEBM - else if(((s_screen *)ptr)->magic == screen_magic) - { - movie_playback_detach_screen((s_screen *)ptr); - } -#endif free(ptr); } diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index 294d610a3..f0a77e591 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1154,7 +1154,6 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) ICMPCONST(MOVIE_PROPERTY_PAUSED) ICMPCONST(MOVIE_PROPERTY_POSITION) ICMPCONST(MOVIE_PROPERTY_REPEAT) - ICMPCONST(MOVIE_PROPERTY_SCREEN) ICMPCONST(MOVIE_PROPERTY_SOUND_CHANNEL) ICMPCONST(MOVIE_PROPERTY_SOURCE) ICMPCONST(MOVIE_PROPERTY_SPEED) diff --git a/engine/source/openborscript/index.c b/engine/source/openborscript/index.c index 41f962130..205a60c78 100644 --- a/engine/source/openborscript/index.c +++ b/engine/source/openborscript/index.c @@ -937,9 +937,9 @@ const char *Script_GetFunctionName(void *functionRef) { return "movie_play"; } - else if (functionRef == ((void *)openbor_movie_set_screen)) + else if (functionRef == ((void *)openbor_movie_draw_to_screen)) { - return "movie_set_screen"; + return "movie_draw_to_screen"; } else if (functionRef == ((void *)openbor_movie_set_sound_channel)) { @@ -1806,7 +1806,7 @@ void Script_LoadSystemFunctions() List_InsertAfter(&theFunctionList, (void *)openbor_movie_play, "movie_play"); List_InsertAfter(&theFunctionList, - (void *)openbor_movie_set_screen, "movie_set_screen"); + (void *)openbor_movie_draw_to_screen, "movie_draw_to_screen"); List_InsertAfter(&theFunctionList, (void *)openbor_movie_set_sound_channel, "movie_set_sound_channel"); List_InsertAfter(&theFunctionList, diff --git a/engine/source/openborscript/movie.c b/engine/source/openborscript/movie.c index 0f45581e4..609900045 100644 --- a/engine/source/openborscript/movie.c +++ b/engine/source/openborscript/movie.c @@ -56,9 +56,6 @@ static const s_movie_property_info movie_properties[] = { { MOVIE_PROPERTY_REPEAT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, PROPERTY_MEMBER_OFFSET(s_movie_playback, repeat), "MOVIE_PROPERTY_REPEAT", VT_INTEGER }, - { MOVIE_PROPERTY_SCREEN, PROPERTY_ACCESS_CONFIG_READ, - PROPERTY_MEMBER_OFFSET(s_movie_playback, screen), - "MOVIE_PROPERTY_SCREEN", VT_PTR }, { MOVIE_PROPERTY_SOUND_CHANNEL, PROPERTY_ACCESS_CONFIG_READ, PROPERTY_MEMBER_OFFSET(s_movie_playback, sound_channel), "MOVIE_PROPERTY_SOUND_CHANNEL", VT_INTEGER }, @@ -245,37 +242,37 @@ HRESULT openbor_movie_play( /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-13 * -* Assign a creator-owned 32-bit screen. Null restores the -* default main-screen fill behavior. +* Draw the retained frame from one movie channel to a creator-owned +* 32-bit screen. Call position determines composition order. */ -HRESULT openbor_movie_set_screen( +HRESULT openbor_movie_draw_to_screen( ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount ) { - s_screen *screen = NULL; + s_screen *screen; + LONG channel; *pretvar = NULL; if(paramCount < 2 || varlist[0]->vt != VT_PTR || !varlist[0]->ptrVal || - movie_playback_get_index(varlist[0]->ptrVal) < 0 || - (varlist[1]->vt != VT_PTR && varlist[1]->vt != VT_EMPTY)) { + FAILED(ScriptVariant_IntegerValue(varlist[1], &channel))) { goto error; } - if(varlist[1]->vt == VT_PTR) { - screen = varlist[1]->ptrVal; - } - if(!movie_playback_set_screen(varlist[0]->ptrVal, screen)) { + screen = varlist[0]->ptrVal; + if(channel < 0 || + (unsigned int)channel >= MOVIE_CHANNEL_COUNT || + !movie_playback_draw_to_screen(screen, (int)channel)) { goto error; } return S_OK; error: - printf("\nScript error: movie_set_screen(void playback, void screen). Playback or screen is invalid.\n"); + printf("\nScript error: movie_draw_to_screen(void screen, int channel). Screen or movie channel is invalid.\n"); return E_FAIL; } @@ -521,7 +518,6 @@ HRESULT openbor_movie_set_property( case MOVIE_PROPERTY_ACTIVE: case MOVIE_PROPERTY_CHANNEL: case MOVIE_PROPERTY_DURATION: - case MOVIE_PROPERTY_SCREEN: case MOVIE_PROPERTY_SOUND_CHANNEL: case MOVIE_PROPERTY_SOURCE: case MOVIE_PROPERTY_END: @@ -566,7 +562,7 @@ static HRESULT openbor_movie_unsupported(ScriptVariant **pretvar) MOVIE_UNSUPPORTED(openbor_movie_load) MOVIE_UNSUPPORTED(openbor_movie_unload) MOVIE_UNSUPPORTED(openbor_movie_play) -MOVIE_UNSUPPORTED(openbor_movie_set_screen) +MOVIE_UNSUPPORTED(openbor_movie_draw_to_screen) MOVIE_UNSUPPORTED(openbor_movie_set_sound_channel) MOVIE_UNSUPPORTED(openbor_movie_stop) MOVIE_UNSUPPORTED(openbor_movie_get_channel_object) diff --git a/engine/source/openborscript/movie.h b/engine/source/openborscript/movie.h index 69aed4920..0af2255e5 100644 --- a/engine/source/openborscript/movie.h +++ b/engine/source/openborscript/movie.h @@ -21,7 +21,6 @@ typedef enum e_movie_properties { MOVIE_PROPERTY_PAUSED, MOVIE_PROPERTY_POSITION, /* Milliseconds; writing seeks. */ MOVIE_PROPERTY_REPEAT, - MOVIE_PROPERTY_SCREEN, MOVIE_PROPERTY_SOUND_CHANNEL, MOVIE_PROPERTY_SOURCE, MOVIE_PROPERTY_SPEED, @@ -32,7 +31,7 @@ typedef enum e_movie_properties { HRESULT openbor_movie_load(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); HRESULT openbor_movie_unload(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); HRESULT openbor_movie_play(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); -HRESULT openbor_movie_set_screen(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); +HRESULT openbor_movie_draw_to_screen(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); HRESULT openbor_movie_set_sound_channel(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); HRESULT openbor_movie_stop(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); HRESULT openbor_movie_get_channel_object(ScriptVariant **varlist, ScriptVariant **pretvar, int paramCount); diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index f285c8ad9..57f00c057 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -636,8 +636,8 @@ s_movie_playback *movie_playback_play( * Caskey, Damon V. * 2026-08-12 * -* Stop individual, grouped, or screen-detached playbacks and -* release their source references and fixed channel slots. +* Stop individual or grouped playbacks and release their source +* references and fixed channel slots. */ void movie_playback_stop(s_movie_playback *playback) { @@ -666,25 +666,6 @@ void movie_playback_stop_all(void) } } -void movie_playback_detach_screen(const s_screen *screen) -{ - uint64_t active_mask; - int channel; - - if(!screen || !movie_playback_pool.initialized) { - return; - } - - active_mask = movie_playback_pool.active_mask; - while((channel = sound_channel_mask_first(active_mask)) >= 0) { - s_movie_playback *playback = &movie_playback_pool.channel[channel]; - active_mask &= ~(UINT64_C(1) << channel); - if(playback->screen == screen) { - movie_playback_stop(playback); - } - } -} - /* * Caskey, Damon V. * 2026-08-12 @@ -717,6 +698,7 @@ static bool movie_playback_prepare_frame( uint32_t protected_black; yuv_to_rgb(playback->current_frame, playback->rgb_frame); + playback->scaled_frame_dirty = 1; if(playback->black_filter) { pixel_count = (size_t)playback->rgb_frame->width * @@ -751,11 +733,12 @@ static bool movie_playback_prepare_frame( if(!playback->scaled_frame) { return false; } - if(refresh) { + if(refresh || playback->scaled_frame_dirty) { scalescreen32(playback->scaled_frame, playback->rgb_frame); } playback->frame_dirty = 0; + playback->scaled_frame_dirty = 0; *render_frame = playback->scaled_frame; return true; } @@ -946,7 +929,7 @@ void movie_playback_update(int interrupt_requested) movie_playback_stop(playback); continue; } - if(playback->terminal_pending && !playback->frame_dirty) { + if(playback->terminal_pending) { if(playback->repeat) { if(!movie_playback_open_media( playback, @@ -1028,46 +1011,29 @@ void movie_playback_update(int interrupt_requested) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-13 * -* Compose bound subscreens before creator scripts, and compose -* unbound playback over the main screen at its presentation stage. +* Draw the retained frame from one active movie channel to a +* creator-selected 32-bit screen. Playback and composition remain +* independent so script call order controls clearing and layering. */ -void movie_playback_render_subscreens(void) -{ - uint64_t active_mask = movie_playback_pool.active_mask; - int channel; - - while((channel = sound_channel_mask_first(active_mask)) >= 0) { - s_movie_playback *playback = &movie_playback_pool.channel[channel]; - active_mask &= ~(UINT64_C(1) << channel); - if(playback->screen && - !movie_playback_render_to(playback, playback->screen)) { - movie_playback_stop(playback); - } - } -} - -bool movie_playback_render_main(s_screen *screen) +bool movie_playback_draw_to_screen(s_screen *screen, int channel) { - uint64_t active_mask; - int channel; - bool result = true; + s_movie_playback *playback; - if(!movie_playback_screen_valid(screen)) { + if(!movie_playback_pool.initialized || + channel < 0 || + (unsigned int)channel >= MOVIE_CHANNEL_COUNT || + !movie_playback_screen_valid(screen)) { return false; } - active_mask = movie_playback_pool.active_mask; - while((channel = sound_channel_mask_first(active_mask)) >= 0) { - s_movie_playback *playback = &movie_playback_pool.channel[channel]; - active_mask &= ~(UINT64_C(1) << channel); - if(!playback->screen && !movie_playback_render_to(playback, screen)) { - movie_playback_stop(playback); - result = false; - } + playback = &movie_playback_pool.channel[channel]; + if(!playback->active || + !(movie_playback_pool.active_mask & (UINT64_C(1) << channel))) { + return false; } - return result; + return movie_playback_render_to(playback, screen); } /* @@ -1197,17 +1163,6 @@ bool movie_playback_set_repeat(s_movie_playback *playback, int repeat) return true; } -bool movie_playback_set_screen(s_movie_playback *playback, s_screen *screen) -{ - if(movie_playback_get_index(playback) < 0 || - (screen && !movie_playback_screen_valid(screen))) { - return false; - } - playback->screen = screen; - playback->frame_dirty = 1; - return true; -} - bool movie_playback_set_sound_channel( s_movie_playback *playback, int sound_channel, diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index e1822fff7..fb34de6c2 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -52,7 +52,6 @@ typedef struct s_movie_playback { float speed; uint64_t duration; uint64_t position; - s_screen *screen; struct webm_context *context; yuv_video_mode video_mode; @@ -65,6 +64,7 @@ typedef struct s_movie_playback { int volume; bool replace_all_audio; int frame_dirty; + int scaled_frame_dirty; int reverse_pending; int terminal_pending; } s_movie_playback; @@ -89,11 +89,9 @@ bool movie_playback_get_snapshot( ); uint64_t movie_playback_get_active_mask(void); void movie_playback_update(int interrupt_requested); -void movie_playback_render_subscreens(void); -bool movie_playback_render_main(s_screen *screen); +bool movie_playback_draw_to_screen(s_screen *screen, int channel); void movie_playback_stop(s_movie_playback *playback); void movie_playback_stop_all(void); -void movie_playback_detach_screen(const s_screen *screen); bool movie_playback_set_height(s_movie_playback *playback, int height); bool movie_playback_set_black_filter( @@ -110,7 +108,6 @@ bool movie_playback_set_position( int volume ); bool movie_playback_set_repeat(s_movie_playback *playback, int repeat); -bool movie_playback_set_screen(s_movie_playback *playback, s_screen *screen); bool movie_playback_set_sound_channel( s_movie_playback *playback, int sound_channel, From 5901cf0862db40247e5c3448330a5a8807a6f8e0 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 13 Aug 2026 10:08:48 -0400 Subject: [PATCH 09/26] Add adaptive caching mode for movie sources Add MOVIE_LOADING_AUTO as an opt-in loading mode while retaining streaming as the default behavior. Allow automatic caching when the movie fits within one-quarter of system memory and leaves a reserve of one-eighth system memory or 128 MiB, whichever is greater. Track all resident movie caches against the cumulative automatic budget without imposing a fixed upper limit. Add nonfatal movie cache allocation and bounded packfile reads. Automatic mode falls back to streaming under memory pressure, while forced cache mode returns a clean load failure. --- engine/source/openborscript/constants.c | 1 + engine/source/openborscript/movie.c | 4 +- engine/source/webmlib/movie_playback.c | 187 +++++++++++++++++++++--- engine/source/webmlib/movie_playback.h | 1 + engine/version.tmp | 6 +- 5 files changed, 173 insertions(+), 26 deletions(-) diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index f0a77e591..e3f01f816 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1164,6 +1164,7 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) ICMPCONST(MOVIE_CHANNEL_AUTO) ICMPCONST(MOVIE_LOADING_STREAM) ICMPCONST(MOVIE_LOADING_CACHE) + ICMPCONST(MOVIE_LOADING_AUTO) ICMPCONST(MOVIE_LOADING_END) #endif diff --git a/engine/source/openborscript/movie.c b/engine/source/openborscript/movie.c index 609900045..d16084f1d 100644 --- a/engine/source/openborscript/movie.c +++ b/engine/source/openborscript/movie.c @@ -117,8 +117,8 @@ static const s_property_access_map movie_get_property_map( * Caskey, Damon V. * 2026-08-12 * -* Load one reusable streamed or cached movie source and return -* its stable integer ID. +* Load one reusable streamed, cached, or automatic movie source +* and return its stable integer ID. Omitted mode remains stream. */ HRESULT openbor_movie_load( ScriptVariant **varlist, diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 57f00c057..3d586eefe 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -13,7 +13,21 @@ #include #include +/* +* Caskey, Damon V. +* 2026-08-13 +* +* Allocate an optional movie cache without the engine's fatal +* allocation wrapper. Automatic mode must be able to recover from +* allocation pressure by falling back to streamed playback. +*/ +static void *movie_cache_allocate_nonfatal(size_t size) +{ + return malloc(size); +} + #include "globals.h" +#include "ram.h" #include "screen.h" #include "sound_channel.h" #include "soundmix.h" @@ -23,8 +37,14 @@ #define MOVIE_SOURCE_CAPACITY_INITIAL 64U #define MOVIE_REVERSE_SEEK_INTERVAL UINT64_C(33333333) +#define MOVIE_CACHE_READ_CHUNK_SIZE (1024U * 1024U) +#define MOVIE_CACHE_RESERVE_MINIMUM (UINT64_C(128) * UINT64_C(1024) * UINT64_C(1024)) -extern int buffer_pakfile(const char *filename, char **pbuffer, size_t *psize); +typedef enum e_movie_cache_result { + MOVIE_CACHE_RESULT_SUCCESS, + MOVIE_CACHE_RESULT_MEMORY, + MOVIE_CACHE_RESULT_IO +} e_movie_cache_result; typedef struct s_movie_source { char *path; @@ -42,6 +62,7 @@ typedef struct s_movie_playback_pool { size_t source_capacity; int next_source_id; uint64_t active_mask; + uint64_t cached_bytes; int initialized; int yuv_initialized; } s_movie_playback_pool; @@ -303,14 +324,111 @@ void movie_playback_shutdown(void) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-13 +* +* Determine whether an automatic movie cache preserves both its +* share of total memory and the required free-memory cushion. +* The budget scales without an upper cap for high-memory systems. +*/ +static bool movie_source_auto_cache_fits( + uint64_t file_size, + uint64_t total_memory, + uint64_t available_memory, + uint64_t cached_bytes +) +{ + uint64_t cache_budget; + uint64_t memory_reserve; + + if(!file_size || + !total_memory || + !available_memory || + file_size > (uint64_t)SIZE_MAX) { + return false; + } + + cache_budget = total_memory / 4U; + memory_reserve = total_memory / 8U; + if(memory_reserve < MOVIE_CACHE_RESERVE_MINIMUM) { + memory_reserve = MOVIE_CACHE_RESERVE_MINIMUM; + } + + if(file_size > cache_budget || + cached_bytes > cache_budget - file_size || + available_memory <= memory_reserve || + file_size > available_memory - memory_reserve) { + return false; + } + return true; +} + +/* +* Caskey, Damon V. +* 2026-08-13 +* +* Read one validated movie source into a nonfatal allocation using +* bounded packfile reads. Allocation pressure is distinguished from +* I/O failure so automatic mode only falls back for memory limits. +*/ +static e_movie_cache_result movie_source_read_cache( + int handle, + uint64_t file_size, + unsigned char **cache_buffer, + size_t *cache_size +) +{ + unsigned char *buffer; + uint64_t read_position = 0; + + if(handle < 0 || + !file_size || + file_size > (uint64_t)SIZE_MAX || + !cache_buffer || + !cache_size) { + return MOVIE_CACHE_RESULT_IO; + } + + buffer = movie_cache_allocate_nonfatal((size_t)file_size); + if(!buffer) { + return MOVIE_CACHE_RESULT_MEMORY; + } + + while(read_position < file_size) { + uint64_t bytes_remaining = file_size - read_position; + int read_size = bytes_remaining > MOVIE_CACHE_READ_CHUNK_SIZE + ? (int)MOVIE_CACHE_READ_CHUNK_SIZE + : (int)bytes_remaining; + + if(readpackfile( + handle, + buffer + (size_t)read_position, + read_size + ) != read_size) { + free(buffer); + return MOVIE_CACHE_RESULT_IO; + } + read_position += (uint64_t)read_size; + } + + *cache_buffer = buffer; + *cache_size = (size_t)file_size; + return MOVIE_CACHE_RESULT_SUCCESS; +} + +/* +* Caskey, Damon V. +* 2026-08-13 * -* Load one reusable movie source. Stream mode validates the -* path; cache mode retains one shared read-only byte buffer. +* Load one reusable movie source. Streaming remains the default, +* forced cache fails cleanly, and automatic cache falls back to +* streaming when its memory policy or allocation cannot be met. */ int movie_source_load(const char *path, e_movie_loading_mode loading_mode) { s_movie_source source = { 0 }; + e_movie_cache_result cache_result = MOVIE_CACHE_RESULT_SUCCESS; + packfile_signed_offset_t file_size; + bool attempt_cache; int handle = -1; int source_id; @@ -326,24 +444,46 @@ int movie_source_load(const char *path, e_movie_loading_mode loading_mode) return -1; } - if(loading_mode == MOVIE_LOADING_CACHE) { - if(buffer_pakfile( - path, - (char**)&source.cache_buffer, - &source.cache_size - ) != 1 || !source.cache_size) { - free(source.path); - free(source.cache_buffer); - return -1; - } - } else { - handle = openpackfile(path, packfile); - if(handle < 0) { - free(source.path); - return -1; - } + handle = openpackfile(path, packfile); + if(handle < 0) { + free(source.path); + return -1; + } + file_size = seekpackfile64(handle, 0, SEEK_END); + if(file_size <= 0 || seekpackfile64(handle, 0, SEEK_SET) < 0) { closepackfile(handle); + free(source.path); + return -1; + } + + attempt_cache = loading_mode == MOVIE_LOADING_CACHE || + (loading_mode == MOVIE_LOADING_AUTO && + movie_source_auto_cache_fits( + (uint64_t)file_size, + getSystemRam(BYTES), + getFreeRam(BYTES), + movie_playback_pool.cached_bytes + )); + if(attempt_cache) { + cache_result = movie_source_read_cache( + handle, + (uint64_t)file_size, + &source.cache_buffer, + &source.cache_size + ); } + closepackfile(handle); + + if(cache_result == MOVIE_CACHE_RESULT_IO || + (cache_result == MOVIE_CACHE_RESULT_MEMORY && + loading_mode == MOVIE_LOADING_CACHE)) { + free(source.path); + free(source.cache_buffer); + return -1; + } + source.loading_mode = source.cache_buffer + ? MOVIE_LOADING_CACHE + : MOVIE_LOADING_STREAM; source_id = movie_playback_pool.next_source_id; if(!movie_source_reserve((size_t)source_id + 1U)) { @@ -353,9 +493,9 @@ int movie_source_load(const char *path, e_movie_loading_mode loading_mode) } source.id = source_id; - source.loading_mode = loading_mode; source.active = 1; movie_playback_pool.source[source_id] = source; + movie_playback_pool.cached_bytes += (uint64_t)source.cache_size; ++movie_playback_pool.next_source_id; return source_id; } @@ -375,6 +515,11 @@ bool movie_source_unload(int source_id) return false; } + if((uint64_t)source->cache_size <= movie_playback_pool.cached_bytes) { + movie_playback_pool.cached_bytes -= (uint64_t)source->cache_size; + } else { + movie_playback_pool.cached_bytes = 0; + } free(source->path); free(source->cache_buffer); memset(source, 0, sizeof(*source)); diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index fb34de6c2..a8bc36345 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -23,6 +23,7 @@ typedef enum e_movie_loading_mode { MOVIE_LOADING_STREAM, MOVIE_LOADING_CACHE, + MOVIE_LOADING_AUTO, MOVIE_LOADING_END } e_movie_loading_mode; diff --git a/engine/version.tmp b/engine/version.tmp index 822303817..bafc697e8 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7904" -#define VERSION_BUILD_INT 7904 -#define VERSION_COMMIT "bcec7cb" +#define VERSION_BUILD "7969" +#define VERSION_BUILD_INT 7969 +#define VERSION_COMMIT "475a06e" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From af2019e5fe04b84ccd9a2389f722e8e2e15a2e06 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 13 Aug 2026 18:15:45 -0400 Subject: [PATCH 10/26] Fix WebM playback initialization and stream I/O Restore compatibility with valid WebM files that omit the optional DefaultDuration element. Playback now relies on packet timestamps and uses packet duration to space laced frames when a default duration is unavailable. Correct the Nestegg streaming callbacks to return the expected seek result, use 64-bit packfile positions, verify read boundaries, and complete partial reads safely. Add explicit diagnostics for container initialization, track discovery, allocation, and worker-thread failures instead of allowing movie playback to fail silently. These fixes apply to both the movie playback API and legacy playwebm(). --- engine/source/webmlib/movie_playback.c | 5 + engine/source/webmlib/vidplay.c | 156 ++++++++++++++++++++----- engine/version.tmp | 6 +- 3 files changed, 135 insertions(+), 32 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 3d586eefe..aa9176bd4 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -704,6 +704,11 @@ static bool movie_playback_open_media( ); } if(!playback->rgb_frame) { + printf( + "Error: Unable to allocate %d*%d movie conversion screen.\n", + playback->video_mode.width, + playback->video_mode.height + ); movie_playback_close_decoder(playback); return false; } diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index b1abcf525..37a83aa3e 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -84,6 +84,7 @@ typedef struct { typedef struct { int packhandle; + packfile_signed_offset_t stream_size; const unsigned char *cache_buffer; size_t cache_size; size_t cache_position; @@ -107,19 +108,27 @@ struct webm_context { /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-13 * -* Read or seek either a creator-requested memory cache or -* the ordinary packfile stream through the same Nestegg I/O. +* Read or seek either a creator-requested memory cache or the +* ordinary packfile stream through the same Nestegg I/O. Streamed +* reads supply complete spans, while successful seeks return zero +* as required by the decoder callback contract. */ static int webm_io_read(void *buffer, size_t length, void *userdata) { webm_io_context *io_ctx = userdata; + packfile_signed_offset_t stream_position; + size_t read_position = 0; int bytesRead; if(io_ctx->cache_buffer) { - size_t remaining = io_ctx->cache_size - io_ctx->cache_position; + size_t remaining; + if(io_ctx->cache_position > io_ctx->cache_size) { + return -1; + } + remaining = io_ctx->cache_size - io_ctx->cache_position; if(remaining < length) { return 0; } @@ -132,13 +141,34 @@ static int webm_io_read(void *buffer, size_t length, void *userdata) return 1; } - if(length > (size_t)INT_MAX) { + stream_position = seekpackfile64(io_ctx->packhandle, 0, SEEK_CUR); + if(stream_position < 0 || + stream_position > io_ctx->stream_size || + (uint64_t)length > + (uint64_t)(io_ctx->stream_size - stream_position)) { + return stream_position < 0 ? -1 : 0; + } + + while(read_position < length) { + size_t remaining = length - read_position; + int read_length = remaining > (size_t)INT_MAX + ? INT_MAX + : (int)remaining; + + bytesRead = readpackfile( + io_ctx->packhandle, + (unsigned char*)buffer + read_position, + read_length + ); + if(bytesRead <= 0) { + return -1; + } + read_position += (size_t)bytesRead; + } + if(read_position != length) { return -1; } - bytesRead = readpackfile(io_ctx->packhandle, buffer, (int)length); - if (bytesRead < 0) return -1; - else if ((size_t)bytesRead != length) return 0; - else return 1; + return 1; } static int webm_io_seek(int64_t offset, int whence, void *userdata) @@ -171,10 +201,11 @@ static int webm_io_seek(int64_t offset, int whence, void *userdata) return 0; } - if(offset < INT_MIN || offset > INT_MAX) { - return -1; - } - return seekpackfile(io_ctx->packhandle, (int)offset, whence); + return seekpackfile64( + io_ctx->packhandle, + (packfile_signed_offset_t)offset, + whence + ) < 0 ? -1 : 0; } static int64_t webm_io_tell(void *userdata) @@ -184,7 +215,7 @@ static int64_t webm_io_tell(void *userdata) if(io_ctx->cache_buffer) { return (int64_t)io_ctx->cache_position; } - return seekpackfile(io_ctx->packhandle, 0, SEEK_CUR); + return seekpackfile64(io_ctx->packhandle, 0, SEEK_CUR); } static FixedSizeQueue *queue_init(int max_size, volatile int *quit) @@ -690,9 +721,37 @@ static void close_audio(audio_context *audio_ctx) ); } +/* +* Caskey, Damon V. +* 2026-08-13 +* +* Resolve timestamps for laced video frames without requiring the +* optional WebM DefaultDuration element. Prefer the track default, +* then the packet duration, and finally preserve decode order with +* the smallest representable timestamp step. +*/ +static uint64_t video_packet_frame_delay( + video_context *video_ctx, + nestegg_packet *packet, + unsigned int chunks +) +{ + uint64_t packet_duration = 0; + + if(video_ctx->frame_delay || chunks < 2) { + return video_ctx->frame_delay; + } + nestegg_packet_duration(packet, &packet_duration); + if(packet_duration) { + packet_duration /= chunks; + } + return packet_duration ? packet_duration : UINT64_C(1); +} + static int video_thread(void *data) { video_context *ctx = (video_context*) data; + uint64_t frame_delay; uint64_t timestamp; while(!*ctx->quit) @@ -705,6 +764,7 @@ static int video_thread(void *data) if (*ctx->quit || pkt == NULL) break; nestegg_packet_count(pkt, &chunks); nestegg_packet_tstamp(pkt, ×tamp); + frame_delay = video_packet_frame_delay(ctx, pkt, chunks); for (chunk = 0; chunk < chunks; ++chunk) { @@ -751,7 +811,9 @@ static int video_thread(void *data) yuv_frame_destroy(frame); break; } - timestamp += ctx->frame_delay; + timestamp = timestamp <= UINT64_MAX - frame_delay + ? timestamp + frame_delay + : UINT64_MAX; } if(*ctx->quit) break; } @@ -765,10 +827,11 @@ static int video_thread(void *data) // returns 0 on success, -1 on error /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-13 * -* Initialize video queues against their owning decoder's -* stop state so multiple contexts can run independently. +* Initialize the video decoder and queues against their owning stop +* state. DefaultDuration is optional - packet timestamps and packet +* duration retain playback timing when the track omits it. */ static int init_video( nestegg *nestegg_ctx, @@ -778,8 +841,13 @@ static int init_video( ) { nestegg_video_params video_params; - if(nestegg_track_video_params(nestegg_ctx, track, &video_params) < 0 || - video_params.stereo_mode != NESTEGG_VIDEO_MONO || + int has_default_duration; + + if(nestegg_track_video_params(nestegg_ctx, track, &video_params) < 0) { + printf("Error: Unable to read WebM video track parameters\n"); + return -1; + } + if(video_params.stereo_mode != NESTEGG_VIDEO_MONO || video_params.width < 2 || video_params.height < 2 || (video_params.width & 1U) || @@ -802,22 +870,27 @@ static int init_video( video_ctx->height = video_params.height; video_ctx->display_width = video_params.display_width; video_ctx->display_height = video_params.display_height; - if(nestegg_track_default_duration( + has_default_duration = nestegg_track_default_duration( nestegg_ctx, track, &(video_ctx->frame_delay) - ) < 0 || !video_ctx->frame_delay) { - vpx_codec_destroy(&(video_ctx->vpx_ctx)); - return -1; - } - printf("Video track: resolution=%i*%i, display resolution=%i*%i, %.2f frames/second\n", + ) == 0 && video_ctx->frame_delay; + if(has_default_duration) { + printf("Video track: resolution=%i*%i, display resolution=%i*%i, %.2f frames/second\n", video_params.width, video_params.height, video_params.display_width, video_params.display_height, 1000000000.0 / video_ctx->frame_delay); + } else { + video_ctx->frame_delay = 0; + printf("Video track: resolution=%i*%i, display resolution=%i*%i, packet-timestamp timing\n", + video_params.width, video_params.height, + video_params.display_width, video_params.display_height); + } video_ctx->quit = quit; video_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE, quit); video_ctx->frame_queue = queue_init(FRAME_QUEUE_SIZE, quit); if(!video_ctx->packet_queue || !video_ctx->frame_queue) { + printf("Error: Unable to allocate WebM video queues\n"); if(video_ctx->packet_queue) queue_destroy(video_ctx->packet_queue); if(video_ctx->frame_queue) queue_destroy(video_ctx->frame_queue); video_ctx->packet_queue = NULL; @@ -919,11 +992,15 @@ webm_context *webm_start_playback_ex( if(!path || !path[0] || sound_channel < 0 || (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX) { + printf("Error: Invalid WebM playback request\n"); return NULL; } ctx = calloc(1, sizeof(*ctx)); - if(!ctx) return NULL; + if(!ctx) { + printf("Error: Unable to allocate WebM playback context\n"); + return NULL; + } ctx->io_ctx.packhandle = -1; ctx->audio_track = -1; ctx->video_track = -1; @@ -935,6 +1012,7 @@ webm_context *webm_start_playback_ex( if(cache_buffer) { if(!cache_size) { + printf("Error: Cached WebM source %s has no data\n", path); goto error_io; } ctx->io_ctx.cache_buffer = cache_buffer; @@ -945,11 +1023,27 @@ webm_context *webm_start_playback_ex( printf("Error: Unable to open file %s for playback\n", path); goto error_io; } + ctx->io_ctx.stream_size = seekpackfile64( + ctx->io_ctx.packhandle, + 0, + SEEK_END + ); + if(ctx->io_ctx.stream_size <= 0 || + seekpackfile64(ctx->io_ctx.packhandle, 0, SEEK_SET) != 0) { + printf("Error: Unable to size WebM stream %s\n", path); + goto error_io; + } } - if(nestegg_init(&(ctx->nestegg_ctx), io, NULL, -1) < 0) goto error_io; + if(nestegg_init(&(ctx->nestegg_ctx), io, NULL, -1) < 0) { + printf("Error: Unable to initialize WebM container %s\n", path); + goto error_io; + } nestegg_duration(ctx->nestegg_ctx, &ctx->duration); - if(nestegg_track_count(ctx->nestegg_ctx, &num_tracks) < 0) goto error_nestegg; + if(nestegg_track_count(ctx->nestegg_ctx, &num_tracks) < 0) { + printf("Error: Unable to read WebM tracks from %s\n", path); + goto error_nestegg; + } for(i = 0; i < num_tracks; i++) { int track_type = nestegg_track_type(ctx->nestegg_ctx, i); @@ -995,6 +1089,7 @@ webm_context *webm_start_playback_ex( } ctx->the_video_thread = thread_create(video_thread, "video", &(ctx->video_ctx)); if(!ctx->the_video_thread) { + printf("Error: Unable to start WebM video decoder thread\n"); webm_close(ctx); return NULL; } @@ -1013,6 +1108,7 @@ webm_context *webm_start_playback_ex( ) == 0) { ctx->the_audio_thread = thread_create(audio_thread, "audio", &(ctx->audio_ctx)); if(!ctx->the_audio_thread) { + printf("Error: Unable to start WebM audio decoder thread\n"); webm_close(ctx); return NULL; } @@ -1026,6 +1122,7 @@ webm_context *webm_start_playback_ex( /* Blocking legacy playback must service existing channel zero audio. */ ctx->the_audio_thread = thread_create(bgm_update_thread, "bgm", ctx); if(!ctx->the_audio_thread) { + printf("Error: Unable to start WebM legacy audio service thread\n"); webm_close(ctx); return NULL; } @@ -1033,6 +1130,7 @@ webm_context *webm_start_playback_ex( ctx->the_demux_thread = thread_create(demux_thread, "demux", ctx); if(!ctx->the_demux_thread) { + printf("Error: Unable to start WebM demux thread\n"); webm_close(ctx); return NULL; } diff --git a/engine/version.tmp b/engine/version.tmp index bafc697e8..8381486f7 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7969" -#define VERSION_BUILD_INT 7969 -#define VERSION_COMMIT "475a06e" +#define VERSION_BUILD "7970" +#define VERSION_BUILD_INT 7970 +#define VERSION_COMMIT "5901cf0" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From e20027930133715264786970194126675724fad1 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 13 Aug 2026 20:27:05 -0400 Subject: [PATCH 11/26] Harden audio-video time sync. Convert creator-supplied playback speed to one canonical float. Clamp supported range and round to thousandths so video timing, audio resampling, property reads, and later decoder reopens all consume the same applied rate. --- engine/source/webmlib/movie_playback.c | 50 +++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index aa9176bd4..daa66e591 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -39,6 +39,7 @@ static void *movie_cache_allocate_nonfatal(size_t size) #define MOVIE_REVERSE_SEEK_INTERVAL UINT64_C(33333333) #define MOVIE_CACHE_READ_CHUNK_SIZE (1024U * 1024U) #define MOVIE_CACHE_RESERVE_MINIMUM (UINT64_C(128) * UINT64_C(1024) * UINT64_C(1024)) +#define MOVIE_SPEED_ROUNDING_FACTOR 1000.0 typedef enum e_movie_cache_result { MOVIE_CACHE_RESULT_SUCCESS, @@ -69,6 +70,40 @@ typedef struct s_movie_playback_pool { static s_movie_playback_pool movie_playback_pool; +/* +* Caskey, Damon V. +* 2026-08-13 +* +* Convert creator-supplied playback speed to one canonical float. +* Clamp supported range and round to thousandths so video timing, +* audio resampling, property reads, and later decoder reopens all +* consume the same applied rate. +*/ +static bool movie_playback_sanitize_speed( + double requested_speed, + float *applied_speed +) +{ + if(!applied_speed || !isfinite(requested_speed)) { + return false; + } + + if(requested_speed < MOVIE_SPEED_MIN) { + requested_speed = MOVIE_SPEED_MIN; + } else if(requested_speed > MOVIE_SPEED_MAX) { + requested_speed = MOVIE_SPEED_MAX; + } + requested_speed = round( + requested_speed * MOVIE_SPEED_ROUNDING_FACTOR + ) / MOVIE_SPEED_ROUNDING_FACTOR; + + *applied_speed = (float)requested_speed; + if(*applied_speed == 0.0f) { + *applied_speed = 0.0f; + } + return true; +} + /* * Caskey, Damon V. * 2026-08-12 @@ -1345,15 +1380,14 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) uint64_t now; uint64_t position; double previous_speed; + float applied_speed; if(movie_playback_get_index(playback) < 0 || !playback->active || - !isfinite(speed) || - speed < MOVIE_SPEED_MIN || - speed > MOVIE_SPEED_MAX) { + !movie_playback_sanitize_speed(speed, &applied_speed)) { return false; } - if(playback->speed == speed) { + if(playback->speed == applied_speed) { return true; } @@ -1362,16 +1396,16 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) previous_speed = playback->speed; playback->position_anchor = position; playback->clock_anchor = now; - playback->speed = speed; + playback->speed = applied_speed; playback->position = position / UINT64_C(1000000); - if(speed > 0.0 && previous_speed <= 0.0) { + if(playback->speed > 0.0 && previous_speed <= 0.0) { if(!movie_playback_open_media(playback, position, playback->volume)) { movie_playback_stop(playback); return false; } - } else if(speed > 0.0) { - webm_set_audio_speed(playback->context, speed); + } else if(playback->speed > 0.0) { + webm_set_audio_speed(playback->context, playback->speed); webm_set_audio_paused(playback->context, playback->paused); } else { playback->reverse_pending = 0; From ea4a479ea6af6180e93ff790e9c5cea630ec46be Mon Sep 17 00:00:00 2001 From: DCurrent Date: Sat, 15 Aug 2026 15:19:00 -0400 Subject: [PATCH 12/26] Increase streamed PCM reserve and preroll WebM audio. --- engine/source/gamelib/sound_stream.c | 2 + engine/source/gamelib/sound_stream.h | 16 +-- engine/source/gamelib/soundmix.c | 47 ++++++++ engine/source/gamelib/soundmix.h | 20 ++++ engine/source/webmlib/movie_playback.c | 5 + engine/source/webmlib/vidplay.c | 157 +++++++++++++++++++++++-- engine/version.tmp | 6 +- 7 files changed, 231 insertions(+), 22 deletions(-) diff --git a/engine/source/gamelib/sound_stream.c b/engine/source/gamelib/sound_stream.c index f1dcbc3d0..1599102fc 100644 --- a/engine/source/gamelib/sound_stream.c +++ b/engine/source/gamelib/sound_stream.c @@ -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(); @@ -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; diff --git a/engine/source/gamelib/sound_stream.h b/engine/source/gamelib/sound_stream.h index 23eaba938..0d0d87b2e 100644 --- a/engine/source/gamelib/sound_stream.h +++ b/engine/source/gamelib/sound_stream.h @@ -17,14 +17,13 @@ * Caskey, Damon V. * 2026-08-01 * -* 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) @@ -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)( diff --git a/engine/source/gamelib/soundmix.c b/engine/source/gamelib/soundmix.c index 5c89b67b7..f60b913bd 100644 --- a/engine/source/gamelib/soundmix.c +++ b/engine/source/gamelib/soundmix.c @@ -1432,9 +1432,16 @@ static void sound_mix_stream_channel( for(;;) { stream_buffer = &stream->buffer[stream->read_buffer]; if(!stream_buffer->ready) { + if(!stream->producer_finished && !stream->underrun_active) { + stream->underrun_active = 1; + if(stream->underrun_count < UINT64_MAX) { + stream->underrun_count++; + } + } stream->fp_buffer_position = buffer_position_fixed; return; } + stream->underrun_active = 0; buffer_length_fixed = SOUND_SAMPLE_INT_TO_FIX(stream_buffer->frame_count); if(buffer_position_fixed < buffer_length_fixed) { @@ -1444,6 +1451,9 @@ static void sound_mix_stream_channel( buffer_position_fixed -= buffer_length_fixed; terminal = stream_buffer->terminal; stream_buffer->ready = 0; + if(stream->ready_buffer_count > 0) { + stream->ready_buffer_count--; + } stream->read_buffer = (stream->read_buffer + 1U) % SOUND_STREAM_BUFFER_COUNT; if(terminal) { @@ -3576,6 +3586,43 @@ int sound_queue_channel_pcm_stream( return result; } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Read live-producer queue state only while the supplied play ID still +* owns the channel. The SDL device lock makes the snapshot coherent with +* both producer publication and real-time mixer consumption. +*/ +bool sound_get_channel_pcm_stream_status_owned( + int channel, + int play_id, + s_sound_pcm_stream_status *status +) { + channelstruct *record; + + if(play_id < 0 || !status) { + return false; + } + + SB_lock_audio(); + record = sound_channel_pool_get(&sound_channel_pool, channel); + if(!record || + !sound_channel_pool_is_active(&sound_channel_pool, channel) || + record->playid != play_id || + record->stream_source != SOUND_CHANNEL_STREAM_SOURCE_PUSH) { + SB_unlock_audio(); + return false; + } + + status->underrun_count = record->stream.underrun_count; + status->ready_buffer_count = record->stream.ready_buffer_count; + status->producer_finished = record->stream.producer_finished; + status->underrun_active = record->stream.underrun_active; + SB_unlock_audio(); + return true; +} + /* * Caskey, Damon V. * 2026-08-05 diff --git a/engine/source/gamelib/soundmix.h b/engine/source/gamelib/soundmix.h index 6ace9aeb3..6ed6d86da 100644 --- a/engine/source/gamelib/soundmix.h +++ b/engine/source/gamelib/soundmix.h @@ -111,6 +111,21 @@ typedef struct bool stream; } s_soundcache; +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Snapshot live-producer buffering without exposing mutable mixer +* internals. WebM uses this state to complete preroll before enabling +* output and to detect a genuine PCM underrun after playback starts. +*/ +typedef struct s_sound_pcm_stream_status { + uint64_t underrun_count; + unsigned int ready_buffer_count; + int producer_finished; + int underrun_active; +} s_sound_pcm_stream_status; + typedef struct s_audio_global { List samplelist; @@ -173,6 +188,11 @@ int sound_getpos_sample(int channel); int sound_open_music(char *filename, char *packname, int volume, int loop, u32 music_offset); int sound_open_channel_pcm_stream(int channel, int frequency, int channels, int volume); int sound_queue_channel_pcm_stream(int channel, int play_id, const void *pcm, uint64_t frame_count, int terminal); +bool sound_get_channel_pcm_stream_status_owned( + int channel, + int play_id, + s_sound_pcm_stream_status *status +); void sound_close_channel_pcm_stream(int channel, int play_id); void sound_close_music(); void sound_update_music(); diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index daa66e591..701f6220b 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -1129,6 +1129,11 @@ void movie_playback_update(int interrupt_requested) continue; } + webm_set_audio_paused( + playback->context, + playback->paused || playback->speed <= 0.0 + ); + position = movie_playback_position_now(playback, now); playback->position = position / UINT64_C(1000000); if(playback->duration && playback->position > playback->duration) { diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 37a83aa3e..5fa1d1653 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -29,10 +29,22 @@ #include "globals.h" #include "borendian.h" #include "soundmix.h" +#include "timer.h" -// lowering these might save a bit of memory but could also cause lag -#define PACKET_QUEUE_SIZE 20 +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Keep enough compressed packets ahead of both decoders that a full +* video queue does not repeatedly stop the shared demuxer before it can +* deliver the next audio packets. PCM output remains separately bounded +* by the generic sound stream ring. +*/ +#define AUDIO_PACKET_QUEUE_SIZE 64 +#define VIDEO_PACKET_QUEUE_SIZE 64 #define FRAME_QUEUE_SIZE 10 +#define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) +#define AUDIO_PREROLL_TIMEOUT_MICROSECONDS UINT64_C(2000000) #define debug_printf(...) //printf(__VA_ARGS__) @@ -63,7 +75,10 @@ typedef struct { int channel; int stream_play_id; int output_active; + int output_paused; + int prerolling; int aligning; + uint64_t observed_underrun_count; uint64_t output_timestamp; uint64_t leading_silence_frames; volatile int *quit; @@ -577,6 +592,50 @@ static int audio_thread(void *data) return 0; } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Let the decoder establish its PCM reserve before the owning movie clock +* starts. The bounded wait preserves failure tolerance for malformed or +* unusually slow inputs while ordinary playback begins with continuous +* audio already available to the mixer. +*/ +static void audio_wait_for_preroll(audio_context *audio_ctx) +{ + uint64_t start_time; + + if(!audio_ctx || !audio_ctx->quit) { + return; + } + start_time = timer_uticks(); + + while(!*audio_ctx->quit) { + s_sound_pcm_stream_status status; + uint64_t now; + + if(!sound_get_channel_pcm_stream_status_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + &status + )) { + return; + } + audio_ctx->observed_underrun_count = status.underrun_count; + if(status.ready_buffer_count >= AUDIO_PREROLL_BUFFER_COUNT || + (status.producer_finished && status.ready_buffer_count > 0)) { + return; + } + + now = timer_uticks(); + if(now < start_time || + now - start_time >= AUDIO_PREROLL_TIMEOUT_MICROSECONDS) { + return; + } + usleep(1000); + } +} + /* * Caskey, Damon V. * 2026-08-12 @@ -634,7 +693,7 @@ static int init_audio( ? seek_timestamp + audioParams.codec_delay : UINT64_MAX; audio_ctx->quit = quit; - audio_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE, quit); + audio_ctx->packet_queue = queue_init(AUDIO_PACKET_QUEUE_SIZE, quit); if(!audio_ctx->packet_queue) { vorbis_destroy(&(audio_ctx->vorbis_ctx)); return -1; @@ -697,6 +756,19 @@ static int init_audio( vorbis_destroy(&(audio_ctx->vorbis_ctx)); return -1; } + audio_ctx->output_paused = 1; + audio_ctx->prerolling = 1; + { + s_sound_pcm_stream_status status; + + if(sound_get_channel_pcm_stream_status_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + &status + )) { + audio_ctx->observed_underrun_count = status.underrun_count; + } + } return 0; } @@ -887,7 +959,7 @@ static int init_video( video_params.display_width, video_params.display_height); } video_ctx->quit = quit; - video_ctx->packet_queue = queue_init(PACKET_QUEUE_SIZE, quit); + video_ctx->packet_queue = queue_init(VIDEO_PACKET_QUEUE_SIZE, quit); video_ctx->frame_queue = queue_init(FRAME_QUEUE_SIZE, quit); if(!video_ctx->packet_queue || !video_ctx->frame_queue) { printf("Error: Unable to allocate WebM video queues\n"); @@ -1134,6 +1206,9 @@ webm_context *webm_start_playback_ex( webm_close(ctx); return NULL; } + if(ctx->audio_track >= 0) { + audio_wait_for_preroll(&ctx->audio_ctx); + } return ctx; error_nestegg: @@ -1193,19 +1268,77 @@ uint64_t webm_get_duration(webm_context *ctx) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-15 * -* Apply pause and speed changes only while this decoder's -* play ID still owns its selected generic sound channel. +* Apply requested pause state while retaining an internal preroll gate. +* New and starved streams remain paused until most PCM buffers are ready, +* preventing the mixer from alternating abruptly between decoded samples +* and absolute silence. Short terminal streams may start with fewer buffers. */ void webm_set_audio_paused(webm_context *ctx, int paused) { - if(ctx && ctx->audio_track >= 0) { - sound_pause_channel_owned( - ctx->audio_ctx.channel, - ctx->audio_ctx.stream_play_id, - paused + audio_context *audio_ctx; + s_sound_pcm_stream_status status; + int preroll_ready; + + if(!ctx || ctx->audio_track < 0) { + return; + } + audio_ctx = &ctx->audio_ctx; + + if(paused) { + audio_ctx->prerolling = 1; + if(!audio_ctx->output_paused && + sound_pause_channel_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + 1 + )) { + audio_ctx->output_paused = 1; + } + return; + } + + if(!sound_get_channel_pcm_stream_status_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + &status + )) { + return; + } + + if(status.underrun_count != audio_ctx->observed_underrun_count) { + audio_ctx->observed_underrun_count = status.underrun_count; + printf( + "Warning: WebM audio underrun on sound channel %d; rebuilding PCM reserve.\n", + audio_ctx->channel ); + audio_ctx->prerolling = 1; + if(!audio_ctx->output_paused && + sound_pause_channel_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + 1 + )) { + audio_ctx->output_paused = 1; + } + } + + preroll_ready = + status.ready_buffer_count >= AUDIO_PREROLL_BUFFER_COUNT || + (status.producer_finished && status.ready_buffer_count > 0); + if(audio_ctx->prerolling && !preroll_ready) { + return; + } + + audio_ctx->prerolling = 0; + if(audio_ctx->output_paused && + sound_pause_channel_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + 0 + )) { + audio_ctx->output_paused = 0; } } diff --git a/engine/version.tmp b/engine/version.tmp index 8381486f7..766736b80 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7970" -#define VERSION_BUILD_INT 7970 -#define VERSION_COMMIT "5901cf0" +#define VERSION_BUILD "7972" +#define VERSION_BUILD_INT 7972 +#define VERSION_COMMIT "e200279" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From d73fb4a029c0ab44040600a9b9b42984931478ef Mon Sep 17 00:00:00 2001 From: DCurrent Date: Sat, 15 Aug 2026 18:13:54 -0400 Subject: [PATCH 13/26] Optimize WebM audio buffering and scaled movie composition - increase streaming audio reserves and improve decoder scheduling - retain underrun telemetry with playback-aware producer backoff - convert YUV directly to requested composition dimensions - allocate native-resolution RGB buffers only when required - add MOVIE_SIZE_NATIVE width and height sentinel - preserve destination-sized rendering when dimensions are zero - correct reported WebM audio sample depth --- engine/source/gamelib/sound_stream.h | 2 +- engine/source/gamelib/soundmix.c | 90 +++++++++++- engine/source/gamelib/soundmix.h | 12 +- engine/source/openborscript/constants.c | 1 + engine/source/openborscript/movie.c | 12 +- engine/source/webmlib/movie_playback.c | 175 ++++++++++++++++-------- engine/source/webmlib/movie_playback.h | 10 +- engine/source/webmlib/vidplay.c | 127 ++++++++--------- engine/source/webmlib/yuv.c | 94 +++++++++++++ engine/source/webmlib/yuv.h | 5 + engine/version.tmp | 6 +- 11 files changed, 390 insertions(+), 144 deletions(-) diff --git a/engine/source/gamelib/sound_stream.h b/engine/source/gamelib/sound_stream.h index 0d0d87b2e..a9aff6e40 100644 --- a/engine/source/gamelib/sound_stream.h +++ b/engine/source/gamelib/sound_stream.h @@ -15,7 +15,7 @@ /* * Caskey, Damon V. -* 2026-08-01 +* 2026-08-15 * * Streamed channels retain SOUND_STREAM_BUFFER_COUNT PCM buffers. * Eight buffers absorb source I/O, decoder, and demux scheduling diff --git a/engine/source/gamelib/soundmix.c b/engine/source/gamelib/soundmix.c index f60b913bd..e14b94a1c 100644 --- a/engine/source/gamelib/soundmix.c +++ b/engine/source/gamelib/soundmix.c @@ -3546,11 +3546,75 @@ int sound_open_channel_pcm_stream( /* * Caskey, Damon V. -* 2026-08-05 +* 2026-08-15 +* +* Estimate when a full producer ring will release its next slot. The +* decoder wakes shortly before that boundary, with a 50 millisecond cap +* so pause, stop, and ownership changes remain responsive. +*/ +static unsigned int sound_stream_producer_retry_delay_microseconds( + const channelstruct *record +) { + const s_sound_stream *stream; + const s_sound_stream_buffer *read_buffer; + const uint64_t delay_maximum = UINT64_C(50000); + const uint64_t delay_minimum = UINT64_C(1000); + const uint64_t delay_margin = UINT64_C(1000); + sound_sample_fixed_t buffer_length; + sound_sample_fixed_t remaining; + uint64_t output_frames; + uint64_t maximum_delay_frames; + uint64_t delay; + + if(!record || record->fp_period == 0 || playfrequency <= 0) { + return (unsigned int)delay_minimum; + } + + stream = &record->stream; + read_buffer = &stream->buffer[stream->read_buffer]; + if(!read_buffer->ready) { + return (unsigned int)delay_minimum; + } + + buffer_length = SOUND_SAMPLE_INT_TO_FIX(read_buffer->frame_count); + if(stream->fp_buffer_position >= buffer_length) { + return (unsigned int)delay_minimum; + } + + remaining = buffer_length - stream->fp_buffer_position; + output_frames = remaining / record->fp_period; + if(remaining % record->fp_period) { + output_frames++; + } + + maximum_delay_frames = + ((uint64_t)playfrequency * delay_maximum + UINT64_C(999999)) / + UINT64_C(1000000); + if(output_frames >= maximum_delay_frames) { + return (unsigned int)delay_maximum; + } + + delay = output_frames * UINT64_C(1000000) / + (uint64_t)playfrequency; + delay = delay > delay_margin + ? delay - delay_margin + : delay_minimum; + if(delay < delay_minimum) { + delay = delay_minimum; + } + return (unsigned int)delay; +} + +/* +* Caskey, Damon V. +* 2026-08-15 * -* Copy one live PCM block into a generic channel's -* rotating queue. Direct SDL locking is used because -* producers such as WebM publish from decoder threads. +* Copy one live PCM block into a generic channel's rotating queue. +* Direct SDL locking is used because producers such as WebM publish +* from decoder threads. Optional telemetry is captured while the same +* lock is held so producers do not need a second status query. Full +* queues return a bounded retry delay instead of encouraging millisecond +* polling against the SDL device lock. * * Returns 1 when queued, 0 while all buffers are ready, * and -1 after replacement or malformed input. @@ -3560,11 +3624,20 @@ int sound_queue_channel_pcm_stream( int play_id, const void *pcm, uint64_t frame_count, - int terminal + int terminal, + uint64_t *underrun_count, + unsigned int *retry_delay_microseconds ) { channelstruct *record; int result; + if(underrun_count) { + *underrun_count = 0; + } + if(retry_delay_microseconds) { + *retry_delay_microseconds = 1000U; + } + SB_lock_audio_direct(); record = sound_channel_pool_get(&sound_channel_pool, channel); if(play_id < 0 || @@ -3582,6 +3655,13 @@ int sound_queue_channel_pcm_stream( frame_count, terminal ); + if(underrun_count) { + *underrun_count = record->stream.underrun_count; + } + if(result == 0 && retry_delay_microseconds) { + *retry_delay_microseconds = + sound_stream_producer_retry_delay_microseconds(record); + } SB_unlock_audio_direct(); return result; } diff --git a/engine/source/gamelib/soundmix.h b/engine/source/gamelib/soundmix.h index 6ed6d86da..0b8be9810 100644 --- a/engine/source/gamelib/soundmix.h +++ b/engine/source/gamelib/soundmix.h @@ -117,7 +117,7 @@ typedef struct * * Snapshot live-producer buffering without exposing mutable mixer * internals. WebM uses this state to complete preroll before enabling -* output and to detect a genuine PCM underrun after playback starts. +* output. Ongoing underrun telemetry rides the normal publication call. */ typedef struct s_sound_pcm_stream_status { uint64_t underrun_count; @@ -187,7 +187,15 @@ bool sound_set_channel_volume_divisor(int channel, int volume_divisor); int sound_getpos_sample(int channel); int sound_open_music(char *filename, char *packname, int volume, int loop, u32 music_offset); int sound_open_channel_pcm_stream(int channel, int frequency, int channels, int volume); -int sound_queue_channel_pcm_stream(int channel, int play_id, const void *pcm, uint64_t frame_count, int terminal); +int sound_queue_channel_pcm_stream( + int channel, + int play_id, + const void *pcm, + uint64_t frame_count, + int terminal, + uint64_t *underrun_count, + unsigned int *retry_delay_microseconds +); bool sound_get_channel_pcm_stream_status_owned( int channel, int play_id, diff --git a/engine/source/openborscript/constants.c b/engine/source/openborscript/constants.c index e3f01f816..db610f02c 100644 --- a/engine/source/openborscript/constants.c +++ b/engine/source/openborscript/constants.c @@ -1162,6 +1162,7 @@ bool mapstrings_transconst(ScriptVariant **varlist, int paramCount) #ifdef WEBM ICMPCONST(MOVIE_CHANNEL_COUNT) ICMPCONST(MOVIE_CHANNEL_AUTO) + ICMPCONSTU64(MOVIE_SIZE_NATIVE) ICMPCONST(MOVIE_LOADING_STREAM) ICMPCONST(MOVIE_LOADING_CACHE) ICMPCONST(MOVIE_LOADING_AUTO) diff --git a/engine/source/openborscript/movie.c b/engine/source/openborscript/movie.c index d16084f1d..dc4140bd8 100644 --- a/engine/source/openborscript/movie.c +++ b/engine/source/openborscript/movie.c @@ -37,7 +37,7 @@ static const s_movie_property_info movie_properties[] = { "MOVIE_PROPERTY_DURATION", VT_UINTEGER64 }, { MOVIE_PROPERTY_HEIGHT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, PROPERTY_MEMBER_OFFSET(s_movie_playback, height), - "MOVIE_PROPERTY_HEIGHT", VT_INTEGER }, + "MOVIE_PROPERTY_HEIGHT", VT_UINTEGER64 }, { MOVIE_PROPERTY_INTERRUPT, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, PROPERTY_MEMBER_OFFSET(s_movie_playback, interrupt), "MOVIE_PROPERTY_INTERRUPT", VT_INTEGER }, @@ -67,7 +67,7 @@ static const s_movie_property_info movie_properties[] = { "MOVIE_PROPERTY_SPEED", VT_DECIMAL }, { MOVIE_PROPERTY_WIDTH, PROPERTY_ACCESS_CONFIG_MACRO_DEFAULT, PROPERTY_MEMBER_OFFSET(s_movie_playback, width), - "MOVIE_PROPERTY_WIDTH", VT_INTEGER }, + "MOVIE_PROPERTY_WIDTH", VT_UINTEGER64 }, { MOVIE_PROPERTY_END, PROPERTY_ACCESS_CONFIG_NONE, 0, "Movie", VT_EMPTY } }; @@ -480,8 +480,8 @@ HRESULT openbor_movie_set_property( movie_playback_set_black_filter(playback, integer_value != 0); break; case MOVIE_PROPERTY_HEIGHT: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - movie_playback_set_height(playback, (int)integer_value); + result = SUCCEEDED(ScriptVariant_Unsigned64Value(varlist[2], &unsigned_value)) && + movie_playback_set_height(playback, unsigned_value); break; case MOVIE_PROPERTY_INTERRUPT: result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && @@ -512,8 +512,8 @@ HRESULT openbor_movie_set_property( movie_playback_set_speed(playback, decimal_value); break; case MOVIE_PROPERTY_WIDTH: - result = SUCCEEDED(ScriptVariant_IntegerValue(varlist[2], &integer_value)) && - movie_playback_set_width(playback, (int)integer_value); + result = SUCCEEDED(ScriptVariant_Unsigned64Value(varlist[2], &unsigned_value)) && + movie_playback_set_width(playback, unsigned_value); break; case MOVIE_PROPERTY_ACTIVE: case MOVIE_PROPERTY_CHANNEL: diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 701f6220b..cef88edac 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -147,6 +147,44 @@ static s_screen *movie_playback_alloc_screen(int width, int height) return screen; } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Resolve each creator-selected composition dimension. Zero follows the +* destination screen, MOVIE_SIZE_NATIVE follows the decoded video, and an +* explicit positive value requests that exact processing size. +*/ +static bool movie_playback_resolve_dimensions( + const s_movie_playback *playback, + const s_screen *destination, + int *width, + int *height +) +{ + uint64_t resolved_width = playback->width; + uint64_t resolved_height = playback->height; + + if(resolved_width == 0) { + resolved_width = (uint64_t)destination->width; + } else if(resolved_width == MOVIE_SIZE_NATIVE) { + resolved_width = (uint64_t)playback->current_frame->width; + } + if(resolved_height == 0) { + resolved_height = (uint64_t)destination->height; + } else if(resolved_height == MOVIE_SIZE_NATIVE) { + resolved_height = (uint64_t)playback->current_frame->height; + } + + if(resolved_width > (uint64_t)INT_MAX || + resolved_height > (uint64_t)INT_MAX) { + return false; + } + *width = (int)resolved_width; + *height = (int)resolved_height; + return movie_playback_dimensions_valid(*width, *height); +} + static char *movie_copy_path(const char *path) { char *copy; @@ -732,21 +770,6 @@ static bool movie_playback_open_media( if(playback->rgb_frame) freescreen(&playback->rgb_frame); if(playback->scaled_frame) freescreen(&playback->scaled_frame); } - if(!playback->rgb_frame) { - playback->rgb_frame = movie_playback_alloc_screen( - playback->video_mode.width, - playback->video_mode.height - ); - } - if(!playback->rgb_frame) { - printf( - "Error: Unable to allocate %d*%d movie conversion screen.\n", - playback->video_mode.width, - playback->video_mode.height - ); - movie_playback_close_decoder(playback); - return false; - } playback->position_anchor = position; playback->clock_anchor = timer_uticks(); @@ -853,10 +876,42 @@ void movie_playback_stop_all(void) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-15 * -* Convert and scale a current frame only when its pixels or -* destination dimensions change, then reuse it for composition. +* Protect absolute black after conversion when a movie frame will be drawn +* into a subscreen that reserves RGB 0,0,0 as its transparency mask. +*/ +static void movie_playback_filter_black( + const s_movie_playback *playback, + s_screen *frame +) +{ + size_t pixel_count; + size_t pixel_index; + uint32_t *pixel; + uint32_t protected_black; + + if(!playback->black_filter) { + return; + } + + pixel_count = (size_t)frame->width * (size_t)frame->height; + pixel = (uint32_t *)frame->data; + protected_black = colour32(1, 1, 1); + for(pixel_index = 0; pixel_index < pixel_count; pixel_index++) { + if(pixel[pixel_index] == 0) { + pixel[pixel_index] = protected_black; + } + } +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Convert a current frame only when its pixels or destination dimensions +* change. Resized output converts directly from YUV at the requested size, +* avoiding a full-resolution RGB conversion and subsequent scaling pass. */ static bool movie_playback_prepare_frame( s_movie_playback *playback, @@ -864,43 +919,40 @@ static bool movie_playback_prepare_frame( s_screen **render_frame ) { - int refresh = playback->frame_dirty; - int width = playback->width ? playback->width : destination->width; - int height = playback->height ? playback->height : destination->height; + int height; + int width; - if(!playback->current_frame || !playback->rgb_frame) { + if(!playback->current_frame) { return false; } - if(width < 1 || height < 1 || - !movie_playback_dimensions_valid(width, height)) { + if(!movie_playback_resolve_dimensions( + playback, + destination, + &width, + &height + )) { return false; } - if(refresh) { - size_t pixel_count; - size_t pixel_index; - uint32_t *pixel; - uint32_t protected_black; - - yuv_to_rgb(playback->current_frame, playback->rgb_frame); + if(playback->frame_dirty) { + playback->rgb_frame_dirty = 1; playback->scaled_frame_dirty = 1; - if(playback->black_filter) { - pixel_count = - (size_t)playback->rgb_frame->width * - (size_t)playback->rgb_frame->height; - pixel = (uint32_t*)playback->rgb_frame->data; - protected_black = colour32(1, 1, 1); - for(pixel_index = 0; - pixel_index < pixel_count; - pixel_index++) { - if(pixel[pixel_index] == 0) { - pixel[pixel_index] = protected_black; - } - } - } } - if(width == playback->rgb_frame->width && - height == playback->rgb_frame->height) { + + if(width == playback->current_frame->width && + height == playback->current_frame->height) { + if(!playback->rgb_frame) { + playback->rgb_frame = movie_playback_alloc_screen(width, height); + playback->rgb_frame_dirty = 1; + } + if(!playback->rgb_frame) { + return false; + } + if(playback->rgb_frame_dirty) { + yuv_to_rgb(playback->current_frame, playback->rgb_frame); + movie_playback_filter_black(playback, playback->rgb_frame); + playback->rgb_frame_dirty = 0; + } playback->frame_dirty = 0; *render_frame = playback->rgb_frame; return true; @@ -913,17 +965,23 @@ static bool movie_playback_prepare_frame( } if(!playback->scaled_frame) { playback->scaled_frame = movie_playback_alloc_screen(width, height); - refresh = 1; + playback->scaled_frame_dirty = 1; } if(!playback->scaled_frame) { return false; } - if(refresh || playback->scaled_frame_dirty) { - scalescreen32(playback->scaled_frame, playback->rgb_frame); + if(playback->scaled_frame_dirty) { + if(!yuv_to_rgb_scaled( + playback->current_frame, + playback->scaled_frame + )) { + return false; + } + movie_playback_filter_black(playback, playback->scaled_frame); + playback->scaled_frame_dirty = 0; } playback->frame_dirty = 0; - playback->scaled_frame_dirty = 0; *render_frame = playback->scaled_frame; return true; } @@ -1129,11 +1187,6 @@ void movie_playback_update(int interrupt_requested) continue; } - webm_set_audio_paused( - playback->context, - playback->paused || playback->speed <= 0.0 - ); - position = movie_playback_position_now(playback, now); playback->position = position / UINT64_C(1000000); if(playback->duration && playback->position > playback->duration) { @@ -1233,9 +1286,10 @@ bool movie_playback_draw_to_screen(s_screen *screen, int channel) * Property mutation helpers preserve decoder, timing, audio, * scaling, signed-rate, and external screen invariants. */ -bool movie_playback_set_height(s_movie_playback *playback, int height) +bool movie_playback_set_height(s_movie_playback *playback, uint64_t height) { - if(movie_playback_get_index(playback) < 0 || height < 0) { + if(movie_playback_get_index(playback) < 0 || + (height != MOVIE_SIZE_NATIVE && height > (uint64_t)INT_MAX)) { return false; } playback->height = height; @@ -1420,9 +1474,10 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) return true; } -bool movie_playback_set_width(s_movie_playback *playback, int width) +bool movie_playback_set_width(s_movie_playback *playback, uint64_t width) { - if(movie_playback_get_index(playback) < 0 || width < 0) { + if(movie_playback_get_index(playback) < 0 || + (width != MOVIE_SIZE_NATIVE && width > (uint64_t)INT_MAX)) { return false; } playback->width = width; diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index a8bc36345..5589c90ce 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -19,6 +19,7 @@ #define MOVIE_CHANNEL_AUTO (-1) #define MOVIE_SPEED_MIN (-16.0) #define MOVIE_SPEED_MAX 16.0 +#define MOVIE_SIZE_NATIVE UINT64_MAX typedef enum e_movie_loading_mode { MOVIE_LOADING_STREAM, @@ -46,8 +47,8 @@ typedef struct s_movie_playback { int black_filter; int offset_x; int offset_y; - int width; - int height; + uint64_t width; + uint64_t height; int sound_channel; int source_id; float speed; @@ -65,6 +66,7 @@ typedef struct s_movie_playback { int volume; bool replace_all_audio; int frame_dirty; + int rgb_frame_dirty; int scaled_frame_dirty; int reverse_pending; int terminal_pending; @@ -94,7 +96,7 @@ bool movie_playback_draw_to_screen(s_screen *screen, int channel); void movie_playback_stop(s_movie_playback *playback); void movie_playback_stop_all(void); -bool movie_playback_set_height(s_movie_playback *playback, int height); +bool movie_playback_set_height(s_movie_playback *playback, uint64_t height); bool movie_playback_set_black_filter( s_movie_playback *playback, int black_filter @@ -115,6 +117,6 @@ bool movie_playback_set_sound_channel( int volume ); bool movie_playback_set_speed(s_movie_playback *playback, double speed); -bool movie_playback_set_width(s_movie_playback *playback, int width); +bool movie_playback_set_width(s_movie_playback *playback, uint64_t width); #endif diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 5fa1d1653..73b93a9ad 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -36,12 +37,13 @@ * 2026-08-15 * * Keep enough compressed packets ahead of both decoders that a full -* video queue does not repeatedly stop the shared demuxer before it can -* deliver the next audio packets. PCM output remains separately bounded -* by the generic sound stream ring. +* video queue does not stop the shared demuxer before the audio packet +* reserve fills. Video needs more packet slots because its packet rate +* may greatly exceed the audio rate. Queue allocation stores pointers; +* PCM output remains separately bounded by the generic stream ring. */ #define AUDIO_PACKET_QUEUE_SIZE 64 -#define VIDEO_PACKET_QUEUE_SIZE 64 +#define VIDEO_PACKET_QUEUE_SIZE 512 #define FRAME_QUEUE_SIZE 10 #define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) #define AUDIO_PREROLL_TIMEOUT_MICROSECONDS UINT64_C(2000000) @@ -76,9 +78,9 @@ typedef struct { int stream_play_id; int output_active; int output_paused; - int prerolling; int aligning; uint64_t observed_underrun_count; + uint64_t reported_underrun_count; uint64_t output_timestamp; uint64_t leading_silence_frames; volatile int *quit; @@ -526,6 +528,14 @@ static int audio_decode_frame( return samples_written * vorbis_ctx->channels * (int)sizeof(int16_t); } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Decode and publish WebM audio from an elevated-priority producer. +* High-resolution VP8 decoding and software composition must not prevent +* this thread from replenishing the real-time mixer's PCM reserve. +*/ static int audio_thread(void *data) { audio_context *audio_ctx = (audio_context *)data; @@ -533,6 +543,11 @@ static int audio_thread(void *data) int frame_count; int queue_result; int terminal; + unsigned int retry_delay_microseconds; + uint64_t underrun_count; + + /* Priority elevation may be denied on restricted platforms. */ + SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); while(!*audio_ctx->quit) { @@ -567,13 +582,32 @@ static int audio_thread(void *data) audio_ctx->stream_play_id, frame_count > 0 ? audio_ctx->pcm_buffer : NULL, (uint64_t)frame_count, - terminal + terminal, + &underrun_count, + &retry_delay_microseconds ); if(queue_result == 0) { - usleep(1000); + usleep(retry_delay_microseconds); } } while(!*audio_ctx->quit && queue_result == 0); + if(queue_result > 0 && + underrun_count != audio_ctx->observed_underrun_count) { + audio_ctx->observed_underrun_count = underrun_count; + if(!audio_ctx->reported_underrun_count || + (underrun_count > audio_ctx->reported_underrun_count && + underrun_count - audio_ctx->reported_underrun_count >= + UINT64_C(16))) { + audio_ctx->reported_underrun_count = underrun_count; + printf( + "Warning: WebM audio underrun on sound channel %d " + "(%" PRIu64 " total).\n", + audio_ctx->channel, + underrun_count + ); + } + } + if(queue_result < 0) { /* * Keep draining this track when another producer replaces @@ -699,7 +733,7 @@ static int init_audio( return -1; } printf("Audio track: %f Hz, %d channels, %d bits/sample\n", - audioParams.rate, audioParams.channels, audioParams.depth / audioParams.channels); + audioParams.rate, audioParams.channels, audioParams.depth); if(audio_ctx->frequency < SOUND_MUSIC_FREQUENCY_MIN || audio_ctx->frequency > SOUND_MUSIC_FREQUENCY_MAX) { @@ -757,7 +791,6 @@ static int init_audio( return -1; } audio_ctx->output_paused = 1; - audio_ctx->prerolling = 1; { s_sound_pcm_stream_status status; @@ -998,11 +1031,25 @@ static void close_video(video_context *video_ctx) } } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Route container packets to their decoder queues. WebM playback with +* audio elevates this shared producer so video work cannot prevent it +* from restoring the compressed audio reserve. +*/ static int demux_thread(void *data) { webm_context *ctx = (webm_context *)data; nestegg_packet *pkt; int r; + + if(ctx->audio_track >= 0) { + /* Priority elevation may be denied on restricted platforms. */ + SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); + } + while ((r = nestegg_read_packet(ctx->nestegg_ctx, &pkt)) > 0) { unsigned int track; @@ -1270,75 +1317,29 @@ uint64_t webm_get_duration(webm_context *ctx) * Caskey, Damon V. * 2026-08-15 * -* Apply requested pause state while retaining an internal preroll gate. -* New and starved streams remain paused until most PCM buffers are ready, -* preventing the mixer from alternating abruptly between decoded samples -* and absolute silence. Short terminal streams may start with fewer buffers. +* Apply pause state only when it actually changes. Initial PCM preroll is +* completed synchronously before the movie clock begins, so ordinary frame +* updates never need to poll stream status or lock the SDL audio device. */ void webm_set_audio_paused(webm_context *ctx, int paused) { audio_context *audio_ctx; - s_sound_pcm_stream_status status; - int preroll_ready; if(!ctx || ctx->audio_track < 0) { return; } audio_ctx = &ctx->audio_ctx; - - if(paused) { - audio_ctx->prerolling = 1; - if(!audio_ctx->output_paused && - sound_pause_channel_owned( - audio_ctx->channel, - audio_ctx->stream_play_id, - 1 - )) { - audio_ctx->output_paused = 1; - } + paused = paused != 0; + if(audio_ctx->output_paused == paused) { return; } - if(!sound_get_channel_pcm_stream_status_owned( + if(sound_pause_channel_owned( audio_ctx->channel, audio_ctx->stream_play_id, - &status + paused )) { - return; - } - - if(status.underrun_count != audio_ctx->observed_underrun_count) { - audio_ctx->observed_underrun_count = status.underrun_count; - printf( - "Warning: WebM audio underrun on sound channel %d; rebuilding PCM reserve.\n", - audio_ctx->channel - ); - audio_ctx->prerolling = 1; - if(!audio_ctx->output_paused && - sound_pause_channel_owned( - audio_ctx->channel, - audio_ctx->stream_play_id, - 1 - )) { - audio_ctx->output_paused = 1; - } - } - - preroll_ready = - status.ready_buffer_count >= AUDIO_PREROLL_BUFFER_COUNT || - (status.producer_finished && status.ready_buffer_count > 0); - if(audio_ctx->prerolling && !preroll_ready) { - return; - } - - audio_ctx->prerolling = 0; - if(audio_ctx->output_paused && - sound_pause_channel_owned( - audio_ctx->channel, - audio_ctx->stream_play_id, - 0 - )) { - audio_ctx->output_paused = 0; + audio_ctx->output_paused = paused; } } diff --git a/engine/source/webmlib/yuv.c b/engine/source/webmlib/yuv.c index 7048f5e47..037e70909 100644 --- a/engine/source/webmlib/yuv.c +++ b/engine/source/webmlib/yuv.c @@ -149,6 +149,8 @@ yuv_frame *yuv_frame_create(int width, int height) yuv_frame_destroy(frame); return NULL; } + frame->width = width; + frame->height = height; return frame; } @@ -422,3 +424,95 @@ void yuv_to_rgb(yuv_frame *in, s_screen *out) convert(in->lum, in->cr, in->cb, out->data, out->height, out->width, 0); #endif } + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Convert YUV 4:2:0 directly into the requested 32-bit output dimensions. +* Nearest-neighbor source selection matches the movie compositing scaler +* without first converting every full-resolution source pixel. +*/ +bool yuv_to_rgb_scaled(const yuv_frame *in, s_screen *out) +{ + const unsigned char *cb; + const unsigned char *cr; + const unsigned char *lum; + uint32_t *destination; + uint64_t x_step; + uint64_t y_position = 0; + uint64_t y_step; + int chroma_width; + int destination_x; + int destination_y; + + if(!yuv_initialized || bytes_per_pixel != 4 || !in || !out || + !in->lum || !in->cb || !in->cr || + in->width < 2 || in->height < 2 || + (in->width & 1) || (in->height & 1) || + out->width < 1 || out->height < 1 || + pixelbytes[out->pixelformat] != 4) { + return false; + } + + lum = (const unsigned char *)in->lum; +#if REVERSE_COLOR + cr = (const unsigned char *)in->cb; + cb = (const unsigned char *)in->cr; +#else + cr = (const unsigned char *)in->cr; + cb = (const unsigned char *)in->cb; +#endif + destination = (uint32_t *)out->data; + chroma_width = in->width / 2; + x_step = ((uint64_t)in->width << 16) / (uint64_t)out->width; + y_step = ((uint64_t)in->height << 16) / (uint64_t)out->height; + + for(destination_y = 0; + destination_y < out->height; + destination_y++) { + const unsigned char *cb_row; + const unsigned char *cr_row; + const unsigned char *lum_row; + uint64_t x_position = 0; + int source_y = (int)(y_position >> 16); + + if(source_y >= in->height) { + source_y = in->height - 1; + } + cb_row = cb + (source_y / 2) * chroma_width; + cr_row = cr + (source_y / 2) * chroma_width; + lum_row = lum + source_y * in->width; + + for(destination_x = 0; + destination_x < out->width; + destination_x++) { + int cb_value; + int cb_b; + int cr_value; + int cr_r; + int crb_g; + int luminance; + int source_x = (int)(x_position >> 16); + + if(source_x >= in->width) { + source_x = in->width - 1; + } + cb_value = cb_row[source_x / 2]; + cr_value = cr_row[source_x / 2]; + cr_r = 256 + colortab[cr_value]; + crb_g = 768 + 256 + + colortab[cr_value + 256] + + colortab[cb_value + 512]; + cb_b = 1536 + 256 + colortab[cb_value + 768]; + luminance = lum_row[source_x]; + *destination++ = + rgb_2_pix[luminance + cr_r] | + rgb_2_pix[luminance + crb_g] | + rgb_2_pix[luminance + cb_b]; + x_position += x_step; + } + y_position += y_step; + } + return true; +} diff --git a/engine/source/webmlib/yuv.h b/engine/source/webmlib/yuv.h index 7a665aca7..e4c71450b 100644 --- a/engine/source/webmlib/yuv.h +++ b/engine/source/webmlib/yuv.h @@ -13,6 +13,8 @@ typedef struct { u64 timestamp; + int width; + int height; void *lum; void *cb; void *cr; @@ -40,4 +42,7 @@ void yuv_clear(void); // Performs YUV->RGB color conversion on a single frame; requires initialized context void yuv_to_rgb(yuv_frame *in, s_screen *out); +// Converts and nearest-neighbor scales a YUV frame directly to a 32-bit screen +bool yuv_to_rgb_scaled(const yuv_frame *in, s_screen *out); + #endif diff --git a/engine/version.tmp b/engine/version.tmp index 766736b80..3b525fe3c 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7972" -#define VERSION_BUILD_INT 7972 -#define VERSION_COMMIT "e200279" +#define VERSION_BUILD "7973" +#define VERSION_BUILD_INT 7973 +#define VERSION_COMMIT "ea4a479" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From b96337dc29dc7f5173e2ad5053a3c55ce4fd1f35 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Sat, 15 Aug 2026 19:01:16 -0400 Subject: [PATCH 14/26] Reduce WebM starvation under high-resolution playback Configure VP8 decoding from source resolution and available processors while reserving capacity for engine and audio work on larger systems. Prevent prolonged active-playback stalls from back-pressuring the shared demuxer by replacing stale decoded frames only after the compressed video queue reaches its safety threshold. Preserve ordered blocking during ordinary read-ahead, startup, and paused playback. Report rate-limited underrun snapshots for PCM, compressed audio and video, decoded frames, and stale-frame replacements. Keep diagnostics off the normal playback path and preserve safe queue lifetime through audio-thread shutdown. Retain direct target-size YUV conversion, eight-buffer PCM reserves, decoder preroll, and playback-aware producer backoff. --- engine/source/webmlib/vidplay.c | 261 ++++++++++++++++++++++++++++++-- engine/version.tmp | 6 +- 2 files changed, 249 insertions(+), 18 deletions(-) diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 73b93a9ad..c3f8d7556 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -45,6 +45,9 @@ #define AUDIO_PACKET_QUEUE_SIZE 64 #define VIDEO_PACKET_QUEUE_SIZE 512 #define FRAME_QUEUE_SIZE 10 +#define VIDEO_DECODER_THREAD_MAX 8U +#define VIDEO_PACKET_QUEUE_BACKPRESSURE_THRESHOLD \ + ((VIDEO_PACKET_QUEUE_SIZE * 3) / 4) #define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) #define AUDIO_PREROLL_TIMEOUT_MICROSECONDS UINT64_C(2000000) @@ -61,6 +64,7 @@ typedef struct { int start; int size; int max_size; + uint64_t replacement_count; volatile int *quit; bor_mutex *mutex; bor_cond *not_full; @@ -70,6 +74,8 @@ typedef struct { typedef struct { FixedSizeQueue *packet_queue; + FixedSizeQueue *video_packet_queue; + FixedSizeQueue *video_frame_queue; vorbis_context vorbis_ctx; int frequency; int avail_samples; @@ -95,6 +101,7 @@ typedef struct { int height; int display_width; int display_height; + int playback_paused; uint64_t frame_delay; volatile int *quit; } video_context; @@ -249,6 +256,7 @@ static FixedSizeQueue *queue_init(int max_size, volatile int *quit) queue->start = 0; queue->size = 0; queue->max_size = max_size; + queue->replacement_count = 0; queue->quit = quit; queue->mutex = mutex_create(); queue->not_full = cond_create(); @@ -348,6 +356,105 @@ static int queue_try_get(FixedSizeQueue *queue, void **data) return 1; } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Capture queue occupancy and replacement totals only when an underrun is +* reported. Normal decoding and publication incur no diagnostic polling. +*/ +static int queue_get_status( + FixedSizeQueue *queue, + uint64_t *replacement_count +) +{ + int size; + + if(replacement_count) { + *replacement_count = 0; + } + if(!queue) { + return -1; + } + + mutex_lock(queue->mutex); + size = queue->size; + if(replacement_count) { + *replacement_count = queue->replacement_count; + } + mutex_unlock(queue->mutex); + return size; +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Publish decoded frames in timestamp order during ordinary read-ahead. +* When active playback stalls long enough to fill most of the compressed +* video reserve, replace the oldest decoded frame instead of allowing video +* backpressure to stop the shared demuxer and starve audio. Paused playback +* retains every queued frame and continues using bounded blocking behavior. +*/ +static int video_frame_queue_insert( + video_context *video_ctx, + void *data, + void **replaced +) +{ + FixedSizeQueue *frame_queue; + + if(!video_ctx || !video_ctx->frame_queue || + !video_ctx->packet_queue || !replaced) { + return -1; + } + frame_queue = video_ctx->frame_queue; + *replaced = NULL; + + while(!*frame_queue->quit) { + int index; + int video_packet_count = queue_get_status( + video_ctx->packet_queue, + NULL + ); + + mutex_lock(frame_queue->mutex); + if(*frame_queue->quit) { + mutex_unlock(frame_queue->mutex); + return -1; + } + if(frame_queue->size == frame_queue->max_size && + !video_ctx->playback_paused && + video_packet_count >= + VIDEO_PACKET_QUEUE_BACKPRESSURE_THRESHOLD) { + *replaced = frame_queue->data[frame_queue->start]; + frame_queue->start = + (frame_queue->start + 1) % frame_queue->max_size; + --frame_queue->size; + if(frame_queue->replacement_count < UINT64_MAX) { + frame_queue->replacement_count++; + } + } + if(frame_queue->size < frame_queue->max_size) { + index = (frame_queue->start + frame_queue->size) % + frame_queue->max_size; + frame_queue->data[index] = data; + ++frame_queue->size; + cond_signal(frame_queue->not_empty); + mutex_unlock(frame_queue->mutex); + return 0; + } + + cond_wait_timed( + frame_queue->not_full, + frame_queue->mutex, + 10 + ); + mutex_unlock(frame_queue->mutex); + } + return -1; +} + void queue_destroy(FixedSizeQueue *queue) { cond_destroy(queue->not_full); @@ -598,12 +705,46 @@ static int audio_thread(void *data) (underrun_count > audio_ctx->reported_underrun_count && underrun_count - audio_ctx->reported_underrun_count >= UINT64_C(16))) { + s_sound_pcm_stream_status status = { 0 }; + uint64_t dropped_frames; + int audio_packets; + int decoded_frames; + int video_packets; + + sound_get_channel_pcm_stream_status_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + &status + ); + audio_packets = queue_get_status( + audio_ctx->packet_queue, + NULL + ); + video_packets = queue_get_status( + audio_ctx->video_packet_queue, + NULL + ); + decoded_frames = queue_get_status( + audio_ctx->video_frame_queue, + &dropped_frames + ); audio_ctx->reported_underrun_count = underrun_count; printf( "Warning: WebM audio underrun on sound channel %d " - "(%" PRIu64 " total).\n", + "(%" PRIu64 " total): PCM=%u/%u, " + "packets=%d/%d audio and %d/%d video, " + "frames=%d/%d (%" PRIu64 " stale dropped).\n", audio_ctx->channel, - underrun_count + underrun_count, + status.ready_buffer_count, + SOUND_STREAM_BUFFER_COUNT, + audio_packets, + AUDIO_PACKET_QUEUE_SIZE, + video_packets, + VIDEO_PACKET_QUEUE_SIZE, + decoded_frames, + FRAME_QUEUE_SIZE, + dropped_frames ); } } @@ -887,6 +1028,8 @@ static int video_thread(void *data) } while((img = vpx_codec_get_frame(&ctx->vpx_ctx, &iter))) { + void *replaced_frame = NULL; + if(img->d_w != (unsigned int)ctx->width || img->d_h != (unsigned int)ctx->height) { printf("Error: WebM frame dimensions changed during playback\n"); @@ -910,12 +1053,17 @@ static int video_thread(void *data) memcpy(frame->cb+(y*img->d_w/2), img->planes[2]+(y*img->stride[2]), img->d_w / 2); } - if (queue_insert(ctx->frame_queue, (void *)frame) < 0) + if(video_frame_queue_insert( + ctx, + (void *)frame, + &replaced_frame + ) < 0) { debug_printf("destroying last frame\n"); yuv_frame_destroy(frame); break; } + yuv_frame_destroy((yuv_frame *)replaced_frame); timestamp = timestamp <= UINT64_MAX - frame_delay ? timestamp + frame_delay : UINT64_MAX; @@ -925,18 +1073,70 @@ static int video_thread(void *data) nestegg_free_packet(pkt); } - queue_insert(ctx->frame_queue, NULL); + { + void *replaced_frame = NULL; + + if(video_frame_queue_insert( + ctx, + NULL, + &replaced_frame + ) == 0) { + yuv_frame_destroy((yuv_frame *)replaced_frame); + } + } return 0; } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Select a bounded VP8 worker count from source resolution and available +* logical processors. Multi-core systems retain one processor for the main +* engine and audio when possible, while one- and two-core devices remain +* able to use every processor required for practical decoding. +*/ +static unsigned int video_decoder_thread_count( + unsigned int width, + unsigned int height +) +{ + uint64_t pixel_count = (uint64_t)width * (uint64_t)height; + unsigned int resolution_limit; + unsigned int threads; + int cpu_count = SDL_GetCPUCount(); + + if(pixel_count <= UINT64_C(640) * UINT64_C(480)) { + resolution_limit = 1; + } else if(pixel_count <= UINT64_C(1280) * UINT64_C(720)) { + resolution_limit = 2; + } else if(pixel_count <= UINT64_C(1920) * UINT64_C(1080)) { + resolution_limit = 4; + } else { + resolution_limit = VIDEO_DECODER_THREAD_MAX; + } + + threads = cpu_count > 0 ? (unsigned int)cpu_count : 1U; + if(threads > 2U) { + --threads; + } + if(threads > resolution_limit) { + threads = resolution_limit; + } + if(threads > VIDEO_DECODER_THREAD_MAX) { + threads = VIDEO_DECODER_THREAD_MAX; + } + return threads ? threads : 1U; +} + // returns 0 on success, -1 on error /* * Caskey, Damon V. -* 2026-08-13 +* 2026-08-15 * -* Initialize the video decoder and queues against their owning stop -* state. DefaultDuration is optional - packet timestamps and packet -* duration retain playback timing when the track omits it. +* Initialize a resolution-aware multithreaded video decoder and queues +* against their owning stop state. DefaultDuration is optional - packet +* timestamps and packet duration retain playback timing when omitted. */ static int init_video( nestegg *nestegg_ctx, @@ -946,6 +1146,7 @@ static int init_video( ) { nestegg_video_params video_params; + vpx_codec_dec_cfg_t decoder_config = { 0 }; int has_default_duration; if(nestegg_track_video_params(nestegg_ctx, track, &video_params) < 0) { @@ -966,7 +1167,18 @@ static int init_video( return -1; } - if (vpx_codec_dec_init(&(video_ctx->vpx_ctx), vpx_codec_vp8_dx(), NULL, 0)) + decoder_config.threads = video_decoder_thread_count( + video_params.width, + video_params.height + ); + decoder_config.w = video_params.width; + decoder_config.h = video_params.height; + if(vpx_codec_dec_init( + &(video_ctx->vpx_ctx), + vpx_codec_vp8_dx(), + &decoder_config, + 0 + )) { printf("Error: failed to initialize libvpx\n"); return -1; @@ -975,6 +1187,7 @@ static int init_video( video_ctx->height = video_params.height; video_ctx->display_width = video_params.display_width; video_ctx->display_height = video_params.display_height; + video_ctx->playback_paused = 1; has_default_duration = nestegg_track_default_duration( nestegg_ctx, track, @@ -991,6 +1204,11 @@ static int init_video( video_params.width, video_params.height, video_params.display_width, video_params.display_height); } + printf( + "Video decoder: %u thread%s.\n", + decoder_config.threads, + decoder_config.threads == 1U ? "" : "s" + ); video_ctx->quit = quit; video_ctx->packet_queue = queue_init(VIDEO_PACKET_QUEUE_SIZE, quit); video_ctx->frame_queue = queue_init(FRAME_QUEUE_SIZE, quit); @@ -1225,6 +1443,10 @@ webm_context *webm_start_playback_ex( replace_all_audio, &ctx->quit ) == 0) { + ctx->audio_ctx.video_packet_queue = + ctx->video_ctx.packet_queue; + ctx->audio_ctx.video_frame_queue = + ctx->video_ctx.frame_queue; ctx->the_audio_thread = thread_create(audio_thread, "audio", &(ctx->audio_ctx)); if(!ctx->the_audio_thread) { printf("Error: Unable to start WebM audio decoder thread\n"); @@ -1284,8 +1506,8 @@ void webm_close(webm_context *ctx) ctx->quit = 1; if(ctx->the_demux_thread) thread_join(ctx->the_demux_thread); if(ctx->the_video_thread) thread_join(ctx->the_video_thread); - close_video(&(ctx->video_ctx)); if(ctx->the_audio_thread) thread_join(ctx->the_audio_thread); + close_video(&(ctx->video_ctx)); if(ctx->audio_track >= 0) close_audio(&(ctx->audio_ctx)); if(ctx->nestegg_ctx) nestegg_destroy(ctx->nestegg_ctx); if(ctx->io_ctx.packhandle >= 0) closepackfile(ctx->io_ctx.packhandle); @@ -1317,19 +1539,28 @@ uint64_t webm_get_duration(webm_context *ctx) * Caskey, Damon V. * 2026-08-15 * -* Apply pause state only when it actually changes. Initial PCM preroll is -* completed synchronously before the movie clock begins, so ordinary frame -* updates never need to poll stream status or lock the SDL audio device. +* Apply pause state to decoder backpressure and audio output. Initial PCM +* preroll is completed synchronously before the movie clock begins, so +* ordinary frame updates never need to poll stream status. */ void webm_set_audio_paused(webm_context *ctx, int paused) { audio_context *audio_ctx; - if(!ctx || ctx->audio_track < 0) { + if(!ctx) { return; } - audio_ctx = &ctx->audio_ctx; paused = paused != 0; + if(ctx->video_ctx.frame_queue) { + mutex_lock(ctx->video_ctx.frame_queue->mutex); + ctx->video_ctx.playback_paused = paused; + mutex_unlock(ctx->video_ctx.frame_queue->mutex); + } + if(ctx->audio_track < 0) { + return; + } + + audio_ctx = &ctx->audio_ctx; if(audio_ctx->output_paused == paused) { return; } diff --git a/engine/version.tmp b/engine/version.tmp index 3b525fe3c..c510b36bf 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7973" -#define VERSION_BUILD_INT 7973 -#define VERSION_COMMIT "ea4a479" +#define VERSION_BUILD "7974" +#define VERSION_BUILD_INT 7974 +#define VERSION_COMMIT "d73fb4a" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From a1e63580dfd98ffa93ec8ad8f545e7be86fa166f Mon Sep 17 00:00:00 2001 From: DCurrent Date: Sat, 15 Aug 2026 21:01:00 -0400 Subject: [PATCH 15/26] Bound WebM frame catch-up per engine update Snapshot decoded-frame queue occupancy before polling and process only that population during the current engine update. Defer frames published concurrently until the next update. This prevents a decoder producing stale frames from continuously refilling the queue and turning nonblocking catch-up into an unbounded main-thread stall. Preserve retained-frame promotion, future-frame timing, reverse playback, and terminal-marker handling. --- engine/source/webmlib/movie_playback.c | 12 +- engine/source/webmlib/vidplay.c | 328 ++++++++++++++++++++++++- engine/source/webmlib/vidplay.h | 1 + engine/version.tmp | 6 +- 4 files changed, 328 insertions(+), 19 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index cef88edac..a13ca43ce 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -1084,8 +1084,10 @@ static bool movie_playback_render_to( * Caskey, Damon V. * 2026-08-12 * -* Poll forward-decoded frames without blocking. Reverse seeks -* retain pending state until reaching the requested timestamp. +* Poll only the decoded frames present when this update begins. A decoder +* producing stale frames must not refill the queue faster than the main thread +* can empty it and turn one nonblocking poll into an unbounded engine stall. +* Reverse seeks retain pending state until reaching the requested timestamp. */ static void movie_playback_poll_frames( s_movie_playback *playback, @@ -1093,9 +1095,13 @@ static void movie_playback_poll_frames( int *terminal ) { + int remaining = webm_get_pending_frame_count(playback->context); int promoted = 0; - while(1) { + if(playback->next_frame) { + ++remaining; + } + while(remaining-- > 0) { int result; if(!playback->next_frame) { diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index c3f8d7556..264dbfe9a 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -48,6 +48,8 @@ #define VIDEO_DECODER_THREAD_MAX 8U #define VIDEO_PACKET_QUEUE_BACKPRESSURE_THRESHOLD \ ((VIDEO_PACKET_QUEUE_SIZE * 3) / 4) +#define AUDIO_RECOVERY_PCM_BUFFER_COUNT \ + (SOUND_STREAM_BUFFER_COUNT / 2U) #define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) #define AUDIO_PREROLL_TIMEOUT_MICROSECONDS UINT64_C(2000000) @@ -65,6 +67,7 @@ typedef struct { int size; int max_size; uint64_t replacement_count; + uint64_t resync_count; volatile int *quit; bor_mutex *mutex; bor_cond *not_full; @@ -257,6 +260,7 @@ static FixedSizeQueue *queue_init(int max_size, volatile int *quit) queue->size = 0; queue->max_size = max_size; queue->replacement_count = 0; + queue->resync_count = 0; queue->quit = quit; queue->mutex = mutex_create(); queue->not_full = cond_create(); @@ -360,12 +364,14 @@ static int queue_try_get(FixedSizeQueue *queue, void **data) * Caskey, Damon V. * 2026-08-15 * -* Capture queue occupancy and replacement totals only when an underrun is -* reported. Normal decoding and publication incur no diagnostic polling. +* Capture queue occupancy, replacement totals, and recovery events only when +* reporting exceptional state. Normal decoding and publication incur no +* diagnostic polling. */ static int queue_get_status( FixedSizeQueue *queue, - uint64_t *replacement_count + uint64_t *replacement_count, + uint64_t *resync_count ) { int size; @@ -373,6 +379,9 @@ static int queue_get_status( if(replacement_count) { *replacement_count = 0; } + if(resync_count) { + *resync_count = 0; + } if(!queue) { return -1; } @@ -382,10 +391,197 @@ static int queue_get_status( if(replacement_count) { *replacement_count = queue->replacement_count; } + if(resync_count) { + *resync_count = queue->resync_count; + } mutex_unlock(queue->mutex); return size; } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Identify a VP8 keyframe from its uncompressed frame tag and start code. +* WebM video packets may contain laced frames; recovery resumes only when +* the first frame in a packet establishes an independent decode sequence. +*/ +static int video_packet_is_keyframe(nestegg_packet *packet) +{ + unsigned char *data; + size_t data_size; + + if(!packet || + nestegg_packet_data(packet, 0, &data, &data_size) < 0 || + !data || + data_size < 10U) { + return 0; + } + + return !(data[0] & 1U) && + data[3] == 0x9dU && + data[4] == 0x01U && + data[5] == 0x2aU; +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Read playback pause state under the same frame-queue lock used by +* webm_set_audio_paused(). Paused movies retain their complete compressed +* and decoded queues because their audio reserve is intentionally idle. +*/ +static int video_playback_is_paused(video_context *video_ctx) +{ + int paused; + + if(!video_ctx || !video_ctx->frame_queue) { + return 1; + } + + mutex_lock(video_ctx->frame_queue->mutex); + paused = video_ctx->playback_paused; + mutex_unlock(video_ctx->frame_queue->mutex); + return paused; +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Detect genuine audio starvation risk before sacrificing video backlog. +* Empty compressed audio alone is harmless while the PCM ring remains +* healthy. Recovery begins only when both reserves are depleted and active +* playback would otherwise let a full video queue block the shared demuxer. +*/ +static int webm_audio_reserve_is_critical(webm_context *ctx) +{ + s_sound_pcm_stream_status status; + + if(!ctx || + ctx->audio_track < 0 || + video_playback_is_paused(&ctx->video_ctx) || + queue_get_status(ctx->audio_ctx.packet_queue, NULL, NULL) != 0 || + !sound_get_channel_pcm_stream_status_owned( + ctx->audio_ctx.channel, + ctx->audio_ctx.stream_play_id, + &status + )) { + return 0; + } + + return !status.producer_finished && + status.ready_buffer_count <= AUDIO_RECOVERY_PCM_BUFFER_COUNT; +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Try to publish one compressed video packet without waiting. Recovery uses +* this only after it has begun shedding newly demuxed video packets. Existing +* queue order and decoder dependencies remain untouched until a later +* keyframe can be admitted safely. +*/ +static int video_packet_queue_try_insert( + FixedSizeQueue *queue, + nestegg_packet *packet +) +{ + int index; + + if(!queue || !packet) { + return -1; + } + + mutex_lock(queue->mutex); + if(*queue->quit) { + mutex_unlock(queue->mutex); + return -1; + } + if(queue->size == queue->max_size) { + mutex_unlock(queue->mutex); + return 0; + } + index = (queue->start + queue->size) % queue->max_size; + queue->data[index] = packet; + queue->size++; + cond_signal(queue->not_empty); + mutex_unlock(queue->mutex); + return 1; +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Record a newly demuxed video packet sacrificed to preserve audio. Beginning +* a recovery increments the event count once; subsequent discards remain in +* that event until a keyframe is admitted without rewriting the live queue. +*/ +static void video_packet_queue_record_drop( + FixedSizeQueue *queue, + int begin_recovery +) +{ + if(!queue) { + return; + } + + mutex_lock(queue->mutex); + if(queue->replacement_count < UINT64_MAX) { + queue->replacement_count++; + } + if(begin_recovery && queue->resync_count < UINT64_MAX) { + queue->resync_count++; + } + mutex_unlock(queue->mutex); +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Insert compressed video with ordinary bounded blocking while audio has +* usable reserves. Return zero without consuming the packet when audio +* becomes critical, allowing the demuxer to perform keyframe recovery. +*/ +static int video_packet_queue_insert( + webm_context *ctx, + nestegg_packet *packet +) +{ + FixedSizeQueue *queue; + + if(!ctx || !(queue = ctx->video_ctx.packet_queue)) { + return -1; + } + + while(!ctx->quit) { + int index; + int queue_full; + + mutex_lock(queue->mutex); + if(queue->size < queue->max_size) { + index = (queue->start + queue->size) % queue->max_size; + queue->data[index] = packet; + queue->size++; + cond_signal(queue->not_empty); + mutex_unlock(queue->mutex); + return 1; + } + + cond_wait_timed(queue->not_full, queue->mutex, 10); + queue_full = queue->size == queue->max_size; + mutex_unlock(queue->mutex); + if(queue_full && webm_audio_reserve_is_critical(ctx)) { + return 0; + } + } + return -1; +} + /* * Caskey, Damon V. * 2026-08-15 @@ -415,6 +611,7 @@ static int video_frame_queue_insert( int index; int video_packet_count = queue_get_status( video_ctx->packet_queue, + NULL, NULL ); @@ -706,7 +903,9 @@ static int audio_thread(void *data) underrun_count - audio_ctx->reported_underrun_count >= UINT64_C(16))) { s_sound_pcm_stream_status status = { 0 }; + uint64_t dropped_packets; uint64_t dropped_frames; + uint64_t resync_count; int audio_packets; int decoded_frames; int video_packets; @@ -718,22 +917,27 @@ static int audio_thread(void *data) ); audio_packets = queue_get_status( audio_ctx->packet_queue, + NULL, NULL ); video_packets = queue_get_status( audio_ctx->video_packet_queue, - NULL + &dropped_packets, + &resync_count ); decoded_frames = queue_get_status( audio_ctx->video_frame_queue, - &dropped_frames + &dropped_frames, + NULL ); audio_ctx->reported_underrun_count = underrun_count; printf( "Warning: WebM audio underrun on sound channel %d " "(%" PRIu64 " total): PCM=%u/%u, " "packets=%d/%d audio and %d/%d video, " - "frames=%d/%d (%" PRIu64 " stale dropped).\n", + "frames=%d/%d (%" PRIu64 " stale dropped), " + "recovery=%" PRIu64 " incoming packets skipped in %" PRIu64 + " keyframe resync event%s.\n", audio_ctx->channel, underrun_count, status.ready_buffer_count, @@ -744,7 +948,10 @@ static int audio_thread(void *data) VIDEO_PACKET_QUEUE_SIZE, decoded_frames, FRAME_QUEUE_SIZE, - dropped_frames + dropped_frames, + dropped_packets, + resync_count, + resync_count == 1U ? "" : "s" ); } } @@ -1253,14 +1460,44 @@ static void close_video(video_context *video_ctx) * Caskey, Damon V. * 2026-08-15 * -* Route container packets to their decoder queues. WebM playback with -* audio elevates this shared producer so video work cannot prevent it -* from restoring the compressed audio reserve. +* Report the first completed compressed-video recovery and then every +* sixteenth occurrence. Producer-side shedding protects the decoder's live +* queue while keeping exceptional recovery visible when audio remains clean. +*/ +static void video_packet_queue_report_resync(FixedSizeQueue *queue) +{ + uint64_t dropped_packets; + uint64_t resync_count; + + queue_get_status(queue, &dropped_packets, &resync_count); + if(resync_count && + (resync_count == 1U || !(resync_count % UINT64_C(16)))) { + printf( + "Warning: WebM video backlog recovery skipped %" PRIu64 + " incoming compressed packets in %" PRIu64 + " keyframe resync event%s.\n", + dropped_packets, + resync_count, + resync_count == 1U ? "" : "s" + ); + } +} + +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Route container packets to their decoder queues. WebM playback with audio +* elevates this shared producer and protects a critical audio reserve from +* compressed-video head-of-line blocking. Recovery leaves the live decoder +* queue intact, sheds only newly arriving video, and resumes admission at a +* verified keyframe once queue capacity is available. */ static int demux_thread(void *data) { webm_context *ctx = (webm_context *)data; nestegg_packet *pkt; + int discard_video_until_keyframe = 0; int r; if(ctx->audio_track >= 0) { @@ -1283,11 +1520,50 @@ static int demux_thread(void *data) } else if (track == ctx->video_track) { - if (queue_insert(ctx->video_ctx.packet_queue, pkt) < 0) - { + int insert_result; + + if(discard_video_until_keyframe) { + if(video_packet_is_keyframe(pkt)) { + insert_result = video_packet_queue_try_insert( + ctx->video_ctx.packet_queue, + pkt + ); + if(insert_result > 0) { + discard_video_until_keyframe = 0; + video_packet_queue_report_resync( + ctx->video_ctx.packet_queue + ); + if(ctx->quit) break; + continue; + } + if(insert_result < 0) { + nestegg_free_packet(pkt); + break; + } + } + video_packet_queue_record_drop( + ctx->video_ctx.packet_queue, + 0 + ); + nestegg_free_packet(pkt); + if(ctx->quit) break; + continue; + } + insert_result = video_packet_queue_insert(ctx, pkt); + if(insert_result < 0) { nestegg_free_packet(pkt); break; } + if(insert_result == 0) { + video_packet_queue_record_drop( + ctx->video_ctx.packet_queue, + 1 + ); + nestegg_free_packet(pkt); + discard_video_until_keyframe = 1; + if(ctx->quit) break; + continue; + } } else { @@ -1296,8 +1572,11 @@ static int demux_thread(void *data) if (ctx->quit) break; } - queue_insert(ctx->video_ctx.packet_queue, NULL); + if(discard_video_until_keyframe) { + video_packet_queue_report_resync(ctx->video_ctx.packet_queue); + } if (ctx->audio_track >= 0) queue_insert(ctx->audio_ctx.packet_queue, NULL); + queue_insert(ctx->video_ctx.packet_queue, NULL); return 0; } @@ -1585,6 +1864,29 @@ void webm_set_audio_speed(webm_context *ctx, double speed) } } +/* +* Caskey, Damon V. +* 2026-08-15 +* +* Snapshot decoded-frame occupancy for one bounded main-thread poll. Frames +* published after this count is captured remain queued for the next engine +* update instead of extending the current update indefinitely. +*/ +int webm_get_pending_frame_count(webm_context *ctx) +{ + int count; + + if(!ctx) { + return 0; + } + count = queue_get_status( + ctx->video_ctx.frame_queue, + NULL, + NULL + ); + return count > 0 ? count : 0; +} + /* * Caskey, Damon V. * 2026-08-12 diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index c3d545f90..4ea076a60 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -32,6 +32,7 @@ uint64_t webm_get_duration(webm_context *ctx); void webm_set_audio_paused(webm_context *ctx, int paused); void webm_set_audio_speed(webm_context *ctx, double speed); void webm_close(webm_context *ctx); +int webm_get_pending_frame_count(webm_context *ctx); int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame); #endif diff --git a/engine/version.tmp b/engine/version.tmp index c510b36bf..1630f4793 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7974" -#define VERSION_BUILD_INT 7974 -#define VERSION_COMMIT "d73fb4a" +#define VERSION_BUILD "7975" +#define VERSION_BUILD_INT 7975 +#define VERSION_COMMIT "b96337d" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From b6b18229e5ebe4e22ac2ee530674bd3e46be1ed7 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Mon, 17 Aug 2026 10:12:45 -0400 Subject: [PATCH 16/26] Fix cross-thread SDL audio locking --- engine/sdl/sblaster.c | 41 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/engine/sdl/sblaster.c b/engine/sdl/sblaster.c index 52a47dc6c..484babf90 100644 --- a/engine/sdl/sblaster.c +++ b/engine/sdl/sblaster.c @@ -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) { @@ -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) From d344019a51c623843a1d0adf8561a0b009f1b165 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Mon, 17 Aug 2026 10:34:00 -0400 Subject: [PATCH 17/26] Make owned audio clock the video master --- engine/source/gamelib/soundmix.c | 35 +++++++++++ engine/source/gamelib/soundmix.h | 5 ++ engine/source/webmlib/movie_playback.c | 20 +++++-- engine/source/webmlib/vidplay.c | 81 ++++++++++++++++++++++++++ engine/source/webmlib/vidplay.h | 4 ++ 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/engine/source/gamelib/soundmix.c b/engine/source/gamelib/soundmix.c index e14b94a1c..974e33e40 100644 --- a/engine/source/gamelib/soundmix.c +++ b/engine/source/gamelib/soundmix.c @@ -3230,6 +3230,41 @@ bool sound_pause_channel_owned(int channel, int play_id, int toggle) { return true; } +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Snapshot the source frame consumed by an active push stream only while +* the supplied play ID still owns its channel. Reading under the SDL audio +* lock turns the callback's fixed-point cursor into a stable playback clock +* without exposing mutable mixer state to decoder or presentation threads. +*/ +bool sound_get_channel_pcm_stream_playback_frame_owned( + int channel, + int play_id, + uint64_t *playback_frame +) { + channelstruct *record; + + if(play_id < 0 || !playback_frame) { + return false; + } + + SB_lock_audio(); + record = sound_channel_pool_get(&sound_channel_pool, channel); + if(!record || + !sound_channel_pool_is_active(&sound_channel_pool, channel) || + record->playid != play_id || + record->stream_source != SOUND_CHANNEL_STREAM_SOURCE_PUSH) { + SB_unlock_audio(); + return false; + } + + *playback_frame = SOUND_SAMPLE_FIX_TO_INT(record->fp_samplepos); + SB_unlock_audio(); + return true; +} + /* * Caskey, Damon V. * 2026-08-04 diff --git a/engine/source/gamelib/soundmix.h b/engine/source/gamelib/soundmix.h index 0b8be9810..5c296a9e0 100644 --- a/engine/source/gamelib/soundmix.h +++ b/engine/source/gamelib/soundmix.h @@ -180,6 +180,11 @@ bool sound_set_channel_loop_offset(int channel, uint64_t loop_start_frame); bool sound_set_channel_period(int channel, uint64_t period); bool sound_set_channel_speed_owned(int channel, int play_id, double speed); bool sound_pause_channel_owned(int channel, int play_id, int toggle); +bool sound_get_channel_pcm_stream_playback_frame_owned( + int channel, + int play_id, + uint64_t *playback_frame +); bool sound_set_channel_priority(int channel, unsigned int priority); bool sound_set_channel_position(int channel, uint64_t sample_position); bool sound_set_channel_volume(int channel, unsigned int spatial_channel, int volume); diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index a13ca43ce..969c7c5f6 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -676,22 +676,34 @@ uint64_t movie_playback_get_active_mask(void) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Resolve signed playback rate into a bounded nanosecond -* position, including stationary and reverse playback. +* Use consumed PCM time as the master during forward playback. Each +* successful owned-channel snapshot also reanchors the monotonic fallback, +* preserving continuity for silent media, reverse playback, and replaced +* audio channels. */ static uint64_t movie_playback_position_now( - const s_movie_playback *playback, + s_movie_playback *playback, uint64_t now ) { + uint64_t audio_position; double elapsed; double position; if(playback->paused || playback->speed == 0.0) { return playback->position_anchor; } + if(playback->speed > 0.0 && + webm_get_audio_playback_position( + playback->context, + &audio_position + )) { + playback->position_anchor = audio_position; + playback->clock_anchor = now; + return audio_position; + } elapsed = now >= playback->clock_anchor ? (double)(now - playback->clock_anchor) * 1000.0 * playback->speed diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 264dbfe9a..05301713b 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -90,6 +90,7 @@ typedef struct { int aligning; uint64_t observed_underrun_count; uint64_t reported_underrun_count; + uint64_t playback_start_timestamp; uint64_t output_timestamp; uint64_t leading_silence_frames; volatile int *quit; @@ -712,6 +713,42 @@ static uint64_t audio_nanoseconds_to_frames( return frames + remainder_frames; } +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Convert a consumed PCM source-frame count to its media timeline duration +* without overflowing the 64-bit nanosecond clock. Whole seconds are split +* before multiplication so long-running streams retain bounded arithmetic. +*/ +static uint64_t audio_frames_to_nanoseconds( + uint64_t frame_count, + int frequency +) +{ + const uint64_t nanoseconds_per_second = UINT64_C(1000000000); + uint64_t whole_seconds; + uint64_t remainder_frames; + uint64_t remainder_nanoseconds; + uint64_t nanoseconds; + + if(frequency <= 0) { + return UINT64_MAX; + } + + whole_seconds = frame_count / (uint64_t)frequency; + remainder_frames = frame_count % (uint64_t)frequency; + if(whole_seconds > UINT64_MAX / nanoseconds_per_second) { + return UINT64_MAX; + } + nanoseconds = whole_seconds * nanoseconds_per_second; + remainder_nanoseconds = + remainder_frames * nanoseconds_per_second / (uint64_t)frequency; + return nanoseconds <= UINT64_MAX - remainder_nanoseconds + ? nanoseconds + remainder_nanoseconds + : UINT64_MAX; +} + /* * Caskey, Damon V. * 2026-08-12 @@ -1070,6 +1107,7 @@ static int init_audio( audio_ctx->frequency = (int)audioParams.rate; audio_ctx->avail_samples = audio_ctx->last_samples = 0; audio_ctx->aligning = 1; + audio_ctx->playback_start_timestamp = seek_timestamp; audio_ctx->output_timestamp = seek_timestamp <= UINT64_MAX - audioParams.codec_delay ? seek_timestamp + audioParams.codec_delay @@ -1814,6 +1852,49 @@ uint64_t webm_get_duration(webm_context *ctx) return ctx ? ctx->duration : 0; } +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Resolve the movie timeline from PCM frames consumed by this WebM object's +* owned sound channel. The clock stops with real audio during pauses and +* underruns, and becomes unavailable if another producer takes the channel. +*/ +int webm_get_audio_playback_position( + webm_context *ctx, + uint64_t *position +) +{ + audio_context *audio_ctx; + uint64_t playback_frame; + uint64_t elapsed; + + if(!ctx || !position || ctx->audio_track < 0) { + return 0; + } + + audio_ctx = &ctx->audio_ctx; + if(!sound_get_channel_pcm_stream_playback_frame_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + &playback_frame + )) { + return 0; + } + + elapsed = audio_frames_to_nanoseconds( + playback_frame, + audio_ctx->frequency + ); + *position = elapsed <= UINT64_MAX - audio_ctx->playback_start_timestamp + ? audio_ctx->playback_start_timestamp + elapsed + : UINT64_MAX; + if(ctx->duration && *position > ctx->duration) { + *position = ctx->duration; + } + return 1; +} + /* * Caskey, Damon V. * 2026-08-15 diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index 4ea076a60..8046493f4 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -29,6 +29,10 @@ webm_context *webm_start_playback_ex( ); void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims); uint64_t webm_get_duration(webm_context *ctx); +int webm_get_audio_playback_position( + webm_context *ctx, + uint64_t *position +); void webm_set_audio_paused(webm_context *ctx, int paused); void webm_set_audio_speed(webm_context *ctx, double speed); void webm_close(webm_context *ctx); From db025c7905d86ffb5d7e8f11e00d37671d2084cb Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Mon, 17 Aug 2026 13:08:50 -0400 Subject: [PATCH 18/26] Replace WebM compressed backlog recovery --- engine/source/webmlib/vidplay.c | 124 +++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 27 deletions(-) diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 05301713b..8be537dd0 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -478,12 +478,11 @@ static int webm_audio_reserve_is_critical(webm_context *ctx) /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * -* Try to publish one compressed video packet without waiting. Recovery uses -* this only after it has begun shedding newly demuxed video packets. Existing -* queue order and decoder dependencies remain untouched until a later -* keyframe can be admitted safely. +* Try to publish the first verified keyframe following a compressed-video +* queue reset without waiting. The demuxer is the queue's only producer, so +* capacity remains available after the stale backlog has been discarded. */ static int video_packet_queue_try_insert( FixedSizeQueue *queue, @@ -515,16 +514,76 @@ static int video_packet_queue_try_insert( /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * -* Record a newly demuxed video packet sacrificed to preserve audio. Beginning -* a recovery increments the event count once; subsequent discards remain in -* that event until a keyframe is admitted without rewriting the live queue. +* Atomically discard the stale compressed-video backlog and optionally make +* the packet that triggered recovery its new head when that packet is already +* a verified keyframe. Packet destruction occurs after releasing the queue +* lock so the decoder can resume immediately from the replacement boundary. */ -static void video_packet_queue_record_drop( +static int video_packet_queue_begin_resync( FixedSizeQueue *queue, - int begin_recovery + nestegg_packet *recovery_keyframe ) +{ + nestegg_packet *discarded_packets[VIDEO_PACKET_QUEUE_SIZE]; + int discarded_count; + int i; + + if(!queue || + queue->max_size < 1 || + queue->max_size > VIDEO_PACKET_QUEUE_SIZE) { + return -1; + } + + mutex_lock(queue->mutex); + if(*queue->quit) { + mutex_unlock(queue->mutex); + return -1; + } + discarded_count = queue->size; + for(i = 0; i < discarded_count; ++i) { + int index = (queue->start + i) % queue->max_size; + + discarded_packets[i] = queue->data[index]; + queue->data[index] = NULL; + } + queue->start = 0; + queue->size = 0; + if((uint64_t)discarded_count > + UINT64_MAX - queue->replacement_count) { + queue->replacement_count = UINT64_MAX; + } + else { + queue->replacement_count += (uint64_t)discarded_count; + } + if(queue->resync_count < UINT64_MAX) { + queue->resync_count++; + } + if(recovery_keyframe) { + queue->data[0] = recovery_keyframe; + queue->size = 1; + cond_signal(queue->not_empty); + } + cond_signal(queue->not_full); + mutex_unlock(queue->mutex); + + for(i = 0; i < discarded_count; ++i) { + if(discarded_packets[i]) { + nestegg_free_packet(discarded_packets[i]); + } + } + return 1; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Record a newly demuxed packet discarded while waiting for the first +* verified keyframe after a compressed-video queue reset. +*/ +static void video_packet_queue_record_drop(FixedSizeQueue *queue) { if(!queue) { return; @@ -534,9 +593,6 @@ static void video_packet_queue_record_drop( if(queue->replacement_count < UINT64_MAX) { queue->replacement_count++; } - if(begin_recovery && queue->resync_count < UINT64_MAX) { - queue->resync_count++; - } mutex_unlock(queue->mutex); } @@ -1496,11 +1552,11 @@ static void close_video(video_context *video_ctx) /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * * Report the first completed compressed-video recovery and then every -* sixteenth occurrence. Producer-side shedding protects the decoder's live -* queue while keeping exceptional recovery visible when audio remains clean. +* sixteenth occurrence. The discard total includes stale queued packets and +* newly demuxed packets skipped before the next verified keyframe. */ static void video_packet_queue_report_resync(FixedSizeQueue *queue) { @@ -1511,8 +1567,8 @@ static void video_packet_queue_report_resync(FixedSizeQueue *queue) if(resync_count && (resync_count == 1U || !(resync_count % UINT64_C(16)))) { printf( - "Warning: WebM video backlog recovery skipped %" PRIu64 - " incoming compressed packets in %" PRIu64 + "Warning: WebM video backlog recovery discarded %" PRIu64 + " compressed packets in %" PRIu64 " keyframe resync event%s.\n", dropped_packets, resync_count, @@ -1523,13 +1579,13 @@ static void video_packet_queue_report_resync(FixedSizeQueue *queue) /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * * Route container packets to their decoder queues. WebM playback with audio * elevates this shared producer and protects a critical audio reserve from -* compressed-video head-of-line blocking. Recovery leaves the live decoder -* queue intact, sheds only newly arriving video, and resumes admission at a -* verified keyframe once queue capacity is available. +* compressed-video head-of-line blocking. Recovery atomically discards the +* stale compressed queue and resumes admission at a verified keyframe so the +* decoder does not spend the recovery interval processing obsolete work. */ static int demux_thread(void *data) { @@ -1580,8 +1636,7 @@ static int demux_thread(void *data) } } video_packet_queue_record_drop( - ctx->video_ctx.packet_queue, - 0 + ctx->video_ctx.packet_queue ); nestegg_free_packet(pkt); if(ctx->quit) break; @@ -1593,9 +1648,24 @@ static int demux_thread(void *data) break; } if(insert_result == 0) { - video_packet_queue_record_drop( + int packet_is_keyframe = video_packet_is_keyframe(pkt); + + if(video_packet_queue_begin_resync( ctx->video_ctx.packet_queue, - 1 + packet_is_keyframe ? pkt : NULL + ) < 0) { + nestegg_free_packet(pkt); + break; + } + if(packet_is_keyframe) { + video_packet_queue_report_resync( + ctx->video_ctx.packet_queue + ); + if(ctx->quit) break; + continue; + } + video_packet_queue_record_drop( + ctx->video_ctx.packet_queue ); nestegg_free_packet(pkt); discard_video_until_keyframe = 1; From 70cc5c051a233702d2c8ab8fa139344c46be63d9 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Mon, 17 Aug 2026 14:01:26 -0400 Subject: [PATCH 19/26] Make WebM decoder lifecycle asynchronous --- engine/openbor.c | 8 +- engine/source/webmlib/movie_playback.c | 423 ++++++++++++--- engine/source/webmlib/movie_playback.h | 1 + engine/source/webmlib/vidplay.c | 707 ++++++++++++++++++++----- engine/source/webmlib/vidplay.h | 23 + 5 files changed, 944 insertions(+), 218 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index dc856a473..e602258d4 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -51936,11 +51936,15 @@ int playwebm(const char *path, int noskip) } quit: + if(playback && playback->failed && retval > 0) { + retval = 0; + } if(playback && playback->active) { movie_playback_stop(playback); } - if(source_id >= 0 && !movie_source_unload(source_id)) { - retval = 0; + while(source_id >= 0 && !movie_source_unload(source_id)) { + movie_playback_update(0); + usleep(1000); } return retval; } diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 969c7c5f6..13c6e0dca 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -57,8 +57,36 @@ typedef struct s_movie_source { int id; } s_movie_source; +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Keep asynchronous decoder ownership outside the stable script-visible +* playback record. Each decoder retains its movie source until lifecycle +* teardown reaches CLOSED, including decoders retired by seeks or channel +* replacement while their worker is still running. +*/ +typedef struct s_movie_decoder { + webm_context *context; + int source_id; + int channel; + int ready; + struct s_movie_decoder *next; +} s_movie_decoder; + +typedef struct s_movie_decoder_request { + uint64_t position; + int source_id; + int volume; + int pending; +} s_movie_decoder_request; + typedef struct s_movie_playback_pool { s_movie_playback channel[MOVIE_CHANNEL_COUNT]; + s_movie_decoder *decoder[MOVIE_CHANNEL_COUNT]; + s_movie_decoder_request decoder_request[MOVIE_CHANNEL_COUNT]; + unsigned int retiring_decoder_count[MOVIE_CHANNEL_COUNT]; + s_movie_decoder *retired_decoder; s_movie_source *source; size_t source_capacity; int next_source_id; @@ -275,16 +303,102 @@ static void movie_source_release(int source_id) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Close decoder-owned resources separately from the retained -* current frame so seeks can display the old frame until ready. +* Poll decoder retirement without waiting during runtime updates. Shutdown +* may explicitly wait after every playback has stopped. Source references +* remain held until each lifecycle worker has released its borrowed path and +* cache storage. */ -static void movie_playback_close_decoder(s_movie_playback *playback) +static void movie_playback_reap_retired_decoders(int wait) +{ + s_movie_decoder **cursor = &movie_playback_pool.retired_decoder; + + while(*cursor) { + s_movie_decoder *decoder = *cursor; + int closed; + + if(wait) { + webm_close(decoder->context); + closed = 1; + } + else { + closed = webm_poll_closed(decoder->context); + } + if(!closed) { + cursor = &decoder->next; + continue; + } + + *cursor = decoder->next; + if(decoder->channel >= 0 && + (unsigned int)decoder->channel < MOVIE_CHANNEL_COUNT && + movie_playback_pool.retiring_decoder_count[ + decoder->channel + ]) { + --movie_playback_pool.retiring_decoder_count[ + decoder->channel + ]; + } + movie_source_release(decoder->source_id); + free(decoder); + } +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Move the channel's decoder directly to asynchronous retirement without +* allocating or joining. Per-channel retirement counts serialize a later +* replacement while allowing repeated pending seeks to collapse to one. +*/ +static void movie_playback_retire_decoder(s_movie_playback *playback) { - if(playback->context) { - webm_close(playback->context); + int channel; + s_movie_decoder *decoder; + + if(!playback) { + return; + } + channel = playback->index; + if(channel >= 0 && + (unsigned int)channel < MOVIE_CHANNEL_COUNT && + &movie_playback_pool.channel[channel] == playback && + (decoder = movie_playback_pool.decoder[channel])) { + movie_playback_pool.decoder[channel] = NULL; playback->context = NULL; + decoder->channel = channel; + ++movie_playback_pool.retiring_decoder_count[channel]; + webm_request_close(decoder->context); + decoder->next = movie_playback_pool.retired_decoder; + movie_playback_pool.retired_decoder = decoder; + } +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Retire the current decoder and cancel pending replacement intent when the +* owning playback itself closes. Its queued look-ahead frame is released +* immediately because it belongs to the canceled decode sequence. +*/ +static void movie_playback_close_decoder(s_movie_playback *playback) +{ + int channel; + + if(!playback) { + return; + } + channel = playback->index; + movie_playback_retire_decoder(playback); + if(channel >= 0 && (unsigned int)channel < MOVIE_CHANNEL_COUNT) { + memset( + &movie_playback_pool.decoder_request[channel], + 0, + sizeof(movie_playback_pool.decoder_request[channel]) + ); } yuv_frame_destroy(playback->next_frame); playback->next_frame = NULL; @@ -356,7 +470,9 @@ bool movie_playback_init(void) movie_playback_reset_record(&movie_playback_pool.channel[channel]); } - if(!movie_source_reserve(MOVIE_SOURCE_CAPACITY_INITIAL)) { + if(!webm_lifecycle_init() || + !movie_source_reserve(MOVIE_SOURCE_CAPACITY_INITIAL)) { + webm_lifecycle_shutdown(); memset(&movie_playback_pool, 0, sizeof(movie_playback_pool)); return false; } @@ -382,6 +498,7 @@ void movie_playback_shutdown(void) } movie_playback_stop_all(); + movie_playback_reap_retired_decoders(1); for(source_id = 0; source_id < movie_playback_pool.source_capacity; source_id++) { @@ -392,6 +509,7 @@ void movie_playback_shutdown(void) if(movie_playback_pool.yuv_initialized) { yuv_clear(); } + webm_lifecycle_shutdown(); memset(&movie_playback_pool, 0, sizeof(movie_playback_pool)); } @@ -575,14 +693,17 @@ int movie_source_load(const char *path, e_movie_loading_mode loading_mode) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Unload an inactive source. Live playbacks retain their -* source until stopped and therefore reject premature unload. +* Unload a source only after active and asynchronously retiring decoders +* release it. Each call first reaps completed teardown without waiting. */ bool movie_source_unload(int source_id) { - s_movie_source *source = movie_source_get(source_id); + s_movie_source *source; + + movie_playback_reap_retired_decoders(0); + source = movie_source_get(source_id); if(!source || source->references) { return false; @@ -674,6 +795,32 @@ uint64_t movie_playback_get_active_mask(void) : UINT64_C(0); } +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Test whether the current channel decoder has crossed its asynchronous +* READY boundary. Property mutations may update pending playback intent +* before this point without touching partially initialized decoder fields. +*/ +static int movie_playback_decoder_ready( + const s_movie_playback *playback +) +{ + int channel; + s_movie_decoder *decoder; + + if(!playback) { + return 0; + } + channel = playback->index; + if(channel < 0 || (unsigned int)channel >= MOVIE_CHANNEL_COUNT) { + return 0; + } + decoder = movie_playback_pool.decoder[channel]; + return decoder && decoder->ready; +} + /* * Caskey, Damon V. * 2026-08-17 @@ -692,7 +839,9 @@ static uint64_t movie_playback_position_now( double elapsed; double position; - if(playback->paused || playback->speed == 0.0) { + if(playback->paused || + playback->speed == 0.0 || + !movie_playback_decoder_ready(playback)) { return playback->position_anchor; } if(playback->speed > 0.0 && @@ -720,83 +869,195 @@ static uint64_t movie_playback_position_now( /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Open or seek the WebM backend from a reusable movie source -* while preserving the last frame until replacement is ready. +* Start the latest pending decoder request only after the previous decoder +* for this movie channel reaches CLOSED. Repeated seeks during teardown +* replace the request in place instead of accumulating lifecycle threads. */ -static bool movie_playback_open_media( - s_movie_playback *playback, - uint64_t position, - int volume +static int movie_playback_start_pending_decoder( + s_movie_playback *playback ) { - s_movie_source *source = movie_source_get(playback->source_id); - yuv_video_mode previous_mode = playback->video_mode; - uint64_t duration; + s_movie_decoder_request *request; + s_movie_source *source; + s_movie_decoder *decoder; + webm_context *context; + int channel = playback->index; - if(!source) { - return false; + if(channel < 0 || (unsigned int)channel >= MOVIE_CHANNEL_COUNT) { + return -1; + } + request = &movie_playback_pool.decoder_request[channel]; + if(!request->pending) { + return movie_playback_pool.decoder[channel] ? 1 : -1; + } + if(movie_playback_pool.retiring_decoder_count[channel]) { + return 0; + } + source = movie_source_get(request->source_id); + if(!source || playback->source_id != request->source_id) { + request->pending = 0; + return -1; } - movie_playback_close_decoder(playback); - playback->volume = volume; - playback->context = webm_start_playback_ex( + decoder = calloc(1, sizeof(*decoder)); + if(!decoder) { + request->pending = 0; + return -1; + } + ++source->references; + context = webm_start_playback_ex( source->path, - volume, + request->volume, playback->sound_channel, source->cache_buffer, source->cache_size, - position, + request->position, playback->speed > 0.0, playback->replace_all_audio ); - if(!playback->context) { + if(!context) { + movie_source_release(request->source_id); + request->pending = 0; + free(decoder); + return -1; + } + + decoder->context = context; + decoder->source_id = request->source_id; + decoder->channel = channel; + movie_playback_pool.decoder[channel] = decoder; + playback->context = context; + request->pending = 0; + return 1; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Begin an asynchronous WebM open or seek from a reusable source while +* preserving the last frame until replacement is ready. Decoder ownership +* retains an additional source reference through asynchronous teardown. +*/ +static bool movie_playback_begin_media( + s_movie_playback *playback, + uint64_t position, + int volume +) +{ + s_movie_source *source = movie_source_get(playback->source_id); + s_movie_decoder_request *request; + int channel = playback->index; + + if(!source || + channel < 0 || + (unsigned int)channel >= MOVIE_CHANNEL_COUNT) { return false; } - webm_get_video_info(playback->context, &playback->video_mode); - duration = webm_get_duration(playback->context); - playback->duration = duration / UINT64_C(1000000); - if((playback->video_mode.width & 1) || - (playback->video_mode.height & 1) || - playback->video_mode.width > (INT_MAX >> 16) || - playback->video_mode.height > (INT_MAX >> 16) || + movie_playback_retire_decoder(playback); + yuv_frame_destroy(playback->next_frame); + playback->next_frame = NULL; + request = &movie_playback_pool.decoder_request[channel]; + request->position = position; + request->source_id = playback->source_id; + request->volume = volume; + request->pending = 1; + playback->volume = volume; + playback->position_anchor = position; + playback->position = position / UINT64_C(1000000); + playback->reverse_pending = playback->speed < 0.0; + playback->terminal_pending = 0; + playback->failed = 0; + return movie_playback_start_pending_decoder(playback) >= 0; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Poll an asynchronous decoder and commit its immutable media metadata once +* READY is published. Movie clocks begin at this boundary, preventing open, +* seek, and preroll latency from advancing the presentation timeline. +*/ +static int movie_playback_poll_decoder(s_movie_playback *playback) +{ + s_movie_decoder *decoder; + e_webm_decoder_state state; + yuv_video_mode video_mode = { 0 }; + uint64_t duration; + int channel = playback->index; + + if(channel < 0 || + (unsigned int)channel >= MOVIE_CHANNEL_COUNT || + !(decoder = movie_playback_pool.decoder[channel])) { + return -1; + } + if(decoder->ready) { + return 1; + } + + state = webm_get_decoder_state(decoder->context); + if(state == WEBM_DECODER_STATE_FAILED || + state == WEBM_DECODER_STATE_CLOSING || + state == WEBM_DECODER_STATE_CLOSED) { + return -1; + } + if(state != WEBM_DECODER_STATE_READY) { + return 0; + } + + webm_get_video_info(decoder->context, &video_mode); + duration = webm_get_duration(decoder->context); + if((video_mode.width & 1) || + (video_mode.height & 1) || + video_mode.width > (INT_MAX >> 16) || + video_mode.height > (INT_MAX >> 16) || !movie_playback_dimensions_valid( - playback->video_mode.width, - playback->video_mode.height + video_mode.width, + video_mode.height )) { printf( "Error: Movie frame dimensions %d*%d cannot be represented by the 32-bit screen renderer.\n", - playback->video_mode.width, - playback->video_mode.height + video_mode.width, + video_mode.height ); - movie_playback_close_decoder(playback); - return false; + return -1; } - if(previous_mode.width != playback->video_mode.width || - previous_mode.height != playback->video_mode.height) { + if(playback->video_mode.width != video_mode.width || + playback->video_mode.height != video_mode.height) { yuv_frame_destroy(playback->current_frame); playback->current_frame = NULL; - if(playback->rgb_frame) freescreen(&playback->rgb_frame); - if(playback->scaled_frame) freescreen(&playback->scaled_frame); + if(playback->rgb_frame) { + freescreen(&playback->rgb_frame); + } + if(playback->scaled_frame) { + freescreen(&playback->scaled_frame); + } + } + playback->video_mode = video_mode; + playback->duration = duration / UINT64_C(1000000); + if(duration && playback->position_anchor >= duration) { + playback->position_anchor = duration - 1U; } - - playback->position_anchor = position; playback->clock_anchor = timer_uticks(); - playback->position = position / UINT64_C(1000000); + playback->position = + playback->position_anchor / UINT64_C(1000000); playback->reverse_pending = playback->speed < 0.0; playback->terminal_pending = 0; + decoder->ready = 1; if(playback->speed > 0.0) { - webm_set_audio_speed(playback->context, playback->speed); + webm_set_audio_speed(decoder->context, playback->speed); } webm_set_audio_paused( - playback->context, + decoder->context, playback->paused || playback->speed <= 0.0 ); - return true; + return 1; } /* @@ -842,7 +1103,7 @@ s_movie_playback *movie_playback_play( playback->source_id = source_id; playback->replace_all_audio = replace_all_audio; ++source->references; - if(!movie_playback_open_media(playback, 0, volume)) { + if(!movie_playback_begin_media(playback, 0, volume)) { movie_playback_reset_record(playback); return NULL; } @@ -1163,10 +1424,11 @@ static void movie_playback_poll_frames( /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Advance playback clocks and decoder state independently from -* composition. Reverse rates periodically seek and decode forward. +* Advance playback clocks and decoder state independently from composition. +* Lifecycle polling keeps open, seek, preroll, and teardown work off the +* engine thread while reverse rates periodically request forward decodes. */ void movie_playback_update(int interrupt_requested) { @@ -1178,11 +1440,13 @@ void movie_playback_update(int interrupt_requested) return; } + movie_playback_reap_retired_decoders(0); now = timer_uticks(); active_mask = movie_playback_pool.active_mask; while((channel = sound_channel_mask_first(active_mask)) >= 0) { s_movie_playback *playback = &movie_playback_pool.channel[channel]; uint64_t position; + int decoder_result; int terminal = 0; active_mask &= ~(UINT64_C(1) << channel); @@ -1190,9 +1454,21 @@ void movie_playback_update(int interrupt_requested) movie_playback_stop(playback); continue; } + decoder_result = movie_playback_start_pending_decoder(playback); + if(decoder_result > 0) { + decoder_result = movie_playback_poll_decoder(playback); + } + if(decoder_result < 0) { + movie_playback_stop(playback); + playback->failed = 1; + continue; + } + if(decoder_result == 0) { + continue; + } if(playback->terminal_pending) { if(playback->repeat) { - if(!movie_playback_open_media( + if(!movie_playback_begin_media( playback, 0, playback->volume @@ -1217,7 +1493,7 @@ void movie_playback_update(int interrupt_requested) playback->position_anchor >= position && playback->position_anchor - position >= MOVIE_REVERSE_SEEK_INTERVAL) { - if(!movie_playback_open_media( + if(!movie_playback_begin_media( playback, position, playback->volume @@ -1239,7 +1515,7 @@ void movie_playback_update(int interrupt_requested) if(playback->repeat && playback->duration) { uint64_t repeat_position = (playback->duration - 1U) * UINT64_C(1000000); - if(!movie_playback_open_media( + if(!movie_playback_begin_media( playback, repeat_position, playback->volume @@ -1256,7 +1532,7 @@ void movie_playback_update(int interrupt_requested) if(playback->frame_dirty) { playback->terminal_pending = 1; } else if(playback->repeat) { - if(!movie_playback_open_media( + if(!movie_playback_begin_media( playback, 0, playback->volume @@ -1383,10 +1659,12 @@ bool movie_playback_set_paused(s_movie_playback *playback, int paused) playback->clock_anchor = now; } playback->paused = paused; - webm_set_audio_paused( - playback->context, - paused || playback->speed <= 0.0 - ); + if(movie_playback_decoder_ready(playback)) { + webm_set_audio_paused( + playback->context, + paused || playback->speed <= 0.0 + ); + } return true; } @@ -1409,7 +1687,7 @@ bool movie_playback_set_position( } position_ns = position * UINT64_C(1000000); - if(!movie_playback_open_media(playback, position_ns, volume)) { + if(!movie_playback_begin_media(playback, position_ns, volume)) { movie_playback_stop(playback); return false; } @@ -1445,7 +1723,7 @@ bool movie_playback_set_sound_channel( position = movie_playback_position_now(playback, timer_uticks()); playback->sound_channel = sound_channel; - if(!movie_playback_open_media(playback, position, volume)) { + if(!movie_playback_begin_media(playback, position, volume)) { movie_playback_stop(playback); return false; } @@ -1477,17 +1755,20 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) playback->position = position / UINT64_C(1000000); if(playback->speed > 0.0 && previous_speed <= 0.0) { - if(!movie_playback_open_media(playback, position, playback->volume)) { + if(!movie_playback_begin_media(playback, position, playback->volume)) { movie_playback_stop(playback); return false; } - } else if(playback->speed > 0.0) { + } else if(playback->speed > 0.0 && + movie_playback_decoder_ready(playback)) { webm_set_audio_speed(playback->context, playback->speed); webm_set_audio_paused(playback->context, playback->paused); - } else { + } else if(playback->speed <= 0.0) { playback->reverse_pending = 0; playback->terminal_pending = 0; - webm_set_audio_paused(playback->context, 1); + if(movie_playback_decoder_ready(playback)) { + webm_set_audio_paused(playback->context, 1); + } } return true; } diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index 5589c90ce..e4e830a07 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -51,6 +51,7 @@ typedef struct s_movie_playback { uint64_t height; int sound_channel; int source_id; + int failed; float speed; uint64_t duration; uint64_t position; diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 8be537dd0..e2c132160 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -68,7 +68,7 @@ typedef struct { int max_size; uint64_t replacement_count; uint64_t resync_count; - volatile int *quit; + SDL_atomic_t *quit; bor_mutex *mutex; bor_cond *not_full; bor_cond *not_empty; @@ -93,7 +93,7 @@ typedef struct { uint64_t playback_start_timestamp; uint64_t output_timestamp; uint64_t leading_silence_frames; - volatile int *quit; + SDL_atomic_t *quit; uint8_t pcm_buffer[SOUND_STREAM_BUFFER_SIZE]; } audio_context; @@ -107,7 +107,7 @@ typedef struct { int display_height; int playback_paused; uint64_t frame_delay; - volatile int *quit; + SDL_atomic_t *quit; } video_context; typedef struct { @@ -130,10 +130,127 @@ struct webm_context { bor_thread *the_demux_thread; bor_thread *the_video_thread; bor_thread *the_audio_thread; + bor_thread *the_lifecycle_thread; + bor_mutex *lifecycle_mutex; + bor_cond *lifecycle_condition; + char *path; uint64_t duration; - volatile int quit; + uint64_t seek_timestamp; + e_webm_decoder_state decoder_state; + int volume; + int sound_channel; + int play_audio; + int replace_all_audio; + int close_requested; + int video_initialized; + int audio_initialized; + SDL_atomic_t quit; }; +static bor_mutex *webm_lifecycle_operation_mutex; + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Read the decoder stop flag with SDL atomics. Queue, demux, codec, and +* lifecycle workers share this flag without relying on volatile accesses +* that provide no cross-thread synchronization in C. +*/ +static int webm_stop_is_requested(SDL_atomic_t *stop) +{ + return !stop || SDL_AtomicGet(stop) != 0; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Publish decoder cancellation through the same SDL atomic consumed by every +* queue and worker, preserving one stop path across lifecycle transitions. +*/ +static void webm_request_decoder_stop(SDL_atomic_t *stop) +{ + if(stop) { + SDL_AtomicSet(stop, 1); + } +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Serialize decoder resource acquisition and release on lifecycle workers. +* Independent decoding remains concurrent, while sound-channel replacement, +* container setup, and teardown cannot overtake one another during rapid +* asynchronous channel replacement. +*/ +int webm_lifecycle_init(void) +{ + if(webm_lifecycle_operation_mutex) { + return 1; + } + webm_lifecycle_operation_mutex = mutex_create(); + return webm_lifecycle_operation_mutex != NULL; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Release shared lifecycle serialization after every decoder has reached +* CLOSED and its lifecycle thread has been reaped during engine shutdown. +*/ +void webm_lifecycle_shutdown(void) +{ + if(webm_lifecycle_operation_mutex) { + mutex_destroy(webm_lifecycle_operation_mutex); + webm_lifecycle_operation_mutex = NULL; + } +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Retain the source path for the lifetime of an asynchronous decoder open. +* The movie source registry may otherwise release its caller-owned string +* before the lifecycle worker has finished opening a streamed source. +*/ +static char *webm_copy_path(const char *path) +{ + char *copy; + size_t length; + + if(!path || !path[0]) { + return NULL; + } + length = strlen(path) + 1U; + copy = malloc(length); + if(copy) { + memcpy(copy, path, length); + } + return copy; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Read asynchronous cancellation under the lifecycle mutex. The engine +* thread publishes only this request; the lifecycle worker remains the sole +* writer of the decoder stop flag consumed by demux and codec workers. +*/ +static int webm_close_is_requested(webm_context *ctx) +{ + int close_requested; + + mutex_lock(ctx->lifecycle_mutex); + close_requested = ctx->close_requested; + mutex_unlock(ctx->lifecycle_mutex); + return close_requested; +} + /* * Caskey, Damon V. * 2026-08-13 @@ -246,7 +363,7 @@ static int64_t webm_io_tell(void *userdata) return seekpackfile64(io_ctx->packhandle, 0, SEEK_CUR); } -static FixedSizeQueue *queue_init(int max_size, volatile int *quit) +static FixedSizeQueue *queue_init(int max_size, SDL_atomic_t *quit) { FixedSizeQueue *queue; @@ -288,7 +405,7 @@ int queue_insert(FixedSizeQueue *queue, void *data) { while(cond_wait_timed(queue->not_full, queue->mutex, 10) != 0) { - if (*queue->quit) + if(webm_stop_is_requested(queue->quit)) { mutex_unlock(queue->mutex); return -1; @@ -316,7 +433,7 @@ void *queue_get(FixedSizeQueue *queue) { while (cond_wait_timed(queue->not_empty, queue->mutex, 10) != 0) { - if (*queue->quit) + if(webm_stop_is_requested(queue->quit)) { mutex_unlock(queue->mutex); return NULL; @@ -496,7 +613,7 @@ static int video_packet_queue_try_insert( } mutex_lock(queue->mutex); - if(*queue->quit) { + if(webm_stop_is_requested(queue->quit)) { mutex_unlock(queue->mutex); return -1; } @@ -537,7 +654,7 @@ static int video_packet_queue_begin_resync( } mutex_lock(queue->mutex); - if(*queue->quit) { + if(webm_stop_is_requested(queue->quit)) { mutex_unlock(queue->mutex); return -1; } @@ -615,7 +732,7 @@ static int video_packet_queue_insert( return -1; } - while(!ctx->quit) { + while(!webm_stop_is_requested(&ctx->quit)) { int index; int queue_full; @@ -664,7 +781,7 @@ static int video_frame_queue_insert( frame_queue = video_ctx->frame_queue; *replaced = NULL; - while(!*frame_queue->quit) { + while(!webm_stop_is_requested(frame_queue->quit)) { int index; int video_packet_count = queue_get_status( video_ctx->packet_queue, @@ -673,7 +790,7 @@ static int video_frame_queue_insert( ); mutex_lock(frame_queue->mutex); - if(*frame_queue->quit) { + if(webm_stop_is_requested(frame_queue->quit)) { mutex_unlock(frame_queue->mutex); return -1; } @@ -722,7 +839,7 @@ static int bgm_update_thread(void *data) { webm_context *ctx = data; - while (!ctx->quit) + while(!webm_stop_is_requested(&ctx->quit)) { sound_update_music(); usleep(5000); @@ -946,7 +1063,7 @@ static int audio_thread(void *data) /* Priority elevation may be denied on restricted platforms. */ SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); - while(!*audio_ctx->quit) + while(!webm_stop_is_requested(audio_ctx->quit)) { decoded_bytes = audio_decode_frame( audio_ctx, @@ -986,7 +1103,8 @@ static int audio_thread(void *data) if(queue_result == 0) { usleep(retry_delay_microseconds); } - } while(!*audio_ctx->quit && queue_result == 0); + } while(!webm_stop_is_requested(audio_ctx->quit) && + queue_result == 0); if(queue_result > 0 && underrun_count != audio_ctx->observed_underrun_count) { @@ -1076,16 +1194,19 @@ static int audio_thread(void *data) * unusually slow inputs while ordinary playback begins with continuous * audio already available to the mixer. */ -static void audio_wait_for_preroll(audio_context *audio_ctx) +static void audio_wait_for_preroll(webm_context *ctx) { + audio_context *audio_ctx; uint64_t start_time; - if(!audio_ctx || !audio_ctx->quit) { + if(!ctx || !ctx->audio_initialized) { return; } + audio_ctx = &ctx->audio_ctx; start_time = timer_uticks(); - while(!*audio_ctx->quit) { + while(!webm_stop_is_requested(audio_ctx->quit) && + !webm_close_is_requested(ctx)) { s_sound_pcm_stream_status status; uint64_t now; @@ -1126,7 +1247,7 @@ static int init_audio( int sound_channel, uint64_t seek_timestamp, int replace_all_audio, - volatile int *quit + SDL_atomic_t *quit ) { // read vorbis header and initialize vorbis decoding @@ -1301,14 +1422,14 @@ static int video_thread(void *data) uint64_t frame_delay; uint64_t timestamp; - while(!*ctx->quit) + while(!webm_stop_is_requested(ctx->quit)) { unsigned int chunk, chunks; nestegg_packet *pkt; debug_printf("video queue size=%i\n", ctx->packet_queue->size); pkt = queue_get(ctx->packet_queue); - if (*ctx->quit || pkt == NULL) break; + if(webm_stop_is_requested(ctx->quit) || pkt == NULL) break; nestegg_packet_count(pkt, &chunks); nestegg_packet_tstamp(pkt, ×tamp); frame_delay = video_packet_frame_delay(ctx, pkt, chunks); @@ -1324,7 +1445,7 @@ static int video_thread(void *data) if (vpx_codec_decode(&ctx->vpx_ctx, data, data_size, NULL, 0)) { printf("Error: libvpx failed to decode chunk\n"); - *ctx->quit = 1; + webm_request_decoder_stop(ctx->quit); break; } while((img = vpx_codec_get_frame(&ctx->vpx_ctx, &iter))) @@ -1334,13 +1455,13 @@ static int video_thread(void *data) if(img->d_w != (unsigned int)ctx->width || img->d_h != (unsigned int)ctx->height) { printf("Error: WebM frame dimensions changed during playback\n"); - *ctx->quit = 1; + webm_request_decoder_stop(ctx->quit); break; } yuv_frame *frame = yuv_frame_create(img->d_w, img->d_h); if(!frame) { printf("Error: Unable to allocate a decoded WebM frame\n"); - *ctx->quit = 1; + webm_request_decoder_stop(ctx->quit); break; } frame->timestamp = timestamp; @@ -1369,7 +1490,7 @@ static int video_thread(void *data) ? timestamp + frame_delay : UINT64_MAX; } - if(*ctx->quit) break; + if(webm_stop_is_requested(ctx->quit)) break; } nestegg_free_packet(pkt); } @@ -1443,7 +1564,7 @@ static int init_video( nestegg *nestegg_ctx, int track, video_context *video_ctx, - volatile int *quit + SDL_atomic_t *quit ) { nestegg_video_params video_params; @@ -1627,7 +1748,7 @@ static int demux_thread(void *data) video_packet_queue_report_resync( ctx->video_ctx.packet_queue ); - if(ctx->quit) break; + if(webm_stop_is_requested(&ctx->quit)) break; continue; } if(insert_result < 0) { @@ -1639,7 +1760,7 @@ static int demux_thread(void *data) ctx->video_ctx.packet_queue ); nestegg_free_packet(pkt); - if(ctx->quit) break; + if(webm_stop_is_requested(&ctx->quit)) break; continue; } insert_result = video_packet_queue_insert(ctx, pkt); @@ -1661,7 +1782,7 @@ static int demux_thread(void *data) video_packet_queue_report_resync( ctx->video_ctx.packet_queue ); - if(ctx->quit) break; + if(webm_stop_is_requested(&ctx->quit)) break; continue; } video_packet_queue_record_drop( @@ -1669,7 +1790,7 @@ static int demux_thread(void *data) ); nestegg_free_packet(pkt); discard_video_until_keyframe = 1; - if(ctx->quit) break; + if(webm_stop_is_requested(&ctx->quit)) break; continue; } } @@ -1678,7 +1799,7 @@ static int demux_thread(void *data) nestegg_free_packet(pkt); } - if (ctx->quit) break; + if(webm_stop_is_requested(&ctx->quit)) break; } if(discard_video_until_keyframe) { video_packet_queue_report_resync(ctx->video_ctx.packet_queue); @@ -1690,62 +1811,106 @@ static int demux_thread(void *data) /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Open one independently owned decoder. Optional shared cache, -* initial seek, and sound routing are established before its worker -* threads become visible. Reverse contexts omit the audio track so -* paused producer queues cannot back-pressure video demuxing. +* Publish an asynchronous lifecycle stage unless teardown was requested. +* Cancellation also raises the decoder stop flag so in-progress preroll and +* decoder workers leave their bounded waits without engine-thread joins. */ -webm_context *webm_start_playback_ex( - const char *path, - int volume, - int sound_channel, - const unsigned char *cache_buffer, - size_t cache_size, - uint64_t seek_timestamp, - int play_audio, - int replace_all_audio +static int webm_publish_decoder_state( + webm_context *ctx, + e_webm_decoder_state state ) { - webm_context *ctx; - nestegg_io io; - int video_track = -1, audio_track = -1; - unsigned int num_tracks, i; + int accepted; - if(!path || !path[0] || - sound_channel < 0 || - (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX) { - printf("Error: Invalid WebM playback request\n"); - return NULL; + mutex_lock(ctx->lifecycle_mutex); + accepted = !ctx->close_requested; + if(accepted) { + ctx->decoder_state = state; + } + else { + ctx->decoder_state = WEBM_DECODER_STATE_CLOSING; + webm_request_decoder_stop(&ctx->quit); } + mutex_unlock(ctx->lifecycle_mutex); + return accepted; +} - ctx = calloc(1, sizeof(*ctx)); - if(!ctx) { - printf("Error: Unable to allocate WebM playback context\n"); - return NULL; +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Join decoder workers and release every resource opened by the lifecycle +* thread. Initialization flags make the same path safe for partial opens, +* failed seeks, ordinary stops, and engine shutdown. +*/ +static void webm_close_resources(webm_context *ctx) +{ + webm_request_decoder_stop(&ctx->quit); + if(ctx->the_demux_thread) { + thread_join(ctx->the_demux_thread); + ctx->the_demux_thread = NULL; } - ctx->io_ctx.packhandle = -1; - ctx->audio_track = -1; - ctx->video_track = -1; + if(ctx->the_video_thread) { + thread_join(ctx->the_video_thread); + ctx->the_video_thread = NULL; + } + if(ctx->the_audio_thread) { + thread_join(ctx->the_audio_thread); + ctx->the_audio_thread = NULL; + } + if(ctx->video_initialized) { + close_video(&ctx->video_ctx); + ctx->video_initialized = 0; + } + if(ctx->audio_initialized) { + close_audio(&ctx->audio_ctx); + ctx->audio_initialized = 0; + } + if(ctx->nestegg_ctx) { + nestegg_destroy(ctx->nestegg_ctx); + ctx->nestegg_ctx = NULL; + } + if(ctx->io_ctx.packhandle >= 0) { + closepackfile(ctx->io_ctx.packhandle); + ctx->io_ctx.packhandle = -1; + } +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Perform container open, initial seek, and decoder creation on the lifecycle +* worker, then publish PREROLLING when audio must establish its PCM reserve. +* Reverse playback and ordinary seeks use the same decoder path as initial +* playback. +*/ +static int webm_open_resources(webm_context *ctx) +{ + nestegg_io io; + int video_track = -1; + int audio_track = -1; + unsigned int num_tracks; + unsigned int i; io.read = webm_io_read; io.seek = webm_io_seek; io.tell = webm_io_tell; io.userdata = &ctx->io_ctx; - if(cache_buffer) { - if(!cache_size) { - printf("Error: Cached WebM source %s has no data\n", path); - goto error_io; + if(ctx->io_ctx.cache_buffer) { + if(!ctx->io_ctx.cache_size) { + printf("Error: Cached WebM source %s has no data\n", ctx->path); + return -1; } - ctx->io_ctx.cache_buffer = cache_buffer; - ctx->io_ctx.cache_size = cache_size; - } else { - ctx->io_ctx.packhandle = openpackfile(path, packfile); + } + else { + ctx->io_ctx.packhandle = openpackfile(ctx->path, packfile); if(ctx->io_ctx.packhandle < 0) { - printf("Error: Unable to open file %s for playback\n", path); - goto error_io; + printf("Error: Unable to open file %s for playback\n", ctx->path); + return -1; } ctx->io_ctx.stream_size = seekpackfile64( ctx->io_ctx.packhandle, @@ -1754,135 +1919,370 @@ webm_context *webm_start_playback_ex( ); if(ctx->io_ctx.stream_size <= 0 || seekpackfile64(ctx->io_ctx.packhandle, 0, SEEK_SET) != 0) { - printf("Error: Unable to size WebM stream %s\n", path); - goto error_io; + printf("Error: Unable to size WebM stream %s\n", ctx->path); + return -1; } } - if(nestegg_init(&(ctx->nestegg_ctx), io, NULL, -1) < 0) { - printf("Error: Unable to initialize WebM container %s\n", path); - goto error_io; + if(!webm_publish_decoder_state( + ctx, + WEBM_DECODER_STATE_OPENING + )) { + return -1; + } + if(nestegg_init(&ctx->nestegg_ctx, io, NULL, -1) < 0) { + printf("Error: Unable to initialize WebM container %s\n", ctx->path); + return -1; } nestegg_duration(ctx->nestegg_ctx, &ctx->duration); if(nestegg_track_count(ctx->nestegg_ctx, &num_tracks) < 0) { - printf("Error: Unable to read WebM tracks from %s\n", path); - goto error_nestegg; + printf("Error: Unable to read WebM tracks from %s\n", ctx->path); + return -1; } - for(i = 0; i < num_tracks; i++) { + for(i = 0; i < num_tracks; ++i) { int track_type = nestegg_track_type(ctx->nestegg_ctx, i); int codec = nestegg_track_codec_id(ctx->nestegg_ctx, i); if(track_type == NESTEGG_TRACK_VIDEO && video_track < 0) { if(codec != NESTEGG_CODEC_VP8) { printf("Error: unsupported video codec; only VP8 is supported\n"); - goto error_nestegg; + return -1; } video_track = (int)i; - } else if(track_type == NESTEGG_TRACK_AUDIO && audio_track < 0) { + } + else if(track_type == NESTEGG_TRACK_AUDIO && audio_track < 0) { if(codec != NESTEGG_CODEC_VORBIS) { printf("Error: unsupported audio codec; only Vorbis is supported\n"); - goto error_nestegg; + return -1; } audio_track = (int)i; } } - if(video_track < 0) { - printf("Error: WebM file %s does not contain a video track\n", path); - goto error_nestegg; + printf("Error: WebM file %s does not contain a video track\n", ctx->path); + return -1; } - if(ctx->duration && seek_timestamp >= ctx->duration) { - seek_timestamp = ctx->duration - 1; + if(!webm_publish_decoder_state( + ctx, + WEBM_DECODER_STATE_SEEKING + )) { + return -1; + } + if(ctx->duration && ctx->seek_timestamp >= ctx->duration) { + ctx->seek_timestamp = ctx->duration - 1U; + } + if(ctx->seek_timestamp && + nestegg_track_seek( + ctx->nestegg_ctx, + (unsigned int)video_track, + ctx->seek_timestamp + ) < 0) { + printf("Error: Unable to seek WebM file %s\n", ctx->path); + return -1; } - if(seek_timestamp && - nestegg_track_seek(ctx->nestegg_ctx, (unsigned int)video_track, seek_timestamp) < 0) { - printf("Error: Unable to seek WebM file %s\n", path); - goto error_nestegg; + if(!webm_publish_decoder_state( + ctx, + WEBM_DECODER_STATE_OPENING + )) { + return -1; } ctx->video_track = video_track; if(init_video( ctx->nestegg_ctx, ctx->video_track, - &(ctx->video_ctx), + &ctx->video_ctx, &ctx->quit ) < 0) { - goto error_nestegg; + return -1; } - ctx->the_video_thread = thread_create(video_thread, "video", &(ctx->video_ctx)); + ctx->video_initialized = 1; + ctx->the_video_thread = thread_create( + video_thread, + "video", + &ctx->video_ctx + ); if(!ctx->the_video_thread) { printf("Error: Unable to start WebM video decoder thread\n"); - webm_close(ctx); - return NULL; + return -1; } - if(audio_track >= 0 && play_audio) { + if(audio_track >= 0 && ctx->play_audio) { ctx->audio_track = audio_track; if(init_audio( ctx->nestegg_ctx, ctx->audio_track, - &(ctx->audio_ctx), - volume, - sound_channel, - seek_timestamp, - replace_all_audio, + &ctx->audio_ctx, + ctx->volume, + ctx->sound_channel, + ctx->seek_timestamp, + ctx->replace_all_audio, &ctx->quit ) == 0) { + ctx->audio_initialized = 1; ctx->audio_ctx.video_packet_queue = ctx->video_ctx.packet_queue; ctx->audio_ctx.video_frame_queue = ctx->video_ctx.frame_queue; - ctx->the_audio_thread = thread_create(audio_thread, "audio", &(ctx->audio_ctx)); + ctx->the_audio_thread = thread_create( + audio_thread, + "audio", + &ctx->audio_ctx + ); if(!ctx->the_audio_thread) { printf("Error: Unable to start WebM audio decoder thread\n"); - webm_close(ctx); - return NULL; + return -1; } - } else { - printf("Warning: Unable to open the WebM audio track on channel %d\n", sound_channel); + } + else { + printf( + "Warning: Unable to open the WebM audio track on channel %d\n", + ctx->sound_channel + ); ctx->audio_track = -1; } - } else if(audio_track < 0 && - replace_all_audio && - sound_query_music(NULL, NULL)) { - /* Blocking legacy playback must service existing channel zero audio. */ + } + else if(audio_track < 0 && + ctx->replace_all_audio && + sound_query_music(NULL, NULL)) { + /* Blocking legacy playback retains its channel-zero service. */ ctx->the_audio_thread = thread_create(bgm_update_thread, "bgm", ctx); if(!ctx->the_audio_thread) { printf("Error: Unable to start WebM legacy audio service thread\n"); - webm_close(ctx); - return NULL; + return -1; } } ctx->the_demux_thread = thread_create(demux_thread, "demux", ctx); if(!ctx->the_demux_thread) { printf("Error: Unable to start WebM demux thread\n"); - webm_close(ctx); - return NULL; + return -1; } if(ctx->audio_track >= 0) { - audio_wait_for_preroll(&ctx->audio_ctx); + if(!webm_publish_decoder_state( + ctx, + WEBM_DECODER_STATE_PREROLLING + )) { + return -1; + } + } + return webm_stop_is_requested(&ctx->quit) ? -1 : 0; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Own the decoder lifecycle after the constructor returns. Successful opens +* wait in READY until an asynchronous close request arrives; failed opens +* remain observable as FAILED until the movie layer retires the context. +*/ +static int webm_lifecycle_thread(void *data) +{ + webm_context *ctx = data; + int open_result; + + mutex_lock(webm_lifecycle_operation_mutex); + open_result = webm_open_resources(ctx); + mutex_unlock(webm_lifecycle_operation_mutex); + if(open_result == 0 && ctx->audio_track >= 0) { + audio_wait_for_preroll(ctx); + if(webm_stop_is_requested(&ctx->quit) || + webm_close_is_requested(ctx)) { + open_result = -1; + } + } + if(open_result < 0) { + mutex_lock(webm_lifecycle_operation_mutex); + webm_close_resources(ctx); + mutex_unlock(webm_lifecycle_operation_mutex); + } + + mutex_lock(ctx->lifecycle_mutex); + if(ctx->close_requested) { + ctx->decoder_state = WEBM_DECODER_STATE_CLOSING; + } + else { + ctx->decoder_state = open_result == 0 + ? WEBM_DECODER_STATE_READY + : WEBM_DECODER_STATE_FAILED; + } + cond_broadcast(ctx->lifecycle_condition); + while(!ctx->close_requested) { + cond_wait(ctx->lifecycle_condition, ctx->lifecycle_mutex); + } + ctx->decoder_state = WEBM_DECODER_STATE_CLOSING; + webm_request_decoder_stop(&ctx->quit); + mutex_unlock(ctx->lifecycle_mutex); + + if(open_result == 0) { + mutex_lock(webm_lifecycle_operation_mutex); + webm_close_resources(ctx); + mutex_unlock(webm_lifecycle_operation_mutex); + } + + mutex_lock(ctx->lifecycle_mutex); + ctx->decoder_state = WEBM_DECODER_STATE_CLOSED; + cond_broadcast(ctx->lifecycle_condition); + mutex_unlock(ctx->lifecycle_mutex); + return 0; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Allocate an independently owned decoder request and return as soon as its +* lifecycle worker starts. Container I/O, seeking, codec initialization, and +* audio preroll continue asynchronously behind the published state machine. +*/ +webm_context *webm_start_playback_ex( + const char *path, + int volume, + int sound_channel, + const unsigned char *cache_buffer, + size_t cache_size, + uint64_t seek_timestamp, + int play_audio, + int replace_all_audio +) +{ + webm_context *ctx; + + if(!path || !path[0] || + sound_channel < 0 || + (unsigned int)sound_channel >= SOUND_CHANNEL_COUNT_MAX || + (cache_buffer && !cache_size)) { + printf("Error: Invalid WebM playback request\n"); + return NULL; + } + if(!webm_lifecycle_init()) { + printf("Error: Unable to initialize WebM lifecycle synchronization\n"); + return NULL; + } + + ctx = calloc(1, sizeof(*ctx)); + if(!ctx) { + printf("Error: Unable to allocate WebM playback context\n"); + return NULL; + } + ctx->path = webm_copy_path(path); + ctx->lifecycle_mutex = mutex_create(); + ctx->lifecycle_condition = cond_create(); + if(!ctx->path || !ctx->lifecycle_mutex || !ctx->lifecycle_condition) { + if(ctx->lifecycle_condition) { + cond_destroy(ctx->lifecycle_condition); + } + if(ctx->lifecycle_mutex) { + mutex_destroy(ctx->lifecycle_mutex); + } + free(ctx->path); + free(ctx); + return NULL; + } + + ctx->io_ctx.packhandle = -1; + ctx->io_ctx.cache_buffer = cache_buffer; + ctx->io_ctx.cache_size = cache_size; + ctx->audio_track = -1; + ctx->video_track = -1; + ctx->seek_timestamp = seek_timestamp; + ctx->volume = volume; + ctx->sound_channel = sound_channel; + ctx->play_audio = play_audio != 0; + ctx->replace_all_audio = replace_all_audio != 0; + ctx->decoder_state = WEBM_DECODER_STATE_OPENING; + SDL_AtomicSet(&ctx->quit, 0); + ctx->the_lifecycle_thread = thread_create( + webm_lifecycle_thread, + "webm-lifecycle", + ctx + ); + if(!ctx->the_lifecycle_thread) { + cond_destroy(ctx->lifecycle_condition); + mutex_destroy(ctx->lifecycle_mutex); + free(ctx->path); + free(ctx); + return NULL; } return ctx; +} -error_nestegg: - nestegg_destroy(ctx->nestegg_ctx); -error_io: - if(ctx->io_ctx.packhandle >= 0) { - closepackfile(ctx->io_ctx.packhandle); +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Snapshot one decoder lifecycle state under its owner mutex so the movie +* layer can advance transitions without reading partially published state. +*/ +e_webm_decoder_state webm_get_decoder_state(webm_context *ctx) +{ + e_webm_decoder_state state; + + if(!ctx) { + return WEBM_DECODER_STATE_FAILED; } + mutex_lock(ctx->lifecycle_mutex); + state = ctx->decoder_state; + mutex_unlock(ctx->lifecycle_mutex); + return state; +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Request decoder cancellation without waiting for lifecycle, demux, audio, +* or video workers. The owning movie channel later reaps the context after +* CLOSED is observable. +*/ +void webm_request_close(webm_context *ctx) +{ + if(!ctx) { + return; + } + + mutex_lock(ctx->lifecycle_mutex); + if(ctx->decoder_state != WEBM_DECODER_STATE_CLOSED) { + ctx->close_requested = 1; + ctx->decoder_state = WEBM_DECODER_STATE_CLOSING; + cond_broadcast(ctx->lifecycle_condition); + } + mutex_unlock(ctx->lifecycle_mutex); +} + +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Reap one decoder only after asynchronous teardown is complete. CLOSED is +* published at the lifecycle worker's exit boundary, keeping this poll free +* of container I/O and decoder-thread waits during normal engine updates. +*/ +int webm_poll_closed(webm_context *ctx) +{ + if(!ctx || + webm_get_decoder_state(ctx) != WEBM_DECODER_STATE_CLOSED) { + return 0; + } + + thread_join(ctx->the_lifecycle_thread); + cond_destroy(ctx->lifecycle_condition); + mutex_destroy(ctx->lifecycle_mutex); + free(ctx->path); free(ctx); - return NULL; + return 1; } /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-17 * -* Stop one decoder without signaling any other WebM -* object, then release its queues, codec, and I/O source. +* Provide a blocking finalizer only for engine shutdown and legacy ownership +* boundaries. Runtime movie stop and replacement use request-plus-poll so +* teardown never joins decoder workers on an engine update. */ void webm_close(webm_context *ctx) { @@ -1890,21 +2290,20 @@ void webm_close(webm_context *ctx) return; } - ctx->quit = 1; - if(ctx->the_demux_thread) thread_join(ctx->the_demux_thread); - if(ctx->the_video_thread) thread_join(ctx->the_video_thread); - if(ctx->the_audio_thread) thread_join(ctx->the_audio_thread); - close_video(&(ctx->video_ctx)); - if(ctx->audio_track >= 0) close_audio(&(ctx->audio_ctx)); - if(ctx->nestegg_ctx) nestegg_destroy(ctx->nestegg_ctx); - if(ctx->io_ctx.packhandle >= 0) closepackfile(ctx->io_ctx.packhandle); + webm_request_close(ctx); + thread_join(ctx->the_lifecycle_thread); + cond_destroy(ctx->lifecycle_condition); + mutex_destroy(ctx->lifecycle_mutex); + free(ctx->path); free(ctx); } void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims) { - assert(ctx); - assert(dims); + if(!ctx || !dims || + webm_get_decoder_state(ctx) != WEBM_DECODER_STATE_READY) { + return; + } dims->width = ctx->video_ctx.width; dims->height = ctx->video_ctx.height; dims->display_width = ctx->video_ctx.display_width; @@ -1919,7 +2318,10 @@ void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims) */ uint64_t webm_get_duration(webm_context *ctx) { - return ctx ? ctx->duration : 0; + return ctx && + webm_get_decoder_state(ctx) == WEBM_DECODER_STATE_READY + ? ctx->duration + : 0; } /* @@ -1939,7 +2341,9 @@ int webm_get_audio_playback_position( uint64_t playback_frame; uint64_t elapsed; - if(!ctx || !position || ctx->audio_track < 0) { + if(!ctx || !position || + webm_get_decoder_state(ctx) != WEBM_DECODER_STATE_READY || + ctx->audio_track < 0) { return 0; } @@ -1967,17 +2371,18 @@ int webm_get_audio_playback_position( /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * -* Apply pause state to decoder backpressure and audio output. Initial PCM -* preroll is completed synchronously before the movie clock begins, so -* ordinary frame updates never need to poll stream status. +* Apply pause state to decoder backpressure and audio output only after the +* asynchronous lifecycle publishes READY. Pending requests retain their +* desired pause state in the owning movie record until that transition. */ void webm_set_audio_paused(webm_context *ctx, int paused) { audio_context *audio_ctx; - if(!ctx) { + if(!ctx || + webm_get_decoder_state(ctx) != WEBM_DECODER_STATE_READY) { return; } paused = paused != 0; @@ -2006,7 +2411,9 @@ void webm_set_audio_paused(webm_context *ctx, int paused) void webm_set_audio_speed(webm_context *ctx, double speed) { - if(ctx && ctx->audio_track >= 0) { + if(ctx && + webm_get_decoder_state(ctx) == WEBM_DECODER_STATE_READY && + ctx->audio_track >= 0) { sound_set_channel_speed_owned( ctx->audio_ctx.channel, ctx->audio_ctx.stream_play_id, @@ -2027,7 +2434,8 @@ int webm_get_pending_frame_count(webm_context *ctx) { int count; - if(!ctx) { + if(!ctx || + webm_get_decoder_state(ctx) != WEBM_DECODER_STATE_READY) { return 0; } count = queue_get_status( @@ -2046,13 +2454,22 @@ int webm_get_pending_frame_count(webm_context *ctx) */ int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame) { + e_webm_decoder_state state; void *queued_frame; if(!ctx || !frame) { return -1; } + state = webm_get_decoder_state(ctx); + if(state != WEBM_DECODER_STATE_READY) { + return state == WEBM_DECODER_STATE_FAILED || + state == WEBM_DECODER_STATE_CLOSING || + state == WEBM_DECODER_STATE_CLOSED + ? -1 + : 0; + } if(!queue_try_get(ctx->video_ctx.frame_queue, &queued_frame)) { - return ctx->quit ? -1 : 0; + return webm_stop_is_requested(&ctx->quit) ? -1 : 0; } *frame = queued_frame; diff --git a/engine/source/webmlib/vidplay.h b/engine/source/webmlib/vidplay.h index 8046493f4..cabd35d8e 100644 --- a/engine/source/webmlib/vidplay.h +++ b/engine/source/webmlib/vidplay.h @@ -17,6 +17,26 @@ struct webm_context; typedef struct webm_context webm_context; +/* +* Caskey, Damon V. +* 2026-08-17 +* +* Describe each externally observable stage of the asynchronous WebM +* decoder lifecycle. Callers poll these states without joining decoder +* workers or performing container I/O on the engine thread. +*/ +typedef enum e_webm_decoder_state { + WEBM_DECODER_STATE_OPENING, + WEBM_DECODER_STATE_SEEKING, + WEBM_DECODER_STATE_PREROLLING, + WEBM_DECODER_STATE_READY, + WEBM_DECODER_STATE_FAILED, + WEBM_DECODER_STATE_CLOSING, + WEBM_DECODER_STATE_CLOSED +} e_webm_decoder_state; + +int webm_lifecycle_init(void); +void webm_lifecycle_shutdown(void); webm_context *webm_start_playback_ex( const char *path, int volume, @@ -27,6 +47,7 @@ webm_context *webm_start_playback_ex( int play_audio, int replace_all_audio ); +e_webm_decoder_state webm_get_decoder_state(webm_context *ctx); void webm_get_video_info(webm_context *ctx, yuv_video_mode *dims); uint64_t webm_get_duration(webm_context *ctx); int webm_get_audio_playback_position( @@ -35,6 +56,8 @@ int webm_get_audio_playback_position( ); void webm_set_audio_paused(webm_context *ctx, int paused); void webm_set_audio_speed(webm_context *ctx, double speed); +void webm_request_close(webm_context *ctx); +int webm_poll_closed(webm_context *ctx); void webm_close(webm_context *ctx); int webm_get_pending_frame_count(webm_context *ctx); int webm_try_get_next_frame(webm_context *ctx, yuv_frame **frame); From 455c6240ad32a0e050b56c24b1df79e7ec10db19 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Mon, 17 Aug 2026 14:17:26 -0400 Subject: [PATCH 20/26] Disable reverse movie playback --- engine/source/webmlib/movie_playback.c | 89 ++++++-------------------- engine/source/webmlib/movie_playback.h | 3 +- engine/source/webmlib/vidplay.c | 4 +- 3 files changed, 21 insertions(+), 75 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 13c6e0dca..9ede4bca5 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -36,7 +36,6 @@ static void *movie_cache_allocate_nonfatal(size_t size) #include "movie_playback.h" #define MOVIE_SOURCE_CAPACITY_INITIAL 64U -#define MOVIE_REVERSE_SEEK_INTERVAL UINT64_C(33333333) #define MOVIE_CACHE_READ_CHUNK_SIZE (1024U * 1024U) #define MOVIE_CACHE_RESERVE_MINIMUM (UINT64_C(128) * UINT64_C(1024) * UINT64_C(1024)) #define MOVIE_SPEED_ROUNDING_FACTOR 1000.0 @@ -100,12 +99,12 @@ static s_movie_playback_pool movie_playback_pool; /* * Caskey, Damon V. -* 2026-08-13 +* 2026-08-17 * -* Convert creator-supplied playback speed to one canonical float. -* Clamp supported range and round to thousandths so video timing, -* audio resampling, property reads, and later decoder reopens all -* consume the same applied rate. +* Convert creator-supplied forward playback speed to one canonical float. +* Reject negative rates because backward navigation is provided by explicit +* seeks, then clamp the upper range and round to thousandths so video timing, +* audio resampling, property reads, and decoder reopens share one rate. */ static bool movie_playback_sanitize_speed( double requested_speed, @@ -117,8 +116,9 @@ static bool movie_playback_sanitize_speed( } if(requested_speed < MOVIE_SPEED_MIN) { - requested_speed = MOVIE_SPEED_MIN; - } else if(requested_speed > MOVIE_SPEED_MAX) { + return false; + } + if(requested_speed > MOVIE_SPEED_MAX) { requested_speed = MOVIE_SPEED_MAX; } requested_speed = round( @@ -827,8 +827,7 @@ static int movie_playback_decoder_ready( * * Use consumed PCM time as the master during forward playback. Each * successful owned-channel snapshot also reanchors the monotonic fallback, -* preserving continuity for silent media, reverse playback, and replaced -* audio channels. +* preserving continuity for silent media and replaced audio channels. */ static uint64_t movie_playback_position_now( s_movie_playback *playback, @@ -968,7 +967,6 @@ static bool movie_playback_begin_media( playback->volume = volume; playback->position_anchor = position; playback->position = position / UINT64_C(1000000); - playback->reverse_pending = playback->speed < 0.0; playback->terminal_pending = 0; playback->failed = 0; return movie_playback_start_pending_decoder(playback) >= 0; @@ -1046,7 +1044,6 @@ static int movie_playback_poll_decoder(s_movie_playback *playback) playback->clock_anchor = timer_uticks(); playback->position = playback->position_anchor / UINT64_C(1000000); - playback->reverse_pending = playback->speed < 0.0; playback->terminal_pending = 0; decoder->ready = 1; @@ -1055,7 +1052,7 @@ static int movie_playback_poll_decoder(s_movie_playback *playback) } webm_set_audio_paused( decoder->context, - playback->paused || playback->speed <= 0.0 + playback->paused || playback->speed == 0.0 ); return 1; } @@ -1360,7 +1357,7 @@ static bool movie_playback_render_to( * Poll only the decoded frames present when this update begins. A decoder * producing stale frames must not refill the queue faster than the main thread * can empty it and turn one nonblocking poll into an unbounded engine stall. -* Reverse seeks retain pending state until reaching the requested timestamp. +* Explicit seeks retain the previous frame until replacement becomes ready. */ static void movie_playback_poll_frames( s_movie_playback *playback, @@ -1390,17 +1387,13 @@ static void movie_playback_poll_frames( } if(playback->next_frame->timestamp > position) { - if((!playback->current_frame || playback->reverse_pending) && - !promoted) { + if(!playback->current_frame && !promoted) { yuv_frame_destroy(playback->current_frame); playback->current_frame = playback->next_frame; playback->next_frame = NULL; playback->frame_dirty = 1; promoted = 1; } - if(playback->reverse_pending) { - playback->reverse_pending = 0; - } break; } @@ -1410,15 +1403,6 @@ static void movie_playback_poll_frames( playback->frame_dirty = 1; promoted = 1; - if(playback->speed < 0.0 && - playback->current_frame->timestamp == position) { - playback->reverse_pending = 0; - break; - } - } - - if(*terminal && playback->reverse_pending) { - playback->reverse_pending = 0; } } @@ -1428,7 +1412,7 @@ static void movie_playback_poll_frames( * * Advance playback clocks and decoder state independently from composition. * Lifecycle polling keeps open, seek, preroll, and teardown work off the -* engine thread while reverse rates periodically request forward decodes. +* engine thread. Backward navigation remains an explicit seek operation. */ void movie_playback_update(int interrupt_requested) { @@ -1487,48 +1471,12 @@ void movie_playback_update(int interrupt_requested) playback->position = playback->duration; } - if(playback->speed < 0.0 && - !playback->paused && - !playback->reverse_pending && - playback->position_anchor >= position && - playback->position_anchor - position >= - MOVIE_REVERSE_SEEK_INTERVAL) { - if(!movie_playback_begin_media( - playback, - position, - playback->volume - )) { - movie_playback_stop(playback); - continue; - } - } - if((playback->speed > 0.0 && !playback->paused) || - !playback->current_frame || - playback->reverse_pending) { + !playback->current_frame) { movie_playback_poll_frames(playback, position, &terminal); } - if(playback->speed < 0.0 && - position == 0 && - !playback->reverse_pending) { - if(playback->repeat && playback->duration) { - uint64_t repeat_position = - (playback->duration - 1U) * UINT64_C(1000000); - if(!movie_playback_begin_media( - playback, - repeat_position, - playback->volume - )) { - movie_playback_stop(playback); - } - } else { - movie_playback_stop(playback); - } - continue; - } - - if(terminal && playback->speed >= 0.0) { + if(terminal) { if(playback->frame_dirty) { playback->terminal_pending = 1; } else if(playback->repeat) { @@ -1662,7 +1610,7 @@ bool movie_playback_set_paused(s_movie_playback *playback, int paused) if(movie_playback_decoder_ready(playback)) { webm_set_audio_paused( playback->context, - paused || playback->speed <= 0.0 + paused || playback->speed == 0.0 ); } return true; @@ -1754,7 +1702,7 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) playback->speed = applied_speed; playback->position = position / UINT64_C(1000000); - if(playback->speed > 0.0 && previous_speed <= 0.0) { + if(playback->speed > 0.0 && previous_speed == 0.0) { if(!movie_playback_begin_media(playback, position, playback->volume)) { movie_playback_stop(playback); return false; @@ -1763,8 +1711,7 @@ bool movie_playback_set_speed(s_movie_playback *playback, double speed) movie_playback_decoder_ready(playback)) { webm_set_audio_speed(playback->context, playback->speed); webm_set_audio_paused(playback->context, playback->paused); - } else if(playback->speed <= 0.0) { - playback->reverse_pending = 0; + } else if(playback->speed == 0.0) { playback->terminal_pending = 0; if(movie_playback_decoder_ready(playback)) { webm_set_audio_paused(playback->context, 1); diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index e4e830a07..776bd62f4 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -17,7 +17,7 @@ #define MOVIE_CHANNEL_COUNT 64U #define MOVIE_CHANNEL_AUTO (-1) -#define MOVIE_SPEED_MIN (-16.0) +#define MOVIE_SPEED_MIN 0.0 #define MOVIE_SPEED_MAX 16.0 #define MOVIE_SIZE_NATIVE UINT64_MAX @@ -69,7 +69,6 @@ typedef struct s_movie_playback { int frame_dirty; int rgb_frame_dirty; int scaled_frame_dirty; - int reverse_pending; int terminal_pending; } s_movie_playback; diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index e2c132160..3a3d3f420 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -1884,8 +1884,7 @@ static void webm_close_resources(webm_context *ctx) * * Perform container open, initial seek, and decoder creation on the lifecycle * worker, then publish PREROLLING when audio must establish its PCM reserve. -* Reverse playback and ordinary seeks use the same decoder path as initial -* playback. +* Explicit seeks use the same decoder path as initial playback. */ static int webm_open_resources(webm_context *ctx) { @@ -2412,6 +2411,7 @@ void webm_set_audio_paused(webm_context *ctx, int paused) void webm_set_audio_speed(webm_context *ctx, double speed) { if(ctx && + speed > 0.0 && webm_get_decoder_state(ctx) == WEBM_DECODER_STATE_READY && ctx->audio_track >= 0) { sound_set_channel_speed_owned( From ae48540dde90fc77aab5f5a36f9cc2552ff12d8f Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Mon, 17 Aug 2026 14:26:48 -0400 Subject: [PATCH 21/26] Tune WebM decoder reserves --- engine/source/webmlib/vidplay.c | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 3a3d3f420..39aa407cc 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -34,20 +34,25 @@ /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * -* Keep enough compressed packets ahead of both decoders that a full -* video queue does not stop the shared demuxer before the audio packet -* reserve fills. Video needs more packet slots because its packet rate -* may greatly exceed the audio rate. Queue allocation stores pointers; -* PCM output remains separately bounded by the generic stream ring. +* Balance decoder reserves after the playback clock, backlog recovery, and +* asynchronous lifecycle are stable. Sixty-four audio packets cover ordinary +* Vorbis scheduling jitter. One hundred twenty-eight video packets retain +* roughly two seconds at 60 FPS without preserving the many seconds of stale +* compressed work held by the former 512-packet queue. Six decoded YUV frames +* provide 100-200 milliseconds of read-ahead across 30-60 FPS while bounding +* the software renderer's per-movie frame memory. Demux and audio producers +* retain high priority because both block frequently; VP8 decoding remains at +* normal priority so it cannot starve the engine. The proven eight-buffer +* generic PCM ring remains unchanged. */ #define AUDIO_PACKET_QUEUE_SIZE 64 -#define VIDEO_PACKET_QUEUE_SIZE 512 -#define FRAME_QUEUE_SIZE 10 +#define VIDEO_PACKET_QUEUE_SIZE 128 +#define FRAME_QUEUE_SIZE 6 #define VIDEO_DECODER_THREAD_MAX 8U #define VIDEO_PACKET_QUEUE_BACKPRESSURE_THRESHOLD \ - ((VIDEO_PACKET_QUEUE_SIZE * 3) / 4) + (VIDEO_PACKET_QUEUE_SIZE / 2) #define AUDIO_RECOVERY_PCM_BUFFER_COUNT \ (SOUND_STREAM_BUFFER_COUNT / 2U) #define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) @@ -758,10 +763,10 @@ static int video_packet_queue_insert( /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-17 * * Publish decoded frames in timestamp order during ordinary read-ahead. -* When active playback stalls long enough to fill most of the compressed +* When active playback stalls long enough to fill half of the compressed * video reserve, replace the oldest decoded frame instead of allowing video * backpressure to stop the shared demuxer and starve audio. Paused playback * retains every queued frame and continues using bounded blocking behavior. @@ -1147,7 +1152,7 @@ static int audio_thread(void *data) "(%" PRIu64 " total): PCM=%u/%u, " "packets=%d/%d audio and %d/%d video, " "frames=%d/%d (%" PRIu64 " stale dropped), " - "recovery=%" PRIu64 " incoming packets skipped in %" PRIu64 + "recovery=%" PRIu64 " compressed packets discarded in %" PRIu64 " keyframe resync event%s.\n", audio_ctx->channel, underrun_count, From 702264c4b17d2c263336b88ea696cd8de1661b28 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Mon, 17 Aug 2026 19:53:21 -0400 Subject: [PATCH 22/26] Fix misnamed onblocka script -> onblocky. --- engine/openbor.c | 10 +++++----- engine/openbor.h | 4 ++-- engine/source/openborscript/commands.c | 2 +- engine/source/openborscript/commands.h | 2 +- engine/version.tmp | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index e602258d4..e50692db5 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -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); @@ -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); @@ -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 diff --git a/engine/openbor.h b/engine/openbor.h index 04c1d59cf..ff629c737 100644 --- a/engine/openbor.h +++ b/engine/openbor.h @@ -2976,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. @@ -4459,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); diff --git a/engine/source/openborscript/commands.c b/engine/source/openborscript/commands.c index 36478f59d..e1b7cc843 100644 --- a/engine/source/openborscript/commands.c +++ b/engine/source/openborscript/commands.c @@ -501,7 +501,7 @@ List *createModelCommandList(void) LIST_ADD(CMD_MODEL_OFFSET, "offset"); LIST_ADD(CMD_MODEL_ON_BIND_UPDATE_OTHER_TO_SELF_SCRIPT, "on_bind_update_other_to_self_script"); LIST_ADD(CMD_MODEL_ON_BIND_UPDATE_SELF_TO_OTHER_SCRIPT, "on_bind_update_self_to_other_script"); - LIST_ADD(CMD_MODEL_ONBLOCKASCRIPT, "onblockascript"); + LIST_ADD(CMD_MODEL_ONBLOCKYSCRIPT, "onblockyscript"); LIST_ADD(CMD_MODEL_ONBLOCKOSCRIPT, "onblockoscript"); LIST_ADD(CMD_MODEL_ONBLOCKPSCRIPT, "onblockpscript"); LIST_ADD(CMD_MODEL_ONBLOCKSSCRIPT, "onblocksscript"); diff --git a/engine/source/openborscript/commands.h b/engine/source/openborscript/commands.h index db92dfed6..b8c9d86bd 100644 --- a/engine/source/openborscript/commands.h +++ b/engine/source/openborscript/commands.h @@ -608,7 +608,7 @@ typedef enum modelCommand CMD_MODEL_OFFSET, CMD_MODEL_ON_BIND_UPDATE_OTHER_TO_SELF_SCRIPT, CMD_MODEL_ON_BIND_UPDATE_SELF_TO_OTHER_SCRIPT, - CMD_MODEL_ONBLOCKASCRIPT, + CMD_MODEL_ONBLOCKYSCRIPT, CMD_MODEL_ONBLOCKOSCRIPT, CMD_MODEL_ONBLOCKPSCRIPT, CMD_MODEL_ONBLOCKSSCRIPT, diff --git a/engine/version.tmp b/engine/version.tmp index 1630f4793..a8ecaa12f 100644 --- a/engine/version.tmp +++ b/engine/version.tmp @@ -12,9 +12,9 @@ #define VERSION_NAME "OpenBOR" #define VERSION_MAJOR "4" #define VERSION_MINOR "0" -#define VERSION_BUILD "7975" -#define VERSION_BUILD_INT 7975 -#define VERSION_COMMIT "b96337d" +#define VERSION_BUILD "7982" +#define VERSION_BUILD_INT 7982 +#define VERSION_COMMIT "ae48540" #define VERSION "v"VERSION_MAJOR"."VERSION_MINOR" Build "VERSION_BUILD" (commit hash: "VERSION_COMMIT")" #endif From 5eaadced7b3c9d19a53b06a3f44d12d0e10b1f08 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Mon, 17 Aug 2026 19:56:58 -0400 Subject: [PATCH 23/26] Fix reversed plane/index arguments for onblockwscript. --- engine/openbor.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/openbor.c b/engine/openbor.c index e50692db5..0a9b0eef0 100644 --- a/engine/openbor.c +++ b/engine/openbor.c @@ -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); From 0c44ea6ccf4138eb3d3124c6438ff61108cb9e5d Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 18 Aug 2026 10:49:35 -0400 Subject: [PATCH 24/26] Add jitter-tolerant WebM frame presentation Preroll decoded video alongside PCM before publishing READY. Preserve one due frame per presentation opportunity, bound audio-master lateness, and report presentation or decoded-queue drops independently from compressed backlog recovery. --- engine/source/webmlib/movie_playback.c | 125 +++++++++++++++--- engine/source/webmlib/movie_playback.h | 3 + engine/source/webmlib/vidplay.c | 171 +++++++++++++++++++------ 3 files changed, 243 insertions(+), 56 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 9ede4bca5..07df792df 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -6,6 +6,7 @@ * Copyright (c) OpenBOR Team */ +#include #include #include #include @@ -39,6 +40,8 @@ static void *movie_cache_allocate_nonfatal(size_t size) #define MOVIE_CACHE_READ_CHUNK_SIZE (1024U * 1024U) #define MOVIE_CACHE_RESERVE_MINIMUM (UINT64_C(128) * UINT64_C(1024) * UINT64_C(1024)) #define MOVIE_SPEED_ROUNDING_FACTOR 1000.0 +#define MOVIE_VIDEO_LATE_TOLERANCE_NANOSECONDS UINT64_C(50000000) +#define MOVIE_PRESENTATION_DROP_REPORT_INTERVAL UINT64_C(16) typedef enum e_movie_cache_result { MOVIE_CACHE_RESULT_SUCCESS, @@ -959,6 +962,7 @@ static bool movie_playback_begin_media( movie_playback_retire_decoder(playback); yuv_frame_destroy(playback->next_frame); playback->next_frame = NULL; + playback->current_frame_presented = playback->current_frame != NULL; request = &movie_playback_pool.decoder_request[channel]; request->position = position; request->source_id = playback->source_id; @@ -1029,6 +1033,7 @@ static int movie_playback_poll_decoder(s_movie_playback *playback) playback->video_mode.height != video_mode.height) { yuv_frame_destroy(playback->current_frame); playback->current_frame = NULL; + playback->current_frame_presented = 0; if(playback->rgb_frame) { freescreen(&playback->rgb_frame); } @@ -1347,17 +1352,77 @@ static bool movie_playback_render_to( playback->offset_x, playback->offset_y ); + playback->current_frame_presented = 1; return true; } /* * Caskey, Damon V. -* 2026-08-12 +* 2026-08-18 +* +* Record decoded frames intentionally omitted from presentation after their +* lateness exceeds the bounded audio-master tolerance. Report the first drop +* and each subsequent group so video loss remains observable independently +* from healthy PCM reserves and compressed-packet recovery. +*/ +static void movie_playback_record_presentation_drop( + s_movie_playback *playback, + uint64_t frame_timestamp, + uint64_t position +) +{ + uint64_t lateness = position >= frame_timestamp + ? position - frame_timestamp + : 0; + + if(playback->presentation_drop_count < UINT64_MAX) { + ++playback->presentation_drop_count; + } + if(lateness > playback->maximum_presentation_lateness) { + playback->maximum_presentation_lateness = lateness; + } + if(playback->presentation_drop_count == 1U || + !(playback->presentation_drop_count % + MOVIE_PRESENTATION_DROP_REPORT_INTERVAL)) { + printf( + "Warning: Movie channel %d discarded %" PRIu64 + " late decoded video frame%s for audio synchronization " + "(maximum lateness=%" PRIu64 " ms).\n", + playback->index, + playback->presentation_drop_count, + playback->presentation_drop_count == 1U ? "" : "s", + playback->maximum_presentation_lateness / UINT64_C(1000000) + ); + } +} + +/* +* Caskey, Damon V. +* 2026-08-18 * -* Poll only the decoded frames present when this update begins. A decoder -* producing stale frames must not refill the queue faster than the main thread -* can empty it and turn one nonblocking poll into an unbounded engine stall. -* Explicit seeks retain the previous frame until replacement becomes ready. +* Transfer the retained look-ahead frame into presentation ownership. One +* promotion remains pending until composition acknowledges it, preventing +* repeated engine updates from silently replacing an undisplayed frame. +*/ +static void movie_playback_promote_next_frame( + s_movie_playback *playback +) +{ + yuv_frame_destroy(playback->current_frame); + playback->current_frame = playback->next_frame; + playback->next_frame = NULL; + playback->frame_dirty = 1; + playback->current_frame_presented = 0; +} + +/* +* Caskey, Damon V. +* 2026-08-18 +* +* Promote one due frame per presentation opportunity and preserve ordinary +* scheduling jitter behind the audio master. Only frames beyond the lateness +* tolerance may be discarded, while the initial queue snapshot still prevents +* a decoder from extending one engine update without bound. */ static void movie_playback_poll_frames( s_movie_playback *playback, @@ -1366,8 +1431,6 @@ static void movie_playback_poll_frames( ) { int remaining = webm_get_pending_frame_count(playback->context); - int promoted = 0; - if(playback->next_frame) { ++remaining; } @@ -1386,23 +1449,49 @@ static void movie_playback_poll_frames( } } + if(playback->current_frame && + !playback->current_frame_presented) { + uint64_t current_timestamp = + playback->current_frame->timestamp; + + if(playback->next_frame->timestamp > position || + current_timestamp > position || + position - current_timestamp <= + MOVIE_VIDEO_LATE_TOLERANCE_NANOSECONDS) { + break; + } + movie_playback_record_presentation_drop( + playback, + current_timestamp, + position + ); + yuv_frame_destroy(playback->current_frame); + playback->current_frame = NULL; + playback->frame_dirty = 0; + } + if(playback->next_frame->timestamp > position) { - if(!playback->current_frame && !promoted) { - yuv_frame_destroy(playback->current_frame); - playback->current_frame = playback->next_frame; - playback->next_frame = NULL; - playback->frame_dirty = 1; - promoted = 1; + if(!playback->current_frame) { + movie_playback_promote_next_frame(playback); } break; } - yuv_frame_destroy(playback->current_frame); - playback->current_frame = playback->next_frame; - playback->next_frame = NULL; - playback->frame_dirty = 1; - promoted = 1; + if(position - playback->next_frame->timestamp > + MOVIE_VIDEO_LATE_TOLERANCE_NANOSECONDS && + remaining > 1) { + movie_playback_record_presentation_drop( + playback, + playback->next_frame->timestamp, + position + ); + yuv_frame_destroy(playback->next_frame); + playback->next_frame = NULL; + continue; + } + movie_playback_promote_next_frame(playback); + break; } } diff --git a/engine/source/webmlib/movie_playback.h b/engine/source/webmlib/movie_playback.h index 776bd62f4..d90c3a4f7 100644 --- a/engine/source/webmlib/movie_playback.h +++ b/engine/source/webmlib/movie_playback.h @@ -70,6 +70,9 @@ typedef struct s_movie_playback { int rgb_frame_dirty; int scaled_frame_dirty; int terminal_pending; + int current_frame_presented; + uint64_t presentation_drop_count; + uint64_t maximum_presentation_lateness; } s_movie_playback; bool movie_playback_init(void); diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index 39aa407cc..b17dd08a9 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -34,7 +34,7 @@ /* * Caskey, Damon V. -* 2026-08-17 +* 2026-08-18 * * Balance decoder reserves after the playback clock, backlog recovery, and * asynchronous lifecycle are stable. Sixty-four audio packets cover ordinary @@ -42,10 +42,11 @@ * roughly two seconds at 60 FPS without preserving the many seconds of stale * compressed work held by the former 512-packet queue. Six decoded YUV frames * provide 100-200 milliseconds of read-ahead across 30-60 FPS while bounding -* the software renderer's per-movie frame memory. Demux and audio producers +* the software renderer's per-movie frame memory. Three decoded frames establish +* video preroll before the audio-master clock starts. Demux and audio producers * retain high priority because both block frequently; VP8 decoding remains at -* normal priority so it cannot starve the engine. The proven eight-buffer -* generic PCM ring remains unchanged. +* normal priority so it cannot starve the engine. The proven eight-buffer PCM +* ring remains unchanged. */ #define AUDIO_PACKET_QUEUE_SIZE 64 #define VIDEO_PACKET_QUEUE_SIZE 128 @@ -56,7 +57,8 @@ #define AUDIO_RECOVERY_PCM_BUFFER_COUNT \ (SOUND_STREAM_BUFFER_COUNT / 2U) #define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) -#define AUDIO_PREROLL_TIMEOUT_MICROSECONDS UINT64_C(2000000) +#define VIDEO_PREROLL_FRAME_COUNT ((FRAME_QUEUE_SIZE + 1) / 2) +#define WEBM_PREROLL_TIMEOUT_MICROSECONDS UINT64_C(2000000) #define debug_printf(...) //printf(__VA_ARGS__) @@ -761,6 +763,30 @@ static int video_packet_queue_insert( return -1; } +/* +* Caskey, Damon V. +* 2026-08-18 +* +* Report decoded frames displaced by sustained frame-queue backpressure even +* when the protected PCM reserve remains healthy. The first replacement and +* each sixteenth total keep video loss visible without flooding the log. +*/ +static void video_frame_queue_report_replacement( + uint64_t replacement_count +) +{ + if(replacement_count && + (replacement_count == 1U || + !(replacement_count % UINT64_C(16)))) { + printf( + "Warning: WebM decoded-frame backpressure discarded %" PRIu64 + " queued frame%s.\n", + replacement_count, + replacement_count == 1U ? "" : "s" + ); + } +} + /* * Caskey, Damon V. * 2026-08-17 @@ -793,6 +819,7 @@ static int video_frame_queue_insert( NULL, NULL ); + uint64_t replacement_count = 0; mutex_lock(frame_queue->mutex); if(webm_stop_is_requested(frame_queue->quit)) { @@ -810,6 +837,7 @@ static int video_frame_queue_insert( if(frame_queue->replacement_count < UINT64_MAX) { frame_queue->replacement_count++; } + replacement_count = frame_queue->replacement_count; } if(frame_queue->size < frame_queue->max_size) { index = (frame_queue->start + frame_queue->size) % @@ -818,6 +846,7 @@ static int video_frame_queue_insert( ++frame_queue->size; cond_signal(frame_queue->not_empty); mutex_unlock(frame_queue->mutex); + video_frame_queue_report_replacement(replacement_count); return 0; } @@ -1192,45 +1221,113 @@ static int audio_thread(void *data) /* * Caskey, Damon V. -* 2026-08-15 +* 2026-08-18 * -* Let the decoder establish its PCM reserve before the owning movie clock -* starts. The bounded wait preserves failure tolerance for malformed or -* unusually slow inputs while ordinary playback begins with continuous -* audio already available to the mixer. +* Snapshot the decoded-video preroll reserve under its queue lock. A queued +* terminal marker also completes preroll for movies shorter than the ordinary +* frame target, including a valid stream that reaches end before three frames. */ -static void audio_wait_for_preroll(webm_context *ctx) +static int video_preroll_ready( + video_context *video_ctx, + int *decoded_frame_count +) +{ + FixedSizeQueue *queue; + int decoded_frames = 0; + int terminal = 0; + int i; + + if(decoded_frame_count) { + *decoded_frame_count = 0; + } + if(!video_ctx || !(queue = video_ctx->frame_queue)) { + return 0; + } + + mutex_lock(queue->mutex); + for(i = 0; i < queue->size; ++i) { + int index = (queue->start + i) % queue->max_size; + + if(queue->data[index]) { + ++decoded_frames; + } + else { + terminal = 1; + } + } + mutex_unlock(queue->mutex); + + if(decoded_frame_count) { + *decoded_frame_count = decoded_frames; + } + return decoded_frames >= VIDEO_PREROLL_FRAME_COUNT || terminal; +} + +/* +* Caskey, Damon V. +* 2026-08-18 +* +* Hold the asynchronous READY boundary until both decoded video and owned +* PCM have useful reserves. A bounded timeout preserves failure tolerance, +* while silent and very short movies become ready from video alone or their +* terminal marker instead of starting an audio clock ahead of presentation. +*/ +static void webm_wait_for_preroll(webm_context *ctx) { - audio_context *audio_ctx; uint64_t start_time; - if(!ctx || !ctx->audio_initialized) { + if(!ctx || !ctx->video_initialized) { return; } - audio_ctx = &ctx->audio_ctx; start_time = timer_uticks(); - while(!webm_stop_is_requested(audio_ctx->quit) && + while(!webm_stop_is_requested(&ctx->quit) && !webm_close_is_requested(ctx)) { - s_sound_pcm_stream_status status; + s_sound_pcm_stream_status status = { 0 }; uint64_t now; + int audio_ready = !ctx->audio_initialized; + int decoded_frame_count = 0; + int video_ready = video_preroll_ready( + &ctx->video_ctx, + &decoded_frame_count + ); - if(!sound_get_channel_pcm_stream_status_owned( - audio_ctx->channel, - audio_ctx->stream_play_id, - &status - )) { - return; + if(ctx->audio_initialized) { + audio_context *audio_ctx = &ctx->audio_ctx; + + if(!sound_get_channel_pcm_stream_status_owned( + audio_ctx->channel, + audio_ctx->stream_play_id, + &status + )) { + audio_ready = 1; + } + else { + audio_ctx->observed_underrun_count = + status.underrun_count; + audio_ready = + status.ready_buffer_count >= + AUDIO_PREROLL_BUFFER_COUNT || + status.producer_finished; + } } - audio_ctx->observed_underrun_count = status.underrun_count; - if(status.ready_buffer_count >= AUDIO_PREROLL_BUFFER_COUNT || - (status.producer_finished && status.ready_buffer_count > 0)) { + if(video_ready && audio_ready) { return; } now = timer_uticks(); if(now < start_time || - now - start_time >= AUDIO_PREROLL_TIMEOUT_MICROSECONDS) { + now - start_time >= WEBM_PREROLL_TIMEOUT_MICROSECONDS) { + printf( + "Warning: WebM preroll timed out with video=%d/%d " + "decoded frames and audio=%u/%u PCM buffers.\n", + decoded_frame_count, + VIDEO_PREROLL_FRAME_COUNT, + status.ready_buffer_count, + ctx->audio_initialized + ? AUDIO_PREROLL_BUFFER_COUNT + : 0U + ); return; } usleep(1000); @@ -1885,11 +1982,11 @@ static void webm_close_resources(webm_context *ctx) /* * Caskey, Damon V. -* 2026-08-17 +* 2026-08-18 * * Perform container open, initial seek, and decoder creation on the lifecycle -* worker, then publish PREROLLING when audio must establish its PCM reserve. -* Explicit seeks use the same decoder path as initial playback. +* worker, then publish PREROLLING while video and optional audio establish +* their reserves. Explicit seeks use the same decoder path as initial playback. */ static int webm_open_resources(webm_context *ctx) { @@ -2064,13 +2161,11 @@ static int webm_open_resources(webm_context *ctx) printf("Error: Unable to start WebM demux thread\n"); return -1; } - if(ctx->audio_track >= 0) { - if(!webm_publish_decoder_state( - ctx, - WEBM_DECODER_STATE_PREROLLING - )) { - return -1; - } + if(!webm_publish_decoder_state( + ctx, + WEBM_DECODER_STATE_PREROLLING + )) { + return -1; } return webm_stop_is_requested(&ctx->quit) ? -1 : 0; } @@ -2091,8 +2186,8 @@ static int webm_lifecycle_thread(void *data) mutex_lock(webm_lifecycle_operation_mutex); open_result = webm_open_resources(ctx); mutex_unlock(webm_lifecycle_operation_mutex); - if(open_result == 0 && ctx->audio_track >= 0) { - audio_wait_for_preroll(ctx); + if(open_result == 0) { + webm_wait_for_preroll(ctx); if(webm_stop_is_requested(&ctx->quit) || webm_close_is_requested(ctx)) { open_result = -1; From cfdcb8e89f10055d3f3b18b4da772656a1eeff62 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 18 Aug 2026 11:05:37 -0400 Subject: [PATCH 25/26] Stop WebM decoder from discarding read-ahead frames Treat a full decoded-frame queue as normal bounded read-ahead instead of replacing ordered frames whenever the compressed queue is populated. Keep exceptional audio-aware compressed recovery, remove obsolete decoded-drop diagnostics, and separate the initial video log block. --- engine/source/webmlib/vidplay.c | 140 +++----------------------------- 1 file changed, 12 insertions(+), 128 deletions(-) diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index b17dd08a9..e3a59f55b 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -43,17 +43,17 @@ * compressed work held by the former 512-packet queue. Six decoded YUV frames * provide 100-200 milliseconds of read-ahead across 30-60 FPS while bounding * the software renderer's per-movie frame memory. Three decoded frames establish -* video preroll before the audio-master clock starts. Demux and audio producers -* retain high priority because both block frequently; VP8 decoding remains at -* normal priority so it cannot starve the engine. The proven eight-buffer PCM -* ring remains unchanged. +* video preroll before the audio-master clock starts. Full decoded-frame queues +* apply backpressure without discarding ordered presentation work; exceptional +* audio starvation remains isolated to compressed keyframe recovery. Demux and +* audio producers retain high priority because both block frequently; VP8 +* decoding remains at normal priority so it cannot starve the engine. The proven +* eight-buffer PCM ring remains unchanged. */ #define AUDIO_PACKET_QUEUE_SIZE 64 #define VIDEO_PACKET_QUEUE_SIZE 128 #define FRAME_QUEUE_SIZE 6 #define VIDEO_DECODER_THREAD_MAX 8U -#define VIDEO_PACKET_QUEUE_BACKPRESSURE_THRESHOLD \ - (VIDEO_PACKET_QUEUE_SIZE / 2) #define AUDIO_RECOVERY_PCM_BUFFER_COUNT \ (SOUND_STREAM_BUFFER_COUNT / 2U) #define AUDIO_PREROLL_BUFFER_COUNT ((SOUND_STREAM_BUFFER_COUNT * 3U) / 4U) @@ -763,103 +763,6 @@ static int video_packet_queue_insert( return -1; } -/* -* Caskey, Damon V. -* 2026-08-18 -* -* Report decoded frames displaced by sustained frame-queue backpressure even -* when the protected PCM reserve remains healthy. The first replacement and -* each sixteenth total keep video loss visible without flooding the log. -*/ -static void video_frame_queue_report_replacement( - uint64_t replacement_count -) -{ - if(replacement_count && - (replacement_count == 1U || - !(replacement_count % UINT64_C(16)))) { - printf( - "Warning: WebM decoded-frame backpressure discarded %" PRIu64 - " queued frame%s.\n", - replacement_count, - replacement_count == 1U ? "" : "s" - ); - } -} - -/* -* Caskey, Damon V. -* 2026-08-17 -* -* Publish decoded frames in timestamp order during ordinary read-ahead. -* When active playback stalls long enough to fill half of the compressed -* video reserve, replace the oldest decoded frame instead of allowing video -* backpressure to stop the shared demuxer and starve audio. Paused playback -* retains every queued frame and continues using bounded blocking behavior. -*/ -static int video_frame_queue_insert( - video_context *video_ctx, - void *data, - void **replaced -) -{ - FixedSizeQueue *frame_queue; - - if(!video_ctx || !video_ctx->frame_queue || - !video_ctx->packet_queue || !replaced) { - return -1; - } - frame_queue = video_ctx->frame_queue; - *replaced = NULL; - - while(!webm_stop_is_requested(frame_queue->quit)) { - int index; - int video_packet_count = queue_get_status( - video_ctx->packet_queue, - NULL, - NULL - ); - uint64_t replacement_count = 0; - - mutex_lock(frame_queue->mutex); - if(webm_stop_is_requested(frame_queue->quit)) { - mutex_unlock(frame_queue->mutex); - return -1; - } - if(frame_queue->size == frame_queue->max_size && - !video_ctx->playback_paused && - video_packet_count >= - VIDEO_PACKET_QUEUE_BACKPRESSURE_THRESHOLD) { - *replaced = frame_queue->data[frame_queue->start]; - frame_queue->start = - (frame_queue->start + 1) % frame_queue->max_size; - --frame_queue->size; - if(frame_queue->replacement_count < UINT64_MAX) { - frame_queue->replacement_count++; - } - replacement_count = frame_queue->replacement_count; - } - if(frame_queue->size < frame_queue->max_size) { - index = (frame_queue->start + frame_queue->size) % - frame_queue->max_size; - frame_queue->data[index] = data; - ++frame_queue->size; - cond_signal(frame_queue->not_empty); - mutex_unlock(frame_queue->mutex); - video_frame_queue_report_replacement(replacement_count); - return 0; - } - - cond_wait_timed( - frame_queue->not_full, - frame_queue->mutex, - 10 - ); - mutex_unlock(frame_queue->mutex); - } - return -1; -} - void queue_destroy(FixedSizeQueue *queue) { cond_destroy(queue->not_full); @@ -1149,7 +1052,6 @@ static int audio_thread(void *data) UINT64_C(16))) { s_sound_pcm_stream_status status = { 0 }; uint64_t dropped_packets; - uint64_t dropped_frames; uint64_t resync_count; int audio_packets; int decoded_frames; @@ -1172,7 +1074,7 @@ static int audio_thread(void *data) ); decoded_frames = queue_get_status( audio_ctx->video_frame_queue, - &dropped_frames, + NULL, NULL ); audio_ctx->reported_underrun_count = underrun_count; @@ -1180,7 +1082,7 @@ static int audio_thread(void *data) "Warning: WebM audio underrun on sound channel %d " "(%" PRIu64 " total): PCM=%u/%u, " "packets=%d/%d audio and %d/%d video, " - "frames=%d/%d (%" PRIu64 " stale dropped), " + "frames=%d/%d, " "recovery=%" PRIu64 " compressed packets discarded in %" PRIu64 " keyframe resync event%s.\n", audio_ctx->channel, @@ -1193,7 +1095,6 @@ static int audio_thread(void *data) VIDEO_PACKET_QUEUE_SIZE, decoded_frames, FRAME_QUEUE_SIZE, - dropped_frames, dropped_packets, resync_count, resync_count == 1U ? "" : "s" @@ -1552,8 +1453,6 @@ static int video_thread(void *data) } while((img = vpx_codec_get_frame(&ctx->vpx_ctx, &iter))) { - void *replaced_frame = NULL; - if(img->d_w != (unsigned int)ctx->width || img->d_h != (unsigned int)ctx->height) { printf("Error: WebM frame dimensions changed during playback\n"); @@ -1577,17 +1476,12 @@ static int video_thread(void *data) memcpy(frame->cb+(y*img->d_w/2), img->planes[2]+(y*img->stride[2]), img->d_w / 2); } - if(video_frame_queue_insert( - ctx, - (void *)frame, - &replaced_frame - ) < 0) + if(queue_insert(ctx->frame_queue, (void *)frame) < 0) { debug_printf("destroying last frame\n"); yuv_frame_destroy(frame); break; } - yuv_frame_destroy((yuv_frame *)replaced_frame); timestamp = timestamp <= UINT64_MAX - frame_delay ? timestamp + frame_delay : UINT64_MAX; @@ -1597,17 +1491,7 @@ static int video_thread(void *data) nestegg_free_packet(pkt); } - { - void *replaced_frame = NULL; - - if(video_frame_queue_insert( - ctx, - NULL, - &replaced_frame - ) == 0) { - yuv_frame_destroy((yuv_frame *)replaced_frame); - } - } + queue_insert(ctx->frame_queue, NULL); return 0; } @@ -1718,13 +1602,13 @@ static int init_video( &(video_ctx->frame_delay) ) == 0 && video_ctx->frame_delay; if(has_default_duration) { - printf("Video track: resolution=%i*%i, display resolution=%i*%i, %.2f frames/second\n", + printf("\n\nVideo track: resolution=%i*%i, display resolution=%i*%i, %.2f frames/second\n", video_params.width, video_params.height, video_params.display_width, video_params.display_height, 1000000000.0 / video_ctx->frame_delay); } else { video_ctx->frame_delay = 0; - printf("Video track: resolution=%i*%i, display resolution=%i*%i, packet-timestamp timing\n", + printf("\n\nVideo track: resolution=%i*%i, display resolution=%i*%i, packet-timestamp timing\n", video_params.width, video_params.height, video_params.display_width, video_params.display_height); } From 48e32ef77e5fa38b2f853d99ddc2d868d3092374 Mon Sep 17 00:00:00 2001 From: "Damon V. Caskey" Date: Tue, 18 Aug 2026 11:33:23 -0400 Subject: [PATCH 26/26] Polish WebM diagnostics and comments Rewrite playback comments as present-tense ownership, timing, and queue contracts. Remove tuning-history and compatibility-development wording, and place a blank line before WebM warnings. --- engine/source/webmlib/movie_playback.c | 55 +++++++---------- engine/source/webmlib/vidplay.c | 84 +++++++++++--------------- 2 files changed, 59 insertions(+), 80 deletions(-) diff --git a/engine/source/webmlib/movie_playback.c b/engine/source/webmlib/movie_playback.c index 07df792df..51da1a2ae 100644 --- a/engine/source/webmlib/movie_playback.c +++ b/engine/source/webmlib/movie_playback.c @@ -63,10 +63,8 @@ typedef struct s_movie_source { * Caskey, Damon V. * 2026-08-17 * -* Keep asynchronous decoder ownership outside the stable script-visible -* playback record. Each decoder retains its movie source until lifecycle -* teardown reaches CLOSED, including decoders retired by seeks or channel -* replacement while their worker is still running. +* Own asynchronous decoder lifecycle independently from the script-visible +* playback record. Each decoder holds its movie source through CLOSED. */ typedef struct s_movie_decoder { webm_context *context; @@ -308,10 +306,9 @@ static void movie_source_release(int source_id) * Caskey, Damon V. * 2026-08-17 * -* Poll decoder retirement without waiting during runtime updates. Shutdown -* may explicitly wait after every playback has stopped. Source references -* remain held until each lifecycle worker has released its borrowed path and -* cache storage. +* Reap completed decoder retirement during runtime updates. The wait option +* provides synchronous shutdown after playback has stopped. Decoder source +* references remain valid through lifecycle completion. */ static void movie_playback_reap_retired_decoders(int wait) { @@ -352,9 +349,8 @@ static void movie_playback_reap_retired_decoders(int wait) * Caskey, Damon V. * 2026-08-17 * -* Move the channel's decoder directly to asynchronous retirement without -* allocating or joining. Per-channel retirement counts serialize a later -* replacement while allowing repeated pending seeks to collapse to one. +* Queue the channel decoder for asynchronous retirement. Per-channel counts +* gate decoder creation, and one request slot coalesces repeated pending seeks. */ static void movie_playback_retire_decoder(s_movie_playback *playback) { @@ -613,9 +609,9 @@ static e_movie_cache_result movie_source_read_cache( * Caskey, Damon V. * 2026-08-13 * -* Load one reusable movie source. Streaming remains the default, -* forced cache fails cleanly, and automatic cache falls back to -* streaming when its memory policy or allocation cannot be met. +* Load one reusable movie source according to its stream, cache, or automatic +* memory policy. Forced cache reports failure; automatic cache may select +* streaming when memory requirements are not met. */ int movie_source_load(const char *path, e_movie_loading_mode loading_mode) { @@ -873,9 +869,8 @@ static uint64_t movie_playback_position_now( * Caskey, Damon V. * 2026-08-17 * -* Start the latest pending decoder request only after the previous decoder -* for this movie channel reaches CLOSED. Repeated seeks during teardown -* replace the request in place instead of accumulating lifecycle threads. +* Start the latest pending decoder request after the retiring decoder reaches +* CLOSED. One pending slot coalesces repeated seeks during teardown. */ static int movie_playback_start_pending_decoder( s_movie_playback *playback @@ -1066,9 +1061,9 @@ static int movie_playback_poll_decoder(s_movie_playback *playback) * Caskey, Damon V. * 2026-08-12 * -* Create one playback from a reusable source. Automatic mode -* takes the lowest free slot; explicit selection replaces it. -* Legacy callers may request their historical audio takeover. +* Create one playback from a reusable source. Automatic mode takes the lowest +* free slot; explicit selection replaces that channel. Exclusive audio mode +* stops other channel playback for fullscreen ownership. */ s_movie_playback *movie_playback_play( int source_id, @@ -1360,10 +1355,8 @@ static bool movie_playback_render_to( * Caskey, Damon V. * 2026-08-18 * -* Record decoded frames intentionally omitted from presentation after their -* lateness exceeds the bounded audio-master tolerance. Report the first drop -* and each subsequent group so video loss remains observable independently -* from healthy PCM reserves and compressed-packet recovery. +* Record decoded frames omitted after exceeding the audio-master lateness +* tolerance. Track maximum lateness and rate-limit diagnostic output. */ static void movie_playback_record_presentation_drop( s_movie_playback *playback, @@ -1385,7 +1378,7 @@ static void movie_playback_record_presentation_drop( !(playback->presentation_drop_count % MOVIE_PRESENTATION_DROP_REPORT_INTERVAL)) { printf( - "Warning: Movie channel %d discarded %" PRIu64 + "\nWarning: Movie channel %d discarded %" PRIu64 " late decoded video frame%s for audio synchronization " "(maximum lateness=%" PRIu64 " ms).\n", playback->index, @@ -1400,9 +1393,8 @@ static void movie_playback_record_presentation_drop( * Caskey, Damon V. * 2026-08-18 * -* Transfer the retained look-ahead frame into presentation ownership. One -* promotion remains pending until composition acknowledges it, preventing -* repeated engine updates from silently replacing an undisplayed frame. +* Transfer the look-ahead frame into presentation ownership and mark it pending +* until composition acknowledges it. */ static void movie_playback_promote_next_frame( s_movie_playback *playback @@ -1419,10 +1411,9 @@ static void movie_playback_promote_next_frame( * Caskey, Damon V. * 2026-08-18 * -* Promote one due frame per presentation opportunity and preserve ordinary -* scheduling jitter behind the audio master. Only frames beyond the lateness -* tolerance may be discarded, while the initial queue snapshot still prevents -* a decoder from extending one engine update without bound. +* Promote one due frame per presentation opportunity under the audio master. +* Protect frames within the lateness tolerance and bound work to the initial +* decoded-queue snapshot. */ static void movie_playback_poll_frames( s_movie_playback *playback, diff --git a/engine/source/webmlib/vidplay.c b/engine/source/webmlib/vidplay.c index e3a59f55b..541377680 100644 --- a/engine/source/webmlib/vidplay.c +++ b/engine/source/webmlib/vidplay.c @@ -36,19 +36,14 @@ * Caskey, Damon V. * 2026-08-18 * -* Balance decoder reserves after the playback clock, backlog recovery, and -* asynchronous lifecycle are stable. Sixty-four audio packets cover ordinary -* Vorbis scheduling jitter. One hundred twenty-eight video packets retain -* roughly two seconds at 60 FPS without preserving the many seconds of stale -* compressed work held by the former 512-packet queue. Six decoded YUV frames -* provide 100-200 milliseconds of read-ahead across 30-60 FPS while bounding -* the software renderer's per-movie frame memory. Three decoded frames establish -* video preroll before the audio-master clock starts. Full decoded-frame queues -* apply backpressure without discarding ordered presentation work; exceptional -* audio starvation remains isolated to compressed keyframe recovery. Demux and -* audio producers retain high priority because both block frequently; VP8 -* decoding remains at normal priority so it cannot starve the engine. The proven -* eight-buffer PCM ring remains unchanged. +* Configure bounded decoder reserves and worker priorities. Sixty-four audio +* packets absorb Vorbis scheduling jitter. One hundred twenty-eight compressed +* video packets cover roughly two seconds at 60 FPS. Six decoded YUV frames +* provide 100-200 milliseconds of presentation read-ahead across 30-60 FPS, +* with three required for preroll. Full decoded-frame queues block their +* producer to preserve presentation order. Audio and demux producers use high +* priority; VP8 decoding uses normal priority to preserve engine capacity. PCM +* output uses an eight-buffer ring. */ #define AUDIO_PACKET_QUEUE_SIZE 64 #define VIDEO_PACKET_QUEUE_SIZE 128 @@ -489,9 +484,8 @@ static int queue_try_get(FixedSizeQueue *queue, void **data) * Caskey, Damon V. * 2026-08-15 * -* Capture queue occupancy, replacement totals, and recovery events only when -* reporting exceptional state. Normal decoding and publication incur no -* diagnostic polling. +* Snapshot queue occupancy and cumulative discard and resynchronization totals +* under the queue lock for diagnostics. */ static int queue_get_status( FixedSizeQueue *queue, @@ -575,10 +569,8 @@ static int video_playback_is_paused(video_context *video_ctx) * Caskey, Damon V. * 2026-08-15 * -* Detect genuine audio starvation risk before sacrificing video backlog. -* Empty compressed audio alone is harmless while the PCM ring remains -* healthy. Recovery begins only when both reserves are depleted and active -* playback would otherwise let a full video queue block the shared demuxer. +* Authorize compressed-video recovery during active playback when the audio +* packet queue is empty and the PCM reserve reaches its critical threshold. */ static int webm_audio_reserve_is_critical(webm_context *ctx) { @@ -724,9 +716,9 @@ static void video_packet_queue_record_drop(FixedSizeQueue *queue) * Caskey, Damon V. * 2026-08-15 * -* Insert compressed video with ordinary bounded blocking while audio has -* usable reserves. Return zero without consuming the packet when audio -* becomes critical, allowing the demuxer to perform keyframe recovery. +* Insert compressed video with bounded blocking while audio has usable +* reserves. Return zero without consuming the packet to request keyframe +* recovery when the audio reserve becomes critical. */ static int video_packet_queue_insert( webm_context *ctx, @@ -771,7 +763,7 @@ void queue_destroy(FixedSizeQueue *queue) free(queue); } -// used to keep playing current BGM in videos with no audio track +// Service existing BGM for videos without an audio track. static int bgm_update_thread(void *data) { webm_context *ctx = data; @@ -1079,7 +1071,7 @@ static int audio_thread(void *data) ); audio_ctx->reported_underrun_count = underrun_count; printf( - "Warning: WebM audio underrun on sound channel %d " + "\nWarning: WebM audio underrun on sound channel %d " "(%" PRIu64 " total): PCM=%u/%u, " "packets=%d/%d audio and %d/%d video, " "frames=%d/%d, " @@ -1124,9 +1116,8 @@ static int audio_thread(void *data) * Caskey, Damon V. * 2026-08-18 * -* Snapshot the decoded-video preroll reserve under its queue lock. A queued -* terminal marker also completes preroll for movies shorter than the ordinary -* frame target, including a valid stream that reaches end before three frames. +* Snapshot the decoded-video preroll reserve under its queue lock. A terminal +* marker satisfies preroll for streams containing fewer than three frames. */ static int video_preroll_ready( video_context *video_ctx, @@ -1168,10 +1159,9 @@ static int video_preroll_ready( * Caskey, Damon V. * 2026-08-18 * -* Hold the asynchronous READY boundary until both decoded video and owned -* PCM have useful reserves. A bounded timeout preserves failure tolerance, -* while silent and very short movies become ready from video alone or their -* terminal marker instead of starting an audio clock ahead of presentation. +* Hold the asynchronous READY boundary until decoded video and owned PCM have +* useful reserves. Silent and short streams satisfy preroll through track +* absence or a terminal marker. A bounded timeout limits initialization delay. */ static void webm_wait_for_preroll(webm_context *ctx) { @@ -1220,7 +1210,7 @@ static void webm_wait_for_preroll(webm_context *ctx) if(now < start_time || now - start_time >= WEBM_PREROLL_TIMEOUT_MICROSECONDS) { printf( - "Warning: WebM preroll timed out with video=%d/%d " + "\nWarning: WebM preroll timed out with video=%d/%d " "decoded frames and audio=%u/%u PCM buffers.\n", decoded_frame_count, VIDEO_PREROLL_FRAME_COUNT, @@ -1239,8 +1229,8 @@ static void webm_wait_for_preroll(webm_context *ctx) * Caskey, Damon V. * 2026-08-12 * -* Initialize WebM audio on an explicit generic channel with -* play-ID ownership and optional legacy replace-all behavior. +* Initialize WebM audio on an explicit generic channel with play-ID ownership +* and optional exclusive audio control. */ static int init_audio( nestegg *ctx, @@ -1303,14 +1293,14 @@ static int init_audio( if(audio_ctx->frequency < SOUND_MUSIC_FREQUENCY_MIN || audio_ctx->frequency > SOUND_MUSIC_FREQUENCY_MAX) { - printf("Warning: the audio frequency (%i Hz) is outside the supported %i-%i Hz range\n", + printf("\nWarning: the audio frequency (%i Hz) is outside the supported %i-%i Hz range\n", audio_ctx->frequency, SOUND_MUSIC_FREQUENCY_MIN, SOUND_MUSIC_FREQUENCY_MAX); } if(replace_all_audio) { - /* Legacy fullscreen playback retains its replace-all behavior. */ + /* Fullscreen playback requests exclusive audio ownership. */ sound_stopall_sample(true); } audio_ctx->channel = sound_channel; @@ -1636,7 +1626,7 @@ static void close_video(video_context *video_ctx) { if(vpx_codec_destroy(&(video_ctx->vpx_ctx))) { - printf("Warning: failed to destroy libvpx context: %s\n", vpx_codec_error(&video_ctx->vpx_ctx)); + printf("\nWarning: failed to destroy libvpx context: %s\n", vpx_codec_error(&video_ctx->vpx_ctx)); } if(video_ctx->packet_queue) { @@ -1674,7 +1664,7 @@ static void video_packet_queue_report_resync(FixedSizeQueue *queue) if(resync_count && (resync_count == 1U || !(resync_count % UINT64_C(16)))) { printf( - "Warning: WebM video backlog recovery discarded %" PRIu64 + "\nWarning: WebM video backlog recovery discarded %" PRIu64 " compressed packets in %" PRIu64 " keyframe resync event%s.\n", dropped_packets, @@ -2023,7 +2013,7 @@ static int webm_open_resources(webm_context *ctx) } else { printf( - "Warning: Unable to open the WebM audio track on channel %d\n", + "\nWarning: Unable to open the WebM audio track on channel %d\n", ctx->sound_channel ); ctx->audio_track = -1; @@ -2032,10 +2022,10 @@ static int webm_open_resources(webm_context *ctx) else if(audio_track < 0 && ctx->replace_all_audio && sound_query_music(NULL, NULL)) { - /* Blocking legacy playback retains its channel-zero service. */ + /* Service channel-zero music during silent fullscreen playback. */ ctx->the_audio_thread = thread_create(bgm_update_thread, "bgm", ctx); if(!ctx->the_audio_thread) { - printf("Error: Unable to start WebM legacy audio service thread\n"); + printf("Error: Unable to start WebM audio service thread\n"); return -1; } } @@ -2263,9 +2253,8 @@ int webm_poll_closed(webm_context *ctx) * Caskey, Damon V. * 2026-08-17 * -* Provide a blocking finalizer only for engine shutdown and legacy ownership -* boundaries. Runtime movie stop and replacement use request-plus-poll so -* teardown never joins decoder workers on an engine update. +* Finalize decoder ownership synchronously where blocking is permitted. +* Runtime movie stop and replacement use asynchronous request and poll calls. */ void webm_close(webm_context *ctx) { @@ -2410,9 +2399,8 @@ void webm_set_audio_speed(webm_context *ctx, double speed) * Caskey, Damon V. * 2026-08-15 * -* Snapshot decoded-frame occupancy for one bounded main-thread poll. Frames -* published after this count is captured remain queued for the next engine -* update instead of extending the current update indefinitely. +* Snapshot decoded-frame occupancy to bound dequeue attempts during the current +* engine update. Frames published afterward remain queued for the next update. */ int webm_get_pending_frame_count(webm_context *ctx) {