Skip to content

Commit

Permalink
Support running Emscripten-compiled code from AudioWorklets (almost) …
Browse files Browse the repository at this point in the history
…as if they were regular Worker pthreads
  • Loading branch information
tklajnscek committed Jun 14, 2021
1 parent a8a0d8e commit cadcdd9
Show file tree
Hide file tree
Showing 13 changed files with 1,785 additions and 30 deletions.
17 changes: 15 additions & 2 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
}


VALID_ENVIRONMENTS = ('web', 'webview', 'worker', 'node', 'shell')
VALID_ENVIRONMENTS = ('web', 'webview', 'worker', 'node', 'shell', 'audioworklet')
SIMD_INTEL_FEATURE_TOWER = ['-msse', '-msse2', '-msse3', '-mssse3', '-msse4.1', '-msse4.2', '-mavx']
SIMD_NEON_FLAGS = ['-mfpu=neon']

Expand Down Expand Up @@ -286,6 +286,9 @@ def setup_environment_settings():
'worker' in environments or \
(settings.ENVIRONMENT_MAY_BE_NODE and settings.USE_PTHREADS)

# Worklet environment must be enabled explicitly for now
settings.ENVIRONMENT_MAY_BE_AUDIOWORKLET = 'audioworklet' in environments

if not settings.ENVIRONMENT_MAY_BE_WORKER and settings.PROXY_TO_WORKER:
exit_with_error('If you specify --proxy-to-worker and specify a "-s ENVIRONMENT=" directive, it must include "worker" as a target! (Try e.g. -s ENVIRONMENT=web,worker)')

Expand Down Expand Up @@ -3248,7 +3251,17 @@ def modularize():
with open(final_js, 'w') as f:
f.write(src)

# Export using a UMD style export, or ES6 exports if selected
# Add ; if src doesn't contain it so we generate valid JS (otherwise acorn fails)
if src.rstrip()[-1] != ';':
f.write(';')

# Store the export on the global scope for audio worklets
if settings.ENVIRONMENT_MAY_BE_AUDIOWORKLET:
f.write('''if (typeof AudioWorkletGlobalScope === 'function')
globalThis["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s;
''' % {
'EXPORT_NAME': settings.EXPORT_NAME
})

if settings.EXPORT_ES6:
f.write('export default %s;' % settings.EXPORT_NAME)
Expand Down
154 changes: 135 additions & 19 deletions src/library_pthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,14 +403,23 @@ var LibraryPThread = {
PThread.receiveObjectTransfer(e.data);
} else if (e.data.target === 'setimmediate') {
worker.postMessage(e.data); // Worker wants to postMessage() to itself to implement setImmediate() emulation.
} else {
}
#if ENVIRONMENT_MAY_BE_AUDIOWORKLET
else if (cmd === 'running') {
// This is only notified for audio worklet contexts to let us know the pthread environment
// is ready and we can go ahead and create any pending AudioWorkletNodes
worker.context.audioWorklet.pthread.resolveRunPromise();
delete worker.context.audioWorklet.pthread.resolveRunPromise;
}
#endif
else {
err("worker sent an unknown command " + cmd);
}
PThread.currentProxiedOperationCallerThread = undefined;
};

worker.onerror = function(e) {
err('pthread sent an error! ' + e.filename + ':' + e.lineno + ': ' + e.message);
err('pthread sent an error! ' + e.stack ? e.stack : (e.filename + ':' + e.lineno + ': ' + e.message));
};

#if ENVIRONMENT_MAY_BE_NODE
Expand Down Expand Up @@ -523,7 +532,78 @@ var LibraryPThread = {
while (performance.now() < t) {
;
}
},

