Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5619,7 +5619,7 @@ def test_audio_worklet_params_mixing(self, args):
@also_with_minimal_runtime
@flaky('https://github.com/emscripten-core/emscripten/issues/25245')
def test_audio_worklet_emscripten_locks(self):
self.btest_exit('webaudio/audioworklet_emscripten_locks.c', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread'])
self.btest_exit('webaudio/audioworklet_emscripten_locks.c', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread', '-DRUN_ON_WORKER'])

# Verifies setting audio context sample rate, and that emscripten_audio_context_sample_rate() works.
@requires_sound_hardware
Expand Down
158 changes: 110 additions & 48 deletions test/webaudio/audioworklet_emscripten_locks.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@
// - emscripten_lock_busyspin_waitinf_acquire()
// - emscripten_lock_release()
// - emscripten_get_now()
//
// Define RUN_ON_WORKER to run on a worker, otherwise it runs on the main thread
// Define QUIETEN_PRINTING to remove most of the emscripten_out() calls

// Global audio context
EMSCRIPTEN_WEBAUDIO_T context;

// Internal, found in 'system/lib/pthread/threading_internal.h' (and requires building with -pthread)
int _emscripten_thread_supports_atomics_wait(void);

typedef enum {
// No wait support in audio worklets
// The test hasn't yet started
TEST_NOT_STARTED,
// No atomics wait support in audio worklets
TEST_HAS_WAIT,
// Acquired in main, fail in process
TEST_TRY_ACQUIRE,
Expand All @@ -39,74 +47,91 @@ typedef enum {

// Lock used in all the tests
emscripten_lock_t testLock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER;
// Which test is running (sometimes in the worklet, sometimes in the main thread)
_Atomic Test whichTest = TEST_HAS_WAIT;
// Which test is running (sometimes in the worklet, sometimes in the worker/main)
_Atomic Test whichTest = TEST_NOT_STARTED;
// Time at which the test starts taken in main()
double startTime = 0;
// Has TEST_WAIT_ACQUIRE unlocked testLock?
int waitAcquireUnlocked = false;

void do_exit() {
emscripten_out("Test success");
emscripten_destroy_audio_context(context);
#ifdef RUN_ON_WORKER
emscripten_terminate_all_wasm_workers();
#endif
emscripten_force_exit(0);
}

bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) {
assert(emscripten_current_thread_is_audio_worklet());

// Produce at few empty frames of audio before we start trying to interact
// with the with main thread.
// On chrome at least it appears the main thread completely blocks until
// a few frames have been produced. This means it may not be safe to interact
// with the main thread during initial frames?
// In my experiments it seems like 5 was the magic number that I needed to
// produce before the main thread could continue to run.
// See https://github.com/emscripten-core/emscripten/issues/24213
static int count = 0;
if (count++ < 5) return true;

int result = 0;
switch (whichTest) {
case TEST_NOT_STARTED:
break;
case TEST_HAS_WAIT:
// Should not have wait support here
result = _emscripten_thread_supports_atomics_wait();
#ifndef QUIETEN_PRINTING
emscripten_outf("TEST_HAS_WAIT: %d (expect: 0)", result);
#endif
assert(!result);
whichTest = TEST_TRY_ACQUIRE;
break;
case TEST_TRY_ACQUIRE:
// Was locked after init, should fail to acquire
result = emscripten_lock_try_acquire(&testLock);
#ifndef QUIETEN_PRINTING
emscripten_outf("TEST_TRY_ACQUIRE: %d (expect: 0)", result);
#endif
assert(!result);
whichTest = TEST_WAIT_ACQUIRE_FAIL;
break;
case TEST_WAIT_ACQUIRE_FAIL:
// Still locked so we fail to acquire
result = emscripten_lock_busyspin_wait_acquire(&testLock, 100);
#ifndef QUIETEN_PRINTING
emscripten_outf("TEST_WAIT_ACQUIRE_FAIL: %d (expect: 0)", result);
#endif
assert(!result);
whichTest = TEST_WAIT_ACQUIRE;
// Fall through here so the worker/main has a chance to unlock whilst spinning
// (otherwise the lock has a chance to be released before spinning).
case TEST_WAIT_ACQUIRE:
// Will get unlocked in main thread, so should quickly acquire
result = emscripten_lock_busyspin_wait_acquire(&testLock, 10000);
// Will get unlocked in worker/main, so should quickly acquire
result = emscripten_lock_busyspin_wait_acquire(&testLock, 1000);
#ifndef QUIETEN_PRINTING
emscripten_outf("TEST_WAIT_ACQUIRE: %d (expect: 1)", result);
#endif
assert(result);
whichTest = TEST_RELEASE;
break;
case TEST_RELEASE:
// Unlock, check the result
emscripten_lock_release(&testLock);
result = emscripten_lock_try_acquire(&testLock);
#ifndef QUIETEN_PRINTING
emscripten_outf("TEST_RELEASE: %d (expect: 1)", result);
#endif
assert(result);
whichTest = TEST_WAIT_INFINTE_1;
break;
case TEST_WAIT_INFINTE_1:
// Still locked when we enter here but move on in the main thread
// Still locked when we enter here but move on in the worker/main
break;
case TEST_WAIT_INFINTE_2:
emscripten_lock_release(&testLock);
whichTest = TEST_GET_NOW;
break;
case TEST_GET_NOW:
result = (int) (emscripten_get_now() - startTime);
#ifndef QUIETEN_PRINTING
emscripten_outf("TEST_GET_NOW: %d (expect: > 0)", result);
#endif
assert(result > 0);
whichTest = TEST_DONE;
// Fall through here and stop playback (shutting down in the worker/main)
case TEST_DONE:
return false;
default:
Expand All @@ -117,70 +142,107 @@ bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs,

EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext), {
let startButton = document.createElement('button');
startButton.innerHTML = 'Start playback';
startButton.innerHTML = 'Start Test';
document.body.appendChild(startButton);

audioContext = emscriptenGetAudioObject(audioContext);
startButton.onclick = () => {
audioContext.resume();
document.body.removeChild(startButton);
};
});

#ifdef RUN_ON_WORKER
void WorkerLoop() {
#else
bool MainLoop(double time, void* data) {
#endif
assert(!emscripten_current_thread_is_audio_worklet());
static int didUnlock = false;
switch (whichTest) {
case TEST_WAIT_ACQUIRE:
if (!didUnlock) {
emscripten_out("main thread releasing lock");
// Release here to acquire in process
emscripten_lock_release(&testLock);
didUnlock = true;
}
break;
case TEST_WAIT_INFINTE_1:
// Spin here until released in process (but don't change test until we know this case ran)
whichTest = TEST_WAIT_INFINTE_2;
emscripten_lock_busyspin_waitinf_acquire(&testLock);
emscripten_out("TEST_WAIT_INFINTE (from main)");
break;
case TEST_DONE:
// Finished, exit from the main thread
emscripten_out("Test success");
emscripten_force_exit(0);
return false;
default:
break;
#ifdef RUN_ON_WORKER
while (true) {
#endif
switch (whichTest) {
case TEST_NOT_STARTED:
emscripten_out("Staring test (may need a button click)");
whichTest = TEST_HAS_WAIT;
break;
case TEST_WAIT_ACQUIRE:
if (!waitAcquireUnlocked) {
// Release here to acquire in process
emscripten_lock_release(&testLock);
#ifndef QUIETEN_PRINTING
emscripten_out("TEST_WAIT_ACQUIRE: worker/main released lock");
#endif
waitAcquireUnlocked = true;
}
break;
case TEST_WAIT_INFINTE_1:
// Spin here until released in process (but don't change test until we know this case ran)
whichTest = TEST_WAIT_INFINTE_2;
emscripten_lock_busyspin_waitinf_acquire(&testLock);
#ifndef QUIETEN_PRINTING
emscripten_out("TEST_WAIT_INFINTE (from worker/main)");
#endif
break;
case TEST_DONE:
// Finished, exit from the main thread (and return out of this loop)
#ifdef RUN_ON_WORKER
emscripten_wasm_worker_post_function_v(EMSCRIPTEN_WASM_WORKER_ID_PARENT, &do_exit);
// WorkerLoop contract (end the worker)
return;
#else
do_exit();
// MainLoop contract (stop the repeats)
return false;
#endif
default:
break;
}
#ifdef RUN_ON_WORKER
// Repeat every 10ms (except when TEST_DONE)
emscripten_wasm_worker_sleep(10 * 1000000ULL);
}
return true;
#else
// Run again
return true;
#endif
}

void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) {
int outputChannelCounts[1] = { 1 };
EmscriptenAudioWorkletNodeCreateOptions options = { .numberOfInputs = 0, .numberOfOutputs = 1, .outputChannelCounts = outputChannelCounts };
EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "noise-generator", &options, &ProcessAudio, NULL);
EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "locks-test", &options, &ProcessAudio, NULL);
emscripten_audio_node_connect(wasmAudioWorklet, audioContext, 0, 0);
InitHtmlUi(audioContext);
}

void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) {
WebAudioWorkletProcessorCreateOptions opts = { .name = "noise-generator" };
WebAudioWorkletProcessorCreateOptions opts = { .name = "locks-test" };
emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, NULL);
}

uint8_t wasmAudioWorkletStack[2048];

int main() {
// Main thread init and acquire (work passes to the processor)
// Main thread init and acquire (work passes to the audio processor)
emscripten_lock_init(&testLock);
int hasLock = emscripten_lock_busyspin_wait_acquire(&testLock, 0);
assert(hasLock);

startTime = emscripten_get_now();

emscripten_set_timeout_loop(MainLoop, 10, NULL);
EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(NULL);
// Audio processor callback setup
context = emscripten_create_audio_context(NULL);
assert(context);
InitHtmlUi(context);
emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, NULL);

// Either call every 10ms or create a worker that sleeps every 10ms
#ifdef RUN_ON_WORKER
emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(1024);
emscripten_wasm_worker_post_function_v(worker, WorkerLoop);
#else
emscripten_set_timeout_loop(MainLoop, 10, NULL);
#endif

emscripten_exit_with_live_runtime();
}