|
1 | | -#include <emscripten/threading.h> |
2 | 1 | #include <emscripten/wasm_worker.h> |
3 | 2 | #include <emscripten/webaudio.h> |
4 | 3 | #include <assert.h> |
|
7 | 6 | // |
8 | 7 | // - _emscripten_thread_supports_atomics_wait() |
9 | 8 | // - emscripten_lock_init() |
10 | | -// - emscripten_lock_try_acquire() |
11 | 9 | // - emscripten_lock_busyspin_wait_acquire() |
12 | | -// - emscripten_lock_busyspin_waitinf_acquire() |
13 | 10 | // - emscripten_lock_release() |
14 | | -// - emscripten_get_now() |
| 11 | +// - emscripten_get_now() in AW |
| 12 | + |
| 13 | +// Build with emcc -sAUDIO_WORKLET -sWASM_WORKERS -pthread -O1 -g -o index.html audioworklet_emscripten_locks.c |
| 14 | + |
| 15 | +// Values -1.5373, 77.2259, -251.4728 |
| 16 | +// Values -0.9080, -42.4902, -250.6685 |
| 17 | + |
| 18 | +// Marks a function to be kept in the Module and exposed to script (instead of adding to EXPORTED_FUNCTIONS) |
| 19 | +#ifndef KEEP_IN_MODULE |
| 20 | +#define KEEP_IN_MODULE __attribute__((used, visibility("default"))) |
| 21 | +#endif |
| 22 | + |
| 23 | +// This needs to be big enough for a stereo output (1024 with a 128 frame) + working stack |
| 24 | +#define AUDIO_STACK_SIZE 2048 |
15 | 25 |
|
16 | 26 | // Internal, found in 'system/lib/pthread/threading_internal.h' (and requires building with -pthread) |
17 | 27 | int _emscripten_thread_supports_atomics_wait(void); |
18 | 28 |
|
19 | 29 | typedef enum { |
20 | | - // No wait support in audio worklets |
21 | | - TEST_HAS_WAIT, |
22 | | - // Acquired in main, fail in process |
23 | | - TEST_TRY_ACQUIRE, |
24 | | - // Keep acquired so time-out |
25 | | - TEST_WAIT_ACQUIRE_FAIL, |
26 | | - // Release in main, succeed in process |
27 | | - TEST_WAIT_ACQUIRE, |
28 | | - // Release in process after above |
29 | | - TEST_RELEASE, |
30 | | - // Released in process above, spin in main |
31 | | - TEST_WAIT_INFINTE_1, |
32 | | - // Release in process to stop spinning in main |
33 | | - TEST_WAIT_INFINTE_2, |
34 | | - // Call emscripten_get_now() in process |
35 | | - TEST_GET_NOW, |
| 30 | + // The test hasn't yet started |
| 31 | + TEST_NOT_STARTED, |
| 32 | + // Worklet ready and running the test |
| 33 | + TEST_RUNNING, |
| 34 | + // Main thread is finished, wait on worklet |
| 35 | + TEST_DONE_MAIN, |
36 | 36 | // Test finished |
37 | 37 | TEST_DONE |
38 | 38 | } Test; |
39 | 39 |
|
| 40 | +// Global audio context |
| 41 | +EMSCRIPTEN_WEBAUDIO_T context; |
40 | 42 | // Lock used in all the tests |
41 | 43 | emscripten_lock_t testLock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; |
42 | 44 | // Which test is running (sometimes in the worklet, sometimes in the main thread) |
43 | | -_Atomic Test whichTest = TEST_HAS_WAIT; |
| 45 | +_Atomic Test whichTest = TEST_NOT_STARTED; |
44 | 46 | // Time at which the test starts taken in main() |
45 | 47 | double startTime = 0; |
46 | 48 |
|
47 | | -bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) { |
48 | | - assert(emscripten_current_thread_is_audio_worklet()); |
| 49 | +// Counter for main, accessed only by main |
| 50 | +int howManyMain = 0; |
| 51 | +// Counter for the audio worklet, accessed only by the AW |
| 52 | +int howManyProc = 0; |
| 53 | + |
| 54 | +// Our dummy container |
| 55 | +typedef struct Dummy { |
| 56 | + uint32_t val0; |
| 57 | + uint32_t val1; |
| 58 | + uint32_t val2; |
| 59 | +} Dummy; |
| 60 | + |
| 61 | +// Start values |
| 62 | +void initDummy(Dummy* dummy) { |
| 63 | + dummy->val0 = 4; |
| 64 | + dummy->val1 = 1; |
| 65 | + dummy->val2 = 2; |
| 66 | +} |
49 | 67 |
|
50 | | - // Produce at few empty frames of audio before we start trying to interact |
51 | | - // with the with main thread. |
52 | | - // On chrome at least it appears the main thread completely blocks until |
53 | | - // a few frames have been produced. This means it may not be safe to interact |
54 | | - // with the main thread during initial frames? |
55 | | - // In my experiments it seems like 5 was the magic number that I needed to |
56 | | - // produce before the main thread could continue to run. |
57 | | - // See https://github.com/emscripten-core/emscripten/issues/24213 |
58 | | - static int count = 0; |
59 | | - if (count++ < 5) return true; |
60 | | - |
61 | | - int result = 0; |
62 | | - switch (whichTest) { |
63 | | - case TEST_HAS_WAIT: |
64 | | - // Should not have wait support here |
65 | | - result = _emscripten_thread_supports_atomics_wait(); |
66 | | - emscripten_outf("TEST_HAS_WAIT: %d (expect: 0)", result); |
67 | | - assert(!result); |
68 | | - whichTest = TEST_TRY_ACQUIRE; |
69 | | - break; |
70 | | - case TEST_TRY_ACQUIRE: |
71 | | - // Was locked after init, should fail to acquire |
72 | | - result = emscripten_lock_try_acquire(&testLock); |
73 | | - emscripten_outf("TEST_TRY_ACQUIRE: %d (expect: 0)", result); |
74 | | - assert(!result); |
75 | | - whichTest = TEST_WAIT_ACQUIRE_FAIL; |
76 | | - break; |
77 | | - case TEST_WAIT_ACQUIRE_FAIL: |
78 | | - // Still locked so we fail to acquire |
79 | | - result = emscripten_lock_busyspin_wait_acquire(&testLock, 100); |
80 | | - emscripten_outf("TEST_WAIT_ACQUIRE_FAIL: %d (expect: 0)", result); |
81 | | - assert(!result); |
82 | | - whichTest = TEST_WAIT_ACQUIRE; |
83 | | - case TEST_WAIT_ACQUIRE: |
84 | | - // Will get unlocked in main thread, so should quickly acquire |
85 | | - result = emscripten_lock_busyspin_wait_acquire(&testLock, 10000); |
86 | | - emscripten_outf("TEST_WAIT_ACQUIRE: %d (expect: 1)", result); |
87 | | - assert(result); |
88 | | - whichTest = TEST_RELEASE; |
89 | | - break; |
90 | | - case TEST_RELEASE: |
91 | | - // Unlock, check the result |
| 68 | +void printDummy(Dummy* dummy) { |
| 69 | + emscripten_outf("Values: %u, %u, %u", dummy->val0, dummy->val1, dummy->val2); |
| 70 | +} |
| 71 | + |
| 72 | +// Run a simple calculation that will only be stable *if* all values are atomically updated |
| 73 | +void runCalcs(Dummy* dummy, int num) { |
| 74 | + for (int n = 0; n < num; n++) { |
| 75 | + int have = emscripten_lock_busyspin_wait_acquire(&testLock, 100); |
| 76 | + assert(have); |
| 77 | + dummy->val0 += dummy->val1 * dummy->val2; |
| 78 | + dummy->val1 += dummy->val2 * dummy->val0; |
| 79 | + dummy->val2 += dummy->val0 * dummy->val1; |
| 80 | + dummy->val0 /= 4; |
| 81 | + dummy->val1 /= 3; |
| 82 | + dummy->val2 /= 2; |
92 | 83 | emscripten_lock_release(&testLock); |
93 | | - result = emscripten_lock_try_acquire(&testLock); |
94 | | - emscripten_outf("TEST_RELEASE: %d (expect: 1)", result); |
95 | | - assert(result); |
96 | | - whichTest = TEST_WAIT_INFINTE_1; |
97 | | - break; |
98 | | - case TEST_WAIT_INFINTE_1: |
99 | | - // Still locked when we enter here but move on in the main thread |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +void stopping() { |
| 88 | + emscripten_out("Expect: 949807601, 1303780836, 243502614"); |
| 89 | + emscripten_out("Ending test"); |
| 90 | + emscripten_destroy_audio_context(context); |
| 91 | + emscripten_force_exit(0); |
| 92 | +} |
| 93 | + |
| 94 | +// AW callback |
| 95 | +bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) { |
| 96 | + assert(emscripten_current_thread_is_audio_worklet()); |
| 97 | + switch (whichTest) { |
| 98 | + case TEST_NOT_STARTED: |
| 99 | + whichTest = TEST_RUNNING; |
100 | 100 | break; |
101 | | - case TEST_WAIT_INFINTE_2: |
102 | | - emscripten_lock_release(&testLock); |
103 | | - whichTest = TEST_GET_NOW; |
| 101 | + case TEST_RUNNING: |
| 102 | + case TEST_DONE_MAIN: |
| 103 | + if (howManyProc-- > 0) { |
| 104 | + runCalcs((Dummy*) data, 250); |
| 105 | + } else { |
| 106 | + if (whichTest == TEST_DONE_MAIN) { |
| 107 | + // Both loops are finished |
| 108 | + whichTest = TEST_DONE; |
| 109 | + } |
| 110 | + } |
104 | 111 | break; |
105 | | - case TEST_GET_NOW: |
106 | | - result = (int) (emscripten_get_now() - startTime); |
107 | | - emscripten_outf("TEST_GET_NOW: %d (expect: > 0)", result); |
108 | | - assert(result > 0); |
109 | | - whichTest = TEST_DONE; |
110 | 112 | case TEST_DONE: |
| 113 | + emscripten_outf("Took %dms (expect: > 0)", (int) (emscripten_get_now() - startTime)); |
111 | 114 | return false; |
112 | | - default: |
113 | | - break; |
114 | 115 | } |
115 | 116 | return true; |
116 | 117 | } |
117 | 118 |
|
118 | | -EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext), { |
119 | | - let startButton = document.createElement('button'); |
120 | | - startButton.innerHTML = 'Start playback'; |
121 | | - document.body.appendChild(startButton); |
122 | | - |
123 | | - audioContext = emscriptenGetAudioObject(audioContext); |
124 | | - startButton.onclick = () => { |
125 | | - audioContext.resume(); |
126 | | - }; |
127 | | -}); |
128 | | - |
129 | | -bool MainLoop(double time, void* data) { |
| 119 | +// Main thread callback |
| 120 | +bool mainLoop(double time, void* data) { |
130 | 121 | assert(!emscripten_current_thread_is_audio_worklet()); |
131 | | - static int didUnlock = false; |
132 | 122 | switch (whichTest) { |
133 | | - case TEST_WAIT_ACQUIRE: |
134 | | - if (!didUnlock) { |
135 | | - emscripten_out("main thread releasing lock"); |
136 | | - // Release here to acquire in process |
137 | | - emscripten_lock_release(&testLock); |
138 | | - didUnlock = true; |
| 123 | + case TEST_NOT_STARTED: |
| 124 | + break; |
| 125 | + case TEST_RUNNING: |
| 126 | + if (howManyMain-- > 0) { |
| 127 | + runCalcs((Dummy*) data, 1000); |
| 128 | + } else { |
| 129 | + // Done here, so signal to process() |
| 130 | + whichTest = TEST_DONE_MAIN; |
139 | 131 | } |
140 | 132 | break; |
141 | | - case TEST_WAIT_INFINTE_1: |
142 | | - // Spin here until released in process (but don't change test until we know this case ran) |
143 | | - whichTest = TEST_WAIT_INFINTE_2; |
144 | | - emscripten_lock_busyspin_waitinf_acquire(&testLock); |
145 | | - emscripten_out("TEST_WAIT_INFINTE (from main)"); |
| 133 | + case TEST_DONE_MAIN: |
| 134 | + // Wait for process() to finish |
146 | 135 | break; |
147 | 136 | case TEST_DONE: |
148 | | - // Finished, exit from the main thread |
149 | | - emscripten_out("Test success"); |
150 | | - emscripten_force_exit(0); |
| 137 | + printDummy((Dummy*) data); |
| 138 | + // 32-bit maths with locks *should* result in these: |
| 139 | + assert(((Dummy*) data)->val0 == 949807601 |
| 140 | + && ((Dummy*) data)->val1 == 1303780836 |
| 141 | + && ((Dummy*) data)->val2 == 243502614); |
| 142 | + stopping(); |
151 | 143 | return false; |
152 | | - default: |
153 | | - break; |
154 | 144 | } |
155 | 145 | return true; |
156 | 146 | } |
157 | 147 |
|
158 | | -void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) { |
159 | | - int outputChannelCounts[1] = { 1 }; |
160 | | - EmscriptenAudioWorkletNodeCreateOptions options = { .numberOfInputs = 0, .numberOfOutputs = 1, .outputChannelCounts = outputChannelCounts }; |
161 | | - EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "noise-generator", &options, &ProcessAudio, NULL); |
162 | | - emscripten_audio_node_connect(wasmAudioWorklet, audioContext, 0, 0); |
163 | | - InitHtmlUi(audioContext); |
| 148 | +KEEP_IN_MODULE void startTest() { |
| 149 | + startTime = emscripten_get_now(); |
| 150 | + if (emscripten_audio_context_state(context) != AUDIO_CONTEXT_STATE_RUNNING) { |
| 151 | + emscripten_resume_audio_context_sync(context); |
| 152 | + } |
| 153 | + howManyMain = 200; |
| 154 | + howManyProc = 200; |
164 | 155 | } |
165 | 156 |
|
166 | | -void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) { |
167 | | - WebAudioWorkletProcessorCreateOptions opts = { .name = "noise-generator" }; |
168 | | - emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, NULL); |
| 157 | +// HTML button to manually run the test |
| 158 | +EM_JS(void, addButton, (), { |
| 159 | + var button = document.createElement("button"); |
| 160 | + button.appendChild(document.createTextNode("Start Test")); |
| 161 | + document.body.appendChild(button); |
| 162 | + document.onclick = () => { |
| 163 | + if (globalThis._startTest) { |
| 164 | + _startTest(); |
| 165 | + } |
| 166 | + }; |
| 167 | +}); |
| 168 | + |
| 169 | +// Audio processor created, now register the audio callback |
| 170 | +void processorCreated(EMSCRIPTEN_WEBAUDIO_T ctx, bool success, void* data) { |
| 171 | + assert(success && "Audio worklet failed in processorCreated()"); |
| 172 | + emscripten_out("Audio worklet processor created"); |
| 173 | + // Single stereo output |
| 174 | + int outputChannelCounts[1] = { 1 }; |
| 175 | + EmscriptenAudioWorkletNodeCreateOptions opts = { |
| 176 | + .numberOfOutputs = 1, |
| 177 | + .outputChannelCounts = outputChannelCounts |
| 178 | + }; |
| 179 | + EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(ctx, "locks-test", &opts, &process, data); |
| 180 | + emscripten_audio_node_connect(worklet, ctx, 0, 0); |
169 | 181 | } |
170 | 182 |
|
171 | | -uint8_t wasmAudioWorkletStack[2048]; |
| 183 | +// Worklet thread inited, now create the audio processor |
| 184 | +void initialised(EMSCRIPTEN_WEBAUDIO_T ctx, bool success, void* data) { |
| 185 | + assert(success && "Audio worklet failed in initialised()"); |
| 186 | + emscripten_out("Audio worklet initialised"); |
| 187 | + WebAudioWorkletProcessorCreateOptions opts = { |
| 188 | + .name = "locks-test" |
| 189 | + }; |
| 190 | + emscripten_create_wasm_audio_worklet_processor_async(ctx, &opts, &processorCreated, data); |
| 191 | +} |
172 | 192 |
|
173 | 193 | int main() { |
174 | | - // Main thread init and acquire (work passes to the processor) |
175 | 194 | emscripten_lock_init(&testLock); |
176 | | - int hasLock = emscripten_lock_busyspin_wait_acquire(&testLock, 0); |
177 | | - assert(hasLock); |
178 | | - |
179 | | - startTime = emscripten_get_now(); |
180 | | - |
181 | | - emscripten_set_timeout_loop(MainLoop, 10, NULL); |
182 | | - EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(NULL); |
183 | | - emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, NULL); |
| 195 | + Dummy* dummy = (Dummy*) malloc(sizeof(Dummy)); |
| 196 | + initDummy(dummy); |
| 197 | + |
| 198 | + char* const workletStack = memalign(16, AUDIO_STACK_SIZE); |
| 199 | + assert(workletStack); |
| 200 | + // Audio processor callback setup |
| 201 | + context = emscripten_create_audio_context(NULL); |
| 202 | + assert(context); |
| 203 | + emscripten_start_wasm_audio_worklet_thread_async(context, workletStack, AUDIO_STACK_SIZE, initialised, dummy); |
| 204 | + |
| 205 | + emscripten_set_timeout_loop(mainLoop, 10, dummy); |
| 206 | + addButton(); |
| 207 | + startTest(); // <-- May need a manual click to start |
184 | 208 |
|
185 | 209 | emscripten_exit_with_live_runtime(); |
186 | 210 | } |
0 commit comments