#if ENVIRONMENT_MAY_BE_AUDIOWORKLET
// Initializes a pthread in the AudioWorkletGlobalScope for this audio context
initAudioWorkletPThread: function(audioCtx, pthreadPtr) {
var aw = audioCtx.audioWorklet;
assert(!aw.pthread, "Can't call initAudioWorkletPThread twice on the same audio context");
aw.pthread = {};

// First, load the code into the worklet context. The .worker.js first, then the main script.
#if MINIMAL_RUNTIME
var pthreadMainJs = Module['worker'];
#else
var pthreadMainJs = locateFile('{{{ PTHREAD_WORKER_FILE }}}');
#endif
Promise.all([
aw.addModule(pthreadMainJs),
aw.addModule(Module['mainScriptUrlOrBlob'] || _scriptDir)
]).then(function() {
// Create a dummy worklet node that we use to establish the message channel
var dummyWorklet = new AudioWorkletNode(audioCtx, 'pthread-dummy-processor', {
numberOfInputs: 0,
numberOfOutputs : 1,
outputChannelCount : [1]
})

// Push this dummyWorklet into the PThread internal worker pool so it
// gets picked up in _pthread_create below.
PThread.unusedWorkers.push(dummyWorklet);

// Add postMessage directly on the object, forwarded to port.postMessage (emulates Worker)
dummyWorklet.postMessage = dummyWorklet.port.postMessage.bind(dummyWorklet.port);

// We still call loadWasmModuleToWorker to setup the pthread environment,
// but we skip the actual WASM loading since it's already been done via
// addModule above.
PThread.loadWasmModuleToWorker(dummyWorklet);

// Forward port.onMessage to onmessage on the object (emulates Worker)
dummyWorklet.port.onmessage = dummyWorklet.onmessage;

// Call pthread_create to have Emscripten init our worklet pthread globals as if
// it was a regular worker
_pthread_create(pthreadPtr, 0, 0, 0);

aw.pthread.dummyWorklet = dummyWorklet;
}, function(err) {
aw.pthread.rejectRunPromise(err);
delete aw.pthread;
});

aw.pthread.runPromise = new Promise(function(resolve, reject) {
aw.pthread.resolveRunPromise = resolve;
aw.pthread.rejectRunPromise = reject;
});

return aw.pthread.runPromise;
},

// Creates an AudioWorkletNode on the specified audio context.
// initAudioWorkletPThread must've been called on the audio context before.
createAudioWorklet: function(audioCtx, processorName, processorOpts) {
assert(audioCtx.audioWorklet.pthread.runPromise, "Call initAudioWorkletPThread once before calling createAudioWorklet");
return new Promise(function(resolve, reject) {
audioCtx.audioWorklet.pthread.runPromise.then(function() {
resolve(new AudioWorkletNode(audioCtx, processorName, processorOpts));
}, function(err) {
reject(err);
})
});
}
#endif
},

$killThread: function(pthread_ptr) {
Expand Down Expand Up @@ -1077,8 +1157,8 @@ var LibraryPThread = {
emscripten_futex_wait__deps: ['emscripten_main_thread_process_queued_calls'],
emscripten_futex_wait: function(addr, val, timeout) {
if (addr <= 0 || addr > HEAP8.length || addr&3 != 0) return -{{{ cDefine('EINVAL') }}};
// We can do a normal blocking wait anywhere but on the main browser thread.
if (!ENVIRONMENT_IS_WEB) {
// We can do a normal blocking wait anywhere but on the main browser thread or in an audio worklet.
if (!ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_AUDIOWORKLET) {
#if PTHREADS_PROFILING
PThread.setThreadStatusConditional(_pthread_self(), {{{ cDefine('EM_THREAD_STATUS_RUNNING') }}}, {{{ cDefine('EM_THREAD_STATUS_WAITFUTEX') }}});
#endif
Expand Down Expand Up @@ -1115,10 +1195,20 @@ var LibraryPThread = {
// ourselves before calling the potentially-recursive call. See below for
// how we handle the case of our futex being notified during the time in
// between when we are not set as the value of __emscripten_main_thread_futex.
//
// For audio worklets we use the same global address since they all run on
// the audio thread. It's all very similar to the main thread case, except we
// don't have to do any nested call special casing.
var theFutex = __emscripten_main_thread_futex;
#if ENVIRONMENT_MAY_BE_AUDIOWORKLET
if (ENVIRONMENT_IS_AUDIOWORKLET) {
theFutex = __emscripten_audio_worklet_futex;
}
#endif
#if ASSERTIONS
assert(__emscripten_main_thread_futex > 0);
assert(theFutex > 0);
#endif
var lastAddr = Atomics.exchange(HEAP32, __emscripten_main_thread_futex >> 2, addr);
var lastAddr = Atomics.exchange(HEAP32, theFutex >> 2, addr);
#if ASSERTIONS
// We must not have already been waiting.
assert(lastAddr == 0);
Expand All @@ -1132,7 +1222,7 @@ var LibraryPThread = {
PThread.setThreadStatusConditional(_pthread_self(), {{{ cDefine('EM_THREAD_STATUS_RUNNING') }}}, {{{ cDefine('EM_THREAD_STATUS_WAITFUTEX') }}});
#endif
// We timed out, so stop marking ourselves as waiting.
lastAddr = Atomics.exchange(HEAP32, __emscripten_main_thread_futex >> 2, 0);
lastAddr = Atomics.exchange(HEAP32, theFutex >> 2, 0);
#if ASSERTIONS
// The current value must have been our address which we set, or
// in a race it was set to 0 which means another thread just allowed
Expand All @@ -1141,12 +1231,25 @@ var LibraryPThread = {
#endif
return -{{{ cDefine('ETIMEDOUT') }}};
}

#if ENVIRONMENT_MAY_BE_AUDIOWORKLET
if (ENVIRONMENT_IS_AUDIOWORKLET) {
// Audio worklet version without any special casing like for the main thread below
lastAddr = Atomics.load(HEAP32, theFutex >> 2);
if (lastAddr != addr) {
// We were told to stop waiting, so stop.
break;
}
continue;
}
#endif

// We are performing a blocking loop here, so we must handle proxied
// events from pthreads, to avoid deadlocks.
// Note that we have to do so carefully, as we may take a lock while
// doing so, which can recurse into this function; stop marking
// ourselves as waiting while we do so.
lastAddr = Atomics.exchange(HEAP32, __emscripten_main_thread_futex >> 2, 0);
lastAddr = Atomics.exchange(HEAP32, theFutex >> 2, 0);
#if ASSERTIONS
assert(lastAddr == addr || lastAddr == 0);
#endif
Expand Down Expand Up @@ -1216,33 +1319,46 @@ var LibraryPThread = {
// For Atomics.notify() API Infinity is to be passed in that case.
if (count >= {{{ cDefine('INT_MAX') }}}) count = Infinity;

// See if main thread is waiting on this address? If so, wake it up by resetting its wake location to zero.
// Note that this is not a fair procedure, since we always wake main thread first before any workers, so
// See if any spinning thread is waiting on this address? If so, wake it up by resetting its wake location to zero.
// Note that this is not a fair procedure, since we always wake these threads first before any workers, so
// this scheme does not adhere to real queue-based waiting.
// Spin-wait futexes are used on the main thread and in worklets due to the lack of Atomic.wait().
var spinFutexesWoken = 0;

#if ENVIRONMENT_MAY_BE_AUDIOWORKLET
var spinFutexes = [__emscripten_main_thread_futex, __emscripten_audio_worklet_futex];
for (var i = 0; i < spinFutexes.length; i++) {
var theFutex = spinFutexes[i];
#else
var theFutex = __emscripten_main_thread_futex;
#endif
#if ASSERTIONS
assert(__emscripten_main_thread_futex > 0);
assert(theFutex > 0);
#endif
var mainThreadWaitAddress = Atomics.load(HEAP32, __emscripten_main_thread_futex >> 2);
var mainThreadWoken = 0;
if (mainThreadWaitAddress == addr) {
var waitAddress = Atomics.load(HEAP32, theFutex >> 2);

if (waitAddress == addr) {
#if ASSERTIONS
// We only use __emscripten_main_thread_futex on the main browser thread, where we
// cannot block while we wait. Therefore we should only see it set from
// other threads, and not on the main thread itself. In other words, the
// main thread must never try to wake itself up!
assert(!ENVIRONMENT_IS_WEB);
assert(theFutex != __emscripten_main_thread_futex || !ENVIRONMENT_IS_WEB);
#endif
var loadedAddr = Atomics.compareExchange(HEAP32, __emscripten_main_thread_futex >> 2, mainThreadWaitAddress, 0);
if (loadedAddr == mainThreadWaitAddress) {
var loadedAddr = Atomics.compareExchange(HEAP32, theFutex >> 2, waitAddress, 0);
if (loadedAddr == waitAddress) {
--count;
mainThreadWoken = 1;
spinFutexesWoken += 1;
if (count <= 0) return 1;
}
}
#if ENVIRONMENT_MAY_BE_AUDIOWORKLET
} // close out the spinFutexes loop
#endif

// Wake any workers waiting on this address.
var ret = Atomics.notify(HEAP32, addr >> 2, count);
if (ret >= 0) return ret + mainThreadWoken;
if (ret >= 0) return ret + spinFutexesWoken;
throw 'Atomics.notify returned an unexpected value ' + ret;
},

Expand Down
1 change: 1 addition & 0 deletions src/minimal_runtime_worker_externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
// This file should go away in the future when worker.js is refactored to live inside the JS module.

var ENVIRONMENT_IS_PTHREAD;
var ENVIRONMENT_IS_AUDIOWORKLET;
/** @suppress {duplicate} */
var wasmMemory;
13 changes: 7 additions & 6 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -606,12 +606,13 @@ var LEGACY_VM_SUPPORT = 0;
// in node.js, or in a JS shell like d8, js, or jsc. You can set this option to
// specify that the output should only run in one particular environment, which
// must be one of
// 'web' - the normal web environment.
// 'webview' - just like web, but in a webview like Cordova;
// considered to be same as "web" in almost every place
// 'worker' - a web worker environment.
// 'node' - Node.js.
// 'shell' - a JS shell like d8, js, or jsc.
// 'web' - the normal web environment.
// 'webview' - just like web, but in a webview like Cordova;
// considered to be same as "web" in almost every place
// 'worker' - a web worker environment.
// 'audioworklet' - an audio worklet environment.
// 'node' - Node.js.
// 'shell' - a JS shell like d8, js, or jsc.
// Or it can be a comma-separated list of them, e.g., "web,worker". If this is
// the empty string, then all runtime environments are supported.
//
Expand Down
1 change: 1 addition & 0 deletions src/settings_internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ var ENVIRONMENT_MAY_BE_WORKER = 1;
var ENVIRONMENT_MAY_BE_NODE = 1;
var ENVIRONMENT_MAY_BE_SHELL = 1;
var ENVIRONMENT_MAY_BE_WEBVIEW = 1;
var ENVIRONMENT_MAY_BE_AUDIOWORKLET = 1;

// Whether to minify import and export names in the minify_wasm_js stage.
var MINIFY_WASM_IMPORTS_AND_EXPORTS = 0;
Expand Down
9 changes: 7 additions & 2 deletions src/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,20 @@ var ENVIRONMENT_IS_WORKER = {{{ ENVIRONMENT === 'worker' }}};
#endif
var ENVIRONMENT_IS_NODE = {{{ ENVIRONMENT === 'node' }}};
var ENVIRONMENT_IS_SHELL = {{{ ENVIRONMENT === 'shell' }}};
var ENVIRONMENT_IS_AUDIOWORKLET = {{{ ENVIRONMENT === 'worklet' }}};
#else // ENVIRONMENT
var ENVIRONMENT_IS_WEB = false;
var ENVIRONMENT_IS_WORKER = false;
var ENVIRONMENT_IS_NODE = false;
var ENVIRONMENT_IS_SHELL = false;
var ENVIRONMENT_IS_AUDIOWORKLET = false;
ENVIRONMENT_IS_WEB = typeof window === 'object';
ENVIRONMENT_IS_WORKER = typeof importScripts === 'function';
ENVIRONMENT_IS_AUDIOWORKLET = typeof AudioWorkletGlobalScope === 'function';
// N.b. Electron.js environment is simultaneously a NODE-environment, but
// also a web environment.
ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER;
ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER && !ENVIRONMENT_IS_AUDIOWORKLET;
#endif // ENVIRONMENT

#if ASSERTIONS
Expand Down Expand Up @@ -343,6 +346,8 @@ if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
}

setWindowTitle = function(title) { document.title = title };
} else if (ENVIRONMENT_IS_AUDIOWORKLET) {
// Nothing for worklets!
} else
#endif // ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER
{
Expand Down Expand Up @@ -406,7 +411,7 @@ assert(typeof Module['TOTAL_MEMORY'] === 'undefined', 'Module.TOTAL_MEMORY has b
{{{ makeRemovedFSAssert('NODEFS') }}}

#if USE_PTHREADS
assert(ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_NODE, 'Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)');
assert(ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_NODE || ENVIRONMENT_IS_AUDIOWORKLET, 'Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)');
#endif // USE_PTHREADS
#endif // ASSERTIONS

Expand Down
4 changes: 3 additions & 1 deletion src/shell_minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ var _scriptDir = (typeof document !== 'undefined' && document.currentScript) ? d

// MINIMAL_RUNTIME does not support --proxy-to-worker option, so Worker and Pthread environments
// coincide.
var ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_PTHREAD = typeof importScripts === 'function';
var ENVIRONMENT_IS_WORKER = typeof importScripts === 'function';
var ENVIRONMENT_IS_AUDIOWORKLET = typeof AudioWorkletGlobalScope === 'function';
var ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_AUDIOWORKLET;

var currentScriptUrl = typeof _scriptDir !== 'undefined' ? _scriptDir : ((typeof document !== 'undefined' && document.currentScript) ? document.currentScript.src : undefined);
#endif // USE_PTHREADS
Expand Down
Loading

0 comments on commit cadcdd9

Please sign in to comment.